{"id":51343335,"url":"https://github.com/blendsdk/jsvision","last_synced_at":"2026-07-02T09:33:52.262Z","repository":{"id":367954778,"uuid":"1282315143","full_name":"blendsdk/jsvision","owner":"blendsdk","description":"TypeScript SDK for building Turbo Vision-style terminal (TUI) apps — ESM-only, zero runtime deps, with a capability-aware engine for detection, width-correct rendering, input decoding, and a host that guarantees terminal restore on every exit. Pre-1.0, under heavy development.","archived":false,"fork":false,"pushed_at":"2026-06-28T22:36:24.000Z","size":1223,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-28T23:07:36.542Z","etag":null,"topics":["ansi","cli","esm","terminal","terminal-emulator","tui","turbo-vision","typescript"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/blendsdk.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-27T15:57:10.000Z","updated_at":"2026-06-28T22:36:27.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/blendsdk/jsvision","commit_stats":null,"previous_names":["blendsdk/tui","blendsdk/jsvision"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/blendsdk/jsvision","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blendsdk%2Fjsvision","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blendsdk%2Fjsvision/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blendsdk%2Fjsvision/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blendsdk%2Fjsvision/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/blendsdk","download_url":"https://codeload.github.com/blendsdk/jsvision/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blendsdk%2Fjsvision/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35041997,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-02T02:00:06.368Z","response_time":173,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ansi","cli","esm","terminal","terminal-emulator","tui","turbo-vision","typescript"],"created_at":"2026-07-02T09:33:51.702Z","updated_at":"2026-07-02T09:33:52.255Z","avatar_url":"https://github.com/blendsdk.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# jsvision\n\nAn SDK for building **Turbo Vision-style terminal applications** in TypeScript.\n\nThis package is the **foundation** of the SDK (RD-01): a clean, typed,\ntree-shakeable **ESM-only** library with **zero runtime dependencies**. The\ncapability, input, rendering, host, and safety subsystems are added by later\nmilestones and re-exported from this package's single public entry point.\n\n\u003e ## 🚧 Under heavy development\n\u003e\n\u003e **`@jsvision/core` is pre-1.0 (`0.1.0`) and under heavy active development.** The\n\u003e public API is still being built out and **may change between minor versions** —\n\u003e pin an exact version if you depend on it. Some capabilities are verified only on\n\u003e Linux/macOS so far (see the [acceptance gate](docs/acceptance-gate.md)). **Not yet\n\u003e recommended for production use.**\n\n## Install\n\n```bash\nnpm install @jsvision/core\n```\n\n**Requirements:** Node.js **\u003e= 20** (active LTS: 20, 22, 24).\n\n## Usage\n\n`@jsvision/core` is **ESM-only**. Import it from an ES module:\n\n```ts\nimport { VERSION } from '@jsvision/core';\n\nconsole.log(VERSION); // \"0.1.0\"\n```\n\nType declarations (`.d.ts`) and source maps ship with the package, so editors get\nfull type information out of the box.\n\n### Capability detection (RD-02)\n\n`resolveCapabilities()` detects the running terminal and returns an immutable\n`CapabilityProfile` plus a per-field **reason trace** showing which layer set each\nfield. Detection is layered with safe fallback — **(1)** explicit override,\n**(2)** live runtime query, **(3)** environment, **(4)** known-terminal table,\n**(5)** conservative defaults — so every later subsystem auto-configures with zero\nsetup.\n\n```ts\nimport { resolveCapabilities } from '@jsvision/core';\n\n// Zero-config: detect from env + known-terminal table + safe defaults.\nconst { profile, reasons } = resolveCapabilities();\nprofile.colorDepth; // 'truecolor' | '256' | '16' | 'mono'\nreasons.colorDepth; // 'override' | 'runtime' | 'env' | 'table' | 'default'\n\n// Force fields (deep partial, merged over detection).\nresolveCapabilities({ override: { mouse: { sgr: false } } }).profile.mouse.sgr; // false\n\n// Re-resolve after a detected terminal change (otherwise cached per process).\nresolveCapabilities({ refresh: true });\n```\n\n`NO_COLOR` (any value) forces `mono`; `FORCE_COLOR=0|1|2|3` selects\n`mono|16|256|truecolor`. The result is deep-frozen, and no environment value is\never logged.\n\nThe live runtime query (layer 2) is asynchronous and bounded. RD-02 ships the\ninjectable `TerminalQuery` seam and the response parser; the real input stream is\nwired in by a later milestone (RD-06). Supply a query via the async resolver:\n\n```ts\nimport { resolveCapabilitiesAsync } from '@jsvision/core';\n\n// `query` implements TerminalQuery; resolution is bounded by `timeoutMs`\n// (default 200 ms) and never hangs on a silent terminal.\nconst { profile } = await resolveCapabilitiesAsync({ query, timeoutMs: 200 });\n```\n\n### Input decoding (RD-06)\n\n`decode()` turns raw terminal bytes into typed input events. It is a **pure**\nfunction of `(bytes, state)` — no timers, no I/O, no logging — so it is\nchunk-boundary-safe and replayable. Feed each chunk and thread the returned\n`state` forward; query responses (DA, `?2026`, XTVERSION) are routed to a\n**separate `queries` array** so a terminal reply can never leak as a keystroke.\n\n```ts\nimport { createDecoderState, decode, flush, createKeymap } from '@jsvision/core';\n\nlet state = createDecoderState();\n\n// A CSI sequence split across two stdin chunks decodes once, on completion.\nstate = decode(Uint8Array.from([0x1b, 0x5b]), state).state; // \"ESC [\" — carried\nconst { events } = decode(Uint8Array.from([0x41]), state); // \"A\" → completes\nevents[0]; // { type: 'key', key: 'up', ctrl: false, alt: false, shift: false }\n```\n\nEvents are a discriminated union: `key`, `mouse` (SGR, 1-based coords), `wheel`,\n`paste` (a bracketed paste delivered as one event, size-capped), and `focus`.\nA lone trailing `ESC` is held ambiguous; the host arms an `ESC_TIMEOUT_MS` (50 ms)\ntimer and calls `flush(state)` to emit the Escape key if no sequence follows.\n\nAn optional pluggable keymap names chords over the events you already received:\n\n```ts\nconst keymap = createKeymap({ 'ctrl+s': 'save', 'alt+x': 'exit' });\nkeymap.lookup({ type: 'key', key: 's', ctrl: true, alt: false, shift: false }); // 'save'\n```\n\nClassic xterm decoding ships now; CSI-u / Kitty keyboard-protocol parsing is a\nlater enhancement (the `caps.keyboard.kittyFlags` branch falls back to classic).\n\n### Rendering (RD-04)\n\nDraw into a width-correct `ScreenBuffer`, then `serialize()` the minimal ANSI to\npaint it over the previous frame. `serialize()` is a **pure** function of\n`(current, previous, options)` — bytes are proportional to what changed (a single\nchanged cell costs under ~32 bytes), and two identical frames cost nothing. The\nhost holds the previous frame and performs the actual write.\n\n```ts\nimport { ScreenBuffer, serialize, resolveCapabilities } from '@jsvision/core';\n\nconst { profile: caps } = resolveCapabilities();\nconst style = { fg: '#c0c0c0', bg: '#000080' };\n\nconst prev = new ScreenBuffer(80, 24, { fg: 'default', bg: 'default' });\nconst next = new ScreenBuffer(80, 24, { fg: 'default', bg: 'default' });\nnext.box(0, 0, 20, 5, style, 'double', 'Hello');\nnext.text(2, 2, '世界 — wide-aware', style); // CJK occupies two columns each\n\nprocess.stdout.write(serialize(next, prev, { caps })); // only the box + text emit\n```\n\nOutput adapts to the detected terminal: box/half-block glyphs fall back to ASCII\n(`+ - | #`) when unsupported, non-UTF-8 glyphs become `?`, and the frame is\nwrapped in synchronized-output markers (`?2026`) when available. Color is encoded\nthrough an injectable `StyleEncoder` seam — the RD-05 depth-aware encoder\n(truecolor→256→16→mono) is the default; an app can still inject its own (see\n_Color \u0026 styling_).\n\nEvery text-accepting path routes through `sanitize()` (the injection boundary):\n`buffer.text()`, the OSC features (`hyperlink`, `setClipboard`, `setTitle`,\n`notify`), and the window title all strip `ESC`/`BEL`/`ST`/C0/C1 control bytes so\nuntrusted text cannot inject an escape sequence. `notify()` picks the best\navailable protocol (Kitty OSC 99 → iTerm2 OSC 9 → urxvt OSC 777 → progress → BEL).\n\n```ts\nimport { notify, setClipboard, cursor } from '@jsvision/core';\n\nnotify('Build', 'done ✓', caps); // → the terminal's best notification protocol\nsetClipboard('copied text', caps); // → OSC 52 (base64), when supported\ncursor.hide() + cursor.to(1, 1) + cursor.show(); // 1-based absolute move\n```\n\n### Color \u0026 styling (RD-05)\n\nThe color layer turns app-specified colors into the **right** ANSI for the\nterminal you actually have — `encode(color, role, depth)` downsamples\n**truecolor → 256 → 16 → mono** instead of assuming 24-bit, which is what fixes\n\"colors all wrong over SSH from a Mac.\" Nearest-color mapping uses a deterministic\nredmean weighted distance; corner colors (pure black/white) are exact.\n\n```ts\nimport { encode, encodeStyle, PALETTE } from '@jsvision/core';\n\nencode('#0000a8', 'bg', 'truecolor'); // '\\x1b[48;2;0;0;168m'\nencode('#0000a8', 'bg', '256'); // '\\x1b[48;5;19m'  (nearest cube index)\nencode('#0000a8', 'bg', '16'); // '\\x1b[44m'        (nearest ANSI blue)\nencode('#0000a8', 'bg', 'mono'); // ''              (attributes only)\n```\n\n`encodeStyle(fg, bg, attrs, caps)` is the seam the renderer uses: it merges\nattributes + fg + bg into one SGR for `caps.colorDepth` and is the `serialize()`\ndefault, so **rendering downsamples with zero configuration**. Attributes\n(`bold|dim|italic|underline|blink|reverse|strike`, the RD-04 `Attr` bits) are\nalways emitted — at `mono` depth no color is sent but `reverse`/`bold` still\nconvey state, keeping `NO_COLOR` UIs legible.\n\n```ts\nimport { Attr, ScreenBuffer } from '@jsvision/core';\nconst buf = new ScreenBuffer(80, 24, { fg: 'default', bg: 'default' });\nbuf.text(2, 1, 'Saved', { fg: PALETTE.brightGreen, bg: 'default', attrs: Attr.bold });\n// serialize(buf, prev, { caps }) now downsamples brightGreen to the detected depth.\n```\n\nColors are **validated**: a malformed color (`encode('#zzz', …)`) throws\n`InvalidColorError` (a `TuiError`) and emits no bytes, so a bad color can never leak\ninto the escape stream; encoders only ever emit numeric SGR. Inside the render loop\nthe encoder degrades a bad cell color to no-color rather than crashing. The DOS-16\n`PALETTE` and a typed `defaultTheme` (the classic Borland look) ship as primitives\nfor the Turbo Vision style. `styleKey(fg, bg, attrs)` gives a stable per-style key\nfor run-merging.\n\n\u003e `NO_COLOR`/`FORCE_COLOR` are already resolved into `caps.colorDepth` by RD-02, so\n\u003e the color layer just honors the detected depth.\n\n### Host \u0026 lifecycle (RD-07)\n\n`createHost()` is the native `tty` host that owns the terminal: it puts stdin in\nraw mode, enters the alternate screen and the mouse / bracketed-paste / focus\nmodes the detected `caps` allow, pumps stdin through the RD-06 decoder, hands each\nframe to the RD-04 serializer as one coalesced write, and — above all —\n**guarantees the terminal is restored on every exit path**: normal `stop()`,\n`SIGINT`/`SIGTERM`/`SIGHUP`, suspend/resume, uncaught exceptions, EPIPE, and even a\nsynchronous crash during setup.\n\n```ts\nimport { createHost, resolveCapabilities, ScreenBuffer, createKeymap } from '@jsvision/core';\n\nconst { profile: caps } = resolveCapabilities();\nconst keymap = createKeymap({ 'ctrl+c': 'quit' });\n\nconst host = createHost({\n  caps,\n  onInput: (e) =\u003e {\n    if (e.type === 'key' \u0026\u0026 keymap.lookup(e) === 'quit') void host.stop();\n  },\n  onResize: ({ columns, rows }) =\u003e draw(columns, rows),\n  onResume: () =\u003e draw(process.stdout.columns ?? 80, process.stdout.rows ?? 24),\n});\n\nawait host.start(); // raw mode, alt-screen, modes per caps\nfunction draw(cols: number, rows: number): void {\n  const frame = new ScreenBuffer(cols, rows, { fg: 'default', bg: 'default' });\n  frame.text(2, 1, 'Hello — Ctrl-C to quit', { fg: '#c0c0c0', bg: 'default' });\n  host.render(frame); // serialize(diff) → single coalesced write\n}\ndraw(process.stdout.columns ?? 80, process.stdout.rows ?? 24);\n// SIGINT/SIGTERM/SIGHUP/throw → terminal restored, process exits with the right code.\n```\n\n`render(buffer)` owns the previous frame, the diff, and the write; the app never\ntouches `serialize` or the stream. Input arrives as decoded `InputEvent`s through\n`onInput` (terminal query replies are routed away so they can never be read as\nkeystrokes); resize is coalesced to one `onResize`; `onSuspend`/`onResume` bracket\nSIGTSTP/SIGCONT with an automatic full repaint on resume. The host owns\n`process.exit` on signal/crash paths (`exitOnSignal: false` opts out, with an\n`onBeforeExit(code)` hook); `stop()` restores without exiting and is idempotent.\n\nOn startup (real TTY, before the alternate screen) the host can probe whether the\nterminal renders our ambiguous-width chrome glyphs double-width. With\n`adaptAmbiguousWidth` (core default `false`; `@jsvision/ui` apps default `true`) a\nwide-rendering terminal automatically gets aligned **ASCII-safe chrome** — the\n`ScreenBuffer` still stores the real Unicode, only the emitted bytes change. Set\n**`JSVISION_ASCII`** (any value, NO_COLOR-style) to force ASCII-safe chrome and skip\nthe probe entirely — e.g. `JSVISION_ASCII=1 yarn workspace @jsvision/examples\ndemo:kitchen` is the manual showcase for the degraded output.\n\nEvery OS effect (raw mode, signals, exit, timers, the sync `'exit'` backstop) sits\nbehind an injectable `RuntimeAdapter`, so an app can run the host headlessly in\ntests; the real adapter is the default. A non-TTY host skips mode setup but still\nwrites frames, and exposes `isTTY` for the caller's degrade policy. On Windows the\nhost uses the `stdout 'resize'` event, `SIGBREAK`, and VT processing in place of\nthe POSIX signals; Windows acceptance awaits a Windows runner.\n\n### Safety: essentials gate, logging \u0026 errors (RD-08)\n\nThe `safety` subsystem decides whether the SDK may run, keeps secrets out of logs,\nand owns the canonical injection boundary. Everything here is **pure and\ninjectable** — no global state, disabled by default.\n\n**Essentials gate.** Before `start()`, evaluate the terminal against the runtime\nessentials. The single hard requirement is an **interactive TTY**; missing mouse,\ncolor, or alt-screen are **degradations** the SDK runs around, never hard stops.\nTTY facts come from the additive `detectTty()` probe (a real terminal can be\ndetected even when stdout is piped, via `/dev/tty`), because `host.isTTY` is only\nvalid after `start()`.\n\n```ts\nimport { detectTty, assertEssentials, resolveCapabilities, createLogger } from '@jsvision/core';\n\nconst { profile: caps } = resolveCapabilities();\nconst facts = { isTTY: detectTty() };\n\n// Throws EssentialsNotMetError on a pipe (before any mode is entered — nothing\n// to restore); returns a report with any degradations otherwise.\nconst report = assertEssentials(caps, facts, { logger: createLogger({ sink: 'ring' }) });\nreport.degradations; // e.g. [{ cap: 'mouse', mode: 'keyboard-only', message: '…' }]\n\n// Pure variants for custom flows:\nimport { evaluateEssentials, essentialsMet } from '@jsvision/core';\nevaluateEssentials(caps, facts); // { met, missing, degradations } — never throws\nessentialsMet(caps, facts); // boolean\n```\n\n**Screen-safe logger.** A TUI owns the screen, so the logger physically refuses\nany sink that resolves to the UI stream (throws `LoggerConfigError` at\nconstruction). It is **disabled by default** — a normal run writes zero bytes —\nand gated by env: set `BLENDTUI_DEBUG=1` to enable, `BLENDTUI_LOG=\u003cpath\u003e` to write\nto a file (else stderr when it is not the UI). Levels are `error|warn|info|debug`;\nthe in-memory `ring` sink is always available for tests.\n\n```ts\nconst log = createLogger(); // disabled unless BLENDTUI_DEBUG=1\nlog.debug('input', 'event', { count: 3 }); // no-op when disabled\n```\n\n**Redaction.** `redactEvent()` reduces an input event to a log-safe shape: a\nprintable key logs only `{ printable: true, ctrl, alt, shift }` (never the\ncharacter or codepoint), a paste logs only its length, and mouse/wheel/focus pass\ntheir non-secret coordinates. `dumpCaps()` renders a one-line, secret-free\ncapabilities summary from the RD-02 reason trace.\n\n```ts\nimport { redactEvent } from '@jsvision/core';\nredactEvent({ type: 'key', key: 'a', codepoint: 0x61, ctrl: false, alt: false, shift: false });\n// → { type: 'key', printable: true, ctrl: false, alt: false, shift: false }\n```\n\n**Sanitizer \u0026 errors.** `sanitize()` (the injection boundary used by every\ntext-accepting render path — see _Rendering_) is the canonical safety primitive\nand lives here. The error model is a single `TuiError` base with\n`EssentialsNotMetError` (carries the unmet essentials) and `LoggerConfigError`, so\na consumer can `catch (e) { if (e instanceof TuiError) … }`. An uncaught error\nthrough the host's loop restores the terminal **before** the process exits.\n\n### Live terminal query (RD-03)\n\n`createTerminalQuery()` is the real, tty-backed implementation of the layer-2\n`TerminalQuery` seam — it writes query requests to the terminal and yields the\nresponse bytes — so `resolveCapabilitiesAsync()` can refine the profile from live\nresponses (e.g. synchronized-output `?2026`), not just env/table heuristics.\n\n```ts\nimport { createTerminalQuery, resolveCapabilitiesAsync } from '@jsvision/core';\n\n// The caller owns raw mode; the adapter only reads/writes bytes and never\n// changes terminal state. Always close() it when done to release the listener.\nconst query = createTerminalQuery({ input: process.stdin, output: process.stdout });\ntry {\n  const { profile } = await resolveCapabilitiesAsync({ query });\n  // profile.sync2026 etc. now reflect the terminal's actual replies\n} finally {\n  query.close();\n}\n```\n\n### Capability probe \u0026 survey harness (RD-03)\n\nA dev-only diagnostic harness lives under `examples/capability-probe/` (not part of\nthe published package). It probes **every** capability the SDK cares about and\nreports what actually works on the running terminal — automatic query-based probes\nfirst, then guided manual confirmation (color swatches, attributes, glyphs,\nUnicode alignment, OSC notifications/hyperlinks/clipboard/title), then a live\ndecoded-input readout — and emits a JSON + table report, accumulating a checked-in\n`terminal-matrix.json` evidence base. It runs non-destructively (alt-screen with\nguaranteed restore) and never stops on a missing capability.\n\n```bash\n# the probe harness lives in the private @jsvision/examples package\nyarn workspace @jsvision/examples probe                 # interactive survey (appends the matrix)\nyarn workspace @jsvision/examples probe --auto \u003e out.json  # non-interactive: auto facts only\nyarn workspace @jsvision/examples probe --out r.json --no-matrix  # standalone JSON; skip matrix\n```\n\nFlags: `--auto` (CI mode, manual items left unverified), `--out \u003cpath\u003e` (standalone\nJSON copy), `--no-matrix` (skip the `terminal-matrix.json` append), `--help`. The\nreport records only terminal/OS plus `TERM`/`COLORTERM`/`TERM_PROGRAM` — no secrets,\nand paste events report a byte length, never contents.\n\n### Testing \u0026 acceptance gate (RD-09)\n\nThe foundation is proven by a four-tier test strategy plus a runnable **project\ngo/no-go gate**. Terminal I/O is \"bytes in → bytes out\", so most of the engine is\npure functions and unusually testable.\n\n- **Tier 1 — recorded input corpus**: checked-in hex-in-JSON fixtures of\n  `bytes → expected events/queries` (`test/fixtures/input-corpus/`) drive a\n  data-driven decoder regression — keyboard, SGR mouse (incl. beyond column 223),\n  wheel, paste, and DA/DECRPM/XTVERSION replies (the last asserted on the isolated\n  `queries` channel).\n- **Tier 2 — golden-screen**: a buffer is serialized and fed to `@xterm/headless`\n  (a pure-JS emulator), then the grid is read back and asserted across all four\n  colour depths (truecolor/256/16/mono), proving the RD-05 downsample chain renders\n  correctly in a real emulator.\n- **Tier 3 — PTY-style integration** (no `node-pty`): a real child process runs the\n  real `createHost`; captured output proves alt-screen/mouse enter and full restore\n  on normal exit, `throw`, SIGTERM, and SIGHUP.\n- **Fuzz + performance**: a seeded fuzzer feeds adversarial byte streams to the\n  decoder (no throw, bounded state), and a byte-proportionality benchmark asserts\n  `serialize` output bytes track changed cells (the deterministic half of the perf\n  gate; wall-clock budgets are deferred to RD-10).\n\nThe corpus, golden, fuzz, and byte-proportionality suites run under `yarn verify`;\nthe Tier-3 integration lives in `test/host-tier3.e2e.test.ts` (explicit, outside the\nunit glob).\n\n```bash\nyarn gate    # the full go/no-go gate: verify + e2e + probe --auto, per-criterion verdict\n```\n\n`yarn gate` (`scripts/gate.mjs`) prints PASS/FAIL/DEFERRED for each of the 11\nRD-09 criteria and exits non-zero if any required one fails. The criteria→evidence\nmap lives in [`docs/acceptance-gate.md`](docs/acceptance-gate.md). Cross-platform CI\ncells, macOS/Windows acceptance, the Tier-4 manual matrix, real-PTY SIGWINCH resize,\nand wall-clock perf budgets are recorded **DEFERRED** (DEF-1…DEF-4), pending a git\nremote, the other platforms, and RD-10.\n\n### ESM-only — `require()` is not supported\n\nThe package declares no CommonJS `require` condition. Importing it from CommonJS\nfails with a clear ESM error:\n\n```js\n// ❌ throws ERR_REQUIRE_ESM\nconst { VERSION } = require('@jsvision/core');\n```\n\nUse `import` (or a dynamic `await import('@jsvision/core')`) instead.\n\n## Versioning \u0026 stability\n\n`@jsvision/core` follows [Semantic Versioning](https://semver.org/). All notable\nchanges are recorded in [`CHANGELOG.md`](CHANGELOG.md) (Keep a Changelog format).\n\n- **SemVer.** While the package is pre-1.0 it is in active development: the public\n  API may change between **minor** versions (consistent with the heavy-development\n  notice above). From 1.0 onward, breaking changes ship only in a **major** release.\n  **Additive** surface (e.g. a new `Theme` role such as `windowInactive`) is\n  non-breaking and ships in a **minor**; see `CHANGELOG.md` for each addition.\n- **Public surface.** The exports of `src/engine/index.ts` (the package entry\n  point) are the contract. Everything else — any deep import into `src/engine/**`\n  internals — is **not** part of the public API and may change at any time.\n- **Deprecation policy.** A symbol slated for removal is first marked\n  `@deprecated` (JSDoc) for **at least one minor release**, then removed in the\n  next **major**. Every removal is recorded under that version in `CHANGELOG.md`.\n\n## Monorepo layout\n\nThis repository is a **yarn 1.x + Turborepo monorepo**:\n\n```\npackages/core/      @jsvision/core — the published foundation engine\npackages/examples/  @jsvision/examples — private dev examples + probe harness\ndocs/  scripts/  .github/   shared docs, tooling, and CI at the root\n```\n\nNew packages are published as `@jsvision/\u003cname\u003e` under `packages/\u003cname\u003e/`,\nand all **public** packages share one lockstep version (`yarn sync-versions`).\n\n## Contributing\n\nThe toolchain is yarn workspaces + Turborepo orchestration; tests run on **vitest**.\n\n| Command              | What it does                                                      |\n| -------------------- | ----------------------------------------------------------------- |\n| `yarn verify`        | `turbo run typecheck build test` across packages — must exit 0    |\n| `yarn gate`          | The RD-09 go/no-go gate: verify + e2e + probe, per criterion      |\n| `yarn lint`          | ESLint + Prettier (check only, repo-wide)                         |\n| `yarn lint:fix`      | ESLint `--fix` + Prettier `--write`                               |\n| `yarn check:deps`    | Fail if any runtime dependency requires native build steps        |\n| `yarn sync-versions` | Write the root version to all public packages (`--check` to lint) |\n| `yarn bench`         | Print frame perf median/p95 (200×50) — informational (RD-10)      |\n\n\u003e **Performance (RD-10).** `yarn bench` reports the 200×50 compose+diff median/p95\n\u003e (informational; it never fails). The 16 ms frame-budget ceiling is asserted\n\u003e off-CI by `packages/core/test/perf-budget.spec.test.ts` and auto-skips its\n\u003e hard assertion under `CI` or when `TUI_SKIP_PERF` is set; CI runs the bench\n\u003e informationally on one matrix cell.\n\nTests follow a strict split:\n\n- `*.spec.test.ts` — specification tests; an immutable oracle derived from the\n  requirements/acceptance criteria.\n- `*.impl.test.ts` — implementation/edge-case tests.\n\nBoth run via `yarn test` (vitest `unit` project). Heavier end-to-end tests end in\n`*.e2e.test.ts` and run in the vitest `e2e` project (via `yarn test:e2e` or `yarn gate`):\n\n```bash\nyarn workspace @jsvision/core test:e2e      # restore-on-exit, signals, pack + clean-install\nyarn workspace @jsvision/examples test:e2e  # the probe harness e2e\n```\n\n## License\n\n[MIT](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fblendsdk%2Fjsvision","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fblendsdk%2Fjsvision","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fblendsdk%2Fjsvision/lists"}