{"owner":"vercel-labs","repo":"native","hasSkills":true,"totalSkillsCount":69,"totalTokensCount":67073,"categories":["anthropic-skill","root-instruction","plugin-manifest","cursor-rule"],"hasMcp":false,"mcpConfig":null,"found":["skill-data/automation/SKILL.md","skill-data/ts-services/SKILL.md","skill-data/zig/SKILL.md","skill-data/ts-core/SKILL.md","skill-data/native-ui/SKILL.md","AGENTS.md","skills/native-sdk/SKILL.md","evals/package.json","docs/AGENTS.md","evals/pnpm-lock.yaml","evals/README.md","skill-data/core/SKILL.md","evals/tsconfig.json","packages/native-sdk/package.json","packages/native-sdk/README.md","packages/core/package.json","packages/core/package-lock.json","packages/core/pnpm-lock.yaml","tools/guest-mac/agents.md","tools/guest-mac/README.md","tools/linux-truth/README.md","tools/windows-truth/README.md","evals/cases/ts-tag-input-core/eval.json","evals/cases/ts-habits-core/eval.json","evals/cases/ts-expenses-filter/eval.json","evals/cases/ts-countdown-bugfix/eval.json","evals/cases/templates-settings-app/eval.json","evals/cases/settings-picker/eval.json","evals/cases/release-dashboard/eval.json","evals/cases/process-monitor/eval.json","evals/cases/playlist-row-actions/eval.json","evals/cases/metrics-dashboard/eval.json","evals/cases/kanban-board/eval.json","evals/cases/inspector-window/eval.json","evals/cases/habits-tracker/eval.json","evals/cases/file-browser-panes/eval.json","evals/cases/expenses-table/eval.json","evals/cases/dual-sysinfo-panel/eval.json","evals/cases/dual-pomodoro-timer/eval.json","evals/cases/dual-notes-autosave/eval.json","evals/cases/dual-list-delete-fix/eval.json","evals/cases/dual-ledger-csv-split/eval.json","evals/cases/dual-feed-table/eval.json","evals/cases/disk-status-spans/eval.json","evals/cases/chat-composer/eval.json","evals/cases/brand-icon-package/eval.json","skill-data/ts-services/references/service-surface.md","skill-data/core/references/app-model-runtime.md","skill-data/core/references/bridge-security-native-capabilities.md","skill-data/core/references/frontend-assets.md","skill-data/core/references/project-anatomy.md","skill-data/core/references/web-engines-packaging-debugging.md","packages/native-sdk/npm/win32-x64/package.json","packages/native-sdk/npm/win32-x64/README.md","packages/native-sdk/npm/win32-arm64/package.json","packages/native-sdk/npm/win32-arm64/README.md","packages/native-sdk/npm/linux-x64-musl/package.json","packages/native-sdk/npm/linux-x64-musl/README.md","packages/native-sdk/npm/linux-x64-gnu/package.json","packages/native-sdk/npm/linux-x64-gnu/README.md","packages/native-sdk/npm/linux-arm64-musl/package.json","packages/native-sdk/npm/linux-arm64-musl/README.md","packages/native-sdk/npm/linux-arm64-gnu/package.json","packages/native-sdk/npm/linux-arm64-gnu/README.md","packages/native-sdk/npm/darwin-x64/package.json","packages/native-sdk/npm/darwin-x64/README.md","packages/native-sdk/npm/darwin-arm64/package.json","packages/native-sdk/npm/darwin-arm64/README.md",".cursorrules (knative-istio-typesense-gpu-cursorrules-prompt-fil.mdc)"],"skills":{"skill-data/automation/SKILL.md":"---\nname: automation\ndescription: Automation and verification guide for running Native SDK apps. Use when the user asks to test a running app, inspect runtime state, list windows, wait for readiness, drive widgets, take deterministic screenshots, send bridge commands, debug why automation is not connected, create smoke tests, or verify a Native SDK example in a GUI-capable session.\n---\n\n# Automate Native SDK apps\n\nEvery Native SDK app embeds an automation server — native-rendered apps and WebView-shell apps alike. It works through file-based IPC in `.zig-cache/native-sdk-automation/` and is intended for smoke tests, CI checks with a GUI session, and quick runtime inspection: accessibility snapshots, widget driving through the real input paths, deterministic reference-renderer screenshots, readiness/state assertions, and bridge round-trips.\n\nAutomation is not browser DOM automation. It reports runtime/window/widget state, drives retained-canvas widgets, and can ask the runtime to reload or dispatch bridge requests. For DOM testing of the optional WebView path, use the frontend framework's tests or a browser automation tool against the dev server.\n\n## What automation can verify\n\n- An automation-enabled app started and published `ready=true`.\n- The runtime loaded the expected app name, source kind, and window metadata.\n- The main window exists and is focused/open.\n- The JavaScript-to-Zig bridge can round-trip a request through `native automate bridge`.\n- Builtin window/WebView commands work when exercised by a smoke test.\n- Reload requests are accepted by the runtime.\n- Real pixels of retained-canvas (`gpu_surface`) views: `native automate screenshot <view-label>` renders the view's current canvas frame through the deterministic CPU reference renderer and writes a PNG artifact. Two captures of an unchanged scene are byte-identical, so screenshots can back golden-image or \"did the UI change\" checks.\n\n## What automation cannot verify\n\n- Screenshots of WebView content. `screenshot` covers `gpu_surface` canvas views only; there is no DOM/WebView pixel capture.\n- Arbitrary DOM queries and clicks.\n- Browser network assertions.\n\n## Prerequisites\n\nBuild/run an app with automation enabled. Generated examples usually expose `-Dautomation=true`:\n\n```bash\nzig build run -Dplatform=macos -Dautomation=true\n```\n\nRepository examples may have specialized steps:\n\n```bash\nzig build run-webview -Dplatform=macos -Dautomation=true\nzig build test-webview-smoke -Dplatform=macos\n```\n\nThe runner must pass an automation server into `RuntimeOptions`:\n\n```zig\nconst server = native_sdk.automation.Server.init(io, \".zig-cache/native-sdk-automation\", \"My App\");\nvar runtime = native_sdk.Runtime.init(.{\n    .platform = my_platform,\n    .automation = server,\n});\n```\n\nApps built without `-Dautomation=true` usually ignore automation files.\n\n## Commands\n\n```bash\nnative automate wait\nnative automate assert 'gpu_nonblank=true' 'role=button name=\"Reset\"'\nnative automate assert --absent 'error event='\nnative automate list\nnative automate snapshot\nnative automate reload\nnative automate screenshot inbox-canvas\nnative automate screenshot inbox-canvas 2\nnative automate widget-action canvas 2 press\nnative automate widget-click canvas 3\nnative automate widget-hold canvas 3\nnative automate widget-context-press canvas 3\nnative automate widget-drag canvas 4 0.25 0.82\nnative automate widget-wheel canvas 5 18\nnative automate widget-key canvas tab\nnative automate widget-key canvas cmd+c\nnative automate widget-pinch canvas 1.5\nnative automate widget-pinch canvas 0.5 120 80\nnative automate profile on\nnative automate profile off\nnative automate bridge '{\"id\":\"smoke\",\"command\":\"native.ping\",\"payload\":{\"source\":\"automation\"}}'\n```\n\nIf using the repository-built CLI:\n\n```bash\nzig-out/bin/native automate wait\nzig-out/bin/native automate snapshot\n```\n\n## Snapshot assertions (`automate assert`)\n\nPrefer `native automate assert` over `snapshot | grep` chains: it polls, so no sleeps, and its failure output carries the evidence (each missing pattern plus the snapshot tail).\n\n```bash\nnative automate assert 'gpu_nonblank=true' 'role=button name=\"Reset\"' 'count: 0'\nnative automate assert --timeout-ms 10000 '4 open'\nnative automate assert --absent 'error event=' 'dispatch_errors=[1-9]'\n```\n\nSemantics:\n\n- Every argument is a regex that must match somewhere in `snapshot.txt`. The command polls (100ms interval) until all match, then exits 0.\n- `--timeout-ms <n>` bounds the polling (default 30000). On timeout it prints `missing: <pattern>` for each unmatched pattern, the last 20 snapshot lines, and exits non-zero — CI-friendly, no wrapper script needed.\n- `--absent` inverts the whole invocation: every pattern must NOT match (poll until gone). Mix presence and absence by running two invocations.\n- Supported regex subset: literals, `.`, postfix `*` `+` `?`, line anchors `^`/`$`, classes `[a-z]`/`[^0-9]`, and `\\d \\w \\s` (with uppercase negations). No groups or alternation — pass multiple patterns instead.\n- Quote patterns in single quotes so the shell leaves `\"`, `$`, and `\\d` alone.\n\n## Standard workflow\n\n1. Start the app with automation enabled.\n2. Run `native automate wait` to block until `snapshot.txt` contains `ready=true`.\n3. Run `native automate assert '<pattern>' ...` for state checks, or `native automate snapshot` to eyeball app/window/source metadata.\n4. Run `native automate list` to inspect window summaries.\n5. Run `native automate bridge '...'` for bridge round-trip checks.\n6. Use `native automate widget-action <view-label> <widget-id> <action> [value]` to exercise retained canvas widget actions. `set_text` routes through the SAME input path real typing uses (focus, select-all, then a text-input event), so a TEA app's `on_input` mirror receives the edits and model state stays consistent with the on-screen field — it is not a presentation-only write.\n7. Use `native automate widget-click <view-label> <widget-id>` to exercise pointer-style retained widget routing. `widget-hold <view-label> <widget-id>` drives a press-and-hold through the same path — pointer down, the reserved hold timer fired, then the suppressed release — so `on_hold` Msgs are live-drivable (a target without `on_hold` degrades to the click a real long press is). `widget-context-press <view-label> <widget-id>` is the secondary click: it presents the widget's context menu, or dispatches `on_hold` immediately when the route declares none.\n8. Use `native automate widget-drag <view-label> <widget-id> <start-x-ratio> <end-x-ratio> [start-y-ratio end-y-ratio]` for continuous pointer controls.\n9. Use `native automate widget-wheel <view-label> <widget-id> <delta-y>` for retained widget scroll input. Wheel targets must be interactive/scrollable widgets — a plain layout column or text node is not a wheel target; aim at the scroll/list widget id from the snapshot. Failures land in the snapshot as named reasons: `error event=automation.widget_wheel name=WheelTargetUnknown|WheelTargetNotInteractive|WheelTargetHasEmptyBounds detail=\"<command args>\"`.\n10. Use `native automate widget-key <view-label> <key> [text]` for focused retained widget keyboard input. The key accepts modifier chords — `cmd+a`, `cmd+c`, `cmd+v`, `cmd+x`, `ctrl+shift+arrowleft` (`cmd` sets the primary shortcut modifier on every platform) — so select-all/copy/cut/paste and shift-extended selection are drivable; after a copy, widget lines in the snapshot show the live selection as `selection=a..b`, and the copied text lands on the real system clipboard (`pbpaste` on macOS).\n11. Use `native automate widget-pinch <view-label> <scale> [x y]` for trackpad pinch gestures against a gpu-surface view: the runtime dispatches the real `pinch_begin`/`pinch_change`/`pinch_end` platform events, with one change carrying `scale - 1`. `<scale>` is the FINAL multiplicative zoom for the gesture — the cumulative gesture scale (the product of `1 + delta`) lands exactly on it — `1.5` zooms in 50%, `0.5` zooms out to half. The optional anchor point is view-local points, defaulting to the view center. Apps hear it through the pinch channel (`Options.on_pinch` / the TS core's `pinchMsg`).\n12. Use `native automate screenshot <view-label> [scale]` to capture the named `gpu_surface` view's canvas as `screenshot-<view-label>.png` (the CLI prints the artifact path and waits for the file).\n13. Before driving an app-menu command, inspect the snapshot's `command id=\"...\"` catalog and `app-menu` / `app-menu-item` rows to prove the running app loaded the expected `app.zon` or runner declarations. Then use `native automate menu-command <id>` to dispatch the same `.menu_command` platform event a real selection emits; the verb remains a raw event injector, so the snapshot receipt is what validates registration.\n14. Use `native automate tray-action <item-id>` for the primary status item, or `native automate tray-action <status-item-id> <item-id>` for an explicit item. Both select a dropdown row through the same platform event a real menu-bar click emits (command dispatch with source `.tray`). Live items appear in `snapshot.txt` as `tray #status-id title=\"...\" visible=... items=N` followed by `  tray-item #item-id ...` rows — the menu bar is outside every window capture, so this is the automation evidence for every model-driven item. Unknown id pairs degrade into the dispatch-error ring as `automation.tray_action`.\n15. Use `native automate reload` to request a WebView reload.\n16. Use `native automate profile on` to enable per-stage frame timing: while on, `snapshot.txt` carries a `frame_profile` line with rolling p50/p90/max microseconds per pipeline stage (`rebuild`, `layout`, `reconcile`, `emit`, `a11y`, `plan`, `patch`, `encode`, `present`, `host_decode`, `host_draw`), each with a lifetime sample count (`<stage>_n=`). Drive some interactions, then `native automate snapshot | grep -o 'frame_profile.*'` to read where frame time goes; `profile off` stops recording and drops the line. Turning it on starts a fresh sample window.\n\n## Screenshots\n\n`screenshot <view-label> [scale]` asks the runtime to rasterize the view's\ncurrent retained canvas frame through the deterministic CPU reference\nrenderer — the same pixel path the Linux software presentation uses — and\npublish it as an uncompressed PNG at\n`.zig-cache/native-sdk-automation/screenshot-<view-label>.png`. The file is\nwritten atomically (temp file + rename), so its presence means the PNG is\ncomplete.\n\nDeterminism semantics:\n\n- Screenshots render at scale 1 by default regardless of the display's\n  backing scale, so an unchanged scene produces byte-identical PNGs from\n  capture to capture on the same machine. Pass an explicit scale (for\n  example `2`) for high-DPI pixel dimensions.\n- Screenshots use the live retained scene, including live design tokens and\n  platform text measurement (CoreText on macOS): the layout matches what is\n  on screen. Glyphs are rasterized by the reference renderer from the\n  bundled faces, not by the platform's font rasterizer, so screenshots are a\n  deterministic layout/structure/color signal rather than a platform\n  font-rendering signal.\n- Cross-machine byte-identity is only guaranteed where text metrics are\n  deterministic (the null platform's estimator). On platforms with a native\n  text measurement provider, text widths can differ between OS versions, so\n  compare screenshots taken on the same machine or assert on properties\n  (dimensions, changed/unchanged) rather than exact bytes across machines.\n- OS-level captures (`screencapture -x` on macOS) are NOT a substitute: in a\n  shell without Screen Recording permission they exit 0 and silently return\n  wallpaper-only images with no app window in them. If you shell out for a\n  real-pixel capture, verify the image is not blank/wallpaper before trusting\n  it; `automate screenshot` plus a semantics snapshot is the reliable pair.\n\n## Bridge smoke test pattern\n\nThe request must be JSON with an ID, command, and payload:\n\n```bash\nnative automate bridge '{\"id\":\"smoke\",\"command\":\"native.ping\",\"payload\":{\"source\":\"automation\"}}'\n```\n\nAutomation sends the request with origin `zero://inline`. The app's bridge policy must allow that origin or the call will reject with `permission_denied`. For packaged asset origins, app code often allows `zero://app`; for automation smoke tests, add `zero://inline` only when the test needs it.\n\nExpected response shape depends on the handler. A typical `native.ping` handler returns:\n\n```json\n{\"id\":\"smoke\",\"ok\":true,\"result\":{\"message\":\"pong\",\"count\":1}}\n```\n\nIf the command fails, inspect the bridge error code:\n\n- `unknown_command`: no handler registered or wrong command name.\n- `permission_denied`: origin or permission policy blocked it.\n- `handler_failed`: Zig handler returned an error or invalid JSON.\n- `payload_too_large`: request exceeded bridge limits.\n\n## File protocol\n\nThe default directory is `.zig-cache/native-sdk-automation/`, resolved against the CLI's CURRENT WORKING DIRECTORY — run `native automate` from the app project's directory (where the app was launched). The dir is created by the running app, never by the CLI: a command sent from the wrong cwd fails loudly (`error: no automation dir at <abs path>`) instead of queueing into a dir no app reads, and every queued command prints the absolute dir it wrote to — check that line when a command seems to do nothing.\n\nFiles:\n\n- `snapshot.txt`: app name, readiness, source kind, source size, window metadata, accessibility summary. The `ready=true` line also carries `protocol=<n>` (the CLI/app handshake: the CLI refuses snapshots — and command queues to a live app — whose protocol version is not its own, naming both versions; the fix is rebuilding whichever binary is stale and comparing `native version`), `dispatch_errors=<total>` and `dropped_trace_records=<total>`, and recent degraded handler/update errors appear as `  error event=<tag> name=<ErrorName> timestamp_ns=...` lines — a handler error no longer exits the app, so grep these to notice one happened. While `profile on` is active, a `frame_profile <stage>_p50_us=... <stage>_p90_us=... <stage>_max_us=... <stage>_n=...` line follows the header with per-stage frame timing. The header also carries `markup_watch=armed|off` — whether the markup hot-reload watch is armed (armed only in builds where the app wired `.markup` with a `watch_path` and `io`, or registered compiled fragments through `fragment_watch` — i.e. Debug dev builds).\n- `windows.txt`: window list.\n- `command.txt`: command input written by CLI and consumed by runtime. The slot is single-entry and the app consumes one command per presented frame: `native automate <command>` WAITS for the running app to consume it and prints `delivered <action> -> <dir>` on success — it refuses/fails loudly instead of silently overwriting an unconsumed command, and exits non-zero if the app never consumes it.\n- `bridge-response.txt`: last bridge response.\n- `screenshot-<view-label>.png`: deterministic reference-rendered PNG of a `gpu_surface` view, written by the `screenshot` command.\n\nThe runtime polls `command.txt`. After processing a command, it writes `done`.\n\n## Debugging automation failures\n\nIf `native automate wait` times out with NO snapshot file at all, it prints a teaching error naming the automation dir it watched and pointing at `-Dautomation=true` and the working directory — start from that message. Otherwise:\n\n1. Confirm the app is still running.\n2. Confirm it was built with `-Dautomation=true`.\n3. Confirm the runner passes `automation` into `Runtime.init`.\n4. Check `.zig-cache/native-sdk-automation/snapshot.txt`.\n5. Delete stale files in `.zig-cache/native-sdk-automation/` and restart the app.\n6. Run with more tracing, for example `zig build run -Dtrace=all`.\n\nIf the CLI reports an automation protocol mismatch (or a snapshot with no protocol version): the `native` binary and the app were built from different framework versions — rebuild the older side (stale `zig-out/bin` copies of the CLI are the classic cause; `native version` prints the commit the binary was built from).\n\nStale INSTANCES are called out too: automate verbs print a LOUD warning when the publishing app's process started BEFORE the newest binary in `zig-out/bin` was built — a leftover instance from an earlier run impersonating the new build. Kill it and relaunch the fresh binary before trusting any snapshot.\n\nIf `snapshot` says no app connected:\n\n- The automation directory may not exist yet.\n- The app may be running from a different working directory.\n- The app may be built without automation.\n- The app may not have reached runtime startup.\n\nIf bridge automation fails:\n\n- Check command name spelling.\n- Check app handler registration.\n- Check bridge policy origins for `zero://inline`.\n- Check runtime permissions.\n- Check that the handler returns valid JSON.\n\n## CI and smoke tests\n\nUse automation for minimal integration confidence:\n\n```bash\nzig build test-webview-smoke -Dplatform=macos\n```\n\nA good smoke test:\n\n1. Builds an example with `-Dautomation=true` and `-Djs-bridge=true`.\n2. Starts the app in a GUI-capable session.\n3. Waits for readiness.\n4. Verifies snapshot metadata (`automate assert` with the patterns that matter).\n5. Sends `native.ping`.\n6. Exercises builtin windows/WebViews if the app enables them.\n7. Fails on timeout or unexpected bridge response.\n\nApps scaffolded with `native init --full` ship this as `.github/workflows/ci.yml` (the zero-config default scaffold skips CI): a null-platform `zig build test` job plus a Linux Xvfb smoke job that launches the binary, runs `automate wait`, asserts on the snapshot with `automate assert`, and checks a non-empty `automate screenshot` artifact. Extend that file rather than writing grep chains by hand.\n\nDo not use automation for exhaustive UI testing. It is a runtime and bridge smoke layer.\n\n## Notes\n\n- Automation is compile-time gated: apps built without `-Dautomation=true` ignore automation files.\n- Screenshots cover retained-canvas (`gpu_surface`) views only; WebView pixels are not captured.\n- WebView DOM interaction is intentionally out of scope for this file-based automation layer.\n- Use `native skills get core --full` for app architecture, bridge policy, packaging, and debugging context.\n","skill-data/ts-services/SKILL.md":"---\nname: ts-services\ndescription: Authoring guide for Native SDK TypeScript services under src/services: ordinary scriptc static-tier TypeScript reached through generated typed Cmd clients, shared record shapes, hermetic vendored npm, streaming, deadlines, cancellation, devhost simulation, authority, replay, and the two carriers (in-process thread pool, child subprocess). Use when adding or modifying src/services modules, moving Node/JSON/regex/Map/Date/class work out of a core, or fixing NS1065-NS1067.\n---\n\n# Author TypeScript services behind the effect boundary\n\nUse `src/services/**/*.ts` for imperative work that needs ordinary TypeScript beyond the deterministic core subset: `fs`, `path`, `process`, `os`, `child_process`, `fetch`, regexes, JSON, `Map`/`Set`, `Date`, and classes supported by the pinned scriptc static tier. Both classes compile with the exact scriptc version in `packages/core/package.json`; neither enables `--dynamic` or ships a JavaScript engine. The exact verdict for every surface the compiler currently projects — with each status and refusal code, plus the manifest's explicit coverage limits — is `references/service-surface.md`, generated from the pinned compiler's own manifest; consult it rather than guessing compiler capability.\n\nThe class line is hard:\n\n- `src/core.ts` and its imports outside `src/services/` retain NS1001–NS1064 and `deterministic: true`.\n- The core never imports a service file, even type-only (NS1065). It imports generated command constructors from `@native-sdk/services`.\n- A service may import shared, subset-legal declarations from the core class. Put every boundary record, enum, or union there so one declaration is authoritative on both sides.\n- A service may import compiler-supported Node built-ins and exact packages declared by app.zon. Undeclared bare imports teach NS1066.\n\n## Typed operation surface\n\nEvery directly exported, non-default named function is an operation named `<module-basename>.<export>`. It must be synchronous, have a body, take zero or one explicitly annotated request, and explicitly return a contract-encodable result. Boundary data may use booleans, numbers, integer-class numbers, bytes, optionals, readonly slices, and named records/enums/kind-unions. Functions and behavior-bearing classes do not cross.\n\n```ts\n// src/shared.ts — core-class file, imported by both classes\nexport type ParseRequest = {\n  readonly source: Uint8Array;\n  readonly caseSensitive: boolean;\n};\n\nexport type ParseResult = {\n  readonly bytes: Uint8Array;\n  readonly matched: boolean;\n};\n\n// src/services/feeds.ts\nimport type { ParseRequest, ParseResult } from \"../shared.ts\";\n\nexport function parse(request: ParseRequest): ParseResult {\n  const text = new TextDecoder().decode(request.source);\n  const matched = request.caseSensitive ? /feed/.test(text) : /feed/i.test(text);\n  return { bytes: new TextEncoder().encode(JSON.stringify({ matched })), matched };\n}\n```\n\n`services.contract.json` carries the complete type table, operation names, exact compiler pin, source hashes, package facts, deadlines, streaming declarations, and `deterministic: false`. Generated host codecs, the Zig registry/result decoders, and the typed client consume that sidecar; none re-read source to infer a second truth.\n\nExplicit boundary throws must be exactly inline `{ kind, message }` shapes with a string-valued message and must escape the operation. They arrive as UTF-8 JSON bytes on the error arm. Do not throw `new Error(...)` from the exported surface.\n\n## Call through the generated client\n\nImport from `@native-sdk/services`; `native check` derives this virtual module, and build/dev stage the same projection without writing `src/services.gen.ts` into the author tree. The editor copy lives under ignored `node_modules/@native-sdk/services`.\n\n```ts\nimport { feedsParse } from \"@native-sdk/services\";\nimport type { ParseRequest, ParseResult } from \"./shared.ts\";\n\nexport type Msg =\n  | { readonly kind: \"parse\"; readonly request: ParseRequest }\n  | { readonly kind: \"parsed\"; readonly result: ParseResult }\n  | { readonly kind: \"parse_failed\"; readonly error: Uint8Array };\n\ncase \"parse\":\n  return [model, feedsParse(msg.request, {\n    key: \"feed-parse\",\n    ok: \"parsed\",\n    err: \"parse_failed\",\n  })];\n```\n\nThe generated route type proves that `ok` names the one Msg arm carrying exactly the declared result and `err` names a one-bytes-field arm. Raw `Cmd.request(name, bytes, route)` remains available as the low-level byte seam.\n\nKeys share the engine effect-key space. A duplicate live service key rejects with `rejected` through `err`. Same-key requests run strictly FIFO on both carriers; the in-process carrier runs different keys in parallel across its pool instances. Terminal results and stream events are journaled; replay feeds the recorded events without starting a child process or a pool thread.\n\n## Hermetic npm\n\nResolve packages only with an explicit author action:\n\n```bash\nnative vendor . escape-string-regexp@5.0.0\n```\n\nThe command installs with lifecycle scripts disabled in a temporary directory, copies the flattened package graph into `src/services/vendor/`, retains package/license files, computes canonical tree hashes, and rewrites app.zon's `service_packages` with exact names, `X.Y.Z` versions, and hashes. Check those bytes and manifest facts into source control.\n\nBuilds never run npm or use the network. They verify package identity and every vendored byte, stage only declared packages, and invoke scriptc with `--npm-static <explicit-list>`. `auto`, dynamic-island fallback, and `--dynamic` are refused. Anything below 100% static coverage fails: `native check` preserves the coverage note verbatim and names the three options—choose another exact package, port/vendor a suitable implementation, or wait for compiler support. The checked-in package spike (`tests/ts-services/npm-static-spike.json`) passed four of five deliberately small candidates and refused `micromark`; npm support is selective.\n\n## Streaming, cancellation, and deadlines\n\nA last `emit` capability declares a stream. Each typed chunk becomes canonical bytes on the existing external channel; the operation still returns one typed terminal result.\n\n```ts\nimport type { ServiceCancellation } from \"@native-sdk/core\";\n\nexport type ParseChunk = { readonly bytes: Uint8Array; readonly index: number };\n\n/**\n * @deadlineMs 5000\n * @streamBuffer 8\n */\nexport function parseLarge(\n  request: ParseRequest,\n  emit: (chunk: ParseChunk) => void,\n  cancellation: ServiceCancellation,\n): ParseResult {\n  for (let index = 0; index < request.source.length; index += 4096) {\n    cancellation.throwIfCancelled();\n    emit({ bytes: request.source.slice(index, index + 4096), index });\n  }\n  return parse(request);\n}\n```\n\nImport `ServiceCancellation` as a type from `@native-sdk/core`; it is legal only as the final capability parameter. The generated client route additionally requires `channelKey` and a channel-event Msg arm. `@streamBuffer` is a 1–64 in-flight contract cap (default 8); `@deadlineMs` is a 1–86400000 operation deadline. A terminal result closes the channel after its interim frames. `Cmd.cancel(key)` flips the token, closes a streaming channel, sends `cancelled` through `err`, and drops every later chunk. Deadline expiry flips the same token and sends JSON with `kind: \"timeout\"`. Poll `cancelled()` or call `throwIfCancelled()` at bounded intervals; a short cooperative grace period follows the token, and an operation that unwinds inside it keeps its instance (child process or pool instance) warm. Ignoring the token has a per-carrier hard edge: the child is killed and the next request starts a clean host; the in-process carrier abandons that instance's thread, routes the failure, and adds a fresh instance to the pool. The abandoned dispatch keeps its key reserved until it physically stops, so a same-key replacement cannot overlap its side effects. A detected runtime trap routes `kind: \"service_trap\"` and poisons only the instance it fired in.\n\n## Dev loop, authority, and current limits\n\n`native dev --core` imports service modules in an isolated Node worker, verifies the same package hashes, decodes the same request contract, cooperatively interrupts cancellation/deadline work, maps errors through the same arms, and emits stream chunks through the same channel-event shape. Use `--script scenario.ndjson --watch` for repeatable logic iterations. The devhost reads `NATIVE_SDK_SESSION_RECORD`/`NATIVE_SDK_SESSION_REPLAY`, writes the native journal format, and starts no service worker during replay. Service-only recordings cross between it and the packaged runtime; packaged recordings containing other effect families must be replayed with `native automate replay`, and devhost rejects those records explicitly. `native dev` runs the compiled service executable beside the real app.\n\nCarrier selection is a build fact: `.service_carrier = \"in_process\" | \"child\"` in app.zon (or `-Dservice-carrier`). Unset/`\"auto\"` preserves the child carrier on desktop and resolves to the in-process pool on iOS/Android (mobile apps cannot spawn a sibling process; an explicit `\"child\"` there is refused with a teaching). The in-process archive is available for native Linux, cross-Linux x86_64/aarch64 (`aarch64-linux-android` included), native Windows x86_64, cross-Windows x86_64 GNU, and the Mach-O targets — macOS, `aarch64-ios`, `aarch64-ios-simulator` — built on macOS. `.service_pool_size` (or `-Dservice-pool-size`, 1–16) sets the in-process pool width; the default is min(4, cores).\n\nAuthority differs by carrier. The child process runs with the app's privileges and app-data cwd, receives an explicit environment allowlist (path, user/home/temp, locale/time zone, certificate, and proxy variables; platform equivalents on Windows) with `NATIVE_SDK_*` internals stripped, and its stdout belongs to framed transport (diagnostics go to stderr). In-process services share the app process outright: the app's full environment and working directory, no transport-owned stdout — and a hardware fault (stack overflow) or `process.exit()` in service code takes the app with it, so the child carrier remains the fully isolated option. Each pool worker owns a separate service-module instance, so mutable module globals are worker-local and requests on different keys may observe different copies; keep shared durable state outside module globals. Either way this authority is why service work never runs synchronously inside `update`.\n\nCurrent limits:\n\n- child executables are desktop-only and use the broad compiler matrix (same-platform builds, Linux/Windows GNU cross-targets from desktop hosts, and macOS targets from macOS), while in-process archives follow the localization matrix above (mobile targets are archive-only: iOS from a macOS host at the 15.0 floor, Android from any desktop host with an NDK at the API 26 floor); Windows MSVC builds natively on a matching Windows host, while cross-Windows uses GNU because Zig supplies that target's CRT and system libraries; any explicitly spelled Linux `-gnu` target — even one matching the build host — must also spell its glibc (`x86_64-linux-gnu.2.36` or later, or `-musl`) because Zig otherwise selects the runtime's too-old default floor;\n- synchronous static-tier operations only—async services wait for the next compiler/runtime phase;\n- no JavaScript engine or dynamic npm fallback;\n- storage remains engine-owned effects, not a service-layer database shortcut;\n- custom widgets, renderers, platform integration, and new engine capabilities remain Zig toolkit extensions.\n","skill-data/zig/SKILL.md":"---\nname: zig\ndescription: Zig 0.16 idioms for Native SDK code, indexed by compile error. Load when `zig build` fails on std APIs with errors like \"struct 'fs' has no member named 'cwd'\", \"struct 'array_list.Aligned(u8,null)' has no member named 'init'\", \"struct 'std' has no member named 'io'\", \"no member named 'GeneralPurposeAllocator'\", \"no member named 'getEnvMap'\", or \"invalid format string\" - the signature of code written for Zig 0.15 or earlier. Covers main(std.process.Init), std.Io file IO and writers, ArrayList, process spawning, environment, clocks and sleep, sockets, custom formatting, and build.zig module shapes, each as this SDK writes them.\n---\n\n# Zig 0.16 for Native SDK code\n\nThe Native SDK requires Zig 0.16.0 (`minimum_zig_version` in `build.zig.zon`; the CLI pins the same version and offers a checksum-verified download into `~/.native/toolchains/` when the `zig` on PATH does not match). Training data and older guides teach Zig 0.15 idioms, and 0.16 moved everything that touches the outside world — files, stdout, clocks, sleeping, process spawning, sockets — behind an explicit `std.Io` value, while containers became allocator-per-call. Each section below is headed by the exact compile error the old idiom produces, so search this file by error text.\n\nTwo rules resolve most failures:\n\n- Operations on the outside world take a `std.Io` first (or right after the receiver). Get one from `init.io` in `main(init: std.process.Init)`, from `std.testing.io` in tests, or from `std.Io.Threaded` in code with no `Init` to thread through.\n- Containers are unmanaged: initialize with `.empty` and pass the allocator to every mutating call.\n\nIn a UiApp, `update` never sees an `Io` — persistence, record and relational storage, secure credentials, subprocesses, HTTP, clocks, and timers go through the typed effects channel (`fx.persist`, `fx.storeSet`/`storeGet`/`storeDelete`/`storeScan`/`storeSetMany`, `fx.dbQuery`/`dbExec`, `fx.credentialsSet`/`credentialsGet`/`credentialsDelete`, `fx.readFile`/`appendFile`/`statFile`/`deleteFile`/streaming file verbs, `fx.spawn`, `fx.fetch`, `fx.wallMs`, `fx.startTimer`; see `native skills get native-ui`). Whole-file IO stays capped at 1 MiB; streamed reads deliver 256-KiB chunks, and streamed writes finalize atomically on close. External raw paths require the `filesystem` permission, while the app's six resolved directories are exempt after symlink-safe canonicalization. Raw `std.Io` belongs in `main`, tests, and standalone tools. Record-store apps declare `\"store\"`; relational apps declare `\"sqlite\"`; credentials require both the `\"credentials\"` capability and permission. Credential Msg arms carry `EffectCredentialsResult`; consume successful get bytes immediately and never retain them in Model. The engine owns database paths and app-scoped credential namespaces, while every result still returns as a Msg and crosses the replay journal (credential bytes are redacted). Use `TestHarness().createWithRelationalStore` for a hermetic in-memory relational database and `TestHarness().createWithCredentials` for a hermetic in-memory credential store. And because Zig analyzes lazily, a 0.15-ism can hide in code only one build path references: run BOTH `zig build` and `zig build test` before calling a change done.\n\n## error: struct 'heap' has no member named 'GeneralPurposeAllocator' — allocators come from `main(init: std.process.Init)`\n\nZig 0.15 and earlier:\n\n```zig\npub fn main() !void {\n    var gpa = std.heap.GeneralPurposeAllocator(.{}){};\n    defer _ = gpa.deinit();\n    const allocator = gpa.allocator();\n}\n```\n\nZig 0.16: `main` takes `std.process.Init`, which carries the process-wide allocators, the `Io`, the environment, and the args — nothing to construct or deinit:\n\n```zig\npub fn main(init: std.process.Init) !void {\n    const gpa = init.gpa;                       // general purpose, leak-checked in Debug\n    const arena = init.arena.allocator();       // process-lifetime arena, freed on exit\n    const io = init.io;                         // the Io every std call below wants\n    const args = try init.minimal.args.toSlice(arena);\n    if (init.environ_map.get(\"HOME\")) |home| { ... }\n}\n```\n\nThe CLI's own entry point is the live reference: `tools/native-sdk/main.zig`. Generated **Zig-core** apps have this shape — `main(init: std.process.Init)` passes `init` through to `runner.runWithOptions(app, options, init)` and `init.io` to the markup hot-reload watcher (see the older Zig-core examples' `src/main.zig`). Default TypeScript apps have no authored Zig entry point. `std.heap.page_allocator` still exists for allocations that live for the whole process. Library code that cannot take an `Init` builds its own `Io`: `var threaded = std.Io.Threaded.init(allocator, .{});` then `threaded.io()` (`src/platform/macos/root.zig`).\n\n## error: struct 'fs' has no member named 'cwd' — file IO moved to `std.Io.Dir`\n\n`std.fs.cwd()`, `std.fs.File`, and `std.fs.selfExePath` are gone (`std.fs` retains only `path` helpers and deprecated aliases). The directory handle is `std.Io.Dir`, and every operation takes `io` right after the receiver:\n\n```zig\nconst cwd = std.Io.Dir.cwd();\nconst content = try cwd.readFileAlloc(io, \"app.zon\", allocator, .limited(1024 * 1024));\ndefer allocator.free(content);\ntry cwd.writeFile(io, .{ .sub_path = \"out.txt\", .data = bytes });\nvar file = try cwd.openFile(io, path, .{});\ndefer file.close(io);\ntry cwd.deleteTree(io, \"zig-out/tmp\");\n```\n\nNote the `readFileAlloc` argument order (`io`, path, allocator, limit) and the size limit as `std.Io.Limit` — `.limited(n)` or `.unlimited`. Live references: `tools/native-sdk/skills.zig` (readFileAlloc), `src/tooling/manifest.zig` (openFile), `src/tooling/cef.zig` tests (writeFile with `std.testing.io`). Path buffers size with `std.Io.Dir.max_path_bytes`. For the executable's own path, `std.fs.selfExePath(&buf)` became `std.process.executablePath(io, &buf)`, returning the length (`tools/native-sdk/skills.zig`).\n\n## error: struct 'std' has no member named 'io' — writers are `std.Io.Writer`, stdout is `std.Io.File.stdout()`\n\n`std.io.getStdOut().writer()` and `std.io.fixedBufferStream` are gone. Stdout takes `io` plus a caller-owned buffer, and the printable interface lives one field deep:\n\n```zig\nvar stdout_buffer: [4096]u8 = undefined;\nvar stdout_writer = std.Io.File.stdout().writer(io, &stdout_buffer);\nconst stdout = &stdout_writer.interface;\ndefer stdout.flush() catch {};\ntry stdout.print(\"{s}\\n\", .{message});\n```\n\nForgetting `flush()` means no output — the buffer is yours. `std.debug.print` is unchanged and needs no `io`; prefer it for diagnostics. The `fixedBufferStream` replacement is a fixed writer over a stack buffer, and the written bytes come back from `buffered()`:\n\n```zig\nvar buf: [256]u8 = undefined;\nvar writer = std.Io.Writer.fixed(&buf);\ntry writer.print(\"n={d}\", .{42});\nconst written = writer.buffered();\n```\n\nFor building a string of unknown size, use an allocating writer instead of an ArrayList writer: `var w = std.Io.Writer.Allocating.init(allocator); defer w.deinit();` then print through `w.writer` (`src/automation/server.zig`). One-shot formatting still has `std.fmt.bufPrint` and `std.fmt.allocPrint`, unchanged. Functions that accept a writer take `*std.Io.Writer`, not `anytype` (`tools/native-sdk/skills.zig`; fixed-writer reference: `src/tooling/doctor.zig`).\n\n## error: struct 'array_list.Aligned(u8,null)' has no member named 'init' — ArrayList is unmanaged\n\n`std.ArrayList(T).init(allocator)` is gone; the list no longer stores its allocator. Initialize with `.empty` and pass the allocator to every call that can allocate or free:\n\n```zig\nvar list: std.ArrayList(u8) = .empty;\ndefer list.deinit(allocator);\ntry list.append(allocator, 'a');\ntry list.appendSlice(allocator, \"bc\");\nconst owned = try list.toOwnedSlice(allocator);\n```\n\nThis is everywhere in the SDK — `src/tooling/templates.zig` builds every generated file this way, `tools/native-sdk/skills.zig` collects skills this way. Passing a managed-style call (`list.append('a')`) fails with \"member function expected 2 argument(s), found 1\". `std.StringHashMap`/`std.AutoHashMap` still have managed `.init(allocator)` forms; only explicitly `Unmanaged` maps take `.empty` (`tools/native-sdk/markup_lsp.zig`).\n\n## error: invalid format string 's' for type — enums print with `{t}`, format methods with `{f}`\n\n`{s}` is for strings only. Enums and tagged unions print their tag name with `{t}` (equivalent to `@tagName`, which also still works):\n\n```zig\nstd.debug.print(\"native {t}: `zig build` step failed (exit code {d})\\n\", .{ verb, code });\n```\n\n(`src/tooling/verbs.zig`.) Custom format methods changed shape twice over: the old four-parameter signature no longer compiles (`error: struct 'fmt' has no member named 'FormatOptions'`), and `{}` no longer calls a `format` method at all — it prints the default field dump silently. Declare the 0.16 signature and print with `{f}`:\n\n```zig\npub fn format(self: Point, writer: *std.Io.Writer) std.Io.Writer.Error!void {\n    try writer.print(\"({d},{d})\", .{ self.x, self.y });\n}\n// call site\ntry writer.print(\"{f}\\n\", .{point});\n```\n\n## error: struct 'time' has no member named 'sleep' / 'milliTimestamp' — clocks live on Io\n\n`std.time` keeps only the unit constants (`ns_per_ms`, `ms_per_s`, ...) and epoch helpers. Sleeping and reading clocks take `io` and typed durations:\n\n```zig\ntry std.Io.sleep(io, std.Io.Duration.fromMilliseconds(50), .awake);\nconst now_ns: i128 = std.Io.Timestamp.now(io, .real).nanoseconds;\n```\n\n(`src/runtime/automation_liveness_tests.zig`, `src/automation/server.zig`.) In app code, do not reach for either: `native_sdk.nowMs()` / `native_sdk.monotonicMs()` are the facade, `fx.wallMs()` is the journaled read inside `update`, and `model.clock` is the testable seam — the native-ui skill's Time section owns that pattern.\n\n## error: struct 'process.Child' has no member named 'init' — spawning is `std.process.spawn(io, ...)`\n\nThe two-step `Child.init` + `child.spawn()` collapsed into one call, and lifecycle methods take `io`:\n\n```zig\nvar child = try std.process.spawn(io, .{\n    .argv = &.{ \"npm\", \"run\", \"build\" },\n    .stdin = .ignore,\n    .stdout = .inherit,\n    .stderr = .inherit,\n});\nconst term = try child.wait(io);   // child.kill(io) to stop it\n```\n\n(`src/tooling/verbs.zig`, `src/tooling/dev.zig` — the latter also shows passing a custom environment via `.environ_map`.) Inside a UiApp, spawn through the effects channel (`fx.spawn`) instead — it is bounded, journaled, and delivers exit/output as Msgs.\n\n## error: struct 'process' has no member named 'getEnvMap' / 'argsAlloc' — environment and args come from Init\n\n`std.process.getEnvMap`, `argsAlloc`, and `argsWithAllocator` are gone. `main` already has both: `init.environ_map` (a `*std.process.Environ.Map`) and `init.minimal.args.toSlice(allocator)` (`tools/native-sdk/main.zig`). Building an environment from scratch — for a child process, or in tests — is `var env = std.process.Environ.Map.init(allocator); defer env.deinit(); try env.put(\"KEY\", \"value\");` (`src/tooling/dev.zig`).\n\n## error: struct 'mem' has no member named 'trimRight' / 'trimLeft' — renamed `trimEnd` / `trimStart`\n\nThe directional trims renamed to match the JS/string convention; arguments are unchanged:\n\n```zig\nconst line = std.mem.trimEnd(u8, raw, \"\\r\\n\");     // was trimRight\nconst body = std.mem.trimStart(u8, line, \" \\t\");   // was trimLeft\nconst both = std.mem.trim(u8, text, \" \");          // unchanged\n```\n\n## error: struct 'std' has no member named 'net' — sockets live on `std.Io.net`\n\n`std.net.Address` became `std.Io.net.IpAddress`, and connect/listen take `io`: `std.Io.net.IpAddress.resolve(io, host, port)`, then `IpAddress.connect(&address, io, .{ .mode = .stream, .protocol = .tcp })`; stream readers/writers follow the buffered-writer shape (`std.Io.net.Stream.writer(stream, io, &buffer)` then `.interface`). Live reference: `src/tooling/dev.zig` (`waitUntilReady`/`httpReady`). App-level HTTP belongs in `fx.fetch`, not hand-rolled sockets.\n\n## error: no field named 'root_source_file' in struct 'Build.ExecutableOptions' — build.zig artifacts take modules\n\nZero-config apps have no `build.zig` — the CLI owns the build graph, so this only appears in apps that ejected or scaffolded `--full`. Artifacts take a module, and the module owns root source, target, and optimize:\n\n```zig\nconst exe = b.addExecutable(.{\n    .name = \"app\",\n    .root_module = b.createModule(.{\n        .root_source_file = b.path(\"src/main.zig\"),\n        .target = target,\n        .optimize = optimize,\n    }),\n});\nconst tests = b.addTest(.{ .root_module = app_mod });\n```\n\nThe generated ejected `build.zig` is the reference shape (`native eject`, emitted by `src/tooling/templates.zig`); the SDK's own `build.zig` uses the same pattern throughout.\n\n## Reading files and streams incrementally\n\n`file.reader()` with no arguments is gone. A reader takes `io` and a buffer, and the stream interface lives on `.interface`:\n\n```zig\nvar read_buffer: [4096]u8 = undefined;\nvar reader = file.reader(io, &read_buffer);\nconst bytes = try reader.interface.allocRemaining(allocator, .limited(1024 * 1024));\nconst line = try reader.interface.takeDelimiterExclusive('\\n');\n```\n\n(`src/tooling/manifest.zig` for allocRemaining, `src/tooling/toolchain.zig` for line reading from stdin.) For whole-file reads, prefer `readFileAlloc` above.\n\n## Tests: the Io is `std.testing.io`\n\nTests never construct an `Io` — `std.testing.io` is the canonical one, next to `std.testing.allocator`:\n\n```zig\ntest \"reads the manifest\" {\n    var tmp = std.testing.tmpDir(.{});\n    defer tmp.cleanup();\n    try tmp.dir.writeFile(std.testing.io, .{ .sub_path = \"app.zon\", .data = source });\n    const content = try tmp.dir.readFileAlloc(std.testing.io, \"app.zon\", std.testing.allocator, .limited(4096));\n    defer std.testing.allocator.free(content);\n}\n```\n\n(`src/tooling/cef.zig` and `src/tooling/manifest.zig` tests.) Model/Msg/update tests need no `Io` at all — the markup-view testing pattern in the native-ui skill is pure.\n\n## Unchanged — do not \"migrate\" these\n\n`std.fmt.bufPrint` / `allocPrint` / `parseInt` / `parseFloat`, `std.mem.*`, `std.debug.print`, `std.heap.page_allocator`, `std.time.ns_per_*` and `ms_per_*` constants, `@embedFile`, `std.testing.expect*`, and managed `std.StringHashMap` / `std.AutoHashMap` all work as before. If code using only these fails, the problem is elsewhere.\n","skill-data/ts-core/SKILL.md":"---\nname: ts-core\ndescription: Authoring guide for the primary Native SDK app-logic path: TypeScript app cores - Model, Msg, update, and the pure functions they call, written in the closed app-core subset, checked by the @native-sdk/core frontend, and compiled ahead-of-time to native code by the external core compiler. Use for new apps unless the user explicitly chose Zig, when writing or modifying a src/core.ts app core, fixing subset checker errors (NS1001-NS1069), or deciding how to express state, messages, text (bytes and the byte-text string methods), text input, continuous controls (sliders, scroll), effects (Cmd), subscriptions (Sub), the host-event wiring channels (frameMsg, keyMsg, pinchMsg, dropMsg, appearanceMsg, chromeMsg, envMsgs, app manifest assets), derived values, the view_unbound lint opt-out, local mutation of owned arrays, or how to split a core into modules under src/ (relative imports, namespace imports, @native-sdk/core/text, @native-sdk/core/events). Use ts-services alongside this guide for src/services work.\n---\n\n# Author app cores in the TypeScript subset\n\nTypeScript is the primary app-authoring language. An app core is a Native SDK app's deterministic logic: `Model` (the app state), `Msg` (a discriminated union of everything that can happen), `update(model, msg)` (the one pure transition function), and the pure helpers they call. You write it as a TypeScript module rooted at `src/core.ts` - splitting into more modules under `src/` when it grows (see \"Splitting a core into modules\") - and the build checks the whole import graph with the `@native-sdk/core` frontend and compiles it to native code with the external core compiler. No JS engine ships in the binary — the program either passes the subset checker and compiles to native, or you get a teaching error naming the rule, the fix, and the reason. The same file is executable TypeScript: it typechecks with stock tsc and runs unmodified under node, so you can poke behavior with plain node scripts before the native build.\n\nA whole TS app starts as three files of truth and zero Zig: `src/core.ts` (this guide; plus core-class modules it imports under `src/`), `src/app.native` (the markup view over the core's model), and `app.json` (windows, identity, permissions). Existing `app.zon` manifests remain supported. Optional ordinary-TypeScript service modules live under `src/services/` and are never imported by the core; load `native skills get ts-services` for that boundary. `native init` scaffolds the three-file base; the build detects `src/core.ts` in the tree (never a flag or config — a tree with both `src/core.ts` and `src/main.zig` is a teaching error) and generates the wiring outside the app. The loop:\n\n```sh\nnative dev --core   # the fastest loop: run the core under node's virtual host —\n                    # dispatch Msgs as JSON lines ({\"kind\":\"add\"}, {\"$bytes\":\"…\"}\n                    # for bytes payloads, {\"advance\":1000} to run virtual timers),\n                    # watch the model + effect transcript. Logic only, no renderer.\nnative dev          # build and run the real app (markup hot reload)\nnative check        # subset-check core.ts + validate markup + app.json/app.zon\nnative build        # ReleaseFast binary; native test runs the app's tests\n```\n\nThe complete reference app in this idiom is `examples/soundboard` in the SDK repo: the soundboard music library as three files and zero Zig — const catalog tables, REAL audio through the `Cmd.audioPlay` stream, scrub-to-seek on a markup slider, a motion-gated `Sub.timer` playback clock, the full text-edit engine on a search field, controlled scroll, registered cover assets, the width-adaptive grid through the frame channel, clipboard, and context menus, with an end-to-end suite driving the shipping markup.\n\n## The contract\n\n```ts\nexport interface Model { /* readonly data fields only */ }\n\nexport type Msg =\n  | { readonly kind: \"add\" }\n  | { readonly kind: \"toggle\"; readonly id: number };\n  // ...one arm per thing that can happen; at least two arms\n\nexport function initialModel(): Model { /* pure */ }\n\nexport function update(model: Model, msg: Msg): Model {\n  switch (msg.kind) {\n    // one case per arm; the switch must be exhaustive (no default needed\n    // once every arm is present — a missing arm is a build error)\n  }\n}\n```\n\n- `update` is pure and synchronous: next model out, plus optionally command data describing effects. When a dispatch needs an effect, declare the return type `Model | [Model, Cmd<Msg>]` and return `[nextModel, cmd]` — the runtime interprets the command after the model commits and dispatches any result back to you as a `Msg`. `initialModel` may return the same pair (`[Model, Cmd<Msg>]`) to run a boot effect once at install, and an app that needs recurring timers exports `subscriptions(model): Sub<Msg>`. See \"Effects are Cmd data\" below.\n- Exported helper functions (`export function doneCount(model: Model): number`) compile to public native functions — and every exported helper taking exactly ONE Model parameter also becomes a Model declaration markup binds by the helper's own name (`{doneCount}`), so derived values need no model field. One binding name per member: a helper that collides with a field (or another helper) is a taught NS1031.\n- Update-only state (fields, helpers, or Msg kinds nothing in markup binds or dispatches — host-fired timer arms, persistence bookkeeping) is declared once: `export const viewUnbound = [\"nextId\", \"tick\"] as const;`. It emits as the `view_unbound` opt-out `native check`'s unbound-state lint reads; a name outside the model surface is a taught NS1032. Entries are the TypeScript names exactly as declared (`\"nextId\"`) and Msg kinds as their `kind` tags — the same names markup binds, because there are no other names.\n- Names: your names are your names — fields, helpers, and locals emit into Zig with their TS spellings (`doneToday` stays `doneToday`), and markup binds them verbatim. String-literal unions emit as native enums.\n\n## Tests against the compiled app\n\nPut Node test files directly in `tests/*.test.ts` and run `native test`. Import `NativeApp` and `findWidget` from `@native-sdk/core/testing` **only in tests**, never in `src/core.ts`. Each `NativeApp.start()` launches a separate headless native process with the real scriptc-compiled core and markup. Drive `click`/`action`/`menu`, assert returned model and accessibility snapshots, inspect and answer fake requests with `respond`, and fire declared timers with `fireTimer`. Supply explicit declared launch values with `NativeApp.start({ environment: [{ name: \"APP_MODE\", value: \"manual\" }] })`; they are journaled and replay does not read ambient environment. Framework app directories still use `appDataDirectory`. Always `close()` in `finally` or use `await using`. `verifyReplay()` verifies checkpoints and the final model in a fresh runtime; afterward only snapshot/close are allowed. Model bytes are JSON number arrays; native identities are decimal strings. The host uses a fixed clock and fake effects, without production persistence/services/app-data startup. See https://native-sdk.dev/docs/testing for the complete API and limits.\n\n## The arena mental model\n\nEverything `update` builds lives in a per-dispatch bump arena that is freed wholesale after the returned model is committed, so spreads, `map`, and `filter` are cheap by construction. At commit, only nodes your update actually created are copied into the persistent model heap — everything you spread through unchanged is shared with the previous model for free.\n\nThat is why the immutable style is not a performance tax: `{ ...model, tasks: model.tasks.map(...) }` copies one small struct and one pointer array, never the world.\n\nBoth regions are the compiled core's own: the frame arena bounds one dispatch's transients, the model heap holds the committed model between dispatches, and the compiler's determinism fences keep every dispatch allocation-shaped and replayable.\n\n## What the subset means\n\nThe subset is TypeScript minus the ecosystem minus the purity violations — never minus basic syntax. Concretely: every basic statement, operator, and declaration form of the language compiles (every loop shape including `do...while`, labels with labeled `break`/`continue`, `switch` with `default`, the full assignment-operator family, `**` and the shifts, const record destructuring, namespace imports over your own modules). What does not compile falls into exactly two families, each with a named teaching rule: the ECOSYSTEM the binary cannot carry (npm packages, Node/DOM APIs, regex/JSON/Promise/generator machinery, `eval` — no JS engine ships), and the constructs that would break a core's guarantees (purity and determinism, fixed shapes, one text representation, functions as declarations not values, static types with no runtime tags). Classes and exceptions are NOT in either family: data classes and `throw`/`try`/`catch`/`finally` compile (see below) — only their guarantee-breaking tails (inheritance, unsafe finally, untagged thrown values) teach. A construct that fails with a generic error instead of a teaching rule is a checker bug — the grammar matrix test (`grammar_matrix.test.ts`) pins every grammar production to its verdict so no silent gap can appear.\n\nThe banned families at a glance (each diagnostic names the fix and the reason at the site):\n\n- **No JS engine ships**: `eval`, `new Function`, dynamic `import()`, `debugger` (NS1013); regexes (NS1040); BigInt/Symbol (NS1044); generators (NS1042); async/Promises (NS1002); npm imports (NS1035).\n- **Purity and determinism**: mutation of SHARED data — parameters, model/msg trees, module tables, escaped locals (NS1001/NS1022/NS1051; your own function-local scratch arrays mutate freely, see \"Local mutation\" below), module-level `let` (NS1010), ambient time/randomness/IO (NS1005), effects outside the Cmd/Sub return paths (NS1017/NS1025).\n- **Fixed shapes and layouts**: the class machinery beyond data classes — `extends`/`super`/`abstract` (NS1055), accessors/`#`-privates/class expressions/`this`-as-a-value (NS1056/NS1006), mutable statics (NS1010), generic classes (NS1053) — plus `delete`/getters/setters/computed keys (NS1012), `for`/`in` (NS1009), Map/Set (NS1011), runtime type/shape tests — `typeof`/`in`/`instanceof`/`Object.*` (NS1041). Data classes themselves compile — `static` methods, `static readonly` consts, and erased `private`/`protected` included (see \"Data classes\" below).\n- **One text representation**: string indexing (NS1004), `+` concatenation and tagged templates (NS1018), `string` model fields (NS1024), the byte-text stays-out spellings — `charCodeAt`/`normalize`/`replace` and friends on bytes teach the byte-honest form (NS1060; the supported method surface is under \"Text is bytes\").\n- **Functions are declarations — or const local helpers**: nested function declarations, non-const function values, `?.()` (NS1046), and const helpers that capture/escape/under-annotate (NS1054 — the legal shape is below under \"Local function values\"); fixed arity — no defaults, rest, `arguments`, call spreads (NS1019); generics live on module-level declarations and monomorphize per call site (NS1050/NS1053).\n- **The mapping stays exact**: `var` hoisting (NS1049), loose `==` (NS1048), comma/void/assignment-as-value outside a for-header (NS1043), array/parameter destructuring (NS1045), `export default`/`export =`/`export * from` (NS1047 — export lists and named value re-exports compile), namespace-alias-as-value and SDK namespace imports (NS1039).\n\n## What compiles (v1)\n\nModel and message shapes:\n\n- `interface` with `readonly` fields; nested interfaces; `T | null` for optional data.\n- Model field types: `number`, `boolean`, string-literal unions (`\"all\" | \"active\" | \"done\"` → native enum), numeric-literal unions, `Uint8Array` (bytes), a nested interface, `readonly Interface[]` (arrays of object types), primitive arrays (`readonly number[]`, `readonly boolean[]`, arrays of literal-union tags), a tag-discriminated union (`{ kind: \"list\" } | { kind: \"detail\"; note: Note }` — arms may carry records, bytes, and primitive arrays), and `T | null` over any of these. Model unions compile to native tagged unions; switching arms in `update` retires the old arm's payload automatically at commit.\n- `Msg`: a discriminated union on a `readonly kind` string tag, with primitive / bytes / interface payload fields. It must be a real union — give it at least two arms, or TypeScript collapses the alias to a plain object type and the checker rejects it.\n\nLogic:\n\n- `switch` on any union's `kind` tag — `msg.kind` (the Msg dispatch) and model-field unions (`switch (model.view.kind)`) alike — with member case labels, label stacking (`case \"a\": case \"b\": body`), `break`, and a trailing `default` covering the unnamed arms (without a `default` the switch must be exhaustive — NS1015; with every arm named the `default` is JS dead code and emits nothing) — and `switch` on a string-literal-union or numeric-literal-union *value* (`switch (model.filter)`) — an uncovered member skips the switch exactly like JS (a `default` anywhere but last is a taught stop in both forms) — and `switch` on a plain `number` or `string` value, lowered to an if/else chain with exact JS semantics: strict equality per case (NaN matches nothing, `-0` matches `0`, strings compare contents), cases tested in source order, `default` matching only after every case misses wherever it sits (an empty `default:` stacking onto the next body included); `if`/`else`; classic `for (let i = 0; ...)` including countdowns (`i--`, `i -= k`), multi-counter inits (`let lo = 0, hi = n`), and comma incrementors (`lo++, hi--` — the for-header is the one home for comma sequences); `do { ... } while (cond)` (the body runs before the first test; `continue` jumps to the test, exactly node); `for (const x of xs)` over arrays and `Uint8Array`, with `break`/`continue`, plus the indexed pair form `for (const [i, x] of xs.entries())` (exactly the `[index, element]` two-identifier binding — the index is the loop index, integer-classed; other tuple shapes stay taught); `while`; labeled statements on loops and blocks with labeled `break`/`continue` (`outer: for (...) { ... continue outer; }` — a labeled `continue` in a classic for still runs the incrementor, like JS); `let` locals with reassignment (and `let x: number;` declared-then-assigned); ternaries; `&&`/`||`/`!`; the empty statement `;`.\n- `const { total, done: doneCount } = stats;` — record-field destructuring into const locals (a compile-time alias per field, renames included). Array patterns, parameter patterns, defaults, rest, and nesting are taught (NS1045 — positions can be silently absent in JS; fields cannot).\n- `import * as util from \"./util.ts\"` — a namespace import over your own modules is pure dot-syntax: `util.helper(x)`, `util.CONST`, and `util.Cfg` in type positions all resolve to the target module's flat names. The alias is not a value (storing or passing `util` itself is taught), and the intrinsic `@native-sdk/core` module is always imported by name (NS1039 — the purity rules recognize `Cmd`/`Sub`/`asciiBytes`/`utf8Bytes` by their imported names).\n- Object spread `{ ...model, field: v }`, array spreads in any shape — append `[...xs, x]`, prepend `[x, ...xs]`, multi-spread `[...a, x, ...b]` (each compiles to one exact-size copy) — `.length`, indexing `xs[i]`.\n- Array methods, lowered to inlined loops and exact-size arena copies: `.map` / `.filter` / `.find` / `.findIndex` / `.some` / `.every` / `.reduce` / `.toSorted` / `.slice` / `.concat` / `.indexOf` / `.includes`. `.map` is type-changing — `tasks.map((t) => t.id)` produces a number array, `t => t.title` a bytes array, and a callback that can return `null` produces an optional-element array. Callbacks on map/filter/find/findIndex/some/every may take the `(element, index)` pair — the index is the loop index, integer-classed (`.reduce` stays `(acc, x)`: its index parameter is not in v1, and no callback takes the third JS parameter, the array itself — reference the array by name). Array-method calls may sit directly in `if`/`else if` and ternary conditions (`if (xs.some((x) => x > 3))`) — the scan lowers to a loop just before the branch; a `while` condition cannot (it re-evaluates per iteration — hoist into the loop body or restructure). Callbacks are arrows (expression or block body), inline `function` expressions, or a BARE REFERENCE to a module-level function or const helper (`xs.map(encodeTurn)`, `xs.toSorted(byAscending)` — the referenced body inlines exactly like the arrow spelled at the site); in a block body every code path must end in an explicit `return` (falling off the end would be JS `undefined`, which has no mapping — a taught stop). JS semantics hold exactly: `.slice` resolves negative and out-of-range indices the JS way, `.indexOf` never matches `NaN` while `.includes` does, `.some`/`.every` keep their vacuous defaults on empty arrays, and `.reduce` needs its initial value (the no-initial form throws on an empty array in JS, so it is a taught NS1007 — pass the starting accumulator). `.indexOf`/`.includes` work on scalar elements (numbers, tags, booleans); on record arrays JS compares references, which has no native mapping — match a field with `.find`/`.findIndex` instead.\n- **Local mutation — your own scratch is yours; shared data is immutable.** An array your function CREATES — an array literal (`const stack: number[] = []`, `const st = [1, 2, 3]`) or a fresh copy (`.slice()` / `.map()` / `.filter()` / `.concat()` / `.toSorted()`) — is locally owned, and the full mutating method set works on it with exact JS semantics: `push(...items)`, `pop()`, `shift()`, `unshift(...items)`, `splice(start, deleteCount?, ...items)` (negative/overshooting indices clamp the JS way; the value is the removed array, also yours), `reverse()`, `fill(v, start?, end?)`, in-place `sort(cmp)`, and indexed writes `xs[i] = v`. A parser stack, a work queue, a copy-then-sort — all legal, deterministic, and byte-identical to node. Ownership ends at the first ESCAPE: once the array is returned from a callback, stored into a record/array/model, aliased by a second binding (`const b = a`), or passed where the callee could keep or mutate it, mutating it afterwards is a taught NS1051 — finish mutating first, then let it escape (an early-exit `return` is fine: execution ends there, so mutations on the other path stay legal). Two loosenings keep real code flowing. BORROWING: passing an owned array into a `readonly T[]` parameter is NOT an escape when the callee only READS it (element/property access, iteration, spreads, further borrowing passes — no return of it, no store, no onward pass into a mutable position; recursion over borrowed slices included), so measure-mutate-measure loops work (`total(out); out.push(x); total(out)`). REASSIGNED-OWNING: a `let` binding whose EVERY assignment installs a fresh owning construction (a literal or a copy — `w = xs.filter(...)`, `acc = []`) stays owned through the reassignments; ONE mixed assignment (an alias, a parameter, a helper result) and the binding never owns (NS1001 names it). Never owned: parameters, model/msg data, module `const` tables, aliases, mixed reassigned bindings, and arrays produced by helper calls (copy with `.slice()` to own one). After the value escapes it is an ordinary immutable value; the commit walkers and sharing discipline are unaffected because ownership ended before the escape.\n- Local-mutation shape notes: `push`/`unshift` return the new length in JS, which has no mapping — mutate as a statement and read `.length` after; `sort`/`reverse`/`fill` return the same array — mutate as a statement, then use the array by name (`return copy.sort(cmp)` is a taught stop; the canonical form is `const copy = xs.slice(); copy.sort(cmp); return copy;`); `pop()`/`shift()` return `T | undefined` — the same one-empty the `.find` miss produces, so test `=== undefined` or fold with `??` (`stack.pop() ?? fallback`); spread arguments (`out.push(...xs)`) stay taught — one element per iteration; `xs[xs.length] = v` on an owned array IS a push (the one growth shape — compound forms like `xs[xs.length] += v` read the missing slot first and stay taught), and other out-of-bounds writes are JS sparse arrays with no mapping (they trap on the native bounds check in safe builds — keep writes inside `0..length-1`); changing the LENGTH of the array a `for...of` (or one of its own callbacks) is iterating is a taught stop (JS walks the live array; fixed-length writes during iteration are fine and identical to node); `copyWithin` stays out of v1 (splice/fill cover it).\n- `.toSorted(cmp)` sorts a copy in one expression; `.sort(cmp)` sorts in place on an array you own (on shared data it keeps the NS1022 teaching, which names the copy idiom). Both comparators follow the same rules: return a sign — `(a, b) => a - b` for ascending numbers, or explicit -1/0/1 branches; a boolean comparator is wrong in JS itself (false claims equality) and is rejected by the types plus a taught NS1023. The comparator-less arity sorts by string ToString order in JS (`[10, 9]` stays `[10, 9]`), which has no float-text mapping — pass a comparator. Both sorts are stable exactly like JS: comparator 0 (or NaN) keeps the original order of the pair. One honesty note: a comparator that is inconsistent over the actual data (e.g. `a - b` when elements can be `NaN`) is implementation-defined in JS itself, so node and native may then disagree — keep comparators consistent.\n- A `.find` miss is the core's one empty value: JS spells it `undefined`, so test the result with `=== undefined` (never `=== null` — the checker teaches the difference) or fold it away with `??`: `tasks.find((t) => t.id === id) ?? fallback`.\n- Optional chaining `?.` on property chains (`model.sel?.at ?? 0`), element hops (`m?.xs[0] ?? 0`, `xs?.[i] ?? d`), and method hops on supported receivers (`xs?.slice(0, 2)`, `xs?.includes(3) ?? false` — every mapped array/bytes method): each hop null-propagates exactly like JS, and the chain value is optional — end it in `??` or compare it against a real value. A `?.` chain compared against `null`/`undefined` is a taught error (NS1021), and `g?.()` on a function value stays taught.\n- Null-guard narrowing through `&&`/`||` chains, exactly the way TS narrows: `if (x !== null && x.items.length > 0)`, the flipped order (`null !== x`), the `||` dual (`x === null || x.items.length === 0`, including as an early-exit guard — the code after the exit stays narrowed), ternary conditions (`x !== null && x.at > 0 ? x.at : -1`), and `while (cur !== null && cur.n > 0)` loops (re-tested per iteration; assigning the guarded local drops the narrowing for what follows, like TS). Relational comparisons on guarded optionals (`cls !== null && cls < lim`) work too.\n- Nullish `??`, comparisons (including `===` on `string`-typed values — content equality, same as node; `==`/`!=` are taught NS1048 — coercion), `+ - * / % **` on numbers, unary `+`/`-`, and the bitwise family `& | ^ ~ << >> >>>` — all with JS number semantics (`/` is float division, `%` truncates, `**` is float pow with the exact JS corners — `1 ** NaN` is NaN, `(-1) ** Infinity` is NaN, right-associative `2 ** 3 ** 2` is 512; bitwise and shifts are ToInt32 with the shift count masked & 31, `>>>` yielding the unsigned 32-bit value; unary `+` is the identity on numbers). `**` and `/` results are float-classed; bitwise/shift operands are integer-required positions (a float operand is a taught NS1016).\n- Every compound assignment as a statement: `+= -= *= /= %= **= &= |= ^= <<= >>= >>>=`, each exactly `x = x op v`, plus the guarded forms `&&=`/`||=` (boolean targets; the right side evaluates only when assigned, like JS) and `??=` (optional targets; assigns only when null). A number `++`/`--`/assignment may sit in a VALUE position when the split statement is provably order-exact — the variable's only mention in the statement, in a position JS cannot skip (`arr[i++]`, `const n = ++count`, `const z = (y = 5)`; postfix yields the pre-step value, everything else the post-step value, exactly JS); every other value-position form is taught (NS1043 — ternary branches, short-circuit right operands, loop conditions, or a second mention of the variable).\n- The Math batch, every corner pinned to node: `Math.min` / `Math.max` (any arity — `Math.min()` is Infinity, `Math.max()` is -Infinity, NaN propagates, -0 orders below +0), `Math.round` (half toward +Infinity), `Math.floor` / `Math.ceil` / `Math.trunc` (NaN/Infinity propagate; the -0 results keep their sign, so `Math.ceil(-0.5)` is -0), `Math.abs` (clears the zero sign), `Math.sign` (NaN stays NaN, a zero keeps its sign), `Math.sqrt` (negative input is NaN, `sqrt(-0)` is -0). `Number.isInteger` / `Number.isFinite` / `Number.isNaN` classify like node, and the `NaN` / `Infinity` globals are ordinary number values. Math calls over compile-time constants fold to their exact JS value — `const HALF = Math.floor(5 / 2)` is the integer 2, `5 % 0` is NaN, `-5 % 5` is -0. A bare `-0` literal (and constant arithmetic folding to -0, like `0 * -1`) is a float value — only f64 carries the signed zero — so it cannot flow into an index or another integer-required slot. The integer rule: floor/ceil/trunc/abs/sign of an integer-classed value stays integer-classed, but of a float value stays float (floor of NaN is NaN), so `bytes[Math.floor(x / 2)]` over a float `x` is still a taught NS1016 — keep index flows integer end to end.\n- Template literals with integer holes (`` `${n} of ${total}` ``) feeding `utf8Bytes` or `asciiBytes` (below). Float-valued holes are not in v1 (JS float-to-string fidelity is a runtime v2 surface).\n- Module-level `const` numbers and strings fold to comptime constants, and const tables emit as rodata (no arena, shared for free at commit): arrays of numbers / booleans / strings / literal-union members (`const WEEKDAYS = [3, 5, 2]`, `const ORDER: readonly Filter[] = [\"done\", \"all\"]`), records annotated with an interface (`const LIMITS: Limits = { lo: 1, hi: 9 }`), and arrays of records (`const SEEDS: readonly Task[] = [...]`, names as `utf8Bytes` literals). Element access, `.length`, `for...of`, and the array methods all work over tables. Everything inside must fold at compile time — no spreads, no calls except `asciiBytes` or `utf8Bytes` on a literal — and a record table needs its interface annotation (an unannotated `{ ... }` is a taught stop naming the fix). Helper functions; recursion.\n- **Generics — module-level, monomorphized per call site.** A generic `function`, `interface`, or `type` declares type parameters and instantiates from tsc's RESOLVED type arguments (explicit or inferred): `export function pick<T>(xs: readonly T[], i: number): T { return xs[i]; }` called with tasks emits `pick__Task`, with numbers `pick__f64` (a bare `number` type argument is always f64 — the JS-exact class), one readable Zig fn per distinct instantiation, deduped. Generics over records, unions, arrays, optionals, and bytes all work; generic interfaces/aliases instantiate structurally (`Box<Task>` emits `Box__Task`; `type Opt<T> = T | null` resolves straight through); generics may recurse and call other generics (the inner call resolves at the outer's instantiation). `typeof CONST` type-query aliases resolve through tsc too (`const LIMIT = 9; type Limit = typeof LIMIT`). The boundaries teach: a call site whose type argument stays abstract (`pick([])` infers `never`; `any`/`unknown`, unnamed literal unions) is NS1053 — annotate the call or name the alias; generic function VALUES and generic entry points are NS1050.\n- **Data classes — fields, one constructor, plain methods, statics; no inheritance.** `class Task { title: Uint8Array; done: boolean = false; constructor(title: Uint8Array) { this.title = title; } toggle(): void { this.done = !this.done; } isDone(): boolean { return this.done; } }` emits as a plain struct plus module-level functions; `new Task(...)` constructs a record-shaped value (field initializers run in declaration order, then the constructor body). `static` members are per-class module declarations: a `static` method lowers to a receiver-less module fn under the class's mangled name (`Task.fromRow(...)` resolves to `Task__fromRow`), and a `static readonly` field with an initializer is a module const (`Task.LIMIT` — the module-const value rules apply: numbers/strings fold, tables need their annotation); a MUTABLE static is module state and teaches NS1010, and inside a static member reach other statics by the class name, never `this` (NS1056). `private`/`protected` keywords are accepted and ERASED — tsc enforces them at the type level, which is their whole meaning (`#`-fields stay taught: runtime privacy brands). `this` reaches instance fields and methods (`this.count`, `this.step()`) — anything that lets `this` escape as a value (returning it, storing it, passing it) is taught (NS1056), so fluent chaining is out. Mutation follows exactly the array ownership rule: an instance your function creates with `new` mutates freely — direct field writes (`t.count = 1`, `t.count += 2`) and methods that write `this` — until it ESCAPES (returned, passed, stored, aliased — then NS1051), and parameters/model data never mutate (NS1001); methods that only read are callable on anything. Fields require type annotations; instances flow between functions, sit in arrays, and compare/narrow like records. The class TAIL teaches by name: `extends`/`super`/`abstract` (NS1055 — compose, or model variants as a `kind`-union), getters/setters/`#`-privates/`accessor`/class expressions (NS1056), mutable statics (NS1010), generic classes (NS1053), parameter properties (NS1008), `instanceof` (NS1041 — a `kind` field is the tag that exists). Class instances stay LOCAL values in v1: storing one in the Model tree is taught (NS1056) — keep records (interfaces) in the Model and construct the class where behavior is needed.\n- **Exceptions — `throw`/`try`/`catch`/`finally` as pure control flow.** Inside a core, exceptions are deterministic: `throw` carries a subset VALUE and unwinds to the nearest enclosing `catch` — across helper calls, out of array-method callbacks (a `throw` inside `.map`'s callback exits the whole loop, like JS), through nested `try`s, with `finally` running on every path (fall-through, `return`, `break`/`continue`, and throw alike). The discipline is two rules. First (NS1057): thrown values are kind-tagged subset shapes — throw kind-discriminated records (`throw { kind: \"bad_digit\", at: i } as ParseError`, where `ParseError` is an interface with a string-literal `kind` field or a `kind`-discriminated union; a single-shape core may also throw a number), and SEVERAL distinct shapes may throw: the checker collects every shape the core throws into its implicit thrown union. The catch binding IS that union — narrow it in place with kind tests, no `as` ceremony: `catch (e) { if (e.kind === \"bad_digit\") return -e.at; if (e.kind === \"io\") return e.code; return -1; }` (or `switch (e.kind)` — tsc cannot prove exhaustiveness over the implicit union, so give the switch a `default` or a trailing return). Bare rethrow (`throw e;`) re-raises the bound value — a narrowed arm included — and `catch { ... }` needs no binding; the single-`as` form (`const err = e as ParseError;`) stays legal in single-shape cores (and for a DECLARED union whose arms equal the thrown set — declare `type AppError = ... | ...` and `as AppError` works). What teaches: untagged values in a heterogeneous set, two shapes sharing one `kind` with different payloads, asserting one member shape of a multi-shape core, the binding escaping untyped into a call/store/return, and `throw new Error(...)` (engine error objects carry stack traces with no native layout). Second (NS1058): `finally` never redirects control flow — no `return`/`throw`/`break`-out inside it (JS's own no-unsafe-finally rule; loops fully inside the finally may break within themselves). An UNCAUGHT throw that reaches an exported function's boundary is a defined deterministic panic — exactly where node's process would crash. A throw mid-mutation of an owned array keeps the mutations applied so far, exactly like JS — the catch sees the array as node would.\n- **Local function values — const helpers hoist.** `const scale = (x: number): number => x * 3;` (arrow or `function` expression) hoists to an ordinary module-level fn when it is capture-free (module constants and other const helpers are fine to reference; enclosing locals/params are not — pass them as parameters), fully annotated (every parameter and the return type), and used only by direct calls (`scale(v)`, recursion included) or as an array-method callback (`xs.map(scale)`, comparators included). Everything else teaches NS1054: captures, missing annotations, `let` bindings, returning/storing the value, passing it to your own functions, calling through a record field. Capturing a locally-owned array also ENDS its ownership at the capture (a later mutation is the NS1051 teach) — the stored closure would retain the reference.\n\nNot yet in v1 — genuine roadmap deferrals, each stopping with a loud, tailored NS9001 naming the rewrite (never missing basic syntax; the banned-with-a-rule families live in \"What the subset means\" above). The rule-level deferrals carry `class: \"deferred\"` in the diagnostics catalogue — NS1011 (Map/Set), NS1019 (fixed arity), NS1040 (regexes), NS1042 (generators), NS1044 (BigInt/Symbol) — and their diagnostics say the capability is deliberately deferred, not impossible; the list below is the method-level remainder: `.toSorted()`/`.sort()` without a comparator (JS ToString ordering; pass `(a, b) => a - b`), `.reduce` without an initial value (a taught NS1007 — JS throws on an empty array) or with an index parameter (use a classic loop), `.indexOf`/`.includes` on record arrays (match a field with `.find`/`.findIndex`), `.join` on number arrays (elements are float-valued; join byte values instead), float values (`/`, `**`, `Math.round`, `Math.sqrt`, float `Math.floor`-family results) where an integer is required such as an index (a taught NS1016 — those values can be fractional or NaN), Math methods beyond the batch above, `Number` methods beyond the three classifiers, float-valued template holes (JS float-to-string fidelity is a runtime v2 surface), arrays of unions (`readonly View[]`) or arrays of byte-strings (`readonly Uint8Array[]`) as model fields (wrap the element in a single-field interface), a collect spawn's stderr tail (v1 delivers the exit code and stdout; stderr is not surfaced — put diagnostics on stdout or check the code), per-line truncation flags (a stdout or response line over its configured bound arrives cut, without a flag), and non-timer subscriptions (`Sub.timer` is the one subscription; one-shot needs are `Cmd.delay`, and process/fetch/audio streams are Cmd-initiated, not subscribed).\n\n## Effects are Cmd data\n\n`update` never performs an effect — it can return one, as inert data, alongside the next model. Import the factories from the SDK and declare the pair-return type:\n\n```ts\nimport { Cmd } from \"@native-sdk/core\";\n\nexport function update(model: Model, msg: Msg): Model | [Model, Cmd<Msg>] {\n  switch (msg.kind) {\n    case \"add\":\n      return [{ ...model, count: model.count + 1 }, Cmd.persist()];\n    case \"request_time\":\n      return [model, Cmd.now(\"tick\")]; // dispatches { kind: \"tick\", at: <ms> }\n    case \"tick\":\n      return { ...model, lastTick: msg.at }; // bare model = [model, Cmd.none]\n    case \"ship\":\n      return [model, Cmd.batch([Cmd.persist(), Cmd.host(\"beep\", model.count)])];\n  }\n}\n```\n\nThe command set (Cmd wire format v7):\n\n- `Cmd.none` — no effects; returning a bare `Model` is sugar for `[model, Cmd.none]`.\n- `Cmd.persist()` — snapshot the just-committed Model through the engine-owned store. Requires `\"persist\"` in `app.zon` capabilities plus `.persist = .{ .version, .restore = .{ .ok, .none, .err } }`; the host owns canonical encoding, debounce/coalescing, atomic app-data placement, backup recovery, and replay.\n- `Cmd.now(\"tick\")` — request a timestamp; the runtime dispatches the named Msg arm with the time (ms) as its payload. The target arm must carry exactly one number field (`{ kind: \"tick\", at: number }`), and tsc checks that for you.\n- `Cmd.host(name, ...args)` — a fire-and-forget host command by literal name; the host decides what the name means. Args are numbers, OR exactly one bytes payload: a `Uint8Array` (`Cmd.host(\"clipboard.write\", model.draft)`) or a flat inline record of number/boolean/`Uint8Array` fields (`Cmd.host(\"cfg.save\", { gain: model.gain, on: model.muted, label: asciiBytes(\"main\") })`) — the record lowers to one bytes payload from your types at build time, byte-identical under node and native. Anything else (a smuggled string, a nested record, a payload plus extra args) is a taught error (NS1020/NS1026).\n- `Cmd.request(name, payload, { key?, ok, err })` — a routed host command: the host performs `name` with the payload (same bytes/record rules) and dispatches exactly one result back to you as an ordinary Msg — the `ok` arm with the result bytes on success, or the `err` arm with the error bytes on failure. Both arms must carry exactly one `Uint8Array` field (`{ kind: \"loaded\", body: Uint8Array }`), checked by tsc and taught by NS1027. The routing is data — string-literal arm names, never callbacks — so the result decoder derives from your Msg types at build time. The optional `key` (a string literal) names the in-flight effect: issuing a request whose key is already in flight replaces it (the old result is dropped), which is the debounce/exactly-one-in-flight discipline.\n- Typed app services are the deliberate record-valued arm of this family: declare shared data shapes outside `src/services/`, import generated constructors from `@native-sdk/services`, and let those constructors encode requests and prove the typed success Msg arm. See `native skills get ts-services`. Raw `Cmd.request` remains bytes-oriented.\n- `Cmd.cancel(key)` — drop the in-flight keyed effect with that key, silently: a cancelled raw request, buffered named engine op (`readFile`/`writeFile`/`fetch`/`clipboardRead`), streamed file read, or armed delay dispatches NEITHER arm. Live spawn, streaming-fetch, streaming-service, and streamed write sinks are the exceptions: cancel ends them through `err: cancelled`; a half-written sink is observable and must never disappear silently.\n- `Cmd.batch([a, b])` — several commands from one dispatch, performed in order.\n\n### The named engine ops\n\nThese map directly onto the host's effect engine — files, HTTP, the clipboard, notifications, one-shot timers. Routed operations follow the `Cmd.request` rules (inline `{ key?, ok, err }`, string-literal arm names, arm shapes checked by tsc and taught by NS1027), with one difference from `request`: each op's `ok` arm has the op's OWN result shape. Keys follow the one keyed-effect rule everywhere: issuing a keyed op whose key is already in flight REPLACES the old one (the superseded op's result is dropped — no message), and `Cmd.cancel(key)` drops it silently. Every `err` arm carries exactly one `Uint8Array` field and receives a machine-readable reason; fire-and-forget writes and notifications have no routing arms. Paths, URLs, and bodies are bytes (`asciiBytes` for literals); dynamic values the engine refuses at runtime surface through the `err` arm, while compile-time-knowable bounds for routed operations stop the build (NS1030). Fire-and-forget operations fail closed under the host's validation instead.\n\n- `Cmd.store.set(key, bytes, { key?, ok, err })` / `get` / `delete` / `scan` / `setMany` — capability-gated, engine-owned per-record storage. Declare `\"store\"` in `app.zon`; keys are UTF-8 strings up to 512 bytes and values are bytes up to 1 MiB. `setMany` applies 1–64 entries atomically. `get`'s ok bytes are `[1][value...]` for a hit and `[0]` for a miss, keeping an empty value distinct from absence. `scan(prefix, { limit?, after? }, route)` returns a little-endian length-prefixed page of key/value pairs plus an opaque next-key cursor; pass those next-key bytes back as `after` (a known literal cursor may be a string). The limit defaults to 100 and is capped at 256. Writes run off-loop in issue order, a synchronous read waits for earlier writes in the same command walk, every result journals, and failures route one of `io_failed`, `over_bound`, `bad_key`, `rejected`, or `busy`. The SQLite backing and app-data path are never part of the app-facing API.\n- **Checked relational SQLite:** declare `\"sqlite\"` in `app.zon`. Add append-only, contiguous `src/schema/NNNN_name.sql` migrations (`STRICT` ordinary tables), commit the generated `src/schema/migrations.lock.json`, and add named statements in `src/queries.sql`: `-- name: notesInFolder`, `-- name: moveNote :exec`, or `-- name: notesInFolder :live`. `native check` applies the migrations and prepares every query with real SQLite, then generates flat `Cmd.q<Name>(params, route)` query constructors, typed `Cmd.q<Name>(params)` exec members composed only through `Cmd.qTx([...], route)`, `Sub.q<Name>(key, params, route)` for `:live`, row/param interfaces, and `decode<Name>Page`. TEXT/BLOB rows are `Uint8Array`; parameters inferred from TEXT columns wrap bytes automatically; raw or uninferred dynamic TEXT (for example an FTS `MATCH` term) uses `dbText(bytes)`. INTEGER is an exact `number` and generated decoders reject values outside ±(2^53−1). Live queries rerun after committed writes to their generated table dependency set, coalesced once per command frame. `native db new-migration <name>`, `status`, and `reset --yes` own development migration operations. Do not edit or reuse a shipped migration number.\n- `Cmd.db.query(sql, params, { key?, page, done, err })` / `Cmd.db.exec(statements, { key?, ok, err })` — the permanent raw SQLite escape hatch. The engine owns `app.db`; parameters are `null | number | string | Uint8Array | dbText(bytes) | boolean`. A read delivers 256-row/256-KiB pages then `done`, with one result capped at 8,192 rows or 8 MiB; crossing either total bound rejects the whole result, so use `LIMIT` and keyset pagination for larger collections. An exec commits its 1–64 statements as ONE transaction. SQL remains pathless: `ATTACH`/`DETACH`/`VACUUM INTO`, TEMP schema objects, and engine lifecycle PRAGMAs are denied. Query keys replace/cancel silently; duplicate transaction keys reject loudly. Error bytes are `constraint`, `busy`, `io_failed`, `corrupt`, `misuse`, `rejected`, or `cancelled`. Every page/terminal journals and replay never opens SQLite. Prefer declared queries; NS1420 nudges raw query literals toward them.\n- `Cmd.readFile(path, { key?, ok, err })` — read a whole file. `ok` arm: one `Uint8Array` field with the content. `err` reasons: `not_found`, `io_failed`, `truncated` (the file exceeds the engine's 1 MiB read bound — a cut file never passes as whole), `rejected`. Paths are at most 1024 bytes.\n- `Cmd.writeFile(path, bytes, { key?, ok, err })` — write a whole file (parent directories created, an existing file replaced whole; at most 1 MiB). `ok` arm: NO payload fields (`{ kind: \"wrote\" }`) — a successful write has nothing to report. `err` reasons: `io_failed`, `rejected`.\n- `Cmd.appendFile(path, bytes, route)` appends one bounded payload; `Cmd.statFile(path, route)` returns `{ exists, size, mtimeMs }`; `Cmd.deleteFile(path, route)` deletes one file with a payload-less `ok` arm and routes a missing path as `not_found` (a final symlink is unlinked without deleting its target). `Cmd.readFileStream(path, { key?, chunk, done, err })` delivers 256-KiB chunks then the total; reissuing or cancelling its key silently replaces/drops the read. Atomic exports open with `Cmd.writeFileStream(key, path, route)`, send one acknowledged `writeFileChunk` (≤1 MiB) at a time, then `writeFileClose`; a duplicate sink rejects, overlapping chunk/close routes `out_of_order`, and sink cancellation is loud. Stream chunks spill to the session blob store for byte-identical replay. Raw paths under this app's resolved data/config/cache/state/logs/temp roots need no grant; every external path requires the `filesystem` permission. The runtime resolves existing parents and symlinks before checking (an in-root symlink pointing out is external); NS1074 catches certainly-external literals.\n- `Cmd.fetch({ url, method?, headers?, body?, timeoutMs? }, { key?, ok, err })` — a buffered HTTP(S) exchange. `ok` arm: exactly two fields, one `number` and one `Uint8Array` (`{ kind: \"fetched\", status: number, body: Uint8Array }`) — matched by type, so the names are yours. The status is the real HTTP status: a 404 is still `ok` (an HTTP-level error is a delivered response). `err` reasons: `connect_failed`, `tls_failed`, `protocol_failed`, `timed_out`, `rejected`, and `truncated` (the body exceeded the engine's 256 KiB buffered bound — never delivered silently cut). The spec is an inline object: `url` bytes (≤ 2 KiB), `method` one of `\"GET\" | \"POST\" | \"PUT\" | \"DELETE\" | \"PATCH\" | \"HEAD\"` (default GET), `headers` an inline flat record — names are compile-time ASCII, values are string literals OR runtime bytes (`{ authorization: bearerToken(model.apiKey), \"content-type\": \"application/json\" }` — how a launch-supplied key rides an `Authorization` header; ≤ 8 headers, ≤ 1 KiB total, NS1029/NS1030), `body` bytes (≤ 64 KiB), `timeoutMs` a positive integer literal (engine default when omitted).\n- `Cmd.clipboardWrite(bytes)` — put bytes on the system clipboard, fire-and-forget: there is no routing, and a refused or over-bound write is dropped by design.\n- `Cmd.clipboardRead({ key?, ok, err })` — read the clipboard. `ok` arm: one `Uint8Array` field with the text. `err` reasons: `failed` (no clipboard service, over-bound content, pasteboard error), `rejected`.\n- `Cmd.showNotification({ id?, title, subtitle?, body?, actionLabel?, actionCommand? })` — request a desktop notification, fire-and-forget: `title` is required bytes (1–128), `subtitle`, `id`, `actionLabel`, and `actionCommand` are optional bytes (up to 128), and `body` is optional bytes (up to 1024). A nonempty `id` replaces the app's earlier notification with that id where the OS supports replacement. `actionLabel` and `actionCommand` must be supplied together; activation dispatches `actionCommand` through the ordinary app-command path while the process is running. The host validates every field and command name before entering the platform service; invalid or unavailable requests fail closed, and Focus / Do Not Disturb and user settings remain authoritative after acceptance. Fake execution and session replay never display one, while the null test platform can activate a recorded notification deterministically.\n- `Cmd.openExternalUrl(url)` — open an HTTP(S) URL in the system browser, fire-and-forget. `url` is bytes; the runtime validates it and enforces `security.navigation.external_links` before entering the platform service. Invalid, denied, or unavailable requests fail closed; fake execution and replay never open one.\n- `Cmd.revealPath(path)` — reveal a byte path in Finder, Files, or Explorer, fire-and-forget. Runtime path validation stays in force; invalid or unavailable requests fail closed, and fake execution/replay never reveal one.\n- `Cmd.credentials.set(key, secret, { key?, ok, err })` / `Cmd.credentials.get(key, route)` / `Cmd.credentials.delete(key, route)` — app-scoped Keychain on macOS/iOS, Secret Service/libsecret on Linux, Credential Manager on Windows, and AndroidKeyStore-wrapped authenticated ciphertext on Android. The authored credential key is a NUL-free string (UTF-8, ≤256 bytes); the manifest app id (≤128 bytes) supplies the service namespace, and secrets are `Uint8Array` values through 2,560 bytes (the cross-platform floor set by WinCred). Set/delete deliver empty bytes on `ok`; get delivers secret bytes; the closed err reasons are `miss`, `denied`, `locked`, `io_failed`, `over_bound`, and `rejected`. Declare BOTH `\"credentials\"` in `app.zon` capabilities and permissions; NS1071 warns when the capability is absent, NS1072 errors when permission is absent, and NS1073 reserves `core.credentials.*` for the typed factories. Never retain a returned secret in Model: consume its Msg immediately to construct the next effect. Recording stores only a successful get's length, per-session salt, and a placeholder digest independent of the secret (never the secret or a blob); replay delivers deterministic same-length placeholder bytes, and devhost keeps an in-memory map while printing only `<redacted, N bytes>`.\n- `Cmd.formatLocalTime(timestampMs, \"date\" | \"time\" | \"datetime\", { key?, ok, err })` — format epoch milliseconds (the same unit `Cmd.now` produces) through the host's current locale and local time zone; `ok` carries localized UTF-8 bytes. This is deliberately an effect rather than a pure helper: locale/timezone are ambient state, so the observed bytes journal and replay without consulting the replay machine. Err reasons are `invalid_request`, `unsupported`, and `failed`.\n- `Cmd.delay(key, ms, \"fired\")` — a keyed ONE-SHOT timer: dispatches the named arm once, `ms` from now, with the fire time (ms) as its single number payload (the same arm shape `Cmd.now` and `Sub.timer` target). Re-issuing a live delay key re-arms it from now — that is the debounce discipline (`Cmd.delay(\"autosave\", 800, \"save_now\")` on every keystroke, one fire after the pause). `Cmd.cancel(key)` drops it silently. The interval is 1ms to one year; a literal outside that stops the build (NS1030).\n\nModel persistence is capability-gated. Without `\"persist\"`, NS1028 warns that `Cmd.persist()` has no linked host binding; declaring the capability without issuing the command produces the inverse warning. C","skill-data/native-ui/SKILL.md":"---\nname: native-ui\ndescription: Authoring guide for the Native markup (.native) view tier of native-rendered Native SDK apps. TypeScript cores are the default and use this skill together with ts-core; Zig cores are an explicit alternative and this guide labels their lower-level UiApp recipes. Use when building or modifying native UI (widgets, layout, bindings, messages), writing .native files, wiring views to Model/Msg/update, testing markup views, or verifying a native app through the automation harness.\n---\n\n# Author native UI with Native markup\n\nA native-rendered Native SDK app is a markup view plus an app core. The primary authoring path is:\n\n- `src/app.native` — the entire UI: elements, layout, bindings, message dispatch.\n- `src/core.ts` — `Model`, the discriminated `Msg` union, `update(model, msg)`, effects, subscriptions, and derived binding helpers.\n- `app.zon` — identity, windows, assets, permissions, and policy.\n\nThe TypeScript core is checked and compiled ahead of time to native code; no JS runtime ships in the binary. Load `native skills get ts-core` beside this skill for every default app implementation. A Zig core (`src/main.zig`) is first-class when explicitly selected with `native init --template zig-core`; the Zig-only wiring, builder, and runtime-extension recipes below are labeled so they are not mistaken for the default.\n\nThe markup compiles to the same widget tree a hand-written `canvas.Ui(Msg)` builder view would produce: identical structural widget ids, identical typed handler table. Markup can never mutate state — it binds values and dispatches messages; all logic lives in the app core, whichever core language the existing tree uses.\n\nEditors highlight `.native` markup well in HTML mode — the default scaffold writes no editor config, so add `.vscode/settings.json` with `\"files.associations\": {\"*.native\": \"html\"}` yourself, or scaffold with `native init --full`, which writes it.\n\nStart a new app with `native init` (zero-config: `app.zon` + `src/core.ts` + `src/app.native` + assets; the CLI generates the build graph). Use `examples/chatbot`, `examples/soundboard`, and `examples/system-monitor` as substantial TypeScript references. The `native check|dev|test|build` verbs drive any app directory shaped this way.\n\n## Menu-bar status items (TypeScript default)\n\nExport `statusItem(model: Model): StatusItemState` from `src/core.ts`. The generated launcher installs icon/tooltip/click-open commands plus the boot presentation/menu, re-derives it after every committed update, and patches presentation/menu independently. Import `StatusItemState` from `@native-sdk/core/events`. Commands route through the core's `commandMsg(name): Msg | null` export. There is no app-owned Zig wiring and `statusItem` is automatically marked shell-bound for the markup lint.\n\n```ts\nimport { asciiBytes, utf8Bytes } from \"@native-sdk/core\";\nimport { type StatusItemState } from \"@native-sdk/core/events\";\n\nexport function statusItem(model: Model): StatusItemState {\n  return {\n    iconPath: asciiBytes(\"assets/menu-bar.svg\"),\n    tooltip: utf8Bytes(\"Player status\"),\n    activationCommand: asciiBytes(\"app.refresh\"),\n    alternateActivationCommand: asciiBytes(\"player.toggle\"),\n    openCommand: asciiBytes(\"app.refresh\"),\n    presentation: { title: model.playing ? utf8Bytes(\"MB PLAY\") : utf8Bytes(\"MB\"), width: 62, tone: \"normal\", iconOpacity: 1, monospaced: true, fontSize: 13, fontWeight: \"semibold\" },\n    items: [\n      { id: 1, label: utf8Bytes(\"Open\"), command: asciiBytes(\"app.open\"), separator: false, enabled: true, detail: asciiBytes(\"\"), role: \"command\", key: asciiBytes(\"\"), modifiers: { primary: false, command: false, control: false, option: false, shift: false } },\n      { id: 0, label: asciiBytes(\"\"), command: asciiBytes(\"\"), separator: true, enabled: false, detail: asciiBytes(\"\"), role: \"command\", key: asciiBytes(\"\"), modifiers: { primary: false, command: false, control: false, option: false, shift: false } },\n      { id: 2, label: model.playing ? utf8Bytes(\"Pause\") : utf8Bytes(\"Play\"), command: asciiBytes(\"player.toggle\"), separator: false, enabled: true, detail: utf8Bytes(\"configured ✓\"), role: \"agent\", key: asciiBytes(\"\"), modifiers: { primary: false, command: false, control: false, option: false, shift: false } },\n    ],\n  };\n}\n```\n\nPresentation fields style the persistent menu-bar item. Inside its dropdown, `metric` carries prominent primary/secondary text, `segmented` carries up to eight typed command options, and `chart` carries 1–32 bounded values with captions and accessibility. These are generic composable row payloads, not text conventions.\n\n## App wiring (Zig cores and extensions only)\n\nA default TypeScript app writes none of this: generated wiring connects `src/core.ts`, `src/app.native`, and `app.zon`. Use the following only when the existing app has a Zig core or the task explicitly requires owned runtime wiring.\n\n```zig\nconst HabitsApp = native_sdk.UiApp(Model, Msg);\n\npub fn main(init: std.process.Init) !void {\n    // `create` heap-allocates the multi-MB app struct and constructs the\n    // Model in place — neither ever rides the stack (avoid `App.init(alloc,\n    // model, ...)`: its by-value Model is a stack-overflow trap once the\n    // Model grows).\n    const app_state = try HabitsApp.create(std.heap.page_allocator, .{\n        .name = \"habits\",\n        .scene = shell_scene,             // one window, one gpu_surface view\n        .canvas_label = \"habits-canvas\",  // must match the ShellView label\n        .update = update,\n        .markup = .{\n            .source = @embedFile(\"app.native\"),\n            .watch_path = \"src/app.native\", // dev hot reload; omit in release\n            .io = init.io,\n        },\n    });\n    defer app_state.destroy();\n    app_state.model = initialModel(); // boot state: assign through the pointer\n    try runner.runWithOptions(app_state.app(), .{ ... }, init);\n}\n```\n\n(`create` requires every Model field to carry a default; the model starts as `.{}` and boot state is assigned through the returned pointer. Tests that instantiate the app per fixture should use `create`/`destroy` too — a runtime-built Model passed to `init` by value crashes the test stack once models get large.)\n\nThe runtime owns the loop: install on first GPU frame, presentation, resize, pointer/keyboard dispatch into `update` + rebuild. With `watch_path` set, editing the `.native` file while the app runs hot-reloads the view within ~2s, preserving model state and widget ids; parse failures keep the last good view and set `app_state.markup_diagnostic` (line/column/message).\n\n**Release: compile the markup at comptime.** `canvas.CompiledMarkupView(Model, Msg, source).build` parses the `.native` source entirely at compile time and produces the identical tree (same ids, handlers, dispatch) with no parser in the binary; markup or binding mistakes become compile errors with line/column. Hand it to `.view`, and gate the runtime engine per build mode:\n\n```zig\nconst dev = @import(\"builtin\").mode == .Debug;\nconst App = native_sdk.UiAppWithFeatures(Model, Msg, .{ .runtime_markup = dev });\nconst CompiledView = canvas.CompiledMarkupView(Model, Msg, @embedFile(\"app.native\"));\n// options:\n.view = CompiledView.build,\n.markup = if (dev) .{ .source = ..., .watch_path = \"src/app.native\", .io = init.io } else null,\n```\n\nWith both set (dev), the compiled view renders until the watched file first changes, then the interpreter hot-reloads it. See the native reference fixture in `tests/ts-core/habits_reference.zig` for this Zig wiring pattern.\n\n### Webview panes: canvas + live web content in one window\n\nFor TypeScript apps, export `webPanes(model: Model): readonly WebViewPane[]` from `core.ts`. Import the descriptor from `@native-sdk/core/events`; return `label`, nullable `anchor`, `url`, `x`, `y`, `width`, `height`, and `reloadToken`. The label identifies an existing manifest WebView; the anchor identifies a widget label such as `<panel label=\"preview-pane\" grow=\"1\"/>`. See `examples/canvas-preview` for pane, frame, and tray coordination in one compiled core. The native adapter copies bytes and validates finite geometry and safe integer tokens; the OS owns WebView hosting and navigation policy.\n\nFor all u64 reload states, return `readonly ExactWebViewPane[]` instead. Its `reloadToken` is decimal ASCII bytes; the host validates the full u64 range without rounding. `WebViewPane` retains its existing safe-number token.\n\n### TypeScript terminal panes\n\nBind `<terminal pty=\"{shellKey}\" scrollback=\"{scrollback}\" on-terminal=\"terminalChanged\" label=\"Shell\" autofocus=\"true\" grow=\"1\"/>` to a `Uint8Array` helper or field naming the same key as `Cmd.ptySpawn`. Import `TerminalState` from `@native-sdk/core/events` for the message payload. The native host resolves the name to its PTY capability; engine identities never cross through a JavaScript number. Both view backends preserve the complete four-field state report. A bound slot remains reserved through exit until app teardown, keeping the ended screen stable across rebuilds. Restarting the same name reuses its slot; the existing four-slot budget applies. Keep `.terminal_sessions = true` and the lazy Ghostty pin in the app's owned build, as in `examples/workbench`. Native integer PTY bindings remain supported.\n\nFor Zig cores and extensions:\n\nDeclare the webview in the scene next to the gpu_surface (parent it to the canvas view), reserve its region with an empty panel carrying a semantics label, and let `Options.web_panes` snap the webview to that widget's layout frame while the model drives navigation:\n\n```zig\nconst shell_views = [_]native_sdk.ShellView{\n    .{ .label = \"app-canvas\", .kind = .gpu_surface, .fill = true, .gpu_backend = .metal },\n    .{ .label = \"preview\", .kind = .webview, .parent = \"app-canvas\", .url = \"https://example.com/\", .x = 240, .y = 76, .width = 704, .height = 548 },\n};\n// view: ui.panel(.{ .grow = 1, .semantics = .{ .label = \"preview-pane\" } }, .{})\nfn panes(model: *const Model, out: []App.WebViewPane) usize {\n    out[0] = .{ .label = \"preview\", .anchor = \"preview-pane\", .url = model.url(), .reload_token = model.reload_token };\n    return 1;\n}\n// options: .web_panes = panes,\n```\n\nURL changes navigate; bumping `reload_token` reloads the same URL (the CenterPane/Preview-tab shape). Pane URLs must pass `security.navigation.allowed_origins`. Panes reconcile against the runtime's live webview state on every rebuild and presented frame, so shell relayouts cannot detach them. `examples/canvas-preview` is the live reference; `zig build test-canvas-preview-smoke` verifies it.\n\n### Menu-bar extra (status item)\n\nThis is the Zig-core/custom-wiring form. Default TypeScript apps use the exported `statusItem(model)` helper above.\n\n`Options.status_item` installs a macOS `NSStatusItem` once, on the installing frame; its menu items dispatch commands through the same `on_command` mapping the toolbar and menus use (source `.tray`):\n\n```zig\n.status_item = .{ .title = \"ZN\", .tooltip = \"My App\", .items = &.{\n    .{ .id = 1, .label = \"Refresh\", .command = \"app.refresh\" },\n    .{ .separator = true },\n    .{ .id = 2, .label = \"Quit\", .command = \"app.quit\" },\n} },\n```\n\nFor a LIVE menu-bar extra (an open-count badge in the title, a latest-items dropdown), add `Options.status_item_fn` — the `web_panes` pattern: consulted on install and after every rebuild, re-applied only when its output changed (title and menu patch independently; the static `status_item` keeps icon/tooltip). Format derived strings into the provided scratch; item `command`s dispatch through `on_command` exactly like static items:\n\n```zig\nfn statusItem(model: *const Model, scratch: *App.StatusItemScratch) App.StatusItemState {\n    const title = std.fmt.bufPrint(&scratch.title_buffer, \"ZN {d}\", .{model.open_count}) catch \"ZN\";\n    scratch.items[0] = .{ .id = 1, .label = \"Refresh\", .command = \"app.refresh\" };\n    var count: usize = 1;\n    for (model.latest(), 0..) |issue, i| { // per-row commands: map \"issue.select.N\" in on_command\n        scratch.items[count] = .{ .id = @intCast(10 + i), .label = issue.title, .command = issue.select_command };\n        count += 1;\n    }\n    return .{ .title = title, .items = scratch.items[0..count] };\n}\n// options: .status_item_fn = statusItem,\n```\n\nTitle updates retitle the live `NSStatusItem` button without re-creating it; platforms without a tray-title seam keep menu updates and log the title gap once.\n\n### Windowed lists in markup\n\nUse `<virtual-window>` to resolve one retained range before rendering, then `<virtual-list>` to request only its rendered rows. The window declares a stable literal `id`, an `as` range name, an `item-count`, and either `item-extent` or an exported Model-first `extent-estimate` helper. Optional `gap`, `overscan`, `viewport-fallback`, `index-base`, and `anchor=\"leading|trailing\"` describe the complete content geometry.\n\n```html\n<virtual-window id=\"records\" as=\"range\" item-count=\"{loaded}\" item-extent=\"48\" overscan=\"4\">\n  <column>\n    <virtual-list window=\"range\" each=\"rows\" as=\"row\" grow=\"1\" label=\"Records\">\n      <list-item key=\"{row.id}\" focusable=\"true\" on-press=\"select:{row.id}\">{row.title}</list-item>\n    </virtual-list>\n    <status-bar>{range.first_visible_index}–{range.last_visible_index}</status-bar>\n  </column>\n</virtual-window>\n```\n\nIn a TypeScript core, `rows(model: Model, range: VirtualListRange): readonly Row[]` takes the complete SDK range from `@native-sdk/core/events` and returns exactly `range.end_index - range.start_index` rows in rendered-index order. An extent estimator has shape `rowExtent(model: Model, index: number): number`; its retained identity survives rebuilds. Each row has one keyed root. Ordinary scroll styling, `on-scroll`, and reach-start/end messages belong on `virtual-list`.\n\nEach window has one root and exactly one consuming list; windows cannot nest. A range can pass through a template argument, and secondary window views have their own retained descriptors. The full range exposes visible and rendered bounds, content extent, row extent, and offset; use it for footer text without copying scroll state into the model. See `examples/feed` for variable-height estimates, stable row queries, and retained selection.\n\n### Native scrolling (macOS)\n\nZero app code: on macOS every non-virtualized `scroll` region — and every windowed virtual list (`ui.virtualList`), whose driver content size is the full virtual extent — is driven by an invisible `NSScrollView` — OS momentum and the system overlay scrollbar — while the engine renders the content. `widget.value` stays the offset of record, so the rebuild reconcile rule (\"user offset survives rebuilds until the source offset changes\"), automation snapshot offsets (`scroll=[offset=..]`), and `Options.sync` all work exactly as before; the engine-drawn scrollbar simply stops painting for natively driven regions. Programmatic scrolls still work: change the source offset (or scroll via keyboard/automation) and the runtime pushes it into the native scroller. GTK/Win32 and mobile embeds keep the engine's wheel physics unchanged. Nested-scroll saturation handoff (inner region exhausted, outer continues) is per-region native today: the inner region stops at its edge like a standalone scroller.\n\n**Overscroll is off by default, per region, on both paths.** Scroll regions pin at their content edges — the native scroller gets non-elastic edges, the engine's wheel/kinetic physics clamp, and kinetic motion stops cleanly at the boundary. Bouncing is a per-region opt-in: `overscroll=\"rubber_band\"` in markup (the `scroll` element only — the validator rejects it elsewhere with a teaching error) or `ElementOptions.overscroll = .rubber_band` in Zig views. The `ScrollPhysics.overscroll` design token (`ScrollPhysicsOverrides` in a theme) flips the app-wide default; per-region values override it, and `.none` pins a region regardless of the token. The rubber-band shape — excursion bound, resistance, spring-back rate — stays themable through the `rubberband_*` physics tokens.\n\n### Context menus: one declared menu, platform-decided presentation\n\nAuthors write ONE menu; the platform decides how it presents. The default is the real OS context menu at the pointer — `NSMenu` on macOS, `TrackPopupMenu` on Windows, `GtkPopoverMenu` on Linux — and the selection dispatches the item's typed `Msg`. On hosts without a native menu presenter (the mobile toolkit hosts and embed hosts today), the SAME declared items present automatically as an anchored canvas surface at the click point, with the standard anchored-surface behavior (Escape and outside-click dismiss, late z-pass, window clipping). Never two authored menus, never a canvas imitation where the OS menu exists.\n\nMarkup declares the menu as a `<context-menu>` element — a DIRECT child of the interactive element whose right-click it answers (a hit-target kind, or an element made interactive by `on-press`/`on-double-press`/`on-toggle`/`on-hold`/`on-drag`). It is metadata, not content: it renders nothing in the row's flow. Children are `menu-item`s (`on-press` required, `disabled` optional, the text content is the label) and bare `<separator/>`s, with `if`/`else`/`for` around them to swap or repeat items — a menu whose items all evaporate at runtime simply declares no menu (the All Notes row pattern). Conditional MENUS are spelled as conditional ITEMS: the `<context-menu>` itself takes no attributes and cannot sit behind a structure tag. No submenus: the platform channel carries flat items (label, enabled, separator) only.\n\n```html\n<list-item on-press=\"open_note:{n.id}\" label=\"{n.title}\">\n  <text grow=\"1\">{n.title}</text>\n  <context-menu>\n    <if test=\"{n.deleted}\">\n      <menu-item on-press=\"restore_note:{n.id}\">Restore</menu-item>\n      <menu-item on-press=\"purge_note:{n.id}\">Delete Permanently</menu-item>\n    </if>\n    <else>\n      <menu-item on-press=\"copy_note_id:{n.id}\">Copy</menu-item>\n      <menu-item on-press=\"trash_note:{n.id}\">Delete</menu-item>\n    </else>\n  </context-menu>\n</list-item>\n```\n\nThe compiled TypeScript view supports one direct menu with at most 32 labeled `menu-item` leaves and bare separators. Items require `on-press` and may bind `disabled`; structure tags inside menus currently remain on the native markup backend. The menu is copied into the native tree arena before the compiled result expires.\n\nThe Zig builder's mirror is `ElementOptions.context_menu` — per-widget items in the chrome-menu shape with typed messages:\n\n```zig\nui.listItem(.{\n    .on_press = Msg{ .select = entry.index },\n    .context_menu = &.{\n        .{ .label = \"Open Section\", .msg = Msg{ .select = entry.index } },\n        .{ .separator = true },\n        .{ .label = \"Refresh Dashboard\", .msg = .refresh },\n    },\n}, entry.title)\n```\n\nThe deepest declaring widget on the hit route wins; disabled items and separators are fine (`enabled = false`, `.separator = true`). Zero-code defaults need no declaration: editable text fields present the standard Cut / Copy / Paste / Select All menu wired to the existing clipboard actions, and a selected static text presents Copy (these defaults are presenter-only — without an OS menu they degrade to the keyboard clipboard paths). Touch long-press is design-noted for the mobile embeds: the iOS host's under-slop `Pending` touch state is the timer seam, pending a secondary-button leg in the embed ABI and `UIEditMenuInteraction` presentation.\n\nAutomation drives the native path honestly: snapshots list every widget's declared items in invocation order (`context_menu=[\"Rename\",\"Delete\"]`, separators keep their slots, disabled items say so), `widget-context-press <view> <id>` performs the real secondary click (presenting the menu), and `widget-context-menu <view> <id> <item-index>` invokes an item — the selection dispatches as the same `context_menu_action` platform event a real pick produces; the journal records the complete widget invocation so replay reconstructs its pending request before selection, because the OS menu's tracking loop cannot be driven programmatically. Dead invocations fail by name (undeclared menu, index out of range, separator slot, disabled item). `examples/notes` row menus and `examples/gpu-dashboard` nav rows carry live menus; `zig build test-example-notes`, `test-example-gpu-dashboard`, and the runtime context-menu suite verify dispatch, the fallback surface, and the verb.\n\n## Elements\n\n| Markup | Widget | Notes |\n| --- | --- | --- |\n| `row`, `column` | flex containers | main axis horizontal / vertical |\n| `stack`, `panel`, `card` | overlay containers | children stack on top of each other — `gap` can never space them and is a validation error (put a `column`/`row` inside for flow) |\n| `scroll` | scroll_view | wrap multiple children in a `column` inside it |\n| `list`, `grid` | list, grid | vertical stack / cell grid |\n| `tabs`, `toggle-group`, `button-group`, `radio-group`, `breadcrumb`, `pagination` | row containers | children flow horizontally. Give every `radio-group` an accessible `label`; it is one logical `radiogroup`: descendant radios at any nesting depth share one Tab stop; arrows wrap while Home/End move to the edges, focus and selection move together, and selecting one clears the rest of the nearest group scope |\n| `table` > `table-row` > `table-cell` | table, data_row, data_cell | rows only inside a table, cells only inside a row (for/if wrappers are fine); cells are text leaves, dispatch with `on-press` |\n| `dropdown-menu` | dropdown_menu | vertical menu surface; children are `menu-item`s. `anchor=\"below\\|above\"` floats it against its PARENT's frame (see Pickers): late z-pass above the whole tree, window-clipped, auto-flipping at the window edges, zero flow space. Pair with `on-dismiss` |\n| `accordion` | accordion | header via `text` attr; children show while `selected`, dispatch `on-toggle` |\n| `alert`, `bubble` | surfaces | `alert` title via `text` attr; children stack inside. `bubble` hugs its message up to 80% of the thread (`ghost` exempt; explicit `width` wins) and takes one `<reactions>` child — the reaction pill straddling its bottom edge, one text run, dock via `text-alignment` (default `end`); `text=` on bubble itself is a teaching error (that channel belongs to the pill). Grouped runs are spacing, not vocabulary: 8 gap within a sender's run, 32 between turns |\n| `dialog`, `drawer`, `sheet` | modal surfaces | root-relative and unaffected by ancestor scroll or clipping: dialogs center in the viewport, drawers span its width and dock to the bottom, and sheets span its height and dock to the right — title via `text` attr, wrap in `<if>` to show conditionally |\n| `resizable` | resizable | engine-managed drag handle; `width` sets the initial width |\n| `split` | split | two-pane horizontal splitter: exactly two element children (nest splits for more panes), the engine synthesizes the draggable divider between them. `value` binds the model-owned first-pane fraction (0 lays out at 0.5), `on-resize` names an f32 Msg variant dispatched with every applied fraction (echo it back through `value` — see Splitters), `min-width` on the panes bounds the drag, `gap` sets the divider band thickness. The divider is focusable: Left/Right (Shift for bigger steps) adjust, Home/End jump to the clamp edges |\n| `tree` | tree | disclosure-tree container (vertical flow): descendant rows carrying `role=\"treeitem\"` — at ANY nesting depth — form one roving keyboard focus set with the ARIA tree keymap. Up/Down walk visible rows (selection follows focus through each row's `on-press`), Left collapses an expanded row or moves to the parent row, Right expands a collapsed row or moves to the first child row, Home/End jump to the edges, Enter/Space activate. Expandable rows bind `expanded` and `on-toggle`; the model owns selection and expansion (collapsed children are simply not rendered) |\n| `text`, `badge`, `tooltip` | text leaves | text content, `{}` interpolation allowed; `text` line policy via `wrap` (`\"true\"` word-wraps; `\"false\"`/unset paint one honest line, overflow eliding by default — `overflow=\"clip\"` opts out), and `text` alone takes the typography rungs `size=\"heading\"`/`size=\"display\"` (themable token steps above title — section headings, hero stats, timer numerals). `tooltip` with `anchor=\"above\\|below\"` floats against its parent (the stack wrapping trigger + tooltip, the dropdown pattern) and the RUNTIME owns its visibility — hover intent on the trigger: shows after `tooltip-delay` ms (default 600; `\"0\"` = instant) and immediately on keyboard focus; hides on leave, focus departure, Escape, or a press of the trigger (a press also closes the warm window), and a shared 400ms warm window after a pointer-hovered tooltip hides on leave (the only hide that warms) shows the next trigger's tooltip instantly; the model never hears hover. These are shadcn/ui's defaults (Base UI). Without `anchor` it stays a static leaf that paints whenever the view renders it |\n| `text` > `span` | inline styled runs | mixed-style text in ONE wrapped paragraph: span children style runs with `weight=\"regular\\|medium\\|bold\"`, `mono`, `italic`, `scale` (a positive multiplier on the paragraph's base size — inline headings, hero stats), `underline`, `foreground` (token name); `{bindings}` interpolate inside spans; whitespace between runs collapses to a single space (none = the runs abut); spans do not nest, take no events, and the paragraph announces as one text run — see \"Rich text\" |\n| `button`, `toggle-button`, `list-item`, `menu-item`, `toggle`, `switch`, `select`, `avatar` | text-bearing controls | label is the text content; `button`, `toggle-button`, `list-item`, and `menu-item` also take `icon=\"save\"` — a vector icon drawn inline (buttons/toggle-buttons before the label, icon-only when the content is empty: add a `label`; list/menu items as a leading slot), ONE hit target whose icon follows the element's enabled/disabled tint (no overlay stacking, no duplicated `on-press`); tab strips are `toggle-button` children, so tabs get icons this way; `select` shows `placeholder` while empty and dispatches `on-press`; `avatar` renders initials, or a runtime image via `image=\"{binding}\"` (see the Images section) |\n| `segmented-control` | exclusive-choice control | standalone text-bearing segment; bind `selected`, dispatch `on-press`, and optionally add a vector `icon`; compose several in a row, or use `tabs` for the strip-and-panel pattern |\n| `checkbox`, `radio`, `slider`, `progress` | value controls | `checked`, `value` (a 0..1 fraction on slider and progress; progress clamps out-of-range values at render, never an error); checkbox/radio visible labels are text content (`<checkbox>Done</checkbox>`), with `text=\"...\"` as the equivalent binding-friendly form and `label=` alone naming one for accessibility without drawing a label; a radio selection TRANSITION (pointer, Space/Enter, grouped navigation, or accessibility selection) dispatches `on-change` when bound, then `on-toggle`, then `on-press` for compatibility — reactivating the already-checked radio has no new `on-change` edge, though a legacy fallback still receives the activation; a slider's `value` follows the source when it MOVES (model-driven progress renders every rebuild) and keeps the user's drag while the source replays the same value — use `slider` for seek bars, `progress` for display-only; a markup slider's `on-change` dispatches a PLAIN Msg with no value payload — mirror the applied value into the model with `Options.sync` (the Zig builder's `on_value = Ui.valueMsg(.tag)` does deliver the applied f32) |\n| `text-field`, `input`, `search-field`, `combobox`, `textarea` | text entry | `placeholder`; edits via `on-input`, enter via `on-submit` on single-line kinds; in a default `textarea`, Enter (and Shift+Enter) inserts a newline and `on-submit` dispatches on primary+Enter (cmd on macOS, ctrl elsewhere). A chat composer opts into `submit-on-enter=\"true\"`: plain Enter submits, Shift+Enter remains a newline, and the primary chord still submits. `search-field` carries a built-in trailing clear affordance whenever it holds text (press the x, or Escape while focused — both clear through the text-edit path, so `on-input` hears it; no attribute, no external Clear button needed) |\n| `status-bar` | status bar | text leaf: content only, no children |\n| `separator`, `spacer` | separator, flexible space | `separator` is axis-aware: a horizontal rule in a `column`, a thin vertical divider in a `row`; give `spacer` a `grow` |\n| `skeleton`, `spinner` | loading leaves | size `skeleton` with `width`/`height` |\n| `icon` | vector icon leaf | `name` picks the icon: a bare literal is a curated built-in stroke icon (compile-checked; 49 names: search, plus, x, x-circle, check, check-circle, chevron-up/down/left/right, arrow-up/down/right, menu, panel-left, panel-right, settings, terminal, wrench, trash, edit, copy, external-link, play, pause, skip-back/forward, shuffle, repeat, music, volume, info, alert, download, save, folder, folder-open, file-text, sun, moon, eye, clock, git-pull-request, git-merge, git-branch, circle-dot, archive, refresh-cw, send); `app:<name>` reaches an icon the app registered at boot with `canvas.icons.registerAppIcons` (declare the table as `pub const app_icons` on the app root so `native check` verifies the name against the model contract), and one `{binding}` defers the choice to model data - an unknown resolved name draws the missing-icon fallback (a slashed circle) with a Debug warning naming the value; tint with `foreground`, size with `width`/`height` |\n| `media-surface` | media surface leaf | composites a texture produced OUTSIDE the widget tree (video decoder, camera, an external renderer like mpv) into the layout like any widget — clipped, z-ordered, rounded. `surface=\"{binding}\"` (required) binds the model-owned u64 surface id a Zig-tier producer targets (`runtime.acquireMediaSurfaceProducer` pushes RGBA8 frames, latest-wins, paced by the presented-frame clock; 0 = unbound, draws nothing; usable ids are nonzero values below the reserved bit 63). No intrinsic size — give it `width`/`height` or `grow`; display-only (presses fall through); `label` it (pictorial content). Texture contents are presentation chrome: goldens, reference screenshots, and session replay show the deterministic id-derived placeholder, never producer frames |\n| `image` | runtime image leaf | draws a RUNTIME-REGISTERED image by its model-owned u64 ImageId — the id `Cmd.imageLoad` (TS) or `fx.loadImage`/`fx.registerImageBytes` (Zig) registered pixels under. `image=\"{binding}\"` (required) binds a model field/fn; ids are model data, never markup literals, and 0 draws nothing (store the id only when the load reports loaded — see the Images section). `source-x`/`source-y`/`source-width`/`source-height` select one atlas region in decoded-image pixel coordinates (declare all four). No intrinsic size — give it `width`/`height` or `grow`; display-only (presses fall through); `label` it (pictorial content) |\n| `code` | bare highlighted source/editor | `source=\"{binding}\"` (required) provides source text and `language=\"tsx\"` selects a literal lexer name; the component supplies no background, border, radius, shadow, or padding, so wrap it in a panel/card when chrome is wanted. It is read-only by default; `editable on-input=\"edit\"` opts into multiline editing while retaining highlighting. It wraps by default, `line-numbers` opts into logical line numbers, `added-lines=\"5\"` / `removed-lines=\"2-4\"` add Geist-style diff rows without changing copied source, and `wrap=\"false\"` preserves lines inside one horizontal scroll region. HTML-family highlighting distinguishes HTML/XML/SVG and JSX/TSX tags, attributes, strings, comments, and embedded expressions. Zig builder: `ui.code(CodeOptions, source)` |\n| `markdown` | rendered markdown subtree | leaf; `source` is one `{binding}` — see \"Markdown in markup\" |\n| `stepper` > `step` | composite stage track | `active=\"{index}\"` (required) derives each step's completed/active/pending state; steps are text leaves (no attributes) joined by connectors; stepper also takes `key`, `global-key`, `label` |\n| `timeline` > `timeline-item` | composite ledger list | items only inside a timeline (for/if fine); items are leaves — `title` (required), `description`, `meta`, `indicator`, `variant`, `connector=\"false\"` on the last item, `selected`; `on-press` makes the whole item pressable with a trailing chevron |\n| `chart` > `series` | composite data chart | series only inside a chart, and only series (the set is static — data varies through bindings); each series is a leaf — `values=\"{binding}\"` (required) names a model `[]const f32` iterable, `kind` is `line`/`area`/`bar` (literal), `color` a token name, `label` the semantics name; chart takes `y-min`, `y-max`, `grid-lines`, `baseline`, `x-labels`, `y-labels`, `hover-details`, `stroke-width`, box options, `label` — see \"Charts\" |\n| `context-menu` | consumed by its parent | right-click menu on its DIRECT parent (a hit-target kind or an element with `on-press`/`on-double-press`/`on-toggle`/`on-hold`/`on-drag`); metadata, never a flow child. Children: `menu-item`s (`on-press` required, `disabled` optional, no `icon`) and bare `separator`s, with `if`/`else`/`for` around them. Attribute-less; presents natively where the host has a menu presenter, as an anchored surface elsewhere — see \"Context menus\" |\n| `input-group` > `textarea` + `input-group-actions` | composite grouped input | the composer shape: ONE bordered field wrapping exactly one `textarea` (first — document order is focus order) plus an optional `input-group-actions` row of controls inside the same border. The group wears the focus ring for its focused descendant and the textarea's own chrome dissolves automatically, so the whole group reads as one field; the textarea keeps its full behavior (`text`, `placeholder`, `on-input`, `on-submit`, `autofocus`, and optional `submit-on-enter`). Group takes `label`, `width`, `height`, `min-width`, `grow`, `key`, `global-key`; the actions row takes `gap` and holds ordinary elements (`if`/`else`/`for` work — swap send for stop while streaming) — put a `<spacer grow=\"1\"/>` between leading and trailing controls (`Ui.inputGroup`/`Ui.inputGroupActions` are the Zig-view equivalents) |\n\nCapabilities still awaiting a separate admission or ruling are image-backed `icon_button` content, `data_grid` per-column cell templates, and `popover`/`menu_surface` anchoring. `segmented-control` is a first-class markup leaf, and vector icon buttons use `<button size=\"icon\" icon=\"...\" label=\"...\" on-press=\"...\" />`. Charts ARE expressible: `<chart>` with `<series values=\"{binding}\">` children binding model f32 iterables — see the Charts section (`.band` series and dynamic series composition stay with `ui.chart`). Built-in vector icons ARE expressible: `<icon name=\"search\"/>` (closed, compile-checked name set; `Ui.icon` is the Zig-view equivalent). App-authored icons: `canvas.svg_icon.parseComptime(@embedFile(\"icons/logo.svg\"))` parses any SVG in the common 24x24 stroke-icon dialect at comptime; register the parsed table once at boot with `canvas.icons.registerAppIcons(&table)` and draw by name via `ui.appIcon(.{...}, \"logo\")` or `ElementOptions.icon` — registered names render exactly like built-ins on every draw path. Markup `<icon>`/`<button icon>` stay built-in-only (the compiled engine validates names at comptime, where runtime registrations cannot exist — engine parity). Runtime images ARE expressible: `<image image=\"{cover}\" width=\"120\" height=\"80\" label=\"Cover art\"/>` and `<avatar image=\"{user_image}\">CT</avatar>` bind a `u64` ImageId model field/fn (the id is just model data; 0 draws nothing / keeps the initials fallback) — see the Images section; the `image` binding is required on the leaf (an unbound `<image>` is dead markup) and stays avatar+image scoped. Add all four `source-x`/`source-y`/`source-width`/`source-height` attributes to draw one decoded-pixel sub-rectangle from a shared atlas.\n\n## Attributes\n\nLayout: `gap` (flow containers only — stacking containers `stack`/`panel`/`card`/`alert`/`bubble`/`dialog`/`drawer`/`sheet`/`resizable` layer their children, so `gap` there is a validation error, not silence: wrap the children in a `column`/`row` inside; on `split` it sets the divider band thickness), `padding` (uniform), `grow`, `width`, `height` (definite: the element is exactly that size — intrinsic content neither shrinks nor silently overflows it; `resizable` treats `width` as the initial width), `min-width` (a floor WITHOUT `width`'s definite max — the element may grow past it but never shrink below; on split panes it bounds the divider drag), `max-width` (a ceiling WITHOUT `width`'s definite min — the element still shrinks with a narrow parent; use a growing child inside a centered row for a responsive content column), `wrap` (`text` only: `wrap=\"true\"` word-wraps at the width the element receives and reserves the wrapped height in columns; `wrap=\"false\"` and unset are honest single-line — one line whose overflow follows `overflow`), `overflow` (`text` only, a teaching error elsewhere: what a single line does with content that does not fit — `ellipsis`, the default, elides behind a trailing … measured with the same metrics paint uses, right for width-constrained list-row titles; `clip` hard-cuts at the frame for fixed-format content like a duration column where \"1…\" beats nothing; there is deliberately no overflow-visible), `text-alignment` (start|center|end — text leaves, status bars, surface titles; controls that own their label placement ignore it), `columns` (`grid` only: fixed column count, omit for the derived near-square grid; a teaching error elsewhere), `main` (start|center|end|space_between), `cross` (stretch|start|center|end), `virtualized`, `virtual-item-extent`, `anchor` (`dropdown-menu` and `tooltip`, literal `below`/`above`: floats the surface against its parent instead of the flow — auto-flips when the preferred side does not fit, height clamps to the chosen side, x clamps into the window; an anchored tooltip's visibility is runtime-owned hover intent, unlike the model-owned dropdown), `anchor-alignment` (with `anchor`: `start`/`end`/`stretch` — stretch also widens the surface to at least the anchor's width, the select-menu look), `anchor-offset` (with `anchor`: literal gap in points, default 4), `tooltip-delay` (`tooltip` only, beside `anchor` — a teaching error elsewhere or without it: hover-intent show delay in ms, default 600, `\"0\"` = instant; keyboard-focus reveals are always immediate), `overscroll` (`scroll` only, a teaching error elsewhere: `none` pins the region at its content edges — the shipped default via the `ScrollPhysics.overscroll` token — `rubber_band` lets it bounce past them on both the engine and native paths, `default` follows the token).\nAppearance/state: `variant` (default|primary|secondary|outline|ghost|destructive), `size` (the control scale default|sm|lg|icon on every sized element; on `text` also the typography rungs heading|display — named typography token steps (`heading_size` 28, `display_size` 48, themable like every token) for section headings and hero stats/timer numerals. The two axes stay apart: heading/display on a control is a teaching error naming text as their home, unknown values list the vocabulary, and numeric sizes are refused by design — retheme the typography tokens to move the whole scale), `disabled`, `checked`, `selected`, `value`, `placeholder`, `icon` (`button`, `toggle-button`, `list-item`, `menu-item`: vector icon drawn inline — buttons/toggle-buttons before the label, list/menu items as a leading slot; a teaching error anywhere else. A built-in name, `app:<name>`, or one `{binding}` resolving to such a name). **One size register per row**: every control class shares the control height at a given register (default 36, sm 31.5, lg 40.5 before density), so a toolbar/filter row reads as one height exactly when every control in it carries the SAME `size` — mixing `size=\"sm\"` buttons with a default field renders two heights in one row, and hand-sized pressable panels (`height=\"30\"`) never land on the scale; compose rows from real controls at one register.\nFocus: `autofocus` (focusable controls only — a teaching error elsewhere): moves keyboard focus to the element when it MOUNTS or when the bound value turns on, edge-triggered so holding it true never re-steals focus from the user. The TEA way to focus an editor on note-create (`<text-field autofocus=\"{editing}\" ...>` or mount the field under an `<if>` with `autofocus=\"true\"`; Zig views use `ElementOptions.autofocus`) and to give keyboard-first apps their first focus without a click.\nSemantics: `role` (listitem, treeitem, button, ...; `treeitem` also makes the row part of its tree's roving keyboard focus set), `label` (accessible name — it REPLACES the element's text content as the announced name, so snapshot greps and screen readers see the label, never the text; don't `label` an element whose visible text you grep for), `expanded` (tree rows: disclosure state, model-owned — omit on leaves). Accessible names are ENFORCED: an interactive control with no text content, no `text=`, and no `label=` is a validation error (icon-only controls need `label`; text-entry controls need `label` or `placeholder`), unknown/misused literal roles are errors (`role=\"tree\"` on a text leaf can never hold rows), unnamed avatars and labels duplicating the text content are warnings (`label=\"\"` marks an image decorative). Zig-built trees get the same discipline from `canvas.expectA11yAuditSweepClean` (missing names as the bridges would announce them, focusables clipped out of keyboard reach, identically labeled siblings) — adopt it next to the layout sweep.\nIdentity: `key` (sibling-scoped), `global-key` (parent-independent — use for items that move between containers, e.g. board cards; ids then survive reparenting).\nWindow chrome: `window-drag=\"true\"` (Zig: `.window_drag = true`) marks the element as a window-drag surface for hidden-titlebar windows — pressing its background or plain text/icons inside moves the WINDOW (drag starts only on actual movement), double-click zooms per the OS convention, and press-claiming children (buttons, fields) stay fully interactive via the ordinary press fall-through. macOS-only; elsewhere the press is dead space. See \"Hidden titlebar\" below.\nRender channel (Zig-only, no markup attributes): `ElementOptions.opacity` and `ElementOptions.transform` wrap the element's emitted commands without reflowing siblings — the defaults (1, identity) emit nothing, opacity 0 culls painting (pair with `disabled` when fading interactive content), and a transform moves both rendering and pointer hit-testing while accessibility frames stay at the layout frame. Pair with `UiApp.Options.animations` for tweening.\n\nNumbers are plain (`gap=\"12\"`), booleans are `true`/`false` or a binding.\n\nWhen children's minimum sizes exceed their container, debug builds log a `zero_canvas_layout` diagnostic naming the container, axis, and overflow in pixels — flex overflow is never silent. In Zig views, `.gap` on a stacking kind (`ui.panel(.{ .gap = 8 }, ...)`) logs a `zero_canvas_ui` warning in debug builds with the same lesson — it never fails the build.\n\n### Chips: exclusive selection with `selected=`\n\nThe chip pattern — an exclusive group where the model owns which one is active — is a `toggle-group` of `toggle-button`s (or plain `button`s) whose `selected=` binds the model:\n\n```html\n<toggle-group gap=\"2\" label=\"Theme\">\n  <for each=\"theme_prefs\" as=\"p\">\n    <toggle-button size=\"sm\" selected=\"{p == theme_pref}\" on-toggle=\"set_theme:{p}\">{p}</toggle-button>\n  </for>\n</toggle-group>\n```\n\nA `toggle-button` whose source asserts `selected` (this rebuild or the previous one) is model-driven: the source wins over the runtime's retained toggle on every rebuild, so exactly the model's selection is active — pressing a chip dispatches the Msg, the model moves the selection, and the old chip deactivates. Without a `selected=` that ever asserts, a `toggle-button` is uncontrolled: the runtime retains its pressed state across rebuilds (the multi-select formatting-bar case — bold/italic chips with zero app wiring). `button` with `selected=` is always model-driven (buttons never retain state) and dispatches `on-press`; `toggle-button` dispatches `on-toggle` (its activation is the toggle intent — an `on-press` there never fires). Model-driven chips need a handler that actually moves the model: a chip whose Msg is ignored keeps its retained press until the model asserts its `selected=`.\n\n### Pickers: select is the trigger — compose the options as an ANCHORED dropdown\n\n`select` and `combobox` are trigger controls, not complete pickers: `select` renders the closed dropdown shape (current value as content, `placeholder` while empty, `on-press` to open) and `combobox` is a text entry with a menu chevron — neither owns an options list. There is no `options=` attribute (the closed grammar has no list-valued attributes — list-shaped vocabulary is element children, the way `<context-menu>` declares its items); the options ARE the composition — a `dropdown-menu` of `menu-item`s under an `if`, beside the trigger inside a `stack`, floated with `anchor`:\n\n```html\n<stack>\n  <select placeholder=\"Pick a repo\" text=\"{current_repo}\" on-press=\"toggle_repo_picker\"/>\n  <if test=\"{repo_picker_open}\">\n    <dropdown-menu anchor=\"below\" anchor-alignment=\"stretch\" on-dismiss=\"close_repo_picker\">\n      <for each=\"repos\" key=\"name\" as=\"r\">\n        <menu-item on-press=\"pick_repo:{r.name}\" selected=\"{r.name == current_repo}\">{r.name}</menu-item>\n      </for>\n    </dropdown-menu>\n  </if>\n</stack>\n```\n\nHow the pieces fit, all model-owned (TEA):\n\n- **Open state is the model's.** `toggle_repo_picker` flips the bool; the surface exists only while the `if` renders it. There is no hidden engine open flag.\n- **`anchor` floats the menu.** The dropdown positions against its PARENT's frame (the `stack`, sized by the trigger): below it by default, flipping above when it doesn't fit and the other side has more room, height clamped to the chosen side, x clamped into the window. It consumes NO space in the flow (siblings never reflow), paints in a late z-pass above the whole tree, and escapes every ancestor scroll/clip region — window-clipped, not pane-clipped. `anchor-alignment=\"stretch\"` widens it to at least the trigger's width (the select look).\n- **`on-dismiss` closes it model-side.** Escape and a click outside the menu dismiss the surface and dispatch the Msg; `close_repo_picker` clears the bool. Escape works even when the trigger took no focus (a plain-text crumb): with no relevant focus chain it dismisses the topmost mounted anchored surface. The engine hides the surface immediately (the optimistic echo), and the next rebuild's source tree is truth — a model that keeps `open` true gets it back. Clicking the TRIGGER while open never double-fires: the anchor region owns its surface's toggling, so only `toggle_repo_picker` dispatches.\n- **Items close on pick.** `pick_repo` sets the value AND clears the open flag — a click inside the surface never dismisses.\n- **Keyboard**: ArrowDown/Up on a trigger with a mounted menu enters its selected visible row, or the first/last eligible row. Arrows move without wrapping; Home/End choose the visible edges. Enter/Space activate items. Tab/Shift+Tab dismiss without committing and return focus to the trigger; the next Tab continues through ordinary controls. Escape also dismisses from the trigger or menu and returns focus.\n- **Automation sees everything**: the floating menu and its items appear in widget snapshots at their real frames and `widget-click <item-id>` works while it is open.\n\n`combobox` composes the same way (the model filters the `for` source as the user types via `on-input`). Markup also supports `<popover>` and `<menu-surface>` with the same anchor and dismissal attributes. The Zig mirror is `ElementOptions.anchor`/`anchor_alignment`/`anchor_offset` + `on_dismiss` on `dropdown_menu`, `popover`, or `menu_surface`, built with `ui.eachCtx` for the options. Budget: at most 16 anchored surfaces may be mounted per view (`max_canvas_widget_anchored_per_view`, loud `error.WidgetAnchoredSurfaceLimitReached`) — an `anchor` inside a `<for>` body is almost always a mistake.\n\n### Splitters: split panes with a model-owned fraction\n\n`split` is the resizable two-pane seam: exactly two element children, and the engine synthesizes the draggable divider between them (resize cursor, focusable, ARIA separator whose value is the fraction). The fraction is MODEL-OWNED — the runtime applies each drag/keyboard step as an optimistic echo, dispatches `on-resize` with the applied fraction, and the model echoes it back through `value` so the next rebuild lays the panes exactly there:\n\n```html\n<split value=\"{sidebar_split}\" on-resize=\"sidebar_resized\">\n  <column min-width=\"150\">…sidebar…</column>\n  <split value=\"{list_split}\" on-resize=\"list_resized\">\n    <column min-width=\"220\">…list…</column>\n    <column min-width=\"280\">…editor…</column>\n  </split>\n</split>\n```\n\n- **`on-resize` names an f32 Msg variant** (`sidebar_resized: f32`); `update` stores it (`model.sidebar_split = fraction`). The delivered fraction is the value the runtime already applied and clamped, so echoing it never fights the reconcile.\n- **`min-width` on the panes bounds the drag** — the divider clamps so neither pane shrinks below its floor, on drag, keyboard, and layout alike.\n- **Uncontrolled works too**: without `on-resize`, the divider position survives rebuilds under the source-wins reconcile (a source-side `value` change wins), but pane CONTENT lays out at the declared fraction until the model echoes — bind the handler for the exact controlled loop.\n- **Keyboard**: Tab reaches the divid","AGENTS.md":"# Agent Guide\n\nGuidance for agents (and humans) working on this repository.\n\n## App authoring default\n\nNative SDK itself is implemented in Zig, but Native SDK **apps are authored in TypeScript + Native markup by default**. Do not infer the app-authoring language from this repository's implementation language or from older Zig-core examples.\n\n- For a new app, use `native init <path>` and expect `src/core.ts`, `src/app.native`, and `app.json`. `app.zon` remains a supported legacy/alternative manifest. Ordinary compiled TypeScript work that needs filesystem, process, JSON, regex, classes, or other static-tier APIs belongs under optional `src/services/`, reached from the core with `Cmd.request`; do not import a service from the core. Do not add Zig app code unless the user explicitly chooses `--template zig-core` or the feature requires a toolkit extension.\n- Before changing an existing app, inspect its tree. A `src/core.ts` app stays TypeScript; a `src/main.zig` app stays Zig unless the task is specifically a migration.\n- For default app work, read `skill-data/native-ui/SKILL.md` and `skill-data/ts-core/SKILL.md`; also read `skill-data/ts-services/SKILL.md` when the tree has `src/services/` or the task needs ordinary TypeScript beyond the core subset. `skill-data/core/SKILL.md` covers shared/runtime concerns; `skill-data/zig/SKILL.md` is for Zig-core apps and SDK implementation work.\n- The `-ts` suffix on a few examples only distinguishes ports from older Zig originals. New TypeScript apps need no suffix because TypeScript is the default.\n\n## Build, test, and gate\n\n```bash\nzig build test                # root engine + runtime suites\nzig build validate            # framework's legacy app.zon manifest check\nzig build test-example-<name> # one example's suite (e.g. test-example-notes)\nscripts/gate.sh fast [ref]    # affected-only local gate for your diff (default base: main)\nscripts/gate.sh full          # everything CI-shaped that runs locally\n```\n\nRun `scripts/gate.sh fast` before finishing any change; it maps your diff to the suites that cover it. The docs site checks with `pnpm --dir docs check` (the gate runs it only when `docs/` changed).\n\nThis repository builds with Zig 0.16.0. If a build fails with \"no member named\" errors on std APIs (`std.fs.cwd`, `ArrayList.init`, `std.io`), you are writing pre-0.16 idioms — `skill-data/zig/SKILL.md` maps each such compile error to the current idiom as this codebase writes it.\n\nPinned goldens (pixel signatures, schema fingerprints, command counts) are updated deliberately: review the rendered output or the counted commands first, and keep the pin's comment a self-contained description of what the value represents.\n\n## Changelog\n\nDo not edit `CHANGELOG.md` as part of regular feature or fix work. The release agent reviews the git history since the previous release and writes the complete changelog entry during release preparation; see [RELEASING.md](./RELEASING.md).\n\n## Where things live\n\n- `src/` — the engine and runtime; `src/primitives/canvas/` holds the widget, markup, and vector core.\n- `apps/schema/` — the standalone static Vercel project for `schema.native-sdk.dev`.\n- `examples/` — the showcase apps, many predating the JSON default (`app.zon` + `src/`).\n- `docs/` — the documentation site; `docs/AGENTS.md` has its MDX conventions.\n- `skills/` and `skill-data/` — the agent skills the CLI ships (`native skills list`).\n- `tools/` and `scripts/` — dev tooling and the local gate.\n\nReleases are maintainer-run; see [RELEASING.md](./RELEASING.md).\n","skills/native-sdk/SKILL.md":"---\nname: native-sdk\ndescription: Discovery skill for the Native SDK, the complete toolkit for building native desktop applications. Apps are authored in TypeScript + declarative Native markup (.native) by default and compiled to native code with no JS runtime in the binary; Zig cores are an explicit alternative, and WebViews are the optional web-content path. Use when the user asks what the Native SDK is, how to build a Native SDK app, author native UI, scaffold an app, configure app.json or legacy app.zon, add bridge commands, embed web content, package an app, test a running app, or automate a Native SDK app.\nallowed-tools: Bash(native:*), Bash(npx @native-sdk/cli:*)\nhidden: true\n---\n\n# Native SDK\n\nThe Native SDK is the complete toolkit for building native desktop applications. **The primary authoring path is TypeScript app logic in `src/core.ts` plus declarative Native markup in `.native` files.** The TypeScript core is checked and compiled ahead of time to native code, so the shipped binary contains no browser, WebView, JS runtime, or interpreter. Zig is how the toolkit itself works and is a first-class, explicitly chosen app-core alternative (`--template zig-core`); it is not the default inferred from the SDK's implementation. Every app embeds a deterministic automation server, so agents can snapshot, drive, and screenshot the running window. Desktop is the mature surface (macOS deepest, Linux and Windows exercised in CI); mobile embedding is experimental. WebView surfaces coexist as the optional path for embedding web content or hosting an existing web frontend.\n\n## Start here\n\nThis file is a discovery stub for agents that installed the Native SDK once with a skills installer such as `npx skills add native-sdk`. Before implementing or explaining Native SDK app work, use the installed CLI to discover and load the current skill content:\n\n```bash\nnative skills list\nnative skills get core\nnative skills get native-ui\nnative skills get ts-core\n```\n\nUse `native skills get core` for initial orientation. For the default app-authoring path, load **both** `native-ui` (views, bindings, the app loop) and `ts-core` (the TypeScript subset, effects, subscriptions, and modules) before implementing. Load `ts-services` too when the tree has `src/services/` or ordinary TypeScript needs filesystem/process/JSON/regex/Map/Date/class work behind `Cmd.request`. Use `native skills get core --full` when work reaches lower-level runtime wiring, WebViews, bridge/security, native capabilities, packaging, or debugging. Use `native skills get automation` when testing a running app, taking snapshots, requesting reloads, or using the built-in automation server. Use `native skills get zig` only for an existing Zig core, a toolkit extension, SDK implementation work, or a Zig 0.16 compile error.\n\n## Quick orientation\n\n```bash\nnpm install -g @native-sdk/cli\nnative init my_app\ncd my_app\nnative dev\n```\n\n`native init my_app` generates the primary three-file app: `app.json`, `src/app.native` (the markup view), and `src/core.ts` (`Model`, `Msg`, `update`). Existing `app.zon` manifests remain supported. Inspect the tree before editing an existing app and preserve the core language it already uses. `src/main.zig` means the app explicitly uses the Zig-core template; web-frontend shells additionally carry `frontend/` and usually owned build/runtime wiring.\n","evals/package.json":"{\n  \"name\": \"@native-sdk/evals\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"packageManager\": \"pnpm@10.23.0\",\n  \"scripts\": {\n    \"typecheck\": \"tsc --noEmit\",\n    \"eval\": \"node src/main.ts\",\n    \"metrics\": \"node src/authoring-metrics.ts\",\n    \"check\": \"pnpm typecheck\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^24\",\n    \"typescript\": \"^5\"\n  },\n  \"dependencies\": {\n    \"@vercel/sandbox\": \"^2.3.0\"\n  },\n  \"engines\": {\n    \"node\": \">=24\"\n  }\n}\n","docs/AGENTS.md":"# Docs Site Conventions\n\n## MDX Tables\n\nAlways use HTML `<table>` syntax in MDX pages, never markdown pipe tables. This ensures consistent styling and avoids MDX parsing edge cases.\n\n```html\n<table>\n  <thead>\n    <tr>\n      <th>Column</th>\n      <th>Description</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>field</code></td>\n      <td>What it does</td>\n    </tr>\n  </tbody>\n</table>\n```\n\n## Geistdocs\n\nThe site uses the public `@vercel/geistdocs` package with Labs branding. Write pages in `content/docs` with a frontmatter `title` and no duplicate H1. Order pages and groups with `content/docs/meta.json` and folder metadata. Keep `/docs/<slug>` URLs and the permanent legacy redirects in `next.config.mjs` stable.\n\nShared navigation, search, page actions, Markdown routes, MDX rendering, and theme behavior belong to Geistdocs. Local adapters live under `src/components/geistdocs` and `src/lib/geistdocs`. Native component previews and the persisted TypeScript/Zig code tabs remain app-owned. `src/lib/remark-native.ts` exports their static data for Markdown; do not evaluate MDX expressions. Preserve `language:filename` fences and existing heading IDs through `src/lib/remark-docs.ts`.\n\nReuse an existing dev server. Run checks with `NEXT_DIST_DIR=.next-check pnpm check` so production checks do not share its `.next` directory. The check builds the site, verifies all canonical pages and Markdown siblings over HTTP, checks legacy redirects and discovery, and checks language tabs and the preview WASM. Also run `scripts/gate.sh fast` from the repository root before finishing.\n","evals/pnpm-lock.yaml":"lockfileVersion: '9.0'\n\nsettings:\n  autoInstallPeers: true\n  excludeLinksFromLockfile: false\n\nimporters:\n\n  .:\n    dependencies:\n      '@vercel/sandbox':\n        specifier: ^2.3.0\n        version: 2.3.0\n    devDependencies:\n      '@types/node':\n        specifier: ^24\n        version: 24.13.2\n      typescript:\n        specifier: ^5\n        version: 5.9.3\n\npackages:\n\n  '@types/node@24.13.2':\n    resolution: {integrity: sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==}\n\n  '@vercel/oidc@3.2.0':\n    resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==}\n    engines: {node: '>= 20'}\n\n  '@vercel/sandbox@2.3.0':\n    resolution: {integrity: sha512-KLhc/sj2JuftgWEivPmmDq3iFLIoYU0XIv+44paTyhk5eGGaqNl6dQdxXSqOf+N31jh55m4UWY7i8oJy8oJRrQ==}\n\n  '@workflow/serde@4.1.0-beta.2':\n    resolution: {integrity: sha512-8kkeoQKLDaKXefjV5dbhBj2aErfKp1Mc4pb6tj8144cF+Em5SPbyMbyLCHp+BVrFfFVCBluCtMx+jjvaFVZGww==}\n\n  async-retry@1.3.3:\n    resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==}\n\n  b4a@1.8.1:\n    resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==}\n    peerDependencies:\n      react-native-b4a: '*'\n    peerDependenciesMeta:\n      react-native-b4a:\n        optional: true\n\n  bare-events@2.9.1:\n    resolution: {integrity: sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==}\n    peerDependencies:\n      bare-abort-controller: '*'\n    peerDependenciesMeta:\n      bare-abort-controller:\n        optional: true\n\n  events-universal@1.0.1:\n    resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==}\n\n  fast-fifo@1.3.2:\n    resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}\n\n  jose@6.2.3:\n    resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==}\n\n  jsonlines@0.1.1:\n    resolution: {integrity: sha512-ekDrAGso79Cvf+dtm+mL8OBI2bmAOt3gssYs833De/C9NmIpWDWyUO4zPgB5x2/OhY366dkhgfPMYfwZF7yOZA==}\n\n  ms@2.1.3:\n    resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}\n\n  os-paths@4.4.0:\n    resolution: {integrity: sha512-wrAwOeXp1RRMFfQY8Sy7VaGVmPocaLwSFOYCGKSyo8qmJ+/yaafCl5BCA1IQZWqFSRBrKDYFeR9d/VyQzfH/jg==}\n    engines: {node: '>= 6.0'}\n\n  picocolors@1.1.1:\n    resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}\n\n  retry@0.13.1:\n    resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}\n    engines: {node: '>= 4'}\n\n  streamx@2.28.0:\n    resolution: {integrity: sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==}\n\n  tar-stream@3.1.7:\n    resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==}\n\n  text-decoder@1.2.7:\n    resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==}\n\n  typescript@5.9.3:\n    resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}\n    engines: {node: '>=14.17'}\n    hasBin: true\n\n  undici-types@7.18.2:\n    resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}\n\n  undici@7.28.0:\n    resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==}\n    engines: {node: '>=20.18.1'}\n\n  xdg-app-paths@5.1.0:\n    resolution: {integrity: sha512-RAQ3WkPf4KTU1A8RtFx3gWywzVKe00tfOPFfl2NDGqbIFENQO4kqAJp7mhQjNj/33W5x5hiWWUdyfPq/5SU3QA==}\n    engines: {node: '>=6'}\n\n  xdg-portable@7.3.0:\n    resolution: {integrity: sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw==}\n    engines: {node: '>= 6.0'}\n\n  zod@4.4.3:\n    resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}\n\nsnapshots:\n\n  '@types/node@24.13.2':\n    dependencies:\n      undici-types: 7.18.2\n\n  '@vercel/oidc@3.2.0': {}\n\n  '@vercel/sandbox@2.3.0':\n    dependencies:\n      '@vercel/oidc': 3.2.0\n      '@workflow/serde': 4.1.0-beta.2\n      async-retry: 1.3.3\n      jose: 6.2.3\n      jsonlines: 0.1.1\n      ms: 2.1.3\n      picocolors: 1.1.1\n      tar-stream: 3.1.7\n      undici: 7.28.0\n      xdg-app-paths: 5.1.0\n      zod: 4.4.3\n    transitivePeerDependencies:\n      - bare-abort-controller\n      - react-native-b4a\n\n  '@workflow/serde@4.1.0-beta.2': {}\n\n  async-retry@1.3.3:\n    dependencies:\n      retry: 0.13.1\n\n  b4a@1.8.1: {}\n\n  bare-events@2.9.1: {}\n\n  events-universal@1.0.1:\n    dependencies:\n      bare-events: 2.9.1\n    transitivePeerDependencies:\n      - bare-abort-controller\n\n  fast-fifo@1.3.2: {}\n\n  jose@6.2.3: {}\n\n  jsonlines@0.1.1: {}\n\n  ms@2.1.3: {}\n\n  os-paths@4.4.0: {}\n\n  picocolors@1.1.1: {}\n\n  retry@0.13.1: {}\n\n  streamx@2.28.0:\n    dependencies:\n      events-universal: 1.0.1\n      fast-fifo: 1.3.2\n      text-decoder: 1.2.7\n    transitivePeerDependencies:\n      - bare-abort-controller\n      - react-native-b4a\n\n  tar-stream@3.1.7:\n    dependencies:\n      b4a: 1.8.1\n      fast-fifo: 1.3.2\n      streamx: 2.28.0\n    transitivePeerDependencies:\n      - bare-abort-controller\n      - react-native-b4a\n\n  text-decoder@1.2.7:\n    dependencies:\n      b4a: 1.8.1\n    transitivePeerDependencies:\n      - react-native-b4a\n\n  typescript@5.9.3: {}\n\n  undici-types@7.18.2: {}\n\n  undici@7.28.0: {}\n\n  xdg-app-paths@5.1.0:\n    dependencies:\n      xdg-portable: 7.3.0\n\n  xdg-portable@7.3.0:\n    dependencies:\n      os-paths: 4.4.0\n\n  zod@4.4.3: {}\n","evals/README.md":"# Native SDK evals\n\nAn eval harness for AI-agent authoring of Native SDK apps. It formalizes the \"clean-agent trial\": give a fresh agent nothing but a scaffolded workspace, the `native-ui` skill, and a task prompt, then grade what it produced deterministically.\n\nPer case the runner:\n\n1. **Scaffolds** a fresh workspace with the repo's own CLI — `zig build` at the repo root, then `zig-out/bin/native init evals/.workspaces/<case> --frontend native` (`--template zig-core` for the pre-existing native cases and the zig side of dual cases, `--template ts-core` for the ts side) — and delivers each track's skills exactly the way a real user gets them: `native skills get <name>` written to `.claude/skills/<name>/SKILL.md` (`init` does not ship skills). The zig track gets `native-ui` + `zig`; the ts app track gets `ts-core` + `native-ui`; core-only ts-core cases get `ts-core`. The workspace is then **pre-warmed** (`native test` once — workspaces are zero-config, so builds go through the CLI verbs) so the agent's own builds are incremental and its wall-clock isn't spent compiling the SDK.\n2. **Runs the agent-under-test**: `claude -p \"<task prompt>\"` headless in the workspace, routed through the Vercel AI Gateway, with a per-run `CLAUDE_CONFIG_DIR` so no user-level memory/plugins/hooks leak in, `--max-turns`, a wall-clock timeout, and the full `stream-json` transcript captured to `results/`.\n3. **Grades** with deterministic checks: `native test` in the workspace, `native markup check` on the `.native` files, per-case file greps (e.g. \"the board uses `<template>`\"), and live automation-snapshot greps (`native build` with `-Dautomation=true`, launch, wait for the automation snapshot, grep it for expected roles/names).\n4. **Judges** quality the deterministic checks can't see — idiomatic Model/Msg design, template factoring, test meaningfulness — with an `llm_judge` check: a judge model called directly through the gateway scores case-specific criteria 0–10 against the task prompt and the agent's code. Advisory by default (the score is recorded and printed but never fails the case); set `\"advisory\": false` on a case to make `minScore` a gate. Skipped in `--dry-run`.\n5. **Reports** a per-case `result.json` (pass/fail per check, judge scores, durations, model, turns, cost) plus a console summary table.\n\n## Requirements\n\n- macOS (live snapshot checks launch the app; use `--skip-live` elsewhere, or `--sandbox` for the Linux lane), Zig 0.16.0, node >= 24, pnpm 10.x.\n- The [Claude Code CLI](https://code.claude.com/docs) (`claude`) on PATH.\n- A [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) API key for real runs:\n\n```sh\nexport AI_GATEWAY_API_KEY=\"vck_...\"   # or VERCEL_AI_GATEWAY_API_KEY\n```\n\nThe runner assembles the gateway env for the claude subprocess per [Vercel's Claude Code guide](https://vercel.com/docs/ai-gateway/coding-agents/claude-code):\n\n```sh\nANTHROPIC_BASE_URL=https://ai-gateway.vercel.sh\nANTHROPIC_AUTH_TOKEN=$AI_GATEWAY_API_KEY\nANTHROPIC_API_KEY=            # empty string on purpose: a non-empty value would win over the auth token\n```\n\nModels are gateway slugs. The coder (agent-under-test) defaults to `anthropic/claude-sonnet-5` (override with `--model` or `NATIVE_SDK_EVAL_MODEL`); the judge defaults to `anthropic/claude-opus-4.8` (override with `--judge-model` or `NATIVE_SDK_EVAL_JUDGE_MODEL`). Both tracks of a dual case always run with the SAME coder model — fairness is a property of the run, so set it once at run time.\n\n## Usage\n\n```sh\ncd evals\npnpm install\n\npnpm eval --list                      # list cases\npnpm eval --dry-run                   # everything except the model call (no key needed):\n                                      #   scaffold + skill delivery, print env + claude argv,\n                                      #   run the graders against the untouched scaffold\n                                      #   (grader FAILs are expected there and exit 0)\npnpm eval templates-settings-app      # one real run\npnpm eval                             # the whole suite\npnpm eval --model anthropic/claude-opus-4.8 templates-settings-app\npnpm eval --judge-model anthropic/claude-fable-5 templates-settings-app\npnpm eval --skip-live                 # skip snapshot checks (no app launch / non-macOS)\npnpm eval --track ts dual-feed-table  # one track of a dual-track case (default: both)\npnpm eval --keep-workspaces           # keep .workspaces/<case> around for inspection\npnpm eval --trials 5 expenses-table   # 5 independent trials per case; report pass rates\npnpm eval --concurrency 3             # run up to 3 case trials in parallel (default 2 locally)\npnpm eval --sandbox                   # run each case in its own Vercel Sandbox microVM\npnpm eval --sandbox --dry-run         # full sandbox path minus the model call\npnpm eval --sandbox --sandbox-vcpus 8 # bigger sandboxes (2048 MB RAM per vCPU)\npnpm typecheck\n```\n\nCases run in parallel (log lines are prefixed `[case-name]`); `--concurrency` caps how many at once — locally the default is 2 to keep zig builds from thrashing, with `--sandbox` the default is 4 (each case has its own VM, but plans rate-limit vCPU allocation).\n\n### Trials\n\nModel runs are stochastic; a single pass or fail is weak evidence. `--trials <n>` runs each case n times, each trial **fully independent** — its own scaffolded workspace (`.workspaces/<case>-trial-<n>`), its own agent run, its own checks and judge call — and reports per-case pass **rates** (e.g. `3/5`), per-check pass counts, and the mean judge score. Trials share the `--concurrency` pool (log lines are prefixed `[case-name#trial]`), and with `--sandbox` each trial gets its own microVM.\n\nWith `--trials 1` (the default) the behavior and file layout are exactly the single-run layout described above. With `--trials > 1` each case directory nests per-trial results plus an aggregate:\n\n```\nresults/<stamp>/\n  summary.json                      # array of per-case aggregates\n  <case>/\n    aggregate.json                  # pass rate, per-check pass counts, mean judge score, per-trial results\n    trial-1/result.json             # exactly a single-run result.json (plus a \"trial\" field)\n    trial-1/transcript.jsonl\n    trial-2/...\n```\n\nThe summary table swaps the PASS/FAIL column for a `pass rate` column, the `checks` column shows per-check pass counts (`3/3 2/3 ...`, `s` = skipped in every trial), `judge` is the mean score, `cost`/`time` are totals across trials, and a per-check breakdown is printed under the table. A real run exits non-zero if any trial failed.\n\n### Lanes\n\nEvery result carries a **lane** — where the case ran and got graded — and the summary table has a lane column:\n\n- `macos-local` (default): the run described above; live snapshot checks launch the app directly and require macOS.\n- `linux-sandbox` (`--sandbox`): each case trial runs in its own isolated [Vercel Sandbox](https://vercel.com/docs/sandbox) microVM booted from a pre-baked Linux image, **including the live checks** — the app builds with `-Dplatform=linux -Dweb-engine=system -Dautomation=true`, launches under Xvfb, and is driven through the same automation dropbox the macOS lane uses. An engine screenshot of the app's gpu_surface is captured through the dropbox and pulled back with the results.\n\nA check that greps a surface which exists on only one OS can declare `\"lanes\": [\"macos-local\"]` in `eval.json`; on other lanes it reports **skipped** (`not graded on the ... lane`) instead of failing, so the summary distinguishes \"fails\" from \"not applicable on this lane\". Audited 2026-07-05: none of the ten shipped cases needs a lane annotation — every snapshot pattern asserts roles/names from the SDK's own automation snapshot, and the surfaces they cover (secondary windows, gpu charts, trees, tables) are proven on Linux by `tools/linux-truth/`. A Linux-lane failure is therefore a real failure until a case says otherwise.\n\n### Vercel Sandbox mode\n\n`--sandbox` boots each case trial from a custom image (see `evals/sandbox/`) that bakes the Linux GUI stack (GTK4 + WebKitGTK + Xvfb), zig, node/pnpm, the Claude Code CLI, and a **pre-warmed build layer**: the repo at a pinned ref with the CLI, workspace-test, and automation build graphs already compiled into fixed cache paths. Per case the runner then:\n\n1. uploads the repo **working tree** as a tarball and rsyncs it over the baked repo — deletions propagate, caches survive, so builds against the current tip are incremental on top of the bake;\n2. starts Xvfb and re-invokes this same harness inside (`pnpm eval --skip-permissions --lane linux-sandbox <case>`) with the gateway env assembled exactly like a local run;\n3. pulls the whole case results directory home — `result.json`, `transcript.jsonl`, `live-*.png` engine screenshots — before the microVM is destroyed.\n\n`--dangerously-skip-permissions` is safe there because the whole VM is the throwaway. `--sandbox --dry-run` exercises the entire path — provisioning, refresh, scaffold, graders (which FAIL against the untouched scaffold, as designed), artifact pull — without the model call, and exits 0.\n\n**Image**: build and push once with `evals/sandbox/build-image.sh` (needs a Docker login to the registry — the two token variants are in the script header). Rebuild when the Dockerfile changes, when zig bumps, or when the tip has drifted far enough from the baked ref that in-sandbox builds stop feeling incremental; runs stay *correct* without a rebuild because of the working-tree refresh. After a push the registry prepares the image for a few minutes; the runner retries `image_not_ready` for up to 10 minutes. `--sandbox-image` overrides the default reference (`eval-sandbox`, resolved in the linked project).\n\n**Auth** (checked before any sandbox work): either an OIDC token — one-time setup in `evals/`:\n\n```sh\nvercel link --scope vercel-labs --project zero-native\nvercel env pull .env.local   # the runner auto-loads VERCEL_OIDC_TOKEN from .env.local\n```\n\n(the token expires ~12h; re-run `vercel env pull .env.local` when sandbox auth fails) — or `VERCEL_TOKEN` + `VERCEL_TEAM_ID` + `VERCEL_PROJECT_ID` for environments where OIDC is unavailable. Real runs additionally need `AI_GATEWAY_API_KEY` as usual.\n\n**Cost and limits**: sandbox compute is metered (active CPU + provisioned memory) — a 4-vCPU case trial that runs 20-30 minutes lands around $0.20-0.35 plus the model tokens through the gateway. Sandboxes cap at 45 minutes wall clock on the Hobby plan (the runner's per-sandbox timeout), and vCPU allocation is rate-limited per plan, which is why the default `--concurrency` in sandbox mode is 4.\n\nReal runs exit non-zero if any case fails. Workspaces live in `.workspaces/` and results in `results/<timestamp>/<case>/` (`result.json`, `transcript.jsonl`, the isolated `claude-config/` — kept in-VM and not pulled for sandbox runs, plus `live-*.png` screenshots from the Linux lane); both directories are gitignored.\n\n### Permissions for the agent-under-test\n\nBy default the agent runs with `--permission-mode acceptEdits` plus an allowlist covering `zig ...`, `native-sdk ...`, and basic file commands — enough for unattended edit/build/test loops without granting arbitrary shell. `--skip-permissions` switches to `--dangerously-skip-permissions`; only use it if the default allowlist blocks a case, and remember the workspace is a throwaway dir but the process is not otherwise sandboxed.\n\nIn both modes the runner passes `--disallowedTools` deny rules for `evals/cases/**` and `evals/results/**`: the workspace references the SDK repo by path, so the harness itself is reachable from the agent's cwd, and agents exploring the repo for docs/examples were observed reading their own grading config (3/20 runs of the 2026-07-04 suite). SDK source and examples stay readable on purpose — a real user has the repo.\n\n## Cases\n\n- `templates-settings-app` — validates the new grammar: repeated grouped toggle sections where `<template>`/`<use>` is the natural shape, plus token style attributes (muted headers, surface cards). Checks: build+tests, markup check, `<template>`/`<use>` greps, token-attribute greps, snapshot roles.\n- `kanban-board` — port of the manual builder trial; card identity must survive moving between columns (`global-key`).\n- `habits-tracker` — port of the manual markup trial; text entry (elm-style mirror), derived/filtered lists, enum filters.\n- `expenses-table` — exercises the newest grammar (every built-in component markup-expressible): an expense ledger whose natural shape is `table` > `table-row` > `table-cell` with `<for>` rows, an exclusive category filter, and an alert-shaped empty state. The prompt describes only requirements (rows-and-columns of data, a callout, pinned display strings); the greps assert the agent reached the table grammar from the skill alone.\n- `process-monitor` — exercises the effects surface: a long-running local command (a harmless `sh -c` tick loop the prompt pins exactly) spawned from update through the effects channel, lines streaming into a bounded 12-line list, cancel by model-owned key, and status/counts derived from the line/exit Msgs. Greps assert `.update_fx` wiring, `fx.spawn`/`fx.cancel`, fake-executor tests, and the absence of hand-rolled `std.process.Child`/`std.Thread`; the live snapshot asserts the idle state (Start/Cancel, \"Status: idle\", \"0 lines · 0 dropped\") so nothing spawns during grading. The judge scores effect-key discipline, non-blocking behavior, honest drop accounting, and derive-don't-store.\n- `release-dashboard` — exercises the pipeline composites and markdown-in-markup: a release dashboard whose natural shape is `<stepper>` for the five-stage track (starting on \"Canary\"), `<timeline>`/`<timeline-item>` for the seeded event history, and a `<markdown>`-sourced notes panel containing a GFM table. Greps assert all three elements plus `<for>` events; the snapshot asserts the composite semantics (`\"Canary (active)\"` stepper labels, timeline items by title, `role=gridcell` markdown table cells, the pinned status summary). The judge scores house-style composition (no ad-hoc reimplementations) and declarative markup.\n- `settings-picker` — exercises the anchored floating surface pattern: two picker rows (Theme/Accent) whose natural shape is a `<select>` trigger + `<if>`-mounted `<dropdown-menu anchor=...>` of `<menu-item>`s with `on-dismiss`, model-owned open state, and an exclusive-open rule. Greps assert the select/anchored-dropdown/on-dismiss/menu-item composition; the snapshot asserts the closed idle state and the pinned status line (the trigger's accessible name may be its value text or an explicit `label` — both legit, the pattern allows either).\n- `file-browser-panes` — exercises split panes and the tree keymap: a resizable two-pane browser (`<split value= on-resize=>` with pane `min-width` floors) whose sidebar is a `<tree>` of `role=\"treeitem\"` rows with model-owned `expanded` state driving a detail pane. The snapshot asserts `role=separator`/`role=tree`/`role=treeitem`, the seeded rows, and the pinned selection status. The judge is explicitly told the tree keymap is engine-provided so it never expects app-side keyboard Msgs.\n- `metrics-dashboard` — exercises the markup chart element and icon-in-button: the whole view is markup (header with `<button icon=...>`, the `<chart>` with a `<series values=\"{binding}\">`, status bar) and the Zig side owns only model/update logic. Greps assert `<chart`, `icon=\"`, and the pinned `y-min=\"0\"`; the snapshot asserts `role=chart` plus the seeded summary.\n- `inspector-window` — exercises model-declared secondary windows: a task list whose inspector opens as a real second OS window through `Options.windows_fn` + `window_view`, live-updates with the selection, reflects a user close via `on_close`, and never duplicates. A negative grep rejects faking it with `<dialog>`/`<sheet>`/`<drawer>`.\n- `disk-status-spans` — exercises inline text spans: one wrapped paragraph mixing bold, muted, and monospace runs (announcing as a single text run), with bound values interpolated inside the spans and a derived percentage summary. Greps assert the three span styles inside one enclosing `<text>`; the snapshot asserts the paragraph as ONE combined text run with resolved bindings.\n- `chat-composer` — exercises the composer composites: an `<input-group>` wrapping a `<textarea>` plus an in-border `<input-group-actions>` row, an attached `<button-group>` cluster, a derived disabled Send, and a `<for>` message list. The snapshot asserts the composer textbox, the attached cluster, Send disabled on the empty draft, the seeded messages, and the live count.\n- `brand-icon-package` — exercises the one-image app-icon pipeline through `native package`: the manifest sources icons from a single square PNG at the brand path, no prebuilt icon containers anywhere, and the packaged macOS bundle carries a generated `AppIcon.icns` wired into its metadata.\n- `playlist-row-actions` — exercises the interaction seams: row-level Enter as a list row's primary action (`on-submit` on `list-item`, distinct from select-on-press) and the app-level key fallback (`Options.on_key`) for Space and the arrows when nothing is focused. Greps assert the row-level submit binding and the fallback wiring; the snapshot asserts the seeded rows, the model-driven selection, and the idle derived status.\n\nAdd a case by creating `cases/<name>/eval.json` (see `src/types.ts` for the schema). Prompts describe app **requirements**, never the solution — the point is to see whether a fresh agent reaches the intended grammar from the skill alone.\n\n## The TS-authoring track\n\nCases with `\"frontend\": \"ts-core\"` measure the other authoring surface: the app core written in the TypeScript subset and compiled by `packages/core`. The scaffold is not an app — it is `src/core.ts` (the case's `starter/` overlay when it ships one, else a minimal counter core), a README with the check loop, and the `ts-core` skill delivered via `native skills get ts-core`. Two graders replace build/markup/snapshot:\n\n- `ts_transpile` — the frontend check must exit clean on `src/core.ts`: tsc-semantics typecheck, every subset rule (NS1001-NS1050) — no Zig emission; the literal name is the `eval.json` check key, the semantics are `ts_check`. Failing diagnostics stay in the result as the violation evidence.\n- `ts_harness` — behavioral grading: compile the core through the external core compiler and drive the compiled archive through a generated mirror — the scratch dir carries the mirror module, the archive, and the case's `harness.zig`, then `zig test harness.zig`. The harness drives the real dispatch cycle (`update` → `commitModelRoot` → `frameReset`) and asserts the prompt's requirements, so the case prompt pins the Model/Msg/export contract exactly (an API spec, not a solution).\n\nBecause the grading harness compiles against the agent's code, ts-core prompts pin names; behavior stays requirements-only. The compiler handles pure `update(model, msg): Model` cores and effectful `Model | [Model, Cmd<Msg>]` pair-returns (the Cmd surface); the wave-2 dual-track cases below are the effects coverage.\n\nThe ts-core cases:\n\n- `ts-habits-core` — a fresh core from a requirements description (the TS mirror of `habits-tracker`): seeded habits, toggle-with-streak math, a trimmed draft add path, exclusive filters, derived counts.\n- `ts-expenses-filter` — a feature-add to an existing subset file: extend a working expense ledger (shipped as `starter/`) with an exclusive `Category | null` filter and derived filtered views, without breaking the existing behavior.\n- `ts-countdown-bugfix` — a bug-fix in an existing subset file: a countdown core whose tick/reset/set_duration transitions drifted from the spec (unguarded ticks, no completion stop/floor, hardcoded reset); the prompt states the intended behavior, the harness isolates each drift.\n- `ts-tag-input-core` — the text seam under pressure: comma-separated tag parsing with byte-level trim, empty-drop, and case-insensitive dedupe, where string methods are the reflex and NS1004 says no.\n\n## The dual-track cases (wave 2)\n\nWave 1's four ts-* cases measured subset compliance on toy katas and compared against Zig numbers gathered **before** the `zig` 0.16-idioms skill existed. Wave 2 replaces that comparison with an honest, contemporaneous one: six realistic asks, each ONE language-blind spec (`\"frontend\": \"app-dual\"`) that runs on **both authoring tracks** — `<case>@ts` scaffolds a full TypeScript app (`native init --frontend native --template ts-core`), `<case>@zig` the Zig app template (`--template zig-core`). Identical prompt, identical shared checks, one behavioral spec asserted by two thin per-track harnesses; `--track ts|zig` selects a lane, the default runs both.\n\nGrading per track: the shared checks (`native test -Dplatform=null`, markup check, view greps, the judge with a case rubric) plus the track's behavioral harness. On the ts track, `ts_harness` compiles `src/core.ts` (its whole import graph) through the external core compiler and `zig test`s the case's `harness-ts.zig` against the compiled core's generated mirror and `harness-lib/cmdview.zig` — a decoder over the Cmd/Sub wire format, so harnesses assert effects semantically (\"one GET to the pinned URL\", \"the delay re-armed on the same key\"). On the zig track, `zig_harness` injects the case's `harness-zig.zig` into the workspace as `src/eval_behavior_spec.zig` (a test import appended to `src/main.zig`, restored afterward) and runs `native test`, so it compiles against the agent's real Model/Msg/update and drives the SDK's deterministic **fake effects executor** (`fx.executor = .fake`, `pendingSpawnAt`/`feedLine`/`feedExit`/`fireTimer`/`feedResponse`/`feedFileResult`).\n\nThe cases — every prompt reads like a real user ask, and every effect result is fed by the harness (no network, no processes, no clocks during grading):\n\n- `dual-feed-table` — \"fetch JSON from this API and show it in a sortable table\": the HTTP effect (pinned URL, buffered GET, keyed), JSON parsing of a pinned flat schema, two sort orders derived at view time, and the five honest states (idle / loading with re-entry guarded / loaded / explicit empty / failed keeping previous rows) across non-200, transport-failure, and malformed-body deliveries.\n- `dual-notes-autosave` — \"add debounced autosave to this notes app\" (starter provided per track, both riding the SDK text-input engines): the keyed one-shot re-arm debounce (Cmd.delay / fx.startTimer replace), byte-exact notes.tsv serialization, Save now with the pending autosave cancelled, a five-state save lifecycle, and the late-result race (a save landing after newer edits must not mark them saved).\n- `dual-pomodoro-timer` — \"build a pomodoro timer with a completion sound\": the recurring timer surface (Sub.timer declared from the model / fx timer armed-cancelled through the channel — armed exactly while running), auto-advancing focus/rest state machine, work-only completion counting, stale-tick guards, and the audio surface at completions.\n- `dual-list-delete-fix` — \"this list doesn't update after delete — fix it\": a realistic seeded bug (a hand-maintained visible-list cache that delete forgot) in a medium app with a markup view; the harness pins list correctness across delete/toggle/add/filter, the judge scores whether the fix is the derive-don't-store root cause or a symptom patch.\n- `dual-ledger-csv-split` — \"split this core into modules and add export-to-CSV in the new module\": multi-file authoring (relative imports on ts, `@import` on zig, both grepped), byte-exact CSV quoting (commas and doubled quotes exercised by the seeds), the file-write effect, and a four-state export lifecycle.\n- `dual-sysinfo-panel` — \"show system info from a shell command in the UI\": a collect-mode background spawn of a pinned argv, byte parsing of the output line, re-entry guarded while probing, and honest ok / failed-with-code / never-started states.\n\nStarters (`starter-ts/`, `starter-zig/`) overlay the scaffold for the feature-add, bug-fix, and split cases — written in each track's idiom (immutable spread cores over the SDK text engine on ts; bounded buffers, `canvas.TextBuffer`, and Model-method bindings on zig) so neither track starts from translated code.\n\n**Fairness and methodology.** Both tracks get identical prompts (language-neutral asks; the pinned contract names arms/fields in the wire spelling and states each track's idiom for the few surfaces that necessarily differ — routed err arms on ts vs payload-carried outcomes on zig). Each track gets its CURRENT skill set by the documented delivery path and nothing extra: ts → `ts-core` + `native-ui`; zig → `native-ui` + `zig` (the 0.16-idioms skill). The same coder model grades both tracks of a run. Known caveats to carry into any writeup: wave-1's Zig numbers predate the `zig` skill (this suite is the corrected baseline — the pre-existing native cases now also receive it, so their history splits at this commit); the behavioral harnesses are compiled contracts, so a run can fail for contract-shape reasons the prompt pins explicitly; and the two tracks' harness glue differs by necessity (wire-format decoding vs fake-executor introspection) while asserting the same spec, test for test.\n\n### Authoring metrics\n\n`pnpm metrics results/<stamp> [...]` post-processes finished runs' transcripts into the agent-authoring metrics the checks cannot see, per case and per track — **ts** (the ts-core cases and the `@ts` side of dual cases) vs **zig** (the pre-existing native cases and the `@zig` side): **first-pass compliance** (did the agent's first compliance check after touching sources pass — the frontend check, `native check`, `native test`, or `native build` on the ts track; `native test` / `native build` / `native check` / `native markup check` on the zig track), **retries-to-green** (failing compliance runs before the first green), **teaching-error encounters** (failing compliance runs that carried a teaching diagnostic — the \"did the diagnostics work\" round-trip count wave 2 compares across tracks), **violation taxonomy** (NS/TS rule IDs on the ts track; zig error lines on the zig track, with `no member named 'X'` bucketed by member so the 0.16-idiom class is visible) raw and per 1k generated LOC (lines written through Write/Edit to source files, `.native` markup included), and **task success** (the run's own pass verdict). Harness friction — permission-refused commands, errored compounds with no diagnostic in the output — is dropped from the event stream so it never masquerades as an authoring failure. It writes `authoring-metrics.json` next to each `summary.json`.\n\n## CI\n\nCI only typechecks the harness (`evals-typecheck` in `.github/workflows/ci.yml`). Eval runs are local/manual — no model calls in CI.\n","skill-data/core/SKILL.md":"---\nname: core\ndescription: Core Native SDK guide for AI agents. Read this before explaining the Native SDK or changing a Native SDK app. Establishes TypeScript + Native markup as the default app-authoring path, routes default app work to the native-ui and ts-core skills, and covers the shared foundation: project structure, app.json and legacy app.zon, lower-level App and Runtime patterns, frontend integration, web engines, JavaScript bridge commands, permissions, windows, WebViews, dialogs, packaging, debugging, and testing. Use when the user asks what the Native SDK is, how to build or modify an app, how to package or debug it, or how to add native capabilities.\n---\n\n# Build Native SDK apps\n\nThe Native SDK is a cross-platform native app toolkit inspired by the web. The default app (`native init`) is native-rendered and authored as a declarative Native markup view (`src/app.native`) plus a TypeScript app core (`src/core.ts`). The core is checked and compiled ahead of time to native code; no JS runtime ships in the binary. **For default app implementation, load both the `native-ui` and `ts-core` skills.** Zig is the implementation language of the toolkit and a first-class app-core choice via `--template zig-core`, not the default app-authoring language.\n\nThis skill covers the shared foundation and lower-level architectures: apps that explicitly choose a Zig core, runtime extensions, and apps that render web frontends in a WebView (`native init --frontend next|vite|react|svelte|vue`). Native-rendered surfaces and WebViews share windows, policies, lifecycle, commands, and platform services, and one app can mix them. WebView engines are the platform WebView (WKWebView on macOS, WebKitGTK on Linux) or Chromium through CEF where supported.\n\nAgents should assume they do not know the Native SDK from general model knowledge. Read this skill first. For ordinary app implementation, load `native-ui` and `ts-core`; for shared/runtime, WebView, packaging, or native-capability work, run `native skills get core --full` so the referenced files are included in the CLI output.\n\n## Mental model\n\n- The default app has three files of truth: `src/core.ts` (`Model`, `Msg`, `update`), `src/app.native` (UI), and `app.json` (manifest).\n- Native markup binds model values and dispatches typed messages; only `update` changes state.\n- The TypeScript core compiles to native code. Node is a build/check/dev tool, not an app runtime.\n- `App` is the lower-level product/runtime interface used by generated wiring, Zig-core apps, WebView shells, and extensions.\n- `Runtime` owns the event loop, windows, bridge dispatch, security checks, automation, tracing, platform services, and window state.\n- `WebViewSource` tells the runtime what to load: inline HTML, a URL, or packaged assets from a local app origin.\n- `app.json` is the default app manifest: identity, icons, windows, frontend assets, web engine, permissions, bridge policy, security policy, and packaging inputs. `app.zon` remains supported.\n- `src/runner.zig` and `src/main.zig` appear only when an app explicitly owns lower-level Zig wiring (for example a `zig-core` app or a WebView shell). `--full` makes a TypeScript app own `build.zig` but still does not give it Zig app logic. Do not add Zig sources to a default TypeScript app merely because SDK examples contain them.\n- `frontend/` is normal web code. It talks to native Zig through `window.zero.invoke()` or builtin helpers when those are enabled.\n\n## Task router\n\nThese references are included by `native skills get core --full`. Use them when the task touches the topic:\n\n- Project creation, generated files, build steps: `references/project-anatomy.md`\n- Default TypeScript core authoring, checker rules, effects, subscriptions, modules: `native skills get ts-core`\n- Native markup, bindings, messages, hot reload, view testing: `native skills get native-ui`\n- `App`, `Runtime`, callbacks, embedding, tests: `references/app-model-runtime.md`\n- React/Vue/Svelte/Next/Vite, dev server, bundled assets: `references/frontend-assets.md`\n- App-defined bridge commands, builtin commands, permissions, windows, WebViews, dialogs: `references/bridge-security-native-capabilities.md`\n- Web engine choice, CEF, packaging, signing, doctor, logs, debugging: `references/web-engines-packaging-debugging.md`\n- Running-app inspection and smoke tests: `native skills get automation`\n- `zig build` fails on std APIs (\"no member named 'cwd'\", ArrayList `init`): `native skills get zig` — the Zig 0.16 idioms, indexed by compile error\n\n## Quick start\n\nUse the CLI for new apps:\n\n```bash\nnpm install -g @native-sdk/cli\nnative init my_app\ncd my_app\nnative dev\n```\n\nThis creates `src/core.ts`, `src/app.native`, and `app.json`; a native window opens with `native dev`. Use `native init my_app --template zig-core` only when the user chooses Zig. Frontend choices (`--frontend next|vite|react|svelte|vue`) are the separate WebView migration/integration path.\n\n## Workflow for existing apps\n\nBefore editing an existing Native SDK app:\n\n1. Read `app.json` or `app.zon` and inspect `src/` before assuming a core language. Read `src/core.ts` + `src/app.native` for the default path; read `src/main.zig`, `src/runner.zig`, and `build.zig` only when they exist.\n2. Identify whether the app is TypeScript + Native markup (default), an explicitly chosen Zig core, or a WebView frontend, then identify the layer the change belongs to.\n3. Follow the generated code and examples in the repository instead of inventing a new app layout.\n4. Prefer exact security policy changes over broad allowances.\n5. Validate with the narrowest useful command.\n\nCommon file ownership:\n\n- `app.json` / `app.zon`: app identity, version, icons, windows, permissions, capabilities, bridge command policy, allowed origins, frontend dist/dev config, web engine, CEF config.\n- `src/core.ts`: default app state and behavior — `Model`, `Msg`, `update`, pure helpers, `Cmd`, and `Sub`.\n- `src/app.native`: default app UI — elements, layout, bindings, and message dispatch.\n- `src/main.zig`: explicitly chosen Zig-core logic or lower-level `App` behavior, source selection, lifecycle callbacks, and custom bridge handlers.\n- `src/runner.zig`: `Runtime.init`, platform selection, security policy, builtin bridge policy, `js_window_api`, automation server, trace sinks, panic capture, window state store.\n- `build.zig`: build options, frontend build/dev/package steps, platform link setup, test steps.\n- `frontend/`: web app implementation, `window.zero` calls, dev/build config.\n\n## Lower-level WebView app model (advanced)\n\nThis is not the default app-authoring path. A lower-level Zig/WebView shell returns `native_sdk.App` with `context`, `name`, and a WebView source:\n\n```zig\nconst App = struct {\n    fn app(self: *@This()) native_sdk.App {\n        return .{\n            .context = self,\n            .name = \"my-app\",\n            .source = native_sdk.WebViewSource.html(\"<h1>Hello from Native SDK</h1>\"),\n        };\n    }\n};\n```\n\nUse these source constructors:\n\n- `native_sdk.WebViewSource.html(content)` for small inline demos.\n- `native_sdk.WebViewSource.url(address)` for an explicit URL.\n- `native_sdk.WebViewSource.assets(.{ .root_path = \"frontend/dist\", .entry = \"index.html\" })` for packaged frontend assets.\n\nFor framework apps, prefer a dynamic source so development loads the local dev server and production loads bundled assets:\n\n```zig\nfn source(context: *anyopaque) anyerror!native_sdk.WebViewSource {\n    const self: *App = @ptrCast(@alignCast(context));\n    return native_sdk.frontend.sourceFromEnv(self.env_map, .{\n        .dist = \"frontend/dist\",\n        .entry = \"index.html\",\n    });\n}\n```\n\n`sourceFromEnv` reads `NATIVE_SDK_FRONTEND_URL`; otherwise it serves the configured asset directory. Use it for most framework apps.\n\n## app.json essentials\n\nKeep `app.json` as the source of truth for app-level behavior. Existing `app.zon` projects use the same fields in ZON syntax.\n\n```json\n{\n  \"$schema\": \"https://schema.native-sdk.dev/app/v1.json\",\n  \"id\": \"com.example.my-app\",\n  \"name\": \"my-app\",\n  \"display_name\": \"My App\",\n  \"description\": \"One line about the app, shown in the About panel.\",\n  \"version\": \"0.1.0\",\n  \"icons\": [\"assets/icon.png\"],\n  \"platforms\": [\"macos\", \"linux\"],\n  \"permissions\": [],\n  \"capabilities\": [\"webview\"],\n  \"frontend\": {\n    \"dist\": \"frontend/dist\",\n    \"entry\": \"index.html\",\n    \"spa_fallback\": true,\n    \"dev\": {\n      \"url\": \"http://127.0.0.1:5173/\",\n      \"command\": [\"npm\", \"--prefix\", \"frontend\", \"run\", \"dev\", \"--\", \"--host\", \"127.0.0.1\"],\n      \"ready_path\": \"/\",\n      \"timeout_ms\": 30000\n    }\n  },\n  \"security\": {\n    \"navigation\": {\n      \"allowed_origins\": [\"zero://app\", \"http://127.0.0.1:5173\"],\n      \"external_links\": { \"action\": \"deny\" }\n    }\n  },\n  \"web_engine\": \"system\",\n  \"windows\": [\n    { \"label\": \"main\", \"title\": \"My App\", \"width\": 960, \"height\": 640, \"restore_state\": true }\n  ]\n}\n```\n\nLegacy ZON equivalent:\n\n```zig\n.{\n    .id = \"com.example.my-app\",\n    .name = \"my-app\",\n    .display_name = \"My App\",\n    .description = \"One line about the app, shown in the About panel.\",\n    .version = \"0.1.0\",\n    .icons = .{\"assets/icon.png\"},\n    .platforms = .{ \"macos\", \"linux\" },\n    .permissions = .{},\n    .capabilities = .{ \"webview\" },\n    .frontend = .{\n        .dist = \"frontend/dist\",\n        .entry = \"index.html\",\n        .spa_fallback = true,\n        .dev = .{\n            .url = \"http://127.0.0.1:5173/\",\n            .command = .{ \"npm\", \"--prefix\", \"frontend\", \"run\", \"dev\", \"--\", \"--host\", \"127.0.0.1\" },\n            .ready_path = \"/\",\n            .timeout_ms = 30000,\n        },\n    },\n    .security = .{\n        .navigation = .{\n            .allowed_origins = .{ \"zero://app\", \"http://127.0.0.1:5173\" },\n            .external_links = .{ .action = \"deny\" },\n        },\n    },\n    .web_engine = \"system\",\n    .windows = .{\n        .{ .label = \"main\", .title = \"My App\", .width = 960, .height = 640, .restore_state = true },\n    },\n}\n```\n\nUse exact local origins for dev servers. Add `zero://inline` only for inline HTML sources.\n\n## Common implementation recipes\n\n### Add a new framework app\n\nUse `native init <path> --frontend <next|vite|react|svelte|vue>`. Then inspect the generated `app.json`, `src/main.zig`, and `build.zig` before customizing. For framework behavior, keep frontend work in `frontend/` and use `sourceFromEnv` so development and packaged builds share one app shell.\n\n### Add a native bridge command\n\n1. Add state and a handler in `src/main.zig`.\n2. Register the handler in `bridge()`.\n3. Allow the command in the app manifest and in the runtime bridge policy if the runner reads manifest policy into runtime.\n4. Call it from JavaScript with `window.zero.invoke(\"namespace.command\", payload)`.\n5. Return valid JSON from Zig. Use `native_sdk.bridge.writeJsonStringValue()` for user-controlled strings.\n\nBridge calls are size-limited, origin-checked, permission-checked, and routed only to registered handlers.\n\n### Add windows, child WebViews, or dialogs\n\nUse builtin bridge commands only after enabling a policy for the exact commands and origins. Window and child WebView commands need the `window` permission when permissions are configured. Dialog commands are always default-deny and require explicit `builtin_bridge` policy. See `references/bridge-security-native-capabilities.md`.\n\n### Choose a web engine\n\nDefault to `.web_engine = \"system\"` for small apps and native footprint. Use `.web_engine = \"chromium\"` plus `.cef` when the app needs a pinned Chromium platform or rendering consistency. Chromium apps must install/package the matching CEF layout.\n\n### Package an app\n\nZero-config apps package WITHOUT ejecting — `native package` works directly on the zero-config build (`native eject` is only for owning the build files, never a packaging prerequisite):\n\n```bash\nnative build\nnative package --target macos --archive\n```\n\nOn macOS, `--archive` adds a zero-config drag-to-Applications DMG with a generated 1×/2× Retina background. The optional `app.zon` `.dmg` block controls its volume name, PNG/JPEG/TIFF background, usable Finder canvas/icon geometry, and Applications link. An adjacent `name@2x.png`/`.jpg` is discovered automatically. Use `.dmg.items` only for a fully art-directed list: exactly one `app`, plus optional `applications`, project-relative `file`, and absolute `link` entries, each with its own icon-center position.\n\nApps that own their build (ejected or scaffolded `--full`) wire the same step into the build graph: keep package metadata in the app manifest, build the frontend assets, build the native binary, then package:\n\n```bash\nzig build package\nnative doctor --manifest app.json --strict\n```\n\nUse signing and CEF options only when the product requires them.\n\n## Development commands\n\nFor a default TypeScript + Native markup app, use:\n\n```bash\nnative check\nnative dev\nnative dev --core\nnative test\n```\n\nFor iterative WebView-frontend work in an app that owns its build, use the managed dev server flow:\n\n```bash\nzig build dev\n```\n\nOr run the CLI directly after building the binary:\n\n```bash\nnative dev --manifest app.json --binary zig-out/bin/MyApp\n```\n\nVite usually uses `http://127.0.0.1:5173/`; Next.js usually uses `http://127.0.0.1:3000/`. The app WebView loads the dev URL directly, so framework HMR remains owned by Vite, Next.js, or the selected dev server.\n\n## Security defaults\n\nTreat WebView content as untrusted:\n\n- List only needed `permissions` and `capabilities`.\n- Prefer exact bridge command origins over `\"*\"`.\n- Keep main-frame navigation allowlisted in `security.navigation.allowed_origins`.\n- Keep external links denied unless the product explicitly needs them.\n- Use a strict CSP for packaged frontend assets.\n- Built-in dialogs are always default-deny and require explicit `builtin_bridge` policy.\n- Child WebViews receive `window.zero` only when explicitly created with `bridge: true`.\n\nCommon bridge failure codes are `invalid_request`, `unknown_command`, `permission_denied`, `handler_failed`, `payload_too_large`, and `internal_error`.\n\n## Validate changes\n\nUseful default-app commands:\n\n```bash\nnative check\nnative test\nnative build\n```\n\nUseful SDK, Zig-core, and owned-build commands:\n\n```bash\nzig build run\nzig build dev\nzig build test\nzig build test-tooling\nnative validate app.json\nnative doctor --manifest app.json --strict\nzig build package\n```\n\nFor an app that owns Zig code, run BOTH `zig build` and `zig build test` before calling a change done: Zig's lazy analysis means code only tests reference (or only `main()` reference) can sit broken under the other command alone. This Zig-specific requirement does not turn a default TypeScript app into a Zig app.\n\nThe toolkit requires Zig 0.16.0. When a build fails with \"no member named\" errors on std APIs (`std.fs.cwd`, `ArrayList.init`, `std.io`, `GeneralPurposeAllocator`), the code was written against older Zig idioms — `native skills get zig` maps each such compile error to the 0.16 idiom as this SDK writes it.\n\nFor GUI smoke tests, build with automation enabled and use the `automation` skill:\n\n```bash\nzig build run -Dplatform=macos -Dautomation=true\nzig-out/bin/native automate snapshot\n```\n\nWhen changing app behavior, keep tests in the app's existing authoring tier. TypeScript cores can run in the node dev loop and through generated full-loop tests; Zig-core logic uses focused Zig tests. Use automation-based tests for live window/runtime integration.\n\n## Examples to inspect\n\n- `examples/chatbot`: a substantial app authored entirely in TypeScript + Native markup, with streaming fetch effects and modules.\n- `examples/soundboard`: TypeScript + Native markup music-player showcase.\n- `examples/system-monitor`: TypeScript + Native markup app using spawn effects, timers, and tables.\n- `examples/hello`: smallest lower-level inline HTML/WebView app.\n- `examples/webview`: bridge and WebView runtime example.\n- `examples/browser`: layered WebView/browser-style example.\n- `examples/next`: Next.js with production assets.\n- `examples/react`, `examples/svelte`, `examples/vue`: Vite frontend apps.\n- `examples/ios`, `examples/android`: mobile host embedding examples.\n\n## When answering users\n\nLead with the primary authoring story: applications are TypeScript cores plus Native markup by default, compiled to a small native binary with no JS runtime. Describe Zig as the toolkit implementation language and an explicit app-core/extension choice. Describe WebViews as a coexisting integration path for existing web content, not the starting point for a fresh app. If asked to implement, read the app files first, preserve the existing core language, and make the smallest change in the correct layer.\n","evals/tsconfig.json":"# Evals Skill\n\nPath: `evals/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/tsconfig.json)","packages/native-sdk/package.json":"# Native-sdk Skill\n\nPath: `packages/native-sdk/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/package.json)","packages/native-sdk/README.md":"# native-sdk Documentation\n\nPath: `packages/native-sdk/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/README.md)","packages/core/package.json":"# Core Skill\n\nPath: `packages/core/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/core/package.json)","packages/core/package-lock.json":"# Core Skill\n\nPath: `packages/core/package-lock.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/core/package-lock.json)","packages/core/pnpm-lock.yaml":"# Core Skill\n\nPath: `packages/core/pnpm-lock.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/core/pnpm-lock.yaml)","tools/guest-mac/agents.md":"# Guest-mac Skill\n\nPath: `tools/guest-mac/agents.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/tools/guest-mac/agents.md)","tools/guest-mac/README.md":"# guest-mac Documentation\n\nPath: `tools/guest-mac/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/tools/guest-mac/README.md)","tools/linux-truth/README.md":"# linux-truth Documentation\n\nPath: `tools/linux-truth/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/tools/linux-truth/README.md)","tools/windows-truth/README.md":"# windows-truth Documentation\n\nPath: `tools/windows-truth/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/tools/windows-truth/README.md)","evals/cases/ts-tag-input-core/eval.json":"# Ts-tag-input-core Skill\n\nPath: `evals/cases/ts-tag-input-core/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/ts-tag-input-core/eval.json)","evals/cases/ts-habits-core/eval.json":"# Ts-habits-core Skill\n\nPath: `evals/cases/ts-habits-core/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/ts-habits-core/eval.json)","evals/cases/ts-expenses-filter/eval.json":"# Ts-expenses-filter Skill\n\nPath: `evals/cases/ts-expenses-filter/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/ts-expenses-filter/eval.json)","evals/cases/ts-countdown-bugfix/eval.json":"# Ts-countdown-bugfix Skill\n\nPath: `evals/cases/ts-countdown-bugfix/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/ts-countdown-bugfix/eval.json)","evals/cases/templates-settings-app/eval.json":"# Templates-settings-app Skill\n\nPath: `evals/cases/templates-settings-app/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/templates-settings-app/eval.json)","evals/cases/settings-picker/eval.json":"# Settings-picker Skill\n\nPath: `evals/cases/settings-picker/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/settings-picker/eval.json)","evals/cases/release-dashboard/eval.json":"# Release-dashboard Skill\n\nPath: `evals/cases/release-dashboard/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/release-dashboard/eval.json)","evals/cases/process-monitor/eval.json":"# Process-monitor Skill\n\nPath: `evals/cases/process-monitor/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/process-monitor/eval.json)","evals/cases/playlist-row-actions/eval.json":"# Playlist-row-actions Skill\n\nPath: `evals/cases/playlist-row-actions/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/playlist-row-actions/eval.json)","evals/cases/metrics-dashboard/eval.json":"# Metrics-dashboard Skill\n\nPath: `evals/cases/metrics-dashboard/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/metrics-dashboard/eval.json)","evals/cases/kanban-board/eval.json":"# Kanban-board Skill\n\nPath: `evals/cases/kanban-board/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/kanban-board/eval.json)","evals/cases/inspector-window/eval.json":"# Inspector-window Skill\n\nPath: `evals/cases/inspector-window/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/inspector-window/eval.json)","evals/cases/habits-tracker/eval.json":"# Habits-tracker Skill\n\nPath: `evals/cases/habits-tracker/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/habits-tracker/eval.json)","evals/cases/file-browser-panes/eval.json":"# File-browser-panes Skill\n\nPath: `evals/cases/file-browser-panes/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/file-browser-panes/eval.json)","evals/cases/expenses-table/eval.json":"# Expenses-table Skill\n\nPath: `evals/cases/expenses-table/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/expenses-table/eval.json)","evals/cases/dual-sysinfo-panel/eval.json":"# Dual-sysinfo-panel Skill\n\nPath: `evals/cases/dual-sysinfo-panel/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/dual-sysinfo-panel/eval.json)","evals/cases/dual-pomodoro-timer/eval.json":"# Dual-pomodoro-timer Skill\n\nPath: `evals/cases/dual-pomodoro-timer/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/dual-pomodoro-timer/eval.json)","evals/cases/dual-notes-autosave/eval.json":"# Dual-notes-autosave Skill\n\nPath: `evals/cases/dual-notes-autosave/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/dual-notes-autosave/eval.json)","evals/cases/dual-list-delete-fix/eval.json":"# Dual-list-delete-fix Skill\n\nPath: `evals/cases/dual-list-delete-fix/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/dual-list-delete-fix/eval.json)","evals/cases/dual-ledger-csv-split/eval.json":"# Dual-ledger-csv-split Skill\n\nPath: `evals/cases/dual-ledger-csv-split/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/dual-ledger-csv-split/eval.json)","evals/cases/dual-feed-table/eval.json":"# Dual-feed-table Skill\n\nPath: `evals/cases/dual-feed-table/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/dual-feed-table/eval.json)","evals/cases/disk-status-spans/eval.json":"# Disk-status-spans Skill\n\nPath: `evals/cases/disk-status-spans/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/disk-status-spans/eval.json)","evals/cases/chat-composer/eval.json":"# Chat-composer Skill\n\nPath: `evals/cases/chat-composer/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/chat-composer/eval.json)","evals/cases/brand-icon-package/eval.json":"# Brand-icon-package Skill\n\nPath: `evals/cases/brand-icon-package/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/brand-icon-package/eval.json)","skill-data/ts-services/references/service-surface.md":"# References Skill\n\nPath: `skill-data/ts-services/references/service-surface.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/skill-data/ts-services/references/service-surface.md)","skill-data/core/references/app-model-runtime.md":"# References Skill\n\nPath: `skill-data/core/references/app-model-runtime.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/skill-data/core/references/app-model-runtime.md)","skill-data/core/references/bridge-security-native-capabilities.md":"# References Skill\n\nPath: `skill-data/core/references/bridge-security-native-capabilities.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/skill-data/core/references/bridge-security-native-capabilities.md)","skill-data/core/references/frontend-assets.md":"# References Skill\n\nPath: `skill-data/core/references/frontend-assets.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/skill-data/core/references/frontend-assets.md)","skill-data/core/references/project-anatomy.md":"# References Skill\n\nPath: `skill-data/core/references/project-anatomy.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/skill-data/core/references/project-anatomy.md)","skill-data/core/references/web-engines-packaging-debugging.md":"# References Skill\n\nPath: `skill-data/core/references/web-engines-packaging-debugging.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/skill-data/core/references/web-engines-packaging-debugging.md)","packages/native-sdk/npm/win32-x64/package.json":"# Win32-x64 Skill\n\nPath: `packages/native-sdk/npm/win32-x64/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/win32-x64/package.json)","packages/native-sdk/npm/win32-x64/README.md":"# win32-x64 Documentation\n\nPath: `packages/native-sdk/npm/win32-x64/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/win32-x64/README.md)","packages/native-sdk/npm/win32-arm64/package.json":"# Win32-arm64 Skill\n\nPath: `packages/native-sdk/npm/win32-arm64/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/win32-arm64/package.json)","packages/native-sdk/npm/win32-arm64/README.md":"# win32-arm64 Documentation\n\nPath: `packages/native-sdk/npm/win32-arm64/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/win32-arm64/README.md)","packages/native-sdk/npm/linux-x64-musl/package.json":"# Linux-x64-musl Skill\n\nPath: `packages/native-sdk/npm/linux-x64-musl/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-x64-musl/package.json)","packages/native-sdk/npm/linux-x64-musl/README.md":"# linux-x64-musl Documentation\n\nPath: `packages/native-sdk/npm/linux-x64-musl/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-x64-musl/README.md)","packages/native-sdk/npm/linux-x64-gnu/package.json":"# Linux-x64-gnu Skill\n\nPath: `packages/native-sdk/npm/linux-x64-gnu/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-x64-gnu/package.json)","packages/native-sdk/npm/linux-x64-gnu/README.md":"# linux-x64-gnu Documentation\n\nPath: `packages/native-sdk/npm/linux-x64-gnu/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-x64-gnu/README.md)","packages/native-sdk/npm/linux-arm64-musl/package.json":"# Linux-arm64-musl Skill\n\nPath: `packages/native-sdk/npm/linux-arm64-musl/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-arm64-musl/package.json)","packages/native-sdk/npm/linux-arm64-musl/README.md":"# linux-arm64-musl Documentation\n\nPath: `packages/native-sdk/npm/linux-arm64-musl/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-arm64-musl/README.md)","packages/native-sdk/npm/linux-arm64-gnu/package.json":"# Linux-arm64-gnu Skill\n\nPath: `packages/native-sdk/npm/linux-arm64-gnu/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-arm64-gnu/package.json)","packages/native-sdk/npm/linux-arm64-gnu/README.md":"# linux-arm64-gnu Documentation\n\nPath: `packages/native-sdk/npm/linux-arm64-gnu/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-arm64-gnu/README.md)","packages/native-sdk/npm/darwin-x64/package.json":"# Darwin-x64 Skill\n\nPath: `packages/native-sdk/npm/darwin-x64/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/darwin-x64/package.json)","packages/native-sdk/npm/darwin-x64/README.md":"# darwin-x64 Documentation\n\nPath: `packages/native-sdk/npm/darwin-x64/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/darwin-x64/README.md)","packages/native-sdk/npm/darwin-arm64/package.json":"# Darwin-arm64 Skill\n\nPath: `packages/native-sdk/npm/darwin-arm64/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/darwin-arm64/package.json)","packages/native-sdk/npm/darwin-arm64/README.md":"# darwin-arm64 Documentation\n\nPath: `packages/native-sdk/npm/darwin-arm64/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/darwin-arm64/README.md)",".cursorrules (knative-istio-typesense-gpu-cursorrules-prompt-fil.mdc)":"---\ndescription: \"Cursor rules for Knative development with Istio, Typesense, and GPU integration.\"\nglobs: **/*\nalwaysApply: false\n---\nYou are an expert AI programming assistant specializing in building Knative, Istio, Typesense, htmx and GPU accelerated applications.\n\nAs an AI assistant, your role is to provide guidance, code snippets, explanations, and troubleshooting support throughout the development process. You should be prepared to assist with all aspects of the project, from architecture design to implementation details.\n\n1. Knative\n  - Provide guidance on creating and managing Knative services\n  - Assist with serverless deployment configurations\n  - Help optimize autoscaling settings\n\n2. Istio\n  - Offer advice on service mesh configuration\n  - Help set up traffic management, security, and observability features\n  - Assist with troubleshooting Istio-related issues\n\n3. Typesense\n  - Provide guidance on Typesense setup and configuration\n  - Assist with index creation and search query optimization\n  - Help integrate Typesense with the backend API\n\n4. Frontend Development\n  - Offer suggestions for improving the HTMX-based frontend\n  - Assist with responsive design and user experience enhancements\n  - Help with client-side performance optimization\n\n5. Backend Development\n  - Guide the creation of serverless functions for the backend API\n  - Assist with integrating all components (htmx, Typesense)\n  - Help optimize API performance and error handling\n\n6. Testing and Monitoring\n  - Guide the creation of test cases for each component\n  - Assist with setting up monitoring and logging\n  - Help interpret performance metrics and suggest optimizations\n\n1. Always consider the serverless nature of the application when providing advice.\n2. Prioritize scalability, performance, and user experience in your suggestions.\n3. Explain complex concepts clearly, assuming the user has basic knowledge of the technologies involved.\n4. Offer alternative approaches or solutions when appropriate.\n5. Be prepared to dive deep into documentation or specifications of the used technologies if needed.\n6. Encourage best practices in cloud-native application development.\n7. When unsure about specific implementation details, clearly state assumptions and provide general guidance.\n\nAlways prioritize security, scalability, and maintainability in your designs and implementations. Leverage the power and simplicity of knative to create efficient and idiomatic code.\n\nProject-Specific Notes\n\n1. The frontend uses HTMX for simplicity. Suggest improvements while maintaining this approach.\n2. The backend should be implemented as Knative services.\n3. Typesense is the primary search engine. Focus on its strengths for fast, typo-tolerant searching.\n4. Istio should be leveraged for inter-service communication, security, and monitoring.\n\nRemember, your goal is to guide the development process, provide helpful insights, and assist in creating a robust, scalable, and efficient AI-powered search application.\n\nThese custom instructions provide a comprehensive guide for Claude to assist you with your AI-powered search project. They cover the key components of your system and outline the areas where you might need assistance.\n"},"files":{"skill-data/automation/SKILL.md":"---\nname: automation\ndescription: Automation and verification guide for running Native SDK apps. Use when the user asks to test a running app, inspect runtime state, list windows, wait for readiness, drive widgets, take deterministic screenshots, send bridge commands, debug why automation is not connected, create smoke tests, or verify a Native SDK example in a GUI-capable session.\n---\n\n# Automate Native SDK apps\n\nEvery Native SDK app embeds an automation server — native-rendered apps and WebView-shell apps alike. It works through file-based IPC in `.zig-cache/native-sdk-automation/` and is intended for smoke tests, CI checks with a GUI session, and quick runtime inspection: accessibility snapshots, widget driving through the real input paths, deterministic reference-renderer screenshots, readiness/state assertions, and bridge round-trips.\n\nAutomation is not browser DOM automation. It reports runtime/window/widget state, drives retained-canvas widgets, and can ask the runtime to reload or dispatch bridge requests. For DOM testing of the optional WebView path, use the frontend framework's tests or a browser automation tool against the dev server.\n\n## What automation can verify\n\n- An automation-enabled app started and published `ready=true`.\n- The runtime loaded the expected app name, source kind, and window metadata.\n- The main window exists and is focused/open.\n- The JavaScript-to-Zig bridge can round-trip a request through `native automate bridge`.\n- Builtin window/WebView commands work when exercised by a smoke test.\n- Reload requests are accepted by the runtime.\n- Real pixels of retained-canvas (`gpu_surface`) views: `native automate screenshot <view-label>` renders the view's current canvas frame through the deterministic CPU reference renderer and writes a PNG artifact. Two captures of an unchanged scene are byte-identical, so screenshots can back golden-image or \"did the UI change\" checks.\n\n## What automation cannot verify\n\n- Screenshots of WebView content. `screenshot` covers `gpu_surface` canvas views only; there is no DOM/WebView pixel capture.\n- Arbitrary DOM queries and clicks.\n- Browser network assertions.\n\n## Prerequisites\n\nBuild/run an app with automation enabled. Generated examples usually expose `-Dautomation=true`:\n\n```bash\nzig build run -Dplatform=macos -Dautomation=true\n```\n\nRepository examples may have specialized steps:\n\n```bash\nzig build run-webview -Dplatform=macos -Dautomation=true\nzig build test-webview-smoke -Dplatform=macos\n```\n\nThe runner must pass an automation server into `RuntimeOptions`:\n\n```zig\nconst server = native_sdk.automation.Server.init(io, \".zig-cache/native-sdk-automation\", \"My App\");\nvar runtime = native_sdk.Runtime.init(.{\n    .platform = my_platform,\n    .automation = server,\n});\n```\n\nApps built without `-Dautomation=true` usually ignore automation files.\n\n## Commands\n\n```bash\nnative automate wait\nnative automate assert 'gpu_nonblank=true' 'role=button name=\"Reset\"'\nnative automate assert --absent 'error event='\nnative automate list\nnative automate snapshot\nnative automate reload\nnative automate screenshot inbox-canvas\nnative automate screenshot inbox-canvas 2\nnative automate widget-action canvas 2 press\nnative automate widget-click canvas 3\nnative automate widget-hold canvas 3\nnative automate widget-context-press canvas 3\nnative automate widget-drag canvas 4 0.25 0.82\nnative automate widget-wheel canvas 5 18\nnative automate widget-key canvas tab\nnative automate widget-key canvas cmd+c\nnative automate widget-pinch canvas 1.5\nnative automate widget-pinch canvas 0.5 120 80\nnative automate profile on\nnative automate profile off\nnative automate bridge '{\"id\":\"smoke\",\"command\":\"native.ping\",\"payload\":{\"source\":\"automation\"}}'\n```\n\nIf using the repository-built CLI:\n\n```bash\nzig-out/bin/native automate wait\nzig-out/bin/native automate snapshot\n```\n\n## Snapshot assertions (`automate assert`)\n\nPrefer `native automate assert` over `snapshot | grep` chains: it polls, so no sleeps, and its failure output carries the evidence (each missing pattern plus the snapshot tail).\n\n```bash\nnative automate assert 'gpu_nonblank=true' 'role=button name=\"Reset\"' 'count: 0'\nnative automate assert --timeout-ms 10000 '4 open'\nnative automate assert --absent 'error event=' 'dispatch_errors=[1-9]'\n```\n\nSemantics:\n\n- Every argument is a regex that must match somewhere in `snapshot.txt`. The command polls (100ms interval) until all match, then exits 0.\n- `--timeout-ms <n>` bounds the polling (default 30000). On timeout it prints `missing: <pattern>` for each unmatched pattern, the last 20 snapshot lines, and exits non-zero — CI-friendly, no wrapper script needed.\n- `--absent` inverts the whole invocation: every pattern must NOT match (poll until gone). Mix presence and absence by running two invocations.\n- Supported regex subset: literals, `.`, postfix `*` `+` `?`, line anchors `^`/`$`, classes `[a-z]`/`[^0-9]`, and `\\d \\w \\s` (with uppercase negations). No groups or alternation — pass multiple patterns instead.\n- Quote patterns in single quotes so the shell leaves `\"`, `$`, and `\\d` alone.\n\n## Standard workflow\n\n1. Start the app with automation enabled.\n2. Run `native automate wait` to block until `snapshot.txt` contains `ready=true`.\n3. Run `native automate assert '<pattern>' ...` for state checks, or `native automate snapshot` to eyeball app/window/source metadata.\n4. Run `native automate list` to inspect window summaries.\n5. Run `native automate bridge '...'` for bridge round-trip checks.\n6. Use `native automate widget-action <view-label> <widget-id> <action> [value]` to exercise retained canvas widget actions. `set_text` routes through the SAME input path real typing uses (focus, select-all, then a text-input event), so a TEA app's `on_input` mirror receives the edits and model state stays consistent with the on-screen field — it is not a presentation-only write.\n7. Use `native automate widget-click <view-label> <widget-id>` to exercise pointer-style retained widget routing. `widget-hold <view-label> <widget-id>` drives a press-and-hold through the same path — pointer down, the reserved hold timer fired, then the suppressed release — so `on_hold` Msgs are live-drivable (a target without `on_hold` degrades to the click a real long press is). `widget-context-press <view-label> <widget-id>` is the secondary click: it presents the widget's context menu, or dispatches `on_hold` immediately when the route declares none.\n8. Use `native automate widget-drag <view-label> <widget-id> <start-x-ratio> <end-x-ratio> [start-y-ratio end-y-ratio]` for continuous pointer controls.\n9. Use `native automate widget-wheel <view-label> <widget-id> <delta-y>` for retained widget scroll input. Wheel targets must be interactive/scrollable widgets — a plain layout column or text node is not a wheel target; aim at the scroll/list widget id from the snapshot. Failures land in the snapshot as named reasons: `error event=automation.widget_wheel name=WheelTargetUnknown|WheelTargetNotInteractive|WheelTargetHasEmptyBounds detail=\"<command args>\"`.\n10. Use `native automate widget-key <view-label> <key> [text]` for focused retained widget keyboard input. The key accepts modifier chords — `cmd+a`, `cmd+c`, `cmd+v`, `cmd+x`, `ctrl+shift+arrowleft` (`cmd` sets the primary shortcut modifier on every platform) — so select-all/copy/cut/paste and shift-extended selection are drivable; after a copy, widget lines in the snapshot show the live selection as `selection=a..b`, and the copied text lands on the real system clipboard (`pbpaste` on macOS).\n11. Use `native automate widget-pinch <view-label> <scale> [x y]` for trackpad pinch gestures against a gpu-surface view: the runtime dispatches the real `pinch_begin`/`pinch_change`/`pinch_end` platform events, with one change carrying `scale - 1`. `<scale>` is the FINAL multiplicative zoom for the gesture — the cumulative gesture scale (the product of `1 + delta`) lands exactly on it — `1.5` zooms in 50%, `0.5` zooms out to half. The optional anchor point is view-local points, defaulting to the view center. Apps hear it through the pinch channel (`Options.on_pinch` / the TS core's `pinchMsg`).\n12. Use `native automate screenshot <view-label> [scale]` to capture the named `gpu_surface` view's canvas as `screenshot-<view-label>.png` (the CLI prints the artifact path and waits for the file).\n13. Before driving an app-menu command, inspect the snapshot's `command id=\"...\"` catalog and `app-menu` / `app-menu-item` rows to prove the running app loaded the expected `app.zon` or runner declarations. Then use `native automate menu-command <id>` to dispatch the same `.menu_command` platform event a real selection emits; the verb remains a raw event injector, so the snapshot receipt is what validates registration.\n14. Use `native automate tray-action <item-id>` for the primary status item, or `native automate tray-action <status-item-id> <item-id>` for an explicit item. Both select a dropdown row through the same platform event a real menu-bar click emits (command dispatch with source `.tray`). Live items appear in `snapshot.txt` as `tray #status-id title=\"...\" visible=... items=N` followed by `  tray-item #item-id ...` rows — the menu bar is outside every window capture, so this is the automation evidence for every model-driven item. Unknown id pairs degrade into the dispatch-error ring as `automation.tray_action`.\n15. Use `native automate reload` to request a WebView reload.\n16. Use `native automate profile on` to enable per-stage frame timing: while on, `snapshot.txt` carries a `frame_profile` line with rolling p50/p90/max microseconds per pipeline stage (`rebuild`, `layout`, `reconcile`, `emit`, `a11y`, `plan`, `patch`, `encode`, `present`, `host_decode`, `host_draw`), each with a lifetime sample count (`<stage>_n=`). Drive some interactions, then `native automate snapshot | grep -o 'frame_profile.*'` to read where frame time goes; `profile off` stops recording and drops the line. Turning it on starts a fresh sample window.\n\n## Screenshots\n\n`screenshot <view-label> [scale]` asks the runtime to rasterize the view's\ncurrent retained canvas frame through the deterministic CPU reference\nrenderer — the same pixel path the Linux software presentation uses — and\npublish it as an uncompressed PNG at\n`.zig-cache/native-sdk-automation/screenshot-<view-label>.png`. The file is\nwritten atomically (temp file + rename), so its presence means the PNG is\ncomplete.\n\nDeterminism semantics:\n\n- Screenshots render at scale 1 by default regardless of the display's\n  backing scale, so an unchanged scene produces byte-identical PNGs from\n  capture to capture on the same machine. Pass an explicit scale (for\n  example `2`) for high-DPI pixel dimensions.\n- Screenshots use the live retained scene, including live design tokens and\n  platform text measurement (CoreText on macOS): the layout matches what is\n  on screen. Glyphs are rasterized by the reference renderer from the\n  bundled faces, not by the platform's font rasterizer, so screenshots are a\n  deterministic layout/structure/color signal rather than a platform\n  font-rendering signal.\n- Cross-machine byte-identity is only guaranteed where text metrics are\n  deterministic (the null platform's estimator). On platforms with a native\n  text measurement provider, text widths can differ between OS versions, so\n  compare screenshots taken on the same machine or assert on properties\n  (dimensions, changed/unchanged) rather than exact bytes across machines.\n- OS-level captures (`screencapture -x` on macOS) are NOT a substitute: in a\n  shell without Screen Recording permission they exit 0 and silently return\n  wallpaper-only images with no app window in them. If you shell out for a\n  real-pixel capture, verify the image is not blank/wallpaper before trusting\n  it; `automate screenshot` plus a semantics snapshot is the reliable pair.\n\n## Bridge smoke test pattern\n\nThe request must be JSON with an ID, command, and payload:\n\n```bash\nnative automate bridge '{\"id\":\"smoke\",\"command\":\"native.ping\",\"payload\":{\"source\":\"automation\"}}'\n```\n\nAutomation sends the request with origin `zero://inline`. The app's bridge policy must allow that origin or the call will reject with `permission_denied`. For packaged asset origins, app code often allows `zero://app`; for automation smoke tests, add `zero://inline` only when the test needs it.\n\nExpected response shape depends on the handler. A typical `native.ping` handler returns:\n\n```json\n{\"id\":\"smoke\",\"ok\":true,\"result\":{\"message\":\"pong\",\"count\":1}}\n```\n\nIf the command fails, inspect the bridge error code:\n\n- `unknown_command`: no handler registered or wrong command name.\n- `permission_denied`: origin or permission policy blocked it.\n- `handler_failed`: Zig handler returned an error or invalid JSON.\n- `payload_too_large`: request exceeded bridge limits.\n\n## File protocol\n\nThe default directory is `.zig-cache/native-sdk-automation/`, resolved against the CLI's CURRENT WORKING DIRECTORY — run `native automate` from the app project's directory (where the app was launched). The dir is created by the running app, never by the CLI: a command sent from the wrong cwd fails loudly (`error: no automation dir at <abs path>`) instead of queueing into a dir no app reads, and every queued command prints the absolute dir it wrote to — check that line when a command seems to do nothing.\n\nFiles:\n\n- `snapshot.txt`: app name, readiness, source kind, source size, window metadata, accessibility summary. The `ready=true` line also carries `protocol=<n>` (the CLI/app handshake: the CLI refuses snapshots — and command queues to a live app — whose protocol version is not its own, naming both versions; the fix is rebuilding whichever binary is stale and comparing `native version`), `dispatch_errors=<total>` and `dropped_trace_records=<total>`, and recent degraded handler/update errors appear as `  error event=<tag> name=<ErrorName> timestamp_ns=...` lines — a handler error no longer exits the app, so grep these to notice one happened. While `profile on` is active, a `frame_profile <stage>_p50_us=... <stage>_p90_us=... <stage>_max_us=... <stage>_n=...` line follows the header with per-stage frame timing. The header also carries `markup_watch=armed|off` — whether the markup hot-reload watch is armed (armed only in builds where the app wired `.markup` with a `watch_path` and `io`, or registered compiled fragments through `fragment_watch` — i.e. Debug dev builds).\n- `windows.txt`: window list.\n- `command.txt`: command input written by CLI and consumed by runtime. The slot is single-entry and the app consumes one command per presented frame: `native automate <command>` WAITS for the running app to consume it and prints `delivered <action> -> <dir>` on success — it refuses/fails loudly instead of silently overwriting an unconsumed command, and exits non-zero if the app never consumes it.\n- `bridge-response.txt`: last bridge response.\n- `screenshot-<view-label>.png`: deterministic reference-rendered PNG of a `gpu_surface` view, written by the `screenshot` command.\n\nThe runtime polls `command.txt`. After processing a command, it writes `done`.\n\n## Debugging automation failures\n\nIf `native automate wait` times out with NO snapshot file at all, it prints a teaching error naming the automation dir it watched and pointing at `-Dautomation=true` and the working directory — start from that message. Otherwise:\n\n1. Confirm the app is still running.\n2. Confirm it was built with `-Dautomation=true`.\n3. Confirm the runner passes `automation` into `Runtime.init`.\n4. Check `.zig-cache/native-sdk-automation/snapshot.txt`.\n5. Delete stale files in `.zig-cache/native-sdk-automation/` and restart the app.\n6. Run with more tracing, for example `zig build run -Dtrace=all`.\n\nIf the CLI reports an automation protocol mismatch (or a snapshot with no protocol version): the `native` binary and the app were built from different framework versions — rebuild the older side (stale `zig-out/bin` copies of the CLI are the classic cause; `native version` prints the commit the binary was built from).\n\nStale INSTANCES are called out too: automate verbs print a LOUD warning when the publishing app's process started BEFORE the newest binary in `zig-out/bin` was built — a leftover instance from an earlier run impersonating the new build. Kill it and relaunch the fresh binary before trusting any snapshot.\n\nIf `snapshot` says no app connected:\n\n- The automation directory may not exist yet.\n- The app may be running from a different working directory.\n- The app may be built without automation.\n- The app may not have reached runtime startup.\n\nIf bridge automation fails:\n\n- Check command name spelling.\n- Check app handler registration.\n- Check bridge policy origins for `zero://inline`.\n- Check runtime permissions.\n- Check that the handler returns valid JSON.\n\n## CI and smoke tests\n\nUse automation for minimal integration confidence:\n\n```bash\nzig build test-webview-smoke -Dplatform=macos\n```\n\nA good smoke test:\n\n1. Builds an example with `-Dautomation=true` and `-Djs-bridge=true`.\n2. Starts the app in a GUI-capable session.\n3. Waits for readiness.\n4. Verifies snapshot metadata (`automate assert` with the patterns that matter).\n5. Sends `native.ping`.\n6. Exercises builtin windows/WebViews if the app enables them.\n7. Fails on timeout or unexpected bridge response.\n\nApps scaffolded with `native init --full` ship this as `.github/workflows/ci.yml` (the zero-config default scaffold skips CI): a null-platform `zig build test` job plus a Linux Xvfb smoke job that launches the binary, runs `automate wait`, asserts on the snapshot with `automate assert`, and checks a non-empty `automate screenshot` artifact. Extend that file rather than writing grep chains by hand.\n\nDo not use automation for exhaustive UI testing. It is a runtime and bridge smoke layer.\n\n## Notes\n\n- Automation is compile-time gated: apps built without `-Dautomation=true` ignore automation files.\n- Screenshots cover retained-canvas (`gpu_surface`) views only; WebView pixels are not captured.\n- WebView DOM interaction is intentionally out of scope for this file-based automation layer.\n- Use `native skills get core --full` for app architecture, bridge policy, packaging, and debugging context.\n","skill-data/ts-services/SKILL.md":"---\nname: ts-services\ndescription: Authoring guide for Native SDK TypeScript services under src/services: ordinary scriptc static-tier TypeScript reached through generated typed Cmd clients, shared record shapes, hermetic vendored npm, streaming, deadlines, cancellation, devhost simulation, authority, replay, and the two carriers (in-process thread pool, child subprocess). Use when adding or modifying src/services modules, moving Node/JSON/regex/Map/Date/class work out of a core, or fixing NS1065-NS1067.\n---\n\n# Author TypeScript services behind the effect boundary\n\nUse `src/services/**/*.ts` for imperative work that needs ordinary TypeScript beyond the deterministic core subset: `fs`, `path`, `process`, `os`, `child_process`, `fetch`, regexes, JSON, `Map`/`Set`, `Date`, and classes supported by the pinned scriptc static tier. Both classes compile with the exact scriptc version in `packages/core/package.json`; neither enables `--dynamic` or ships a JavaScript engine. The exact verdict for every surface the compiler currently projects — with each status and refusal code, plus the manifest's explicit coverage limits — is `references/service-surface.md`, generated from the pinned compiler's own manifest; consult it rather than guessing compiler capability.\n\nThe class line is hard:\n\n- `src/core.ts` and its imports outside `src/services/` retain NS1001–NS1064 and `deterministic: true`.\n- The core never imports a service file, even type-only (NS1065). It imports generated command constructors from `@native-sdk/services`.\n- A service may import shared, subset-legal declarations from the core class. Put every boundary record, enum, or union there so one declaration is authoritative on both sides.\n- A service may import compiler-supported Node built-ins and exact packages declared by app.zon. Undeclared bare imports teach NS1066.\n\n## Typed operation surface\n\nEvery directly exported, non-default named function is an operation named `<module-basename>.<export>`. It must be synchronous, have a body, take zero or one explicitly annotated request, and explicitly return a contract-encodable result. Boundary data may use booleans, numbers, integer-class numbers, bytes, optionals, readonly slices, and named records/enums/kind-unions. Functions and behavior-bearing classes do not cross.\n\n```ts\n// src/shared.ts — core-class file, imported by both classes\nexport type ParseRequest = {\n  readonly source: Uint8Array;\n  readonly caseSensitive: boolean;\n};\n\nexport type ParseResult = {\n  readonly bytes: Uint8Array;\n  readonly matched: boolean;\n};\n\n// src/services/feeds.ts\nimport type { ParseRequest, ParseResult } from \"../shared.ts\";\n\nexport function parse(request: ParseRequest): ParseResult {\n  const text = new TextDecoder().decode(request.source);\n  const matched = request.caseSensitive ? /feed/.test(text) : /feed/i.test(text);\n  return { bytes: new TextEncoder().encode(JSON.stringify({ matched })), matched };\n}\n```\n\n`services.contract.json` carries the complete type table, operation names, exact compiler pin, source hashes, package facts, deadlines, streaming declarations, and `deterministic: false`. Generated host codecs, the Zig registry/result decoders, and the typed client consume that sidecar; none re-read source to infer a second truth.\n\nExplicit boundary throws must be exactly inline `{ kind, message }` shapes with a string-valued message and must escape the operation. They arrive as UTF-8 JSON bytes on the error arm. Do not throw `new Error(...)` from the exported surface.\n\n## Call through the generated client\n\nImport from `@native-sdk/services`; `native check` derives this virtual module, and build/dev stage the same projection without writing `src/services.gen.ts` into the author tree. The editor copy lives under ignored `node_modules/@native-sdk/services`.\n\n```ts\nimport { feedsParse } from \"@native-sdk/services\";\nimport type { ParseRequest, ParseResult } from \"./shared.ts\";\n\nexport type Msg =\n  | { readonly kind: \"parse\"; readonly request: ParseRequest }\n  | { readonly kind: \"parsed\"; readonly result: ParseResult }\n  | { readonly kind: \"parse_failed\"; readonly error: Uint8Array };\n\ncase \"parse\":\n  return [model, feedsParse(msg.request, {\n    key: \"feed-parse\",\n    ok: \"parsed\",\n    err: \"parse_failed\",\n  })];\n```\n\nThe generated route type proves that `ok` names the one Msg arm carrying exactly the declared result and `err` names a one-bytes-field arm. Raw `Cmd.request(name, bytes, route)` remains available as the low-level byte seam.\n\nKeys share the engine effect-key space. A duplicate live service key rejects with `rejected` through `err`. Same-key requests run strictly FIFO on both carriers; the in-process carrier runs different keys in parallel across its pool instances. Terminal results and stream events are journaled; replay feeds the recorded events without starting a child process or a pool thread.\n\n## Hermetic npm\n\nResolve packages only with an explicit author action:\n\n```bash\nnative vendor . escape-string-regexp@5.0.0\n```\n\nThe command installs with lifecycle scripts disabled in a temporary directory, copies the flattened package graph into `src/services/vendor/`, retains package/license files, computes canonical tree hashes, and rewrites app.zon's `service_packages` with exact names, `X.Y.Z` versions, and hashes. Check those bytes and manifest facts into source control.\n\nBuilds never run npm or use the network. They verify package identity and every vendored byte, stage only declared packages, and invoke scriptc with `--npm-static <explicit-list>`. `auto`, dynamic-island fallback, and `--dynamic` are refused. Anything below 100% static coverage fails: `native check` preserves the coverage note verbatim and names the three options—choose another exact package, port/vendor a suitable implementation, or wait for compiler support. The checked-in package spike (`tests/ts-services/npm-static-spike.json`) passed four of five deliberately small candidates and refused `micromark`; npm support is selective.\n\n## Streaming, cancellation, and deadlines\n\nA last `emit` capability declares a stream. Each typed chunk becomes canonical bytes on the existing external channel; the operation still returns one typed terminal result.\n\n```ts\nimport type { ServiceCancellation } from \"@native-sdk/core\";\n\nexport type ParseChunk = { readonly bytes: Uint8Array; readonly index: number };\n\n/**\n * @deadlineMs 5000\n * @streamBuffer 8\n */\nexport function parseLarge(\n  request: ParseRequest,\n  emit: (chunk: ParseChunk) => void,\n  cancellation: ServiceCancellation,\n): ParseResult {\n  for (let index = 0; index < request.source.length; index += 4096) {\n    cancellation.throwIfCancelled();\n    emit({ bytes: request.source.slice(index, index + 4096), index });\n  }\n  return parse(request);\n}\n```\n\nImport `ServiceCancellation` as a type from `@native-sdk/core`; it is legal only as the final capability parameter. The generated client route additionally requires `channelKey` and a channel-event Msg arm. `@streamBuffer` is a 1–64 in-flight contract cap (default 8); `@deadlineMs` is a 1–86400000 operation deadline. A terminal result closes the channel after its interim frames. `Cmd.cancel(key)` flips the token, closes a streaming channel, sends `cancelled` through `err`, and drops every later chunk. Deadline expiry flips the same token and sends JSON with `kind: \"timeout\"`. Poll `cancelled()` or call `throwIfCancelled()` at bounded intervals; a short cooperative grace period follows the token, and an operation that unwinds inside it keeps its instance (child process or pool instance) warm. Ignoring the token has a per-carrier hard edge: the child is killed and the next request starts a clean host; the in-process carrier abandons that instance's thread, routes the failure, and adds a fresh instance to the pool. The abandoned dispatch keeps its key reserved until it physically stops, so a same-key replacement cannot overlap its side effects. A detected runtime trap routes `kind: \"service_trap\"` and poisons only the instance it fired in.\n\n## Dev loop, authority, and current limits\n\n`native dev --core` imports service modules in an isolated Node worker, verifies the same package hashes, decodes the same request contract, cooperatively interrupts cancellation/deadline work, maps errors through the same arms, and emits stream chunks through the same channel-event shape. Use `--script scenario.ndjson --watch` for repeatable logic iterations. The devhost reads `NATIVE_SDK_SESSION_RECORD`/`NATIVE_SDK_SESSION_REPLAY`, writes the native journal format, and starts no service worker during replay. Service-only recordings cross between it and the packaged runtime; packaged recordings containing other effect families must be replayed with `native automate replay`, and devhost rejects those records explicitly. `native dev` runs the compiled service executable beside the real app.\n\nCarrier selection is a build fact: `.service_carrier = \"in_process\" | \"child\"` in app.zon (or `-Dservice-carrier`). Unset/`\"auto\"` preserves the child carrier on desktop and resolves to the in-process pool on iOS/Android (mobile apps cannot spawn a sibling process; an explicit `\"child\"` there is refused with a teaching). The in-process archive is available for native Linux, cross-Linux x86_64/aarch64 (`aarch64-linux-android` included), native Windows x86_64, cross-Windows x86_64 GNU, and the Mach-O targets — macOS, `aarch64-ios`, `aarch64-ios-simulator` — built on macOS. `.service_pool_size` (or `-Dservice-pool-size`, 1–16) sets the in-process pool width; the default is min(4, cores).\n\nAuthority differs by carrier. The child process runs with the app's privileges and app-data cwd, receives an explicit environment allowlist (path, user/home/temp, locale/time zone, certificate, and proxy variables; platform equivalents on Windows) with `NATIVE_SDK_*` internals stripped, and its stdout belongs to framed transport (diagnostics go to stderr). In-process services share the app process outright: the app's full environment and working directory, no transport-owned stdout — and a hardware fault (stack overflow) or `process.exit()` in service code takes the app with it, so the child carrier remains the fully isolated option. Each pool worker owns a separate service-module instance, so mutable module globals are worker-local and requests on different keys may observe different copies; keep shared durable state outside module globals. Either way this authority is why service work never runs synchronously inside `update`.\n\nCurrent limits:\n\n- child executables are desktop-only and use the broad compiler matrix (same-platform builds, Linux/Windows GNU cross-targets from desktop hosts, and macOS targets from macOS), while in-process archives follow the localization matrix above (mobile targets are archive-only: iOS from a macOS host at the 15.0 floor, Android from any desktop host with an NDK at the API 26 floor); Windows MSVC builds natively on a matching Windows host, while cross-Windows uses GNU because Zig supplies that target's CRT and system libraries; any explicitly spelled Linux `-gnu` target — even one matching the build host — must also spell its glibc (`x86_64-linux-gnu.2.36` or later, or `-musl`) because Zig otherwise selects the runtime's too-old default floor;\n- synchronous static-tier operations only—async services wait for the next compiler/runtime phase;\n- no JavaScript engine or dynamic npm fallback;\n- storage remains engine-owned effects, not a service-layer database shortcut;\n- custom widgets, renderers, platform integration, and new engine capabilities remain Zig toolkit extensions.\n","skill-data/zig/SKILL.md":"---\nname: zig\ndescription: Zig 0.16 idioms for Native SDK code, indexed by compile error. Load when `zig build` fails on std APIs with errors like \"struct 'fs' has no member named 'cwd'\", \"struct 'array_list.Aligned(u8,null)' has no member named 'init'\", \"struct 'std' has no member named 'io'\", \"no member named 'GeneralPurposeAllocator'\", \"no member named 'getEnvMap'\", or \"invalid format string\" - the signature of code written for Zig 0.15 or earlier. Covers main(std.process.Init), std.Io file IO and writers, ArrayList, process spawning, environment, clocks and sleep, sockets, custom formatting, and build.zig module shapes, each as this SDK writes them.\n---\n\n# Zig 0.16 for Native SDK code\n\nThe Native SDK requires Zig 0.16.0 (`minimum_zig_version` in `build.zig.zon`; the CLI pins the same version and offers a checksum-verified download into `~/.native/toolchains/` when the `zig` on PATH does not match). Training data and older guides teach Zig 0.15 idioms, and 0.16 moved everything that touches the outside world — files, stdout, clocks, sleeping, process spawning, sockets — behind an explicit `std.Io` value, while containers became allocator-per-call. Each section below is headed by the exact compile error the old idiom produces, so search this file by error text.\n\nTwo rules resolve most failures:\n\n- Operations on the outside world take a `std.Io` first (or right after the receiver). Get one from `init.io` in `main(init: std.process.Init)`, from `std.testing.io` in tests, or from `std.Io.Threaded` in code with no `Init` to thread through.\n- Containers are unmanaged: initialize with `.empty` and pass the allocator to every mutating call.\n\nIn a UiApp, `update` never sees an `Io` — persistence, record and relational storage, secure credentials, subprocesses, HTTP, clocks, and timers go through the typed effects channel (`fx.persist`, `fx.storeSet`/`storeGet`/`storeDelete`/`storeScan`/`storeSetMany`, `fx.dbQuery`/`dbExec`, `fx.credentialsSet`/`credentialsGet`/`credentialsDelete`, `fx.readFile`/`appendFile`/`statFile`/`deleteFile`/streaming file verbs, `fx.spawn`, `fx.fetch`, `fx.wallMs`, `fx.startTimer`; see `native skills get native-ui`). Whole-file IO stays capped at 1 MiB; streamed reads deliver 256-KiB chunks, and streamed writes finalize atomically on close. External raw paths require the `filesystem` permission, while the app's six resolved directories are exempt after symlink-safe canonicalization. Raw `std.Io` belongs in `main`, tests, and standalone tools. Record-store apps declare `\"store\"`; relational apps declare `\"sqlite\"`; credentials require both the `\"credentials\"` capability and permission. Credential Msg arms carry `EffectCredentialsResult`; consume successful get bytes immediately and never retain them in Model. The engine owns database paths and app-scoped credential namespaces, while every result still returns as a Msg and crosses the replay journal (credential bytes are redacted). Use `TestHarness().createWithRelationalStore` for a hermetic in-memory relational database and `TestHarness().createWithCredentials` for a hermetic in-memory credential store. And because Zig analyzes lazily, a 0.15-ism can hide in code only one build path references: run BOTH `zig build` and `zig build test` before calling a change done.\n\n## error: struct 'heap' has no member named 'GeneralPurposeAllocator' — allocators come from `main(init: std.process.Init)`\n\nZig 0.15 and earlier:\n\n```zig\npub fn main() !void {\n    var gpa = std.heap.GeneralPurposeAllocator(.{}){};\n    defer _ = gpa.deinit();\n    const allocator = gpa.allocator();\n}\n```\n\nZig 0.16: `main` takes `std.process.Init`, which carries the process-wide allocators, the `Io`, the environment, and the args — nothing to construct or deinit:\n\n```zig\npub fn main(init: std.process.Init) !void {\n    const gpa = init.gpa;                       // general purpose, leak-checked in Debug\n    const arena = init.arena.allocator();       // process-lifetime arena, freed on exit\n    const io = init.io;                         // the Io every std call below wants\n    const args = try init.minimal.args.toSlice(arena);\n    if (init.environ_map.get(\"HOME\")) |home| { ... }\n}\n```\n\nThe CLI's own entry point is the live reference: `tools/native-sdk/main.zig`. Generated **Zig-core** apps have this shape — `main(init: std.process.Init)` passes `init` through to `runner.runWithOptions(app, options, init)` and `init.io` to the markup hot-reload watcher (see the older Zig-core examples' `src/main.zig`). Default TypeScript apps have no authored Zig entry point. `std.heap.page_allocator` still exists for allocations that live for the whole process. Library code that cannot take an `Init` builds its own `Io`: `var threaded = std.Io.Threaded.init(allocator, .{});` then `threaded.io()` (`src/platform/macos/root.zig`).\n\n## error: struct 'fs' has no member named 'cwd' — file IO moved to `std.Io.Dir`\n\n`std.fs.cwd()`, `std.fs.File`, and `std.fs.selfExePath` are gone (`std.fs` retains only `path` helpers and deprecated aliases). The directory handle is `std.Io.Dir`, and every operation takes `io` right after the receiver:\n\n```zig\nconst cwd = std.Io.Dir.cwd();\nconst content = try cwd.readFileAlloc(io, \"app.zon\", allocator, .limited(1024 * 1024));\ndefer allocator.free(content);\ntry cwd.writeFile(io, .{ .sub_path = \"out.txt\", .data = bytes });\nvar file = try cwd.openFile(io, path, .{});\ndefer file.close(io);\ntry cwd.deleteTree(io, \"zig-out/tmp\");\n```\n\nNote the `readFileAlloc` argument order (`io`, path, allocator, limit) and the size limit as `std.Io.Limit` — `.limited(n)` or `.unlimited`. Live references: `tools/native-sdk/skills.zig` (readFileAlloc), `src/tooling/manifest.zig` (openFile), `src/tooling/cef.zig` tests (writeFile with `std.testing.io`). Path buffers size with `std.Io.Dir.max_path_bytes`. For the executable's own path, `std.fs.selfExePath(&buf)` became `std.process.executablePath(io, &buf)`, returning the length (`tools/native-sdk/skills.zig`).\n\n## error: struct 'std' has no member named 'io' — writers are `std.Io.Writer`, stdout is `std.Io.File.stdout()`\n\n`std.io.getStdOut().writer()` and `std.io.fixedBufferStream` are gone. Stdout takes `io` plus a caller-owned buffer, and the printable interface lives one field deep:\n\n```zig\nvar stdout_buffer: [4096]u8 = undefined;\nvar stdout_writer = std.Io.File.stdout().writer(io, &stdout_buffer);\nconst stdout = &stdout_writer.interface;\ndefer stdout.flush() catch {};\ntry stdout.print(\"{s}\\n\", .{message});\n```\n\nForgetting `flush()` means no output — the buffer is yours. `std.debug.print` is unchanged and needs no `io`; prefer it for diagnostics. The `fixedBufferStream` replacement is a fixed writer over a stack buffer, and the written bytes come back from `buffered()`:\n\n```zig\nvar buf: [256]u8 = undefined;\nvar writer = std.Io.Writer.fixed(&buf);\ntry writer.print(\"n={d}\", .{42});\nconst written = writer.buffered();\n```\n\nFor building a string of unknown size, use an allocating writer instead of an ArrayList writer: `var w = std.Io.Writer.Allocating.init(allocator); defer w.deinit();` then print through `w.writer` (`src/automation/server.zig`). One-shot formatting still has `std.fmt.bufPrint` and `std.fmt.allocPrint`, unchanged. Functions that accept a writer take `*std.Io.Writer`, not `anytype` (`tools/native-sdk/skills.zig`; fixed-writer reference: `src/tooling/doctor.zig`).\n\n## error: struct 'array_list.Aligned(u8,null)' has no member named 'init' — ArrayList is unmanaged\n\n`std.ArrayList(T).init(allocator)` is gone; the list no longer stores its allocator. Initialize with `.empty` and pass the allocator to every call that can allocate or free:\n\n```zig\nvar list: std.ArrayList(u8) = .empty;\ndefer list.deinit(allocator);\ntry list.append(allocator, 'a');\ntry list.appendSlice(allocator, \"bc\");\nconst owned = try list.toOwnedSlice(allocator);\n```\n\nThis is everywhere in the SDK — `src/tooling/templates.zig` builds every generated file this way, `tools/native-sdk/skills.zig` collects skills this way. Passing a managed-style call (`list.append('a')`) fails with \"member function expected 2 argument(s), found 1\". `std.StringHashMap`/`std.AutoHashMap` still have managed `.init(allocator)` forms; only explicitly `Unmanaged` maps take `.empty` (`tools/native-sdk/markup_lsp.zig`).\n\n## error: invalid format string 's' for type — enums print with `{t}`, format methods with `{f}`\n\n`{s}` is for strings only. Enums and tagged unions print their tag name with `{t}` (equivalent to `@tagName`, which also still works):\n\n```zig\nstd.debug.print(\"native {t}: `zig build` step failed (exit code {d})\\n\", .{ verb, code });\n```\n\n(`src/tooling/verbs.zig`.) Custom format methods changed shape twice over: the old four-parameter signature no longer compiles (`error: struct 'fmt' has no member named 'FormatOptions'`), and `{}` no longer calls a `format` method at all — it prints the default field dump silently. Declare the 0.16 signature and print with `{f}`:\n\n```zig\npub fn format(self: Point, writer: *std.Io.Writer) std.Io.Writer.Error!void {\n    try writer.print(\"({d},{d})\", .{ self.x, self.y });\n}\n// call site\ntry writer.print(\"{f}\\n\", .{point});\n```\n\n## error: struct 'time' has no member named 'sleep' / 'milliTimestamp' — clocks live on Io\n\n`std.time` keeps only the unit constants (`ns_per_ms`, `ms_per_s`, ...) and epoch helpers. Sleeping and reading clocks take `io` and typed durations:\n\n```zig\ntry std.Io.sleep(io, std.Io.Duration.fromMilliseconds(50), .awake);\nconst now_ns: i128 = std.Io.Timestamp.now(io, .real).nanoseconds;\n```\n\n(`src/runtime/automation_liveness_tests.zig`, `src/automation/server.zig`.) In app code, do not reach for either: `native_sdk.nowMs()` / `native_sdk.monotonicMs()` are the facade, `fx.wallMs()` is the journaled read inside `update`, and `model.clock` is the testable seam — the native-ui skill's Time section owns that pattern.\n\n## error: struct 'process.Child' has no member named 'init' — spawning is `std.process.spawn(io, ...)`\n\nThe two-step `Child.init` + `child.spawn()` collapsed into one call, and lifecycle methods take `io`:\n\n```zig\nvar child = try std.process.spawn(io, .{\n    .argv = &.{ \"npm\", \"run\", \"build\" },\n    .stdin = .ignore,\n    .stdout = .inherit,\n    .stderr = .inherit,\n});\nconst term = try child.wait(io);   // child.kill(io) to stop it\n```\n\n(`src/tooling/verbs.zig`, `src/tooling/dev.zig` — the latter also shows passing a custom environment via `.environ_map`.) Inside a UiApp, spawn through the effects channel (`fx.spawn`) instead — it is bounded, journaled, and delivers exit/output as Msgs.\n\n## error: struct 'process' has no member named 'getEnvMap' / 'argsAlloc' — environment and args come from Init\n\n`std.process.getEnvMap`, `argsAlloc`, and `argsWithAllocator` are gone. `main` already has both: `init.environ_map` (a `*std.process.Environ.Map`) and `init.minimal.args.toSlice(allocator)` (`tools/native-sdk/main.zig`). Building an environment from scratch — for a child process, or in tests — is `var env = std.process.Environ.Map.init(allocator); defer env.deinit(); try env.put(\"KEY\", \"value\");` (`src/tooling/dev.zig`).\n\n## error: struct 'mem' has no member named 'trimRight' / 'trimLeft' — renamed `trimEnd` / `trimStart`\n\nThe directional trims renamed to match the JS/string convention; arguments are unchanged:\n\n```zig\nconst line = std.mem.trimEnd(u8, raw, \"\\r\\n\");     // was trimRight\nconst body = std.mem.trimStart(u8, line, \" \\t\");   // was trimLeft\nconst both = std.mem.trim(u8, text, \" \");          // unchanged\n```\n\n## error: struct 'std' has no member named 'net' — sockets live on `std.Io.net`\n\n`std.net.Address` became `std.Io.net.IpAddress`, and connect/listen take `io`: `std.Io.net.IpAddress.resolve(io, host, port)`, then `IpAddress.connect(&address, io, .{ .mode = .stream, .protocol = .tcp })`; stream readers/writers follow the buffered-writer shape (`std.Io.net.Stream.writer(stream, io, &buffer)` then `.interface`). Live reference: `src/tooling/dev.zig` (`waitUntilReady`/`httpReady`). App-level HTTP belongs in `fx.fetch`, not hand-rolled sockets.\n\n## error: no field named 'root_source_file' in struct 'Build.ExecutableOptions' — build.zig artifacts take modules\n\nZero-config apps have no `build.zig` — the CLI owns the build graph, so this only appears in apps that ejected or scaffolded `--full`. Artifacts take a module, and the module owns root source, target, and optimize:\n\n```zig\nconst exe = b.addExecutable(.{\n    .name = \"app\",\n    .root_module = b.createModule(.{\n        .root_source_file = b.path(\"src/main.zig\"),\n        .target = target,\n        .optimize = optimize,\n    }),\n});\nconst tests = b.addTest(.{ .root_module = app_mod });\n```\n\nThe generated ejected `build.zig` is the reference shape (`native eject`, emitted by `src/tooling/templates.zig`); the SDK's own `build.zig` uses the same pattern throughout.\n\n## Reading files and streams incrementally\n\n`file.reader()` with no arguments is gone. A reader takes `io` and a buffer, and the stream interface lives on `.interface`:\n\n```zig\nvar read_buffer: [4096]u8 = undefined;\nvar reader = file.reader(io, &read_buffer);\nconst bytes = try reader.interface.allocRemaining(allocator, .limited(1024 * 1024));\nconst line = try reader.interface.takeDelimiterExclusive('\\n');\n```\n\n(`src/tooling/manifest.zig` for allocRemaining, `src/tooling/toolchain.zig` for line reading from stdin.) For whole-file reads, prefer `readFileAlloc` above.\n\n## Tests: the Io is `std.testing.io`\n\nTests never construct an `Io` — `std.testing.io` is the canonical one, next to `std.testing.allocator`:\n\n```zig\ntest \"reads the manifest\" {\n    var tmp = std.testing.tmpDir(.{});\n    defer tmp.cleanup();\n    try tmp.dir.writeFile(std.testing.io, .{ .sub_path = \"app.zon\", .data = source });\n    const content = try tmp.dir.readFileAlloc(std.testing.io, \"app.zon\", std.testing.allocator, .limited(4096));\n    defer std.testing.allocator.free(content);\n}\n```\n\n(`src/tooling/cef.zig` and `src/tooling/manifest.zig` tests.) Model/Msg/update tests need no `Io` at all — the markup-view testing pattern in the native-ui skill is pure.\n\n## Unchanged — do not \"migrate\" these\n\n`std.fmt.bufPrint` / `allocPrint` / `parseInt` / `parseFloat`, `std.mem.*`, `std.debug.print`, `std.heap.page_allocator`, `std.time.ns_per_*` and `ms_per_*` constants, `@embedFile`, `std.testing.expect*`, and managed `std.StringHashMap` / `std.AutoHashMap` all work as before. If code using only these fails, the problem is elsewhere.\n","skill-data/ts-core/SKILL.md":"---\nname: ts-core\ndescription: Authoring guide for the primary Native SDK app-logic path: TypeScript app cores - Model, Msg, update, and the pure functions they call, written in the closed app-core subset, checked by the @native-sdk/core frontend, and compiled ahead-of-time to native code by the external core compiler. Use for new apps unless the user explicitly chose Zig, when writing or modifying a src/core.ts app core, fixing subset checker errors (NS1001-NS1069), or deciding how to express state, messages, text (bytes and the byte-text string methods), text input, continuous controls (sliders, scroll), effects (Cmd), subscriptions (Sub), the host-event wiring channels (frameMsg, keyMsg, pinchMsg, dropMsg, appearanceMsg, chromeMsg, envMsgs, app manifest assets), derived values, the view_unbound lint opt-out, local mutation of owned arrays, or how to split a core into modules under src/ (relative imports, namespace imports, @native-sdk/core/text, @native-sdk/core/events). Use ts-services alongside this guide for src/services work.\n---\n\n# Author app cores in the TypeScript subset\n\nTypeScript is the primary app-authoring language. An app core is a Native SDK app's deterministic logic: `Model` (the app state), `Msg` (a discriminated union of everything that can happen), `update(model, msg)` (the one pure transition function), and the pure helpers they call. You write it as a TypeScript module rooted at `src/core.ts` - splitting into more modules under `src/` when it grows (see \"Splitting a core into modules\") - and the build checks the whole import graph with the `@native-sdk/core` frontend and compiles it to native code with the external core compiler. No JS engine ships in the binary — the program either passes the subset checker and compiles to native, or you get a teaching error naming the rule, the fix, and the reason. The same file is executable TypeScript: it typechecks with stock tsc and runs unmodified under node, so you can poke behavior with plain node scripts before the native build.\n\nA whole TS app starts as three files of truth and zero Zig: `src/core.ts` (this guide; plus core-class modules it imports under `src/`), `src/app.native` (the markup view over the core's model), and `app.json` (windows, identity, permissions). Existing `app.zon` manifests remain supported. Optional ordinary-TypeScript service modules live under `src/services/` and are never imported by the core; load `native skills get ts-services` for that boundary. `native init` scaffolds the three-file base; the build detects `src/core.ts` in the tree (never a flag or config — a tree with both `src/core.ts` and `src/main.zig` is a teaching error) and generates the wiring outside the app. The loop:\n\n```sh\nnative dev --core   # the fastest loop: run the core under node's virtual host —\n                    # dispatch Msgs as JSON lines ({\"kind\":\"add\"}, {\"$bytes\":\"…\"}\n                    # for bytes payloads, {\"advance\":1000} to run virtual timers),\n                    # watch the model + effect transcript. Logic only, no renderer.\nnative dev          # build and run the real app (markup hot reload)\nnative check        # subset-check core.ts + validate markup + app.json/app.zon\nnative build        # ReleaseFast binary; native test runs the app's tests\n```\n\nThe complete reference app in this idiom is `examples/soundboard` in the SDK repo: the soundboard music library as three files and zero Zig — const catalog tables, REAL audio through the `Cmd.audioPlay` stream, scrub-to-seek on a markup slider, a motion-gated `Sub.timer` playback clock, the full text-edit engine on a search field, controlled scroll, registered cover assets, the width-adaptive grid through the frame channel, clipboard, and context menus, with an end-to-end suite driving the shipping markup.\n\n## The contract\n\n```ts\nexport interface Model { /* readonly data fields only */ }\n\nexport type Msg =\n  | { readonly kind: \"add\" }\n  | { readonly kind: \"toggle\"; readonly id: number };\n  // ...one arm per thing that can happen; at least two arms\n\nexport function initialModel(): Model { /* pure */ }\n\nexport function update(model: Model, msg: Msg): Model {\n  switch (msg.kind) {\n    // one case per arm; the switch must be exhaustive (no default needed\n    // once every arm is present — a missing arm is a build error)\n  }\n}\n```\n\n- `update` is pure and synchronous: next model out, plus optionally command data describing effects. When a dispatch needs an effect, declare the return type `Model | [Model, Cmd<Msg>]` and return `[nextModel, cmd]` — the runtime interprets the command after the model commits and dispatches any result back to you as a `Msg`. `initialModel` may return the same pair (`[Model, Cmd<Msg>]`) to run a boot effect once at install, and an app that needs recurring timers exports `subscriptions(model): Sub<Msg>`. See \"Effects are Cmd data\" below.\n- Exported helper functions (`export function doneCount(model: Model): number`) compile to public native functions — and every exported helper taking exactly ONE Model parameter also becomes a Model declaration markup binds by the helper's own name (`{doneCount}`), so derived values need no model field. One binding name per member: a helper that collides with a field (or another helper) is a taught NS1031.\n- Update-only state (fields, helpers, or Msg kinds nothing in markup binds or dispatches — host-fired timer arms, persistence bookkeeping) is declared once: `export const viewUnbound = [\"nextId\", \"tick\"] as const;`. It emits as the `view_unbound` opt-out `native check`'s unbound-state lint reads; a name outside the model surface is a taught NS1032. Entries are the TypeScript names exactly as declared (`\"nextId\"`) and Msg kinds as their `kind` tags — the same names markup binds, because there are no other names.\n- Names: your names are your names — fields, helpers, and locals emit into Zig with their TS spellings (`doneToday` stays `doneToday`), and markup binds them verbatim. String-literal unions emit as native enums.\n\n## Tests against the compiled app\n\nPut Node test files directly in `tests/*.test.ts` and run `native test`. Import `NativeApp` and `findWidget` from `@native-sdk/core/testing` **only in tests**, never in `src/core.ts`. Each `NativeApp.start()` launches a separate headless native process with the real scriptc-compiled core and markup. Drive `click`/`action`/`menu`, assert returned model and accessibility snapshots, inspect and answer fake requests with `respond`, and fire declared timers with `fireTimer`. Supply explicit declared launch values with `NativeApp.start({ environment: [{ name: \"APP_MODE\", value: \"manual\" }] })`; they are journaled and replay does not read ambient environment. Framework app directories still use `appDataDirectory`. Always `close()` in `finally` or use `await using`. `verifyReplay()` verifies checkpoints and the final model in a fresh runtime; afterward only snapshot/close are allowed. Model bytes are JSON number arrays; native identities are decimal strings. The host uses a fixed clock and fake effects, without production persistence/services/app-data startup. See https://native-sdk.dev/docs/testing for the complete API and limits.\n\n## The arena mental model\n\nEverything `update` builds lives in a per-dispatch bump arena that is freed wholesale after the returned model is committed, so spreads, `map`, and `filter` are cheap by construction. At commit, only nodes your update actually created are copied into the persistent model heap — everything you spread through unchanged is shared with the previous model for free.\n\nThat is why the immutable style is not a performance tax: `{ ...model, tasks: model.tasks.map(...) }` copies one small struct and one pointer array, never the world.\n\nBoth regions are the compiled core's own: the frame arena bounds one dispatch's transients, the model heap holds the committed model between dispatches, and the compiler's determinism fences keep every dispatch allocation-shaped and replayable.\n\n## What the subset means\n\nThe subset is TypeScript minus the ecosystem minus the purity violations — never minus basic syntax. Concretely: every basic statement, operator, and declaration form of the language compiles (every loop shape including `do...while`, labels with labeled `break`/`continue`, `switch` with `default`, the full assignment-operator family, `**` and the shifts, const record destructuring, namespace imports over your own modules). What does not compile falls into exactly two families, each with a named teaching rule: the ECOSYSTEM the binary cannot carry (npm packages, Node/DOM APIs, regex/JSON/Promise/generator machinery, `eval` — no JS engine ships), and the constructs that would break a core's guarantees (purity and determinism, fixed shapes, one text representation, functions as declarations not values, static types with no runtime tags). Classes and exceptions are NOT in either family: data classes and `throw`/`try`/`catch`/`finally` compile (see below) — only their guarantee-breaking tails (inheritance, unsafe finally, untagged thrown values) teach. A construct that fails with a generic error instead of a teaching rule is a checker bug — the grammar matrix test (`grammar_matrix.test.ts`) pins every grammar production to its verdict so no silent gap can appear.\n\nThe banned families at a glance (each diagnostic names the fix and the reason at the site):\n\n- **No JS engine ships**: `eval`, `new Function`, dynamic `import()`, `debugger` (NS1013); regexes (NS1040); BigInt/Symbol (NS1044); generators (NS1042); async/Promises (NS1002); npm imports (NS1035).\n- **Purity and determinism**: mutation of SHARED data — parameters, model/msg trees, module tables, escaped locals (NS1001/NS1022/NS1051; your own function-local scratch arrays mutate freely, see \"Local mutation\" below), module-level `let` (NS1010), ambient time/randomness/IO (NS1005), effects outside the Cmd/Sub return paths (NS1017/NS1025).\n- **Fixed shapes and layouts**: the class machinery beyond data classes — `extends`/`super`/`abstract` (NS1055), accessors/`#`-privates/class expressions/`this`-as-a-value (NS1056/NS1006), mutable statics (NS1010), generic classes (NS1053) — plus `delete`/getters/setters/computed keys (NS1012), `for`/`in` (NS1009), Map/Set (NS1011), runtime type/shape tests — `typeof`/`in`/`instanceof`/`Object.*` (NS1041). Data classes themselves compile — `static` methods, `static readonly` consts, and erased `private`/`protected` included (see \"Data classes\" below).\n- **One text representation**: string indexing (NS1004), `+` concatenation and tagged templates (NS1018), `string` model fields (NS1024), the byte-text stays-out spellings — `charCodeAt`/`normalize`/`replace` and friends on bytes teach the byte-honest form (NS1060; the supported method surface is under \"Text is bytes\").\n- **Functions are declarations — or const local helpers**: nested function declarations, non-const function values, `?.()` (NS1046), and const helpers that capture/escape/under-annotate (NS1054 — the legal shape is below under \"Local function values\"); fixed arity — no defaults, rest, `arguments`, call spreads (NS1019); generics live on module-level declarations and monomorphize per call site (NS1050/NS1053).\n- **The mapping stays exact**: `var` hoisting (NS1049), loose `==` (NS1048), comma/void/assignment-as-value outside a for-header (NS1043), array/parameter destructuring (NS1045), `export default`/`export =`/`export * from` (NS1047 — export lists and named value re-exports compile), namespace-alias-as-value and SDK namespace imports (NS1039).\n\n## What compiles (v1)\n\nModel and message shapes:\n\n- `interface` with `readonly` fields; nested interfaces; `T | null` for optional data.\n- Model field types: `number`, `boolean`, string-literal unions (`\"all\" | \"active\" | \"done\"` → native enum), numeric-literal unions, `Uint8Array` (bytes), a nested interface, `readonly Interface[]` (arrays of object types), primitive arrays (`readonly number[]`, `readonly boolean[]`, arrays of literal-union tags), a tag-discriminated union (`{ kind: \"list\" } | { kind: \"detail\"; note: Note }` — arms may carry records, bytes, and primitive arrays), and `T | null` over any of these. Model unions compile to native tagged unions; switching arms in `update` retires the old arm's payload automatically at commit.\n- `Msg`: a discriminated union on a `readonly kind` string tag, with primitive / bytes / interface payload fields. It must be a real union — give it at least two arms, or TypeScript collapses the alias to a plain object type and the checker rejects it.\n\nLogic:\n\n- `switch` on any union's `kind` tag — `msg.kind` (the Msg dispatch) and model-field unions (`switch (model.view.kind)`) alike — with member case labels, label stacking (`case \"a\": case \"b\": body`), `break`, and a trailing `default` covering the unnamed arms (without a `default` the switch must be exhaustive — NS1015; with every arm named the `default` is JS dead code and emits nothing) — and `switch` on a string-literal-union or numeric-literal-union *value* (`switch (model.filter)`) — an uncovered member skips the switch exactly like JS (a `default` anywhere but last is a taught stop in both forms) — and `switch` on a plain `number` or `string` value, lowered to an if/else chain with exact JS semantics: strict equality per case (NaN matches nothing, `-0` matches `0`, strings compare contents), cases tested in source order, `default` matching only after every case misses wherever it sits (an empty `default:` stacking onto the next body included); `if`/`else`; classic `for (let i = 0; ...)` including countdowns (`i--`, `i -= k`), multi-counter inits (`let lo = 0, hi = n`), and comma incrementors (`lo++, hi--` — the for-header is the one home for comma sequences); `do { ... } while (cond)` (the body runs before the first test; `continue` jumps to the test, exactly node); `for (const x of xs)` over arrays and `Uint8Array`, with `break`/`continue`, plus the indexed pair form `for (const [i, x] of xs.entries())` (exactly the `[index, element]` two-identifier binding — the index is the loop index, integer-classed; other tuple shapes stay taught); `while`; labeled statements on loops and blocks with labeled `break`/`continue` (`outer: for (...) { ... continue outer; }` — a labeled `continue` in a classic for still runs the incrementor, like JS); `let` locals with reassignment (and `let x: number;` declared-then-assigned); ternaries; `&&`/`||`/`!`; the empty statement `;`.\n- `const { total, done: doneCount } = stats;` — record-field destructuring into const locals (a compile-time alias per field, renames included). Array patterns, parameter patterns, defaults, rest, and nesting are taught (NS1045 — positions can be silently absent in JS; fields cannot).\n- `import * as util from \"./util.ts\"` — a namespace import over your own modules is pure dot-syntax: `util.helper(x)`, `util.CONST`, and `util.Cfg` in type positions all resolve to the target module's flat names. The alias is not a value (storing or passing `util` itself is taught), and the intrinsic `@native-sdk/core` module is always imported by name (NS1039 — the purity rules recognize `Cmd`/`Sub`/`asciiBytes`/`utf8Bytes` by their imported names).\n- Object spread `{ ...model, field: v }`, array spreads in any shape — append `[...xs, x]`, prepend `[x, ...xs]`, multi-spread `[...a, x, ...b]` (each compiles to one exact-size copy) — `.length`, indexing `xs[i]`.\n- Array methods, lowered to inlined loops and exact-size arena copies: `.map` / `.filter` / `.find` / `.findIndex` / `.some` / `.every` / `.reduce` / `.toSorted` / `.slice` / `.concat` / `.indexOf` / `.includes`. `.map` is type-changing — `tasks.map((t) => t.id)` produces a number array, `t => t.title` a bytes array, and a callback that can return `null` produces an optional-element array. Callbacks on map/filter/find/findIndex/some/every may take the `(element, index)` pair — the index is the loop index, integer-classed (`.reduce` stays `(acc, x)`: its index parameter is not in v1, and no callback takes the third JS parameter, the array itself — reference the array by name). Array-method calls may sit directly in `if`/`else if` and ternary conditions (`if (xs.some((x) => x > 3))`) — the scan lowers to a loop just before the branch; a `while` condition cannot (it re-evaluates per iteration — hoist into the loop body or restructure). Callbacks are arrows (expression or block body), inline `function` expressions, or a BARE REFERENCE to a module-level function or const helper (`xs.map(encodeTurn)`, `xs.toSorted(byAscending)` — the referenced body inlines exactly like the arrow spelled at the site); in a block body every code path must end in an explicit `return` (falling off the end would be JS `undefined`, which has no mapping — a taught stop). JS semantics hold exactly: `.slice` resolves negative and out-of-range indices the JS way, `.indexOf` never matches `NaN` while `.includes` does, `.some`/`.every` keep their vacuous defaults on empty arrays, and `.reduce` needs its initial value (the no-initial form throws on an empty array in JS, so it is a taught NS1007 — pass the starting accumulator). `.indexOf`/`.includes` work on scalar elements (numbers, tags, booleans); on record arrays JS compares references, which has no native mapping — match a field with `.find`/`.findIndex` instead.\n- **Local mutation — your own scratch is yours; shared data is immutable.** An array your function CREATES — an array literal (`const stack: number[] = []`, `const st = [1, 2, 3]`) or a fresh copy (`.slice()` / `.map()` / `.filter()` / `.concat()` / `.toSorted()`) — is locally owned, and the full mutating method set works on it with exact JS semantics: `push(...items)`, `pop()`, `shift()`, `unshift(...items)`, `splice(start, deleteCount?, ...items)` (negative/overshooting indices clamp the JS way; the value is the removed array, also yours), `reverse()`, `fill(v, start?, end?)`, in-place `sort(cmp)`, and indexed writes `xs[i] = v`. A parser stack, a work queue, a copy-then-sort — all legal, deterministic, and byte-identical to node. Ownership ends at the first ESCAPE: once the array is returned from a callback, stored into a record/array/model, aliased by a second binding (`const b = a`), or passed where the callee could keep or mutate it, mutating it afterwards is a taught NS1051 — finish mutating first, then let it escape (an early-exit `return` is fine: execution ends there, so mutations on the other path stay legal). Two loosenings keep real code flowing. BORROWING: passing an owned array into a `readonly T[]` parameter is NOT an escape when the callee only READS it (element/property access, iteration, spreads, further borrowing passes — no return of it, no store, no onward pass into a mutable position; recursion over borrowed slices included), so measure-mutate-measure loops work (`total(out); out.push(x); total(out)`). REASSIGNED-OWNING: a `let` binding whose EVERY assignment installs a fresh owning construction (a literal or a copy — `w = xs.filter(...)`, `acc = []`) stays owned through the reassignments; ONE mixed assignment (an alias, a parameter, a helper result) and the binding never owns (NS1001 names it). Never owned: parameters, model/msg data, module `const` tables, aliases, mixed reassigned bindings, and arrays produced by helper calls (copy with `.slice()` to own one). After the value escapes it is an ordinary immutable value; the commit walkers and sharing discipline are unaffected because ownership ended before the escape.\n- Local-mutation shape notes: `push`/`unshift` return the new length in JS, which has no mapping — mutate as a statement and read `.length` after; `sort`/`reverse`/`fill` return the same array — mutate as a statement, then use the array by name (`return copy.sort(cmp)` is a taught stop; the canonical form is `const copy = xs.slice(); copy.sort(cmp); return copy;`); `pop()`/`shift()` return `T | undefined` — the same one-empty the `.find` miss produces, so test `=== undefined` or fold with `??` (`stack.pop() ?? fallback`); spread arguments (`out.push(...xs)`) stay taught — one element per iteration; `xs[xs.length] = v` on an owned array IS a push (the one growth shape — compound forms like `xs[xs.length] += v` read the missing slot first and stay taught), and other out-of-bounds writes are JS sparse arrays with no mapping (they trap on the native bounds check in safe builds — keep writes inside `0..length-1`); changing the LENGTH of the array a `for...of` (or one of its own callbacks) is iterating is a taught stop (JS walks the live array; fixed-length writes during iteration are fine and identical to node); `copyWithin` stays out of v1 (splice/fill cover it).\n- `.toSorted(cmp)` sorts a copy in one expression; `.sort(cmp)` sorts in place on an array you own (on shared data it keeps the NS1022 teaching, which names the copy idiom). Both comparators follow the same rules: return a sign — `(a, b) => a - b` for ascending numbers, or explicit -1/0/1 branches; a boolean comparator is wrong in JS itself (false claims equality) and is rejected by the types plus a taught NS1023. The comparator-less arity sorts by string ToString order in JS (`[10, 9]` stays `[10, 9]`), which has no float-text mapping — pass a comparator. Both sorts are stable exactly like JS: comparator 0 (or NaN) keeps the original order of the pair. One honesty note: a comparator that is inconsistent over the actual data (e.g. `a - b` when elements can be `NaN`) is implementation-defined in JS itself, so node and native may then disagree — keep comparators consistent.\n- A `.find` miss is the core's one empty value: JS spells it `undefined`, so test the result with `=== undefined` (never `=== null` — the checker teaches the difference) or fold it away with `??`: `tasks.find((t) => t.id === id) ?? fallback`.\n- Optional chaining `?.` on property chains (`model.sel?.at ?? 0`), element hops (`m?.xs[0] ?? 0`, `xs?.[i] ?? d`), and method hops on supported receivers (`xs?.slice(0, 2)`, `xs?.includes(3) ?? false` — every mapped array/bytes method): each hop null-propagates exactly like JS, and the chain value is optional — end it in `??` or compare it against a real value. A `?.` chain compared against `null`/`undefined` is a taught error (NS1021), and `g?.()` on a function value stays taught.\n- Null-guard narrowing through `&&`/`||` chains, exactly the way TS narrows: `if (x !== null && x.items.length > 0)`, the flipped order (`null !== x`), the `||` dual (`x === null || x.items.length === 0`, including as an early-exit guard — the code after the exit stays narrowed), ternary conditions (`x !== null && x.at > 0 ? x.at : -1`), and `while (cur !== null && cur.n > 0)` loops (re-tested per iteration; assigning the guarded local drops the narrowing for what follows, like TS). Relational comparisons on guarded optionals (`cls !== null && cls < lim`) work too.\n- Nullish `??`, comparisons (including `===` on `string`-typed values — content equality, same as node; `==`/`!=` are taught NS1048 — coercion), `+ - * / % **` on numbers, unary `+`/`-`, and the bitwise family `& | ^ ~ << >> >>>` — all with JS number semantics (`/` is float division, `%` truncates, `**` is float pow with the exact JS corners — `1 ** NaN` is NaN, `(-1) ** Infinity` is NaN, right-associative `2 ** 3 ** 2` is 512; bitwise and shifts are ToInt32 with the shift count masked & 31, `>>>` yielding the unsigned 32-bit value; unary `+` is the identity on numbers). `**` and `/` results are float-classed; bitwise/shift operands are integer-required positions (a float operand is a taught NS1016).\n- Every compound assignment as a statement: `+= -= *= /= %= **= &= |= ^= <<= >>= >>>=`, each exactly `x = x op v`, plus the guarded forms `&&=`/`||=` (boolean targets; the right side evaluates only when assigned, like JS) and `??=` (optional targets; assigns only when null). A number `++`/`--`/assignment may sit in a VALUE position when the split statement is provably order-exact — the variable's only mention in the statement, in a position JS cannot skip (`arr[i++]`, `const n = ++count`, `const z = (y = 5)`; postfix yields the pre-step value, everything else the post-step value, exactly JS); every other value-position form is taught (NS1043 — ternary branches, short-circuit right operands, loop conditions, or a second mention of the variable).\n- The Math batch, every corner pinned to node: `Math.min` / `Math.max` (any arity — `Math.min()` is Infinity, `Math.max()` is -Infinity, NaN propagates, -0 orders below +0), `Math.round` (half toward +Infinity), `Math.floor` / `Math.ceil` / `Math.trunc` (NaN/Infinity propagate; the -0 results keep their sign, so `Math.ceil(-0.5)` is -0), `Math.abs` (clears the zero sign), `Math.sign` (NaN stays NaN, a zero keeps its sign), `Math.sqrt` (negative input is NaN, `sqrt(-0)` is -0). `Number.isInteger` / `Number.isFinite` / `Number.isNaN` classify like node, and the `NaN` / `Infinity` globals are ordinary number values. Math calls over compile-time constants fold to their exact JS value — `const HALF = Math.floor(5 / 2)` is the integer 2, `5 % 0` is NaN, `-5 % 5` is -0. A bare `-0` literal (and constant arithmetic folding to -0, like `0 * -1`) is a float value — only f64 carries the signed zero — so it cannot flow into an index or another integer-required slot. The integer rule: floor/ceil/trunc/abs/sign of an integer-classed value stays integer-classed, but of a float value stays float (floor of NaN is NaN), so `bytes[Math.floor(x / 2)]` over a float `x` is still a taught NS1016 — keep index flows integer end to end.\n- Template literals with integer holes (`` `${n} of ${total}` ``) feeding `utf8Bytes` or `asciiBytes` (below). Float-valued holes are not in v1 (JS float-to-string fidelity is a runtime v2 surface).\n- Module-level `const` numbers and strings fold to comptime constants, and const tables emit as rodata (no arena, shared for free at commit): arrays of numbers / booleans / strings / literal-union members (`const WEEKDAYS = [3, 5, 2]`, `const ORDER: readonly Filter[] = [\"done\", \"all\"]`), records annotated with an interface (`const LIMITS: Limits = { lo: 1, hi: 9 }`), and arrays of records (`const SEEDS: readonly Task[] = [...]`, names as `utf8Bytes` literals). Element access, `.length`, `for...of`, and the array methods all work over tables. Everything inside must fold at compile time — no spreads, no calls except `asciiBytes` or `utf8Bytes` on a literal — and a record table needs its interface annotation (an unannotated `{ ... }` is a taught stop naming the fix). Helper functions; recursion.\n- **Generics — module-level, monomorphized per call site.** A generic `function`, `interface`, or `type` declares type parameters and instantiates from tsc's RESOLVED type arguments (explicit or inferred): `export function pick<T>(xs: readonly T[], i: number): T { return xs[i]; }` called with tasks emits `pick__Task`, with numbers `pick__f64` (a bare `number` type argument is always f64 — the JS-exact class), one readable Zig fn per distinct instantiation, deduped. Generics over records, unions, arrays, optionals, and bytes all work; generic interfaces/aliases instantiate structurally (`Box<Task>` emits `Box__Task`; `type Opt<T> = T | null` resolves straight through); generics may recurse and call other generics (the inner call resolves at the outer's instantiation). `typeof CONST` type-query aliases resolve through tsc too (`const LIMIT = 9; type Limit = typeof LIMIT`). The boundaries teach: a call site whose type argument stays abstract (`pick([])` infers `never`; `any`/`unknown`, unnamed literal unions) is NS1053 — annotate the call or name the alias; generic function VALUES and generic entry points are NS1050.\n- **Data classes — fields, one constructor, plain methods, statics; no inheritance.** `class Task { title: Uint8Array; done: boolean = false; constructor(title: Uint8Array) { this.title = title; } toggle(): void { this.done = !this.done; } isDone(): boolean { return this.done; } }` emits as a plain struct plus module-level functions; `new Task(...)` constructs a record-shaped value (field initializers run in declaration order, then the constructor body). `static` members are per-class module declarations: a `static` method lowers to a receiver-less module fn under the class's mangled name (`Task.fromRow(...)` resolves to `Task__fromRow`), and a `static readonly` field with an initializer is a module const (`Task.LIMIT` — the module-const value rules apply: numbers/strings fold, tables need their annotation); a MUTABLE static is module state and teaches NS1010, and inside a static member reach other statics by the class name, never `this` (NS1056). `private`/`protected` keywords are accepted and ERASED — tsc enforces them at the type level, which is their whole meaning (`#`-fields stay taught: runtime privacy brands). `this` reaches instance fields and methods (`this.count`, `this.step()`) — anything that lets `this` escape as a value (returning it, storing it, passing it) is taught (NS1056), so fluent chaining is out. Mutation follows exactly the array ownership rule: an instance your function creates with `new` mutates freely — direct field writes (`t.count = 1`, `t.count += 2`) and methods that write `this` — until it ESCAPES (returned, passed, stored, aliased — then NS1051), and parameters/model data never mutate (NS1001); methods that only read are callable on anything. Fields require type annotations; instances flow between functions, sit in arrays, and compare/narrow like records. The class TAIL teaches by name: `extends`/`super`/`abstract` (NS1055 — compose, or model variants as a `kind`-union), getters/setters/`#`-privates/`accessor`/class expressions (NS1056), mutable statics (NS1010), generic classes (NS1053), parameter properties (NS1008), `instanceof` (NS1041 — a `kind` field is the tag that exists). Class instances stay LOCAL values in v1: storing one in the Model tree is taught (NS1056) — keep records (interfaces) in the Model and construct the class where behavior is needed.\n- **Exceptions — `throw`/`try`/`catch`/`finally` as pure control flow.** Inside a core, exceptions are deterministic: `throw` carries a subset VALUE and unwinds to the nearest enclosing `catch` — across helper calls, out of array-method callbacks (a `throw` inside `.map`'s callback exits the whole loop, like JS), through nested `try`s, with `finally` running on every path (fall-through, `return`, `break`/`continue`, and throw alike). The discipline is two rules. First (NS1057): thrown values are kind-tagged subset shapes — throw kind-discriminated records (`throw { kind: \"bad_digit\", at: i } as ParseError`, where `ParseError` is an interface with a string-literal `kind` field or a `kind`-discriminated union; a single-shape core may also throw a number), and SEVERAL distinct shapes may throw: the checker collects every shape the core throws into its implicit thrown union. The catch binding IS that union — narrow it in place with kind tests, no `as` ceremony: `catch (e) { if (e.kind === \"bad_digit\") return -e.at; if (e.kind === \"io\") return e.code; return -1; }` (or `switch (e.kind)` — tsc cannot prove exhaustiveness over the implicit union, so give the switch a `default` or a trailing return). Bare rethrow (`throw e;`) re-raises the bound value — a narrowed arm included — and `catch { ... }` needs no binding; the single-`as` form (`const err = e as ParseError;`) stays legal in single-shape cores (and for a DECLARED union whose arms equal the thrown set — declare `type AppError = ... | ...` and `as AppError` works). What teaches: untagged values in a heterogeneous set, two shapes sharing one `kind` with different payloads, asserting one member shape of a multi-shape core, the binding escaping untyped into a call/store/return, and `throw new Error(...)` (engine error objects carry stack traces with no native layout). Second (NS1058): `finally` never redirects control flow — no `return`/`throw`/`break`-out inside it (JS's own no-unsafe-finally rule; loops fully inside the finally may break within themselves). An UNCAUGHT throw that reaches an exported function's boundary is a defined deterministic panic — exactly where node's process would crash. A throw mid-mutation of an owned array keeps the mutations applied so far, exactly like JS — the catch sees the array as node would.\n- **Local function values — const helpers hoist.** `const scale = (x: number): number => x * 3;` (arrow or `function` expression) hoists to an ordinary module-level fn when it is capture-free (module constants and other const helpers are fine to reference; enclosing locals/params are not — pass them as parameters), fully annotated (every parameter and the return type), and used only by direct calls (`scale(v)`, recursion included) or as an array-method callback (`xs.map(scale)`, comparators included). Everything else teaches NS1054: captures, missing annotations, `let` bindings, returning/storing the value, passing it to your own functions, calling through a record field. Capturing a locally-owned array also ENDS its ownership at the capture (a later mutation is the NS1051 teach) — the stored closure would retain the reference.\n\nNot yet in v1 — genuine roadmap deferrals, each stopping with a loud, tailored NS9001 naming the rewrite (never missing basic syntax; the banned-with-a-rule families live in \"What the subset means\" above). The rule-level deferrals carry `class: \"deferred\"` in the diagnostics catalogue — NS1011 (Map/Set), NS1019 (fixed arity), NS1040 (regexes), NS1042 (generators), NS1044 (BigInt/Symbol) — and their diagnostics say the capability is deliberately deferred, not impossible; the list below is the method-level remainder: `.toSorted()`/`.sort()` without a comparator (JS ToString ordering; pass `(a, b) => a - b`), `.reduce` without an initial value (a taught NS1007 — JS throws on an empty array) or with an index parameter (use a classic loop), `.indexOf`/`.includes` on record arrays (match a field with `.find`/`.findIndex`), `.join` on number arrays (elements are float-valued; join byte values instead), float values (`/`, `**`, `Math.round`, `Math.sqrt`, float `Math.floor`-family results) where an integer is required such as an index (a taught NS1016 — those values can be fractional or NaN), Math methods beyond the batch above, `Number` methods beyond the three classifiers, float-valued template holes (JS float-to-string fidelity is a runtime v2 surface), arrays of unions (`readonly View[]`) or arrays of byte-strings (`readonly Uint8Array[]`) as model fields (wrap the element in a single-field interface), a collect spawn's stderr tail (v1 delivers the exit code and stdout; stderr is not surfaced — put diagnostics on stdout or check the code), per-line truncation flags (a stdout or response line over its configured bound arrives cut, without a flag), and non-timer subscriptions (`Sub.timer` is the one subscription; one-shot needs are `Cmd.delay`, and process/fetch/audio streams are Cmd-initiated, not subscribed).\n\n## Effects are Cmd data\n\n`update` never performs an effect — it can return one, as inert data, alongside the next model. Import the factories from the SDK and declare the pair-return type:\n\n```ts\nimport { Cmd } from \"@native-sdk/core\";\n\nexport function update(model: Model, msg: Msg): Model | [Model, Cmd<Msg>] {\n  switch (msg.kind) {\n    case \"add\":\n      return [{ ...model, count: model.count + 1 }, Cmd.persist()];\n    case \"request_time\":\n      return [model, Cmd.now(\"tick\")]; // dispatches { kind: \"tick\", at: <ms> }\n    case \"tick\":\n      return { ...model, lastTick: msg.at }; // bare model = [model, Cmd.none]\n    case \"ship\":\n      return [model, Cmd.batch([Cmd.persist(), Cmd.host(\"beep\", model.count)])];\n  }\n}\n```\n\nThe command set (Cmd wire format v7):\n\n- `Cmd.none` — no effects; returning a bare `Model` is sugar for `[model, Cmd.none]`.\n- `Cmd.persist()` — snapshot the just-committed Model through the engine-owned store. Requires `\"persist\"` in `app.zon` capabilities plus `.persist = .{ .version, .restore = .{ .ok, .none, .err } }`; the host owns canonical encoding, debounce/coalescing, atomic app-data placement, backup recovery, and replay.\n- `Cmd.now(\"tick\")` — request a timestamp; the runtime dispatches the named Msg arm with the time (ms) as its payload. The target arm must carry exactly one number field (`{ kind: \"tick\", at: number }`), and tsc checks that for you.\n- `Cmd.host(name, ...args)` — a fire-and-forget host command by literal name; the host decides what the name means. Args are numbers, OR exactly one bytes payload: a `Uint8Array` (`Cmd.host(\"clipboard.write\", model.draft)`) or a flat inline record of number/boolean/`Uint8Array` fields (`Cmd.host(\"cfg.save\", { gain: model.gain, on: model.muted, label: asciiBytes(\"main\") })`) — the record lowers to one bytes payload from your types at build time, byte-identical under node and native. Anything else (a smuggled string, a nested record, a payload plus extra args) is a taught error (NS1020/NS1026).\n- `Cmd.request(name, payload, { key?, ok, err })` — a routed host command: the host performs `name` with the payload (same bytes/record rules) and dispatches exactly one result back to you as an ordinary Msg — the `ok` arm with the result bytes on success, or the `err` arm with the error bytes on failure. Both arms must carry exactly one `Uint8Array` field (`{ kind: \"loaded\", body: Uint8Array }`), checked by tsc and taught by NS1027. The routing is data — string-literal arm names, never callbacks — so the result decoder derives from your Msg types at build time. The optional `key` (a string literal) names the in-flight effect: issuing a request whose key is already in flight replaces it (the old result is dropped), which is the debounce/exactly-one-in-flight discipline.\n- Typed app services are the deliberate record-valued arm of this family: declare shared data shapes outside `src/services/`, import generated constructors from `@native-sdk/services`, and let those constructors encode requests and prove the typed success Msg arm. See `native skills get ts-services`. Raw `Cmd.request` remains bytes-oriented.\n- `Cmd.cancel(key)` — drop the in-flight keyed effect with that key, silently: a cancelled raw request, buffered named engine op (`readFile`/`writeFile`/`fetch`/`clipboardRead`), streamed file read, or armed delay dispatches NEITHER arm. Live spawn, streaming-fetch, streaming-service, and streamed write sinks are the exceptions: cancel ends them through `err: cancelled`; a half-written sink is observable and must never disappear silently.\n- `Cmd.batch([a, b])` — several commands from one dispatch, performed in order.\n\n### The named engine ops\n\nThese map directly onto the host's effect engine — files, HTTP, the clipboard, notifications, one-shot timers. Routed operations follow the `Cmd.request` rules (inline `{ key?, ok, err }`, string-literal arm names, arm shapes checked by tsc and taught by NS1027), with one difference from `request`: each op's `ok` arm has the op's OWN result shape. Keys follow the one keyed-effect rule everywhere: issuing a keyed op whose key is already in flight REPLACES the old one (the superseded op's result is dropped — no message), and `Cmd.cancel(key)` drops it silently. Every `err` arm carries exactly one `Uint8Array` field and receives a machine-readable reason; fire-and-forget writes and notifications have no routing arms. Paths, URLs, and bodies are bytes (`asciiBytes` for literals); dynamic values the engine refuses at runtime surface through the `err` arm, while compile-time-knowable bounds for routed operations stop the build (NS1030). Fire-and-forget operations fail closed under the host's validation instead.\n\n- `Cmd.store.set(key, bytes, { key?, ok, err })` / `get` / `delete` / `scan` / `setMany` — capability-gated, engine-owned per-record storage. Declare `\"store\"` in `app.zon`; keys are UTF-8 strings up to 512 bytes and values are bytes up to 1 MiB. `setMany` applies 1–64 entries atomically. `get`'s ok bytes are `[1][value...]` for a hit and `[0]` for a miss, keeping an empty value distinct from absence. `scan(prefix, { limit?, after? }, route)` returns a little-endian length-prefixed page of key/value pairs plus an opaque next-key cursor; pass those next-key bytes back as `after` (a known literal cursor may be a string). The limit defaults to 100 and is capped at 256. Writes run off-loop in issue order, a synchronous read waits for earlier writes in the same command walk, every result journals, and failures route one of `io_failed`, `over_bound`, `bad_key`, `rejected`, or `busy`. The SQLite backing and app-data path are never part of the app-facing API.\n- **Checked relational SQLite:** declare `\"sqlite\"` in `app.zon`. Add append-only, contiguous `src/schema/NNNN_name.sql` migrations (`STRICT` ordinary tables), commit the generated `src/schema/migrations.lock.json`, and add named statements in `src/queries.sql`: `-- name: notesInFolder`, `-- name: moveNote :exec`, or `-- name: notesInFolder :live`. `native check` applies the migrations and prepares every query with real SQLite, then generates flat `Cmd.q<Name>(params, route)` query constructors, typed `Cmd.q<Name>(params)` exec members composed only through `Cmd.qTx([...], route)`, `Sub.q<Name>(key, params, route)` for `:live`, row/param interfaces, and `decode<Name>Page`. TEXT/BLOB rows are `Uint8Array`; parameters inferred from TEXT columns wrap bytes automatically; raw or uninferred dynamic TEXT (for example an FTS `MATCH` term) uses `dbText(bytes)`. INTEGER is an exact `number` and generated decoders reject values outside ±(2^53−1). Live queries rerun after committed writes to their generated table dependency set, coalesced once per command frame. `native db new-migration <name>`, `status`, and `reset --yes` own development migration operations. Do not edit or reuse a shipped migration number.\n- `Cmd.db.query(sql, params, { key?, page, done, err })` / `Cmd.db.exec(statements, { key?, ok, err })` — the permanent raw SQLite escape hatch. The engine owns `app.db`; parameters are `null | number | string | Uint8Array | dbText(bytes) | boolean`. A read delivers 256-row/256-KiB pages then `done`, with one result capped at 8,192 rows or 8 MiB; crossing either total bound rejects the whole result, so use `LIMIT` and keyset pagination for larger collections. An exec commits its 1–64 statements as ONE transaction. SQL remains pathless: `ATTACH`/`DETACH`/`VACUUM INTO`, TEMP schema objects, and engine lifecycle PRAGMAs are denied. Query keys replace/cancel silently; duplicate transaction keys reject loudly. Error bytes are `constraint`, `busy`, `io_failed`, `corrupt`, `misuse`, `rejected`, or `cancelled`. Every page/terminal journals and replay never opens SQLite. Prefer declared queries; NS1420 nudges raw query literals toward them.\n- `Cmd.readFile(path, { key?, ok, err })` — read a whole file. `ok` arm: one `Uint8Array` field with the content. `err` reasons: `not_found`, `io_failed`, `truncated` (the file exceeds the engine's 1 MiB read bound — a cut file never passes as whole), `rejected`. Paths are at most 1024 bytes.\n- `Cmd.writeFile(path, bytes, { key?, ok, err })` — write a whole file (parent directories created, an existing file replaced whole; at most 1 MiB). `ok` arm: NO payload fields (`{ kind: \"wrote\" }`) — a successful write has nothing to report. `err` reasons: `io_failed`, `rejected`.\n- `Cmd.appendFile(path, bytes, route)` appends one bounded payload; `Cmd.statFile(path, route)` returns `{ exists, size, mtimeMs }`; `Cmd.deleteFile(path, route)` deletes one file with a payload-less `ok` arm and routes a missing path as `not_found` (a final symlink is unlinked without deleting its target). `Cmd.readFileStream(path, { key?, chunk, done, err })` delivers 256-KiB chunks then the total; reissuing or cancelling its key silently replaces/drops the read. Atomic exports open with `Cmd.writeFileStream(key, path, route)`, send one acknowledged `writeFileChunk` (≤1 MiB) at a time, then `writeFileClose`; a duplicate sink rejects, overlapping chunk/close routes `out_of_order`, and sink cancellation is loud. Stream chunks spill to the session blob store for byte-identical replay. Raw paths under this app's resolved data/config/cache/state/logs/temp roots need no grant; every external path requires the `filesystem` permission. The runtime resolves existing parents and symlinks before checking (an in-root symlink pointing out is external); NS1074 catches certainly-external literals.\n- `Cmd.fetch({ url, method?, headers?, body?, timeoutMs? }, { key?, ok, err })` — a buffered HTTP(S) exchange. `ok` arm: exactly two fields, one `number` and one `Uint8Array` (`{ kind: \"fetched\", status: number, body: Uint8Array }`) — matched by type, so the names are yours. The status is the real HTTP status: a 404 is still `ok` (an HTTP-level error is a delivered response). `err` reasons: `connect_failed`, `tls_failed`, `protocol_failed`, `timed_out`, `rejected`, and `truncated` (the body exceeded the engine's 256 KiB buffered bound — never delivered silently cut). The spec is an inline object: `url` bytes (≤ 2 KiB), `method` one of `\"GET\" | \"POST\" | \"PUT\" | \"DELETE\" | \"PATCH\" | \"HEAD\"` (default GET), `headers` an inline flat record — names are compile-time ASCII, values are string literals OR runtime bytes (`{ authorization: bearerToken(model.apiKey), \"content-type\": \"application/json\" }` — how a launch-supplied key rides an `Authorization` header; ≤ 8 headers, ≤ 1 KiB total, NS1029/NS1030), `body` bytes (≤ 64 KiB), `timeoutMs` a positive integer literal (engine default when omitted).\n- `Cmd.clipboardWrite(bytes)` — put bytes on the system clipboard, fire-and-forget: there is no routing, and a refused or over-bound write is dropped by design.\n- `Cmd.clipboardRead({ key?, ok, err })` — read the clipboard. `ok` arm: one `Uint8Array` field with the text. `err` reasons: `failed` (no clipboard service, over-bound content, pasteboard error), `rejected`.\n- `Cmd.showNotification({ id?, title, subtitle?, body?, actionLabel?, actionCommand? })` — request a desktop notification, fire-and-forget: `title` is required bytes (1–128), `subtitle`, `id`, `actionLabel`, and `actionCommand` are optional bytes (up to 128), and `body` is optional bytes (up to 1024). A nonempty `id` replaces the app's earlier notification with that id where the OS supports replacement. `actionLabel` and `actionCommand` must be supplied together; activation dispatches `actionCommand` through the ordinary app-command path while the process is running. The host validates every field and command name before entering the platform service; invalid or unavailable requests fail closed, and Focus / Do Not Disturb and user settings remain authoritative after acceptance. Fake execution and session replay never display one, while the null test platform can activate a recorded notification deterministically.\n- `Cmd.openExternalUrl(url)` — open an HTTP(S) URL in the system browser, fire-and-forget. `url` is bytes; the runtime validates it and enforces `security.navigation.external_links` before entering the platform service. Invalid, denied, or unavailable requests fail closed; fake execution and replay never open one.\n- `Cmd.revealPath(path)` — reveal a byte path in Finder, Files, or Explorer, fire-and-forget. Runtime path validation stays in force; invalid or unavailable requests fail closed, and fake execution/replay never reveal one.\n- `Cmd.credentials.set(key, secret, { key?, ok, err })` / `Cmd.credentials.get(key, route)` / `Cmd.credentials.delete(key, route)` — app-scoped Keychain on macOS/iOS, Secret Service/libsecret on Linux, Credential Manager on Windows, and AndroidKeyStore-wrapped authenticated ciphertext on Android. The authored credential key is a NUL-free string (UTF-8, ≤256 bytes); the manifest app id (≤128 bytes) supplies the service namespace, and secrets are `Uint8Array` values through 2,560 bytes (the cross-platform floor set by WinCred). Set/delete deliver empty bytes on `ok`; get delivers secret bytes; the closed err reasons are `miss`, `denied`, `locked`, `io_failed`, `over_bound`, and `rejected`. Declare BOTH `\"credentials\"` in `app.zon` capabilities and permissions; NS1071 warns when the capability is absent, NS1072 errors when permission is absent, and NS1073 reserves `core.credentials.*` for the typed factories. Never retain a returned secret in Model: consume its Msg immediately to construct the next effect. Recording stores only a successful get's length, per-session salt, and a placeholder digest independent of the secret (never the secret or a blob); replay delivers deterministic same-length placeholder bytes, and devhost keeps an in-memory map while printing only `<redacted, N bytes>`.\n- `Cmd.formatLocalTime(timestampMs, \"date\" | \"time\" | \"datetime\", { key?, ok, err })` — format epoch milliseconds (the same unit `Cmd.now` produces) through the host's current locale and local time zone; `ok` carries localized UTF-8 bytes. This is deliberately an effect rather than a pure helper: locale/timezone are ambient state, so the observed bytes journal and replay without consulting the replay machine. Err reasons are `invalid_request`, `unsupported`, and `failed`.\n- `Cmd.delay(key, ms, \"fired\")` — a keyed ONE-SHOT timer: dispatches the named arm once, `ms` from now, with the fire time (ms) as its single number payload (the same arm shape `Cmd.now` and `Sub.timer` target). Re-issuing a live delay key re-arms it from now — that is the debounce discipline (`Cmd.delay(\"autosave\", 800, \"save_now\")` on every keystroke, one fire after the pause). `Cmd.cancel(key)` drops it silently. The interval is 1ms to one year; a literal outside that stops the build (NS1030).\n\nModel persistence is capability-gated. Without `\"persist\"`, NS1028 warns that `Cmd.persist()` has no linked host binding; declaring the capability without issuing the command produces the inverse warning. C","skill-data/native-ui/SKILL.md":"---\nname: native-ui\ndescription: Authoring guide for the Native markup (.native) view tier of native-rendered Native SDK apps. TypeScript cores are the default and use this skill together with ts-core; Zig cores are an explicit alternative and this guide labels their lower-level UiApp recipes. Use when building or modifying native UI (widgets, layout, bindings, messages), writing .native files, wiring views to Model/Msg/update, testing markup views, or verifying a native app through the automation harness.\n---\n\n# Author native UI with Native markup\n\nA native-rendered Native SDK app is a markup view plus an app core. The primary authoring path is:\n\n- `src/app.native` — the entire UI: elements, layout, bindings, message dispatch.\n- `src/core.ts` — `Model`, the discriminated `Msg` union, `update(model, msg)`, effects, subscriptions, and derived binding helpers.\n- `app.zon` — identity, windows, assets, permissions, and policy.\n\nThe TypeScript core is checked and compiled ahead of time to native code; no JS runtime ships in the binary. Load `native skills get ts-core` beside this skill for every default app implementation. A Zig core (`src/main.zig`) is first-class when explicitly selected with `native init --template zig-core`; the Zig-only wiring, builder, and runtime-extension recipes below are labeled so they are not mistaken for the default.\n\nThe markup compiles to the same widget tree a hand-written `canvas.Ui(Msg)` builder view would produce: identical structural widget ids, identical typed handler table. Markup can never mutate state — it binds values and dispatches messages; all logic lives in the app core, whichever core language the existing tree uses.\n\nEditors highlight `.native` markup well in HTML mode — the default scaffold writes no editor config, so add `.vscode/settings.json` with `\"files.associations\": {\"*.native\": \"html\"}` yourself, or scaffold with `native init --full`, which writes it.\n\nStart a new app with `native init` (zero-config: `app.zon` + `src/core.ts` + `src/app.native` + assets; the CLI generates the build graph). Use `examples/chatbot`, `examples/soundboard`, and `examples/system-monitor` as substantial TypeScript references. The `native check|dev|test|build` verbs drive any app directory shaped this way.\n\n## Menu-bar status items (TypeScript default)\n\nExport `statusItem(model: Model): StatusItemState` from `src/core.ts`. The generated launcher installs icon/tooltip/click-open commands plus the boot presentation/menu, re-derives it after every committed update, and patches presentation/menu independently. Import `StatusItemState` from `@native-sdk/core/events`. Commands route through the core's `commandMsg(name): Msg | null` export. There is no app-owned Zig wiring and `statusItem` is automatically marked shell-bound for the markup lint.\n\n```ts\nimport { asciiBytes, utf8Bytes } from \"@native-sdk/core\";\nimport { type StatusItemState } from \"@native-sdk/core/events\";\n\nexport function statusItem(model: Model): StatusItemState {\n  return {\n    iconPath: asciiBytes(\"assets/menu-bar.svg\"),\n    tooltip: utf8Bytes(\"Player status\"),\n    activationCommand: asciiBytes(\"app.refresh\"),\n    alternateActivationCommand: asciiBytes(\"player.toggle\"),\n    openCommand: asciiBytes(\"app.refresh\"),\n    presentation: { title: model.playing ? utf8Bytes(\"MB PLAY\") : utf8Bytes(\"MB\"), width: 62, tone: \"normal\", iconOpacity: 1, monospaced: true, fontSize: 13, fontWeight: \"semibold\" },\n    items: [\n      { id: 1, label: utf8Bytes(\"Open\"), command: asciiBytes(\"app.open\"), separator: false, enabled: true, detail: asciiBytes(\"\"), role: \"command\", key: asciiBytes(\"\"), modifiers: { primary: false, command: false, control: false, option: false, shift: false } },\n      { id: 0, label: asciiBytes(\"\"), command: asciiBytes(\"\"), separator: true, enabled: false, detail: asciiBytes(\"\"), role: \"command\", key: asciiBytes(\"\"), modifiers: { primary: false, command: false, control: false, option: false, shift: false } },\n      { id: 2, label: model.playing ? utf8Bytes(\"Pause\") : utf8Bytes(\"Play\"), command: asciiBytes(\"player.toggle\"), separator: false, enabled: true, detail: utf8Bytes(\"configured ✓\"), role: \"agent\", key: asciiBytes(\"\"), modifiers: { primary: false, command: false, control: false, option: false, shift: false } },\n    ],\n  };\n}\n```\n\nPresentation fields style the persistent menu-bar item. Inside its dropdown, `metric` carries prominent primary/secondary text, `segmented` carries up to eight typed command options, and `chart` carries 1–32 bounded values with captions and accessibility. These are generic composable row payloads, not text conventions.\n\n## App wiring (Zig cores and extensions only)\n\nA default TypeScript app writes none of this: generated wiring connects `src/core.ts`, `src/app.native`, and `app.zon`. Use the following only when the existing app has a Zig core or the task explicitly requires owned runtime wiring.\n\n```zig\nconst HabitsApp = native_sdk.UiApp(Model, Msg);\n\npub fn main(init: std.process.Init) !void {\n    // `create` heap-allocates the multi-MB app struct and constructs the\n    // Model in place — neither ever rides the stack (avoid `App.init(alloc,\n    // model, ...)`: its by-value Model is a stack-overflow trap once the\n    // Model grows).\n    const app_state = try HabitsApp.create(std.heap.page_allocator, .{\n        .name = \"habits\",\n        .scene = shell_scene,             // one window, one gpu_surface view\n        .canvas_label = \"habits-canvas\",  // must match the ShellView label\n        .update = update,\n        .markup = .{\n            .source = @embedFile(\"app.native\"),\n            .watch_path = \"src/app.native\", // dev hot reload; omit in release\n            .io = init.io,\n        },\n    });\n    defer app_state.destroy();\n    app_state.model = initialModel(); // boot state: assign through the pointer\n    try runner.runWithOptions(app_state.app(), .{ ... }, init);\n}\n```\n\n(`create` requires every Model field to carry a default; the model starts as `.{}` and boot state is assigned through the returned pointer. Tests that instantiate the app per fixture should use `create`/`destroy` too — a runtime-built Model passed to `init` by value crashes the test stack once models get large.)\n\nThe runtime owns the loop: install on first GPU frame, presentation, resize, pointer/keyboard dispatch into `update` + rebuild. With `watch_path` set, editing the `.native` file while the app runs hot-reloads the view within ~2s, preserving model state and widget ids; parse failures keep the last good view and set `app_state.markup_diagnostic` (line/column/message).\n\n**Release: compile the markup at comptime.** `canvas.CompiledMarkupView(Model, Msg, source).build` parses the `.native` source entirely at compile time and produces the identical tree (same ids, handlers, dispatch) with no parser in the binary; markup or binding mistakes become compile errors with line/column. Hand it to `.view`, and gate the runtime engine per build mode:\n\n```zig\nconst dev = @import(\"builtin\").mode == .Debug;\nconst App = native_sdk.UiAppWithFeatures(Model, Msg, .{ .runtime_markup = dev });\nconst CompiledView = canvas.CompiledMarkupView(Model, Msg, @embedFile(\"app.native\"));\n// options:\n.view = CompiledView.build,\n.markup = if (dev) .{ .source = ..., .watch_path = \"src/app.native\", .io = init.io } else null,\n```\n\nWith both set (dev), the compiled view renders until the watched file first changes, then the interpreter hot-reloads it. See the native reference fixture in `tests/ts-core/habits_reference.zig` for this Zig wiring pattern.\n\n### Webview panes: canvas + live web content in one window\n\nFor TypeScript apps, export `webPanes(model: Model): readonly WebViewPane[]` from `core.ts`. Import the descriptor from `@native-sdk/core/events`; return `label`, nullable `anchor`, `url`, `x`, `y`, `width`, `height`, and `reloadToken`. The label identifies an existing manifest WebView; the anchor identifies a widget label such as `<panel label=\"preview-pane\" grow=\"1\"/>`. See `examples/canvas-preview` for pane, frame, and tray coordination in one compiled core. The native adapter copies bytes and validates finite geometry and safe integer tokens; the OS owns WebView hosting and navigation policy.\n\nFor all u64 reload states, return `readonly ExactWebViewPane[]` instead. Its `reloadToken` is decimal ASCII bytes; the host validates the full u64 range without rounding. `WebViewPane` retains its existing safe-number token.\n\n### TypeScript terminal panes\n\nBind `<terminal pty=\"{shellKey}\" scrollback=\"{scrollback}\" on-terminal=\"terminalChanged\" label=\"Shell\" autofocus=\"true\" grow=\"1\"/>` to a `Uint8Array` helper or field naming the same key as `Cmd.ptySpawn`. Import `TerminalState` from `@native-sdk/core/events` for the message payload. The native host resolves the name to its PTY capability; engine identities never cross through a JavaScript number. Both view backends preserve the complete four-field state report. A bound slot remains reserved through exit until app teardown, keeping the ended screen stable across rebuilds. Restarting the same name reuses its slot; the existing four-slot budget applies. Keep `.terminal_sessions = true` and the lazy Ghostty pin in the app's owned build, as in `examples/workbench`. Native integer PTY bindings remain supported.\n\nFor Zig cores and extensions:\n\nDeclare the webview in the scene next to the gpu_surface (parent it to the canvas view), reserve its region with an empty panel carrying a semantics label, and let `Options.web_panes` snap the webview to that widget's layout frame while the model drives navigation:\n\n```zig\nconst shell_views = [_]native_sdk.ShellView{\n    .{ .label = \"app-canvas\", .kind = .gpu_surface, .fill = true, .gpu_backend = .metal },\n    .{ .label = \"preview\", .kind = .webview, .parent = \"app-canvas\", .url = \"https://example.com/\", .x = 240, .y = 76, .width = 704, .height = 548 },\n};\n// view: ui.panel(.{ .grow = 1, .semantics = .{ .label = \"preview-pane\" } }, .{})\nfn panes(model: *const Model, out: []App.WebViewPane) usize {\n    out[0] = .{ .label = \"preview\", .anchor = \"preview-pane\", .url = model.url(), .reload_token = model.reload_token };\n    return 1;\n}\n// options: .web_panes = panes,\n```\n\nURL changes navigate; bumping `reload_token` reloads the same URL (the CenterPane/Preview-tab shape). Pane URLs must pass `security.navigation.allowed_origins`. Panes reconcile against the runtime's live webview state on every rebuild and presented frame, so shell relayouts cannot detach them. `examples/canvas-preview` is the live reference; `zig build test-canvas-preview-smoke` verifies it.\n\n### Menu-bar extra (status item)\n\nThis is the Zig-core/custom-wiring form. Default TypeScript apps use the exported `statusItem(model)` helper above.\n\n`Options.status_item` installs a macOS `NSStatusItem` once, on the installing frame; its menu items dispatch commands through the same `on_command` mapping the toolbar and menus use (source `.tray`):\n\n```zig\n.status_item = .{ .title = \"ZN\", .tooltip = \"My App\", .items = &.{\n    .{ .id = 1, .label = \"Refresh\", .command = \"app.refresh\" },\n    .{ .separator = true },\n    .{ .id = 2, .label = \"Quit\", .command = \"app.quit\" },\n} },\n```\n\nFor a LIVE menu-bar extra (an open-count badge in the title, a latest-items dropdown), add `Options.status_item_fn` — the `web_panes` pattern: consulted on install and after every rebuild, re-applied only when its output changed (title and menu patch independently; the static `status_item` keeps icon/tooltip). Format derived strings into the provided scratch; item `command`s dispatch through `on_command` exactly like static items:\n\n```zig\nfn statusItem(model: *const Model, scratch: *App.StatusItemScratch) App.StatusItemState {\n    const title = std.fmt.bufPrint(&scratch.title_buffer, \"ZN {d}\", .{model.open_count}) catch \"ZN\";\n    scratch.items[0] = .{ .id = 1, .label = \"Refresh\", .command = \"app.refresh\" };\n    var count: usize = 1;\n    for (model.latest(), 0..) |issue, i| { // per-row commands: map \"issue.select.N\" in on_command\n        scratch.items[count] = .{ .id = @intCast(10 + i), .label = issue.title, .command = issue.select_command };\n        count += 1;\n    }\n    return .{ .title = title, .items = scratch.items[0..count] };\n}\n// options: .status_item_fn = statusItem,\n```\n\nTitle updates retitle the live `NSStatusItem` button without re-creating it; platforms without a tray-title seam keep menu updates and log the title gap once.\n\n### Windowed lists in markup\n\nUse `<virtual-window>` to resolve one retained range before rendering, then `<virtual-list>` to request only its rendered rows. The window declares a stable literal `id`, an `as` range name, an `item-count`, and either `item-extent` or an exported Model-first `extent-estimate` helper. Optional `gap`, `overscan`, `viewport-fallback`, `index-base`, and `anchor=\"leading|trailing\"` describe the complete content geometry.\n\n```html\n<virtual-window id=\"records\" as=\"range\" item-count=\"{loaded}\" item-extent=\"48\" overscan=\"4\">\n  <column>\n    <virtual-list window=\"range\" each=\"rows\" as=\"row\" grow=\"1\" label=\"Records\">\n      <list-item key=\"{row.id}\" focusable=\"true\" on-press=\"select:{row.id}\">{row.title}</list-item>\n    </virtual-list>\n    <status-bar>{range.first_visible_index}–{range.last_visible_index}</status-bar>\n  </column>\n</virtual-window>\n```\n\nIn a TypeScript core, `rows(model: Model, range: VirtualListRange): readonly Row[]` takes the complete SDK range from `@native-sdk/core/events` and returns exactly `range.end_index - range.start_index` rows in rendered-index order. An extent estimator has shape `rowExtent(model: Model, index: number): number`; its retained identity survives rebuilds. Each row has one keyed root. Ordinary scroll styling, `on-scroll`, and reach-start/end messages belong on `virtual-list`.\n\nEach window has one root and exactly one consuming list; windows cannot nest. A range can pass through a template argument, and secondary window views have their own retained descriptors. The full range exposes visible and rendered bounds, content extent, row extent, and offset; use it for footer text without copying scroll state into the model. See `examples/feed` for variable-height estimates, stable row queries, and retained selection.\n\n### Native scrolling (macOS)\n\nZero app code: on macOS every non-virtualized `scroll` region — and every windowed virtual list (`ui.virtualList`), whose driver content size is the full virtual extent — is driven by an invisible `NSScrollView` — OS momentum and the system overlay scrollbar — while the engine renders the content. `widget.value` stays the offset of record, so the rebuild reconcile rule (\"user offset survives rebuilds until the source offset changes\"), automation snapshot offsets (`scroll=[offset=..]`), and `Options.sync` all work exactly as before; the engine-drawn scrollbar simply stops painting for natively driven regions. Programmatic scrolls still work: change the source offset (or scroll via keyboard/automation) and the runtime pushes it into the native scroller. GTK/Win32 and mobile embeds keep the engine's wheel physics unchanged. Nested-scroll saturation handoff (inner region exhausted, outer continues) is per-region native today: the inner region stops at its edge like a standalone scroller.\n\n**Overscroll is off by default, per region, on both paths.** Scroll regions pin at their content edges — the native scroller gets non-elastic edges, the engine's wheel/kinetic physics clamp, and kinetic motion stops cleanly at the boundary. Bouncing is a per-region opt-in: `overscroll=\"rubber_band\"` in markup (the `scroll` element only — the validator rejects it elsewhere with a teaching error) or `ElementOptions.overscroll = .rubber_band` in Zig views. The `ScrollPhysics.overscroll` design token (`ScrollPhysicsOverrides` in a theme) flips the app-wide default; per-region values override it, and `.none` pins a region regardless of the token. The rubber-band shape — excursion bound, resistance, spring-back rate — stays themable through the `rubberband_*` physics tokens.\n\n### Context menus: one declared menu, platform-decided presentation\n\nAuthors write ONE menu; the platform decides how it presents. The default is the real OS context menu at the pointer — `NSMenu` on macOS, `TrackPopupMenu` on Windows, `GtkPopoverMenu` on Linux — and the selection dispatches the item's typed `Msg`. On hosts without a native menu presenter (the mobile toolkit hosts and embed hosts today), the SAME declared items present automatically as an anchored canvas surface at the click point, with the standard anchored-surface behavior (Escape and outside-click dismiss, late z-pass, window clipping). Never two authored menus, never a canvas imitation where the OS menu exists.\n\nMarkup declares the menu as a `<context-menu>` element — a DIRECT child of the interactive element whose right-click it answers (a hit-target kind, or an element made interactive by `on-press`/`on-double-press`/`on-toggle`/`on-hold`/`on-drag`). It is metadata, not content: it renders nothing in the row's flow. Children are `menu-item`s (`on-press` required, `disabled` optional, the text content is the label) and bare `<separator/>`s, with `if`/`else`/`for` around them to swap or repeat items — a menu whose items all evaporate at runtime simply declares no menu (the All Notes row pattern). Conditional MENUS are spelled as conditional ITEMS: the `<context-menu>` itself takes no attributes and cannot sit behind a structure tag. No submenus: the platform channel carries flat items (label, enabled, separator) only.\n\n```html\n<list-item on-press=\"open_note:{n.id}\" label=\"{n.title}\">\n  <text grow=\"1\">{n.title}</text>\n  <context-menu>\n    <if test=\"{n.deleted}\">\n      <menu-item on-press=\"restore_note:{n.id}\">Restore</menu-item>\n      <menu-item on-press=\"purge_note:{n.id}\">Delete Permanently</menu-item>\n    </if>\n    <else>\n      <menu-item on-press=\"copy_note_id:{n.id}\">Copy</menu-item>\n      <menu-item on-press=\"trash_note:{n.id}\">Delete</menu-item>\n    </else>\n  </context-menu>\n</list-item>\n```\n\nThe compiled TypeScript view supports one direct menu with at most 32 labeled `menu-item` leaves and bare separators. Items require `on-press` and may bind `disabled`; structure tags inside menus currently remain on the native markup backend. The menu is copied into the native tree arena before the compiled result expires.\n\nThe Zig builder's mirror is `ElementOptions.context_menu` — per-widget items in the chrome-menu shape with typed messages:\n\n```zig\nui.listItem(.{\n    .on_press = Msg{ .select = entry.index },\n    .context_menu = &.{\n        .{ .label = \"Open Section\", .msg = Msg{ .select = entry.index } },\n        .{ .separator = true },\n        .{ .label = \"Refresh Dashboard\", .msg = .refresh },\n    },\n}, entry.title)\n```\n\nThe deepest declaring widget on the hit route wins; disabled items and separators are fine (`enabled = false`, `.separator = true`). Zero-code defaults need no declaration: editable text fields present the standard Cut / Copy / Paste / Select All menu wired to the existing clipboard actions, and a selected static text presents Copy (these defaults are presenter-only — without an OS menu they degrade to the keyboard clipboard paths). Touch long-press is design-noted for the mobile embeds: the iOS host's under-slop `Pending` touch state is the timer seam, pending a secondary-button leg in the embed ABI and `UIEditMenuInteraction` presentation.\n\nAutomation drives the native path honestly: snapshots list every widget's declared items in invocation order (`context_menu=[\"Rename\",\"Delete\"]`, separators keep their slots, disabled items say so), `widget-context-press <view> <id>` performs the real secondary click (presenting the menu), and `widget-context-menu <view> <id> <item-index>` invokes an item — the selection dispatches as the same `context_menu_action` platform event a real pick produces; the journal records the complete widget invocation so replay reconstructs its pending request before selection, because the OS menu's tracking loop cannot be driven programmatically. Dead invocations fail by name (undeclared menu, index out of range, separator slot, disabled item). `examples/notes` row menus and `examples/gpu-dashboard` nav rows carry live menus; `zig build test-example-notes`, `test-example-gpu-dashboard`, and the runtime context-menu suite verify dispatch, the fallback surface, and the verb.\n\n## Elements\n\n| Markup | Widget | Notes |\n| --- | --- | --- |\n| `row`, `column` | flex containers | main axis horizontal / vertical |\n| `stack`, `panel`, `card` | overlay containers | children stack on top of each other — `gap` can never space them and is a validation error (put a `column`/`row` inside for flow) |\n| `scroll` | scroll_view | wrap multiple children in a `column` inside it |\n| `list`, `grid` | list, grid | vertical stack / cell grid |\n| `tabs`, `toggle-group`, `button-group`, `radio-group`, `breadcrumb`, `pagination` | row containers | children flow horizontally. Give every `radio-group` an accessible `label`; it is one logical `radiogroup`: descendant radios at any nesting depth share one Tab stop; arrows wrap while Home/End move to the edges, focus and selection move together, and selecting one clears the rest of the nearest group scope |\n| `table` > `table-row` > `table-cell` | table, data_row, data_cell | rows only inside a table, cells only inside a row (for/if wrappers are fine); cells are text leaves, dispatch with `on-press` |\n| `dropdown-menu` | dropdown_menu | vertical menu surface; children are `menu-item`s. `anchor=\"below\\|above\"` floats it against its PARENT's frame (see Pickers): late z-pass above the whole tree, window-clipped, auto-flipping at the window edges, zero flow space. Pair with `on-dismiss` |\n| `accordion` | accordion | header via `text` attr; children show while `selected`, dispatch `on-toggle` |\n| `alert`, `bubble` | surfaces | `alert` title via `text` attr; children stack inside. `bubble` hugs its message up to 80% of the thread (`ghost` exempt; explicit `width` wins) and takes one `<reactions>` child — the reaction pill straddling its bottom edge, one text run, dock via `text-alignment` (default `end`); `text=` on bubble itself is a teaching error (that channel belongs to the pill). Grouped runs are spacing, not vocabulary: 8 gap within a sender's run, 32 between turns |\n| `dialog`, `drawer`, `sheet` | modal surfaces | root-relative and unaffected by ancestor scroll or clipping: dialogs center in the viewport, drawers span its width and dock to the bottom, and sheets span its height and dock to the right — title via `text` attr, wrap in `<if>` to show conditionally |\n| `resizable` | resizable | engine-managed drag handle; `width` sets the initial width |\n| `split` | split | two-pane horizontal splitter: exactly two element children (nest splits for more panes), the engine synthesizes the draggable divider between them. `value` binds the model-owned first-pane fraction (0 lays out at 0.5), `on-resize` names an f32 Msg variant dispatched with every applied fraction (echo it back through `value` — see Splitters), `min-width` on the panes bounds the drag, `gap` sets the divider band thickness. The divider is focusable: Left/Right (Shift for bigger steps) adjust, Home/End jump to the clamp edges |\n| `tree` | tree | disclosure-tree container (vertical flow): descendant rows carrying `role=\"treeitem\"` — at ANY nesting depth — form one roving keyboard focus set with the ARIA tree keymap. Up/Down walk visible rows (selection follows focus through each row's `on-press`), Left collapses an expanded row or moves to the parent row, Right expands a collapsed row or moves to the first child row, Home/End jump to the edges, Enter/Space activate. Expandable rows bind `expanded` and `on-toggle`; the model owns selection and expansion (collapsed children are simply not rendered) |\n| `text`, `badge`, `tooltip` | text leaves | text content, `{}` interpolation allowed; `text` line policy via `wrap` (`\"true\"` word-wraps; `\"false\"`/unset paint one honest line, overflow eliding by default — `overflow=\"clip\"` opts out), and `text` alone takes the typography rungs `size=\"heading\"`/`size=\"display\"` (themable token steps above title — section headings, hero stats, timer numerals). `tooltip` with `anchor=\"above\\|below\"` floats against its parent (the stack wrapping trigger + tooltip, the dropdown pattern) and the RUNTIME owns its visibility — hover intent on the trigger: shows after `tooltip-delay` ms (default 600; `\"0\"` = instant) and immediately on keyboard focus; hides on leave, focus departure, Escape, or a press of the trigger (a press also closes the warm window), and a shared 400ms warm window after a pointer-hovered tooltip hides on leave (the only hide that warms) shows the next trigger's tooltip instantly; the model never hears hover. These are shadcn/ui's defaults (Base UI). Without `anchor` it stays a static leaf that paints whenever the view renders it |\n| `text` > `span` | inline styled runs | mixed-style text in ONE wrapped paragraph: span children style runs with `weight=\"regular\\|medium\\|bold\"`, `mono`, `italic`, `scale` (a positive multiplier on the paragraph's base size — inline headings, hero stats), `underline`, `foreground` (token name); `{bindings}` interpolate inside spans; whitespace between runs collapses to a single space (none = the runs abut); spans do not nest, take no events, and the paragraph announces as one text run — see \"Rich text\" |\n| `button`, `toggle-button`, `list-item`, `menu-item`, `toggle`, `switch`, `select`, `avatar` | text-bearing controls | label is the text content; `button`, `toggle-button`, `list-item`, and `menu-item` also take `icon=\"save\"` — a vector icon drawn inline (buttons/toggle-buttons before the label, icon-only when the content is empty: add a `label`; list/menu items as a leading slot), ONE hit target whose icon follows the element's enabled/disabled tint (no overlay stacking, no duplicated `on-press`); tab strips are `toggle-button` children, so tabs get icons this way; `select` shows `placeholder` while empty and dispatches `on-press`; `avatar` renders initials, or a runtime image via `image=\"{binding}\"` (see the Images section) |\n| `segmented-control` | exclusive-choice control | standalone text-bearing segment; bind `selected`, dispatch `on-press`, and optionally add a vector `icon`; compose several in a row, or use `tabs` for the strip-and-panel pattern |\n| `checkbox`, `radio`, `slider`, `progress` | value controls | `checked`, `value` (a 0..1 fraction on slider and progress; progress clamps out-of-range values at render, never an error); checkbox/radio visible labels are text content (`<checkbox>Done</checkbox>`), with `text=\"...\"` as the equivalent binding-friendly form and `label=` alone naming one for accessibility without drawing a label; a radio selection TRANSITION (pointer, Space/Enter, grouped navigation, or accessibility selection) dispatches `on-change` when bound, then `on-toggle`, then `on-press` for compatibility — reactivating the already-checked radio has no new `on-change` edge, though a legacy fallback still receives the activation; a slider's `value` follows the source when it MOVES (model-driven progress renders every rebuild) and keeps the user's drag while the source replays the same value — use `slider` for seek bars, `progress` for display-only; a markup slider's `on-change` dispatches a PLAIN Msg with no value payload — mirror the applied value into the model with `Options.sync` (the Zig builder's `on_value = Ui.valueMsg(.tag)` does deliver the applied f32) |\n| `text-field`, `input`, `search-field`, `combobox`, `textarea` | text entry | `placeholder`; edits via `on-input`, enter via `on-submit` on single-line kinds; in a default `textarea`, Enter (and Shift+Enter) inserts a newline and `on-submit` dispatches on primary+Enter (cmd on macOS, ctrl elsewhere). A chat composer opts into `submit-on-enter=\"true\"`: plain Enter submits, Shift+Enter remains a newline, and the primary chord still submits. `search-field` carries a built-in trailing clear affordance whenever it holds text (press the x, or Escape while focused — both clear through the text-edit path, so `on-input` hears it; no attribute, no external Clear button needed) |\n| `status-bar` | status bar | text leaf: content only, no children |\n| `separator`, `spacer` | separator, flexible space | `separator` is axis-aware: a horizontal rule in a `column`, a thin vertical divider in a `row`; give `spacer` a `grow` |\n| `skeleton`, `spinner` | loading leaves | size `skeleton` with `width`/`height` |\n| `icon` | vector icon leaf | `name` picks the icon: a bare literal is a curated built-in stroke icon (compile-checked; 49 names: search, plus, x, x-circle, check, check-circle, chevron-up/down/left/right, arrow-up/down/right, menu, panel-left, panel-right, settings, terminal, wrench, trash, edit, copy, external-link, play, pause, skip-back/forward, shuffle, repeat, music, volume, info, alert, download, save, folder, folder-open, file-text, sun, moon, eye, clock, git-pull-request, git-merge, git-branch, circle-dot, archive, refresh-cw, send); `app:<name>` reaches an icon the app registered at boot with `canvas.icons.registerAppIcons` (declare the table as `pub const app_icons` on the app root so `native check` verifies the name against the model contract), and one `{binding}` defers the choice to model data - an unknown resolved name draws the missing-icon fallback (a slashed circle) with a Debug warning naming the value; tint with `foreground`, size with `width`/`height` |\n| `media-surface` | media surface leaf | composites a texture produced OUTSIDE the widget tree (video decoder, camera, an external renderer like mpv) into the layout like any widget — clipped, z-ordered, rounded. `surface=\"{binding}\"` (required) binds the model-owned u64 surface id a Zig-tier producer targets (`runtime.acquireMediaSurfaceProducer` pushes RGBA8 frames, latest-wins, paced by the presented-frame clock; 0 = unbound, draws nothing; usable ids are nonzero values below the reserved bit 63). No intrinsic size — give it `width`/`height` or `grow`; display-only (presses fall through); `label` it (pictorial content). Texture contents are presentation chrome: goldens, reference screenshots, and session replay show the deterministic id-derived placeholder, never producer frames |\n| `image` | runtime image leaf | draws a RUNTIME-REGISTERED image by its model-owned u64 ImageId — the id `Cmd.imageLoad` (TS) or `fx.loadImage`/`fx.registerImageBytes` (Zig) registered pixels under. `image=\"{binding}\"` (required) binds a model field/fn; ids are model data, never markup literals, and 0 draws nothing (store the id only when the load reports loaded — see the Images section). `source-x`/`source-y`/`source-width`/`source-height` select one atlas region in decoded-image pixel coordinates (declare all four). No intrinsic size — give it `width`/`height` or `grow`; display-only (presses fall through); `label` it (pictorial content) |\n| `code` | bare highlighted source/editor | `source=\"{binding}\"` (required) provides source text and `language=\"tsx\"` selects a literal lexer name; the component supplies no background, border, radius, shadow, or padding, so wrap it in a panel/card when chrome is wanted. It is read-only by default; `editable on-input=\"edit\"` opts into multiline editing while retaining highlighting. It wraps by default, `line-numbers` opts into logical line numbers, `added-lines=\"5\"` / `removed-lines=\"2-4\"` add Geist-style diff rows without changing copied source, and `wrap=\"false\"` preserves lines inside one horizontal scroll region. HTML-family highlighting distinguishes HTML/XML/SVG and JSX/TSX tags, attributes, strings, comments, and embedded expressions. Zig builder: `ui.code(CodeOptions, source)` |\n| `markdown` | rendered markdown subtree | leaf; `source` is one `{binding}` — see \"Markdown in markup\" |\n| `stepper` > `step` | composite stage track | `active=\"{index}\"` (required) derives each step's completed/active/pending state; steps are text leaves (no attributes) joined by connectors; stepper also takes `key`, `global-key`, `label` |\n| `timeline` > `timeline-item` | composite ledger list | items only inside a timeline (for/if fine); items are leaves — `title` (required), `description`, `meta`, `indicator`, `variant`, `connector=\"false\"` on the last item, `selected`; `on-press` makes the whole item pressable with a trailing chevron |\n| `chart` > `series` | composite data chart | series only inside a chart, and only series (the set is static — data varies through bindings); each series is a leaf — `values=\"{binding}\"` (required) names a model `[]const f32` iterable, `kind` is `line`/`area`/`bar` (literal), `color` a token name, `label` the semantics name; chart takes `y-min`, `y-max`, `grid-lines`, `baseline`, `x-labels`, `y-labels`, `hover-details`, `stroke-width`, box options, `label` — see \"Charts\" |\n| `context-menu` | consumed by its parent | right-click menu on its DIRECT parent (a hit-target kind or an element with `on-press`/`on-double-press`/`on-toggle`/`on-hold`/`on-drag`); metadata, never a flow child. Children: `menu-item`s (`on-press` required, `disabled` optional, no `icon`) and bare `separator`s, with `if`/`else`/`for` around them. Attribute-less; presents natively where the host has a menu presenter, as an anchored surface elsewhere — see \"Context menus\" |\n| `input-group` > `textarea` + `input-group-actions` | composite grouped input | the composer shape: ONE bordered field wrapping exactly one `textarea` (first — document order is focus order) plus an optional `input-group-actions` row of controls inside the same border. The group wears the focus ring for its focused descendant and the textarea's own chrome dissolves automatically, so the whole group reads as one field; the textarea keeps its full behavior (`text`, `placeholder`, `on-input`, `on-submit`, `autofocus`, and optional `submit-on-enter`). Group takes `label`, `width`, `height`, `min-width`, `grow`, `key`, `global-key`; the actions row takes `gap` and holds ordinary elements (`if`/`else`/`for` work — swap send for stop while streaming) — put a `<spacer grow=\"1\"/>` between leading and trailing controls (`Ui.inputGroup`/`Ui.inputGroupActions` are the Zig-view equivalents) |\n\nCapabilities still awaiting a separate admission or ruling are image-backed `icon_button` content, `data_grid` per-column cell templates, and `popover`/`menu_surface` anchoring. `segmented-control` is a first-class markup leaf, and vector icon buttons use `<button size=\"icon\" icon=\"...\" label=\"...\" on-press=\"...\" />`. Charts ARE expressible: `<chart>` with `<series values=\"{binding}\">` children binding model f32 iterables — see the Charts section (`.band` series and dynamic series composition stay with `ui.chart`). Built-in vector icons ARE expressible: `<icon name=\"search\"/>` (closed, compile-checked name set; `Ui.icon` is the Zig-view equivalent). App-authored icons: `canvas.svg_icon.parseComptime(@embedFile(\"icons/logo.svg\"))` parses any SVG in the common 24x24 stroke-icon dialect at comptime; register the parsed table once at boot with `canvas.icons.registerAppIcons(&table)` and draw by name via `ui.appIcon(.{...}, \"logo\")` or `ElementOptions.icon` — registered names render exactly like built-ins on every draw path. Markup `<icon>`/`<button icon>` stay built-in-only (the compiled engine validates names at comptime, where runtime registrations cannot exist — engine parity). Runtime images ARE expressible: `<image image=\"{cover}\" width=\"120\" height=\"80\" label=\"Cover art\"/>` and `<avatar image=\"{user_image}\">CT</avatar>` bind a `u64` ImageId model field/fn (the id is just model data; 0 draws nothing / keeps the initials fallback) — see the Images section; the `image` binding is required on the leaf (an unbound `<image>` is dead markup) and stays avatar+image scoped. Add all four `source-x`/`source-y`/`source-width`/`source-height` attributes to draw one decoded-pixel sub-rectangle from a shared atlas.\n\n## Attributes\n\nLayout: `gap` (flow containers only — stacking containers `stack`/`panel`/`card`/`alert`/`bubble`/`dialog`/`drawer`/`sheet`/`resizable` layer their children, so `gap` there is a validation error, not silence: wrap the children in a `column`/`row` inside; on `split` it sets the divider band thickness), `padding` (uniform), `grow`, `width`, `height` (definite: the element is exactly that size — intrinsic content neither shrinks nor silently overflows it; `resizable` treats `width` as the initial width), `min-width` (a floor WITHOUT `width`'s definite max — the element may grow past it but never shrink below; on split panes it bounds the divider drag), `max-width` (a ceiling WITHOUT `width`'s definite min — the element still shrinks with a narrow parent; use a growing child inside a centered row for a responsive content column), `wrap` (`text` only: `wrap=\"true\"` word-wraps at the width the element receives and reserves the wrapped height in columns; `wrap=\"false\"` and unset are honest single-line — one line whose overflow follows `overflow`), `overflow` (`text` only, a teaching error elsewhere: what a single line does with content that does not fit — `ellipsis`, the default, elides behind a trailing … measured with the same metrics paint uses, right for width-constrained list-row titles; `clip` hard-cuts at the frame for fixed-format content like a duration column where \"1…\" beats nothing; there is deliberately no overflow-visible), `text-alignment` (start|center|end — text leaves, status bars, surface titles; controls that own their label placement ignore it), `columns` (`grid` only: fixed column count, omit for the derived near-square grid; a teaching error elsewhere), `main` (start|center|end|space_between), `cross` (stretch|start|center|end), `virtualized`, `virtual-item-extent`, `anchor` (`dropdown-menu` and `tooltip`, literal `below`/`above`: floats the surface against its parent instead of the flow — auto-flips when the preferred side does not fit, height clamps to the chosen side, x clamps into the window; an anchored tooltip's visibility is runtime-owned hover intent, unlike the model-owned dropdown), `anchor-alignment` (with `anchor`: `start`/`end`/`stretch` — stretch also widens the surface to at least the anchor's width, the select-menu look), `anchor-offset` (with `anchor`: literal gap in points, default 4), `tooltip-delay` (`tooltip` only, beside `anchor` — a teaching error elsewhere or without it: hover-intent show delay in ms, default 600, `\"0\"` = instant; keyboard-focus reveals are always immediate), `overscroll` (`scroll` only, a teaching error elsewhere: `none` pins the region at its content edges — the shipped default via the `ScrollPhysics.overscroll` token — `rubber_band` lets it bounce past them on both the engine and native paths, `default` follows the token).\nAppearance/state: `variant` (default|primary|secondary|outline|ghost|destructive), `size` (the control scale default|sm|lg|icon on every sized element; on `text` also the typography rungs heading|display — named typography token steps (`heading_size` 28, `display_size` 48, themable like every token) for section headings and hero stats/timer numerals. The two axes stay apart: heading/display on a control is a teaching error naming text as their home, unknown values list the vocabulary, and numeric sizes are refused by design — retheme the typography tokens to move the whole scale), `disabled`, `checked`, `selected`, `value`, `placeholder`, `icon` (`button`, `toggle-button`, `list-item`, `menu-item`: vector icon drawn inline — buttons/toggle-buttons before the label, list/menu items as a leading slot; a teaching error anywhere else. A built-in name, `app:<name>`, or one `{binding}` resolving to such a name). **One size register per row**: every control class shares the control height at a given register (default 36, sm 31.5, lg 40.5 before density), so a toolbar/filter row reads as one height exactly when every control in it carries the SAME `size` — mixing `size=\"sm\"` buttons with a default field renders two heights in one row, and hand-sized pressable panels (`height=\"30\"`) never land on the scale; compose rows from real controls at one register.\nFocus: `autofocus` (focusable controls only — a teaching error elsewhere): moves keyboard focus to the element when it MOUNTS or when the bound value turns on, edge-triggered so holding it true never re-steals focus from the user. The TEA way to focus an editor on note-create (`<text-field autofocus=\"{editing}\" ...>` or mount the field under an `<if>` with `autofocus=\"true\"`; Zig views use `ElementOptions.autofocus`) and to give keyboard-first apps their first focus without a click.\nSemantics: `role` (listitem, treeitem, button, ...; `treeitem` also makes the row part of its tree's roving keyboard focus set), `label` (accessible name — it REPLACES the element's text content as the announced name, so snapshot greps and screen readers see the label, never the text; don't `label` an element whose visible text you grep for), `expanded` (tree rows: disclosure state, model-owned — omit on leaves). Accessible names are ENFORCED: an interactive control with no text content, no `text=`, and no `label=` is a validation error (icon-only controls need `label`; text-entry controls need `label` or `placeholder`), unknown/misused literal roles are errors (`role=\"tree\"` on a text leaf can never hold rows), unnamed avatars and labels duplicating the text content are warnings (`label=\"\"` marks an image decorative). Zig-built trees get the same discipline from `canvas.expectA11yAuditSweepClean` (missing names as the bridges would announce them, focusables clipped out of keyboard reach, identically labeled siblings) — adopt it next to the layout sweep.\nIdentity: `key` (sibling-scoped), `global-key` (parent-independent — use for items that move between containers, e.g. board cards; ids then survive reparenting).\nWindow chrome: `window-drag=\"true\"` (Zig: `.window_drag = true`) marks the element as a window-drag surface for hidden-titlebar windows — pressing its background or plain text/icons inside moves the WINDOW (drag starts only on actual movement), double-click zooms per the OS convention, and press-claiming children (buttons, fields) stay fully interactive via the ordinary press fall-through. macOS-only; elsewhere the press is dead space. See \"Hidden titlebar\" below.\nRender channel (Zig-only, no markup attributes): `ElementOptions.opacity` and `ElementOptions.transform` wrap the element's emitted commands without reflowing siblings — the defaults (1, identity) emit nothing, opacity 0 culls painting (pair with `disabled` when fading interactive content), and a transform moves both rendering and pointer hit-testing while accessibility frames stay at the layout frame. Pair with `UiApp.Options.animations` for tweening.\n\nNumbers are plain (`gap=\"12\"`), booleans are `true`/`false` or a binding.\n\nWhen children's minimum sizes exceed their container, debug builds log a `zero_canvas_layout` diagnostic naming the container, axis, and overflow in pixels — flex overflow is never silent. In Zig views, `.gap` on a stacking kind (`ui.panel(.{ .gap = 8 }, ...)`) logs a `zero_canvas_ui` warning in debug builds with the same lesson — it never fails the build.\n\n### Chips: exclusive selection with `selected=`\n\nThe chip pattern — an exclusive group where the model owns which one is active — is a `toggle-group` of `toggle-button`s (or plain `button`s) whose `selected=` binds the model:\n\n```html\n<toggle-group gap=\"2\" label=\"Theme\">\n  <for each=\"theme_prefs\" as=\"p\">\n    <toggle-button size=\"sm\" selected=\"{p == theme_pref}\" on-toggle=\"set_theme:{p}\">{p}</toggle-button>\n  </for>\n</toggle-group>\n```\n\nA `toggle-button` whose source asserts `selected` (this rebuild or the previous one) is model-driven: the source wins over the runtime's retained toggle on every rebuild, so exactly the model's selection is active — pressing a chip dispatches the Msg, the model moves the selection, and the old chip deactivates. Without a `selected=` that ever asserts, a `toggle-button` is uncontrolled: the runtime retains its pressed state across rebuilds (the multi-select formatting-bar case — bold/italic chips with zero app wiring). `button` with `selected=` is always model-driven (buttons never retain state) and dispatches `on-press`; `toggle-button` dispatches `on-toggle` (its activation is the toggle intent — an `on-press` there never fires). Model-driven chips need a handler that actually moves the model: a chip whose Msg is ignored keeps its retained press until the model asserts its `selected=`.\n\n### Pickers: select is the trigger — compose the options as an ANCHORED dropdown\n\n`select` and `combobox` are trigger controls, not complete pickers: `select` renders the closed dropdown shape (current value as content, `placeholder` while empty, `on-press` to open) and `combobox` is a text entry with a menu chevron — neither owns an options list. There is no `options=` attribute (the closed grammar has no list-valued attributes — list-shaped vocabulary is element children, the way `<context-menu>` declares its items); the options ARE the composition — a `dropdown-menu` of `menu-item`s under an `if`, beside the trigger inside a `stack`, floated with `anchor`:\n\n```html\n<stack>\n  <select placeholder=\"Pick a repo\" text=\"{current_repo}\" on-press=\"toggle_repo_picker\"/>\n  <if test=\"{repo_picker_open}\">\n    <dropdown-menu anchor=\"below\" anchor-alignment=\"stretch\" on-dismiss=\"close_repo_picker\">\n      <for each=\"repos\" key=\"name\" as=\"r\">\n        <menu-item on-press=\"pick_repo:{r.name}\" selected=\"{r.name == current_repo}\">{r.name}</menu-item>\n      </for>\n    </dropdown-menu>\n  </if>\n</stack>\n```\n\nHow the pieces fit, all model-owned (TEA):\n\n- **Open state is the model's.** `toggle_repo_picker` flips the bool; the surface exists only while the `if` renders it. There is no hidden engine open flag.\n- **`anchor` floats the menu.** The dropdown positions against its PARENT's frame (the `stack`, sized by the trigger): below it by default, flipping above when it doesn't fit and the other side has more room, height clamped to the chosen side, x clamped into the window. It consumes NO space in the flow (siblings never reflow), paints in a late z-pass above the whole tree, and escapes every ancestor scroll/clip region — window-clipped, not pane-clipped. `anchor-alignment=\"stretch\"` widens it to at least the trigger's width (the select look).\n- **`on-dismiss` closes it model-side.** Escape and a click outside the menu dismiss the surface and dispatch the Msg; `close_repo_picker` clears the bool. Escape works even when the trigger took no focus (a plain-text crumb): with no relevant focus chain it dismisses the topmost mounted anchored surface. The engine hides the surface immediately (the optimistic echo), and the next rebuild's source tree is truth — a model that keeps `open` true gets it back. Clicking the TRIGGER while open never double-fires: the anchor region owns its surface's toggling, so only `toggle_repo_picker` dispatches.\n- **Items close on pick.** `pick_repo` sets the value AND clears the open flag — a click inside the surface never dismisses.\n- **Keyboard**: ArrowDown/Up on a trigger with a mounted menu enters its selected visible row, or the first/last eligible row. Arrows move without wrapping; Home/End choose the visible edges. Enter/Space activate items. Tab/Shift+Tab dismiss without committing and return focus to the trigger; the next Tab continues through ordinary controls. Escape also dismisses from the trigger or menu and returns focus.\n- **Automation sees everything**: the floating menu and its items appear in widget snapshots at their real frames and `widget-click <item-id>` works while it is open.\n\n`combobox` composes the same way (the model filters the `for` source as the user types via `on-input`). Markup also supports `<popover>` and `<menu-surface>` with the same anchor and dismissal attributes. The Zig mirror is `ElementOptions.anchor`/`anchor_alignment`/`anchor_offset` + `on_dismiss` on `dropdown_menu`, `popover`, or `menu_surface`, built with `ui.eachCtx` for the options. Budget: at most 16 anchored surfaces may be mounted per view (`max_canvas_widget_anchored_per_view`, loud `error.WidgetAnchoredSurfaceLimitReached`) — an `anchor` inside a `<for>` body is almost always a mistake.\n\n### Splitters: split panes with a model-owned fraction\n\n`split` is the resizable two-pane seam: exactly two element children, and the engine synthesizes the draggable divider between them (resize cursor, focusable, ARIA separator whose value is the fraction). The fraction is MODEL-OWNED — the runtime applies each drag/keyboard step as an optimistic echo, dispatches `on-resize` with the applied fraction, and the model echoes it back through `value` so the next rebuild lays the panes exactly there:\n\n```html\n<split value=\"{sidebar_split}\" on-resize=\"sidebar_resized\">\n  <column min-width=\"150\">…sidebar…</column>\n  <split value=\"{list_split}\" on-resize=\"list_resized\">\n    <column min-width=\"220\">…list…</column>\n    <column min-width=\"280\">…editor…</column>\n  </split>\n</split>\n```\n\n- **`on-resize` names an f32 Msg variant** (`sidebar_resized: f32`); `update` stores it (`model.sidebar_split = fraction`). The delivered fraction is the value the runtime already applied and clamped, so echoing it never fights the reconcile.\n- **`min-width` on the panes bounds the drag** — the divider clamps so neither pane shrinks below its floor, on drag, keyboard, and layout alike.\n- **Uncontrolled works too**: without `on-resize`, the divider position survives rebuilds under the source-wins reconcile (a source-side `value` change wins), but pane CONTENT lays out at the declared fraction until the model echoes — bind the handler for the exact controlled loop.\n- **Keyboard**: Tab reaches the divid","AGENTS.md":"# Agent Guide\n\nGuidance for agents (and humans) working on this repository.\n\n## App authoring default\n\nNative SDK itself is implemented in Zig, but Native SDK **apps are authored in TypeScript + Native markup by default**. Do not infer the app-authoring language from this repository's implementation language or from older Zig-core examples.\n\n- For a new app, use `native init <path>` and expect `src/core.ts`, `src/app.native`, and `app.json`. `app.zon` remains a supported legacy/alternative manifest. Ordinary compiled TypeScript work that needs filesystem, process, JSON, regex, classes, or other static-tier APIs belongs under optional `src/services/`, reached from the core with `Cmd.request`; do not import a service from the core. Do not add Zig app code unless the user explicitly chooses `--template zig-core` or the feature requires a toolkit extension.\n- Before changing an existing app, inspect its tree. A `src/core.ts` app stays TypeScript; a `src/main.zig` app stays Zig unless the task is specifically a migration.\n- For default app work, read `skill-data/native-ui/SKILL.md` and `skill-data/ts-core/SKILL.md`; also read `skill-data/ts-services/SKILL.md` when the tree has `src/services/` or the task needs ordinary TypeScript beyond the core subset. `skill-data/core/SKILL.md` covers shared/runtime concerns; `skill-data/zig/SKILL.md` is for Zig-core apps and SDK implementation work.\n- The `-ts` suffix on a few examples only distinguishes ports from older Zig originals. New TypeScript apps need no suffix because TypeScript is the default.\n\n## Build, test, and gate\n\n```bash\nzig build test                # root engine + runtime suites\nzig build validate            # framework's legacy app.zon manifest check\nzig build test-example-<name> # one example's suite (e.g. test-example-notes)\nscripts/gate.sh fast [ref]    # affected-only local gate for your diff (default base: main)\nscripts/gate.sh full          # everything CI-shaped that runs locally\n```\n\nRun `scripts/gate.sh fast` before finishing any change; it maps your diff to the suites that cover it. The docs site checks with `pnpm --dir docs check` (the gate runs it only when `docs/` changed).\n\nThis repository builds with Zig 0.16.0. If a build fails with \"no member named\" errors on std APIs (`std.fs.cwd`, `ArrayList.init`, `std.io`), you are writing pre-0.16 idioms — `skill-data/zig/SKILL.md` maps each such compile error to the current idiom as this codebase writes it.\n\nPinned goldens (pixel signatures, schema fingerprints, command counts) are updated deliberately: review the rendered output or the counted commands first, and keep the pin's comment a self-contained description of what the value represents.\n\n## Changelog\n\nDo not edit `CHANGELOG.md` as part of regular feature or fix work. The release agent reviews the git history since the previous release and writes the complete changelog entry during release preparation; see [RELEASING.md](./RELEASING.md).\n\n## Where things live\n\n- `src/` — the engine and runtime; `src/primitives/canvas/` holds the widget, markup, and vector core.\n- `apps/schema/` — the standalone static Vercel project for `schema.native-sdk.dev`.\n- `examples/` — the showcase apps, many predating the JSON default (`app.zon` + `src/`).\n- `docs/` — the documentation site; `docs/AGENTS.md` has its MDX conventions.\n- `skills/` and `skill-data/` — the agent skills the CLI ships (`native skills list`).\n- `tools/` and `scripts/` — dev tooling and the local gate.\n\nReleases are maintainer-run; see [RELEASING.md](./RELEASING.md).\n","skills/native-sdk/SKILL.md":"---\nname: native-sdk\ndescription: Discovery skill for the Native SDK, the complete toolkit for building native desktop applications. Apps are authored in TypeScript + declarative Native markup (.native) by default and compiled to native code with no JS runtime in the binary; Zig cores are an explicit alternative, and WebViews are the optional web-content path. Use when the user asks what the Native SDK is, how to build a Native SDK app, author native UI, scaffold an app, configure app.json or legacy app.zon, add bridge commands, embed web content, package an app, test a running app, or automate a Native SDK app.\nallowed-tools: Bash(native:*), Bash(npx @native-sdk/cli:*)\nhidden: true\n---\n\n# Native SDK\n\nThe Native SDK is the complete toolkit for building native desktop applications. **The primary authoring path is TypeScript app logic in `src/core.ts` plus declarative Native markup in `.native` files.** The TypeScript core is checked and compiled ahead of time to native code, so the shipped binary contains no browser, WebView, JS runtime, or interpreter. Zig is how the toolkit itself works and is a first-class, explicitly chosen app-core alternative (`--template zig-core`); it is not the default inferred from the SDK's implementation. Every app embeds a deterministic automation server, so agents can snapshot, drive, and screenshot the running window. Desktop is the mature surface (macOS deepest, Linux and Windows exercised in CI); mobile embedding is experimental. WebView surfaces coexist as the optional path for embedding web content or hosting an existing web frontend.\n\n## Start here\n\nThis file is a discovery stub for agents that installed the Native SDK once with a skills installer such as `npx skills add native-sdk`. Before implementing or explaining Native SDK app work, use the installed CLI to discover and load the current skill content:\n\n```bash\nnative skills list\nnative skills get core\nnative skills get native-ui\nnative skills get ts-core\n```\n\nUse `native skills get core` for initial orientation. For the default app-authoring path, load **both** `native-ui` (views, bindings, the app loop) and `ts-core` (the TypeScript subset, effects, subscriptions, and modules) before implementing. Load `ts-services` too when the tree has `src/services/` or ordinary TypeScript needs filesystem/process/JSON/regex/Map/Date/class work behind `Cmd.request`. Use `native skills get core --full` when work reaches lower-level runtime wiring, WebViews, bridge/security, native capabilities, packaging, or debugging. Use `native skills get automation` when testing a running app, taking snapshots, requesting reloads, or using the built-in automation server. Use `native skills get zig` only for an existing Zig core, a toolkit extension, SDK implementation work, or a Zig 0.16 compile error.\n\n## Quick orientation\n\n```bash\nnpm install -g @native-sdk/cli\nnative init my_app\ncd my_app\nnative dev\n```\n\n`native init my_app` generates the primary three-file app: `app.json`, `src/app.native` (the markup view), and `src/core.ts` (`Model`, `Msg`, `update`). Existing `app.zon` manifests remain supported. Inspect the tree before editing an existing app and preserve the core language it already uses. `src/main.zig` means the app explicitly uses the Zig-core template; web-frontend shells additionally carry `frontend/` and usually owned build/runtime wiring.\n","evals/package.json":"{\n  \"name\": \"@native-sdk/evals\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"packageManager\": \"pnpm@10.23.0\",\n  \"scripts\": {\n    \"typecheck\": \"tsc --noEmit\",\n    \"eval\": \"node src/main.ts\",\n    \"metrics\": \"node src/authoring-metrics.ts\",\n    \"check\": \"pnpm typecheck\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^24\",\n    \"typescript\": \"^5\"\n  },\n  \"dependencies\": {\n    \"@vercel/sandbox\": \"^2.3.0\"\n  },\n  \"engines\": {\n    \"node\": \">=24\"\n  }\n}\n","docs/AGENTS.md":"# Docs Site Conventions\n\n## MDX Tables\n\nAlways use HTML `<table>` syntax in MDX pages, never markdown pipe tables. This ensures consistent styling and avoids MDX parsing edge cases.\n\n```html\n<table>\n  <thead>\n    <tr>\n      <th>Column</th>\n      <th>Description</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>field</code></td>\n      <td>What it does</td>\n    </tr>\n  </tbody>\n</table>\n```\n\n## Geistdocs\n\nThe site uses the public `@vercel/geistdocs` package with Labs branding. Write pages in `content/docs` with a frontmatter `title` and no duplicate H1. Order pages and groups with `content/docs/meta.json` and folder metadata. Keep `/docs/<slug>` URLs and the permanent legacy redirects in `next.config.mjs` stable.\n\nShared navigation, search, page actions, Markdown routes, MDX rendering, and theme behavior belong to Geistdocs. Local adapters live under `src/components/geistdocs` and `src/lib/geistdocs`. Native component previews and the persisted TypeScript/Zig code tabs remain app-owned. `src/lib/remark-native.ts` exports their static data for Markdown; do not evaluate MDX expressions. Preserve `language:filename` fences and existing heading IDs through `src/lib/remark-docs.ts`.\n\nReuse an existing dev server. Run checks with `NEXT_DIST_DIR=.next-check pnpm check` so production checks do not share its `.next` directory. The check builds the site, verifies all canonical pages and Markdown siblings over HTTP, checks legacy redirects and discovery, and checks language tabs and the preview WASM. Also run `scripts/gate.sh fast` from the repository root before finishing.\n","evals/pnpm-lock.yaml":"lockfileVersion: '9.0'\n\nsettings:\n  autoInstallPeers: true\n  excludeLinksFromLockfile: false\n\nimporters:\n\n  .:\n    dependencies:\n      '@vercel/sandbox':\n        specifier: ^2.3.0\n        version: 2.3.0\n    devDependencies:\n      '@types/node':\n        specifier: ^24\n        version: 24.13.2\n      typescript:\n        specifier: ^5\n        version: 5.9.3\n\npackages:\n\n  '@types/node@24.13.2':\n    resolution: {integrity: sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==}\n\n  '@vercel/oidc@3.2.0':\n    resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==}\n    engines: {node: '>= 20'}\n\n  '@vercel/sandbox@2.3.0':\n    resolution: {integrity: sha512-KLhc/sj2JuftgWEivPmmDq3iFLIoYU0XIv+44paTyhk5eGGaqNl6dQdxXSqOf+N31jh55m4UWY7i8oJy8oJRrQ==}\n\n  '@workflow/serde@4.1.0-beta.2':\n    resolution: {integrity: sha512-8kkeoQKLDaKXefjV5dbhBj2aErfKp1Mc4pb6tj8144cF+Em5SPbyMbyLCHp+BVrFfFVCBluCtMx+jjvaFVZGww==}\n\n  async-retry@1.3.3:\n    resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==}\n\n  b4a@1.8.1:\n    resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==}\n    peerDependencies:\n      react-native-b4a: '*'\n    peerDependenciesMeta:\n      react-native-b4a:\n        optional: true\n\n  bare-events@2.9.1:\n    resolution: {integrity: sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==}\n    peerDependencies:\n      bare-abort-controller: '*'\n    peerDependenciesMeta:\n      bare-abort-controller:\n        optional: true\n\n  events-universal@1.0.1:\n    resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==}\n\n  fast-fifo@1.3.2:\n    resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}\n\n  jose@6.2.3:\n    resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==}\n\n  jsonlines@0.1.1:\n    resolution: {integrity: sha512-ekDrAGso79Cvf+dtm+mL8OBI2bmAOt3gssYs833De/C9NmIpWDWyUO4zPgB5x2/OhY366dkhgfPMYfwZF7yOZA==}\n\n  ms@2.1.3:\n    resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}\n\n  os-paths@4.4.0:\n    resolution: {integrity: sha512-wrAwOeXp1RRMFfQY8Sy7VaGVmPocaLwSFOYCGKSyo8qmJ+/yaafCl5BCA1IQZWqFSRBrKDYFeR9d/VyQzfH/jg==}\n    engines: {node: '>= 6.0'}\n\n  picocolors@1.1.1:\n    resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}\n\n  retry@0.13.1:\n    resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}\n    engines: {node: '>= 4'}\n\n  streamx@2.28.0:\n    resolution: {integrity: sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==}\n\n  tar-stream@3.1.7:\n    resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==}\n\n  text-decoder@1.2.7:\n    resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==}\n\n  typescript@5.9.3:\n    resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}\n    engines: {node: '>=14.17'}\n    hasBin: true\n\n  undici-types@7.18.2:\n    resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}\n\n  undici@7.28.0:\n    resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==}\n    engines: {node: '>=20.18.1'}\n\n  xdg-app-paths@5.1.0:\n    resolution: {integrity: sha512-RAQ3WkPf4KTU1A8RtFx3gWywzVKe00tfOPFfl2NDGqbIFENQO4kqAJp7mhQjNj/33W5x5hiWWUdyfPq/5SU3QA==}\n    engines: {node: '>=6'}\n\n  xdg-portable@7.3.0:\n    resolution: {integrity: sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw==}\n    engines: {node: '>= 6.0'}\n\n  zod@4.4.3:\n    resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}\n\nsnapshots:\n\n  '@types/node@24.13.2':\n    dependencies:\n      undici-types: 7.18.2\n\n  '@vercel/oidc@3.2.0': {}\n\n  '@vercel/sandbox@2.3.0':\n    dependencies:\n      '@vercel/oidc': 3.2.0\n      '@workflow/serde': 4.1.0-beta.2\n      async-retry: 1.3.3\n      jose: 6.2.3\n      jsonlines: 0.1.1\n      ms: 2.1.3\n      picocolors: 1.1.1\n      tar-stream: 3.1.7\n      undici: 7.28.0\n      xdg-app-paths: 5.1.0\n      zod: 4.4.3\n    transitivePeerDependencies:\n      - bare-abort-controller\n      - react-native-b4a\n\n  '@workflow/serde@4.1.0-beta.2': {}\n\n  async-retry@1.3.3:\n    dependencies:\n      retry: 0.13.1\n\n  b4a@1.8.1: {}\n\n  bare-events@2.9.1: {}\n\n  events-universal@1.0.1:\n    dependencies:\n      bare-events: 2.9.1\n    transitivePeerDependencies:\n      - bare-abort-controller\n\n  fast-fifo@1.3.2: {}\n\n  jose@6.2.3: {}\n\n  jsonlines@0.1.1: {}\n\n  ms@2.1.3: {}\n\n  os-paths@4.4.0: {}\n\n  picocolors@1.1.1: {}\n\n  retry@0.13.1: {}\n\n  streamx@2.28.0:\n    dependencies:\n      events-universal: 1.0.1\n      fast-fifo: 1.3.2\n      text-decoder: 1.2.7\n    transitivePeerDependencies:\n      - bare-abort-controller\n      - react-native-b4a\n\n  tar-stream@3.1.7:\n    dependencies:\n      b4a: 1.8.1\n      fast-fifo: 1.3.2\n      streamx: 2.28.0\n    transitivePeerDependencies:\n      - bare-abort-controller\n      - react-native-b4a\n\n  text-decoder@1.2.7:\n    dependencies:\n      b4a: 1.8.1\n    transitivePeerDependencies:\n      - react-native-b4a\n\n  typescript@5.9.3: {}\n\n  undici-types@7.18.2: {}\n\n  undici@7.28.0: {}\n\n  xdg-app-paths@5.1.0:\n    dependencies:\n      xdg-portable: 7.3.0\n\n  xdg-portable@7.3.0:\n    dependencies:\n      os-paths: 4.4.0\n\n  zod@4.4.3: {}\n","evals/README.md":"# Native SDK evals\n\nAn eval harness for AI-agent authoring of Native SDK apps. It formalizes the \"clean-agent trial\": give a fresh agent nothing but a scaffolded workspace, the `native-ui` skill, and a task prompt, then grade what it produced deterministically.\n\nPer case the runner:\n\n1. **Scaffolds** a fresh workspace with the repo's own CLI — `zig build` at the repo root, then `zig-out/bin/native init evals/.workspaces/<case> --frontend native` (`--template zig-core` for the pre-existing native cases and the zig side of dual cases, `--template ts-core` for the ts side) — and delivers each track's skills exactly the way a real user gets them: `native skills get <name>` written to `.claude/skills/<name>/SKILL.md` (`init` does not ship skills). The zig track gets `native-ui` + `zig`; the ts app track gets `ts-core` + `native-ui`; core-only ts-core cases get `ts-core`. The workspace is then **pre-warmed** (`native test` once — workspaces are zero-config, so builds go through the CLI verbs) so the agent's own builds are incremental and its wall-clock isn't spent compiling the SDK.\n2. **Runs the agent-under-test**: `claude -p \"<task prompt>\"` headless in the workspace, routed through the Vercel AI Gateway, with a per-run `CLAUDE_CONFIG_DIR` so no user-level memory/plugins/hooks leak in, `--max-turns`, a wall-clock timeout, and the full `stream-json` transcript captured to `results/`.\n3. **Grades** with deterministic checks: `native test` in the workspace, `native markup check` on the `.native` files, per-case file greps (e.g. \"the board uses `<template>`\"), and live automation-snapshot greps (`native build` with `-Dautomation=true`, launch, wait for the automation snapshot, grep it for expected roles/names).\n4. **Judges** quality the deterministic checks can't see — idiomatic Model/Msg design, template factoring, test meaningfulness — with an `llm_judge` check: a judge model called directly through the gateway scores case-specific criteria 0–10 against the task prompt and the agent's code. Advisory by default (the score is recorded and printed but never fails the case); set `\"advisory\": false` on a case to make `minScore` a gate. Skipped in `--dry-run`.\n5. **Reports** a per-case `result.json` (pass/fail per check, judge scores, durations, model, turns, cost) plus a console summary table.\n\n## Requirements\n\n- macOS (live snapshot checks launch the app; use `--skip-live` elsewhere, or `--sandbox` for the Linux lane), Zig 0.16.0, node >= 24, pnpm 10.x.\n- The [Claude Code CLI](https://code.claude.com/docs) (`claude`) on PATH.\n- A [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) API key for real runs:\n\n```sh\nexport AI_GATEWAY_API_KEY=\"vck_...\"   # or VERCEL_AI_GATEWAY_API_KEY\n```\n\nThe runner assembles the gateway env for the claude subprocess per [Vercel's Claude Code guide](https://vercel.com/docs/ai-gateway/coding-agents/claude-code):\n\n```sh\nANTHROPIC_BASE_URL=https://ai-gateway.vercel.sh\nANTHROPIC_AUTH_TOKEN=$AI_GATEWAY_API_KEY\nANTHROPIC_API_KEY=            # empty string on purpose: a non-empty value would win over the auth token\n```\n\nModels are gateway slugs. The coder (agent-under-test) defaults to `anthropic/claude-sonnet-5` (override with `--model` or `NATIVE_SDK_EVAL_MODEL`); the judge defaults to `anthropic/claude-opus-4.8` (override with `--judge-model` or `NATIVE_SDK_EVAL_JUDGE_MODEL`). Both tracks of a dual case always run with the SAME coder model — fairness is a property of the run, so set it once at run time.\n\n## Usage\n\n```sh\ncd evals\npnpm install\n\npnpm eval --list                      # list cases\npnpm eval --dry-run                   # everything except the model call (no key needed):\n                                      #   scaffold + skill delivery, print env + claude argv,\n                                      #   run the graders against the untouched scaffold\n                                      #   (grader FAILs are expected there and exit 0)\npnpm eval templates-settings-app      # one real run\npnpm eval                             # the whole suite\npnpm eval --model anthropic/claude-opus-4.8 templates-settings-app\npnpm eval --judge-model anthropic/claude-fable-5 templates-settings-app\npnpm eval --skip-live                 # skip snapshot checks (no app launch / non-macOS)\npnpm eval --track ts dual-feed-table  # one track of a dual-track case (default: both)\npnpm eval --keep-workspaces           # keep .workspaces/<case> around for inspection\npnpm eval --trials 5 expenses-table   # 5 independent trials per case; report pass rates\npnpm eval --concurrency 3             # run up to 3 case trials in parallel (default 2 locally)\npnpm eval --sandbox                   # run each case in its own Vercel Sandbox microVM\npnpm eval --sandbox --dry-run         # full sandbox path minus the model call\npnpm eval --sandbox --sandbox-vcpus 8 # bigger sandboxes (2048 MB RAM per vCPU)\npnpm typecheck\n```\n\nCases run in parallel (log lines are prefixed `[case-name]`); `--concurrency` caps how many at once — locally the default is 2 to keep zig builds from thrashing, with `--sandbox` the default is 4 (each case has its own VM, but plans rate-limit vCPU allocation).\n\n### Trials\n\nModel runs are stochastic; a single pass or fail is weak evidence. `--trials <n>` runs each case n times, each trial **fully independent** — its own scaffolded workspace (`.workspaces/<case>-trial-<n>`), its own agent run, its own checks and judge call — and reports per-case pass **rates** (e.g. `3/5`), per-check pass counts, and the mean judge score. Trials share the `--concurrency` pool (log lines are prefixed `[case-name#trial]`), and with `--sandbox` each trial gets its own microVM.\n\nWith `--trials 1` (the default) the behavior and file layout are exactly the single-run layout described above. With `--trials > 1` each case directory nests per-trial results plus an aggregate:\n\n```\nresults/<stamp>/\n  summary.json                      # array of per-case aggregates\n  <case>/\n    aggregate.json                  # pass rate, per-check pass counts, mean judge score, per-trial results\n    trial-1/result.json             # exactly a single-run result.json (plus a \"trial\" field)\n    trial-1/transcript.jsonl\n    trial-2/...\n```\n\nThe summary table swaps the PASS/FAIL column for a `pass rate` column, the `checks` column shows per-check pass counts (`3/3 2/3 ...`, `s` = skipped in every trial), `judge` is the mean score, `cost`/`time` are totals across trials, and a per-check breakdown is printed under the table. A real run exits non-zero if any trial failed.\n\n### Lanes\n\nEvery result carries a **lane** — where the case ran and got graded — and the summary table has a lane column:\n\n- `macos-local` (default): the run described above; live snapshot checks launch the app directly and require macOS.\n- `linux-sandbox` (`--sandbox`): each case trial runs in its own isolated [Vercel Sandbox](https://vercel.com/docs/sandbox) microVM booted from a pre-baked Linux image, **including the live checks** — the app builds with `-Dplatform=linux -Dweb-engine=system -Dautomation=true`, launches under Xvfb, and is driven through the same automation dropbox the macOS lane uses. An engine screenshot of the app's gpu_surface is captured through the dropbox and pulled back with the results.\n\nA check that greps a surface which exists on only one OS can declare `\"lanes\": [\"macos-local\"]` in `eval.json`; on other lanes it reports **skipped** (`not graded on the ... lane`) instead of failing, so the summary distinguishes \"fails\" from \"not applicable on this lane\". Audited 2026-07-05: none of the ten shipped cases needs a lane annotation — every snapshot pattern asserts roles/names from the SDK's own automation snapshot, and the surfaces they cover (secondary windows, gpu charts, trees, tables) are proven on Linux by `tools/linux-truth/`. A Linux-lane failure is therefore a real failure until a case says otherwise.\n\n### Vercel Sandbox mode\n\n`--sandbox` boots each case trial from a custom image (see `evals/sandbox/`) that bakes the Linux GUI stack (GTK4 + WebKitGTK + Xvfb), zig, node/pnpm, the Claude Code CLI, and a **pre-warmed build layer**: the repo at a pinned ref with the CLI, workspace-test, and automation build graphs already compiled into fixed cache paths. Per case the runner then:\n\n1. uploads the repo **working tree** as a tarball and rsyncs it over the baked repo — deletions propagate, caches survive, so builds against the current tip are incremental on top of the bake;\n2. starts Xvfb and re-invokes this same harness inside (`pnpm eval --skip-permissions --lane linux-sandbox <case>`) with the gateway env assembled exactly like a local run;\n3. pulls the whole case results directory home — `result.json`, `transcript.jsonl`, `live-*.png` engine screenshots — before the microVM is destroyed.\n\n`--dangerously-skip-permissions` is safe there because the whole VM is the throwaway. `--sandbox --dry-run` exercises the entire path — provisioning, refresh, scaffold, graders (which FAIL against the untouched scaffold, as designed), artifact pull — without the model call, and exits 0.\n\n**Image**: build and push once with `evals/sandbox/build-image.sh` (needs a Docker login to the registry — the two token variants are in the script header). Rebuild when the Dockerfile changes, when zig bumps, or when the tip has drifted far enough from the baked ref that in-sandbox builds stop feeling incremental; runs stay *correct* without a rebuild because of the working-tree refresh. After a push the registry prepares the image for a few minutes; the runner retries `image_not_ready` for up to 10 minutes. `--sandbox-image` overrides the default reference (`eval-sandbox`, resolved in the linked project).\n\n**Auth** (checked before any sandbox work): either an OIDC token — one-time setup in `evals/`:\n\n```sh\nvercel link --scope vercel-labs --project zero-native\nvercel env pull .env.local   # the runner auto-loads VERCEL_OIDC_TOKEN from .env.local\n```\n\n(the token expires ~12h; re-run `vercel env pull .env.local` when sandbox auth fails) — or `VERCEL_TOKEN` + `VERCEL_TEAM_ID` + `VERCEL_PROJECT_ID` for environments where OIDC is unavailable. Real runs additionally need `AI_GATEWAY_API_KEY` as usual.\n\n**Cost and limits**: sandbox compute is metered (active CPU + provisioned memory) — a 4-vCPU case trial that runs 20-30 minutes lands around $0.20-0.35 plus the model tokens through the gateway. Sandboxes cap at 45 minutes wall clock on the Hobby plan (the runner's per-sandbox timeout), and vCPU allocation is rate-limited per plan, which is why the default `--concurrency` in sandbox mode is 4.\n\nReal runs exit non-zero if any case fails. Workspaces live in `.workspaces/` and results in `results/<timestamp>/<case>/` (`result.json`, `transcript.jsonl`, the isolated `claude-config/` — kept in-VM and not pulled for sandbox runs, plus `live-*.png` screenshots from the Linux lane); both directories are gitignored.\n\n### Permissions for the agent-under-test\n\nBy default the agent runs with `--permission-mode acceptEdits` plus an allowlist covering `zig ...`, `native-sdk ...`, and basic file commands — enough for unattended edit/build/test loops without granting arbitrary shell. `--skip-permissions` switches to `--dangerously-skip-permissions`; only use it if the default allowlist blocks a case, and remember the workspace is a throwaway dir but the process is not otherwise sandboxed.\n\nIn both modes the runner passes `--disallowedTools` deny rules for `evals/cases/**` and `evals/results/**`: the workspace references the SDK repo by path, so the harness itself is reachable from the agent's cwd, and agents exploring the repo for docs/examples were observed reading their own grading config (3/20 runs of the 2026-07-04 suite). SDK source and examples stay readable on purpose — a real user has the repo.\n\n## Cases\n\n- `templates-settings-app` — validates the new grammar: repeated grouped toggle sections where `<template>`/`<use>` is the natural shape, plus token style attributes (muted headers, surface cards). Checks: build+tests, markup check, `<template>`/`<use>` greps, token-attribute greps, snapshot roles.\n- `kanban-board` — port of the manual builder trial; card identity must survive moving between columns (`global-key`).\n- `habits-tracker` — port of the manual markup trial; text entry (elm-style mirror), derived/filtered lists, enum filters.\n- `expenses-table` — exercises the newest grammar (every built-in component markup-expressible): an expense ledger whose natural shape is `table` > `table-row` > `table-cell` with `<for>` rows, an exclusive category filter, and an alert-shaped empty state. The prompt describes only requirements (rows-and-columns of data, a callout, pinned display strings); the greps assert the agent reached the table grammar from the skill alone.\n- `process-monitor` — exercises the effects surface: a long-running local command (a harmless `sh -c` tick loop the prompt pins exactly) spawned from update through the effects channel, lines streaming into a bounded 12-line list, cancel by model-owned key, and status/counts derived from the line/exit Msgs. Greps assert `.update_fx` wiring, `fx.spawn`/`fx.cancel`, fake-executor tests, and the absence of hand-rolled `std.process.Child`/`std.Thread`; the live snapshot asserts the idle state (Start/Cancel, \"Status: idle\", \"0 lines · 0 dropped\") so nothing spawns during grading. The judge scores effect-key discipline, non-blocking behavior, honest drop accounting, and derive-don't-store.\n- `release-dashboard` — exercises the pipeline composites and markdown-in-markup: a release dashboard whose natural shape is `<stepper>` for the five-stage track (starting on \"Canary\"), `<timeline>`/`<timeline-item>` for the seeded event history, and a `<markdown>`-sourced notes panel containing a GFM table. Greps assert all three elements plus `<for>` events; the snapshot asserts the composite semantics (`\"Canary (active)\"` stepper labels, timeline items by title, `role=gridcell` markdown table cells, the pinned status summary). The judge scores house-style composition (no ad-hoc reimplementations) and declarative markup.\n- `settings-picker` — exercises the anchored floating surface pattern: two picker rows (Theme/Accent) whose natural shape is a `<select>` trigger + `<if>`-mounted `<dropdown-menu anchor=...>` of `<menu-item>`s with `on-dismiss`, model-owned open state, and an exclusive-open rule. Greps assert the select/anchored-dropdown/on-dismiss/menu-item composition; the snapshot asserts the closed idle state and the pinned status line (the trigger's accessible name may be its value text or an explicit `label` — both legit, the pattern allows either).\n- `file-browser-panes` — exercises split panes and the tree keymap: a resizable two-pane browser (`<split value= on-resize=>` with pane `min-width` floors) whose sidebar is a `<tree>` of `role=\"treeitem\"` rows with model-owned `expanded` state driving a detail pane. The snapshot asserts `role=separator`/`role=tree`/`role=treeitem`, the seeded rows, and the pinned selection status. The judge is explicitly told the tree keymap is engine-provided so it never expects app-side keyboard Msgs.\n- `metrics-dashboard` — exercises the markup chart element and icon-in-button: the whole view is markup (header with `<button icon=...>`, the `<chart>` with a `<series values=\"{binding}\">`, status bar) and the Zig side owns only model/update logic. Greps assert `<chart`, `icon=\"`, and the pinned `y-min=\"0\"`; the snapshot asserts `role=chart` plus the seeded summary.\n- `inspector-window` — exercises model-declared secondary windows: a task list whose inspector opens as a real second OS window through `Options.windows_fn` + `window_view`, live-updates with the selection, reflects a user close via `on_close`, and never duplicates. A negative grep rejects faking it with `<dialog>`/`<sheet>`/`<drawer>`.\n- `disk-status-spans` — exercises inline text spans: one wrapped paragraph mixing bold, muted, and monospace runs (announcing as a single text run), with bound values interpolated inside the spans and a derived percentage summary. Greps assert the three span styles inside one enclosing `<text>`; the snapshot asserts the paragraph as ONE combined text run with resolved bindings.\n- `chat-composer` — exercises the composer composites: an `<input-group>` wrapping a `<textarea>` plus an in-border `<input-group-actions>` row, an attached `<button-group>` cluster, a derived disabled Send, and a `<for>` message list. The snapshot asserts the composer textbox, the attached cluster, Send disabled on the empty draft, the seeded messages, and the live count.\n- `brand-icon-package` — exercises the one-image app-icon pipeline through `native package`: the manifest sources icons from a single square PNG at the brand path, no prebuilt icon containers anywhere, and the packaged macOS bundle carries a generated `AppIcon.icns` wired into its metadata.\n- `playlist-row-actions` — exercises the interaction seams: row-level Enter as a list row's primary action (`on-submit` on `list-item`, distinct from select-on-press) and the app-level key fallback (`Options.on_key`) for Space and the arrows when nothing is focused. Greps assert the row-level submit binding and the fallback wiring; the snapshot asserts the seeded rows, the model-driven selection, and the idle derived status.\n\nAdd a case by creating `cases/<name>/eval.json` (see `src/types.ts` for the schema). Prompts describe app **requirements**, never the solution — the point is to see whether a fresh agent reaches the intended grammar from the skill alone.\n\n## The TS-authoring track\n\nCases with `\"frontend\": \"ts-core\"` measure the other authoring surface: the app core written in the TypeScript subset and compiled by `packages/core`. The scaffold is not an app — it is `src/core.ts` (the case's `starter/` overlay when it ships one, else a minimal counter core), a README with the check loop, and the `ts-core` skill delivered via `native skills get ts-core`. Two graders replace build/markup/snapshot:\n\n- `ts_transpile` — the frontend check must exit clean on `src/core.ts`: tsc-semantics typecheck, every subset rule (NS1001-NS1050) — no Zig emission; the literal name is the `eval.json` check key, the semantics are `ts_check`. Failing diagnostics stay in the result as the violation evidence.\n- `ts_harness` — behavioral grading: compile the core through the external core compiler and drive the compiled archive through a generated mirror — the scratch dir carries the mirror module, the archive, and the case's `harness.zig`, then `zig test harness.zig`. The harness drives the real dispatch cycle (`update` → `commitModelRoot` → `frameReset`) and asserts the prompt's requirements, so the case prompt pins the Model/Msg/export contract exactly (an API spec, not a solution).\n\nBecause the grading harness compiles against the agent's code, ts-core prompts pin names; behavior stays requirements-only. The compiler handles pure `update(model, msg): Model` cores and effectful `Model | [Model, Cmd<Msg>]` pair-returns (the Cmd surface); the wave-2 dual-track cases below are the effects coverage.\n\nThe ts-core cases:\n\n- `ts-habits-core` — a fresh core from a requirements description (the TS mirror of `habits-tracker`): seeded habits, toggle-with-streak math, a trimmed draft add path, exclusive filters, derived counts.\n- `ts-expenses-filter` — a feature-add to an existing subset file: extend a working expense ledger (shipped as `starter/`) with an exclusive `Category | null` filter and derived filtered views, without breaking the existing behavior.\n- `ts-countdown-bugfix` — a bug-fix in an existing subset file: a countdown core whose tick/reset/set_duration transitions drifted from the spec (unguarded ticks, no completion stop/floor, hardcoded reset); the prompt states the intended behavior, the harness isolates each drift.\n- `ts-tag-input-core` — the text seam under pressure: comma-separated tag parsing with byte-level trim, empty-drop, and case-insensitive dedupe, where string methods are the reflex and NS1004 says no.\n\n## The dual-track cases (wave 2)\n\nWave 1's four ts-* cases measured subset compliance on toy katas and compared against Zig numbers gathered **before** the `zig` 0.16-idioms skill existed. Wave 2 replaces that comparison with an honest, contemporaneous one: six realistic asks, each ONE language-blind spec (`\"frontend\": \"app-dual\"`) that runs on **both authoring tracks** — `<case>@ts` scaffolds a full TypeScript app (`native init --frontend native --template ts-core`), `<case>@zig` the Zig app template (`--template zig-core`). Identical prompt, identical shared checks, one behavioral spec asserted by two thin per-track harnesses; `--track ts|zig` selects a lane, the default runs both.\n\nGrading per track: the shared checks (`native test -Dplatform=null`, markup check, view greps, the judge with a case rubric) plus the track's behavioral harness. On the ts track, `ts_harness` compiles `src/core.ts` (its whole import graph) through the external core compiler and `zig test`s the case's `harness-ts.zig` against the compiled core's generated mirror and `harness-lib/cmdview.zig` — a decoder over the Cmd/Sub wire format, so harnesses assert effects semantically (\"one GET to the pinned URL\", \"the delay re-armed on the same key\"). On the zig track, `zig_harness` injects the case's `harness-zig.zig` into the workspace as `src/eval_behavior_spec.zig` (a test import appended to `src/main.zig`, restored afterward) and runs `native test`, so it compiles against the agent's real Model/Msg/update and drives the SDK's deterministic **fake effects executor** (`fx.executor = .fake`, `pendingSpawnAt`/`feedLine`/`feedExit`/`fireTimer`/`feedResponse`/`feedFileResult`).\n\nThe cases — every prompt reads like a real user ask, and every effect result is fed by the harness (no network, no processes, no clocks during grading):\n\n- `dual-feed-table` — \"fetch JSON from this API and show it in a sortable table\": the HTTP effect (pinned URL, buffered GET, keyed), JSON parsing of a pinned flat schema, two sort orders derived at view time, and the five honest states (idle / loading with re-entry guarded / loaded / explicit empty / failed keeping previous rows) across non-200, transport-failure, and malformed-body deliveries.\n- `dual-notes-autosave` — \"add debounced autosave to this notes app\" (starter provided per track, both riding the SDK text-input engines): the keyed one-shot re-arm debounce (Cmd.delay / fx.startTimer replace), byte-exact notes.tsv serialization, Save now with the pending autosave cancelled, a five-state save lifecycle, and the late-result race (a save landing after newer edits must not mark them saved).\n- `dual-pomodoro-timer` — \"build a pomodoro timer with a completion sound\": the recurring timer surface (Sub.timer declared from the model / fx timer armed-cancelled through the channel — armed exactly while running), auto-advancing focus/rest state machine, work-only completion counting, stale-tick guards, and the audio surface at completions.\n- `dual-list-delete-fix` — \"this list doesn't update after delete — fix it\": a realistic seeded bug (a hand-maintained visible-list cache that delete forgot) in a medium app with a markup view; the harness pins list correctness across delete/toggle/add/filter, the judge scores whether the fix is the derive-don't-store root cause or a symptom patch.\n- `dual-ledger-csv-split` — \"split this core into modules and add export-to-CSV in the new module\": multi-file authoring (relative imports on ts, `@import` on zig, both grepped), byte-exact CSV quoting (commas and doubled quotes exercised by the seeds), the file-write effect, and a four-state export lifecycle.\n- `dual-sysinfo-panel` — \"show system info from a shell command in the UI\": a collect-mode background spawn of a pinned argv, byte parsing of the output line, re-entry guarded while probing, and honest ok / failed-with-code / never-started states.\n\nStarters (`starter-ts/`, `starter-zig/`) overlay the scaffold for the feature-add, bug-fix, and split cases — written in each track's idiom (immutable spread cores over the SDK text engine on ts; bounded buffers, `canvas.TextBuffer`, and Model-method bindings on zig) so neither track starts from translated code.\n\n**Fairness and methodology.** Both tracks get identical prompts (language-neutral asks; the pinned contract names arms/fields in the wire spelling and states each track's idiom for the few surfaces that necessarily differ — routed err arms on ts vs payload-carried outcomes on zig). Each track gets its CURRENT skill set by the documented delivery path and nothing extra: ts → `ts-core` + `native-ui`; zig → `native-ui` + `zig` (the 0.16-idioms skill). The same coder model grades both tracks of a run. Known caveats to carry into any writeup: wave-1's Zig numbers predate the `zig` skill (this suite is the corrected baseline — the pre-existing native cases now also receive it, so their history splits at this commit); the behavioral harnesses are compiled contracts, so a run can fail for contract-shape reasons the prompt pins explicitly; and the two tracks' harness glue differs by necessity (wire-format decoding vs fake-executor introspection) while asserting the same spec, test for test.\n\n### Authoring metrics\n\n`pnpm metrics results/<stamp> [...]` post-processes finished runs' transcripts into the agent-authoring metrics the checks cannot see, per case and per track — **ts** (the ts-core cases and the `@ts` side of dual cases) vs **zig** (the pre-existing native cases and the `@zig` side): **first-pass compliance** (did the agent's first compliance check after touching sources pass — the frontend check, `native check`, `native test`, or `native build` on the ts track; `native test` / `native build` / `native check` / `native markup check` on the zig track), **retries-to-green** (failing compliance runs before the first green), **teaching-error encounters** (failing compliance runs that carried a teaching diagnostic — the \"did the diagnostics work\" round-trip count wave 2 compares across tracks), **violation taxonomy** (NS/TS rule IDs on the ts track; zig error lines on the zig track, with `no member named 'X'` bucketed by member so the 0.16-idiom class is visible) raw and per 1k generated LOC (lines written through Write/Edit to source files, `.native` markup included), and **task success** (the run's own pass verdict). Harness friction — permission-refused commands, errored compounds with no diagnostic in the output — is dropped from the event stream so it never masquerades as an authoring failure. It writes `authoring-metrics.json` next to each `summary.json`.\n\n## CI\n\nCI only typechecks the harness (`evals-typecheck` in `.github/workflows/ci.yml`). Eval runs are local/manual — no model calls in CI.\n","skill-data/core/SKILL.md":"---\nname: core\ndescription: Core Native SDK guide for AI agents. Read this before explaining the Native SDK or changing a Native SDK app. Establishes TypeScript + Native markup as the default app-authoring path, routes default app work to the native-ui and ts-core skills, and covers the shared foundation: project structure, app.json and legacy app.zon, lower-level App and Runtime patterns, frontend integration, web engines, JavaScript bridge commands, permissions, windows, WebViews, dialogs, packaging, debugging, and testing. Use when the user asks what the Native SDK is, how to build or modify an app, how to package or debug it, or how to add native capabilities.\n---\n\n# Build Native SDK apps\n\nThe Native SDK is a cross-platform native app toolkit inspired by the web. The default app (`native init`) is native-rendered and authored as a declarative Native markup view (`src/app.native`) plus a TypeScript app core (`src/core.ts`). The core is checked and compiled ahead of time to native code; no JS runtime ships in the binary. **For default app implementation, load both the `native-ui` and `ts-core` skills.** Zig is the implementation language of the toolkit and a first-class app-core choice via `--template zig-core`, not the default app-authoring language.\n\nThis skill covers the shared foundation and lower-level architectures: apps that explicitly choose a Zig core, runtime extensions, and apps that render web frontends in a WebView (`native init --frontend next|vite|react|svelte|vue`). Native-rendered surfaces and WebViews share windows, policies, lifecycle, commands, and platform services, and one app can mix them. WebView engines are the platform WebView (WKWebView on macOS, WebKitGTK on Linux) or Chromium through CEF where supported.\n\nAgents should assume they do not know the Native SDK from general model knowledge. Read this skill first. For ordinary app implementation, load `native-ui` and `ts-core`; for shared/runtime, WebView, packaging, or native-capability work, run `native skills get core --full` so the referenced files are included in the CLI output.\n\n## Mental model\n\n- The default app has three files of truth: `src/core.ts` (`Model`, `Msg`, `update`), `src/app.native` (UI), and `app.json` (manifest).\n- Native markup binds model values and dispatches typed messages; only `update` changes state.\n- The TypeScript core compiles to native code. Node is a build/check/dev tool, not an app runtime.\n- `App` is the lower-level product/runtime interface used by generated wiring, Zig-core apps, WebView shells, and extensions.\n- `Runtime` owns the event loop, windows, bridge dispatch, security checks, automation, tracing, platform services, and window state.\n- `WebViewSource` tells the runtime what to load: inline HTML, a URL, or packaged assets from a local app origin.\n- `app.json` is the default app manifest: identity, icons, windows, frontend assets, web engine, permissions, bridge policy, security policy, and packaging inputs. `app.zon` remains supported.\n- `src/runner.zig` and `src/main.zig` appear only when an app explicitly owns lower-level Zig wiring (for example a `zig-core` app or a WebView shell). `--full` makes a TypeScript app own `build.zig` but still does not give it Zig app logic. Do not add Zig sources to a default TypeScript app merely because SDK examples contain them.\n- `frontend/` is normal web code. It talks to native Zig through `window.zero.invoke()` or builtin helpers when those are enabled.\n\n## Task router\n\nThese references are included by `native skills get core --full`. Use them when the task touches the topic:\n\n- Project creation, generated files, build steps: `references/project-anatomy.md`\n- Default TypeScript core authoring, checker rules, effects, subscriptions, modules: `native skills get ts-core`\n- Native markup, bindings, messages, hot reload, view testing: `native skills get native-ui`\n- `App`, `Runtime`, callbacks, embedding, tests: `references/app-model-runtime.md`\n- React/Vue/Svelte/Next/Vite, dev server, bundled assets: `references/frontend-assets.md`\n- App-defined bridge commands, builtin commands, permissions, windows, WebViews, dialogs: `references/bridge-security-native-capabilities.md`\n- Web engine choice, CEF, packaging, signing, doctor, logs, debugging: `references/web-engines-packaging-debugging.md`\n- Running-app inspection and smoke tests: `native skills get automation`\n- `zig build` fails on std APIs (\"no member named 'cwd'\", ArrayList `init`): `native skills get zig` — the Zig 0.16 idioms, indexed by compile error\n\n## Quick start\n\nUse the CLI for new apps:\n\n```bash\nnpm install -g @native-sdk/cli\nnative init my_app\ncd my_app\nnative dev\n```\n\nThis creates `src/core.ts`, `src/app.native`, and `app.json`; a native window opens with `native dev`. Use `native init my_app --template zig-core` only when the user chooses Zig. Frontend choices (`--frontend next|vite|react|svelte|vue`) are the separate WebView migration/integration path.\n\n## Workflow for existing apps\n\nBefore editing an existing Native SDK app:\n\n1. Read `app.json` or `app.zon` and inspect `src/` before assuming a core language. Read `src/core.ts` + `src/app.native` for the default path; read `src/main.zig`, `src/runner.zig`, and `build.zig` only when they exist.\n2. Identify whether the app is TypeScript + Native markup (default), an explicitly chosen Zig core, or a WebView frontend, then identify the layer the change belongs to.\n3. Follow the generated code and examples in the repository instead of inventing a new app layout.\n4. Prefer exact security policy changes over broad allowances.\n5. Validate with the narrowest useful command.\n\nCommon file ownership:\n\n- `app.json` / `app.zon`: app identity, version, icons, windows, permissions, capabilities, bridge command policy, allowed origins, frontend dist/dev config, web engine, CEF config.\n- `src/core.ts`: default app state and behavior — `Model`, `Msg`, `update`, pure helpers, `Cmd`, and `Sub`.\n- `src/app.native`: default app UI — elements, layout, bindings, and message dispatch.\n- `src/main.zig`: explicitly chosen Zig-core logic or lower-level `App` behavior, source selection, lifecycle callbacks, and custom bridge handlers.\n- `src/runner.zig`: `Runtime.init`, platform selection, security policy, builtin bridge policy, `js_window_api`, automation server, trace sinks, panic capture, window state store.\n- `build.zig`: build options, frontend build/dev/package steps, platform link setup, test steps.\n- `frontend/`: web app implementation, `window.zero` calls, dev/build config.\n\n## Lower-level WebView app model (advanced)\n\nThis is not the default app-authoring path. A lower-level Zig/WebView shell returns `native_sdk.App` with `context`, `name`, and a WebView source:\n\n```zig\nconst App = struct {\n    fn app(self: *@This()) native_sdk.App {\n        return .{\n            .context = self,\n            .name = \"my-app\",\n            .source = native_sdk.WebViewSource.html(\"<h1>Hello from Native SDK</h1>\"),\n        };\n    }\n};\n```\n\nUse these source constructors:\n\n- `native_sdk.WebViewSource.html(content)` for small inline demos.\n- `native_sdk.WebViewSource.url(address)` for an explicit URL.\n- `native_sdk.WebViewSource.assets(.{ .root_path = \"frontend/dist\", .entry = \"index.html\" })` for packaged frontend assets.\n\nFor framework apps, prefer a dynamic source so development loads the local dev server and production loads bundled assets:\n\n```zig\nfn source(context: *anyopaque) anyerror!native_sdk.WebViewSource {\n    const self: *App = @ptrCast(@alignCast(context));\n    return native_sdk.frontend.sourceFromEnv(self.env_map, .{\n        .dist = \"frontend/dist\",\n        .entry = \"index.html\",\n    });\n}\n```\n\n`sourceFromEnv` reads `NATIVE_SDK_FRONTEND_URL`; otherwise it serves the configured asset directory. Use it for most framework apps.\n\n## app.json essentials\n\nKeep `app.json` as the source of truth for app-level behavior. Existing `app.zon` projects use the same fields in ZON syntax.\n\n```json\n{\n  \"$schema\": \"https://schema.native-sdk.dev/app/v1.json\",\n  \"id\": \"com.example.my-app\",\n  \"name\": \"my-app\",\n  \"display_name\": \"My App\",\n  \"description\": \"One line about the app, shown in the About panel.\",\n  \"version\": \"0.1.0\",\n  \"icons\": [\"assets/icon.png\"],\n  \"platforms\": [\"macos\", \"linux\"],\n  \"permissions\": [],\n  \"capabilities\": [\"webview\"],\n  \"frontend\": {\n    \"dist\": \"frontend/dist\",\n    \"entry\": \"index.html\",\n    \"spa_fallback\": true,\n    \"dev\": {\n      \"url\": \"http://127.0.0.1:5173/\",\n      \"command\": [\"npm\", \"--prefix\", \"frontend\", \"run\", \"dev\", \"--\", \"--host\", \"127.0.0.1\"],\n      \"ready_path\": \"/\",\n      \"timeout_ms\": 30000\n    }\n  },\n  \"security\": {\n    \"navigation\": {\n      \"allowed_origins\": [\"zero://app\", \"http://127.0.0.1:5173\"],\n      \"external_links\": { \"action\": \"deny\" }\n    }\n  },\n  \"web_engine\": \"system\",\n  \"windows\": [\n    { \"label\": \"main\", \"title\": \"My App\", \"width\": 960, \"height\": 640, \"restore_state\": true }\n  ]\n}\n```\n\nLegacy ZON equivalent:\n\n```zig\n.{\n    .id = \"com.example.my-app\",\n    .name = \"my-app\",\n    .display_name = \"My App\",\n    .description = \"One line about the app, shown in the About panel.\",\n    .version = \"0.1.0\",\n    .icons = .{\"assets/icon.png\"},\n    .platforms = .{ \"macos\", \"linux\" },\n    .permissions = .{},\n    .capabilities = .{ \"webview\" },\n    .frontend = .{\n        .dist = \"frontend/dist\",\n        .entry = \"index.html\",\n        .spa_fallback = true,\n        .dev = .{\n            .url = \"http://127.0.0.1:5173/\",\n            .command = .{ \"npm\", \"--prefix\", \"frontend\", \"run\", \"dev\", \"--\", \"--host\", \"127.0.0.1\" },\n            .ready_path = \"/\",\n            .timeout_ms = 30000,\n        },\n    },\n    .security = .{\n        .navigation = .{\n            .allowed_origins = .{ \"zero://app\", \"http://127.0.0.1:5173\" },\n            .external_links = .{ .action = \"deny\" },\n        },\n    },\n    .web_engine = \"system\",\n    .windows = .{\n        .{ .label = \"main\", .title = \"My App\", .width = 960, .height = 640, .restore_state = true },\n    },\n}\n```\n\nUse exact local origins for dev servers. Add `zero://inline` only for inline HTML sources.\n\n## Common implementation recipes\n\n### Add a new framework app\n\nUse `native init <path> --frontend <next|vite|react|svelte|vue>`. Then inspect the generated `app.json`, `src/main.zig`, and `build.zig` before customizing. For framework behavior, keep frontend work in `frontend/` and use `sourceFromEnv` so development and packaged builds share one app shell.\n\n### Add a native bridge command\n\n1. Add state and a handler in `src/main.zig`.\n2. Register the handler in `bridge()`.\n3. Allow the command in the app manifest and in the runtime bridge policy if the runner reads manifest policy into runtime.\n4. Call it from JavaScript with `window.zero.invoke(\"namespace.command\", payload)`.\n5. Return valid JSON from Zig. Use `native_sdk.bridge.writeJsonStringValue()` for user-controlled strings.\n\nBridge calls are size-limited, origin-checked, permission-checked, and routed only to registered handlers.\n\n### Add windows, child WebViews, or dialogs\n\nUse builtin bridge commands only after enabling a policy for the exact commands and origins. Window and child WebView commands need the `window` permission when permissions are configured. Dialog commands are always default-deny and require explicit `builtin_bridge` policy. See `references/bridge-security-native-capabilities.md`.\n\n### Choose a web engine\n\nDefault to `.web_engine = \"system\"` for small apps and native footprint. Use `.web_engine = \"chromium\"` plus `.cef` when the app needs a pinned Chromium platform or rendering consistency. Chromium apps must install/package the matching CEF layout.\n\n### Package an app\n\nZero-config apps package WITHOUT ejecting — `native package` works directly on the zero-config build (`native eject` is only for owning the build files, never a packaging prerequisite):\n\n```bash\nnative build\nnative package --target macos --archive\n```\n\nOn macOS, `--archive` adds a zero-config drag-to-Applications DMG with a generated 1×/2× Retina background. The optional `app.zon` `.dmg` block controls its volume name, PNG/JPEG/TIFF background, usable Finder canvas/icon geometry, and Applications link. An adjacent `name@2x.png`/`.jpg` is discovered automatically. Use `.dmg.items` only for a fully art-directed list: exactly one `app`, plus optional `applications`, project-relative `file`, and absolute `link` entries, each with its own icon-center position.\n\nApps that own their build (ejected or scaffolded `--full`) wire the same step into the build graph: keep package metadata in the app manifest, build the frontend assets, build the native binary, then package:\n\n```bash\nzig build package\nnative doctor --manifest app.json --strict\n```\n\nUse signing and CEF options only when the product requires them.\n\n## Development commands\n\nFor a default TypeScript + Native markup app, use:\n\n```bash\nnative check\nnative dev\nnative dev --core\nnative test\n```\n\nFor iterative WebView-frontend work in an app that owns its build, use the managed dev server flow:\n\n```bash\nzig build dev\n```\n\nOr run the CLI directly after building the binary:\n\n```bash\nnative dev --manifest app.json --binary zig-out/bin/MyApp\n```\n\nVite usually uses `http://127.0.0.1:5173/`; Next.js usually uses `http://127.0.0.1:3000/`. The app WebView loads the dev URL directly, so framework HMR remains owned by Vite, Next.js, or the selected dev server.\n\n## Security defaults\n\nTreat WebView content as untrusted:\n\n- List only needed `permissions` and `capabilities`.\n- Prefer exact bridge command origins over `\"*\"`.\n- Keep main-frame navigation allowlisted in `security.navigation.allowed_origins`.\n- Keep external links denied unless the product explicitly needs them.\n- Use a strict CSP for packaged frontend assets.\n- Built-in dialogs are always default-deny and require explicit `builtin_bridge` policy.\n- Child WebViews receive `window.zero` only when explicitly created with `bridge: true`.\n\nCommon bridge failure codes are `invalid_request`, `unknown_command`, `permission_denied`, `handler_failed`, `payload_too_large`, and `internal_error`.\n\n## Validate changes\n\nUseful default-app commands:\n\n```bash\nnative check\nnative test\nnative build\n```\n\nUseful SDK, Zig-core, and owned-build commands:\n\n```bash\nzig build run\nzig build dev\nzig build test\nzig build test-tooling\nnative validate app.json\nnative doctor --manifest app.json --strict\nzig build package\n```\n\nFor an app that owns Zig code, run BOTH `zig build` and `zig build test` before calling a change done: Zig's lazy analysis means code only tests reference (or only `main()` reference) can sit broken under the other command alone. This Zig-specific requirement does not turn a default TypeScript app into a Zig app.\n\nThe toolkit requires Zig 0.16.0. When a build fails with \"no member named\" errors on std APIs (`std.fs.cwd`, `ArrayList.init`, `std.io`, `GeneralPurposeAllocator`), the code was written against older Zig idioms — `native skills get zig` maps each such compile error to the 0.16 idiom as this SDK writes it.\n\nFor GUI smoke tests, build with automation enabled and use the `automation` skill:\n\n```bash\nzig build run -Dplatform=macos -Dautomation=true\nzig-out/bin/native automate snapshot\n```\n\nWhen changing app behavior, keep tests in the app's existing authoring tier. TypeScript cores can run in the node dev loop and through generated full-loop tests; Zig-core logic uses focused Zig tests. Use automation-based tests for live window/runtime integration.\n\n## Examples to inspect\n\n- `examples/chatbot`: a substantial app authored entirely in TypeScript + Native markup, with streaming fetch effects and modules.\n- `examples/soundboard`: TypeScript + Native markup music-player showcase.\n- `examples/system-monitor`: TypeScript + Native markup app using spawn effects, timers, and tables.\n- `examples/hello`: smallest lower-level inline HTML/WebView app.\n- `examples/webview`: bridge and WebView runtime example.\n- `examples/browser`: layered WebView/browser-style example.\n- `examples/next`: Next.js with production assets.\n- `examples/react`, `examples/svelte`, `examples/vue`: Vite frontend apps.\n- `examples/ios`, `examples/android`: mobile host embedding examples.\n\n## When answering users\n\nLead with the primary authoring story: applications are TypeScript cores plus Native markup by default, compiled to a small native binary with no JS runtime. Describe Zig as the toolkit implementation language and an explicit app-core/extension choice. Describe WebViews as a coexisting integration path for existing web content, not the starting point for a fresh app. If asked to implement, read the app files first, preserve the existing core language, and make the smallest change in the correct layer.\n","evals/tsconfig.json":"# Evals Skill\n\nPath: `evals/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/tsconfig.json)","packages/native-sdk/package.json":"# Native-sdk Skill\n\nPath: `packages/native-sdk/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/package.json)","packages/native-sdk/README.md":"# native-sdk Documentation\n\nPath: `packages/native-sdk/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/README.md)","packages/core/package.json":"# Core Skill\n\nPath: `packages/core/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/core/package.json)","packages/core/package-lock.json":"# Core Skill\n\nPath: `packages/core/package-lock.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/core/package-lock.json)","packages/core/pnpm-lock.yaml":"# Core Skill\n\nPath: `packages/core/pnpm-lock.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/core/pnpm-lock.yaml)","tools/guest-mac/agents.md":"# Guest-mac Skill\n\nPath: `tools/guest-mac/agents.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/tools/guest-mac/agents.md)","tools/guest-mac/README.md":"# guest-mac Documentation\n\nPath: `tools/guest-mac/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/tools/guest-mac/README.md)","tools/linux-truth/README.md":"# linux-truth Documentation\n\nPath: `tools/linux-truth/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/tools/linux-truth/README.md)","tools/windows-truth/README.md":"# windows-truth Documentation\n\nPath: `tools/windows-truth/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/tools/windows-truth/README.md)","evals/cases/ts-tag-input-core/eval.json":"# Ts-tag-input-core Skill\n\nPath: `evals/cases/ts-tag-input-core/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/ts-tag-input-core/eval.json)","evals/cases/ts-habits-core/eval.json":"# Ts-habits-core Skill\n\nPath: `evals/cases/ts-habits-core/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/ts-habits-core/eval.json)","evals/cases/ts-expenses-filter/eval.json":"# Ts-expenses-filter Skill\n\nPath: `evals/cases/ts-expenses-filter/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/ts-expenses-filter/eval.json)","evals/cases/ts-countdown-bugfix/eval.json":"# Ts-countdown-bugfix Skill\n\nPath: `evals/cases/ts-countdown-bugfix/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/ts-countdown-bugfix/eval.json)","evals/cases/templates-settings-app/eval.json":"# Templates-settings-app Skill\n\nPath: `evals/cases/templates-settings-app/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/templates-settings-app/eval.json)","evals/cases/settings-picker/eval.json":"# Settings-picker Skill\n\nPath: `evals/cases/settings-picker/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/settings-picker/eval.json)","evals/cases/release-dashboard/eval.json":"# Release-dashboard Skill\n\nPath: `evals/cases/release-dashboard/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/release-dashboard/eval.json)","evals/cases/process-monitor/eval.json":"# Process-monitor Skill\n\nPath: `evals/cases/process-monitor/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/process-monitor/eval.json)","evals/cases/playlist-row-actions/eval.json":"# Playlist-row-actions Skill\n\nPath: `evals/cases/playlist-row-actions/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/playlist-row-actions/eval.json)","evals/cases/metrics-dashboard/eval.json":"# Metrics-dashboard Skill\n\nPath: `evals/cases/metrics-dashboard/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/metrics-dashboard/eval.json)","evals/cases/kanban-board/eval.json":"# Kanban-board Skill\n\nPath: `evals/cases/kanban-board/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/kanban-board/eval.json)","evals/cases/inspector-window/eval.json":"# Inspector-window Skill\n\nPath: `evals/cases/inspector-window/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/inspector-window/eval.json)","evals/cases/habits-tracker/eval.json":"# Habits-tracker Skill\n\nPath: `evals/cases/habits-tracker/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/habits-tracker/eval.json)","evals/cases/file-browser-panes/eval.json":"# File-browser-panes Skill\n\nPath: `evals/cases/file-browser-panes/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/file-browser-panes/eval.json)","evals/cases/expenses-table/eval.json":"# Expenses-table Skill\n\nPath: `evals/cases/expenses-table/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/expenses-table/eval.json)","evals/cases/dual-sysinfo-panel/eval.json":"# Dual-sysinfo-panel Skill\n\nPath: `evals/cases/dual-sysinfo-panel/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/dual-sysinfo-panel/eval.json)","evals/cases/dual-pomodoro-timer/eval.json":"# Dual-pomodoro-timer Skill\n\nPath: `evals/cases/dual-pomodoro-timer/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/dual-pomodoro-timer/eval.json)","evals/cases/dual-notes-autosave/eval.json":"# Dual-notes-autosave Skill\n\nPath: `evals/cases/dual-notes-autosave/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/dual-notes-autosave/eval.json)","evals/cases/dual-list-delete-fix/eval.json":"# Dual-list-delete-fix Skill\n\nPath: `evals/cases/dual-list-delete-fix/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/dual-list-delete-fix/eval.json)","evals/cases/dual-ledger-csv-split/eval.json":"# Dual-ledger-csv-split Skill\n\nPath: `evals/cases/dual-ledger-csv-split/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/dual-ledger-csv-split/eval.json)","evals/cases/dual-feed-table/eval.json":"# Dual-feed-table Skill\n\nPath: `evals/cases/dual-feed-table/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/dual-feed-table/eval.json)","evals/cases/disk-status-spans/eval.json":"# Disk-status-spans Skill\n\nPath: `evals/cases/disk-status-spans/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/disk-status-spans/eval.json)","evals/cases/chat-composer/eval.json":"# Chat-composer Skill\n\nPath: `evals/cases/chat-composer/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/chat-composer/eval.json)","evals/cases/brand-icon-package/eval.json":"# Brand-icon-package Skill\n\nPath: `evals/cases/brand-icon-package/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/brand-icon-package/eval.json)","skill-data/ts-services/references/service-surface.md":"# References Skill\n\nPath: `skill-data/ts-services/references/service-surface.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/skill-data/ts-services/references/service-surface.md)","skill-data/core/references/app-model-runtime.md":"# References Skill\n\nPath: `skill-data/core/references/app-model-runtime.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/skill-data/core/references/app-model-runtime.md)","skill-data/core/references/bridge-security-native-capabilities.md":"# References Skill\n\nPath: `skill-data/core/references/bridge-security-native-capabilities.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/skill-data/core/references/bridge-security-native-capabilities.md)","skill-data/core/references/frontend-assets.md":"# References Skill\n\nPath: `skill-data/core/references/frontend-assets.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/skill-data/core/references/frontend-assets.md)","skill-data/core/references/project-anatomy.md":"# References Skill\n\nPath: `skill-data/core/references/project-anatomy.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/skill-data/core/references/project-anatomy.md)","skill-data/core/references/web-engines-packaging-debugging.md":"# References Skill\n\nPath: `skill-data/core/references/web-engines-packaging-debugging.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/skill-data/core/references/web-engines-packaging-debugging.md)","packages/native-sdk/npm/win32-x64/package.json":"# Win32-x64 Skill\n\nPath: `packages/native-sdk/npm/win32-x64/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/win32-x64/package.json)","packages/native-sdk/npm/win32-x64/README.md":"# win32-x64 Documentation\n\nPath: `packages/native-sdk/npm/win32-x64/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/win32-x64/README.md)","packages/native-sdk/npm/win32-arm64/package.json":"# Win32-arm64 Skill\n\nPath: `packages/native-sdk/npm/win32-arm64/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/win32-arm64/package.json)","packages/native-sdk/npm/win32-arm64/README.md":"# win32-arm64 Documentation\n\nPath: `packages/native-sdk/npm/win32-arm64/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/win32-arm64/README.md)","packages/native-sdk/npm/linux-x64-musl/package.json":"# Linux-x64-musl Skill\n\nPath: `packages/native-sdk/npm/linux-x64-musl/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-x64-musl/package.json)","packages/native-sdk/npm/linux-x64-musl/README.md":"# linux-x64-musl Documentation\n\nPath: `packages/native-sdk/npm/linux-x64-musl/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-x64-musl/README.md)","packages/native-sdk/npm/linux-x64-gnu/package.json":"# Linux-x64-gnu Skill\n\nPath: `packages/native-sdk/npm/linux-x64-gnu/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-x64-gnu/package.json)","packages/native-sdk/npm/linux-x64-gnu/README.md":"# linux-x64-gnu Documentation\n\nPath: `packages/native-sdk/npm/linux-x64-gnu/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-x64-gnu/README.md)","packages/native-sdk/npm/linux-arm64-musl/package.json":"# Linux-arm64-musl Skill\n\nPath: `packages/native-sdk/npm/linux-arm64-musl/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-arm64-musl/package.json)","packages/native-sdk/npm/linux-arm64-musl/README.md":"# linux-arm64-musl Documentation\n\nPath: `packages/native-sdk/npm/linux-arm64-musl/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-arm64-musl/README.md)","packages/native-sdk/npm/linux-arm64-gnu/package.json":"# Linux-arm64-gnu Skill\n\nPath: `packages/native-sdk/npm/linux-arm64-gnu/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-arm64-gnu/package.json)","packages/native-sdk/npm/linux-arm64-gnu/README.md":"# linux-arm64-gnu Documentation\n\nPath: `packages/native-sdk/npm/linux-arm64-gnu/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-arm64-gnu/README.md)","packages/native-sdk/npm/darwin-x64/package.json":"# Darwin-x64 Skill\n\nPath: `packages/native-sdk/npm/darwin-x64/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/darwin-x64/package.json)","packages/native-sdk/npm/darwin-x64/README.md":"# darwin-x64 Documentation\n\nPath: `packages/native-sdk/npm/darwin-x64/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/darwin-x64/README.md)","packages/native-sdk/npm/darwin-arm64/package.json":"# Darwin-arm64 Skill\n\nPath: `packages/native-sdk/npm/darwin-arm64/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/darwin-arm64/package.json)","packages/native-sdk/npm/darwin-arm64/README.md":"# darwin-arm64 Documentation\n\nPath: `packages/native-sdk/npm/darwin-arm64/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/darwin-arm64/README.md)",".cursorrules (knative-istio-typesense-gpu-cursorrules-prompt-fil.mdc)":"---\ndescription: \"Cursor rules for Knative development with Istio, Typesense, and GPU integration.\"\nglobs: **/*\nalwaysApply: false\n---\nYou are an expert AI programming assistant specializing in building Knative, Istio, Typesense, htmx and GPU accelerated applications.\n\nAs an AI assistant, your role is to provide guidance, code snippets, explanations, and troubleshooting support throughout the development process. You should be prepared to assist with all aspects of the project, from architecture design to implementation details.\n\n1. Knative\n  - Provide guidance on creating and managing Knative services\n  - Assist with serverless deployment configurations\n  - Help optimize autoscaling settings\n\n2. Istio\n  - Offer advice on service mesh configuration\n  - Help set up traffic management, security, and observability features\n  - Assist with troubleshooting Istio-related issues\n\n3. Typesense\n  - Provide guidance on Typesense setup and configuration\n  - Assist with index creation and search query optimization\n  - Help integrate Typesense with the backend API\n\n4. Frontend Development\n  - Offer suggestions for improving the HTMX-based frontend\n  - Assist with responsive design and user experience enhancements\n  - Help with client-side performance optimization\n\n5. Backend Development\n  - Guide the creation of serverless functions for the backend API\n  - Assist with integrating all components (htmx, Typesense)\n  - Help optimize API performance and error handling\n\n6. Testing and Monitoring\n  - Guide the creation of test cases for each component\n  - Assist with setting up monitoring and logging\n  - Help interpret performance metrics and suggest optimizations\n\n1. Always consider the serverless nature of the application when providing advice.\n2. Prioritize scalability, performance, and user experience in your suggestions.\n3. Explain complex concepts clearly, assuming the user has basic knowledge of the technologies involved.\n4. Offer alternative approaches or solutions when appropriate.\n5. Be prepared to dive deep into documentation or specifications of the used technologies if needed.\n6. Encourage best practices in cloud-native application development.\n7. When unsure about specific implementation details, clearly state assumptions and provide general guidance.\n\nAlways prioritize security, scalability, and maintainability in your designs and implementations. Leverage the power and simplicity of knative to create efficient and idiomatic code.\n\nProject-Specific Notes\n\n1. The frontend uses HTMX for simplicity. Suggest improvements while maintaining this approach.\n2. The backend should be implemented as Knative services.\n3. Typesense is the primary search engine. Focus on its strengths for fast, typo-tolerant searching.\n4. Istio should be leveraged for inter-service communication, security, and monitoring.\n\nRemember, your goal is to guide the development process, provide helpful insights, and assist in creating a robust, scalable, and efficient AI-powered search application.\n\nThese custom instructions provide a comprehensive guide for Claude to assist you with your AI-powered search project. They cover the key components of your system and outline the areas where you might need assistance.\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# Agent Guide\n\nGuidance for agents (and humans) working on this repository.\n\n## App authoring default\n\nNative SDK itself is implemented in Zig, but Native SDK **apps are authored in TypeScript + Native markup by default**. Do not infer the app-authoring language from this repository's implementation language or from older Zig-core examples.\n\n- For a new app, use `native init <path>` and expect `src/core.ts`, `src/app.native`, and `app.json`. `app.zon` remains a supported legacy/alternative manifest. Ordinary compiled TypeScript work that needs filesystem, process, JSON, regex, classes, or other static-tier APIs belongs under optional `src/services/`, reached from the core with `Cmd.request`; do not import a service from the core. Do not add Zig app code unless the user explicitly chooses `--template zig-core` or the feature requires a toolkit extension.\n- Before changing an existing app, inspect its tree. A `src/core.ts` app stays TypeScript; a `src/main.zig` app stays Zig unless the task is specifically a migration.\n- For default app work, read `skill-data/native-ui/SKILL.md` and `skill-data/ts-core/SKILL.md`; also read `skill-data/ts-services/SKILL.md` when the tree has `src/services/` or the task needs ordinary TypeScript beyond the core subset. `skill-data/core/SKILL.md` covers shared/runtime concerns; `skill-data/zig/SKILL.md` is for Zig-core apps and SDK implementation work.\n- The `-ts` suffix on a few examples only distinguishes ports from older Zig originals. New TypeScript apps need no suffix because TypeScript is the default.\n\n## Build, test, and gate\n\n```bash\nzig build test                # root engine + runtime suites\nzig build validate            # framework's legacy app.zon manifest check\nzig build test-example-<name> # one example's suite (e.g. test-example-notes)\nscripts/gate.sh fast [ref]    # affected-only local gate for your diff (default base: main)\nscripts/gate.sh full          # everything CI-shaped that runs locally\n```\n\nRun `scripts/gate.sh fast` before finishing any change; it maps your diff to the suites that cover it. The docs site checks with `pnpm --dir docs check` (the gate runs it only when `docs/` changed).\n\nThis repository builds with Zig 0.16.0. If a build fails with \"no member named\" errors on std APIs (`std.fs.cwd`, `ArrayList.init`, `std.io`), you are writing pre-0.16 idioms — `skill-data/zig/SKILL.md` maps each such compile error to the current idiom as this codebase writes it.\n\nPinned goldens (pixel signatures, schema fingerprints, command counts) are updated deliberately: review the rendered output or the counted commands first, and keep the pin's comment a self-contained description of what the value represents.\n\n## Changelog\n\nDo not edit `CHANGELOG.md` as part of regular feature or fix work. The release agent reviews the git history since the previous release and writes the complete changelog entry during release preparation; see [RELEASING.md](./RELEASING.md).\n\n## Where things live\n\n- `src/` — the engine and runtime; `src/primitives/canvas/` holds the widget, markup, and vector core.\n- `apps/schema/` — the standalone static Vercel project for `schema.native-sdk.dev`.\n- `examples/` — the showcase apps, many predating the JSON default (`app.zon` + `src/`).\n- `docs/` — the documentation site; `docs/AGENTS.md` has its MDX conventions.\n- `skills/` and `skill-data/` — the agent skills the CLI ships (`native skills list`).\n- `tools/` and `scripts/` — dev tooling and the local gate.\n\nReleases are maintainer-run; see [RELEASING.md](./RELEASING.md).\n","isInternal":false,"tokens":843,"sizeBytes":3561},{"name":"AGENTS.md","path":"docs/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/docs/AGENTS.md","title":"docs - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# Docs Site Conventions\n\n## MDX Tables\n\nAlways use HTML `<table>` syntax in MDX pages, never markdown pipe tables. This ensures consistent styling and avoids MDX parsing edge cases.\n\n```html\n<table>\n  <thead>\n    <tr>\n      <th>Column</th>\n      <th>Description</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>field</code></td>\n      <td>What it does</td>\n    </tr>\n  </tbody>\n</table>\n```\n\n## Geistdocs\n\nThe site uses the public `@vercel/geistdocs` package with Labs branding. Write pages in `content/docs` with a frontmatter `title` and no duplicate H1. Order pages and groups with `content/docs/meta.json` and folder metadata. Keep `/docs/<slug>` URLs and the permanent legacy redirects in `next.config.mjs` stable.\n\nShared navigation, search, page actions, Markdown routes, MDX rendering, and theme behavior belong to Geistdocs. Local adapters live under `src/components/geistdocs` and `src/lib/geistdocs`. Native component previews and the persisted TypeScript/Zig code tabs remain app-owned. `src/lib/remark-native.ts` exports their static data for Markdown; do not evaluate MDX expressions. Preserve `language:filename` fences and existing heading IDs through `src/lib/remark-docs.ts`.\n\nReuse an existing dev server. Run checks with `NEXT_DIST_DIR=.next-check pnpm check` so production checks do not share its `.next` directory. The check builds the site, verifies all canonical pages and Markdown siblings over HTTP, checks legacy redirects and discovery, and checks language tabs and the preview WASM. Also run `scripts/gate.sh fast` from the repository root before finishing.\n","isInternal":false,"tokens":379,"sizeBytes":1599},{"name":"eval.json","path":"evals/cases/brand-icon-package/eval.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/brand-icon-package/eval.json","title":"Brand-icon-package Skill","category":"anthropic-skill","format":"json","content":"# Brand-icon-package Skill\n\nPath: `evals/cases/brand-icon-package/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/brand-icon-package/eval.json)","tokens":300,"sizeBytes":206},{"name":"eval.json","path":"evals/cases/chat-composer/eval.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/chat-composer/eval.json","title":"Chat-composer Skill","category":"anthropic-skill","format":"json","content":"# Chat-composer Skill\n\nPath: `evals/cases/chat-composer/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/chat-composer/eval.json)","tokens":300,"sizeBytes":191},{"name":"eval.json","path":"evals/cases/disk-status-spans/eval.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/disk-status-spans/eval.json","title":"Disk-status-spans Skill","category":"anthropic-skill","format":"json","content":"# Disk-status-spans Skill\n\nPath: `evals/cases/disk-status-spans/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/disk-status-spans/eval.json)","tokens":300,"sizeBytes":203},{"name":"eval.json","path":"evals/cases/dual-feed-table/eval.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/dual-feed-table/eval.json","title":"Dual-feed-table Skill","category":"anthropic-skill","format":"json","content":"# Dual-feed-table Skill\n\nPath: `evals/cases/dual-feed-table/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/dual-feed-table/eval.json)","tokens":300,"sizeBytes":197},{"name":"eval.json","path":"evals/cases/dual-ledger-csv-split/eval.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/dual-ledger-csv-split/eval.json","title":"Dual-ledger-csv-split Skill","category":"anthropic-skill","format":"json","content":"# Dual-ledger-csv-split Skill\n\nPath: `evals/cases/dual-ledger-csv-split/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/dual-ledger-csv-split/eval.json)","tokens":300,"sizeBytes":215},{"name":"eval.json","path":"evals/cases/dual-list-delete-fix/eval.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/dual-list-delete-fix/eval.json","title":"Dual-list-delete-fix Skill","category":"anthropic-skill","format":"json","content":"# Dual-list-delete-fix Skill\n\nPath: `evals/cases/dual-list-delete-fix/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/dual-list-delete-fix/eval.json)","tokens":300,"sizeBytes":212},{"name":"eval.json","path":"evals/cases/dual-notes-autosave/eval.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/dual-notes-autosave/eval.json","title":"Dual-notes-autosave Skill","category":"anthropic-skill","format":"json","content":"# Dual-notes-autosave Skill\n\nPath: `evals/cases/dual-notes-autosave/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/dual-notes-autosave/eval.json)","tokens":300,"sizeBytes":209},{"name":"eval.json","path":"evals/cases/dual-pomodoro-timer/eval.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/dual-pomodoro-timer/eval.json","title":"Dual-pomodoro-timer Skill","category":"anthropic-skill","format":"json","content":"# Dual-pomodoro-timer Skill\n\nPath: `evals/cases/dual-pomodoro-timer/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/dual-pomodoro-timer/eval.json)","tokens":300,"sizeBytes":209},{"name":"eval.json","path":"evals/cases/dual-sysinfo-panel/eval.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/dual-sysinfo-panel/eval.json","title":"Dual-sysinfo-panel Skill","category":"anthropic-skill","format":"json","content":"# Dual-sysinfo-panel Skill\n\nPath: `evals/cases/dual-sysinfo-panel/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/dual-sysinfo-panel/eval.json)","tokens":300,"sizeBytes":206},{"name":"eval.json","path":"evals/cases/expenses-table/eval.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/expenses-table/eval.json","title":"Expenses-table Skill","category":"anthropic-skill","format":"json","content":"# Expenses-table Skill\n\nPath: `evals/cases/expenses-table/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/expenses-table/eval.json)","tokens":300,"sizeBytes":194},{"name":"eval.json","path":"evals/cases/file-browser-panes/eval.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/file-browser-panes/eval.json","title":"File-browser-panes Skill","category":"anthropic-skill","format":"json","content":"# File-browser-panes Skill\n\nPath: `evals/cases/file-browser-panes/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/file-browser-panes/eval.json)","tokens":300,"sizeBytes":206},{"name":"eval.json","path":"evals/cases/habits-tracker/eval.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/habits-tracker/eval.json","title":"Habits-tracker Skill","category":"anthropic-skill","format":"json","content":"# Habits-tracker Skill\n\nPath: `evals/cases/habits-tracker/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/habits-tracker/eval.json)","tokens":300,"sizeBytes":194},{"name":"eval.json","path":"evals/cases/inspector-window/eval.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/inspector-window/eval.json","title":"Inspector-window Skill","category":"anthropic-skill","format":"json","content":"# Inspector-window Skill\n\nPath: `evals/cases/inspector-window/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/inspector-window/eval.json)","tokens":300,"sizeBytes":200},{"name":"eval.json","path":"evals/cases/kanban-board/eval.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/kanban-board/eval.json","title":"Kanban-board Skill","category":"anthropic-skill","format":"json","content":"# Kanban-board Skill\n\nPath: `evals/cases/kanban-board/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/kanban-board/eval.json)","tokens":300,"sizeBytes":188},{"name":"eval.json","path":"evals/cases/metrics-dashboard/eval.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/metrics-dashboard/eval.json","title":"Metrics-dashboard Skill","category":"anthropic-skill","format":"json","content":"# Metrics-dashboard Skill\n\nPath: `evals/cases/metrics-dashboard/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/metrics-dashboard/eval.json)","tokens":300,"sizeBytes":203},{"name":"eval.json","path":"evals/cases/playlist-row-actions/eval.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/playlist-row-actions/eval.json","title":"Playlist-row-actions Skill","category":"anthropic-skill","format":"json","content":"# Playlist-row-actions Skill\n\nPath: `evals/cases/playlist-row-actions/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/playlist-row-actions/eval.json)","tokens":300,"sizeBytes":212},{"name":"eval.json","path":"evals/cases/process-monitor/eval.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/process-monitor/eval.json","title":"Process-monitor Skill","category":"anthropic-skill","format":"json","content":"# Process-monitor Skill\n\nPath: `evals/cases/process-monitor/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/process-monitor/eval.json)","tokens":300,"sizeBytes":197},{"name":"eval.json","path":"evals/cases/release-dashboard/eval.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/release-dashboard/eval.json","title":"Release-dashboard Skill","category":"anthropic-skill","format":"json","content":"# Release-dashboard Skill\n\nPath: `evals/cases/release-dashboard/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/release-dashboard/eval.json)","tokens":300,"sizeBytes":203},{"name":"eval.json","path":"evals/cases/settings-picker/eval.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/settings-picker/eval.json","title":"Settings-picker Skill","category":"anthropic-skill","format":"json","content":"# Settings-picker Skill\n\nPath: `evals/cases/settings-picker/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/settings-picker/eval.json)","tokens":300,"sizeBytes":197},{"name":"eval.json","path":"evals/cases/templates-settings-app/eval.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/templates-settings-app/eval.json","title":"Templates-settings-app Skill","category":"anthropic-skill","format":"json","content":"# Templates-settings-app Skill\n\nPath: `evals/cases/templates-settings-app/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/templates-settings-app/eval.json)","tokens":300,"sizeBytes":218},{"name":"eval.json","path":"evals/cases/ts-countdown-bugfix/eval.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/ts-countdown-bugfix/eval.json","title":"Ts-countdown-bugfix Skill","category":"anthropic-skill","format":"json","content":"# Ts-countdown-bugfix Skill\n\nPath: `evals/cases/ts-countdown-bugfix/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/ts-countdown-bugfix/eval.json)","tokens":300,"sizeBytes":209},{"name":"eval.json","path":"evals/cases/ts-expenses-filter/eval.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/ts-expenses-filter/eval.json","title":"Ts-expenses-filter Skill","category":"anthropic-skill","format":"json","content":"# Ts-expenses-filter Skill\n\nPath: `evals/cases/ts-expenses-filter/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/ts-expenses-filter/eval.json)","tokens":300,"sizeBytes":206},{"name":"eval.json","path":"evals/cases/ts-habits-core/eval.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/ts-habits-core/eval.json","title":"Ts-habits-core Skill","category":"anthropic-skill","format":"json","content":"# Ts-habits-core Skill\n\nPath: `evals/cases/ts-habits-core/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/ts-habits-core/eval.json)","tokens":300,"sizeBytes":194},{"name":"eval.json","path":"evals/cases/ts-tag-input-core/eval.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/ts-tag-input-core/eval.json","title":"Ts-tag-input-core Skill","category":"anthropic-skill","format":"json","content":"# Ts-tag-input-core Skill\n\nPath: `evals/cases/ts-tag-input-core/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/ts-tag-input-core/eval.json)","tokens":300,"sizeBytes":203},{"name":"package.json","path":"evals/package.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/package.json","title":"@native-sdk/evals","category":"anthropic-skill","format":"json","content":"{\n  \"name\": \"@native-sdk/evals\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"packageManager\": \"pnpm@10.23.0\",\n  \"scripts\": {\n    \"typecheck\": \"tsc --noEmit\",\n    \"eval\": \"node src/main.ts\",\n    \"metrics\": \"node src/authoring-metrics.ts\",\n    \"check\": \"pnpm typecheck\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^24\",\n    \"typescript\": \"^5\"\n  },\n  \"dependencies\": {\n    \"@vercel/sandbox\": \"^2.3.0\"\n  },\n  \"engines\": {\n    \"node\": \">=24\"\n  }\n}\n","frontmatter":{"name":"@native-sdk/evals","version":"0.0.0","private":true,"type":"module","packageManager":"pnpm@10.23.0","scripts":{"typecheck":"tsc --noEmit","eval":"node src/main.ts","metrics":"node src/authoring-metrics.ts","check":"pnpm typecheck"},"devDependencies":{"@types/node":"^24","typescript":"^5"},"dependencies":{"@vercel/sandbox":"^2.3.0"},"engines":{"node":">=24"}},"isInternal":false,"tokens":166,"sizeBytes":467},{"name":"pnpm-lock.yaml","path":"evals/pnpm-lock.yaml","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/pnpm-lock.yaml","title":"Evals Skill","category":"anthropic-skill","format":"yaml","content":"lockfileVersion: '9.0'\n\nsettings:\n  autoInstallPeers: true\n  excludeLinksFromLockfile: false\n\nimporters:\n\n  .:\n    dependencies:\n      '@vercel/sandbox':\n        specifier: ^2.3.0\n        version: 2.3.0\n    devDependencies:\n      '@types/node':\n        specifier: ^24\n        version: 24.13.2\n      typescript:\n        specifier: ^5\n        version: 5.9.3\n\npackages:\n\n  '@types/node@24.13.2':\n    resolution: {integrity: sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==}\n\n  '@vercel/oidc@3.2.0':\n    resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==}\n    engines: {node: '>= 20'}\n\n  '@vercel/sandbox@2.3.0':\n    resolution: {integrity: sha512-KLhc/sj2JuftgWEivPmmDq3iFLIoYU0XIv+44paTyhk5eGGaqNl6dQdxXSqOf+N31jh55m4UWY7i8oJy8oJRrQ==}\n\n  '@workflow/serde@4.1.0-beta.2':\n    resolution: {integrity: sha512-8kkeoQKLDaKXefjV5dbhBj2aErfKp1Mc4pb6tj8144cF+Em5SPbyMbyLCHp+BVrFfFVCBluCtMx+jjvaFVZGww==}\n\n  async-retry@1.3.3:\n    resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==}\n\n  b4a@1.8.1:\n    resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==}\n    peerDependencies:\n      react-native-b4a: '*'\n    peerDependenciesMeta:\n      react-native-b4a:\n        optional: true\n\n  bare-events@2.9.1:\n    resolution: {integrity: sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==}\n    peerDependencies:\n      bare-abort-controller: '*'\n    peerDependenciesMeta:\n      bare-abort-controller:\n        optional: true\n\n  events-universal@1.0.1:\n    resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==}\n\n  fast-fifo@1.3.2:\n    resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}\n\n  jose@6.2.3:\n    resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==}\n\n  jsonlines@0.1.1:\n    resolution: {integrity: sha512-ekDrAGso79Cvf+dtm+mL8OBI2bmAOt3gssYs833De/C9NmIpWDWyUO4zPgB5x2/OhY366dkhgfPMYfwZF7yOZA==}\n\n  ms@2.1.3:\n    resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}\n\n  os-paths@4.4.0:\n    resolution: {integrity: sha512-wrAwOeXp1RRMFfQY8Sy7VaGVmPocaLwSFOYCGKSyo8qmJ+/yaafCl5BCA1IQZWqFSRBrKDYFeR9d/VyQzfH/jg==}\n    engines: {node: '>= 6.0'}\n\n  picocolors@1.1.1:\n    resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}\n\n  retry@0.13.1:\n    resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}\n    engines: {node: '>= 4'}\n\n  streamx@2.28.0:\n    resolution: {integrity: sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==}\n\n  tar-stream@3.1.7:\n    resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==}\n\n  text-decoder@1.2.7:\n    resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==}\n\n  typescript@5.9.3:\n    resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}\n    engines: {node: '>=14.17'}\n    hasBin: true\n\n  undici-types@7.18.2:\n    resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}\n\n  undici@7.28.0:\n    resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==}\n    engines: {node: '>=20.18.1'}\n\n  xdg-app-paths@5.1.0:\n    resolution: {integrity: sha512-RAQ3WkPf4KTU1A8RtFx3gWywzVKe00tfOPFfl2NDGqbIFENQO4kqAJp7mhQjNj/33W5x5hiWWUdyfPq/5SU3QA==}\n    engines: {node: '>=6'}\n\n  xdg-portable@7.3.0:\n    resolution: {integrity: sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw==}\n    engines: {node: '>= 6.0'}\n\n  zod@4.4.3:\n    resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}\n\nsnapshots:\n\n  '@types/node@24.13.2':\n    dependencies:\n      undici-types: 7.18.2\n\n  '@vercel/oidc@3.2.0': {}\n\n  '@vercel/sandbox@2.3.0':\n    dependencies:\n      '@vercel/oidc': 3.2.0\n      '@workflow/serde': 4.1.0-beta.2\n      async-retry: 1.3.3\n      jose: 6.2.3\n      jsonlines: 0.1.1\n      ms: 2.1.3\n      picocolors: 1.1.1\n      tar-stream: 3.1.7\n      undici: 7.28.0\n      xdg-app-paths: 5.1.0\n      zod: 4.4.3\n    transitivePeerDependencies:\n      - bare-abort-controller\n      - react-native-b4a\n\n  '@workflow/serde@4.1.0-beta.2': {}\n\n  async-retry@1.3.3:\n    dependencies:\n      retry: 0.13.1\n\n  b4a@1.8.1: {}\n\n  bare-events@2.9.1: {}\n\n  events-universal@1.0.1:\n    dependencies:\n      bare-events: 2.9.1\n    transitivePeerDependencies:\n      - bare-abort-controller\n\n  fast-fifo@1.3.2: {}\n\n  jose@6.2.3: {}\n\n  jsonlines@0.1.1: {}\n\n  ms@2.1.3: {}\n\n  os-paths@4.4.0: {}\n\n  picocolors@1.1.1: {}\n\n  retry@0.13.1: {}\n\n  streamx@2.28.0:\n    dependencies:\n      events-universal: 1.0.1\n      fast-fifo: 1.3.2\n      text-decoder: 1.2.7\n    transitivePeerDependencies:\n      - bare-abort-controller\n      - react-native-b4a\n\n  tar-stream@3.1.7:\n    dependencies:\n      b4a: 1.8.1\n      fast-fifo: 1.3.2\n      streamx: 2.28.0\n    transitivePeerDependencies:\n      - bare-abort-controller\n      - react-native-b4a\n\n  text-decoder@1.2.7:\n    dependencies:\n      b4a: 1.8.1\n    transitivePeerDependencies:\n      - react-native-b4a\n\n  typescript@5.9.3: {}\n\n  undici-types@7.18.2: {}\n\n  undici@7.28.0: {}\n\n  xdg-app-paths@5.1.0:\n    dependencies:\n      xdg-portable: 7.3.0\n\n  xdg-portable@7.3.0:\n    dependencies:\n      os-paths: 4.4.0\n\n  zod@4.4.3: {}\n","isInternal":false,"tokens":2957,"sizeBytes":6008},{"name":"README.md","path":"evals/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/README.md","title":"Evals Skill","category":"anthropic-skill","format":"markdown","content":"# Native SDK evals\n\nAn eval harness for AI-agent authoring of Native SDK apps. It formalizes the \"clean-agent trial\": give a fresh agent nothing but a scaffolded workspace, the `native-ui` skill, and a task prompt, then grade what it produced deterministically.\n\nPer case the runner:\n\n1. **Scaffolds** a fresh workspace with the repo's own CLI — `zig build` at the repo root, then `zig-out/bin/native init evals/.workspaces/<case> --frontend native` (`--template zig-core` for the pre-existing native cases and the zig side of dual cases, `--template ts-core` for the ts side) — and delivers each track's skills exactly the way a real user gets them: `native skills get <name>` written to `.claude/skills/<name>/SKILL.md` (`init` does not ship skills). The zig track gets `native-ui` + `zig`; the ts app track gets `ts-core` + `native-ui`; core-only ts-core cases get `ts-core`. The workspace is then **pre-warmed** (`native test` once — workspaces are zero-config, so builds go through the CLI verbs) so the agent's own builds are incremental and its wall-clock isn't spent compiling the SDK.\n2. **Runs the agent-under-test**: `claude -p \"<task prompt>\"` headless in the workspace, routed through the Vercel AI Gateway, with a per-run `CLAUDE_CONFIG_DIR` so no user-level memory/plugins/hooks leak in, `--max-turns`, a wall-clock timeout, and the full `stream-json` transcript captured to `results/`.\n3. **Grades** with deterministic checks: `native test` in the workspace, `native markup check` on the `.native` files, per-case file greps (e.g. \"the board uses `<template>`\"), and live automation-snapshot greps (`native build` with `-Dautomation=true`, launch, wait for the automation snapshot, grep it for expected roles/names).\n4. **Judges** quality the deterministic checks can't see — idiomatic Model/Msg design, template factoring, test meaningfulness — with an `llm_judge` check: a judge model called directly through the gateway scores case-specific criteria 0–10 against the task prompt and the agent's code. Advisory by default (the score is recorded and printed but never fails the case); set `\"advisory\": false` on a case to make `minScore` a gate. Skipped in `--dry-run`.\n5. **Reports** a per-case `result.json` (pass/fail per check, judge scores, durations, model, turns, cost) plus a console summary table.\n\n## Requirements\n\n- macOS (live snapshot checks launch the app; use `--skip-live` elsewhere, or `--sandbox` for the Linux lane), Zig 0.16.0, node >= 24, pnpm 10.x.\n- The [Claude Code CLI](https://code.claude.com/docs) (`claude`) on PATH.\n- A [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) API key for real runs:\n\n```sh\nexport AI_GATEWAY_API_KEY=\"vck_...\"   # or VERCEL_AI_GATEWAY_API_KEY\n```\n\nThe runner assembles the gateway env for the claude subprocess per [Vercel's Claude Code guide](https://vercel.com/docs/ai-gateway/coding-agents/claude-code):\n\n```sh\nANTHROPIC_BASE_URL=https://ai-gateway.vercel.sh\nANTHROPIC_AUTH_TOKEN=$AI_GATEWAY_API_KEY\nANTHROPIC_API_KEY=            # empty string on purpose: a non-empty value would win over the auth token\n```\n\nModels are gateway slugs. The coder (agent-under-test) defaults to `anthropic/claude-sonnet-5` (override with `--model` or `NATIVE_SDK_EVAL_MODEL`); the judge defaults to `anthropic/claude-opus-4.8` (override with `--judge-model` or `NATIVE_SDK_EVAL_JUDGE_MODEL`). Both tracks of a dual case always run with the SAME coder model — fairness is a property of the run, so set it once at run time.\n\n## Usage\n\n```sh\ncd evals\npnpm install\n\npnpm eval --list                      # list cases\npnpm eval --dry-run                   # everything except the model call (no key needed):\n                                      #   scaffold + skill delivery, print env + claude argv,\n                                      #   run the graders against the untouched scaffold\n                                      #   (grader FAILs are expected there and exit 0)\npnpm eval templates-settings-app      # one real run\npnpm eval                             # the whole suite\npnpm eval --model anthropic/claude-opus-4.8 templates-settings-app\npnpm eval --judge-model anthropic/claude-fable-5 templates-settings-app\npnpm eval --skip-live                 # skip snapshot checks (no app launch / non-macOS)\npnpm eval --track ts dual-feed-table  # one track of a dual-track case (default: both)\npnpm eval --keep-workspaces           # keep .workspaces/<case> around for inspection\npnpm eval --trials 5 expenses-table   # 5 independent trials per case; report pass rates\npnpm eval --concurrency 3             # run up to 3 case trials in parallel (default 2 locally)\npnpm eval --sandbox                   # run each case in its own Vercel Sandbox microVM\npnpm eval --sandbox --dry-run         # full sandbox path minus the model call\npnpm eval --sandbox --sandbox-vcpus 8 # bigger sandboxes (2048 MB RAM per vCPU)\npnpm typecheck\n```\n\nCases run in parallel (log lines are prefixed `[case-name]`); `--concurrency` caps how many at once — locally the default is 2 to keep zig builds from thrashing, with `--sandbox` the default is 4 (each case has its own VM, but plans rate-limit vCPU allocation).\n\n### Trials\n\nModel runs are stochastic; a single pass or fail is weak evidence. `--trials <n>` runs each case n times, each trial **fully independent** — its own scaffolded workspace (`.workspaces/<case>-trial-<n>`), its own agent run, its own checks and judge call — and reports per-case pass **rates** (e.g. `3/5`), per-check pass counts, and the mean judge score. Trials share the `--concurrency` pool (log lines are prefixed `[case-name#trial]`), and with `--sandbox` each trial gets its own microVM.\n\nWith `--trials 1` (the default) the behavior and file layout are exactly the single-run layout described above. With `--trials > 1` each case directory nests per-trial results plus an aggregate:\n\n```\nresults/<stamp>/\n  summary.json                      # array of per-case aggregates\n  <case>/\n    aggregate.json                  # pass rate, per-check pass counts, mean judge score, per-trial results\n    trial-1/result.json             # exactly a single-run result.json (plus a \"trial\" field)\n    trial-1/transcript.jsonl\n    trial-2/...\n```\n\nThe summary table swaps the PASS/FAIL column for a `pass rate` column, the `checks` column shows per-check pass counts (`3/3 2/3 ...`, `s` = skipped in every trial), `judge` is the mean score, `cost`/`time` are totals across trials, and a per-check breakdown is printed under the table. A real run exits non-zero if any trial failed.\n\n### Lanes\n\nEvery result carries a **lane** — where the case ran and got graded — and the summary table has a lane column:\n\n- `macos-local` (default): the run described above; live snapshot checks launch the app directly and require macOS.\n- `linux-sandbox` (`--sandbox`): each case trial runs in its own isolated [Vercel Sandbox](https://vercel.com/docs/sandbox) microVM booted from a pre-baked Linux image, **including the live checks** — the app builds with `-Dplatform=linux -Dweb-engine=system -Dautomation=true`, launches under Xvfb, and is driven through the same automation dropbox the macOS lane uses. An engine screenshot of the app's gpu_surface is captured through the dropbox and pulled back with the results.\n\nA check that greps a surface which exists on only one OS can declare `\"lanes\": [\"macos-local\"]` in `eval.json`; on other lanes it reports **skipped** (`not graded on the ... lane`) instead of failing, so the summary distinguishes \"fails\" from \"not applicable on this lane\". Audited 2026-07-05: none of the ten shipped cases needs a lane annotation — every snapshot pattern asserts roles/names from the SDK's own automation snapshot, and the surfaces they cover (secondary windows, gpu charts, trees, tables) are proven on Linux by `tools/linux-truth/`. A Linux-lane failure is therefore a real failure until a case says otherwise.\n\n### Vercel Sandbox mode\n\n`--sandbox` boots each case trial from a custom image (see `evals/sandbox/`) that bakes the Linux GUI stack (GTK4 + WebKitGTK + Xvfb), zig, node/pnpm, the Claude Code CLI, and a **pre-warmed build layer**: the repo at a pinned ref with the CLI, workspace-test, and automation build graphs already compiled into fixed cache paths. Per case the runner then:\n\n1. uploads the repo **working tree** as a tarball and rsyncs it over the baked repo — deletions propagate, caches survive, so builds against the current tip are incremental on top of the bake;\n2. starts Xvfb and re-invokes this same harness inside (`pnpm eval --skip-permissions --lane linux-sandbox <case>`) with the gateway env assembled exactly like a local run;\n3. pulls the whole case results directory home — `result.json`, `transcript.jsonl`, `live-*.png` engine screenshots — before the microVM is destroyed.\n\n`--dangerously-skip-permissions` is safe there because the whole VM is the throwaway. `--sandbox --dry-run` exercises the entire path — provisioning, refresh, scaffold, graders (which FAIL against the untouched scaffold, as designed), artifact pull — without the model call, and exits 0.\n\n**Image**: build and push once with `evals/sandbox/build-image.sh` (needs a Docker login to the registry — the two token variants are in the script header). Rebuild when the Dockerfile changes, when zig bumps, or when the tip has drifted far enough from the baked ref that in-sandbox builds stop feeling incremental; runs stay *correct* without a rebuild because of the working-tree refresh. After a push the registry prepares the image for a few minutes; the runner retries `image_not_ready` for up to 10 minutes. `--sandbox-image` overrides the default reference (`eval-sandbox`, resolved in the linked project).\n\n**Auth** (checked before any sandbox work): either an OIDC token — one-time setup in `evals/`:\n\n```sh\nvercel link --scope vercel-labs --project zero-native\nvercel env pull .env.local   # the runner auto-loads VERCEL_OIDC_TOKEN from .env.local\n```\n\n(the token expires ~12h; re-run `vercel env pull .env.local` when sandbox auth fails) — or `VERCEL_TOKEN` + `VERCEL_TEAM_ID` + `VERCEL_PROJECT_ID` for environments where OIDC is unavailable. Real runs additionally need `AI_GATEWAY_API_KEY` as usual.\n\n**Cost and limits**: sandbox compute is metered (active CPU + provisioned memory) — a 4-vCPU case trial that runs 20-30 minutes lands around $0.20-0.35 plus the model tokens through the gateway. Sandboxes cap at 45 minutes wall clock on the Hobby plan (the runner's per-sandbox timeout), and vCPU allocation is rate-limited per plan, which is why the default `--concurrency` in sandbox mode is 4.\n\nReal runs exit non-zero if any case fails. Workspaces live in `.workspaces/` and results in `results/<timestamp>/<case>/` (`result.json`, `transcript.jsonl`, the isolated `claude-config/` — kept in-VM and not pulled for sandbox runs, plus `live-*.png` screenshots from the Linux lane); both directories are gitignored.\n\n### Permissions for the agent-under-test\n\nBy default the agent runs with `--permission-mode acceptEdits` plus an allowlist covering `zig ...`, `native-sdk ...`, and basic file commands — enough for unattended edit/build/test loops without granting arbitrary shell. `--skip-permissions` switches to `--dangerously-skip-permissions`; only use it if the default allowlist blocks a case, and remember the workspace is a throwaway dir but the process is not otherwise sandboxed.\n\nIn both modes the runner passes `--disallowedTools` deny rules for `evals/cases/**` and `evals/results/**`: the workspace references the SDK repo by path, so the harness itself is reachable from the agent's cwd, and agents exploring the repo for docs/examples were observed reading their own grading config (3/20 runs of the 2026-07-04 suite). SDK source and examples stay readable on purpose — a real user has the repo.\n\n## Cases\n\n- `templates-settings-app` — validates the new grammar: repeated grouped toggle sections where `<template>`/`<use>` is the natural shape, plus token style attributes (muted headers, surface cards). Checks: build+tests, markup check, `<template>`/`<use>` greps, token-attribute greps, snapshot roles.\n- `kanban-board` — port of the manual builder trial; card identity must survive moving between columns (`global-key`).\n- `habits-tracker` — port of the manual markup trial; text entry (elm-style mirror), derived/filtered lists, enum filters.\n- `expenses-table` — exercises the newest grammar (every built-in component markup-expressible): an expense ledger whose natural shape is `table` > `table-row` > `table-cell` with `<for>` rows, an exclusive category filter, and an alert-shaped empty state. The prompt describes only requirements (rows-and-columns of data, a callout, pinned display strings); the greps assert the agent reached the table grammar from the skill alone.\n- `process-monitor` — exercises the effects surface: a long-running local command (a harmless `sh -c` tick loop the prompt pins exactly) spawned from update through the effects channel, lines streaming into a bounded 12-line list, cancel by model-owned key, and status/counts derived from the line/exit Msgs. Greps assert `.update_fx` wiring, `fx.spawn`/`fx.cancel`, fake-executor tests, and the absence of hand-rolled `std.process.Child`/`std.Thread`; the live snapshot asserts the idle state (Start/Cancel, \"Status: idle\", \"0 lines · 0 dropped\") so nothing spawns during grading. The judge scores effect-key discipline, non-blocking behavior, honest drop accounting, and derive-don't-store.\n- `release-dashboard` — exercises the pipeline composites and markdown-in-markup: a release dashboard whose natural shape is `<stepper>` for the five-stage track (starting on \"Canary\"), `<timeline>`/`<timeline-item>` for the seeded event history, and a `<markdown>`-sourced notes panel containing a GFM table. Greps assert all three elements plus `<for>` events; the snapshot asserts the composite semantics (`\"Canary (active)\"` stepper labels, timeline items by title, `role=gridcell` markdown table cells, the pinned status summary). The judge scores house-style composition (no ad-hoc reimplementations) and declarative markup.\n- `settings-picker` — exercises the anchored floating surface pattern: two picker rows (Theme/Accent) whose natural shape is a `<select>` trigger + `<if>`-mounted `<dropdown-menu anchor=...>` of `<menu-item>`s with `on-dismiss`, model-owned open state, and an exclusive-open rule. Greps assert the select/anchored-dropdown/on-dismiss/menu-item composition; the snapshot asserts the closed idle state and the pinned status line (the trigger's accessible name may be its value text or an explicit `label` — both legit, the pattern allows either).\n- `file-browser-panes` — exercises split panes and the tree keymap: a resizable two-pane browser (`<split value= on-resize=>` with pane `min-width` floors) whose sidebar is a `<tree>` of `role=\"treeitem\"` rows with model-owned `expanded` state driving a detail pane. The snapshot asserts `role=separator`/`role=tree`/`role=treeitem`, the seeded rows, and the pinned selection status. The judge is explicitly told the tree keymap is engine-provided so it never expects app-side keyboard Msgs.\n- `metrics-dashboard` — exercises the markup chart element and icon-in-button: the whole view is markup (header with `<button icon=...>`, the `<chart>` with a `<series values=\"{binding}\">`, status bar) and the Zig side owns only model/update logic. Greps assert `<chart`, `icon=\"`, and the pinned `y-min=\"0\"`; the snapshot asserts `role=chart` plus the seeded summary.\n- `inspector-window` — exercises model-declared secondary windows: a task list whose inspector opens as a real second OS window through `Options.windows_fn` + `window_view`, live-updates with the selection, reflects a user close via `on_close`, and never duplicates. A negative grep rejects faking it with `<dialog>`/`<sheet>`/`<drawer>`.\n- `disk-status-spans` — exercises inline text spans: one wrapped paragraph mixing bold, muted, and monospace runs (announcing as a single text run), with bound values interpolated inside the spans and a derived percentage summary. Greps assert the three span styles inside one enclosing `<text>`; the snapshot asserts the paragraph as ONE combined text run with resolved bindings.\n- `chat-composer` — exercises the composer composites: an `<input-group>` wrapping a `<textarea>` plus an in-border `<input-group-actions>` row, an attached `<button-group>` cluster, a derived disabled Send, and a `<for>` message list. The snapshot asserts the composer textbox, the attached cluster, Send disabled on the empty draft, the seeded messages, and the live count.\n- `brand-icon-package` — exercises the one-image app-icon pipeline through `native package`: the manifest sources icons from a single square PNG at the brand path, no prebuilt icon containers anywhere, and the packaged macOS bundle carries a generated `AppIcon.icns` wired into its metadata.\n- `playlist-row-actions` — exercises the interaction seams: row-level Enter as a list row's primary action (`on-submit` on `list-item`, distinct from select-on-press) and the app-level key fallback (`Options.on_key`) for Space and the arrows when nothing is focused. Greps assert the row-level submit binding and the fallback wiring; the snapshot asserts the seeded rows, the model-driven selection, and the idle derived status.\n\nAdd a case by creating `cases/<name>/eval.json` (see `src/types.ts` for the schema). Prompts describe app **requirements**, never the solution — the point is to see whether a fresh agent reaches the intended grammar from the skill alone.\n\n## The TS-authoring track\n\nCases with `\"frontend\": \"ts-core\"` measure the other authoring surface: the app core written in the TypeScript subset and compiled by `packages/core`. The scaffold is not an app — it is `src/core.ts` (the case's `starter/` overlay when it ships one, else a minimal counter core), a README with the check loop, and the `ts-core` skill delivered via `native skills get ts-core`. Two graders replace build/markup/snapshot:\n\n- `ts_transpile` — the frontend check must exit clean on `src/core.ts`: tsc-semantics typecheck, every subset rule (NS1001-NS1050) — no Zig emission; the literal name is the `eval.json` check key, the semantics are `ts_check`. Failing diagnostics stay in the result as the violation evidence.\n- `ts_harness` — behavioral grading: compile the core through the external core compiler and drive the compiled archive through a generated mirror — the scratch dir carries the mirror module, the archive, and the case's `harness.zig`, then `zig test harness.zig`. The harness drives the real dispatch cycle (`update` → `commitModelRoot` → `frameReset`) and asserts the prompt's requirements, so the case prompt pins the Model/Msg/export contract exactly (an API spec, not a solution).\n\nBecause the grading harness compiles against the agent's code, ts-core prompts pin names; behavior stays requirements-only. The compiler handles pure `update(model, msg): Model` cores and effectful `Model | [Model, Cmd<Msg>]` pair-returns (the Cmd surface); the wave-2 dual-track cases below are the effects coverage.\n\nThe ts-core cases:\n\n- `ts-habits-core` — a fresh core from a requirements description (the TS mirror of `habits-tracker`): seeded habits, toggle-with-streak math, a trimmed draft add path, exclusive filters, derived counts.\n- `ts-expenses-filter` — a feature-add to an existing subset file: extend a working expense ledger (shipped as `starter/`) with an exclusive `Category | null` filter and derived filtered views, without breaking the existing behavior.\n- `ts-countdown-bugfix` — a bug-fix in an existing subset file: a countdown core whose tick/reset/set_duration transitions drifted from the spec (unguarded ticks, no completion stop/floor, hardcoded reset); the prompt states the intended behavior, the harness isolates each drift.\n- `ts-tag-input-core` — the text seam under pressure: comma-separated tag parsing with byte-level trim, empty-drop, and case-insensitive dedupe, where string methods are the reflex and NS1004 says no.\n\n## The dual-track cases (wave 2)\n\nWave 1's four ts-* cases measured subset compliance on toy katas and compared against Zig numbers gathered **before** the `zig` 0.16-idioms skill existed. Wave 2 replaces that comparison with an honest, contemporaneous one: six realistic asks, each ONE language-blind spec (`\"frontend\": \"app-dual\"`) that runs on **both authoring tracks** — `<case>@ts` scaffolds a full TypeScript app (`native init --frontend native --template ts-core`), `<case>@zig` the Zig app template (`--template zig-core`). Identical prompt, identical shared checks, one behavioral spec asserted by two thin per-track harnesses; `--track ts|zig` selects a lane, the default runs both.\n\nGrading per track: the shared checks (`native test -Dplatform=null`, markup check, view greps, the judge with a case rubric) plus the track's behavioral harness. On the ts track, `ts_harness` compiles `src/core.ts` (its whole import graph) through the external core compiler and `zig test`s the case's `harness-ts.zig` against the compiled core's generated mirror and `harness-lib/cmdview.zig` — a decoder over the Cmd/Sub wire format, so harnesses assert effects semantically (\"one GET to the pinned URL\", \"the delay re-armed on the same key\"). On the zig track, `zig_harness` injects the case's `harness-zig.zig` into the workspace as `src/eval_behavior_spec.zig` (a test import appended to `src/main.zig`, restored afterward) and runs `native test`, so it compiles against the agent's real Model/Msg/update and drives the SDK's deterministic **fake effects executor** (`fx.executor = .fake`, `pendingSpawnAt`/`feedLine`/`feedExit`/`fireTimer`/`feedResponse`/`feedFileResult`).\n\nThe cases — every prompt reads like a real user ask, and every effect result is fed by the harness (no network, no processes, no clocks during grading):\n\n- `dual-feed-table` — \"fetch JSON from this API and show it in a sortable table\": the HTTP effect (pinned URL, buffered GET, keyed), JSON parsing of a pinned flat schema, two sort orders derived at view time, and the five honest states (idle / loading with re-entry guarded / loaded / explicit empty / failed keeping previous rows) across non-200, transport-failure, and malformed-body deliveries.\n- `dual-notes-autosave` — \"add debounced autosave to this notes app\" (starter provided per track, both riding the SDK text-input engines): the keyed one-shot re-arm debounce (Cmd.delay / fx.startTimer replace), byte-exact notes.tsv serialization, Save now with the pending autosave cancelled, a five-state save lifecycle, and the late-result race (a save landing after newer edits must not mark them saved).\n- `dual-pomodoro-timer` — \"build a pomodoro timer with a completion sound\": the recurring timer surface (Sub.timer declared from the model / fx timer armed-cancelled through the channel — armed exactly while running), auto-advancing focus/rest state machine, work-only completion counting, stale-tick guards, and the audio surface at completions.\n- `dual-list-delete-fix` — \"this list doesn't update after delete — fix it\": a realistic seeded bug (a hand-maintained visible-list cache that delete forgot) in a medium app with a markup view; the harness pins list correctness across delete/toggle/add/filter, the judge scores whether the fix is the derive-don't-store root cause or a symptom patch.\n- `dual-ledger-csv-split` — \"split this core into modules and add export-to-CSV in the new module\": multi-file authoring (relative imports on ts, `@import` on zig, both grepped), byte-exact CSV quoting (commas and doubled quotes exercised by the seeds), the file-write effect, and a four-state export lifecycle.\n- `dual-sysinfo-panel` — \"show system info from a shell command in the UI\": a collect-mode background spawn of a pinned argv, byte parsing of the output line, re-entry guarded while probing, and honest ok / failed-with-code / never-started states.\n\nStarters (`starter-ts/`, `starter-zig/`) overlay the scaffold for the feature-add, bug-fix, and split cases — written in each track's idiom (immutable spread cores over the SDK text engine on ts; bounded buffers, `canvas.TextBuffer`, and Model-method bindings on zig) so neither track starts from translated code.\n\n**Fairness and methodology.** Both tracks get identical prompts (language-neutral asks; the pinned contract names arms/fields in the wire spelling and states each track's idiom for the few surfaces that necessarily differ — routed err arms on ts vs payload-carried outcomes on zig). Each track gets its CURRENT skill set by the documented delivery path and nothing extra: ts → `ts-core` + `native-ui`; zig → `native-ui` + `zig` (the 0.16-idioms skill). The same coder model grades both tracks of a run. Known caveats to carry into any writeup: wave-1's Zig numbers predate the `zig` skill (this suite is the corrected baseline — the pre-existing native cases now also receive it, so their history splits at this commit); the behavioral harnesses are compiled contracts, so a run can fail for contract-shape reasons the prompt pins explicitly; and the two tracks' harness glue differs by necessity (wire-format decoding vs fake-executor introspection) while asserting the same spec, test for test.\n\n### Authoring metrics\n\n`pnpm metrics results/<stamp> [...]` post-processes finished runs' transcripts into the agent-authoring metrics the checks cannot see, per case and per track — **ts** (the ts-core cases and the `@ts` side of dual cases) vs **zig** (the pre-existing native cases and the `@zig` side): **first-pass compliance** (did the agent's first compliance check after touching sources pass — the frontend check, `native check`, `native test`, or `native build` on the ts track; `native test` / `native build` / `native check` / `native markup check` on the zig track), **retries-to-green** (failing compliance runs before the first green), **teaching-error encounters** (failing compliance runs that carried a teaching diagnostic — the \"did the diagnostics work\" round-trip count wave 2 compares across tracks), **violation taxonomy** (NS/TS rule IDs on the ts track; zig error lines on the zig track, with `no member named 'X'` bucketed by member so the 0.16-idiom class is visible) raw and per 1k generated LOC (lines written through Write/Edit to source files, `.native` markup included), and **task success** (the run's own pass verdict). Harness friction — permission-refused commands, errored compounds with no diagnostic in the output — is dropped from the event stream so it never masquerades as an authoring failure. It writes `authoring-metrics.json` next to each `summary.json`.\n\n## CI\n\nCI only typechecks the harness (`evals-typecheck` in `.github/workflows/ci.yml`). Eval runs are local/manual — no model calls in CI.\n","isInternal":false,"tokens":6337,"sizeBytes":26981},{"name":"tsconfig.json","path":"evals/tsconfig.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/tsconfig.json","title":"Evals Skill","category":"anthropic-skill","format":"json","content":"# Evals Skill\n\nPath: `evals/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/tsconfig.json)","tokens":300,"sizeBytes":151},{"name":"package-lock.json","path":"packages/core/package-lock.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/core/package-lock.json","title":"Core Skill","category":"anthropic-skill","format":"json","content":"# Core Skill\n\nPath: `packages/core/package-lock.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/core/package-lock.json)","tokens":300,"sizeBytes":174},{"name":"package.json","path":"packages/core/package.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/core/package.json","title":"Core Skill","category":"anthropic-skill","format":"json","content":"# Core Skill\n\nPath: `packages/core/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/core/package.json)","tokens":300,"sizeBytes":164},{"name":"pnpm-lock.yaml","path":"packages/core/pnpm-lock.yaml","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/core/pnpm-lock.yaml","title":"Core Skill","category":"anthropic-skill","format":"yaml","content":"# Core Skill\n\nPath: `packages/core/pnpm-lock.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/core/pnpm-lock.yaml)","tokens":300,"sizeBytes":168},{"name":"package.json","path":"packages/native-sdk/npm/darwin-arm64/package.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/darwin-arm64/package.json","title":"Darwin-arm64 Skill","category":"anthropic-skill","format":"json","content":"# Darwin-arm64 Skill\n\nPath: `packages/native-sdk/npm/darwin-arm64/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/darwin-arm64/package.json)","tokens":300,"sizeBytes":218},{"name":"package.json","path":"packages/native-sdk/npm/darwin-x64/package.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/darwin-x64/package.json","title":"Darwin-x64 Skill","category":"anthropic-skill","format":"json","content":"# Darwin-x64 Skill\n\nPath: `packages/native-sdk/npm/darwin-x64/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/darwin-x64/package.json)","tokens":300,"sizeBytes":212},{"name":"package.json","path":"packages/native-sdk/npm/linux-arm64-gnu/package.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-arm64-gnu/package.json","title":"Linux-arm64-gnu Skill","category":"anthropic-skill","format":"json","content":"# Linux-arm64-gnu Skill\n\nPath: `packages/native-sdk/npm/linux-arm64-gnu/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-arm64-gnu/package.json)","tokens":300,"sizeBytes":227},{"name":"package.json","path":"packages/native-sdk/npm/linux-arm64-musl/package.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-arm64-musl/package.json","title":"Linux-arm64-musl Skill","category":"anthropic-skill","format":"json","content":"# Linux-arm64-musl Skill\n\nPath: `packages/native-sdk/npm/linux-arm64-musl/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-arm64-musl/package.json)","tokens":300,"sizeBytes":230},{"name":"package.json","path":"packages/native-sdk/npm/linux-x64-gnu/package.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-x64-gnu/package.json","title":"Linux-x64-gnu Skill","category":"anthropic-skill","format":"json","content":"# Linux-x64-gnu Skill\n\nPath: `packages/native-sdk/npm/linux-x64-gnu/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-x64-gnu/package.json)","tokens":300,"sizeBytes":221},{"name":"package.json","path":"packages/native-sdk/npm/linux-x64-musl/package.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-x64-musl/package.json","title":"Linux-x64-musl Skill","category":"anthropic-skill","format":"json","content":"# Linux-x64-musl Skill\n\nPath: `packages/native-sdk/npm/linux-x64-musl/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-x64-musl/package.json)","tokens":300,"sizeBytes":224},{"name":"package.json","path":"packages/native-sdk/npm/win32-arm64/package.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/win32-arm64/package.json","title":"Win32-arm64 Skill","category":"anthropic-skill","format":"json","content":"# Win32-arm64 Skill\n\nPath: `packages/native-sdk/npm/win32-arm64/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/win32-arm64/package.json)","tokens":300,"sizeBytes":215},{"name":"package.json","path":"packages/native-sdk/npm/win32-x64/package.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/win32-x64/package.json","title":"Win32-x64 Skill","category":"anthropic-skill","format":"json","content":"# Win32-x64 Skill\n\nPath: `packages/native-sdk/npm/win32-x64/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/win32-x64/package.json)","tokens":300,"sizeBytes":209},{"name":"package.json","path":"packages/native-sdk/package.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/package.json","title":"Native-sdk Skill","category":"anthropic-skill","format":"json","content":"# Native-sdk Skill\n\nPath: `packages/native-sdk/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/package.json)","tokens":300,"sizeBytes":182},{"name":"SKILL.md","path":"skill-data/automation/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/skill-data/automation/SKILL.md","title":"automation","category":"anthropic-skill","format":"markdown","content":"---\nname: automation\ndescription: Automation and verification guide for running Native SDK apps. Use when the user asks to test a running app, inspect runtime state, list windows, wait for readiness, drive widgets, take deterministic screenshots, send bridge commands, debug why automation is not connected, create smoke tests, or verify a Native SDK example in a GUI-capable session.\n---\n\n# Automate Native SDK apps\n\nEvery Native SDK app embeds an automation server — native-rendered apps and WebView-shell apps alike. It works through file-based IPC in `.zig-cache/native-sdk-automation/` and is intended for smoke tests, CI checks with a GUI session, and quick runtime inspection: accessibility snapshots, widget driving through the real input paths, deterministic reference-renderer screenshots, readiness/state assertions, and bridge round-trips.\n\nAutomation is not browser DOM automation. It reports runtime/window/widget state, drives retained-canvas widgets, and can ask the runtime to reload or dispatch bridge requests. For DOM testing of the optional WebView path, use the frontend framework's tests or a browser automation tool against the dev server.\n\n## What automation can verify\n\n- An automation-enabled app started and published `ready=true`.\n- The runtime loaded the expected app name, source kind, and window metadata.\n- The main window exists and is focused/open.\n- The JavaScript-to-Zig bridge can round-trip a request through `native automate bridge`.\n- Builtin window/WebView commands work when exercised by a smoke test.\n- Reload requests are accepted by the runtime.\n- Real pixels of retained-canvas (`gpu_surface`) views: `native automate screenshot <view-label>` renders the view's current canvas frame through the deterministic CPU reference renderer and writes a PNG artifact. Two captures of an unchanged scene are byte-identical, so screenshots can back golden-image or \"did the UI change\" checks.\n\n## What automation cannot verify\n\n- Screenshots of WebView content. `screenshot` covers `gpu_surface` canvas views only; there is no DOM/WebView pixel capture.\n- Arbitrary DOM queries and clicks.\n- Browser network assertions.\n\n## Prerequisites\n\nBuild/run an app with automation enabled. Generated examples usually expose `-Dautomation=true`:\n\n```bash\nzig build run -Dplatform=macos -Dautomation=true\n```\n\nRepository examples may have specialized steps:\n\n```bash\nzig build run-webview -Dplatform=macos -Dautomation=true\nzig build test-webview-smoke -Dplatform=macos\n```\n\nThe runner must pass an automation server into `RuntimeOptions`:\n\n```zig\nconst server = native_sdk.automation.Server.init(io, \".zig-cache/native-sdk-automation\", \"My App\");\nvar runtime = native_sdk.Runtime.init(.{\n    .platform = my_platform,\n    .automation = server,\n});\n```\n\nApps built without `-Dautomation=true` usually ignore automation files.\n\n## Commands\n\n```bash\nnative automate wait\nnative automate assert 'gpu_nonblank=true' 'role=button name=\"Reset\"'\nnative automate assert --absent 'error event='\nnative automate list\nnative automate snapshot\nnative automate reload\nnative automate screenshot inbox-canvas\nnative automate screenshot inbox-canvas 2\nnative automate widget-action canvas 2 press\nnative automate widget-click canvas 3\nnative automate widget-hold canvas 3\nnative automate widget-context-press canvas 3\nnative automate widget-drag canvas 4 0.25 0.82\nnative automate widget-wheel canvas 5 18\nnative automate widget-key canvas tab\nnative automate widget-key canvas cmd+c\nnative automate widget-pinch canvas 1.5\nnative automate widget-pinch canvas 0.5 120 80\nnative automate profile on\nnative automate profile off\nnative automate bridge '{\"id\":\"smoke\",\"command\":\"native.ping\",\"payload\":{\"source\":\"automation\"}}'\n```\n\nIf using the repository-built CLI:\n\n```bash\nzig-out/bin/native automate wait\nzig-out/bin/native automate snapshot\n```\n\n## Snapshot assertions (`automate assert`)\n\nPrefer `native automate assert` over `snapshot | grep` chains: it polls, so no sleeps, and its failure output carries the evidence (each missing pattern plus the snapshot tail).\n\n```bash\nnative automate assert 'gpu_nonblank=true' 'role=button name=\"Reset\"' 'count: 0'\nnative automate assert --timeout-ms 10000 '4 open'\nnative automate assert --absent 'error event=' 'dispatch_errors=[1-9]'\n```\n\nSemantics:\n\n- Every argument is a regex that must match somewhere in `snapshot.txt`. The command polls (100ms interval) until all match, then exits 0.\n- `--timeout-ms <n>` bounds the polling (default 30000). On timeout it prints `missing: <pattern>` for each unmatched pattern, the last 20 snapshot lines, and exits non-zero — CI-friendly, no wrapper script needed.\n- `--absent` inverts the whole invocation: every pattern must NOT match (poll until gone). Mix presence and absence by running two invocations.\n- Supported regex subset: literals, `.`, postfix `*` `+` `?`, line anchors `^`/`$`, classes `[a-z]`/`[^0-9]`, and `\\d \\w \\s` (with uppercase negations). No groups or alternation — pass multiple patterns instead.\n- Quote patterns in single quotes so the shell leaves `\"`, `$`, and `\\d` alone.\n\n## Standard workflow\n\n1. Start the app with automation enabled.\n2. Run `native automate wait` to block until `snapshot.txt` contains `ready=true`.\n3. Run `native automate assert '<pattern>' ...` for state checks, or `native automate snapshot` to eyeball app/window/source metadata.\n4. Run `native automate list` to inspect window summaries.\n5. Run `native automate bridge '...'` for bridge round-trip checks.\n6. Use `native automate widget-action <view-label> <widget-id> <action> [value]` to exercise retained canvas widget actions. `set_text` routes through the SAME input path real typing uses (focus, select-all, then a text-input event), so a TEA app's `on_input` mirror receives the edits and model state stays consistent with the on-screen field — it is not a presentation-only write.\n7. Use `native automate widget-click <view-label> <widget-id>` to exercise pointer-style retained widget routing. `widget-hold <view-label> <widget-id>` drives a press-and-hold through the same path — pointer down, the reserved hold timer fired, then the suppressed release — so `on_hold` Msgs are live-drivable (a target without `on_hold` degrades to the click a real long press is). `widget-context-press <view-label> <widget-id>` is the secondary click: it presents the widget's context menu, or dispatches `on_hold` immediately when the route declares none.\n8. Use `native automate widget-drag <view-label> <widget-id> <start-x-ratio> <end-x-ratio> [start-y-ratio end-y-ratio]` for continuous pointer controls.\n9. Use `native automate widget-wheel <view-label> <widget-id> <delta-y>` for retained widget scroll input. Wheel targets must be interactive/scrollable widgets — a plain layout column or text node is not a wheel target; aim at the scroll/list widget id from the snapshot. Failures land in the snapshot as named reasons: `error event=automation.widget_wheel name=WheelTargetUnknown|WheelTargetNotInteractive|WheelTargetHasEmptyBounds detail=\"<command args>\"`.\n10. Use `native automate widget-key <view-label> <key> [text]` for focused retained widget keyboard input. The key accepts modifier chords — `cmd+a`, `cmd+c`, `cmd+v`, `cmd+x`, `ctrl+shift+arrowleft` (`cmd` sets the primary shortcut modifier on every platform) — so select-all/copy/cut/paste and shift-extended selection are drivable; after a copy, widget lines in the snapshot show the live selection as `selection=a..b`, and the copied text lands on the real system clipboard (`pbpaste` on macOS).\n11. Use `native automate widget-pinch <view-label> <scale> [x y]` for trackpad pinch gestures against a gpu-surface view: the runtime dispatches the real `pinch_begin`/`pinch_change`/`pinch_end` platform events, with one change carrying `scale - 1`. `<scale>` is the FINAL multiplicative zoom for the gesture — the cumulative gesture scale (the product of `1 + delta`) lands exactly on it — `1.5` zooms in 50%, `0.5` zooms out to half. The optional anchor point is view-local points, defaulting to the view center. Apps hear it through the pinch channel (`Options.on_pinch` / the TS core's `pinchMsg`).\n12. Use `native automate screenshot <view-label> [scale]` to capture the named `gpu_surface` view's canvas as `screenshot-<view-label>.png` (the CLI prints the artifact path and waits for the file).\n13. Before driving an app-menu command, inspect the snapshot's `command id=\"...\"` catalog and `app-menu` / `app-menu-item` rows to prove the running app loaded the expected `app.zon` or runner declarations. Then use `native automate menu-command <id>` to dispatch the same `.menu_command` platform event a real selection emits; the verb remains a raw event injector, so the snapshot receipt is what validates registration.\n14. Use `native automate tray-action <item-id>` for the primary status item, or `native automate tray-action <status-item-id> <item-id>` for an explicit item. Both select a dropdown row through the same platform event a real menu-bar click emits (command dispatch with source `.tray`). Live items appear in `snapshot.txt` as `tray #status-id title=\"...\" visible=... items=N` followed by `  tray-item #item-id ...` rows — the menu bar is outside every window capture, so this is the automation evidence for every model-driven item. Unknown id pairs degrade into the dispatch-error ring as `automation.tray_action`.\n15. Use `native automate reload` to request a WebView reload.\n16. Use `native automate profile on` to enable per-stage frame timing: while on, `snapshot.txt` carries a `frame_profile` line with rolling p50/p90/max microseconds per pipeline stage (`rebuild`, `layout`, `reconcile`, `emit`, `a11y`, `plan`, `patch`, `encode`, `present`, `host_decode`, `host_draw`), each with a lifetime sample count (`<stage>_n=`). Drive some interactions, then `native automate snapshot | grep -o 'frame_profile.*'` to read where frame time goes; `profile off` stops recording and drops the line. Turning it on starts a fresh sample window.\n\n## Screenshots\n\n`screenshot <view-label> [scale]` asks the runtime to rasterize the view's\ncurrent retained canvas frame through the deterministic CPU reference\nrenderer — the same pixel path the Linux software presentation uses — and\npublish it as an uncompressed PNG at\n`.zig-cache/native-sdk-automation/screenshot-<view-label>.png`. The file is\nwritten atomically (temp file + rename), so its presence means the PNG is\ncomplete.\n\nDeterminism semantics:\n\n- Screenshots render at scale 1 by default regardless of the display's\n  backing scale, so an unchanged scene produces byte-identical PNGs from\n  capture to capture on the same machine. Pass an explicit scale (for\n  example `2`) for high-DPI pixel dimensions.\n- Screenshots use the live retained scene, including live design tokens and\n  platform text measurement (CoreText on macOS): the layout matches what is\n  on screen. Glyphs are rasterized by the reference renderer from the\n  bundled faces, not by the platform's font rasterizer, so screenshots are a\n  deterministic layout/structure/color signal rather than a platform\n  font-rendering signal.\n- Cross-machine byte-identity is only guaranteed where text metrics are\n  deterministic (the null platform's estimator). On platforms with a native\n  text measurement provider, text widths can differ between OS versions, so\n  compare screenshots taken on the same machine or assert on properties\n  (dimensions, changed/unchanged) rather than exact bytes across machines.\n- OS-level captures (`screencapture -x` on macOS) are NOT a substitute: in a\n  shell without Screen Recording permission they exit 0 and silently return\n  wallpaper-only images with no app window in them. If you shell out for a\n  real-pixel capture, verify the image is not blank/wallpaper before trusting\n  it; `automate screenshot` plus a semantics snapshot is the reliable pair.\n\n## Bridge smoke test pattern\n\nThe request must be JSON with an ID, command, and payload:\n\n```bash\nnative automate bridge '{\"id\":\"smoke\",\"command\":\"native.ping\",\"payload\":{\"source\":\"automation\"}}'\n```\n\nAutomation sends the request with origin `zero://inline`. The app's bridge policy must allow that origin or the call will reject with `permission_denied`. For packaged asset origins, app code often allows `zero://app`; for automation smoke tests, add `zero://inline` only when the test needs it.\n\nExpected response shape depends on the handler. A typical `native.ping` handler returns:\n\n```json\n{\"id\":\"smoke\",\"ok\":true,\"result\":{\"message\":\"pong\",\"count\":1}}\n```\n\nIf the command fails, inspect the bridge error code:\n\n- `unknown_command`: no handler registered or wrong command name.\n- `permission_denied`: origin or permission policy blocked it.\n- `handler_failed`: Zig handler returned an error or invalid JSON.\n- `payload_too_large`: request exceeded bridge limits.\n\n## File protocol\n\nThe default directory is `.zig-cache/native-sdk-automation/`, resolved against the CLI's CURRENT WORKING DIRECTORY — run `native automate` from the app project's directory (where the app was launched). The dir is created by the running app, never by the CLI: a command sent from the wrong cwd fails loudly (`error: no automation dir at <abs path>`) instead of queueing into a dir no app reads, and every queued command prints the absolute dir it wrote to — check that line when a command seems to do nothing.\n\nFiles:\n\n- `snapshot.txt`: app name, readiness, source kind, source size, window metadata, accessibility summary. The `ready=true` line also carries `protocol=<n>` (the CLI/app handshake: the CLI refuses snapshots — and command queues to a live app — whose protocol version is not its own, naming both versions; the fix is rebuilding whichever binary is stale and comparing `native version`), `dispatch_errors=<total>` and `dropped_trace_records=<total>`, and recent degraded handler/update errors appear as `  error event=<tag> name=<ErrorName> timestamp_ns=...` lines — a handler error no longer exits the app, so grep these to notice one happened. While `profile on` is active, a `frame_profile <stage>_p50_us=... <stage>_p90_us=... <stage>_max_us=... <stage>_n=...` line follows the header with per-stage frame timing. The header also carries `markup_watch=armed|off` — whether the markup hot-reload watch is armed (armed only in builds where the app wired `.markup` with a `watch_path` and `io`, or registered compiled fragments through `fragment_watch` — i.e. Debug dev builds).\n- `windows.txt`: window list.\n- `command.txt`: command input written by CLI and consumed by runtime. The slot is single-entry and the app consumes one command per presented frame: `native automate <command>` WAITS for the running app to consume it and prints `delivered <action> -> <dir>` on success — it refuses/fails loudly instead of silently overwriting an unconsumed command, and exits non-zero if the app never consumes it.\n- `bridge-response.txt`: last bridge response.\n- `screenshot-<view-label>.png`: deterministic reference-rendered PNG of a `gpu_surface` view, written by the `screenshot` command.\n\nThe runtime polls `command.txt`. After processing a command, it writes `done`.\n\n## Debugging automation failures\n\nIf `native automate wait` times out with NO snapshot file at all, it prints a teaching error naming the automation dir it watched and pointing at `-Dautomation=true` and the working directory — start from that message. Otherwise:\n\n1. Confirm the app is still running.\n2. Confirm it was built with `-Dautomation=true`.\n3. Confirm the runner passes `automation` into `Runtime.init`.\n4. Check `.zig-cache/native-sdk-automation/snapshot.txt`.\n5. Delete stale files in `.zig-cache/native-sdk-automation/` and restart the app.\n6. Run with more tracing, for example `zig build run -Dtrace=all`.\n\nIf the CLI reports an automation protocol mismatch (or a snapshot with no protocol version): the `native` binary and the app were built from different framework versions — rebuild the older side (stale `zig-out/bin` copies of the CLI are the classic cause; `native version` prints the commit the binary was built from).\n\nStale INSTANCES are called out too: automate verbs print a LOUD warning when the publishing app's process started BEFORE the newest binary in `zig-out/bin` was built — a leftover instance from an earlier run impersonating the new build. Kill it and relaunch the fresh binary before trusting any snapshot.\n\nIf `snapshot` says no app connected:\n\n- The automation directory may not exist yet.\n- The app may be running from a different working directory.\n- The app may be built without automation.\n- The app may not have reached runtime startup.\n\nIf bridge automation fails:\n\n- Check command name spelling.\n- Check app handler registration.\n- Check bridge policy origins for `zero://inline`.\n- Check runtime permissions.\n- Check that the handler returns valid JSON.\n\n## CI and smoke tests\n\nUse automation for minimal integration confidence:\n\n```bash\nzig build test-webview-smoke -Dplatform=macos\n```\n\nA good smoke test:\n\n1. Builds an example with `-Dautomation=true` and `-Djs-bridge=true`.\n2. Starts the app in a GUI-capable session.\n3. Waits for readiness.\n4. Verifies snapshot metadata (`automate assert` with the patterns that matter).\n5. Sends `native.ping`.\n6. Exercises builtin windows/WebViews if the app enables them.\n7. Fails on timeout or unexpected bridge response.\n\nApps scaffolded with `native init --full` ship this as `.github/workflows/ci.yml` (the zero-config default scaffold skips CI): a null-platform `zig build test` job plus a Linux Xvfb smoke job that launches the binary, runs `automate wait`, asserts on the snapshot with `automate assert`, and checks a non-empty `automate screenshot` artifact. Extend that file rather than writing grep chains by hand.\n\nDo not use automation for exhaustive UI testing. It is a runtime and bridge smoke layer.\n\n## Notes\n\n- Automation is compile-time gated: apps built without `-Dautomation=true` ignore automation files.\n- Screenshots cover retained-canvas (`gpu_surface`) views only; WebView pixels are not captured.\n- WebView DOM interaction is intentionally out of scope for this file-based automation layer.\n- Use `native skills get core --full` for app architecture, bridge policy, packaging, and debugging context.\n","frontmatter":{"name":"automation","description":"Automation and verification guide for running Native SDK apps. Use when the user asks to test a running app, inspect runtime state, list windows, wait for readiness, drive widgets, take deterministic screenshots, send bridge commands, debug why automation is not connected, create smoke tests, or verify a Native SDK example in a GUI-capable session."},"isInternal":false,"tokens":4114,"sizeBytes":18336},{"name":"app-model-runtime.md","path":"skill-data/core/references/app-model-runtime.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/skill-data/core/references/app-model-runtime.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `skill-data/core/references/app-model-runtime.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/skill-data/core/references/app-model-runtime.md)","tokens":300,"sizeBytes":212},{"name":"bridge-security-native-capabilities.md","path":"skill-data/core/references/bridge-security-native-capabilities.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/skill-data/core/references/bridge-security-native-capabilities.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `skill-data/core/references/bridge-security-native-capabilities.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/skill-data/core/references/bridge-security-native-capabilities.md)","tokens":300,"sizeBytes":248},{"name":"frontend-assets.md","path":"skill-data/core/references/frontend-assets.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/skill-data/core/references/frontend-assets.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `skill-data/core/references/frontend-assets.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/skill-data/core/references/frontend-assets.md)","tokens":300,"sizeBytes":208},{"name":"project-anatomy.md","path":"skill-data/core/references/project-anatomy.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/skill-data/core/references/project-anatomy.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `skill-data/core/references/project-anatomy.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/skill-data/core/references/project-anatomy.md)","tokens":300,"sizeBytes":208},{"name":"web-engines-packaging-debugging.md","path":"skill-data/core/references/web-engines-packaging-debugging.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/skill-data/core/references/web-engines-packaging-debugging.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `skill-data/core/references/web-engines-packaging-debugging.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/skill-data/core/references/web-engines-packaging-debugging.md)","tokens":300,"sizeBytes":240},{"name":"SKILL.md","path":"skill-data/core/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/skill-data/core/SKILL.md","title":"core","category":"anthropic-skill","format":"markdown","content":"---\nname: core\ndescription: Core Native SDK guide for AI agents. Read this before explaining the Native SDK or changing a Native SDK app. Establishes TypeScript + Native markup as the default app-authoring path, routes default app work to the native-ui and ts-core skills, and covers the shared foundation: project structure, app.json and legacy app.zon, lower-level App and Runtime patterns, frontend integration, web engines, JavaScript bridge commands, permissions, windows, WebViews, dialogs, packaging, debugging, and testing. Use when the user asks what the Native SDK is, how to build or modify an app, how to package or debug it, or how to add native capabilities.\n---\n\n# Build Native SDK apps\n\nThe Native SDK is a cross-platform native app toolkit inspired by the web. The default app (`native init`) is native-rendered and authored as a declarative Native markup view (`src/app.native`) plus a TypeScript app core (`src/core.ts`). The core is checked and compiled ahead of time to native code; no JS runtime ships in the binary. **For default app implementation, load both the `native-ui` and `ts-core` skills.** Zig is the implementation language of the toolkit and a first-class app-core choice via `--template zig-core`, not the default app-authoring language.\n\nThis skill covers the shared foundation and lower-level architectures: apps that explicitly choose a Zig core, runtime extensions, and apps that render web frontends in a WebView (`native init --frontend next|vite|react|svelte|vue`). Native-rendered surfaces and WebViews share windows, policies, lifecycle, commands, and platform services, and one app can mix them. WebView engines are the platform WebView (WKWebView on macOS, WebKitGTK on Linux) or Chromium through CEF where supported.\n\nAgents should assume they do not know the Native SDK from general model knowledge. Read this skill first. For ordinary app implementation, load `native-ui` and `ts-core`; for shared/runtime, WebView, packaging, or native-capability work, run `native skills get core --full` so the referenced files are included in the CLI output.\n\n## Mental model\n\n- The default app has three files of truth: `src/core.ts` (`Model`, `Msg`, `update`), `src/app.native` (UI), and `app.json` (manifest).\n- Native markup binds model values and dispatches typed messages; only `update` changes state.\n- The TypeScript core compiles to native code. Node is a build/check/dev tool, not an app runtime.\n- `App` is the lower-level product/runtime interface used by generated wiring, Zig-core apps, WebView shells, and extensions.\n- `Runtime` owns the event loop, windows, bridge dispatch, security checks, automation, tracing, platform services, and window state.\n- `WebViewSource` tells the runtime what to load: inline HTML, a URL, or packaged assets from a local app origin.\n- `app.json` is the default app manifest: identity, icons, windows, frontend assets, web engine, permissions, bridge policy, security policy, and packaging inputs. `app.zon` remains supported.\n- `src/runner.zig` and `src/main.zig` appear only when an app explicitly owns lower-level Zig wiring (for example a `zig-core` app or a WebView shell). `--full` makes a TypeScript app own `build.zig` but still does not give it Zig app logic. Do not add Zig sources to a default TypeScript app merely because SDK examples contain them.\n- `frontend/` is normal web code. It talks to native Zig through `window.zero.invoke()` or builtin helpers when those are enabled.\n\n## Task router\n\nThese references are included by `native skills get core --full`. Use them when the task touches the topic:\n\n- Project creation, generated files, build steps: `references/project-anatomy.md`\n- Default TypeScript core authoring, checker rules, effects, subscriptions, modules: `native skills get ts-core`\n- Native markup, bindings, messages, hot reload, view testing: `native skills get native-ui`\n- `App`, `Runtime`, callbacks, embedding, tests: `references/app-model-runtime.md`\n- React/Vue/Svelte/Next/Vite, dev server, bundled assets: `references/frontend-assets.md`\n- App-defined bridge commands, builtin commands, permissions, windows, WebViews, dialogs: `references/bridge-security-native-capabilities.md`\n- Web engine choice, CEF, packaging, signing, doctor, logs, debugging: `references/web-engines-packaging-debugging.md`\n- Running-app inspection and smoke tests: `native skills get automation`\n- `zig build` fails on std APIs (\"no member named 'cwd'\", ArrayList `init`): `native skills get zig` — the Zig 0.16 idioms, indexed by compile error\n\n## Quick start\n\nUse the CLI for new apps:\n\n```bash\nnpm install -g @native-sdk/cli\nnative init my_app\ncd my_app\nnative dev\n```\n\nThis creates `src/core.ts`, `src/app.native`, and `app.json`; a native window opens with `native dev`. Use `native init my_app --template zig-core` only when the user chooses Zig. Frontend choices (`--frontend next|vite|react|svelte|vue`) are the separate WebView migration/integration path.\n\n## Workflow for existing apps\n\nBefore editing an existing Native SDK app:\n\n1. Read `app.json` or `app.zon` and inspect `src/` before assuming a core language. Read `src/core.ts` + `src/app.native` for the default path; read `src/main.zig`, `src/runner.zig`, and `build.zig` only when they exist.\n2. Identify whether the app is TypeScript + Native markup (default), an explicitly chosen Zig core, or a WebView frontend, then identify the layer the change belongs to.\n3. Follow the generated code and examples in the repository instead of inventing a new app layout.\n4. Prefer exact security policy changes over broad allowances.\n5. Validate with the narrowest useful command.\n\nCommon file ownership:\n\n- `app.json` / `app.zon`: app identity, version, icons, windows, permissions, capabilities, bridge command policy, allowed origins, frontend dist/dev config, web engine, CEF config.\n- `src/core.ts`: default app state and behavior — `Model`, `Msg`, `update`, pure helpers, `Cmd`, and `Sub`.\n- `src/app.native`: default app UI — elements, layout, bindings, and message dispatch.\n- `src/main.zig`: explicitly chosen Zig-core logic or lower-level `App` behavior, source selection, lifecycle callbacks, and custom bridge handlers.\n- `src/runner.zig`: `Runtime.init`, platform selection, security policy, builtin bridge policy, `js_window_api`, automation server, trace sinks, panic capture, window state store.\n- `build.zig`: build options, frontend build/dev/package steps, platform link setup, test steps.\n- `frontend/`: web app implementation, `window.zero` calls, dev/build config.\n\n## Lower-level WebView app model (advanced)\n\nThis is not the default app-authoring path. A lower-level Zig/WebView shell returns `native_sdk.App` with `context`, `name`, and a WebView source:\n\n```zig\nconst App = struct {\n    fn app(self: *@This()) native_sdk.App {\n        return .{\n            .context = self,\n            .name = \"my-app\",\n            .source = native_sdk.WebViewSource.html(\"<h1>Hello from Native SDK</h1>\"),\n        };\n    }\n};\n```\n\nUse these source constructors:\n\n- `native_sdk.WebViewSource.html(content)` for small inline demos.\n- `native_sdk.WebViewSource.url(address)` for an explicit URL.\n- `native_sdk.WebViewSource.assets(.{ .root_path = \"frontend/dist\", .entry = \"index.html\" })` for packaged frontend assets.\n\nFor framework apps, prefer a dynamic source so development loads the local dev server and production loads bundled assets:\n\n```zig\nfn source(context: *anyopaque) anyerror!native_sdk.WebViewSource {\n    const self: *App = @ptrCast(@alignCast(context));\n    return native_sdk.frontend.sourceFromEnv(self.env_map, .{\n        .dist = \"frontend/dist\",\n        .entry = \"index.html\",\n    });\n}\n```\n\n`sourceFromEnv` reads `NATIVE_SDK_FRONTEND_URL`; otherwise it serves the configured asset directory. Use it for most framework apps.\n\n## app.json essentials\n\nKeep `app.json` as the source of truth for app-level behavior. Existing `app.zon` projects use the same fields in ZON syntax.\n\n```json\n{\n  \"$schema\": \"https://schema.native-sdk.dev/app/v1.json\",\n  \"id\": \"com.example.my-app\",\n  \"name\": \"my-app\",\n  \"display_name\": \"My App\",\n  \"description\": \"One line about the app, shown in the About panel.\",\n  \"version\": \"0.1.0\",\n  \"icons\": [\"assets/icon.png\"],\n  \"platforms\": [\"macos\", \"linux\"],\n  \"permissions\": [],\n  \"capabilities\": [\"webview\"],\n  \"frontend\": {\n    \"dist\": \"frontend/dist\",\n    \"entry\": \"index.html\",\n    \"spa_fallback\": true,\n    \"dev\": {\n      \"url\": \"http://127.0.0.1:5173/\",\n      \"command\": [\"npm\", \"--prefix\", \"frontend\", \"run\", \"dev\", \"--\", \"--host\", \"127.0.0.1\"],\n      \"ready_path\": \"/\",\n      \"timeout_ms\": 30000\n    }\n  },\n  \"security\": {\n    \"navigation\": {\n      \"allowed_origins\": [\"zero://app\", \"http://127.0.0.1:5173\"],\n      \"external_links\": { \"action\": \"deny\" }\n    }\n  },\n  \"web_engine\": \"system\",\n  \"windows\": [\n    { \"label\": \"main\", \"title\": \"My App\", \"width\": 960, \"height\": 640, \"restore_state\": true }\n  ]\n}\n```\n\nLegacy ZON equivalent:\n\n```zig\n.{\n    .id = \"com.example.my-app\",\n    .name = \"my-app\",\n    .display_name = \"My App\",\n    .description = \"One line about the app, shown in the About panel.\",\n    .version = \"0.1.0\",\n    .icons = .{\"assets/icon.png\"},\n    .platforms = .{ \"macos\", \"linux\" },\n    .permissions = .{},\n    .capabilities = .{ \"webview\" },\n    .frontend = .{\n        .dist = \"frontend/dist\",\n        .entry = \"index.html\",\n        .spa_fallback = true,\n        .dev = .{\n            .url = \"http://127.0.0.1:5173/\",\n            .command = .{ \"npm\", \"--prefix\", \"frontend\", \"run\", \"dev\", \"--\", \"--host\", \"127.0.0.1\" },\n            .ready_path = \"/\",\n            .timeout_ms = 30000,\n        },\n    },\n    .security = .{\n        .navigation = .{\n            .allowed_origins = .{ \"zero://app\", \"http://127.0.0.1:5173\" },\n            .external_links = .{ .action = \"deny\" },\n        },\n    },\n    .web_engine = \"system\",\n    .windows = .{\n        .{ .label = \"main\", .title = \"My App\", .width = 960, .height = 640, .restore_state = true },\n    },\n}\n```\n\nUse exact local origins for dev servers. Add `zero://inline` only for inline HTML sources.\n\n## Common implementation recipes\n\n### Add a new framework app\n\nUse `native init <path> --frontend <next|vite|react|svelte|vue>`. Then inspect the generated `app.json`, `src/main.zig`, and `build.zig` before customizing. For framework behavior, keep frontend work in `frontend/` and use `sourceFromEnv` so development and packaged builds share one app shell.\n\n### Add a native bridge command\n\n1. Add state and a handler in `src/main.zig`.\n2. Register the handler in `bridge()`.\n3. Allow the command in the app manifest and in the runtime bridge policy if the runner reads manifest policy into runtime.\n4. Call it from JavaScript with `window.zero.invoke(\"namespace.command\", payload)`.\n5. Return valid JSON from Zig. Use `native_sdk.bridge.writeJsonStringValue()` for user-controlled strings.\n\nBridge calls are size-limited, origin-checked, permission-checked, and routed only to registered handlers.\n\n### Add windows, child WebViews, or dialogs\n\nUse builtin bridge commands only after enabling a policy for the exact commands and origins. Window and child WebView commands need the `window` permission when permissions are configured. Dialog commands are always default-deny and require explicit `builtin_bridge` policy. See `references/bridge-security-native-capabilities.md`.\n\n### Choose a web engine\n\nDefault to `.web_engine = \"system\"` for small apps and native footprint. Use `.web_engine = \"chromium\"` plus `.cef` when the app needs a pinned Chromium platform or rendering consistency. Chromium apps must install/package the matching CEF layout.\n\n### Package an app\n\nZero-config apps package WITHOUT ejecting — `native package` works directly on the zero-config build (`native eject` is only for owning the build files, never a packaging prerequisite):\n\n```bash\nnative build\nnative package --target macos --archive\n```\n\nOn macOS, `--archive` adds a zero-config drag-to-Applications DMG with a generated 1×/2× Retina background. The optional `app.zon` `.dmg` block controls its volume name, PNG/JPEG/TIFF background, usable Finder canvas/icon geometry, and Applications link. An adjacent `name@2x.png`/`.jpg` is discovered automatically. Use `.dmg.items` only for a fully art-directed list: exactly one `app`, plus optional `applications`, project-relative `file`, and absolute `link` entries, each with its own icon-center position.\n\nApps that own their build (ejected or scaffolded `--full`) wire the same step into the build graph: keep package metadata in the app manifest, build the frontend assets, build the native binary, then package:\n\n```bash\nzig build package\nnative doctor --manifest app.json --strict\n```\n\nUse signing and CEF options only when the product requires them.\n\n## Development commands\n\nFor a default TypeScript + Native markup app, use:\n\n```bash\nnative check\nnative dev\nnative dev --core\nnative test\n```\n\nFor iterative WebView-frontend work in an app that owns its build, use the managed dev server flow:\n\n```bash\nzig build dev\n```\n\nOr run the CLI directly after building the binary:\n\n```bash\nnative dev --manifest app.json --binary zig-out/bin/MyApp\n```\n\nVite usually uses `http://127.0.0.1:5173/`; Next.js usually uses `http://127.0.0.1:3000/`. The app WebView loads the dev URL directly, so framework HMR remains owned by Vite, Next.js, or the selected dev server.\n\n## Security defaults\n\nTreat WebView content as untrusted:\n\n- List only needed `permissions` and `capabilities`.\n- Prefer exact bridge command origins over `\"*\"`.\n- Keep main-frame navigation allowlisted in `security.navigation.allowed_origins`.\n- Keep external links denied unless the product explicitly needs them.\n- Use a strict CSP for packaged frontend assets.\n- Built-in dialogs are always default-deny and require explicit `builtin_bridge` policy.\n- Child WebViews receive `window.zero` only when explicitly created with `bridge: true`.\n\nCommon bridge failure codes are `invalid_request`, `unknown_command`, `permission_denied`, `handler_failed`, `payload_too_large`, and `internal_error`.\n\n## Validate changes\n\nUseful default-app commands:\n\n```bash\nnative check\nnative test\nnative build\n```\n\nUseful SDK, Zig-core, and owned-build commands:\n\n```bash\nzig build run\nzig build dev\nzig build test\nzig build test-tooling\nnative validate app.json\nnative doctor --manifest app.json --strict\nzig build package\n```\n\nFor an app that owns Zig code, run BOTH `zig build` and `zig build test` before calling a change done: Zig's lazy analysis means code only tests reference (or only `main()` reference) can sit broken under the other command alone. This Zig-specific requirement does not turn a default TypeScript app into a Zig app.\n\nThe toolkit requires Zig 0.16.0. When a build fails with \"no member named\" errors on std APIs (`std.fs.cwd`, `ArrayList.init`, `std.io`, `GeneralPurposeAllocator`), the code was written against older Zig idioms — `native skills get zig` maps each such compile error to the 0.16 idiom as this SDK writes it.\n\nFor GUI smoke tests, build with automation enabled and use the `automation` skill:\n\n```bash\nzig build run -Dplatform=macos -Dautomation=true\nzig-out/bin/native automate snapshot\n```\n\nWhen changing app behavior, keep tests in the app's existing authoring tier. TypeScript cores can run in the node dev loop and through generated full-loop tests; Zig-core logic uses focused Zig tests. Use automation-based tests for live window/runtime integration.\n\n## Examples to inspect\n\n- `examples/chatbot`: a substantial app authored entirely in TypeScript + Native markup, with streaming fetch effects and modules.\n- `examples/soundboard`: TypeScript + Native markup music-player showcase.\n- `examples/system-monitor`: TypeScript + Native markup app using spawn effects, timers, and tables.\n- `examples/hello`: smallest lower-level inline HTML/WebView app.\n- `examples/webview`: bridge and WebView runtime example.\n- `examples/browser`: layered WebView/browser-style example.\n- `examples/next`: Next.js with production assets.\n- `examples/react`, `examples/svelte`, `examples/vue`: Vite frontend apps.\n- `examples/ios`, `examples/android`: mobile host embedding examples.\n\n## When answering users\n\nLead with the primary authoring story: applications are TypeScript cores plus Native markup by default, compiled to a small native binary with no JS runtime. Describe Zig as the toolkit implementation language and an explicit app-core/extension choice. Describe WebViews as a coexisting integration path for existing web content, not the starting point for a fresh app. If asked to implement, read the app files first, preserve the existing core language, and make the smallest change in the correct layer.\n","isInternal":false,"tokens":3932,"sizeBytes":16729},{"name":"SKILL.md","path":"skill-data/native-ui/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/skill-data/native-ui/SKILL.md","title":"native-ui","category":"anthropic-skill","format":"markdown","content":"---\nname: native-ui\ndescription: Authoring guide for the Native markup (.native) view tier of native-rendered Native SDK apps. TypeScript cores are the default and use this skill together with ts-core; Zig cores are an explicit alternative and this guide labels their lower-level UiApp recipes. Use when building or modifying native UI (widgets, layout, bindings, messages), writing .native files, wiring views to Model/Msg/update, testing markup views, or verifying a native app through the automation harness.\n---\n\n# Author native UI with Native markup\n\nA native-rendered Native SDK app is a markup view plus an app core. The primary authoring path is:\n\n- `src/app.native` — the entire UI: elements, layout, bindings, message dispatch.\n- `src/core.ts` — `Model`, the discriminated `Msg` union, `update(model, msg)`, effects, subscriptions, and derived binding helpers.\n- `app.zon` — identity, windows, assets, permissions, and policy.\n\nThe TypeScript core is checked and compiled ahead of time to native code; no JS runtime ships in the binary. Load `native skills get ts-core` beside this skill for every default app implementation. A Zig core (`src/main.zig`) is first-class when explicitly selected with `native init --template zig-core`; the Zig-only wiring, builder, and runtime-extension recipes below are labeled so they are not mistaken for the default.\n\nThe markup compiles to the same widget tree a hand-written `canvas.Ui(Msg)` builder view would produce: identical structural widget ids, identical typed handler table. Markup can never mutate state — it binds values and dispatches messages; all logic lives in the app core, whichever core language the existing tree uses.\n\nEditors highlight `.native` markup well in HTML mode — the default scaffold writes no editor config, so add `.vscode/settings.json` with `\"files.associations\": {\"*.native\": \"html\"}` yourself, or scaffold with `native init --full`, which writes it.\n\nStart a new app with `native init` (zero-config: `app.zon` + `src/core.ts` + `src/app.native` + assets; the CLI generates the build graph). Use `examples/chatbot`, `examples/soundboard`, and `examples/system-monitor` as substantial TypeScript references. The `native check|dev|test|build` verbs drive any app directory shaped this way.\n\n## Menu-bar status items (TypeScript default)\n\nExport `statusItem(model: Model): StatusItemState` from `src/core.ts`. The generated launcher installs icon/tooltip/click-open commands plus the boot presentation/menu, re-derives it after every committed update, and patches presentation/menu independently. Import `StatusItemState` from `@native-sdk/core/events`. Commands route through the core's `commandMsg(name): Msg | null` export. There is no app-owned Zig wiring and `statusItem` is automatically marked shell-bound for the markup lint.\n\n```ts\nimport { asciiBytes, utf8Bytes } from \"@native-sdk/core\";\nimport { type StatusItemState } from \"@native-sdk/core/events\";\n\nexport function statusItem(model: Model): StatusItemState {\n  return {\n    iconPath: asciiBytes(\"assets/menu-bar.svg\"),\n    tooltip: utf8Bytes(\"Player status\"),\n    activationCommand: asciiBytes(\"app.refresh\"),\n    alternateActivationCommand: asciiBytes(\"player.toggle\"),\n    openCommand: asciiBytes(\"app.refresh\"),\n    presentation: { title: model.playing ? utf8Bytes(\"MB PLAY\") : utf8Bytes(\"MB\"), width: 62, tone: \"normal\", iconOpacity: 1, monospaced: true, fontSize: 13, fontWeight: \"semibold\" },\n    items: [\n      { id: 1, label: utf8Bytes(\"Open\"), command: asciiBytes(\"app.open\"), separator: false, enabled: true, detail: asciiBytes(\"\"), role: \"command\", key: asciiBytes(\"\"), modifiers: { primary: false, command: false, control: false, option: false, shift: false } },\n      { id: 0, label: asciiBytes(\"\"), command: asciiBytes(\"\"), separator: true, enabled: false, detail: asciiBytes(\"\"), role: \"command\", key: asciiBytes(\"\"), modifiers: { primary: false, command: false, control: false, option: false, shift: false } },\n      { id: 2, label: model.playing ? utf8Bytes(\"Pause\") : utf8Bytes(\"Play\"), command: asciiBytes(\"player.toggle\"), separator: false, enabled: true, detail: utf8Bytes(\"configured ✓\"), role: \"agent\", key: asciiBytes(\"\"), modifiers: { primary: false, command: false, control: false, option: false, shift: false } },\n    ],\n  };\n}\n```\n\nPresentation fields style the persistent menu-bar item. Inside its dropdown, `metric` carries prominent primary/secondary text, `segmented` carries up to eight typed command options, and `chart` carries 1–32 bounded values with captions and accessibility. These are generic composable row payloads, not text conventions.\n\n## App wiring (Zig cores and extensions only)\n\nA default TypeScript app writes none of this: generated wiring connects `src/core.ts`, `src/app.native`, and `app.zon`. Use the following only when the existing app has a Zig core or the task explicitly requires owned runtime wiring.\n\n```zig\nconst HabitsApp = native_sdk.UiApp(Model, Msg);\n\npub fn main(init: std.process.Init) !void {\n    // `create` heap-allocates the multi-MB app struct and constructs the\n    // Model in place — neither ever rides the stack (avoid `App.init(alloc,\n    // model, ...)`: its by-value Model is a stack-overflow trap once the\n    // Model grows).\n    const app_state = try HabitsApp.create(std.heap.page_allocator, .{\n        .name = \"habits\",\n        .scene = shell_scene,             // one window, one gpu_surface view\n        .canvas_label = \"habits-canvas\",  // must match the ShellView label\n        .update = update,\n        .markup = .{\n            .source = @embedFile(\"app.native\"),\n            .watch_path = \"src/app.native\", // dev hot reload; omit in release\n            .io = init.io,\n        },\n    });\n    defer app_state.destroy();\n    app_state.model = initialModel(); // boot state: assign through the pointer\n    try runner.runWithOptions(app_state.app(), .{ ... }, init);\n}\n```\n\n(`create` requires every Model field to carry a default; the model starts as `.{}` and boot state is assigned through the returned pointer. Tests that instantiate the app per fixture should use `create`/`destroy` too — a runtime-built Model passed to `init` by value crashes the test stack once models get large.)\n\nThe runtime owns the loop: install on first GPU frame, presentation, resize, pointer/keyboard dispatch into `update` + rebuild. With `watch_path` set, editing the `.native` file while the app runs hot-reloads the view within ~2s, preserving model state and widget ids; parse failures keep the last good view and set `app_state.markup_diagnostic` (line/column/message).\n\n**Release: compile the markup at comptime.** `canvas.CompiledMarkupView(Model, Msg, source).build` parses the `.native` source entirely at compile time and produces the identical tree (same ids, handlers, dispatch) with no parser in the binary; markup or binding mistakes become compile errors with line/column. Hand it to `.view`, and gate the runtime engine per build mode:\n\n```zig\nconst dev = @import(\"builtin\").mode == .Debug;\nconst App = native_sdk.UiAppWithFeatures(Model, Msg, .{ .runtime_markup = dev });\nconst CompiledView = canvas.CompiledMarkupView(Model, Msg, @embedFile(\"app.native\"));\n// options:\n.view = CompiledView.build,\n.markup = if (dev) .{ .source = ..., .watch_path = \"src/app.native\", .io = init.io } else null,\n```\n\nWith both set (dev), the compiled view renders until the watched file first changes, then the interpreter hot-reloads it. See the native reference fixture in `tests/ts-core/habits_reference.zig` for this Zig wiring pattern.\n\n### Webview panes: canvas + live web content in one window\n\nFor TypeScript apps, export `webPanes(model: Model): readonly WebViewPane[]` from `core.ts`. Import the descriptor from `@native-sdk/core/events`; return `label`, nullable `anchor`, `url`, `x`, `y`, `width`, `height`, and `reloadToken`. The label identifies an existing manifest WebView; the anchor identifies a widget label such as `<panel label=\"preview-pane\" grow=\"1\"/>`. See `examples/canvas-preview` for pane, frame, and tray coordination in one compiled core. The native adapter copies bytes and validates finite geometry and safe integer tokens; the OS owns WebView hosting and navigation policy.\n\nFor all u64 reload states, return `readonly ExactWebViewPane[]` instead. Its `reloadToken` is decimal ASCII bytes; the host validates the full u64 range without rounding. `WebViewPane` retains its existing safe-number token.\n\n### TypeScript terminal panes\n\nBind `<terminal pty=\"{shellKey}\" scrollback=\"{scrollback}\" on-terminal=\"terminalChanged\" label=\"Shell\" autofocus=\"true\" grow=\"1\"/>` to a `Uint8Array` helper or field naming the same key as `Cmd.ptySpawn`. Import `TerminalState` from `@native-sdk/core/events` for the message payload. The native host resolves the name to its PTY capability; engine identities never cross through a JavaScript number. Both view backends preserve the complete four-field state report. A bound slot remains reserved through exit until app teardown, keeping the ended screen stable across rebuilds. Restarting the same name reuses its slot; the existing four-slot budget applies. Keep `.terminal_sessions = true` and the lazy Ghostty pin in the app's owned build, as in `examples/workbench`. Native integer PTY bindings remain supported.\n\nFor Zig cores and extensions:\n\nDeclare the webview in the scene next to the gpu_surface (parent it to the canvas view), reserve its region with an empty panel carrying a semantics label, and let `Options.web_panes` snap the webview to that widget's layout frame while the model drives navigation:\n\n```zig\nconst shell_views = [_]native_sdk.ShellView{\n    .{ .label = \"app-canvas\", .kind = .gpu_surface, .fill = true, .gpu_backend = .metal },\n    .{ .label = \"preview\", .kind = .webview, .parent = \"app-canvas\", .url = \"https://example.com/\", .x = 240, .y = 76, .width = 704, .height = 548 },\n};\n// view: ui.panel(.{ .grow = 1, .semantics = .{ .label = \"preview-pane\" } }, .{})\nfn panes(model: *const Model, out: []App.WebViewPane) usize {\n    out[0] = .{ .label = \"preview\", .anchor = \"preview-pane\", .url = model.url(), .reload_token = model.reload_token };\n    return 1;\n}\n// options: .web_panes = panes,\n```\n\nURL changes navigate; bumping `reload_token` reloads the same URL (the CenterPane/Preview-tab shape). Pane URLs must pass `security.navigation.allowed_origins`. Panes reconcile against the runtime's live webview state on every rebuild and presented frame, so shell relayouts cannot detach them. `examples/canvas-preview` is the live reference; `zig build test-canvas-preview-smoke` verifies it.\n\n### Menu-bar extra (status item)\n\nThis is the Zig-core/custom-wiring form. Default TypeScript apps use the exported `statusItem(model)` helper above.\n\n`Options.status_item` installs a macOS `NSStatusItem` once, on the installing frame; its menu items dispatch commands through the same `on_command` mapping the toolbar and menus use (source `.tray`):\n\n```zig\n.status_item = .{ .title = \"ZN\", .tooltip = \"My App\", .items = &.{\n    .{ .id = 1, .label = \"Refresh\", .command = \"app.refresh\" },\n    .{ .separator = true },\n    .{ .id = 2, .label = \"Quit\", .command = \"app.quit\" },\n} },\n```\n\nFor a LIVE menu-bar extra (an open-count badge in the title, a latest-items dropdown), add `Options.status_item_fn` — the `web_panes` pattern: consulted on install and after every rebuild, re-applied only when its output changed (title and menu patch independently; the static `status_item` keeps icon/tooltip). Format derived strings into the provided scratch; item `command`s dispatch through `on_command` exactly like static items:\n\n```zig\nfn statusItem(model: *const Model, scratch: *App.StatusItemScratch) App.StatusItemState {\n    const title = std.fmt.bufPrint(&scratch.title_buffer, \"ZN {d}\", .{model.open_count}) catch \"ZN\";\n    scratch.items[0] = .{ .id = 1, .label = \"Refresh\", .command = \"app.refresh\" };\n    var count: usize = 1;\n    for (model.latest(), 0..) |issue, i| { // per-row commands: map \"issue.select.N\" in on_command\n        scratch.items[count] = .{ .id = @intCast(10 + i), .label = issue.title, .command = issue.select_command };\n        count += 1;\n    }\n    return .{ .title = title, .items = scratch.items[0..count] };\n}\n// options: .status_item_fn = statusItem,\n```\n\nTitle updates retitle the live `NSStatusItem` button without re-creating it; platforms without a tray-title seam keep menu updates and log the title gap once.\n\n### Windowed lists in markup\n\nUse `<virtual-window>` to resolve one retained range before rendering, then `<virtual-list>` to request only its rendered rows. The window declares a stable literal `id`, an `as` range name, an `item-count`, and either `item-extent` or an exported Model-first `extent-estimate` helper. Optional `gap`, `overscan`, `viewport-fallback`, `index-base`, and `anchor=\"leading|trailing\"` describe the complete content geometry.\n\n```html\n<virtual-window id=\"records\" as=\"range\" item-count=\"{loaded}\" item-extent=\"48\" overscan=\"4\">\n  <column>\n    <virtual-list window=\"range\" each=\"rows\" as=\"row\" grow=\"1\" label=\"Records\">\n      <list-item key=\"{row.id}\" focusable=\"true\" on-press=\"select:{row.id}\">{row.title}</list-item>\n    </virtual-list>\n    <status-bar>{range.first_visible_index}–{range.last_visible_index}</status-bar>\n  </column>\n</virtual-window>\n```\n\nIn a TypeScript core, `rows(model: Model, range: VirtualListRange): readonly Row[]` takes the complete SDK range from `@native-sdk/core/events` and returns exactly `range.end_index - range.start_index` rows in rendered-index order. An extent estimator has shape `rowExtent(model: Model, index: number): number`; its retained identity survives rebuilds. Each row has one keyed root. Ordinary scroll styling, `on-scroll`, and reach-start/end messages belong on `virtual-list`.\n\nEach window has one root and exactly one consuming list; windows cannot nest. A range can pass through a template argument, and secondary window views have their own retained descriptors. The full range exposes visible and rendered bounds, content extent, row extent, and offset; use it for footer text without copying scroll state into the model. See `examples/feed` for variable-height estimates, stable row queries, and retained selection.\n\n### Native scrolling (macOS)\n\nZero app code: on macOS every non-virtualized `scroll` region — and every windowed virtual list (`ui.virtualList`), whose driver content size is the full virtual extent — is driven by an invisible `NSScrollView` — OS momentum and the system overlay scrollbar — while the engine renders the content. `widget.value` stays the offset of record, so the rebuild reconcile rule (\"user offset survives rebuilds until the source offset changes\"), automation snapshot offsets (`scroll=[offset=..]`), and `Options.sync` all work exactly as before; the engine-drawn scrollbar simply stops painting for natively driven regions. Programmatic scrolls still work: change the source offset (or scroll via keyboard/automation) and the runtime pushes it into the native scroller. GTK/Win32 and mobile embeds keep the engine's wheel physics unchanged. Nested-scroll saturation handoff (inner region exhausted, outer continues) is per-region native today: the inner region stops at its edge like a standalone scroller.\n\n**Overscroll is off by default, per region, on both paths.** Scroll regions pin at their content edges — the native scroller gets non-elastic edges, the engine's wheel/kinetic physics clamp, and kinetic motion stops cleanly at the boundary. Bouncing is a per-region opt-in: `overscroll=\"rubber_band\"` in markup (the `scroll` element only — the validator rejects it elsewhere with a teaching error) or `ElementOptions.overscroll = .rubber_band` in Zig views. The `ScrollPhysics.overscroll` design token (`ScrollPhysicsOverrides` in a theme) flips the app-wide default; per-region values override it, and `.none` pins a region regardless of the token. The rubber-band shape — excursion bound, resistance, spring-back rate — stays themable through the `rubberband_*` physics tokens.\n\n### Context menus: one declared menu, platform-decided presentation\n\nAuthors write ONE menu; the platform decides how it presents. The default is the real OS context menu at the pointer — `NSMenu` on macOS, `TrackPopupMenu` on Windows, `GtkPopoverMenu` on Linux — and the selection dispatches the item's typed `Msg`. On hosts without a native menu presenter (the mobile toolkit hosts and embed hosts today), the SAME declared items present automatically as an anchored canvas surface at the click point, with the standard anchored-surface behavior (Escape and outside-click dismiss, late z-pass, window clipping). Never two authored menus, never a canvas imitation where the OS menu exists.\n\nMarkup declares the menu as a `<context-menu>` element — a DIRECT child of the interactive element whose right-click it answers (a hit-target kind, or an element made interactive by `on-press`/`on-double-press`/`on-toggle`/`on-hold`/`on-drag`). It is metadata, not content: it renders nothing in the row's flow. Children are `menu-item`s (`on-press` required, `disabled` optional, the text content is the label) and bare `<separator/>`s, with `if`/`else`/`for` around them to swap or repeat items — a menu whose items all evaporate at runtime simply declares no menu (the All Notes row pattern). Conditional MENUS are spelled as conditional ITEMS: the `<context-menu>` itself takes no attributes and cannot sit behind a structure tag. No submenus: the platform channel carries flat items (label, enabled, separator) only.\n\n```html\n<list-item on-press=\"open_note:{n.id}\" label=\"{n.title}\">\n  <text grow=\"1\">{n.title}</text>\n  <context-menu>\n    <if test=\"{n.deleted}\">\n      <menu-item on-press=\"restore_note:{n.id}\">Restore</menu-item>\n      <menu-item on-press=\"purge_note:{n.id}\">Delete Permanently</menu-item>\n    </if>\n    <else>\n      <menu-item on-press=\"copy_note_id:{n.id}\">Copy</menu-item>\n      <menu-item on-press=\"trash_note:{n.id}\">Delete</menu-item>\n    </else>\n  </context-menu>\n</list-item>\n```\n\nThe compiled TypeScript view supports one direct menu with at most 32 labeled `menu-item` leaves and bare separators. Items require `on-press` and may bind `disabled`; structure tags inside menus currently remain on the native markup backend. The menu is copied into the native tree arena before the compiled result expires.\n\nThe Zig builder's mirror is `ElementOptions.context_menu` — per-widget items in the chrome-menu shape with typed messages:\n\n```zig\nui.listItem(.{\n    .on_press = Msg{ .select = entry.index },\n    .context_menu = &.{\n        .{ .label = \"Open Section\", .msg = Msg{ .select = entry.index } },\n        .{ .separator = true },\n        .{ .label = \"Refresh Dashboard\", .msg = .refresh },\n    },\n}, entry.title)\n```\n\nThe deepest declaring widget on the hit route wins; disabled items and separators are fine (`enabled = false`, `.separator = true`). Zero-code defaults need no declaration: editable text fields present the standard Cut / Copy / Paste / Select All menu wired to the existing clipboard actions, and a selected static text presents Copy (these defaults are presenter-only — without an OS menu they degrade to the keyboard clipboard paths). Touch long-press is design-noted for the mobile embeds: the iOS host's under-slop `Pending` touch state is the timer seam, pending a secondary-button leg in the embed ABI and `UIEditMenuInteraction` presentation.\n\nAutomation drives the native path honestly: snapshots list every widget's declared items in invocation order (`context_menu=[\"Rename\",\"Delete\"]`, separators keep their slots, disabled items say so), `widget-context-press <view> <id>` performs the real secondary click (presenting the menu), and `widget-context-menu <view> <id> <item-index>` invokes an item — the selection dispatches as the same `context_menu_action` platform event a real pick produces; the journal records the complete widget invocation so replay reconstructs its pending request before selection, because the OS menu's tracking loop cannot be driven programmatically. Dead invocations fail by name (undeclared menu, index out of range, separator slot, disabled item). `examples/notes` row menus and `examples/gpu-dashboard` nav rows carry live menus; `zig build test-example-notes`, `test-example-gpu-dashboard`, and the runtime context-menu suite verify dispatch, the fallback surface, and the verb.\n\n## Elements\n\n| Markup | Widget | Notes |\n| --- | --- | --- |\n| `row`, `column` | flex containers | main axis horizontal / vertical |\n| `stack`, `panel`, `card` | overlay containers | children stack on top of each other — `gap` can never space them and is a validation error (put a `column`/`row` inside for flow) |\n| `scroll` | scroll_view | wrap multiple children in a `column` inside it |\n| `list`, `grid` | list, grid | vertical stack / cell grid |\n| `tabs`, `toggle-group`, `button-group`, `radio-group`, `breadcrumb`, `pagination` | row containers | children flow horizontally. Give every `radio-group` an accessible `label`; it is one logical `radiogroup`: descendant radios at any nesting depth share one Tab stop; arrows wrap while Home/End move to the edges, focus and selection move together, and selecting one clears the rest of the nearest group scope |\n| `table` > `table-row` > `table-cell` | table, data_row, data_cell | rows only inside a table, cells only inside a row (for/if wrappers are fine); cells are text leaves, dispatch with `on-press` |\n| `dropdown-menu` | dropdown_menu | vertical menu surface; children are `menu-item`s. `anchor=\"below\\|above\"` floats it against its PARENT's frame (see Pickers): late z-pass above the whole tree, window-clipped, auto-flipping at the window edges, zero flow space. Pair with `on-dismiss` |\n| `accordion` | accordion | header via `text` attr; children show while `selected`, dispatch `on-toggle` |\n| `alert`, `bubble` | surfaces | `alert` title via `text` attr; children stack inside. `bubble` hugs its message up to 80% of the thread (`ghost` exempt; explicit `width` wins) and takes one `<reactions>` child — the reaction pill straddling its bottom edge, one text run, dock via `text-alignment` (default `end`); `text=` on bubble itself is a teaching error (that channel belongs to the pill). Grouped runs are spacing, not vocabulary: 8 gap within a sender's run, 32 between turns |\n| `dialog`, `drawer`, `sheet` | modal surfaces | root-relative and unaffected by ancestor scroll or clipping: dialogs center in the viewport, drawers span its width and dock to the bottom, and sheets span its height and dock to the right — title via `text` attr, wrap in `<if>` to show conditionally |\n| `resizable` | resizable | engine-managed drag handle; `width` sets the initial width |\n| `split` | split | two-pane horizontal splitter: exactly two element children (nest splits for more panes), the engine synthesizes the draggable divider between them. `value` binds the model-owned first-pane fraction (0 lays out at 0.5), `on-resize` names an f32 Msg variant dispatched with every applied fraction (echo it back through `value` — see Splitters), `min-width` on the panes bounds the drag, `gap` sets the divider band thickness. The divider is focusable: Left/Right (Shift for bigger steps) adjust, Home/End jump to the clamp edges |\n| `tree` | tree | disclosure-tree container (vertical flow): descendant rows carrying `role=\"treeitem\"` — at ANY nesting depth — form one roving keyboard focus set with the ARIA tree keymap. Up/Down walk visible rows (selection follows focus through each row's `on-press`), Left collapses an expanded row or moves to the parent row, Right expands a collapsed row or moves to the first child row, Home/End jump to the edges, Enter/Space activate. Expandable rows bind `expanded` and `on-toggle`; the model owns selection and expansion (collapsed children are simply not rendered) |\n| `text`, `badge`, `tooltip` | text leaves | text content, `{}` interpolation allowed; `text` line policy via `wrap` (`\"true\"` word-wraps; `\"false\"`/unset paint one honest line, overflow eliding by default — `overflow=\"clip\"` opts out), and `text` alone takes the typography rungs `size=\"heading\"`/`size=\"display\"` (themable token steps above title — section headings, hero stats, timer numerals). `tooltip` with `anchor=\"above\\|below\"` floats against its parent (the stack wrapping trigger + tooltip, the dropdown pattern) and the RUNTIME owns its visibility — hover intent on the trigger: shows after `tooltip-delay` ms (default 600; `\"0\"` = instant) and immediately on keyboard focus; hides on leave, focus departure, Escape, or a press of the trigger (a press also closes the warm window), and a shared 400ms warm window after a pointer-hovered tooltip hides on leave (the only hide that warms) shows the next trigger's tooltip instantly; the model never hears hover. These are shadcn/ui's defaults (Base UI). Without `anchor` it stays a static leaf that paints whenever the view renders it |\n| `text` > `span` | inline styled runs | mixed-style text in ONE wrapped paragraph: span children style runs with `weight=\"regular\\|medium\\|bold\"`, `mono`, `italic`, `scale` (a positive multiplier on the paragraph's base size — inline headings, hero stats), `underline`, `foreground` (token name); `{bindings}` interpolate inside spans; whitespace between runs collapses to a single space (none = the runs abut); spans do not nest, take no events, and the paragraph announces as one text run — see \"Rich text\" |\n| `button`, `toggle-button`, `list-item`, `menu-item`, `toggle`, `switch`, `select`, `avatar` | text-bearing controls | label is the text content; `button`, `toggle-button`, `list-item`, and `menu-item` also take `icon=\"save\"` — a vector icon drawn inline (buttons/toggle-buttons before the label, icon-only when the content is empty: add a `label`; list/menu items as a leading slot), ONE hit target whose icon follows the element's enabled/disabled tint (no overlay stacking, no duplicated `on-press`); tab strips are `toggle-button` children, so tabs get icons this way; `select` shows `placeholder` while empty and dispatches `on-press`; `avatar` renders initials, or a runtime image via `image=\"{binding}\"` (see the Images section) |\n| `segmented-control` | exclusive-choice control | standalone text-bearing segment; bind `selected`, dispatch `on-press`, and optionally add a vector `icon`; compose several in a row, or use `tabs` for the strip-and-panel pattern |\n| `checkbox`, `radio`, `slider`, `progress` | value controls | `checked`, `value` (a 0..1 fraction on slider and progress; progress clamps out-of-range values at render, never an error); checkbox/radio visible labels are text content (`<checkbox>Done</checkbox>`), with `text=\"...\"` as the equivalent binding-friendly form and `label=` alone naming one for accessibility without drawing a label; a radio selection TRANSITION (pointer, Space/Enter, grouped navigation, or accessibility selection) dispatches `on-change` when bound, then `on-toggle`, then `on-press` for compatibility — reactivating the already-checked radio has no new `on-change` edge, though a legacy fallback still receives the activation; a slider's `value` follows the source when it MOVES (model-driven progress renders every rebuild) and keeps the user's drag while the source replays the same value — use `slider` for seek bars, `progress` for display-only; a markup slider's `on-change` dispatches a PLAIN Msg with no value payload — mirror the applied value into the model with `Options.sync` (the Zig builder's `on_value = Ui.valueMsg(.tag)` does deliver the applied f32) |\n| `text-field`, `input`, `search-field`, `combobox`, `textarea` | text entry | `placeholder`; edits via `on-input`, enter via `on-submit` on single-line kinds; in a default `textarea`, Enter (and Shift+Enter) inserts a newline and `on-submit` dispatches on primary+Enter (cmd on macOS, ctrl elsewhere). A chat composer opts into `submit-on-enter=\"true\"`: plain Enter submits, Shift+Enter remains a newline, and the primary chord still submits. `search-field` carries a built-in trailing clear affordance whenever it holds text (press the x, or Escape while focused — both clear through the text-edit path, so `on-input` hears it; no attribute, no external Clear button needed) |\n| `status-bar` | status bar | text leaf: content only, no children |\n| `separator`, `spacer` | separator, flexible space | `separator` is axis-aware: a horizontal rule in a `column`, a thin vertical divider in a `row`; give `spacer` a `grow` |\n| `skeleton`, `spinner` | loading leaves | size `skeleton` with `width`/`height` |\n| `icon` | vector icon leaf | `name` picks the icon: a bare literal is a curated built-in stroke icon (compile-checked; 49 names: search, plus, x, x-circle, check, check-circle, chevron-up/down/left/right, arrow-up/down/right, menu, panel-left, panel-right, settings, terminal, wrench, trash, edit, copy, external-link, play, pause, skip-back/forward, shuffle, repeat, music, volume, info, alert, download, save, folder, folder-open, file-text, sun, moon, eye, clock, git-pull-request, git-merge, git-branch, circle-dot, archive, refresh-cw, send); `app:<name>` reaches an icon the app registered at boot with `canvas.icons.registerAppIcons` (declare the table as `pub const app_icons` on the app root so `native check` verifies the name against the model contract), and one `{binding}` defers the choice to model data - an unknown resolved name draws the missing-icon fallback (a slashed circle) with a Debug warning naming the value; tint with `foreground`, size with `width`/`height` |\n| `media-surface` | media surface leaf | composites a texture produced OUTSIDE the widget tree (video decoder, camera, an external renderer like mpv) into the layout like any widget — clipped, z-ordered, rounded. `surface=\"{binding}\"` (required) binds the model-owned u64 surface id a Zig-tier producer targets (`runtime.acquireMediaSurfaceProducer` pushes RGBA8 frames, latest-wins, paced by the presented-frame clock; 0 = unbound, draws nothing; usable ids are nonzero values below the reserved bit 63). No intrinsic size — give it `width`/`height` or `grow`; display-only (presses fall through); `label` it (pictorial content). Texture contents are presentation chrome: goldens, reference screenshots, and session replay show the deterministic id-derived placeholder, never producer frames |\n| `image` | runtime image leaf | draws a RUNTIME-REGISTERED image by its model-owned u64 ImageId — the id `Cmd.imageLoad` (TS) or `fx.loadImage`/`fx.registerImageBytes` (Zig) registered pixels under. `image=\"{binding}\"` (required) binds a model field/fn; ids are model data, never markup literals, and 0 draws nothing (store the id only when the load reports loaded — see the Images section). `source-x`/`source-y`/`source-width`/`source-height` select one atlas region in decoded-image pixel coordinates (declare all four). No intrinsic size — give it `width`/`height` or `grow`; display-only (presses fall through); `label` it (pictorial content) |\n| `code` | bare highlighted source/editor | `source=\"{binding}\"` (required) provides source text and `language=\"tsx\"` selects a literal lexer name; the component supplies no background, border, radius, shadow, or padding, so wrap it in a panel/card when chrome is wanted. It is read-only by default; `editable on-input=\"edit\"` opts into multiline editing while retaining highlighting. It wraps by default, `line-numbers` opts into logical line numbers, `added-lines=\"5\"` / `removed-lines=\"2-4\"` add Geist-style diff rows without changing copied source, and `wrap=\"false\"` preserves lines inside one horizontal scroll region. HTML-family highlighting distinguishes HTML/XML/SVG and JSX/TSX tags, attributes, strings, comments, and embedded expressions. Zig builder: `ui.code(CodeOptions, source)` |\n| `markdown` | rendered markdown subtree | leaf; `source` is one `{binding}` — see \"Markdown in markup\" |\n| `stepper` > `step` | composite stage track | `active=\"{index}\"` (required) derives each step's completed/active/pending state; steps are text leaves (no attributes) joined by connectors; stepper also takes `key`, `global-key`, `label` |\n| `timeline` > `timeline-item` | composite ledger list | items only inside a timeline (for/if fine); items are leaves — `title` (required), `description`, `meta`, `indicator`, `variant`, `connector=\"false\"` on the last item, `selected`; `on-press` makes the whole item pressable with a trailing chevron |\n| `chart` > `series` | composite data chart | series only inside a chart, and only series (the set is static — data varies through bindings); each series is a leaf — `values=\"{binding}\"` (required) names a model `[]const f32` iterable, `kind` is `line`/`area`/`bar` (literal), `color` a token name, `label` the semantics name; chart takes `y-min`, `y-max`, `grid-lines`, `baseline`, `x-labels`, `y-labels`, `hover-details`, `stroke-width`, box options, `label` — see \"Charts\" |\n| `context-menu` | consumed by its parent | right-click menu on its DIRECT parent (a hit-target kind or an element with `on-press`/`on-double-press`/`on-toggle`/`on-hold`/`on-drag`); metadata, never a flow child. Children: `menu-item`s (`on-press` required, `disabled` optional, no `icon`) and bare `separator`s, with `if`/`else`/`for` around them. Attribute-less; presents natively where the host has a menu presenter, as an anchored surface elsewhere — see \"Context menus\" |\n| `input-group` > `textarea` + `input-group-actions` | composite grouped input | the composer shape: ONE bordered field wrapping exactly one `textarea` (first — document order is focus order) plus an optional `input-group-actions` row of controls inside the same border. The group wears the focus ring for its focused descendant and the textarea's own chrome dissolves automatically, so the whole group reads as one field; the textarea keeps its full behavior (`text`, `placeholder`, `on-input`, `on-submit`, `autofocus`, and optional `submit-on-enter`). Group takes `label`, `width`, `height`, `min-width`, `grow`, `key`, `global-key`; the actions row takes `gap` and holds ordinary elements (`if`/`else`/`for` work — swap send for stop while streaming) — put a `<spacer grow=\"1\"/>` between leading and trailing controls (`Ui.inputGroup`/`Ui.inputGroupActions` are the Zig-view equivalents) |\n\nCapabilities still awaiting a separate admission or ruling are image-backed `icon_button` content, `data_grid` per-column cell templates, and `popover`/`menu_surface` anchoring. `segmented-control` is a first-class markup leaf, and vector icon buttons use `<button size=\"icon\" icon=\"...\" label=\"...\" on-press=\"...\" />`. Charts ARE expressible: `<chart>` with `<series values=\"{binding}\">` children binding model f32 iterables — see the Charts section (`.band` series and dynamic series composition stay with `ui.chart`). Built-in vector icons ARE expressible: `<icon name=\"search\"/>` (closed, compile-checked name set; `Ui.icon` is the Zig-view equivalent). App-authored icons: `canvas.svg_icon.parseComptime(@embedFile(\"icons/logo.svg\"))` parses any SVG in the common 24x24 stroke-icon dialect at comptime; register the parsed table once at boot with `canvas.icons.registerAppIcons(&table)` and draw by name via `ui.appIcon(.{...}, \"logo\")` or `ElementOptions.icon` — registered names render exactly like built-ins on every draw path. Markup `<icon>`/`<button icon>` stay built-in-only (the compiled engine validates names at comptime, where runtime registrations cannot exist — engine parity). Runtime images ARE expressible: `<image image=\"{cover}\" width=\"120\" height=\"80\" label=\"Cover art\"/>` and `<avatar image=\"{user_image}\">CT</avatar>` bind a `u64` ImageId model field/fn (the id is just model data; 0 draws nothing / keeps the initials fallback) — see the Images section; the `image` binding is required on the leaf (an unbound `<image>` is dead markup) and stays avatar+image scoped. Add all four `source-x`/`source-y`/`source-width`/`source-height` attributes to draw one decoded-pixel sub-rectangle from a shared atlas.\n\n## Attributes\n\nLayout: `gap` (flow containers only — stacking containers `stack`/`panel`/`card`/`alert`/`bubble`/`dialog`/`drawer`/`sheet`/`resizable` layer their children, so `gap` there is a validation error, not silence: wrap the children in a `column`/`row` inside; on `split` it sets the divider band thickness), `padding` (uniform), `grow`, `width`, `height` (definite: the element is exactly that size — intrinsic content neither shrinks nor silently overflows it; `resizable` treats `width` as the initial width), `min-width` (a floor WITHOUT `width`'s definite max — the element may grow past it but never shrink below; on split panes it bounds the divider drag), `max-width` (a ceiling WITHOUT `width`'s definite min — the element still shrinks with a narrow parent; use a growing child inside a centered row for a responsive content column), `wrap` (`text` only: `wrap=\"true\"` word-wraps at the width the element receives and reserves the wrapped height in columns; `wrap=\"false\"` and unset are honest single-line — one line whose overflow follows `overflow`), `overflow` (`text` only, a teaching error elsewhere: what a single line does with content that does not fit — `ellipsis`, the default, elides behind a trailing … measured with the same metrics paint uses, right for width-constrained list-row titles; `clip` hard-cuts at the frame for fixed-format content like a duration column where \"1…\" beats nothing; there is deliberately no overflow-visible), `text-alignment` (start|center|end — text leaves, status bars, surface titles; controls that own their label placement ignore it), `columns` (`grid` only: fixed column count, omit for the derived near-square grid; a teaching error elsewhere), `main` (start|center|end|space_between), `cross` (stretch|start|center|end), `virtualized`, `virtual-item-extent`, `anchor` (`dropdown-menu` and `tooltip`, literal `below`/`above`: floats the surface against its parent instead of the flow — auto-flips when the preferred side does not fit, height clamps to the chosen side, x clamps into the window; an anchored tooltip's visibility is runtime-owned hover intent, unlike the model-owned dropdown), `anchor-alignment` (with `anchor`: `start`/`end`/`stretch` — stretch also widens the surface to at least the anchor's width, the select-menu look), `anchor-offset` (with `anchor`: literal gap in points, default 4), `tooltip-delay` (`tooltip` only, beside `anchor` — a teaching error elsewhere or without it: hover-intent show delay in ms, default 600, `\"0\"` = instant; keyboard-focus reveals are always immediate), `overscroll` (`scroll` only, a teaching error elsewhere: `none` pins the region at its content edges — the shipped default via the `ScrollPhysics.overscroll` token — `rubber_band` lets it bounce past them on both the engine and native paths, `default` follows the token).\nAppearance/state: `variant` (default|primary|secondary|outline|ghost|destructive), `size` (the control scale default|sm|lg|icon on every sized element; on `text` also the typography rungs heading|display — named typography token steps (`heading_size` 28, `display_size` 48, themable like every token) for section headings and hero stats/timer numerals. The two axes stay apart: heading/display on a control is a teaching error naming text as their home, unknown values list the vocabulary, and numeric sizes are refused by design — retheme the typography tokens to move the whole scale), `disabled`, `checked`, `selected`, `value`, `placeholder`, `icon` (`button`, `toggle-button`, `list-item`, `menu-item`: vector icon drawn inline — buttons/toggle-buttons before the label, list/menu items as a leading slot; a teaching error anywhere else. A built-in name, `app:<name>`, or one `{binding}` resolving to such a name). **One size register per row**: every control class shares the control height at a given register (default 36, sm 31.5, lg 40.5 before density), so a toolbar/filter row reads as one height exactly when every control in it carries the SAME `size` — mixing `size=\"sm\"` buttons with a default field renders two heights in one row, and hand-sized pressable panels (`height=\"30\"`) never land on the scale; compose rows from real controls at one register.\nFocus: `autofocus` (focusable controls only — a teaching error elsewhere): moves keyboard focus to the element when it MOUNTS or when the bound value turns on, edge-triggered so holding it true never re-steals focus from the user. The TEA way to focus an editor on note-create (`<text-field autofocus=\"{editing}\" ...>` or mount the field under an `<if>` with `autofocus=\"true\"`; Zig views use `ElementOptions.autofocus`) and to give keyboard-first apps their first focus without a click.\nSemantics: `role` (listitem, treeitem, button, ...; `treeitem` also makes the row part of its tree's roving keyboard focus set), `label` (accessible name — it REPLACES the element's text content as the announced name, so snapshot greps and screen readers see the label, never the text; don't `label` an element whose visible text you grep for), `expanded` (tree rows: disclosure state, model-owned — omit on leaves). Accessible names are ENFORCED: an interactive control with no text content, no `text=`, and no `label=` is a validation error (icon-only controls need `label`; text-entry controls need `label` or `placeholder`), unknown/misused literal roles are errors (`role=\"tree\"` on a text leaf can never hold rows), unnamed avatars and labels duplicating the text content are warnings (`label=\"\"` marks an image decorative). Zig-built trees get the same discipline from `canvas.expectA11yAuditSweepClean` (missing names as the bridges would announce them, focusables clipped out of keyboard reach, identically labeled siblings) — adopt it next to the layout sweep.\nIdentity: `key` (sibling-scoped), `global-key` (parent-independent — use for items that move between containers, e.g. board cards; ids then survive reparenting).\nWindow chrome: `window-drag=\"true\"` (Zig: `.window_drag = true`) marks the element as a window-drag surface for hidden-titlebar windows — pressing its background or plain text/icons inside moves the WINDOW (drag starts only on actual movement), double-click zooms per the OS convention, and press-claiming children (buttons, fields) stay fully interactive via the ordinary press fall-through. macOS-only; elsewhere the press is dead space. See \"Hidden titlebar\" below.\nRender channel (Zig-only, no markup attributes): `ElementOptions.opacity` and `ElementOptions.transform` wrap the element's emitted commands without reflowing siblings — the defaults (1, identity) emit nothing, opacity 0 culls painting (pair with `disabled` when fading interactive content), and a transform moves both rendering and pointer hit-testing while accessibility frames stay at the layout frame. Pair with `UiApp.Options.animations` for tweening.\n\nNumbers are plain (`gap=\"12\"`), booleans are `true`/`false` or a binding.\n\nWhen children's minimum sizes exceed their container, debug builds log a `zero_canvas_layout` diagnostic naming the container, axis, and overflow in pixels — flex overflow is never silent. In Zig views, `.gap` on a stacking kind (`ui.panel(.{ .gap = 8 }, ...)`) logs a `zero_canvas_ui` warning in debug builds with the same lesson — it never fails the build.\n\n### Chips: exclusive selection with `selected=`\n\nThe chip pattern — an exclusive group where the model owns which one is active — is a `toggle-group` of `toggle-button`s (or plain `button`s) whose `selected=` binds the model:\n\n```html\n<toggle-group gap=\"2\" label=\"Theme\">\n  <for each=\"theme_prefs\" as=\"p\">\n    <toggle-button size=\"sm\" selected=\"{p == theme_pref}\" on-toggle=\"set_theme:{p}\">{p}</toggle-button>\n  </for>\n</toggle-group>\n```\n\nA `toggle-button` whose source asserts `selected` (this rebuild or the previous one) is model-driven: the source wins over the runtime's retained toggle on every rebuild, so exactly the model's selection is active — pressing a chip dispatches the Msg, the model moves the selection, and the old chip deactivates. Without a `selected=` that ever asserts, a `toggle-button` is uncontrolled: the runtime retains its pressed state across rebuilds (the multi-select formatting-bar case — bold/italic chips with zero app wiring). `button` with `selected=` is always model-driven (buttons never retain state) and dispatches `on-press`; `toggle-button` dispatches `on-toggle` (its activation is the toggle intent — an `on-press` there never fires). Model-driven chips need a handler that actually moves the model: a chip whose Msg is ignored keeps its retained press until the model asserts its `selected=`.\n\n### Pickers: select is the trigger — compose the options as an ANCHORED dropdown\n\n`select` and `combobox` are trigger controls, not complete pickers: `select` renders the closed dropdown shape (current value as content, `placeholder` while empty, `on-press` to open) and `combobox` is a text entry with a menu chevron — neither owns an options list. There is no `options=` attribute (the closed grammar has no list-valued attributes — list-shaped vocabulary is element children, the way `<context-menu>` declares its items); the options ARE the composition — a `dropdown-menu` of `menu-item`s under an `if`, beside the trigger inside a `stack`, floated with `anchor`:\n\n```html\n<stack>\n  <select placeholder=\"Pick a repo\" text=\"{current_repo}\" on-press=\"toggle_repo_picker\"/>\n  <if test=\"{repo_picker_open}\">\n    <dropdown-menu anchor=\"below\" anchor-alignment=\"stretch\" on-dismiss=\"close_repo_picker\">\n      <for each=\"repos\" key=\"name\" as=\"r\">\n        <menu-item on-press=\"pick_repo:{r.name}\" selected=\"{r.name == current_repo}\">{r.name}</menu-item>\n      </for>\n    </dropdown-menu>\n  </if>\n</stack>\n```\n\nHow the pieces fit, all model-owned (TEA):\n\n- **Open state is the model's.** `toggle_repo_picker` flips the bool; the surface exists only while the `if` renders it. There is no hidden engine open flag.\n- **`anchor` floats the menu.** The dropdown positions against its PARENT's frame (the `stack`, sized by the trigger): below it by default, flipping above when it doesn't fit and the other side has more room, height clamped to the chosen side, x clamped into the window. It consumes NO space in the flow (siblings never reflow), paints in a late z-pass above the whole tree, and escapes every ancestor scroll/clip region — window-clipped, not pane-clipped. `anchor-alignment=\"stretch\"` widens it to at least the trigger's width (the select look).\n- **`on-dismiss` closes it model-side.** Escape and a click outside the menu dismiss the surface and dispatch the Msg; `close_repo_picker` clears the bool. Escape works even when the trigger took no focus (a plain-text crumb): with no relevant focus chain it dismisses the topmost mounted anchored surface. The engine hides the surface immediately (the optimistic echo), and the next rebuild's source tree is truth — a model that keeps `open` true gets it back. Clicking the TRIGGER while open never double-fires: the anchor region owns its surface's toggling, so only `toggle_repo_picker` dispatches.\n- **Items close on pick.** `pick_repo` sets the value AND clears the open flag — a click inside the surface never dismisses.\n- **Keyboard**: ArrowDown/Up on a trigger with a mounted menu enters its selected visible row, or the first/last eligible row. Arrows move without wrapping; Home/End choose the visible edges. Enter/Space activate items. Tab/Shift+Tab dismiss without committing and return focus to the trigger; the next Tab continues through ordinary controls. Escape also dismisses from the trigger or menu and returns focus.\n- **Automation sees everything**: the floating menu and its items appear in widget snapshots at their real frames and `widget-click <item-id>` works while it is open.\n\n`combobox` composes the same way (the model filters the `for` source as the user types via `on-input`). Markup also supports `<popover>` and `<menu-surface>` with the same anchor and dismissal attributes. The Zig mirror is `ElementOptions.anchor`/`anchor_alignment`/`anchor_offset` + `on_dismiss` on `dropdown_menu`, `popover`, or `menu_surface`, built with `ui.eachCtx` for the options. Budget: at most 16 anchored surfaces may be mounted per view (`max_canvas_widget_anchored_per_view`, loud `error.WidgetAnchoredSurfaceLimitReached`) — an `anchor` inside a `<for>` body is almost always a mistake.\n\n### Splitters: split panes with a model-owned fraction\n\n`split` is the resizable two-pane seam: exactly two element children, and the engine synthesizes the draggable divider between them (resize cursor, focusable, ARIA separator whose value is the fraction). The fraction is MODEL-OWNED — the runtime applies each drag/keyboard step as an optimistic echo, dispatches `on-resize` with the applied fraction, and the model echoes it back through `value` so the next rebuild lays the panes exactly there:\n\n```html\n<split value=\"{sidebar_split}\" on-resize=\"sidebar_resized\">\n  <column min-width=\"150\">…sidebar…</column>\n  <split value=\"{list_split}\" on-resize=\"list_resized\">\n    <column min-width=\"220\">…list…</column>\n    <column min-width=\"280\">…editor…</column>\n  </split>\n</split>\n```\n\n- **`on-resize` names an f32 Msg variant** (`sidebar_resized: f32`); `update` stores it (`model.sidebar_split = fraction`). The delivered fraction is the value the runtime already applied and clamped, so echoing it never fights the reconcile.\n- **`min-width` on the panes bounds the drag** — the divider clamps so neither pane shrinks below its floor, on drag, keyboard, and layout alike.\n- **Uncontrolled works too**: without `on-resize`, the divider position survives rebuilds under the source-wins reconcile (a source-side `value` change wins), but pane CONTENT lays out at the declared fraction until the model echoes — bind the handler for the exact controlled loop.\n- **Keyboard**: Tab reaches the divid","frontmatter":{"name":"native-ui","description":"Authoring guide for the Native markup (.native) view tier of native-rendered Native SDK apps. TypeScript cores are the default and use this skill together with ts-core; Zig cores are an explicit alternative and this guide labels their lower-level UiApp recipes. Use when building or modifying native UI (widgets, layout, bindings, messages), writing .native files, wiring views to Model/Msg/update, testing markup views, or verifying a native app through the automation harness."},"isInternal":false,"tokens":11814,"sizeBytes":50218},{"name":"SKILL.md","path":"skill-data/ts-core/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/skill-data/ts-core/SKILL.md","title":"ts-core","category":"anthropic-skill","format":"markdown","content":"---\nname: ts-core\ndescription: Authoring guide for the primary Native SDK app-logic path: TypeScript app cores - Model, Msg, update, and the pure functions they call, written in the closed app-core subset, checked by the @native-sdk/core frontend, and compiled ahead-of-time to native code by the external core compiler. Use for new apps unless the user explicitly chose Zig, when writing or modifying a src/core.ts app core, fixing subset checker errors (NS1001-NS1069), or deciding how to express state, messages, text (bytes and the byte-text string methods), text input, continuous controls (sliders, scroll), effects (Cmd), subscriptions (Sub), the host-event wiring channels (frameMsg, keyMsg, pinchMsg, dropMsg, appearanceMsg, chromeMsg, envMsgs, app manifest assets), derived values, the view_unbound lint opt-out, local mutation of owned arrays, or how to split a core into modules under src/ (relative imports, namespace imports, @native-sdk/core/text, @native-sdk/core/events). Use ts-services alongside this guide for src/services work.\n---\n\n# Author app cores in the TypeScript subset\n\nTypeScript is the primary app-authoring language. An app core is a Native SDK app's deterministic logic: `Model` (the app state), `Msg` (a discriminated union of everything that can happen), `update(model, msg)` (the one pure transition function), and the pure helpers they call. You write it as a TypeScript module rooted at `src/core.ts` - splitting into more modules under `src/` when it grows (see \"Splitting a core into modules\") - and the build checks the whole import graph with the `@native-sdk/core` frontend and compiles it to native code with the external core compiler. No JS engine ships in the binary — the program either passes the subset checker and compiles to native, or you get a teaching error naming the rule, the fix, and the reason. The same file is executable TypeScript: it typechecks with stock tsc and runs unmodified under node, so you can poke behavior with plain node scripts before the native build.\n\nA whole TS app starts as three files of truth and zero Zig: `src/core.ts` (this guide; plus core-class modules it imports under `src/`), `src/app.native` (the markup view over the core's model), and `app.json` (windows, identity, permissions). Existing `app.zon` manifests remain supported. Optional ordinary-TypeScript service modules live under `src/services/` and are never imported by the core; load `native skills get ts-services` for that boundary. `native init` scaffolds the three-file base; the build detects `src/core.ts` in the tree (never a flag or config — a tree with both `src/core.ts` and `src/main.zig` is a teaching error) and generates the wiring outside the app. The loop:\n\n```sh\nnative dev --core   # the fastest loop: run the core under node's virtual host —\n                    # dispatch Msgs as JSON lines ({\"kind\":\"add\"}, {\"$bytes\":\"…\"}\n                    # for bytes payloads, {\"advance\":1000} to run virtual timers),\n                    # watch the model + effect transcript. Logic only, no renderer.\nnative dev          # build and run the real app (markup hot reload)\nnative check        # subset-check core.ts + validate markup + app.json/app.zon\nnative build        # ReleaseFast binary; native test runs the app's tests\n```\n\nThe complete reference app in this idiom is `examples/soundboard` in the SDK repo: the soundboard music library as three files and zero Zig — const catalog tables, REAL audio through the `Cmd.audioPlay` stream, scrub-to-seek on a markup slider, a motion-gated `Sub.timer` playback clock, the full text-edit engine on a search field, controlled scroll, registered cover assets, the width-adaptive grid through the frame channel, clipboard, and context menus, with an end-to-end suite driving the shipping markup.\n\n## The contract\n\n```ts\nexport interface Model { /* readonly data fields only */ }\n\nexport type Msg =\n  | { readonly kind: \"add\" }\n  | { readonly kind: \"toggle\"; readonly id: number };\n  // ...one arm per thing that can happen; at least two arms\n\nexport function initialModel(): Model { /* pure */ }\n\nexport function update(model: Model, msg: Msg): Model {\n  switch (msg.kind) {\n    // one case per arm; the switch must be exhaustive (no default needed\n    // once every arm is present — a missing arm is a build error)\n  }\n}\n```\n\n- `update` is pure and synchronous: next model out, plus optionally command data describing effects. When a dispatch needs an effect, declare the return type `Model | [Model, Cmd<Msg>]` and return `[nextModel, cmd]` — the runtime interprets the command after the model commits and dispatches any result back to you as a `Msg`. `initialModel` may return the same pair (`[Model, Cmd<Msg>]`) to run a boot effect once at install, and an app that needs recurring timers exports `subscriptions(model): Sub<Msg>`. See \"Effects are Cmd data\" below.\n- Exported helper functions (`export function doneCount(model: Model): number`) compile to public native functions — and every exported helper taking exactly ONE Model parameter also becomes a Model declaration markup binds by the helper's own name (`{doneCount}`), so derived values need no model field. One binding name per member: a helper that collides with a field (or another helper) is a taught NS1031.\n- Update-only state (fields, helpers, or Msg kinds nothing in markup binds or dispatches — host-fired timer arms, persistence bookkeeping) is declared once: `export const viewUnbound = [\"nextId\", \"tick\"] as const;`. It emits as the `view_unbound` opt-out `native check`'s unbound-state lint reads; a name outside the model surface is a taught NS1032. Entries are the TypeScript names exactly as declared (`\"nextId\"`) and Msg kinds as their `kind` tags — the same names markup binds, because there are no other names.\n- Names: your names are your names — fields, helpers, and locals emit into Zig with their TS spellings (`doneToday` stays `doneToday`), and markup binds them verbatim. String-literal unions emit as native enums.\n\n## Tests against the compiled app\n\nPut Node test files directly in `tests/*.test.ts` and run `native test`. Import `NativeApp` and `findWidget` from `@native-sdk/core/testing` **only in tests**, never in `src/core.ts`. Each `NativeApp.start()` launches a separate headless native process with the real scriptc-compiled core and markup. Drive `click`/`action`/`menu`, assert returned model and accessibility snapshots, inspect and answer fake requests with `respond`, and fire declared timers with `fireTimer`. Supply explicit declared launch values with `NativeApp.start({ environment: [{ name: \"APP_MODE\", value: \"manual\" }] })`; they are journaled and replay does not read ambient environment. Framework app directories still use `appDataDirectory`. Always `close()` in `finally` or use `await using`. `verifyReplay()` verifies checkpoints and the final model in a fresh runtime; afterward only snapshot/close are allowed. Model bytes are JSON number arrays; native identities are decimal strings. The host uses a fixed clock and fake effects, without production persistence/services/app-data startup. See https://native-sdk.dev/docs/testing for the complete API and limits.\n\n## The arena mental model\n\nEverything `update` builds lives in a per-dispatch bump arena that is freed wholesale after the returned model is committed, so spreads, `map`, and `filter` are cheap by construction. At commit, only nodes your update actually created are copied into the persistent model heap — everything you spread through unchanged is shared with the previous model for free.\n\nThat is why the immutable style is not a performance tax: `{ ...model, tasks: model.tasks.map(...) }` copies one small struct and one pointer array, never the world.\n\nBoth regions are the compiled core's own: the frame arena bounds one dispatch's transients, the model heap holds the committed model between dispatches, and the compiler's determinism fences keep every dispatch allocation-shaped and replayable.\n\n## What the subset means\n\nThe subset is TypeScript minus the ecosystem minus the purity violations — never minus basic syntax. Concretely: every basic statement, operator, and declaration form of the language compiles (every loop shape including `do...while`, labels with labeled `break`/`continue`, `switch` with `default`, the full assignment-operator family, `**` and the shifts, const record destructuring, namespace imports over your own modules). What does not compile falls into exactly two families, each with a named teaching rule: the ECOSYSTEM the binary cannot carry (npm packages, Node/DOM APIs, regex/JSON/Promise/generator machinery, `eval` — no JS engine ships), and the constructs that would break a core's guarantees (purity and determinism, fixed shapes, one text representation, functions as declarations not values, static types with no runtime tags). Classes and exceptions are NOT in either family: data classes and `throw`/`try`/`catch`/`finally` compile (see below) — only their guarantee-breaking tails (inheritance, unsafe finally, untagged thrown values) teach. A construct that fails with a generic error instead of a teaching rule is a checker bug — the grammar matrix test (`grammar_matrix.test.ts`) pins every grammar production to its verdict so no silent gap can appear.\n\nThe banned families at a glance (each diagnostic names the fix and the reason at the site):\n\n- **No JS engine ships**: `eval`, `new Function`, dynamic `import()`, `debugger` (NS1013); regexes (NS1040); BigInt/Symbol (NS1044); generators (NS1042); async/Promises (NS1002); npm imports (NS1035).\n- **Purity and determinism**: mutation of SHARED data — parameters, model/msg trees, module tables, escaped locals (NS1001/NS1022/NS1051; your own function-local scratch arrays mutate freely, see \"Local mutation\" below), module-level `let` (NS1010), ambient time/randomness/IO (NS1005), effects outside the Cmd/Sub return paths (NS1017/NS1025).\n- **Fixed shapes and layouts**: the class machinery beyond data classes — `extends`/`super`/`abstract` (NS1055), accessors/`#`-privates/class expressions/`this`-as-a-value (NS1056/NS1006), mutable statics (NS1010), generic classes (NS1053) — plus `delete`/getters/setters/computed keys (NS1012), `for`/`in` (NS1009), Map/Set (NS1011), runtime type/shape tests — `typeof`/`in`/`instanceof`/`Object.*` (NS1041). Data classes themselves compile — `static` methods, `static readonly` consts, and erased `private`/`protected` included (see \"Data classes\" below).\n- **One text representation**: string indexing (NS1004), `+` concatenation and tagged templates (NS1018), `string` model fields (NS1024), the byte-text stays-out spellings — `charCodeAt`/`normalize`/`replace` and friends on bytes teach the byte-honest form (NS1060; the supported method surface is under \"Text is bytes\").\n- **Functions are declarations — or const local helpers**: nested function declarations, non-const function values, `?.()` (NS1046), and const helpers that capture/escape/under-annotate (NS1054 — the legal shape is below under \"Local function values\"); fixed arity — no defaults, rest, `arguments`, call spreads (NS1019); generics live on module-level declarations and monomorphize per call site (NS1050/NS1053).\n- **The mapping stays exact**: `var` hoisting (NS1049), loose `==` (NS1048), comma/void/assignment-as-value outside a for-header (NS1043), array/parameter destructuring (NS1045), `export default`/`export =`/`export * from` (NS1047 — export lists and named value re-exports compile), namespace-alias-as-value and SDK namespace imports (NS1039).\n\n## What compiles (v1)\n\nModel and message shapes:\n\n- `interface` with `readonly` fields; nested interfaces; `T | null` for optional data.\n- Model field types: `number`, `boolean`, string-literal unions (`\"all\" | \"active\" | \"done\"` → native enum), numeric-literal unions, `Uint8Array` (bytes), a nested interface, `readonly Interface[]` (arrays of object types), primitive arrays (`readonly number[]`, `readonly boolean[]`, arrays of literal-union tags), a tag-discriminated union (`{ kind: \"list\" } | { kind: \"detail\"; note: Note }` — arms may carry records, bytes, and primitive arrays), and `T | null` over any of these. Model unions compile to native tagged unions; switching arms in `update` retires the old arm's payload automatically at commit.\n- `Msg`: a discriminated union on a `readonly kind` string tag, with primitive / bytes / interface payload fields. It must be a real union — give it at least two arms, or TypeScript collapses the alias to a plain object type and the checker rejects it.\n\nLogic:\n\n- `switch` on any union's `kind` tag — `msg.kind` (the Msg dispatch) and model-field unions (`switch (model.view.kind)`) alike — with member case labels, label stacking (`case \"a\": case \"b\": body`), `break`, and a trailing `default` covering the unnamed arms (without a `default` the switch must be exhaustive — NS1015; with every arm named the `default` is JS dead code and emits nothing) — and `switch` on a string-literal-union or numeric-literal-union *value* (`switch (model.filter)`) — an uncovered member skips the switch exactly like JS (a `default` anywhere but last is a taught stop in both forms) — and `switch` on a plain `number` or `string` value, lowered to an if/else chain with exact JS semantics: strict equality per case (NaN matches nothing, `-0` matches `0`, strings compare contents), cases tested in source order, `default` matching only after every case misses wherever it sits (an empty `default:` stacking onto the next body included); `if`/`else`; classic `for (let i = 0; ...)` including countdowns (`i--`, `i -= k`), multi-counter inits (`let lo = 0, hi = n`), and comma incrementors (`lo++, hi--` — the for-header is the one home for comma sequences); `do { ... } while (cond)` (the body runs before the first test; `continue` jumps to the test, exactly node); `for (const x of xs)` over arrays and `Uint8Array`, with `break`/`continue`, plus the indexed pair form `for (const [i, x] of xs.entries())` (exactly the `[index, element]` two-identifier binding — the index is the loop index, integer-classed; other tuple shapes stay taught); `while`; labeled statements on loops and blocks with labeled `break`/`continue` (`outer: for (...) { ... continue outer; }` — a labeled `continue` in a classic for still runs the incrementor, like JS); `let` locals with reassignment (and `let x: number;` declared-then-assigned); ternaries; `&&`/`||`/`!`; the empty statement `;`.\n- `const { total, done: doneCount } = stats;` — record-field destructuring into const locals (a compile-time alias per field, renames included). Array patterns, parameter patterns, defaults, rest, and nesting are taught (NS1045 — positions can be silently absent in JS; fields cannot).\n- `import * as util from \"./util.ts\"` — a namespace import over your own modules is pure dot-syntax: `util.helper(x)`, `util.CONST`, and `util.Cfg` in type positions all resolve to the target module's flat names. The alias is not a value (storing or passing `util` itself is taught), and the intrinsic `@native-sdk/core` module is always imported by name (NS1039 — the purity rules recognize `Cmd`/`Sub`/`asciiBytes`/`utf8Bytes` by their imported names).\n- Object spread `{ ...model, field: v }`, array spreads in any shape — append `[...xs, x]`, prepend `[x, ...xs]`, multi-spread `[...a, x, ...b]` (each compiles to one exact-size copy) — `.length`, indexing `xs[i]`.\n- Array methods, lowered to inlined loops and exact-size arena copies: `.map` / `.filter` / `.find` / `.findIndex` / `.some` / `.every` / `.reduce` / `.toSorted` / `.slice` / `.concat` / `.indexOf` / `.includes`. `.map` is type-changing — `tasks.map((t) => t.id)` produces a number array, `t => t.title` a bytes array, and a callback that can return `null` produces an optional-element array. Callbacks on map/filter/find/findIndex/some/every may take the `(element, index)` pair — the index is the loop index, integer-classed (`.reduce` stays `(acc, x)`: its index parameter is not in v1, and no callback takes the third JS parameter, the array itself — reference the array by name). Array-method calls may sit directly in `if`/`else if` and ternary conditions (`if (xs.some((x) => x > 3))`) — the scan lowers to a loop just before the branch; a `while` condition cannot (it re-evaluates per iteration — hoist into the loop body or restructure). Callbacks are arrows (expression or block body), inline `function` expressions, or a BARE REFERENCE to a module-level function or const helper (`xs.map(encodeTurn)`, `xs.toSorted(byAscending)` — the referenced body inlines exactly like the arrow spelled at the site); in a block body every code path must end in an explicit `return` (falling off the end would be JS `undefined`, which has no mapping — a taught stop). JS semantics hold exactly: `.slice` resolves negative and out-of-range indices the JS way, `.indexOf` never matches `NaN` while `.includes` does, `.some`/`.every` keep their vacuous defaults on empty arrays, and `.reduce` needs its initial value (the no-initial form throws on an empty array in JS, so it is a taught NS1007 — pass the starting accumulator). `.indexOf`/`.includes` work on scalar elements (numbers, tags, booleans); on record arrays JS compares references, which has no native mapping — match a field with `.find`/`.findIndex` instead.\n- **Local mutation — your own scratch is yours; shared data is immutable.** An array your function CREATES — an array literal (`const stack: number[] = []`, `const st = [1, 2, 3]`) or a fresh copy (`.slice()` / `.map()` / `.filter()` / `.concat()` / `.toSorted()`) — is locally owned, and the full mutating method set works on it with exact JS semantics: `push(...items)`, `pop()`, `shift()`, `unshift(...items)`, `splice(start, deleteCount?, ...items)` (negative/overshooting indices clamp the JS way; the value is the removed array, also yours), `reverse()`, `fill(v, start?, end?)`, in-place `sort(cmp)`, and indexed writes `xs[i] = v`. A parser stack, a work queue, a copy-then-sort — all legal, deterministic, and byte-identical to node. Ownership ends at the first ESCAPE: once the array is returned from a callback, stored into a record/array/model, aliased by a second binding (`const b = a`), or passed where the callee could keep or mutate it, mutating it afterwards is a taught NS1051 — finish mutating first, then let it escape (an early-exit `return` is fine: execution ends there, so mutations on the other path stay legal). Two loosenings keep real code flowing. BORROWING: passing an owned array into a `readonly T[]` parameter is NOT an escape when the callee only READS it (element/property access, iteration, spreads, further borrowing passes — no return of it, no store, no onward pass into a mutable position; recursion over borrowed slices included), so measure-mutate-measure loops work (`total(out); out.push(x); total(out)`). REASSIGNED-OWNING: a `let` binding whose EVERY assignment installs a fresh owning construction (a literal or a copy — `w = xs.filter(...)`, `acc = []`) stays owned through the reassignments; ONE mixed assignment (an alias, a parameter, a helper result) and the binding never owns (NS1001 names it). Never owned: parameters, model/msg data, module `const` tables, aliases, mixed reassigned bindings, and arrays produced by helper calls (copy with `.slice()` to own one). After the value escapes it is an ordinary immutable value; the commit walkers and sharing discipline are unaffected because ownership ended before the escape.\n- Local-mutation shape notes: `push`/`unshift` return the new length in JS, which has no mapping — mutate as a statement and read `.length` after; `sort`/`reverse`/`fill` return the same array — mutate as a statement, then use the array by name (`return copy.sort(cmp)` is a taught stop; the canonical form is `const copy = xs.slice(); copy.sort(cmp); return copy;`); `pop()`/`shift()` return `T | undefined` — the same one-empty the `.find` miss produces, so test `=== undefined` or fold with `??` (`stack.pop() ?? fallback`); spread arguments (`out.push(...xs)`) stay taught — one element per iteration; `xs[xs.length] = v` on an owned array IS a push (the one growth shape — compound forms like `xs[xs.length] += v` read the missing slot first and stay taught), and other out-of-bounds writes are JS sparse arrays with no mapping (they trap on the native bounds check in safe builds — keep writes inside `0..length-1`); changing the LENGTH of the array a `for...of` (or one of its own callbacks) is iterating is a taught stop (JS walks the live array; fixed-length writes during iteration are fine and identical to node); `copyWithin` stays out of v1 (splice/fill cover it).\n- `.toSorted(cmp)` sorts a copy in one expression; `.sort(cmp)` sorts in place on an array you own (on shared data it keeps the NS1022 teaching, which names the copy idiom). Both comparators follow the same rules: return a sign — `(a, b) => a - b` for ascending numbers, or explicit -1/0/1 branches; a boolean comparator is wrong in JS itself (false claims equality) and is rejected by the types plus a taught NS1023. The comparator-less arity sorts by string ToString order in JS (`[10, 9]` stays `[10, 9]`), which has no float-text mapping — pass a comparator. Both sorts are stable exactly like JS: comparator 0 (or NaN) keeps the original order of the pair. One honesty note: a comparator that is inconsistent over the actual data (e.g. `a - b` when elements can be `NaN`) is implementation-defined in JS itself, so node and native may then disagree — keep comparators consistent.\n- A `.find` miss is the core's one empty value: JS spells it `undefined`, so test the result with `=== undefined` (never `=== null` — the checker teaches the difference) or fold it away with `??`: `tasks.find((t) => t.id === id) ?? fallback`.\n- Optional chaining `?.` on property chains (`model.sel?.at ?? 0`), element hops (`m?.xs[0] ?? 0`, `xs?.[i] ?? d`), and method hops on supported receivers (`xs?.slice(0, 2)`, `xs?.includes(3) ?? false` — every mapped array/bytes method): each hop null-propagates exactly like JS, and the chain value is optional — end it in `??` or compare it against a real value. A `?.` chain compared against `null`/`undefined` is a taught error (NS1021), and `g?.()` on a function value stays taught.\n- Null-guard narrowing through `&&`/`||` chains, exactly the way TS narrows: `if (x !== null && x.items.length > 0)`, the flipped order (`null !== x`), the `||` dual (`x === null || x.items.length === 0`, including as an early-exit guard — the code after the exit stays narrowed), ternary conditions (`x !== null && x.at > 0 ? x.at : -1`), and `while (cur !== null && cur.n > 0)` loops (re-tested per iteration; assigning the guarded local drops the narrowing for what follows, like TS). Relational comparisons on guarded optionals (`cls !== null && cls < lim`) work too.\n- Nullish `??`, comparisons (including `===` on `string`-typed values — content equality, same as node; `==`/`!=` are taught NS1048 — coercion), `+ - * / % **` on numbers, unary `+`/`-`, and the bitwise family `& | ^ ~ << >> >>>` — all with JS number semantics (`/` is float division, `%` truncates, `**` is float pow with the exact JS corners — `1 ** NaN` is NaN, `(-1) ** Infinity` is NaN, right-associative `2 ** 3 ** 2` is 512; bitwise and shifts are ToInt32 with the shift count masked & 31, `>>>` yielding the unsigned 32-bit value; unary `+` is the identity on numbers). `**` and `/` results are float-classed; bitwise/shift operands are integer-required positions (a float operand is a taught NS1016).\n- Every compound assignment as a statement: `+= -= *= /= %= **= &= |= ^= <<= >>= >>>=`, each exactly `x = x op v`, plus the guarded forms `&&=`/`||=` (boolean targets; the right side evaluates only when assigned, like JS) and `??=` (optional targets; assigns only when null). A number `++`/`--`/assignment may sit in a VALUE position when the split statement is provably order-exact — the variable's only mention in the statement, in a position JS cannot skip (`arr[i++]`, `const n = ++count`, `const z = (y = 5)`; postfix yields the pre-step value, everything else the post-step value, exactly JS); every other value-position form is taught (NS1043 — ternary branches, short-circuit right operands, loop conditions, or a second mention of the variable).\n- The Math batch, every corner pinned to node: `Math.min` / `Math.max` (any arity — `Math.min()` is Infinity, `Math.max()` is -Infinity, NaN propagates, -0 orders below +0), `Math.round` (half toward +Infinity), `Math.floor` / `Math.ceil` / `Math.trunc` (NaN/Infinity propagate; the -0 results keep their sign, so `Math.ceil(-0.5)` is -0), `Math.abs` (clears the zero sign), `Math.sign` (NaN stays NaN, a zero keeps its sign), `Math.sqrt` (negative input is NaN, `sqrt(-0)` is -0). `Number.isInteger` / `Number.isFinite` / `Number.isNaN` classify like node, and the `NaN` / `Infinity` globals are ordinary number values. Math calls over compile-time constants fold to their exact JS value — `const HALF = Math.floor(5 / 2)` is the integer 2, `5 % 0` is NaN, `-5 % 5` is -0. A bare `-0` literal (and constant arithmetic folding to -0, like `0 * -1`) is a float value — only f64 carries the signed zero — so it cannot flow into an index or another integer-required slot. The integer rule: floor/ceil/trunc/abs/sign of an integer-classed value stays integer-classed, but of a float value stays float (floor of NaN is NaN), so `bytes[Math.floor(x / 2)]` over a float `x` is still a taught NS1016 — keep index flows integer end to end.\n- Template literals with integer holes (`` `${n} of ${total}` ``) feeding `utf8Bytes` or `asciiBytes` (below). Float-valued holes are not in v1 (JS float-to-string fidelity is a runtime v2 surface).\n- Module-level `const` numbers and strings fold to comptime constants, and const tables emit as rodata (no arena, shared for free at commit): arrays of numbers / booleans / strings / literal-union members (`const WEEKDAYS = [3, 5, 2]`, `const ORDER: readonly Filter[] = [\"done\", \"all\"]`), records annotated with an interface (`const LIMITS: Limits = { lo: 1, hi: 9 }`), and arrays of records (`const SEEDS: readonly Task[] = [...]`, names as `utf8Bytes` literals). Element access, `.length`, `for...of`, and the array methods all work over tables. Everything inside must fold at compile time — no spreads, no calls except `asciiBytes` or `utf8Bytes` on a literal — and a record table needs its interface annotation (an unannotated `{ ... }` is a taught stop naming the fix). Helper functions; recursion.\n- **Generics — module-level, monomorphized per call site.** A generic `function`, `interface`, or `type` declares type parameters and instantiates from tsc's RESOLVED type arguments (explicit or inferred): `export function pick<T>(xs: readonly T[], i: number): T { return xs[i]; }` called with tasks emits `pick__Task`, with numbers `pick__f64` (a bare `number` type argument is always f64 — the JS-exact class), one readable Zig fn per distinct instantiation, deduped. Generics over records, unions, arrays, optionals, and bytes all work; generic interfaces/aliases instantiate structurally (`Box<Task>` emits `Box__Task`; `type Opt<T> = T | null` resolves straight through); generics may recurse and call other generics (the inner call resolves at the outer's instantiation). `typeof CONST` type-query aliases resolve through tsc too (`const LIMIT = 9; type Limit = typeof LIMIT`). The boundaries teach: a call site whose type argument stays abstract (`pick([])` infers `never`; `any`/`unknown`, unnamed literal unions) is NS1053 — annotate the call or name the alias; generic function VALUES and generic entry points are NS1050.\n- **Data classes — fields, one constructor, plain methods, statics; no inheritance.** `class Task { title: Uint8Array; done: boolean = false; constructor(title: Uint8Array) { this.title = title; } toggle(): void { this.done = !this.done; } isDone(): boolean { return this.done; } }` emits as a plain struct plus module-level functions; `new Task(...)` constructs a record-shaped value (field initializers run in declaration order, then the constructor body). `static` members are per-class module declarations: a `static` method lowers to a receiver-less module fn under the class's mangled name (`Task.fromRow(...)` resolves to `Task__fromRow`), and a `static readonly` field with an initializer is a module const (`Task.LIMIT` — the module-const value rules apply: numbers/strings fold, tables need their annotation); a MUTABLE static is module state and teaches NS1010, and inside a static member reach other statics by the class name, never `this` (NS1056). `private`/`protected` keywords are accepted and ERASED — tsc enforces them at the type level, which is their whole meaning (`#`-fields stay taught: runtime privacy brands). `this` reaches instance fields and methods (`this.count`, `this.step()`) — anything that lets `this` escape as a value (returning it, storing it, passing it) is taught (NS1056), so fluent chaining is out. Mutation follows exactly the array ownership rule: an instance your function creates with `new` mutates freely — direct field writes (`t.count = 1`, `t.count += 2`) and methods that write `this` — until it ESCAPES (returned, passed, stored, aliased — then NS1051), and parameters/model data never mutate (NS1001); methods that only read are callable on anything. Fields require type annotations; instances flow between functions, sit in arrays, and compare/narrow like records. The class TAIL teaches by name: `extends`/`super`/`abstract` (NS1055 — compose, or model variants as a `kind`-union), getters/setters/`#`-privates/`accessor`/class expressions (NS1056), mutable statics (NS1010), generic classes (NS1053), parameter properties (NS1008), `instanceof` (NS1041 — a `kind` field is the tag that exists). Class instances stay LOCAL values in v1: storing one in the Model tree is taught (NS1056) — keep records (interfaces) in the Model and construct the class where behavior is needed.\n- **Exceptions — `throw`/`try`/`catch`/`finally` as pure control flow.** Inside a core, exceptions are deterministic: `throw` carries a subset VALUE and unwinds to the nearest enclosing `catch` — across helper calls, out of array-method callbacks (a `throw` inside `.map`'s callback exits the whole loop, like JS), through nested `try`s, with `finally` running on every path (fall-through, `return`, `break`/`continue`, and throw alike). The discipline is two rules. First (NS1057): thrown values are kind-tagged subset shapes — throw kind-discriminated records (`throw { kind: \"bad_digit\", at: i } as ParseError`, where `ParseError` is an interface with a string-literal `kind` field or a `kind`-discriminated union; a single-shape core may also throw a number), and SEVERAL distinct shapes may throw: the checker collects every shape the core throws into its implicit thrown union. The catch binding IS that union — narrow it in place with kind tests, no `as` ceremony: `catch (e) { if (e.kind === \"bad_digit\") return -e.at; if (e.kind === \"io\") return e.code; return -1; }` (or `switch (e.kind)` — tsc cannot prove exhaustiveness over the implicit union, so give the switch a `default` or a trailing return). Bare rethrow (`throw e;`) re-raises the bound value — a narrowed arm included — and `catch { ... }` needs no binding; the single-`as` form (`const err = e as ParseError;`) stays legal in single-shape cores (and for a DECLARED union whose arms equal the thrown set — declare `type AppError = ... | ...` and `as AppError` works). What teaches: untagged values in a heterogeneous set, two shapes sharing one `kind` with different payloads, asserting one member shape of a multi-shape core, the binding escaping untyped into a call/store/return, and `throw new Error(...)` (engine error objects carry stack traces with no native layout). Second (NS1058): `finally` never redirects control flow — no `return`/`throw`/`break`-out inside it (JS's own no-unsafe-finally rule; loops fully inside the finally may break within themselves). An UNCAUGHT throw that reaches an exported function's boundary is a defined deterministic panic — exactly where node's process would crash. A throw mid-mutation of an owned array keeps the mutations applied so far, exactly like JS — the catch sees the array as node would.\n- **Local function values — const helpers hoist.** `const scale = (x: number): number => x * 3;` (arrow or `function` expression) hoists to an ordinary module-level fn when it is capture-free (module constants and other const helpers are fine to reference; enclosing locals/params are not — pass them as parameters), fully annotated (every parameter and the return type), and used only by direct calls (`scale(v)`, recursion included) or as an array-method callback (`xs.map(scale)`, comparators included). Everything else teaches NS1054: captures, missing annotations, `let` bindings, returning/storing the value, passing it to your own functions, calling through a record field. Capturing a locally-owned array also ENDS its ownership at the capture (a later mutation is the NS1051 teach) — the stored closure would retain the reference.\n\nNot yet in v1 — genuine roadmap deferrals, each stopping with a loud, tailored NS9001 naming the rewrite (never missing basic syntax; the banned-with-a-rule families live in \"What the subset means\" above). The rule-level deferrals carry `class: \"deferred\"` in the diagnostics catalogue — NS1011 (Map/Set), NS1019 (fixed arity), NS1040 (regexes), NS1042 (generators), NS1044 (BigInt/Symbol) — and their diagnostics say the capability is deliberately deferred, not impossible; the list below is the method-level remainder: `.toSorted()`/`.sort()` without a comparator (JS ToString ordering; pass `(a, b) => a - b`), `.reduce` without an initial value (a taught NS1007 — JS throws on an empty array) or with an index parameter (use a classic loop), `.indexOf`/`.includes` on record arrays (match a field with `.find`/`.findIndex`), `.join` on number arrays (elements are float-valued; join byte values instead), float values (`/`, `**`, `Math.round`, `Math.sqrt`, float `Math.floor`-family results) where an integer is required such as an index (a taught NS1016 — those values can be fractional or NaN), Math methods beyond the batch above, `Number` methods beyond the three classifiers, float-valued template holes (JS float-to-string fidelity is a runtime v2 surface), arrays of unions (`readonly View[]`) or arrays of byte-strings (`readonly Uint8Array[]`) as model fields (wrap the element in a single-field interface), a collect spawn's stderr tail (v1 delivers the exit code and stdout; stderr is not surfaced — put diagnostics on stdout or check the code), per-line truncation flags (a stdout or response line over its configured bound arrives cut, without a flag), and non-timer subscriptions (`Sub.timer` is the one subscription; one-shot needs are `Cmd.delay`, and process/fetch/audio streams are Cmd-initiated, not subscribed).\n\n## Effects are Cmd data\n\n`update` never performs an effect — it can return one, as inert data, alongside the next model. Import the factories from the SDK and declare the pair-return type:\n\n```ts\nimport { Cmd } from \"@native-sdk/core\";\n\nexport function update(model: Model, msg: Msg): Model | [Model, Cmd<Msg>] {\n  switch (msg.kind) {\n    case \"add\":\n      return [{ ...model, count: model.count + 1 }, Cmd.persist()];\n    case \"request_time\":\n      return [model, Cmd.now(\"tick\")]; // dispatches { kind: \"tick\", at: <ms> }\n    case \"tick\":\n      return { ...model, lastTick: msg.at }; // bare model = [model, Cmd.none]\n    case \"ship\":\n      return [model, Cmd.batch([Cmd.persist(), Cmd.host(\"beep\", model.count)])];\n  }\n}\n```\n\nThe command set (Cmd wire format v7):\n\n- `Cmd.none` — no effects; returning a bare `Model` is sugar for `[model, Cmd.none]`.\n- `Cmd.persist()` — snapshot the just-committed Model through the engine-owned store. Requires `\"persist\"` in `app.zon` capabilities plus `.persist = .{ .version, .restore = .{ .ok, .none, .err } }`; the host owns canonical encoding, debounce/coalescing, atomic app-data placement, backup recovery, and replay.\n- `Cmd.now(\"tick\")` — request a timestamp; the runtime dispatches the named Msg arm with the time (ms) as its payload. The target arm must carry exactly one number field (`{ kind: \"tick\", at: number }`), and tsc checks that for you.\n- `Cmd.host(name, ...args)` — a fire-and-forget host command by literal name; the host decides what the name means. Args are numbers, OR exactly one bytes payload: a `Uint8Array` (`Cmd.host(\"clipboard.write\", model.draft)`) or a flat inline record of number/boolean/`Uint8Array` fields (`Cmd.host(\"cfg.save\", { gain: model.gain, on: model.muted, label: asciiBytes(\"main\") })`) — the record lowers to one bytes payload from your types at build time, byte-identical under node and native. Anything else (a smuggled string, a nested record, a payload plus extra args) is a taught error (NS1020/NS1026).\n- `Cmd.request(name, payload, { key?, ok, err })` — a routed host command: the host performs `name` with the payload (same bytes/record rules) and dispatches exactly one result back to you as an ordinary Msg — the `ok` arm with the result bytes on success, or the `err` arm with the error bytes on failure. Both arms must carry exactly one `Uint8Array` field (`{ kind: \"loaded\", body: Uint8Array }`), checked by tsc and taught by NS1027. The routing is data — string-literal arm names, never callbacks — so the result decoder derives from your Msg types at build time. The optional `key` (a string literal) names the in-flight effect: issuing a request whose key is already in flight replaces it (the old result is dropped), which is the debounce/exactly-one-in-flight discipline.\n- Typed app services are the deliberate record-valued arm of this family: declare shared data shapes outside `src/services/`, import generated constructors from `@native-sdk/services`, and let those constructors encode requests and prove the typed success Msg arm. See `native skills get ts-services`. Raw `Cmd.request` remains bytes-oriented.\n- `Cmd.cancel(key)` — drop the in-flight keyed effect with that key, silently: a cancelled raw request, buffered named engine op (`readFile`/`writeFile`/`fetch`/`clipboardRead`), streamed file read, or armed delay dispatches NEITHER arm. Live spawn, streaming-fetch, streaming-service, and streamed write sinks are the exceptions: cancel ends them through `err: cancelled`; a half-written sink is observable and must never disappear silently.\n- `Cmd.batch([a, b])` — several commands from one dispatch, performed in order.\n\n### The named engine ops\n\nThese map directly onto the host's effect engine — files, HTTP, the clipboard, notifications, one-shot timers. Routed operations follow the `Cmd.request` rules (inline `{ key?, ok, err }`, string-literal arm names, arm shapes checked by tsc and taught by NS1027), with one difference from `request`: each op's `ok` arm has the op's OWN result shape. Keys follow the one keyed-effect rule everywhere: issuing a keyed op whose key is already in flight REPLACES the old one (the superseded op's result is dropped — no message), and `Cmd.cancel(key)` drops it silently. Every `err` arm carries exactly one `Uint8Array` field and receives a machine-readable reason; fire-and-forget writes and notifications have no routing arms. Paths, URLs, and bodies are bytes (`asciiBytes` for literals); dynamic values the engine refuses at runtime surface through the `err` arm, while compile-time-knowable bounds for routed operations stop the build (NS1030). Fire-and-forget operations fail closed under the host's validation instead.\n\n- `Cmd.store.set(key, bytes, { key?, ok, err })` / `get` / `delete` / `scan` / `setMany` — capability-gated, engine-owned per-record storage. Declare `\"store\"` in `app.zon`; keys are UTF-8 strings up to 512 bytes and values are bytes up to 1 MiB. `setMany` applies 1–64 entries atomically. `get`'s ok bytes are `[1][value...]` for a hit and `[0]` for a miss, keeping an empty value distinct from absence. `scan(prefix, { limit?, after? }, route)` returns a little-endian length-prefixed page of key/value pairs plus an opaque next-key cursor; pass those next-key bytes back as `after` (a known literal cursor may be a string). The limit defaults to 100 and is capped at 256. Writes run off-loop in issue order, a synchronous read waits for earlier writes in the same command walk, every result journals, and failures route one of `io_failed`, `over_bound`, `bad_key`, `rejected`, or `busy`. The SQLite backing and app-data path are never part of the app-facing API.\n- **Checked relational SQLite:** declare `\"sqlite\"` in `app.zon`. Add append-only, contiguous `src/schema/NNNN_name.sql` migrations (`STRICT` ordinary tables), commit the generated `src/schema/migrations.lock.json`, and add named statements in `src/queries.sql`: `-- name: notesInFolder`, `-- name: moveNote :exec`, or `-- name: notesInFolder :live`. `native check` applies the migrations and prepares every query with real SQLite, then generates flat `Cmd.q<Name>(params, route)` query constructors, typed `Cmd.q<Name>(params)` exec members composed only through `Cmd.qTx([...], route)`, `Sub.q<Name>(key, params, route)` for `:live`, row/param interfaces, and `decode<Name>Page`. TEXT/BLOB rows are `Uint8Array`; parameters inferred from TEXT columns wrap bytes automatically; raw or uninferred dynamic TEXT (for example an FTS `MATCH` term) uses `dbText(bytes)`. INTEGER is an exact `number` and generated decoders reject values outside ±(2^53−1). Live queries rerun after committed writes to their generated table dependency set, coalesced once per command frame. `native db new-migration <name>`, `status`, and `reset --yes` own development migration operations. Do not edit or reuse a shipped migration number.\n- `Cmd.db.query(sql, params, { key?, page, done, err })` / `Cmd.db.exec(statements, { key?, ok, err })` — the permanent raw SQLite escape hatch. The engine owns `app.db`; parameters are `null | number | string | Uint8Array | dbText(bytes) | boolean`. A read delivers 256-row/256-KiB pages then `done`, with one result capped at 8,192 rows or 8 MiB; crossing either total bound rejects the whole result, so use `LIMIT` and keyset pagination for larger collections. An exec commits its 1–64 statements as ONE transaction. SQL remains pathless: `ATTACH`/`DETACH`/`VACUUM INTO`, TEMP schema objects, and engine lifecycle PRAGMAs are denied. Query keys replace/cancel silently; duplicate transaction keys reject loudly. Error bytes are `constraint`, `busy`, `io_failed`, `corrupt`, `misuse`, `rejected`, or `cancelled`. Every page/terminal journals and replay never opens SQLite. Prefer declared queries; NS1420 nudges raw query literals toward them.\n- `Cmd.readFile(path, { key?, ok, err })` — read a whole file. `ok` arm: one `Uint8Array` field with the content. `err` reasons: `not_found`, `io_failed`, `truncated` (the file exceeds the engine's 1 MiB read bound — a cut file never passes as whole), `rejected`. Paths are at most 1024 bytes.\n- `Cmd.writeFile(path, bytes, { key?, ok, err })` — write a whole file (parent directories created, an existing file replaced whole; at most 1 MiB). `ok` arm: NO payload fields (`{ kind: \"wrote\" }`) — a successful write has nothing to report. `err` reasons: `io_failed`, `rejected`.\n- `Cmd.appendFile(path, bytes, route)` appends one bounded payload; `Cmd.statFile(path, route)` returns `{ exists, size, mtimeMs }`; `Cmd.deleteFile(path, route)` deletes one file with a payload-less `ok` arm and routes a missing path as `not_found` (a final symlink is unlinked without deleting its target). `Cmd.readFileStream(path, { key?, chunk, done, err })` delivers 256-KiB chunks then the total; reissuing or cancelling its key silently replaces/drops the read. Atomic exports open with `Cmd.writeFileStream(key, path, route)`, send one acknowledged `writeFileChunk` (≤1 MiB) at a time, then `writeFileClose`; a duplicate sink rejects, overlapping chunk/close routes `out_of_order`, and sink cancellation is loud. Stream chunks spill to the session blob store for byte-identical replay. Raw paths under this app's resolved data/config/cache/state/logs/temp roots need no grant; every external path requires the `filesystem` permission. The runtime resolves existing parents and symlinks before checking (an in-root symlink pointing out is external); NS1074 catches certainly-external literals.\n- `Cmd.fetch({ url, method?, headers?, body?, timeoutMs? }, { key?, ok, err })` — a buffered HTTP(S) exchange. `ok` arm: exactly two fields, one `number` and one `Uint8Array` (`{ kind: \"fetched\", status: number, body: Uint8Array }`) — matched by type, so the names are yours. The status is the real HTTP status: a 404 is still `ok` (an HTTP-level error is a delivered response). `err` reasons: `connect_failed`, `tls_failed`, `protocol_failed`, `timed_out`, `rejected`, and `truncated` (the body exceeded the engine's 256 KiB buffered bound — never delivered silently cut). The spec is an inline object: `url` bytes (≤ 2 KiB), `method` one of `\"GET\" | \"POST\" | \"PUT\" | \"DELETE\" | \"PATCH\" | \"HEAD\"` (default GET), `headers` an inline flat record — names are compile-time ASCII, values are string literals OR runtime bytes (`{ authorization: bearerToken(model.apiKey), \"content-type\": \"application/json\" }` — how a launch-supplied key rides an `Authorization` header; ≤ 8 headers, ≤ 1 KiB total, NS1029/NS1030), `body` bytes (≤ 64 KiB), `timeoutMs` a positive integer literal (engine default when omitted).\n- `Cmd.clipboardWrite(bytes)` — put bytes on the system clipboard, fire-and-forget: there is no routing, and a refused or over-bound write is dropped by design.\n- `Cmd.clipboardRead({ key?, ok, err })` — read the clipboard. `ok` arm: one `Uint8Array` field with the text. `err` reasons: `failed` (no clipboard service, over-bound content, pasteboard error), `rejected`.\n- `Cmd.showNotification({ id?, title, subtitle?, body?, actionLabel?, actionCommand? })` — request a desktop notification, fire-and-forget: `title` is required bytes (1–128), `subtitle`, `id`, `actionLabel`, and `actionCommand` are optional bytes (up to 128), and `body` is optional bytes (up to 1024). A nonempty `id` replaces the app's earlier notification with that id where the OS supports replacement. `actionLabel` and `actionCommand` must be supplied together; activation dispatches `actionCommand` through the ordinary app-command path while the process is running. The host validates every field and command name before entering the platform service; invalid or unavailable requests fail closed, and Focus / Do Not Disturb and user settings remain authoritative after acceptance. Fake execution and session replay never display one, while the null test platform can activate a recorded notification deterministically.\n- `Cmd.openExternalUrl(url)` — open an HTTP(S) URL in the system browser, fire-and-forget. `url` is bytes; the runtime validates it and enforces `security.navigation.external_links` before entering the platform service. Invalid, denied, or unavailable requests fail closed; fake execution and replay never open one.\n- `Cmd.revealPath(path)` — reveal a byte path in Finder, Files, or Explorer, fire-and-forget. Runtime path validation stays in force; invalid or unavailable requests fail closed, and fake execution/replay never reveal one.\n- `Cmd.credentials.set(key, secret, { key?, ok, err })` / `Cmd.credentials.get(key, route)` / `Cmd.credentials.delete(key, route)` — app-scoped Keychain on macOS/iOS, Secret Service/libsecret on Linux, Credential Manager on Windows, and AndroidKeyStore-wrapped authenticated ciphertext on Android. The authored credential key is a NUL-free string (UTF-8, ≤256 bytes); the manifest app id (≤128 bytes) supplies the service namespace, and secrets are `Uint8Array` values through 2,560 bytes (the cross-platform floor set by WinCred). Set/delete deliver empty bytes on `ok`; get delivers secret bytes; the closed err reasons are `miss`, `denied`, `locked`, `io_failed`, `over_bound`, and `rejected`. Declare BOTH `\"credentials\"` in `app.zon` capabilities and permissions; NS1071 warns when the capability is absent, NS1072 errors when permission is absent, and NS1073 reserves `core.credentials.*` for the typed factories. Never retain a returned secret in Model: consume its Msg immediately to construct the next effect. Recording stores only a successful get's length, per-session salt, and a placeholder digest independent of the secret (never the secret or a blob); replay delivers deterministic same-length placeholder bytes, and devhost keeps an in-memory map while printing only `<redacted, N bytes>`.\n- `Cmd.formatLocalTime(timestampMs, \"date\" | \"time\" | \"datetime\", { key?, ok, err })` — format epoch milliseconds (the same unit `Cmd.now` produces) through the host's current locale and local time zone; `ok` carries localized UTF-8 bytes. This is deliberately an effect rather than a pure helper: locale/timezone are ambient state, so the observed bytes journal and replay without consulting the replay machine. Err reasons are `invalid_request`, `unsupported`, and `failed`.\n- `Cmd.delay(key, ms, \"fired\")` — a keyed ONE-SHOT timer: dispatches the named arm once, `ms` from now, with the fire time (ms) as its single number payload (the same arm shape `Cmd.now` and `Sub.timer` target). Re-issuing a live delay key re-arms it from now — that is the debounce discipline (`Cmd.delay(\"autosave\", 800, \"save_now\")` on every keystroke, one fire after the pause). `Cmd.cancel(key)` drops it silently. The interval is 1ms to one year; a literal outside that stops the build (NS1030).\n\nModel persistence is capability-gated. Without `\"persist\"`, NS1028 warns that `Cmd.persist()` has no linked host binding; declaring the capability without issuing the command produces the inverse warning. C","isInternal":false,"tokens":12201,"sizeBytes":50329},{"name":"service-surface.md","path":"skill-data/ts-services/references/service-surface.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/skill-data/ts-services/references/service-surface.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `skill-data/ts-services/references/service-surface.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/skill-data/ts-services/references/service-surface.md)","tokens":300,"sizeBytes":222},{"name":"SKILL.md","path":"skill-data/ts-services/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/skill-data/ts-services/SKILL.md","title":"ts-services","category":"anthropic-skill","format":"markdown","content":"---\nname: ts-services\ndescription: Authoring guide for Native SDK TypeScript services under src/services: ordinary scriptc static-tier TypeScript reached through generated typed Cmd clients, shared record shapes, hermetic vendored npm, streaming, deadlines, cancellation, devhost simulation, authority, replay, and the two carriers (in-process thread pool, child subprocess). Use when adding or modifying src/services modules, moving Node/JSON/regex/Map/Date/class work out of a core, or fixing NS1065-NS1067.\n---\n\n# Author TypeScript services behind the effect boundary\n\nUse `src/services/**/*.ts` for imperative work that needs ordinary TypeScript beyond the deterministic core subset: `fs`, `path`, `process`, `os`, `child_process`, `fetch`, regexes, JSON, `Map`/`Set`, `Date`, and classes supported by the pinned scriptc static tier. Both classes compile with the exact scriptc version in `packages/core/package.json`; neither enables `--dynamic` or ships a JavaScript engine. The exact verdict for every surface the compiler currently projects — with each status and refusal code, plus the manifest's explicit coverage limits — is `references/service-surface.md`, generated from the pinned compiler's own manifest; consult it rather than guessing compiler capability.\n\nThe class line is hard:\n\n- `src/core.ts` and its imports outside `src/services/` retain NS1001–NS1064 and `deterministic: true`.\n- The core never imports a service file, even type-only (NS1065). It imports generated command constructors from `@native-sdk/services`.\n- A service may import shared, subset-legal declarations from the core class. Put every boundary record, enum, or union there so one declaration is authoritative on both sides.\n- A service may import compiler-supported Node built-ins and exact packages declared by app.zon. Undeclared bare imports teach NS1066.\n\n## Typed operation surface\n\nEvery directly exported, non-default named function is an operation named `<module-basename>.<export>`. It must be synchronous, have a body, take zero or one explicitly annotated request, and explicitly return a contract-encodable result. Boundary data may use booleans, numbers, integer-class numbers, bytes, optionals, readonly slices, and named records/enums/kind-unions. Functions and behavior-bearing classes do not cross.\n\n```ts\n// src/shared.ts — core-class file, imported by both classes\nexport type ParseRequest = {\n  readonly source: Uint8Array;\n  readonly caseSensitive: boolean;\n};\n\nexport type ParseResult = {\n  readonly bytes: Uint8Array;\n  readonly matched: boolean;\n};\n\n// src/services/feeds.ts\nimport type { ParseRequest, ParseResult } from \"../shared.ts\";\n\nexport function parse(request: ParseRequest): ParseResult {\n  const text = new TextDecoder().decode(request.source);\n  const matched = request.caseSensitive ? /feed/.test(text) : /feed/i.test(text);\n  return { bytes: new TextEncoder().encode(JSON.stringify({ matched })), matched };\n}\n```\n\n`services.contract.json` carries the complete type table, operation names, exact compiler pin, source hashes, package facts, deadlines, streaming declarations, and `deterministic: false`. Generated host codecs, the Zig registry/result decoders, and the typed client consume that sidecar; none re-read source to infer a second truth.\n\nExplicit boundary throws must be exactly inline `{ kind, message }` shapes with a string-valued message and must escape the operation. They arrive as UTF-8 JSON bytes on the error arm. Do not throw `new Error(...)` from the exported surface.\n\n## Call through the generated client\n\nImport from `@native-sdk/services`; `native check` derives this virtual module, and build/dev stage the same projection without writing `src/services.gen.ts` into the author tree. The editor copy lives under ignored `node_modules/@native-sdk/services`.\n\n```ts\nimport { feedsParse } from \"@native-sdk/services\";\nimport type { ParseRequest, ParseResult } from \"./shared.ts\";\n\nexport type Msg =\n  | { readonly kind: \"parse\"; readonly request: ParseRequest }\n  | { readonly kind: \"parsed\"; readonly result: ParseResult }\n  | { readonly kind: \"parse_failed\"; readonly error: Uint8Array };\n\ncase \"parse\":\n  return [model, feedsParse(msg.request, {\n    key: \"feed-parse\",\n    ok: \"parsed\",\n    err: \"parse_failed\",\n  })];\n```\n\nThe generated route type proves that `ok` names the one Msg arm carrying exactly the declared result and `err` names a one-bytes-field arm. Raw `Cmd.request(name, bytes, route)` remains available as the low-level byte seam.\n\nKeys share the engine effect-key space. A duplicate live service key rejects with `rejected` through `err`. Same-key requests run strictly FIFO on both carriers; the in-process carrier runs different keys in parallel across its pool instances. Terminal results and stream events are journaled; replay feeds the recorded events without starting a child process or a pool thread.\n\n## Hermetic npm\n\nResolve packages only with an explicit author action:\n\n```bash\nnative vendor . escape-string-regexp@5.0.0\n```\n\nThe command installs with lifecycle scripts disabled in a temporary directory, copies the flattened package graph into `src/services/vendor/`, retains package/license files, computes canonical tree hashes, and rewrites app.zon's `service_packages` with exact names, `X.Y.Z` versions, and hashes. Check those bytes and manifest facts into source control.\n\nBuilds never run npm or use the network. They verify package identity and every vendored byte, stage only declared packages, and invoke scriptc with `--npm-static <explicit-list>`. `auto`, dynamic-island fallback, and `--dynamic` are refused. Anything below 100% static coverage fails: `native check` preserves the coverage note verbatim and names the three options—choose another exact package, port/vendor a suitable implementation, or wait for compiler support. The checked-in package spike (`tests/ts-services/npm-static-spike.json`) passed four of five deliberately small candidates and refused `micromark`; npm support is selective.\n\n## Streaming, cancellation, and deadlines\n\nA last `emit` capability declares a stream. Each typed chunk becomes canonical bytes on the existing external channel; the operation still returns one typed terminal result.\n\n```ts\nimport type { ServiceCancellation } from \"@native-sdk/core\";\n\nexport type ParseChunk = { readonly bytes: Uint8Array; readonly index: number };\n\n/**\n * @deadlineMs 5000\n * @streamBuffer 8\n */\nexport function parseLarge(\n  request: ParseRequest,\n  emit: (chunk: ParseChunk) => void,\n  cancellation: ServiceCancellation,\n): ParseResult {\n  for (let index = 0; index < request.source.length; index += 4096) {\n    cancellation.throwIfCancelled();\n    emit({ bytes: request.source.slice(index, index + 4096), index });\n  }\n  return parse(request);\n}\n```\n\nImport `ServiceCancellation` as a type from `@native-sdk/core`; it is legal only as the final capability parameter. The generated client route additionally requires `channelKey` and a channel-event Msg arm. `@streamBuffer` is a 1–64 in-flight contract cap (default 8); `@deadlineMs` is a 1–86400000 operation deadline. A terminal result closes the channel after its interim frames. `Cmd.cancel(key)` flips the token, closes a streaming channel, sends `cancelled` through `err`, and drops every later chunk. Deadline expiry flips the same token and sends JSON with `kind: \"timeout\"`. Poll `cancelled()` or call `throwIfCancelled()` at bounded intervals; a short cooperative grace period follows the token, and an operation that unwinds inside it keeps its instance (child process or pool instance) warm. Ignoring the token has a per-carrier hard edge: the child is killed and the next request starts a clean host; the in-process carrier abandons that instance's thread, routes the failure, and adds a fresh instance to the pool. The abandoned dispatch keeps its key reserved until it physically stops, so a same-key replacement cannot overlap its side effects. A detected runtime trap routes `kind: \"service_trap\"` and poisons only the instance it fired in.\n\n## Dev loop, authority, and current limits\n\n`native dev --core` imports service modules in an isolated Node worker, verifies the same package hashes, decodes the same request contract, cooperatively interrupts cancellation/deadline work, maps errors through the same arms, and emits stream chunks through the same channel-event shape. Use `--script scenario.ndjson --watch` for repeatable logic iterations. The devhost reads `NATIVE_SDK_SESSION_RECORD`/`NATIVE_SDK_SESSION_REPLAY`, writes the native journal format, and starts no service worker during replay. Service-only recordings cross between it and the packaged runtime; packaged recordings containing other effect families must be replayed with `native automate replay`, and devhost rejects those records explicitly. `native dev` runs the compiled service executable beside the real app.\n\nCarrier selection is a build fact: `.service_carrier = \"in_process\" | \"child\"` in app.zon (or `-Dservice-carrier`). Unset/`\"auto\"` preserves the child carrier on desktop and resolves to the in-process pool on iOS/Android (mobile apps cannot spawn a sibling process; an explicit `\"child\"` there is refused with a teaching). The in-process archive is available for native Linux, cross-Linux x86_64/aarch64 (`aarch64-linux-android` included), native Windows x86_64, cross-Windows x86_64 GNU, and the Mach-O targets — macOS, `aarch64-ios`, `aarch64-ios-simulator` — built on macOS. `.service_pool_size` (or `-Dservice-pool-size`, 1–16) sets the in-process pool width; the default is min(4, cores).\n\nAuthority differs by carrier. The child process runs with the app's privileges and app-data cwd, receives an explicit environment allowlist (path, user/home/temp, locale/time zone, certificate, and proxy variables; platform equivalents on Windows) with `NATIVE_SDK_*` internals stripped, and its stdout belongs to framed transport (diagnostics go to stderr). In-process services share the app process outright: the app's full environment and working directory, no transport-owned stdout — and a hardware fault (stack overflow) or `process.exit()` in service code takes the app with it, so the child carrier remains the fully isolated option. Each pool worker owns a separate service-module instance, so mutable module globals are worker-local and requests on different keys may observe different copies; keep shared durable state outside module globals. Either way this authority is why service work never runs synchronously inside `update`.\n\nCurrent limits:\n\n- child executables are desktop-only and use the broad compiler matrix (same-platform builds, Linux/Windows GNU cross-targets from desktop hosts, and macOS targets from macOS), while in-process archives follow the localization matrix above (mobile targets are archive-only: iOS from a macOS host at the 15.0 floor, Android from any desktop host with an NDK at the API 26 floor); Windows MSVC builds natively on a matching Windows host, while cross-Windows uses GNU because Zig supplies that target's CRT and system libraries; any explicitly spelled Linux `-gnu` target — even one matching the build host — must also spell its glibc (`x86_64-linux-gnu.2.36` or later, or `-musl`) because Zig otherwise selects the runtime's too-old default floor;\n- synchronous static-tier operations only—async services wait for the next compiler/runtime phase;\n- no JavaScript engine or dynamic npm fallback;\n- storage remains engine-owned effects, not a service-layer database shortcut;\n- custom widgets, renderers, platform integration, and new engine capabilities remain Zig toolkit extensions.\n","isInternal":false,"tokens":2479,"sizeBytes":11650},{"name":"SKILL.md","path":"skill-data/zig/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/skill-data/zig/SKILL.md","title":"zig","category":"anthropic-skill","format":"markdown","content":"---\nname: zig\ndescription: Zig 0.16 idioms for Native SDK code, indexed by compile error. Load when `zig build` fails on std APIs with errors like \"struct 'fs' has no member named 'cwd'\", \"struct 'array_list.Aligned(u8,null)' has no member named 'init'\", \"struct 'std' has no member named 'io'\", \"no member named 'GeneralPurposeAllocator'\", \"no member named 'getEnvMap'\", or \"invalid format string\" - the signature of code written for Zig 0.15 or earlier. Covers main(std.process.Init), std.Io file IO and writers, ArrayList, process spawning, environment, clocks and sleep, sockets, custom formatting, and build.zig module shapes, each as this SDK writes them.\n---\n\n# Zig 0.16 for Native SDK code\n\nThe Native SDK requires Zig 0.16.0 (`minimum_zig_version` in `build.zig.zon`; the CLI pins the same version and offers a checksum-verified download into `~/.native/toolchains/` when the `zig` on PATH does not match). Training data and older guides teach Zig 0.15 idioms, and 0.16 moved everything that touches the outside world — files, stdout, clocks, sleeping, process spawning, sockets — behind an explicit `std.Io` value, while containers became allocator-per-call. Each section below is headed by the exact compile error the old idiom produces, so search this file by error text.\n\nTwo rules resolve most failures:\n\n- Operations on the outside world take a `std.Io` first (or right after the receiver). Get one from `init.io` in `main(init: std.process.Init)`, from `std.testing.io` in tests, or from `std.Io.Threaded` in code with no `Init` to thread through.\n- Containers are unmanaged: initialize with `.empty` and pass the allocator to every mutating call.\n\nIn a UiApp, `update` never sees an `Io` — persistence, record and relational storage, secure credentials, subprocesses, HTTP, clocks, and timers go through the typed effects channel (`fx.persist`, `fx.storeSet`/`storeGet`/`storeDelete`/`storeScan`/`storeSetMany`, `fx.dbQuery`/`dbExec`, `fx.credentialsSet`/`credentialsGet`/`credentialsDelete`, `fx.readFile`/`appendFile`/`statFile`/`deleteFile`/streaming file verbs, `fx.spawn`, `fx.fetch`, `fx.wallMs`, `fx.startTimer`; see `native skills get native-ui`). Whole-file IO stays capped at 1 MiB; streamed reads deliver 256-KiB chunks, and streamed writes finalize atomically on close. External raw paths require the `filesystem` permission, while the app's six resolved directories are exempt after symlink-safe canonicalization. Raw `std.Io` belongs in `main`, tests, and standalone tools. Record-store apps declare `\"store\"`; relational apps declare `\"sqlite\"`; credentials require both the `\"credentials\"` capability and permission. Credential Msg arms carry `EffectCredentialsResult`; consume successful get bytes immediately and never retain them in Model. The engine owns database paths and app-scoped credential namespaces, while every result still returns as a Msg and crosses the replay journal (credential bytes are redacted). Use `TestHarness().createWithRelationalStore` for a hermetic in-memory relational database and `TestHarness().createWithCredentials` for a hermetic in-memory credential store. And because Zig analyzes lazily, a 0.15-ism can hide in code only one build path references: run BOTH `zig build` and `zig build test` before calling a change done.\n\n## error: struct 'heap' has no member named 'GeneralPurposeAllocator' — allocators come from `main(init: std.process.Init)`\n\nZig 0.15 and earlier:\n\n```zig\npub fn main() !void {\n    var gpa = std.heap.GeneralPurposeAllocator(.{}){};\n    defer _ = gpa.deinit();\n    const allocator = gpa.allocator();\n}\n```\n\nZig 0.16: `main` takes `std.process.Init`, which carries the process-wide allocators, the `Io`, the environment, and the args — nothing to construct or deinit:\n\n```zig\npub fn main(init: std.process.Init) !void {\n    const gpa = init.gpa;                       // general purpose, leak-checked in Debug\n    const arena = init.arena.allocator();       // process-lifetime arena, freed on exit\n    const io = init.io;                         // the Io every std call below wants\n    const args = try init.minimal.args.toSlice(arena);\n    if (init.environ_map.get(\"HOME\")) |home| { ... }\n}\n```\n\nThe CLI's own entry point is the live reference: `tools/native-sdk/main.zig`. Generated **Zig-core** apps have this shape — `main(init: std.process.Init)` passes `init` through to `runner.runWithOptions(app, options, init)` and `init.io` to the markup hot-reload watcher (see the older Zig-core examples' `src/main.zig`). Default TypeScript apps have no authored Zig entry point. `std.heap.page_allocator` still exists for allocations that live for the whole process. Library code that cannot take an `Init` builds its own `Io`: `var threaded = std.Io.Threaded.init(allocator, .{});` then `threaded.io()` (`src/platform/macos/root.zig`).\n\n## error: struct 'fs' has no member named 'cwd' — file IO moved to `std.Io.Dir`\n\n`std.fs.cwd()`, `std.fs.File`, and `std.fs.selfExePath` are gone (`std.fs` retains only `path` helpers and deprecated aliases). The directory handle is `std.Io.Dir`, and every operation takes `io` right after the receiver:\n\n```zig\nconst cwd = std.Io.Dir.cwd();\nconst content = try cwd.readFileAlloc(io, \"app.zon\", allocator, .limited(1024 * 1024));\ndefer allocator.free(content);\ntry cwd.writeFile(io, .{ .sub_path = \"out.txt\", .data = bytes });\nvar file = try cwd.openFile(io, path, .{});\ndefer file.close(io);\ntry cwd.deleteTree(io, \"zig-out/tmp\");\n```\n\nNote the `readFileAlloc` argument order (`io`, path, allocator, limit) and the size limit as `std.Io.Limit` — `.limited(n)` or `.unlimited`. Live references: `tools/native-sdk/skills.zig` (readFileAlloc), `src/tooling/manifest.zig` (openFile), `src/tooling/cef.zig` tests (writeFile with `std.testing.io`). Path buffers size with `std.Io.Dir.max_path_bytes`. For the executable's own path, `std.fs.selfExePath(&buf)` became `std.process.executablePath(io, &buf)`, returning the length (`tools/native-sdk/skills.zig`).\n\n## error: struct 'std' has no member named 'io' — writers are `std.Io.Writer`, stdout is `std.Io.File.stdout()`\n\n`std.io.getStdOut().writer()` and `std.io.fixedBufferStream` are gone. Stdout takes `io` plus a caller-owned buffer, and the printable interface lives one field deep:\n\n```zig\nvar stdout_buffer: [4096]u8 = undefined;\nvar stdout_writer = std.Io.File.stdout().writer(io, &stdout_buffer);\nconst stdout = &stdout_writer.interface;\ndefer stdout.flush() catch {};\ntry stdout.print(\"{s}\\n\", .{message});\n```\n\nForgetting `flush()` means no output — the buffer is yours. `std.debug.print` is unchanged and needs no `io`; prefer it for diagnostics. The `fixedBufferStream` replacement is a fixed writer over a stack buffer, and the written bytes come back from `buffered()`:\n\n```zig\nvar buf: [256]u8 = undefined;\nvar writer = std.Io.Writer.fixed(&buf);\ntry writer.print(\"n={d}\", .{42});\nconst written = writer.buffered();\n```\n\nFor building a string of unknown size, use an allocating writer instead of an ArrayList writer: `var w = std.Io.Writer.Allocating.init(allocator); defer w.deinit();` then print through `w.writer` (`src/automation/server.zig`). One-shot formatting still has `std.fmt.bufPrint` and `std.fmt.allocPrint`, unchanged. Functions that accept a writer take `*std.Io.Writer`, not `anytype` (`tools/native-sdk/skills.zig`; fixed-writer reference: `src/tooling/doctor.zig`).\n\n## error: struct 'array_list.Aligned(u8,null)' has no member named 'init' — ArrayList is unmanaged\n\n`std.ArrayList(T).init(allocator)` is gone; the list no longer stores its allocator. Initialize with `.empty` and pass the allocator to every call that can allocate or free:\n\n```zig\nvar list: std.ArrayList(u8) = .empty;\ndefer list.deinit(allocator);\ntry list.append(allocator, 'a');\ntry list.appendSlice(allocator, \"bc\");\nconst owned = try list.toOwnedSlice(allocator);\n```\n\nThis is everywhere in the SDK — `src/tooling/templates.zig` builds every generated file this way, `tools/native-sdk/skills.zig` collects skills this way. Passing a managed-style call (`list.append('a')`) fails with \"member function expected 2 argument(s), found 1\". `std.StringHashMap`/`std.AutoHashMap` still have managed `.init(allocator)` forms; only explicitly `Unmanaged` maps take `.empty` (`tools/native-sdk/markup_lsp.zig`).\n\n## error: invalid format string 's' for type — enums print with `{t}`, format methods with `{f}`\n\n`{s}` is for strings only. Enums and tagged unions print their tag name with `{t}` (equivalent to `@tagName`, which also still works):\n\n```zig\nstd.debug.print(\"native {t}: `zig build` step failed (exit code {d})\\n\", .{ verb, code });\n```\n\n(`src/tooling/verbs.zig`.) Custom format methods changed shape twice over: the old four-parameter signature no longer compiles (`error: struct 'fmt' has no member named 'FormatOptions'`), and `{}` no longer calls a `format` method at all — it prints the default field dump silently. Declare the 0.16 signature and print with `{f}`:\n\n```zig\npub fn format(self: Point, writer: *std.Io.Writer) std.Io.Writer.Error!void {\n    try writer.print(\"({d},{d})\", .{ self.x, self.y });\n}\n// call site\ntry writer.print(\"{f}\\n\", .{point});\n```\n\n## error: struct 'time' has no member named 'sleep' / 'milliTimestamp' — clocks live on Io\n\n`std.time` keeps only the unit constants (`ns_per_ms`, `ms_per_s`, ...) and epoch helpers. Sleeping and reading clocks take `io` and typed durations:\n\n```zig\ntry std.Io.sleep(io, std.Io.Duration.fromMilliseconds(50), .awake);\nconst now_ns: i128 = std.Io.Timestamp.now(io, .real).nanoseconds;\n```\n\n(`src/runtime/automation_liveness_tests.zig`, `src/automation/server.zig`.) In app code, do not reach for either: `native_sdk.nowMs()` / `native_sdk.monotonicMs()` are the facade, `fx.wallMs()` is the journaled read inside `update`, and `model.clock` is the testable seam — the native-ui skill's Time section owns that pattern.\n\n## error: struct 'process.Child' has no member named 'init' — spawning is `std.process.spawn(io, ...)`\n\nThe two-step `Child.init` + `child.spawn()` collapsed into one call, and lifecycle methods take `io`:\n\n```zig\nvar child = try std.process.spawn(io, .{\n    .argv = &.{ \"npm\", \"run\", \"build\" },\n    .stdin = .ignore,\n    .stdout = .inherit,\n    .stderr = .inherit,\n});\nconst term = try child.wait(io);   // child.kill(io) to stop it\n```\n\n(`src/tooling/verbs.zig`, `src/tooling/dev.zig` — the latter also shows passing a custom environment via `.environ_map`.) Inside a UiApp, spawn through the effects channel (`fx.spawn`) instead — it is bounded, journaled, and delivers exit/output as Msgs.\n\n## error: struct 'process' has no member named 'getEnvMap' / 'argsAlloc' — environment and args come from Init\n\n`std.process.getEnvMap`, `argsAlloc`, and `argsWithAllocator` are gone. `main` already has both: `init.environ_map` (a `*std.process.Environ.Map`) and `init.minimal.args.toSlice(allocator)` (`tools/native-sdk/main.zig`). Building an environment from scratch — for a child process, or in tests — is `var env = std.process.Environ.Map.init(allocator); defer env.deinit(); try env.put(\"KEY\", \"value\");` (`src/tooling/dev.zig`).\n\n## error: struct 'mem' has no member named 'trimRight' / 'trimLeft' — renamed `trimEnd` / `trimStart`\n\nThe directional trims renamed to match the JS/string convention; arguments are unchanged:\n\n```zig\nconst line = std.mem.trimEnd(u8, raw, \"\\r\\n\");     // was trimRight\nconst body = std.mem.trimStart(u8, line, \" \\t\");   // was trimLeft\nconst both = std.mem.trim(u8, text, \" \");          // unchanged\n```\n\n## error: struct 'std' has no member named 'net' — sockets live on `std.Io.net`\n\n`std.net.Address` became `std.Io.net.IpAddress`, and connect/listen take `io`: `std.Io.net.IpAddress.resolve(io, host, port)`, then `IpAddress.connect(&address, io, .{ .mode = .stream, .protocol = .tcp })`; stream readers/writers follow the buffered-writer shape (`std.Io.net.Stream.writer(stream, io, &buffer)` then `.interface`). Live reference: `src/tooling/dev.zig` (`waitUntilReady`/`httpReady`). App-level HTTP belongs in `fx.fetch`, not hand-rolled sockets.\n\n## error: no field named 'root_source_file' in struct 'Build.ExecutableOptions' — build.zig artifacts take modules\n\nZero-config apps have no `build.zig` — the CLI owns the build graph, so this only appears in apps that ejected or scaffolded `--full`. Artifacts take a module, and the module owns root source, target, and optimize:\n\n```zig\nconst exe = b.addExecutable(.{\n    .name = \"app\",\n    .root_module = b.createModule(.{\n        .root_source_file = b.path(\"src/main.zig\"),\n        .target = target,\n        .optimize = optimize,\n    }),\n});\nconst tests = b.addTest(.{ .root_module = app_mod });\n```\n\nThe generated ejected `build.zig` is the reference shape (`native eject`, emitted by `src/tooling/templates.zig`); the SDK's own `build.zig` uses the same pattern throughout.\n\n## Reading files and streams incrementally\n\n`file.reader()` with no arguments is gone. A reader takes `io` and a buffer, and the stream interface lives on `.interface`:\n\n```zig\nvar read_buffer: [4096]u8 = undefined;\nvar reader = file.reader(io, &read_buffer);\nconst bytes = try reader.interface.allocRemaining(allocator, .limited(1024 * 1024));\nconst line = try reader.interface.takeDelimiterExclusive('\\n');\n```\n\n(`src/tooling/manifest.zig` for allocRemaining, `src/tooling/toolchain.zig` for line reading from stdin.) For whole-file reads, prefer `readFileAlloc` above.\n\n## Tests: the Io is `std.testing.io`\n\nTests never construct an `Io` — `std.testing.io` is the canonical one, next to `std.testing.allocator`:\n\n```zig\ntest \"reads the manifest\" {\n    var tmp = std.testing.tmpDir(.{});\n    defer tmp.cleanup();\n    try tmp.dir.writeFile(std.testing.io, .{ .sub_path = \"app.zon\", .data = source });\n    const content = try tmp.dir.readFileAlloc(std.testing.io, \"app.zon\", std.testing.allocator, .limited(4096));\n    defer std.testing.allocator.free(content);\n}\n```\n\n(`src/tooling/cef.zig` and `src/tooling/manifest.zig` tests.) Model/Msg/update tests need no `Io` at all — the markup-view testing pattern in the native-ui skill is pure.\n\n## Unchanged — do not \"migrate\" these\n\n`std.fmt.bufPrint` / `allocPrint` / `parseInt` / `parseFloat`, `std.mem.*`, `std.debug.print`, `std.heap.page_allocator`, `std.time.ns_per_*` and `ms_per_*` constants, `@embedFile`, `std.testing.expect*`, and managed `std.StringHashMap` / `std.AutoHashMap` all work as before. If code using only these fails, the problem is elsewhere.\n","frontmatter":{"name":"zig","description":"Zig 0.16 idioms for Native SDK code, indexed by compile error. Load when `zig build` fails on std APIs with errors like \"struct 'fs' has no member named 'cwd'\", \"struct 'array_list.Aligned(u8,null)' has no member named 'init'\", \"struct 'std' has no member named 'io'\", \"no member named 'GeneralPurposeAllocator'\", \"no member named 'getEnvMap'\", or \"invalid format string\" - the signature of code written for Zig 0.15 or earlier. Covers main(std.process.Init), std.Io file IO and writers, ArrayList, process spawning, environment, clocks and sleep, sockets, custom formatting, and build.zig module shapes, each as this SDK writes them."},"isInternal":false,"tokens":3714,"sizeBytes":14513},{"name":"SKILL.md","path":"skills/native-sdk/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/skills/native-sdk/SKILL.md","title":"native-sdk","category":"anthropic-skill","format":"markdown","content":"---\nname: native-sdk\ndescription: Discovery skill for the Native SDK, the complete toolkit for building native desktop applications. Apps are authored in TypeScript + declarative Native markup (.native) by default and compiled to native code with no JS runtime in the binary; Zig cores are an explicit alternative, and WebViews are the optional web-content path. Use when the user asks what the Native SDK is, how to build a Native SDK app, author native UI, scaffold an app, configure app.json or legacy app.zon, add bridge commands, embed web content, package an app, test a running app, or automate a Native SDK app.\nallowed-tools: Bash(native:*), Bash(npx @native-sdk/cli:*)\nhidden: true\n---\n\n# Native SDK\n\nThe Native SDK is the complete toolkit for building native desktop applications. **The primary authoring path is TypeScript app logic in `src/core.ts` plus declarative Native markup in `.native` files.** The TypeScript core is checked and compiled ahead of time to native code, so the shipped binary contains no browser, WebView, JS runtime, or interpreter. Zig is how the toolkit itself works and is a first-class, explicitly chosen app-core alternative (`--template zig-core`); it is not the default inferred from the SDK's implementation. Every app embeds a deterministic automation server, so agents can snapshot, drive, and screenshot the running window. Desktop is the mature surface (macOS deepest, Linux and Windows exercised in CI); mobile embedding is experimental. WebView surfaces coexist as the optional path for embedding web content or hosting an existing web frontend.\n\n## Start here\n\nThis file is a discovery stub for agents that installed the Native SDK once with a skills installer such as `npx skills add native-sdk`. Before implementing or explaining Native SDK app work, use the installed CLI to discover and load the current skill content:\n\n```bash\nnative skills list\nnative skills get core\nnative skills get native-ui\nnative skills get ts-core\n```\n\nUse `native skills get core` for initial orientation. For the default app-authoring path, load **both** `native-ui` (views, bindings, the app loop) and `ts-core` (the TypeScript subset, effects, subscriptions, and modules) before implementing. Load `ts-services` too when the tree has `src/services/` or ordinary TypeScript needs filesystem/process/JSON/regex/Map/Date/class work behind `Cmd.request`. Use `native skills get core --full` when work reaches lower-level runtime wiring, WebViews, bridge/security, native capabilities, packaging, or debugging. Use `native skills get automation` when testing a running app, taking snapshots, requesting reloads, or using the built-in automation server. Use `native skills get zig` only for an existing Zig core, a toolkit extension, SDK implementation work, or a Zig 0.16 compile error.\n\n## Quick orientation\n\n```bash\nnpm install -g @native-sdk/cli\nnative init my_app\ncd my_app\nnative dev\n```\n\n`native init my_app` generates the primary three-file app: `app.json`, `src/app.native` (the markup view), and `src/core.ts` (`Model`, `Msg`, `update`). Existing `app.zon` manifests remain supported. Inspect the tree before editing an existing app and preserve the core language it already uses. `src/main.zig` means the app explicitly uses the Zig-core template; web-frontend shells additionally carry `frontend/` and usually owned build/runtime wiring.\n","frontmatter":{"name":"native-sdk","description":"Discovery skill for the Native SDK, the complete toolkit for building native desktop applications. Apps are authored in TypeScript + declarative Native markup (.native) by default and compiled to native code with no JS runtime in the binary; Zig cores are an explicit alternative, and WebViews are the optional web-content path. Use when the user asks what the Native SDK is, how to build a Native SDK app, author native UI, scaffold an app, configure app.json or legacy app.zon, add bridge commands, embed web content, package an app, test a running app, or automate a Native SDK app.","allowed-tools":"Bash(native:*), Bash(npx @native-sdk/cli:*)","hidden":true,"allowedTools":["Bash(native:*),","Bash(npx","@native-sdk/cli:*)"]},"allowedTools":["Bash(native:*),","Bash(npx","@native-sdk/cli:*)"],"isInternal":false,"tokens":722,"sizeBytes":3379},{"name":"agents.md","path":"tools/guest-mac/agents.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/tools/guest-mac/agents.md","title":"Guest-mac Skill","category":"anthropic-skill","format":"markdown","content":"# Guest-mac Skill\n\nPath: `tools/guest-mac/agents.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/tools/guest-mac/agents.md)","tokens":300,"sizeBytes":167},{"name":"README.md","path":"packages/native-sdk/npm/darwin-arm64/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/darwin-arm64/README.md","title":"darwin-arm64 Documentation","category":"plugin-manifest","format":"markdown","content":"# darwin-arm64 Documentation\n\nPath: `packages/native-sdk/npm/darwin-arm64/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/darwin-arm64/README.md)","tokens":300,"sizeBytes":220},{"name":"README.md","path":"packages/native-sdk/npm/darwin-x64/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/darwin-x64/README.md","title":"darwin-x64 Documentation","category":"plugin-manifest","format":"markdown","content":"# darwin-x64 Documentation\n\nPath: `packages/native-sdk/npm/darwin-x64/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/darwin-x64/README.md)","tokens":300,"sizeBytes":214},{"name":"README.md","path":"packages/native-sdk/npm/linux-arm64-gnu/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-arm64-gnu/README.md","title":"linux-arm64-gnu Documentation","category":"plugin-manifest","format":"markdown","content":"# linux-arm64-gnu Documentation\n\nPath: `packages/native-sdk/npm/linux-arm64-gnu/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-arm64-gnu/README.md)","tokens":300,"sizeBytes":229},{"name":"README.md","path":"packages/native-sdk/npm/linux-arm64-musl/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-arm64-musl/README.md","title":"linux-arm64-musl Documentation","category":"plugin-manifest","format":"markdown","content":"# linux-arm64-musl Documentation\n\nPath: `packages/native-sdk/npm/linux-arm64-musl/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-arm64-musl/README.md)","tokens":300,"sizeBytes":232},{"name":"README.md","path":"packages/native-sdk/npm/linux-x64-gnu/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-x64-gnu/README.md","title":"linux-x64-gnu Documentation","category":"plugin-manifest","format":"markdown","content":"# linux-x64-gnu Documentation\n\nPath: `packages/native-sdk/npm/linux-x64-gnu/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-x64-gnu/README.md)","tokens":300,"sizeBytes":223},{"name":"README.md","path":"packages/native-sdk/npm/linux-x64-musl/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-x64-musl/README.md","title":"linux-x64-musl Documentation","category":"plugin-manifest","format":"markdown","content":"# linux-x64-musl Documentation\n\nPath: `packages/native-sdk/npm/linux-x64-musl/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/linux-x64-musl/README.md)","tokens":300,"sizeBytes":226},{"name":"README.md","path":"packages/native-sdk/npm/win32-arm64/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/win32-arm64/README.md","title":"win32-arm64 Documentation","category":"plugin-manifest","format":"markdown","content":"# win32-arm64 Documentation\n\nPath: `packages/native-sdk/npm/win32-arm64/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/win32-arm64/README.md)","tokens":300,"sizeBytes":217},{"name":"README.md","path":"packages/native-sdk/npm/win32-x64/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/win32-x64/README.md","title":"win32-x64 Documentation","category":"plugin-manifest","format":"markdown","content":"# win32-x64 Documentation\n\nPath: `packages/native-sdk/npm/win32-x64/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/npm/win32-x64/README.md)","tokens":300,"sizeBytes":211},{"name":"README.md","path":"packages/native-sdk/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/README.md","title":"native-sdk Documentation","category":"plugin-manifest","format":"markdown","content":"# native-sdk Documentation\n\nPath: `packages/native-sdk/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/packages/native-sdk/README.md)","tokens":300,"sizeBytes":184},{"name":"README.md","path":"tools/guest-mac/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/tools/guest-mac/README.md","title":"guest-mac Documentation","category":"plugin-manifest","format":"markdown","content":"# guest-mac Documentation\n\nPath: `tools/guest-mac/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/tools/guest-mac/README.md)","tokens":300,"sizeBytes":175},{"name":"README.md","path":"tools/linux-truth/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/tools/linux-truth/README.md","title":"linux-truth Documentation","category":"plugin-manifest","format":"markdown","content":"# linux-truth Documentation\n\nPath: `tools/linux-truth/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/tools/linux-truth/README.md)","tokens":300,"sizeBytes":181},{"name":"README.md","path":"tools/windows-truth/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/native/HEAD/tools/windows-truth/README.md","title":"windows-truth Documentation","category":"plugin-manifest","format":"markdown","content":"# windows-truth Documentation\n\nPath: `tools/windows-truth/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/tools/windows-truth/README.md)","tokens":300,"sizeBytes":187},{"name":".cursorrules","path":".cursorrules (knative-istio-typesense-gpu-cursorrules-prompt-fil.mdc)","rawUrl":"https://raw.githubusercontent.com/PatrickJS/awesome-cursorrules/main/rules/knative-istio-typesense-gpu-cursorrules-prompt-fil.mdc","title":"Awesome Cursorrules: knative istio typesense gpu cursorrules prompt fil","category":"cursor-rule","format":"markdown","content":"---\ndescription: \"Cursor rules for Knative development with Istio, Typesense, and GPU integration.\"\nglobs: **/*\nalwaysApply: false\n---\nYou are an expert AI programming assistant specializing in building Knative, Istio, Typesense, htmx and GPU accelerated applications.\n\nAs an AI assistant, your role is to provide guidance, code snippets, explanations, and troubleshooting support throughout the development process. You should be prepared to assist with all aspects of the project, from architecture design to implementation details.\n\n1. Knative\n  - Provide guidance on creating and managing Knative services\n  - Assist with serverless deployment configurations\n  - Help optimize autoscaling settings\n\n2. Istio\n  - Offer advice on service mesh configuration\n  - Help set up traffic management, security, and observability features\n  - Assist with troubleshooting Istio-related issues\n\n3. Typesense\n  - Provide guidance on Typesense setup and configuration\n  - Assist with index creation and search query optimization\n  - Help integrate Typesense with the backend API\n\n4. Frontend Development\n  - Offer suggestions for improving the HTMX-based frontend\n  - Assist with responsive design and user experience enhancements\n  - Help with client-side performance optimization\n\n5. Backend Development\n  - Guide the creation of serverless functions for the backend API\n  - Assist with integrating all components (htmx, Typesense)\n  - Help optimize API performance and error handling\n\n6. Testing and Monitoring\n  - Guide the creation of test cases for each component\n  - Assist with setting up monitoring and logging\n  - Help interpret performance metrics and suggest optimizations\n\n1. Always consider the serverless nature of the application when providing advice.\n2. Prioritize scalability, performance, and user experience in your suggestions.\n3. Explain complex concepts clearly, assuming the user has basic knowledge of the technologies involved.\n4. Offer alternative approaches or solutions when appropriate.\n5. Be prepared to dive deep into documentation or specifications of the used technologies if needed.\n6. Encourage best practices in cloud-native application development.\n7. When unsure about specific implementation details, clearly state assumptions and provide general guidance.\n\nAlways prioritize security, scalability, and maintainability in your designs and implementations. Leverage the power and simplicity of knative to create efficient and idiomatic code.\n\nProject-Specific Notes\n\n1. The frontend uses HTMX for simplicity. Suggest improvements while maintaining this approach.\n2. The backend should be implemented as Knative services.\n3. Typesense is the primary search engine. Focus on its strengths for fast, typo-tolerant searching.\n4. Istio should be leveraged for inter-service communication, security, and monitoring.\n\nRemember, your goal is to guide the development process, provide helpful insights, and assist in creating a robust, scalable, and efficient AI-powered search application.\n\nThese custom instructions provide a comprehensive guide for Claude to assist you with your AI-powered search project. They cover the key components of your system and outline the areas where you might need assistance.\n","tokens":615,"sizeBytes":3230}],"systemPromptSnippet":"<yakaai_skills repo=\"vercel-labs/native\">\n<!-- File: AGENTS.md (Tokens: ~843 | Category: root-instruction) -->\n# Agent Guide\n\nGuidance for agents (and humans) working on this repository.\n\n## App authoring default\n\nNative SDK itself is implemented in Zig, but Native SDK **apps are authored in TypeScript + Native markup by default**. Do not infer the app-authoring language from this repository's implementation language or from older Zig-core examples.\n\n- For a new app, use `native init <path>` and expect `src/core.ts`, `src/app.native`, and `app.json`. `app.zon` remains a supported legacy/alternative manifest. Ordinary compiled TypeScript work that needs filesystem, process, JSON, regex, classes, or other static-tier APIs belongs under optional `src/services/`, reached from the core with `Cmd.request`; do not import a service from the core. Do not add Zig app code unless the user explicitly chooses `--template zig-core` or the feature requires a toolkit extension.\n- Before changing an existing app, inspect its tree. A `src/core.ts` app stays TypeScript; a `src/main.zig` app stays Zig unless the task is specifically a migration.\n- For default app work, read `skill-data/native-ui/SKILL.md` and `skill-data/ts-core/SKILL.md`; also read `skill-data/ts-services/SKILL.md` when the tree has `src/services/` or the task needs ordinary TypeScript beyond the core subset. `skill-data/core/SKILL.md` covers shared/runtime concerns; `skill-data/zig/SKILL.md` is for Zig-core apps and SDK implementation work.\n- The `-ts` suffix on a few examples only distinguishes ports from older Zig originals. New TypeScript apps need no suffix because TypeScript is the default.\n\n## Build, test, and gate\n\n```bash\nzig build test                # root engine + runtime suites\nzig build validate            # framework's legacy app.zon manifest check\nzig build test-example-<name> # one example's suite (e.g. test-example-notes)\nscripts/gate.sh fast [ref]    # affected-only local gate for your diff (default base: main)\nscripts/gate.sh full          # everything CI-shaped that runs locally\n```\n\nRun `scripts/gate.sh fast` before finishing any change; it maps your diff to the suites that cover it. The docs site checks with `pnpm --dir docs check` (the gate runs it only when `docs/` changed).\n\nThis repository builds with Zig 0.16.0. If a build fails with \"no member named\" errors on std APIs (`std.fs.cwd`, `ArrayList.init`, `std.io`), you are writing pre-0.16 idioms — `skill-data/zig/SKILL.md` maps each such compile error to the current idiom as this codebase writes it.\n\nPinned goldens (pixel signatures, schema fingerprints, command counts) are updated deliberately: review the rendered output or the counted commands first, and keep the pin's comment a self-contained description of what the value represents.\n\n## Changelog\n\nDo not edit `CHANGELOG.md` as part of regular feature or fix work. The release agent reviews the git history since the previous release and writes the complete changelog entry during release preparation; see [RELEASING.md](./RELEASING.md).\n\n## Where things live\n\n- `src/` — the engine and runtime; `src/primitives/canvas/` holds the widget, markup, and vector core.\n- `apps/schema/` — the standalone static Vercel project for `schema.native-sdk.dev`.\n- `examples/` — the showcase apps, many predating the JSON default (`app.zon` + `src/`).\n- `docs/` — the documentation site; `docs/AGENTS.md` has its MDX conventions.\n- `skills/` and `skill-data/` — the agent skills the CLI ships (`native skills list`).\n- `tools/` and `scripts/` — dev tooling and the local gate.\n\nReleases are maintainer-run; see [RELEASING.md](./RELEASING.md).\n\n<!-- File: docs/AGENTS.md (Tokens: ~379 | Category: root-instruction) -->\n# Docs Site Conventions\n\n## MDX Tables\n\nAlways use HTML `<table>` syntax in MDX pages, never markdown pipe tables. This ensures consistent styling and avoids MDX parsing edge cases.\n\n```html\n<table>\n  <thead>\n    <tr>\n      <th>Column</th>\n      <th>Description</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>field</code></td>\n      <td>What it does</td>\n    </tr>\n  </tbody>\n</table>\n```\n\n## Geistdocs\n\nThe site uses the public `@vercel/geistdocs` package with Labs branding. Write pages in `content/docs` with a frontmatter `title` and no duplicate H1. Order pages and groups with `content/docs/meta.json` and folder metadata. Keep `/docs/<slug>` URLs and the permanent legacy redirects in `next.config.mjs` stable.\n\nShared navigation, search, page actions, Markdown routes, MDX rendering, and theme behavior belong to Geistdocs. Local adapters live under `src/components/geistdocs` and `src/lib/geistdocs`. Native component previews and the persisted TypeScript/Zig code tabs remain app-owned. `src/lib/remark-native.ts` exports their static data for Markdown; do not evaluate MDX expressions. Preserve `language:filename` fences and existing heading IDs through `src/lib/remark-docs.ts`.\n\nReuse an existing dev server. Run checks with `NEXT_DIST_DIR=.next-check pnpm check` so production checks do not share its `.next` directory. The check builds the site, verifies all canonical pages and Markdown siblings over HTTP, checks legacy redirects and discovery, and checks language tabs and the preview WASM. Also run `scripts/gate.sh fast` from the repository root before finishing.\n\n<!-- File: evals/cases/brand-icon-package/eval.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Brand-icon-package Skill\n\nPath: `evals/cases/brand-icon-package/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/brand-icon-package/eval.json)\n\n<!-- File: evals/cases/chat-composer/eval.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Chat-composer Skill\n\nPath: `evals/cases/chat-composer/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/chat-composer/eval.json)\n\n<!-- File: evals/cases/disk-status-spans/eval.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Disk-status-spans Skill\n\nPath: `evals/cases/disk-status-spans/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/disk-status-spans/eval.json)\n\n<!-- File: evals/cases/dual-feed-table/eval.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Dual-feed-table Skill\n\nPath: `evals/cases/dual-feed-table/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/dual-feed-table/eval.json)\n\n<!-- File: evals/cases/dual-ledger-csv-split/eval.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Dual-ledger-csv-split Skill\n\nPath: `evals/cases/dual-ledger-csv-split/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/dual-ledger-csv-split/eval.json)\n\n<!-- File: evals/cases/dual-list-delete-fix/eval.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Dual-list-delete-fix Skill\n\nPath: `evals/cases/dual-list-delete-fix/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/dual-list-delete-fix/eval.json)\n\n<!-- File: evals/cases/dual-notes-autosave/eval.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Dual-notes-autosave Skill\n\nPath: `evals/cases/dual-notes-autosave/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/dual-notes-autosave/eval.json)\n\n<!-- File: evals/cases/dual-pomodoro-timer/eval.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Dual-pomodoro-timer Skill\n\nPath: `evals/cases/dual-pomodoro-timer/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/dual-pomodoro-timer/eval.json)\n\n<!-- File: evals/cases/dual-sysinfo-panel/eval.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Dual-sysinfo-panel Skill\n\nPath: `evals/cases/dual-sysinfo-panel/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/dual-sysinfo-panel/eval.json)\n\n<!-- File: evals/cases/expenses-table/eval.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Expenses-table Skill\n\nPath: `evals/cases/expenses-table/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/expenses-table/eval.json)\n\n<!-- File: evals/cases/file-browser-panes/eval.json (Tokens: ~300 | Category: anthropic-skill) -->\n# File-browser-panes Skill\n\nPath: `evals/cases/file-browser-panes/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/file-browser-panes/eval.json)\n\n<!-- File: evals/cases/habits-tracker/eval.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Habits-tracker Skill\n\nPath: `evals/cases/habits-tracker/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/habits-tracker/eval.json)\n\n<!-- File: evals/cases/inspector-window/eval.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Inspector-window Skill\n\nPath: `evals/cases/inspector-window/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/inspector-window/eval.json)\n\n<!-- File: evals/cases/kanban-board/eval.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Kanban-board Skill\n\nPath: `evals/cases/kanban-board/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/kanban-board/eval.json)\n\n<!-- File: evals/cases/metrics-dashboard/eval.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Metrics-dashboard Skill\n\nPath: `evals/cases/metrics-dashboard/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/metrics-dashboard/eval.json)\n\n<!-- File: evals/cases/playlist-row-actions/eval.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Playlist-row-actions Skill\n\nPath: `evals/cases/playlist-row-actions/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/playlist-row-actions/eval.json)\n\n<!-- File: evals/cases/process-monitor/eval.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Process-monitor Skill\n\nPath: `evals/cases/process-monitor/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/process-monitor/eval.json)\n\n<!-- File: evals/cases/release-dashboard/eval.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Release-dashboard Skill\n\nPath: `evals/cases/release-dashboard/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/release-dashboard/eval.json)\n\n<!-- File: evals/cases/settings-picker/eval.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Settings-picker Skill\n\nPath: `evals/cases/settings-picker/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/settings-picker/eval.json)\n\n<!-- File: evals/cases/templates-settings-app/eval.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Templates-settings-app Skill\n\nPath: `evals/cases/templates-settings-app/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/templates-settings-app/eval.json)\n\n<!-- File: evals/cases/ts-countdown-bugfix/eval.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Ts-countdown-bugfix Skill\n\nPath: `evals/cases/ts-countdown-bugfix/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/ts-countdown-bugfix/eval.json)\n\n<!-- File: evals/cases/ts-expenses-filter/eval.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Ts-expenses-filter Skill\n\nPath: `evals/cases/ts-expenses-filter/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/ts-expenses-filter/eval.json)\n\n<!-- File: evals/cases/ts-habits-core/eval.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Ts-habits-core Skill\n\nPath: `evals/cases/ts-habits-core/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/ts-habits-core/eval.json)\n\n<!-- File: evals/cases/ts-tag-input-core/eval.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Ts-tag-input-core Skill\n\nPath: `evals/cases/ts-tag-input-core/eval.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/cases/ts-tag-input-core/eval.json)\n\n<!-- File: evals/package.json (Tokens: ~166 | Category: anthropic-skill) -->\n{\n  \"name\": \"@native-sdk/evals\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"packageManager\": \"pnpm@10.23.0\",\n  \"scripts\": {\n    \"typecheck\": \"tsc --noEmit\",\n    \"eval\": \"node src/main.ts\",\n    \"metrics\": \"node src/authoring-metrics.ts\",\n    \"check\": \"pnpm typecheck\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^24\",\n    \"typescript\": \"^5\"\n  },\n  \"dependencies\": {\n    \"@vercel/sandbox\": \"^2.3.0\"\n  },\n  \"engines\": {\n    \"node\": \">=24\"\n  }\n}\n\n<!-- File: evals/pnpm-lock.yaml (Tokens: ~2957 | Category: anthropic-skill) -->\nlockfileVersion: '9.0'\n\nsettings:\n  autoInstallPeers: true\n  excludeLinksFromLockfile: false\n\nimporters:\n\n  .:\n    dependencies:\n      '@vercel/sandbox':\n        specifier: ^2.3.0\n        version: 2.3.0\n    devDependencies:\n      '@types/node':\n        specifier: ^24\n        version: 24.13.2\n      typescript:\n        specifier: ^5\n        version: 5.9.3\n\npackages:\n\n  '@types/node@24.13.2':\n    resolution: {integrity: sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==}\n\n  '@vercel/oidc@3.2.0':\n    resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==}\n    engines: {node: '>= 20'}\n\n  '@vercel/sandbox@2.3.0':\n    resolution: {integrity: sha512-KLhc/sj2JuftgWEivPmmDq3iFLIoYU0XIv+44paTyhk5eGGaqNl6dQdxXSqOf+N31jh55m4UWY7i8oJy8oJRrQ==}\n\n  '@workflow/serde@4.1.0-beta.2':\n    resolution: {integrity: sha512-8kkeoQKLDaKXefjV5dbhBj2aErfKp1Mc4pb6tj8144cF+Em5SPbyMbyLCHp+BVrFfFVCBluCtMx+jjvaFVZGww==}\n\n  async-retry@1.3.3:\n    resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==}\n\n  b4a@1.8.1:\n    resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==}\n    peerDependencies:\n      react-native-b4a: '*'\n    peerDependenciesMeta:\n      react-native-b4a:\n        optional: true\n\n  bare-events@2.9.1:\n    resolution: {integrity: sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==}\n    peerDependencies:\n      bare-abort-controller: '*'\n    peerDependenciesMeta:\n      bare-abort-controller:\n        optional: true\n\n  events-universal@1.0.1:\n    resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==}\n\n  fast-fifo@1.3.2:\n    resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}\n\n  jose@6.2.3:\n    resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==}\n\n  jsonlines@0.1.1:\n    resolution: {integrity: sha512-ekDrAGso79Cvf+dtm+mL8OBI2bmAOt3gssYs833De/C9NmIpWDWyUO4zPgB5x2/OhY366dkhgfPMYfwZF7yOZA==}\n\n  ms@2.1.3:\n    resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}\n\n  os-paths@4.4.0:\n    resolution: {integrity: sha512-wrAwOeXp1RRMFfQY8Sy7VaGVmPocaLwSFOYCGKSyo8qmJ+/yaafCl5BCA1IQZWqFSRBrKDYFeR9d/VyQzfH/jg==}\n    engines: {node: '>= 6.0'}\n\n  picocolors@1.1.1:\n    resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}\n\n  retry@0.13.1:\n    resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}\n    engines: {node: '>= 4'}\n\n  streamx@2.28.0:\n    resolution: {integrity: sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==}\n\n  tar-stream@3.1.7:\n    resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==}\n\n  text-decoder@1.2.7:\n    resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==}\n\n  typescript@5.9.3:\n    resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}\n    engines: {node: '>=14.17'}\n    hasBin: true\n\n  undici-types@7.18.2:\n    resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}\n\n  undici@7.28.0:\n    resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==}\n    engines: {node: '>=20.18.1'}\n\n  xdg-app-paths@5.1.0:\n    resolution: {integrity: sha512-RAQ3WkPf4KTU1A8RtFx3gWywzVKe00tfOPFfl2NDGqbIFENQO4kqAJp7mhQjNj/33W5x5hiWWUdyfPq/5SU3QA==}\n    engines: {node: '>=6'}\n\n  xdg-portable@7.3.0:\n    resolution: {integrity: sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw==}\n    engines: {node: '>= 6.0'}\n\n  zod@4.4.3:\n    resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}\n\nsnapshots:\n\n  '@types/node@24.13.2':\n    dependencies:\n      undici-types: 7.18.2\n\n  '@vercel/oidc@3.2.0': {}\n\n  '@vercel/sandbox@2.3.0':\n    dependencies:\n      '@vercel/oidc': 3.2.0\n      '@workflow/serde': 4.1.0-beta.2\n      async-retry: 1.3.3\n      jose: 6.2.3\n      jsonlines: 0.1.1\n      ms: 2.1.3\n      picocolors: 1.1.1\n      tar-stream: 3.1.7\n      undici: 7.28.0\n      xdg-app-paths: 5.1.0\n      zod: 4.4.3\n    transitivePeerDependencies:\n      - bare-abort-controller\n      - react-native-b4a\n\n  '@workflow/serde@4.1.0-beta.2': {}\n\n  async-retry@1.3.3:\n    dependencies:\n      retry: 0.13.1\n\n  b4a@1.8.1: {}\n\n  bare-events@2.9.1: {}\n\n  events-universal@1.0.1:\n    dependencies:\n      bare-events: 2.9.1\n    transitivePeerDependencies:\n      - bare-abort-controller\n\n  fast-fifo@1.3.2: {}\n\n  jose@6.2.3: {}\n\n  jsonlines@0.1.1: {}\n\n  ms@2.1.3: {}\n\n  os-paths@4.4.0: {}\n\n  picocolors@1.1.1: {}\n\n  retry@0.13.1: {}\n\n  streamx@2.28.0:\n    dependencies:\n      events-universal: 1.0.1\n      fast-fifo: 1.3.2\n      text-decoder: 1.2.7\n    transitivePeerDependencies:\n      - bare-abort-controller\n      - react-native-b4a\n\n  tar-stream@3.1.7:\n    dependencies:\n      b4a: 1.8.1\n      fast-fifo: 1.3.2\n      streamx: 2.28.0\n    transitivePeerDependencies:\n      - bare-abort-controller\n      - react-native-b4a\n\n  text-decoder@1.2.7:\n    dependencies:\n      b4a: 1.8.1\n    transitivePeerDependencies:\n      - react-native-b4a\n\n  typescript@5.9.3: {}\n\n  undici-types@7.18.2: {}\n\n  undici@7.28.0: {}\n\n  xdg-app-paths@5.1.0:\n    dependencies:\n      xdg-portable: 7.3.0\n\n  xdg-portable@7.3.0:\n    dependencies:\n      os-paths: 4.4.0\n\n  zod@4.4.3: {}\n\n<!-- File: evals/README.md (Tokens: ~6337 | Category: anthropic-skill) -->\n# Native SDK evals\n\nAn eval harness for AI-agent authoring of Native SDK apps. It formalizes the \"clean-agent trial\": give a fresh agent nothing but a scaffolded workspace, the `native-ui` skill, and a task prompt, then grade what it produced deterministically.\n\nPer case the runner:\n\n1. **Scaffolds** a fresh workspace with the repo's own CLI — `zig build` at the repo root, then `zig-out/bin/native init evals/.workspaces/<case> --frontend native` (`--template zig-core` for the pre-existing native cases and the zig side of dual cases, `--template ts-core` for the ts side) — and delivers each track's skills exactly the way a real user gets them: `native skills get <name>` written to `.claude/skills/<name>/SKILL.md` (`init` does not ship skills). The zig track gets `native-ui` + `zig`; the ts app track gets `ts-core` + `native-ui`; core-only ts-core cases get `ts-core`. The workspace is then **pre-warmed** (`native test` once — workspaces are zero-config, so builds go through the CLI verbs) so the agent's own builds are incremental and its wall-clock isn't spent compiling the SDK.\n2. **Runs the agent-under-test**: `claude -p \"<task prompt>\"` headless in the workspace, routed through the Vercel AI Gateway, with a per-run `CLAUDE_CONFIG_DIR` so no user-level memory/plugins/hooks leak in, `--max-turns`, a wall-clock timeout, and the full `stream-json` transcript captured to `results/`.\n3. **Grades** with deterministic checks: `native test` in the workspace, `native markup check` on the `.native` files, per-case file greps (e.g. \"the board uses `<template>`\"), and live automation-snapshot greps (`native build` with `-Dautomation=true`, launch, wait for the automation snapshot, grep it for expected roles/names).\n4. **Judges** quality the deterministic checks can't see — idiomatic Model/Msg design, template factoring, test meaningfulness — with an `llm_judge` check: a judge model called directly through the gateway scores case-specific criteria 0–10 against the task prompt and the agent's code. Advisory by default (the score is recorded and printed but never fails the case); set `\"advisory\": false` on a case to make `minScore` a gate. Skipped in `--dry-run`.\n5. **Reports** a per-case `result.json` (pass/fail per check, judge scores, durations, model, turns, cost) plus a console summary table.\n\n## Requirements\n\n- macOS (live snapshot checks launch the app; use `--skip-live` elsewhere, or `--sandbox` for the Linux lane), Zig 0.16.0, node >= 24, pnpm 10.x.\n- The [Claude Code CLI](https://code.claude.com/docs) (`claude`) on PATH.\n- A [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) API key for real runs:\n\n```sh\nexport AI_GATEWAY_API_KEY=\"vck_...\"   # or VERCEL_AI_GATEWAY_API_KEY\n```\n\nThe runner assembles the gateway env for the claude subprocess per [Vercel's Claude Code guide](https://vercel.com/docs/ai-gateway/coding-agents/claude-code):\n\n```sh\nANTHROPIC_BASE_URL=https://ai-gateway.vercel.sh\nANTHROPIC_AUTH_TOKEN=$AI_GATEWAY_API_KEY\nANTHROPIC_API_KEY=            # empty string on purpose: a non-empty value would win over the auth token\n```\n\nModels are gateway slugs. The coder (agent-under-test) defaults to `anthropic/claude-sonnet-5` (override with `--model` or `NATIVE_SDK_EVAL_MODEL`); the judge defaults to `anthropic/claude-opus-4.8` (override with `--judge-model` or `NATIVE_SDK_EVAL_JUDGE_MODEL`). Both tracks of a dual case always run with the SAME coder model — fairness is a property of the run, so set it once at run time.\n\n## Usage\n\n```sh\ncd evals\npnpm install\n\npnpm eval --list                      # list cases\npnpm eval --dry-run                   # everything except the model call (no key needed):\n                                      #   scaffold + skill delivery, print env + claude argv,\n                                      #   run the graders against the untouched scaffold\n                                      #   (grader FAILs are expected there and exit 0)\npnpm eval templates-settings-app      # one real run\npnpm eval                             # the whole suite\npnpm eval --model anthropic/claude-opus-4.8 templates-settings-app\npnpm eval --judge-model anthropic/claude-fable-5 templates-settings-app\npnpm eval --skip-live                 # skip snapshot checks (no app launch / non-macOS)\npnpm eval --track ts dual-feed-table  # one track of a dual-track case (default: both)\npnpm eval --keep-workspaces           # keep .workspaces/<case> around for inspection\npnpm eval --trials 5 expenses-table   # 5 independent trials per case; report pass rates\npnpm eval --concurrency 3             # run up to 3 case trials in parallel (default 2 locally)\npnpm eval --sandbox                   # run each case in its own Vercel Sandbox microVM\npnpm eval --sandbox --dry-run         # full sandbox path minus the model call\npnpm eval --sandbox --sandbox-vcpus 8 # bigger sandboxes (2048 MB RAM per vCPU)\npnpm typecheck\n```\n\nCases run in parallel (log lines are prefixed `[case-name]`); `--concurrency` caps how many at once — locally the default is 2 to keep zig builds from thrashing, with `--sandbox` the default is 4 (each case has its own VM, but plans rate-limit vCPU allocation).\n\n### Trials\n\nModel runs are stochastic; a single pass or fail is weak evidence. `--trials <n>` runs each case n times, each trial **fully independent** — its own scaffolded workspace (`.workspaces/<case>-trial-<n>`), its own agent run, its own checks and judge call — and reports per-case pass **rates** (e.g. `3/5`), per-check pass counts, and the mean judge score. Trials share the `--concurrency` pool (log lines are prefixed `[case-name#trial]`), and with `--sandbox` each trial gets its own microVM.\n\nWith `--trials 1` (the default) the behavior and file layout are exactly the single-run layout described above. With `--trials > 1` each case directory nests per-trial results plus an aggregate:\n\n```\nresults/<stamp>/\n  summary.json                      # array of per-case aggregates\n  <case>/\n    aggregate.json                  # pass rate, per-check pass counts, mean judge score, per-trial results\n    trial-1/result.json             # exactly a single-run result.json (plus a \"trial\" field)\n    trial-1/transcript.jsonl\n    trial-2/...\n```\n\nThe summary table swaps the PASS/FAIL column for a `pass rate` column, the `checks` column shows per-check pass counts (`3/3 2/3 ...`, `s` = skipped in every trial), `judge` is the mean score, `cost`/`time` are totals across trials, and a per-check breakdown is printed under the table. A real run exits non-zero if any trial failed.\n\n### Lanes\n\nEvery result carries a **lane** — where the case ran and got graded — and the summary table has a lane column:\n\n- `macos-local` (default): the run described above; live snapshot checks launch the app directly and require macOS.\n- `linux-sandbox` (`--sandbox`): each case trial runs in its own isolated [Vercel Sandbox](https://vercel.com/docs/sandbox) microVM booted from a pre-baked Linux image, **including the live checks** — the app builds with `-Dplatform=linux -Dweb-engine=system -Dautomation=true`, launches under Xvfb, and is driven through the same automation dropbox the macOS lane uses. An engine screenshot of the app's gpu_surface is captured through the dropbox and pulled back with the results.\n\nA check that greps a surface which exists on only one OS can declare `\"lanes\": [\"macos-local\"]` in `eval.json`; on other lanes it reports **skipped** (`not graded on the ... lane`) instead of failing, so the summary distinguishes \"fails\" from \"not applicable on this lane\". Audited 2026-07-05: none of the ten shipped cases needs a lane annotation — every snapshot pattern asserts roles/names from the SDK's own automation snapshot, and the surfaces they cover (secondary windows, gpu charts, trees, tables) are proven on Linux by `tools/linux-truth/`. A Linux-lane failure is therefore a real failure until a case says otherwise.\n\n### Vercel Sandbox mode\n\n`--sandbox` boots each case trial from a custom image (see `evals/sandbox/`) that bakes the Linux GUI stack (GTK4 + WebKitGTK + Xvfb), zig, node/pnpm, the Claude Code CLI, and a **pre-warmed build layer**: the repo at a pinned ref with the CLI, workspace-test, and automation build graphs already compiled into fixed cache paths. Per case the runner then:\n\n1. uploads the repo **working tree** as a tarball and rsyncs it over the baked repo — deletions propagate, caches survive, so builds against the current tip are incremental on top of the bake;\n2. starts Xvfb and re-invokes this same harness inside (`pnpm eval --skip-permissions --lane linux-sandbox <case>`) with the gateway env assembled exactly like a local run;\n3. pulls the whole case results directory home — `result.json`, `transcript.jsonl`, `live-*.png` engine screenshots — before the microVM is destroyed.\n\n`--dangerously-skip-permissions` is safe there because the whole VM is the throwaway. `--sandbox --dry-run` exercises the entire path — provisioning, refresh, scaffold, graders (which FAIL against the untouched scaffold, as designed), artifact pull — without the model call, and exits 0.\n\n**Image**: build and push once with `evals/sandbox/build-image.sh` (needs a Docker login to the registry — the two token variants are in the script header). Rebuild when the Dockerfile changes, when zig bumps, or when the tip has drifted far enough from the baked ref that in-sandbox builds stop feeling incremental; runs stay *correct* without a rebuild because of the working-tree refresh. After a push the registry prepares the image for a few minutes; the runner retries `image_not_ready` for up to 10 minutes. `--sandbox-image` overrides the default reference (`eval-sandbox`, resolved in the linked project).\n\n**Auth** (checked before any sandbox work): either an OIDC token — one-time setup in `evals/`:\n\n```sh\nvercel link --scope vercel-labs --project zero-native\nvercel env pull .env.local   # the runner auto-loads VERCEL_OIDC_TOKEN from .env.local\n```\n\n(the token expires ~12h; re-run `vercel env pull .env.local` when sandbox auth fails) — or `VERCEL_TOKEN` + `VERCEL_TEAM_ID` + `VERCEL_PROJECT_ID` for environments where OIDC is unavailable. Real runs additionally need `AI_GATEWAY_API_KEY` as usual.\n\n**Cost and limits**: sandbox compute is metered (active CPU + provisioned memory) — a 4-vCPU case trial that runs 20-30 minutes lands around $0.20-0.35 plus the model tokens through the gateway. Sandboxes cap at 45 minutes wall clock on the Hobby plan (the runner's per-sandbox timeout), and vCPU allocation is rate-limited per plan, which is why the default `--concurrency` in sandbox mode is 4.\n\nReal runs exit non-zero if any case fails. Workspaces live in `.workspaces/` and results in `results/<timestamp>/<case>/` (`result.json`, `transcript.jsonl`, the isolated `claude-config/` — kept in-VM and not pulled for sandbox runs, plus `live-*.png` screenshots from the Linux lane); both directories are gitignored.\n\n### Permissions for the agent-under-test\n\nBy default the agent runs with `--permission-mode acceptEdits` plus an allowlist covering `zig ...`, `native-sdk ...`, and basic file commands — enough for unattended edit/build/test loops without granting arbitrary shell. `--skip-permissions` switches to `--dangerously-skip-permissions`; only use it if the default allowlist blocks a case, and remember the workspace is a throwaway dir but the process is not otherwise sandboxed.\n\nIn both modes the runner passes `--disallowedTools` deny rules for `evals/cases/**` and `evals/results/**`: the workspace references the SDK repo by path, so the harness itself is reachable from the agent's cwd, and agents exploring the repo for docs/examples were observed reading their own grading config (3/20 runs of the 2026-07-04 suite). SDK source and examples stay readable on purpose — a real user has the repo.\n\n## Cases\n\n- `templates-settings-app` — validates the new grammar: repeated grouped toggle sections where `<template>`/`<use>` is the natural shape, plus token style attributes (muted headers, surface cards). Checks: build+tests, markup check, `<template>`/`<use>` greps, token-attribute greps, snapshot roles.\n- `kanban-board` — port of the manual builder trial; card identity must survive moving between columns (`global-key`).\n- `habits-tracker` — port of the manual markup trial; text entry (elm-style mirror), derived/filtered lists, enum filters.\n- `expenses-table` — exercises the newest grammar (every built-in component markup-expressible): an expense ledger whose natural shape is `table` > `table-row` > `table-cell` with `<for>` rows, an exclusive category filter, and an alert-shaped empty state. The prompt describes only requirements (rows-and-columns of data, a callout, pinned display strings); the greps assert the agent reached the table grammar from the skill alone.\n- `process-monitor` — exercises the effects surface: a long-running local command (a harmless `sh -c` tick loop the prompt pins exactly) spawned from update through the effects channel, lines streaming into a bounded 12-line list, cancel by model-owned key, and status/counts derived from the line/exit Msgs. Greps assert `.update_fx` wiring, `fx.spawn`/`fx.cancel`, fake-executor tests, and the absence of hand-rolled `std.process.Child`/`std.Thread`; the live snapshot asserts the idle state (Start/Cancel, \"Status: idle\", \"0 lines · 0 dropped\") so nothing spawns during grading. The judge scores effect-key discipline, non-blocking behavior, honest drop accounting, and derive-don't-store.\n- `release-dashboard` — exercises the pipeline composites and markdown-in-markup: a release dashboard whose natural shape is `<stepper>` for the five-stage track (starting on \"Canary\"), `<timeline>`/`<timeline-item>` for the seeded event history, and a `<markdown>`-sourced notes panel containing a GFM table. Greps assert all three elements plus `<for>` events; the snapshot asserts the composite semantics (`\"Canary (active)\"` stepper labels, timeline items by title, `role=gridcell` markdown table cells, the pinned status summary). The judge scores house-style composition (no ad-hoc reimplementations) and declarative markup.\n- `settings-picker` — exercises the anchored floating surface pattern: two picker rows (Theme/Accent) whose natural shape is a `<select>` trigger + `<if>`-mounted `<dropdown-menu anchor=...>` of `<menu-item>`s with `on-dismiss`, model-owned open state, and an exclusive-open rule. Greps assert the select/anchored-dropdown/on-dismiss/menu-item composition; the snapshot asserts the closed idle state and the pinned status line (the trigger's accessible name may be its value text or an explicit `label` — both legit, the pattern allows either).\n- `file-browser-panes` — exercises split panes and the tree keymap: a resizable two-pane browser (`<split value= on-resize=>` with pane `min-width` floors) whose sidebar is a `<tree>` of `role=\"treeitem\"` rows with model-owned `expanded` state driving a detail pane. The snapshot asserts `role=separator`/`role=tree`/`role=treeitem`, the seeded rows, and the pinned selection status. The judge is explicitly told the tree keymap is engine-provided so it never expects app-side keyboard Msgs.\n- `metrics-dashboard` — exercises the markup chart element and icon-in-button: the whole view is markup (header with `<button icon=...>`, the `<chart>` with a `<series values=\"{binding}\">`, status bar) and the Zig side owns only model/update logic. Greps assert `<chart`, `icon=\"`, and the pinned `y-min=\"0\"`; the snapshot asserts `role=chart` plus the seeded summary.\n- `inspector-window` — exercises model-declared secondary windows: a task list whose inspector opens as a real second OS window through `Options.windows_fn` + `window_view`, live-updates with the selection, reflects a user close via `on_close`, and never duplicates. A negative grep rejects faking it with `<dialog>`/`<sheet>`/`<drawer>`.\n- `disk-status-spans` — exercises inline text spans: one wrapped paragraph mixing bold, muted, and monospace runs (announcing as a single text run), with bound values interpolated inside the spans and a derived percentage summary. Greps assert the three span styles inside one enclosing `<text>`; the snapshot asserts the paragraph as ONE combined text run with resolved bindings.\n- `chat-composer` — exercises the composer composites: an `<input-group>` wrapping a `<textarea>` plus an in-border `<input-group-actions>` row, an attached `<button-group>` cluster, a derived disabled Send, and a `<for>` message list. The snapshot asserts the composer textbox, the attached cluster, Send disabled on the empty draft, the seeded messages, and the live count.\n- `brand-icon-package` — exercises the one-image app-icon pipeline through `native package`: the manifest sources icons from a single square PNG at the brand path, no prebuilt icon containers anywhere, and the packaged macOS bundle carries a generated `AppIcon.icns` wired into its metadata.\n- `playlist-row-actions` — exercises the interaction seams: row-level Enter as a list row's primary action (`on-submit` on `list-item`, distinct from select-on-press) and the app-level key fallback (`Options.on_key`) for Space and the arrows when nothing is focused. Greps assert the row-level submit binding and the fallback wiring; the snapshot asserts the seeded rows, the model-driven selection, and the idle derived status.\n\nAdd a case by creating `cases/<name>/eval.json` (see `src/types.ts` for the schema). Prompts describe app **requirements**, never the solution — the point is to see whether a fresh agent reaches the intended grammar from the skill alone.\n\n## The TS-authoring track\n\nCases with `\"frontend\": \"ts-core\"` measure the other authoring surface: the app core written in the TypeScript subset and compiled by `packages/core`. The scaffold is not an app — it is `src/core.ts` (the case's `starter/` overlay when it ships one, else a minimal counter core), a README with the check loop, and the `ts-core` skill delivered via `native skills get ts-core`. Two graders replace build/markup/snapshot:\n\n- `ts_transpile` — the frontend check must exit clean on `src/core.ts`: tsc-semantics typecheck, every subset rule (NS1001-NS1050) — no Zig emission; the literal name is the `eval.json` check key, the semantics are `ts_check`. Failing diagnostics stay in the result as the violation evidence.\n- `ts_harness` — behavioral grading: compile the core through the external core compiler and drive the compiled archive through a generated mirror — the scratch dir carries the mirror module, the archive, and the case's `harness.zig`, then `zig test harness.zig`. The harness drives the real dispatch cycle (`update` → `commitModelRoot` → `frameReset`) and asserts the prompt's requirements, so the case prompt pins the Model/Msg/export contract exactly (an API spec, not a solution).\n\nBecause the grading harness compiles against the agent's code, ts-core prompts pin names; behavior stays requirements-only. The compiler handles pure `update(model, msg): Model` cores and effectful `Model | [Model, Cmd<Msg>]` pair-returns (the Cmd surface); the wave-2 dual-track cases below are the effects coverage.\n\nThe ts-core cases:\n\n- `ts-habits-core` — a fresh core from a requirements description (the TS mirror of `habits-tracker`): seeded habits, toggle-with-streak math, a trimmed draft add path, exclusive filters, derived counts.\n- `ts-expenses-filter` — a feature-add to an existing subset file: extend a working expense ledger (shipped as `starter/`) with an exclusive `Category | null` filter and derived filtered views, without breaking the existing behavior.\n- `ts-countdown-bugfix` — a bug-fix in an existing subset file: a countdown core whose tick/reset/set_duration transitions drifted from the spec (unguarded ticks, no completion stop/floor, hardcoded reset); the prompt states the intended behavior, the harness isolates each drift.\n- `ts-tag-input-core` — the text seam under pressure: comma-separated tag parsing with byte-level trim, empty-drop, and case-insensitive dedupe, where string methods are the reflex and NS1004 says no.\n\n## The dual-track cases (wave 2)\n\nWave 1's four ts-* cases measured subset compliance on toy katas and compared against Zig numbers gathered **before** the `zig` 0.16-idioms skill existed. Wave 2 replaces that comparison with an honest, contemporaneous one: six realistic asks, each ONE language-blind spec (`\"frontend\": \"app-dual\"`) that runs on **both authoring tracks** — `<case>@ts` scaffolds a full TypeScript app (`native init --frontend native --template ts-core`), `<case>@zig` the Zig app template (`--template zig-core`). Identical prompt, identical shared checks, one behavioral spec asserted by two thin per-track harnesses; `--track ts|zig` selects a lane, the default runs both.\n\nGrading per track: the shared checks (`native test -Dplatform=null`, markup check, view greps, the judge with a case rubric) plus the track's behavioral harness. On the ts track, `ts_harness` compiles `src/core.ts` (its whole import graph) through the external core compiler and `zig test`s the case's `harness-ts.zig` against the compiled core's generated mirror and `harness-lib/cmdview.zig` — a decoder over the Cmd/Sub wire format, so harnesses assert effects semantically (\"one GET to the pinned URL\", \"the delay re-armed on the same key\"). On the zig track, `zig_harness` injects the case's `harness-zig.zig` into the workspace as `src/eval_behavior_spec.zig` (a test import appended to `src/main.zig`, restored afterward) and runs `native test`, so it compiles against the agent's real Model/Msg/update and drives the SDK's deterministic **fake effects executor** (`fx.executor = .fake`, `pendingSpawnAt`/`feedLine`/`feedExit`/`fireTimer`/`feedResponse`/`feedFileResult`).\n\nThe cases — every prompt reads like a real user ask, and every effect result is fed by the harness (no network, no processes, no clocks during grading):\n\n- `dual-feed-table` — \"fetch JSON from this API and show it in a sortable table\": the HTTP effect (pinned URL, buffered GET, keyed), JSON parsing of a pinned flat schema, two sort orders derived at view time, and the five honest states (idle / loading with re-entry guarded / loaded / explicit empty / failed keeping previous rows) across non-200, transport-failure, and malformed-body deliveries.\n- `dual-notes-autosave` — \"add debounced autosave to this notes app\" (starter provided per track, both riding the SDK text-input engines): the keyed one-shot re-arm debounce (Cmd.delay / fx.startTimer replace), byte-exact notes.tsv serialization, Save now with the pending autosave cancelled, a five-state save lifecycle, and the late-result race (a save landing after newer edits must not mark them saved).\n- `dual-pomodoro-timer` — \"build a pomodoro timer with a completion sound\": the recurring timer surface (Sub.timer declared from the model / fx timer armed-cancelled through the channel — armed exactly while running), auto-advancing focus/rest state machine, work-only completion counting, stale-tick guards, and the audio surface at completions.\n- `dual-list-delete-fix` — \"this list doesn't update after delete — fix it\": a realistic seeded bug (a hand-maintained visible-list cache that delete forgot) in a medium app with a markup view; the harness pins list correctness across delete/toggle/add/filter, the judge scores whether the fix is the derive-don't-store root cause or a symptom patch.\n- `dual-ledger-csv-split` — \"split this core into modules and add export-to-CSV in the new module\": multi-file authoring (relative imports on ts, `@import` on zig, both grepped), byte-exact CSV quoting (commas and doubled quotes exercised by the seeds), the file-write effect, and a four-state export lifecycle.\n- `dual-sysinfo-panel` — \"show system info from a shell command in the UI\": a collect-mode background spawn of a pinned argv, byte parsing of the output line, re-entry guarded while probing, and honest ok / failed-with-code / never-started states.\n\nStarters (`starter-ts/`, `starter-zig/`) overlay the scaffold for the feature-add, bug-fix, and split cases — written in each track's idiom (immutable spread cores over the SDK text engine on ts; bounded buffers, `canvas.TextBuffer`, and Model-method bindings on zig) so neither track starts from translated code.\n\n**Fairness and methodology.** Both tracks get identical prompts (language-neutral asks; the pinned contract names arms/fields in the wire spelling and states each track's idiom for the few surfaces that necessarily differ — routed err arms on ts vs payload-carried outcomes on zig). Each track gets its CURRENT skill set by the documented delivery path and nothing extra: ts → `ts-core` + `native-ui`; zig → `native-ui` + `zig` (the 0.16-idioms skill). The same coder model grades both tracks of a run. Known caveats to carry into any writeup: wave-1's Zig numbers predate the `zig` skill (this suite is the corrected baseline — the pre-existing native cases now also receive it, so their history splits at this commit); the behavioral harnesses are compiled contracts, so a run can fail for contract-shape reasons the prompt pins explicitly; and the two tracks' harness glue differs by necessity (wire-format decoding vs fake-executor introspection) while asserting the same spec, test for test.\n\n### Authoring metrics\n\n`pnpm metrics results/<stamp> [...]` post-processes finished runs' transcripts into the agent-authoring metrics the checks cannot see, per case and per track — **ts** (the ts-core cases and the `@ts` side of dual cases) vs **zig** (the pre-existing native cases and the `@zig` side): **first-pass compliance** (did the agent's first compliance check after touching sources pass — the frontend check, `native check`, `native test`, or `native build` on the ts track; `native test` / `native build` / `native check` / `native markup check` on the zig track), **retries-to-green** (failing compliance runs before the first green), **teaching-error encounters** (failing compliance runs that carried a teaching diagnostic — the \"did the diagnostics work\" round-trip count wave 2 compares across tracks), **violation taxonomy** (NS/TS rule IDs on the ts track; zig error lines on the zig track, with `no member named 'X'` bucketed by member so the 0.16-idiom class is visible) raw and per 1k generated LOC (lines written through Write/Edit to source files, `.native` markup included), and **task success** (the run's own pass verdict). Harness friction — permission-refused commands, errored compounds with no diagnostic in the output — is dropped from the event stream so it never masquerades as an authoring failure. It writes `authoring-metrics.json` next to each `summary.json`.\n\n## CI\n\nCI only typechecks the harness (`evals-typecheck` in `.github/workflows/ci.yml`). Eval runs are local/manual — no model calls in CI.\n\n<!-- File: evals/tsconfig.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Evals Skill\n\nPath: `evals/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/vercel-labs/native/HEAD/evals/tsconfig.json)\n\n<!-- ... +39 more modular skills available via /api/skills/vercel-labs/native?path={path} -->\n</yakaai_skills>"}