diff --git a/.claude/skills/updating-node/SKILL.md b/.claude/skills/updating-node/SKILL.md index 0d8d185a4..7f3fb2467 100644 --- a/.claude/skills/updating-node/SKILL.md +++ b/.claude/skills/updating-node/SKILL.md @@ -65,17 +65,29 @@ sequential: 2. **stubs** — consumes curl + lief; produces platform stubs that binsuite + node-smol SEA-inject. 3. **binsuite** — consumes stubs (+ curl, lief). -4. **node-smol** — consumes stubs + binsuite + curl + lief; the - final layer. +4. **temporal-infra** — invokes `/updating-temporal-infra` to refresh + the parity reference + audit the C++ port for drift. Short- + circuits if `boa-dev/temporal` hasn't cut a new tag since the + last run (no commit, cascade proceeds). When it DOES move, the + C++ port catches up before node-smol consumes the changes via + `additions/source-patched/`. +5. **node-smol** — consumes stubs + binsuite + curl + lief + the + refreshed temporal C++ port; the final layer. Adjacent vendor syncs (independent of the chain): `updating-fast-webstreams`, `updating-zstd` — can run any time. **Why the order matters:** node-smol embeds the stub-injected `curl` -binary plus the LIEF library; dispatching node-smol before its +binary plus the LIEF library AND consumes the temporal C++ port via +`additions/source-patched/`; dispatching node-smol before its prerequisites cascade leaves it building against stale dependencies and surfaces "fixed" issues in the wrong layer. +**Coupling is one-way:** `/updating-node` exercises +`/updating-temporal-infra` so every Node bump has a current parity +reference. A standalone `/updating-temporal-infra` run (boa-dev/temporal +cuts a tag while Node is current) does NOT drag in a Node rebuild. + ### Phase 4: Report Version change, commits created, patch status, post-update results. diff --git a/.claude/skills/updating-temporal-infra/SKILL.md b/.claude/skills/updating-temporal-infra/SKILL.md index 7bfa212d7..a44224fef 100644 --- a/.claude/skills/updating-temporal-infra/SKILL.md +++ b/.claude/skills/updating-temporal-infra/SKILL.md @@ -22,69 +22,19 @@ submodule + lockstep row when upstream cuts a new release. - **Kind**: `feature-parity` (lockstep.json) — the port re-implements the Rust crate's externally observable behavior, not the source. -## Why this tracks-latest (not locked) — emerging language feature - -[`Temporal`](https://tc39.es/proposal-temporal/) is the -**Stage 4** ECMAScript proposal (recently promoted from Stage 3) -for first-class date/time/timezone/calendar handling. Spec: -. Implementations are still -shipping (V8 14.x has it behind a flag), boa-dev/temporal lands -fixes regularly, and divergence between our port and the -canonical Rust implementation is a real risk. - -### UNLIKE lief / curl / cjson / libdeflate / etc. - -For most upstreams socket-btm vendors, we sync the submodule SHA -to **whatever version upstream Node ships** (via `deps//`). -That's the right policy for stable C/C++ libraries with frozen -APIs — the goal is reproducible Node builds, not tracking the -library's own cadence. - -**Temporal is different.** It's an emerging language feature, not -a stable utility library: - -- The TC39 proposal is still settling edge cases (calendar - ambiguity, ISO week math, leap-second semantics). -- boa-dev/temporal cuts releases on its own cadence, often - faster than upstream Node bumps. -- V8's Temporal implementation lives in - `deps/v8/src/objects/js-temporal-objects.cc` and depends on - the Rust crate via FFI through `temporal_capi`. V8 may pin - an older boa-dev/temporal than what's current. -- Locking us to V8's pin would mean the C++ port can never - exercise newer Temporal API shapes than what V8 happens to - ship — defeats the point of an independent port. - -**Two submodules, two policies:** - -| Submodule | Policy | Driven by | -|---|---|---| -| `packages/node-smol-builder/upstream/temporal` | **locked** to upstream Node's `deps/crates/Cargo.toml` pin (currently v0.1.0) | `updating-node` cascade | -| `packages/temporal-infra/upstream/temporal` | **track-latest** boa-dev/temporal release | this skill | - -**They DO NOT need to agree.** node-smol's submodule is what V8 -links against (the Rust crate compiled into the binary). -temporal-infra's submodule is the **parity reference** for the -hand-written C++ port — source of truth for "what should the API -surface look like." A newer parity reference than what V8 ships -against is fine; the C++ port matches the upstream API even when -V8 doesn't expose every new symbol yet. - -The annotations in `.gitmodules` make this explicit: +## Why this tracks-latest — emerging language feature -``` -# temporal-v0.1.0 (locked: pinned by upstream Node ...) -[submodule "packages/node-smol-builder/upstream/temporal"] - ... -# temporal-vX.Y.Z (track-latest: bump independently via updating-temporal-infra) -[submodule "packages/temporal-infra/upstream/temporal"] - ... -``` +[`Temporal`](https://tc39.es/proposal-temporal/) is the **Stage 4** ECMAScript proposal (recently promoted from Stage 3) for first-class date/time/timezone/calendar handling. boa-dev/temporal lands fixes on its own cadence — often faster than upstream Node bumps — and the C++ port at `packages/temporal-infra/src/socketsecurity/temporal/` mirrors the canonical Rust crate so the port stays aligned with what the spec is actually doing. + +V8's link target is the **vendored copy** inside the Node submodule at `deps/crates/vendor/temporal_rs/`. That's V8's concern; we don't track it explicitly. Our single top-level temporal submodule (`packages/temporal-infra/upstream/temporal`) exists for the C++ port to consume — track-latest, no separate locked copy. + +The same logic applies to any **future emerging-feature ports** (decorators, pattern matching, etc.) — the `*-infra` package tracks the proposal cadence; V8's link target stays whatever Node ships. + +## Coupling with `/updating-node` + +`/updating-node` invokes this skill as a sub-step in its Phase 3 cascade order (between `binsuite` and `node-smol`). When Node cuts a new tag, the cascade refreshes the parity reference and audits the C++ port for drift before building node-smol. If this skill's Phase 2 short-circuits at "already at latest," the cascade proceeds straight to node-smol with no temporal commit. -The same logic applies to any **future emerging-feature ports** -(decorators, pattern matching, etc.) — the *-infra package -tracks the proposal cadence, the node-smol vendor copy stays -locked to whatever Node ships. +The reverse coupling does not apply: a standalone temporal bump (this skill invoked directly) does NOT drag in a Node rebuild. ## Process @@ -103,21 +53,16 @@ CURRENT=$(git describe --tags 2>/dev/null || echo "unknown") If `LATEST == CURRENT`, exit 0 with "already at latest." -### Phase 3 — Bump only temporal-infra's submodule +### Phase 3 — Bump the temporal submodule ```bash -# Bump temporal-infra to the latest upstream tag. +# Bump the canonical temporal submodule to the latest upstream tag. git -C packages/temporal-infra/upstream/temporal checkout "$LATEST" ``` -**Do NOT bump `packages/node-smol-builder/upstream/temporal`** — -that submodule is locked to upstream Node's `deps/crates/Cargo.toml` -pin. Bumping it independently would diverge what V8 links against -from what upstream expects, and is the `updating-node` skill's -job, not this one. +Update the `.gitmodules` annotation: `# temporal-vX.Y.Z (canonical temporal submodule; …)` → new tag. -Update `.gitmodules` annotation for THIS submodule only: -`# temporal-vX.Y.Z (track-latest: ...)` → new tag. +There is exactly one temporal submodule (consolidated from the earlier two-submodule split in commit `67919e29`). V8's link target lives in the vendored Rust crate inside the Node submodule (`deps/crates/vendor/temporal_rs/`) and is unaffected by this bump — bumping the parity reference cannot diverge V8's link target. ### Phase 4 — Update lockstep.json @@ -202,10 +147,12 @@ a parser update across all 4 ultrathink lang impls. follow-ups for task #217 (the implementation work). Don't block the SHA bump on having every symbol ported; the port catches up incrementally. -- **node-smol's submodule SHA drifts ahead of temporal-infra's**: - fine — node-smol's vendored copy is the V8 link target; - temporal-infra's is the parity reference and may legitimately - be ahead. Concerning only in the reverse direction (V8 has a - newer Temporal API than the parity reference), in which case - consult upstream Node's `deps/crates/Cargo.toml` and decide - whether to bump temporal-infra forward. +- **V8's link target is newer than the parity reference**: + V8's vendored `deps/crates/vendor/temporal_rs/` (inside the Node + submodule) is whatever Node ships; the parity reference at + `packages/temporal-infra/upstream/temporal` is whatever + boa-dev/temporal cuts. Usually parity is ahead. If V8 is ahead + (rare — only when Node ships a brand-new temporal_rs before + boa-dev tags it), consult upstream Node's `deps/crates/Cargo.toml` + and bump the parity submodule to a commit that matches or + exceeds V8's pin. diff --git a/.config/lockstep.json b/.config/lockstep.json index 05edc7b58..d43916b7f 100644 --- a/.config/lockstep.json +++ b/.config/lockstep.json @@ -59,6 +59,22 @@ "submodule": "packages/node-smol-builder/upstream/uWebSockets", "repo": "https://github.com/uNetworking/uWebSockets" }, + "dawn": { + "submodule": "packages/dawn-builder/upstream/dawn", + "repo": "https://dawn.googlesource.com/dawn" + }, + "libqrencode": { + "submodule": "packages/node-smol-builder/upstream/libqrencode", + "repo": "https://github.com/fukuchi/libqrencode" + }, + "md4c": { + "submodule": "packages/node-smol-builder/upstream/md4c", + "repo": "https://github.com/mity/md4c" + }, + "tree-sitter": { + "submodule": "packages/node-smol-builder/upstream/tree-sitter", + "repo": "https://github.com/tree-sitter/tree-sitter" + }, "opentui": { "submodule": "packages/opentui-builder/upstream/opentui", "repo": "https://github.com/anomalyco/opentui" @@ -80,10 +96,6 @@ "repo": "https://github.com/facebook/zstd" }, "temporal-rs": { - "submodule": "packages/node-smol-builder/upstream/temporal", - "repo": "https://github.com/boa-dev/temporal" - }, - "temporal-rs-parity": { "submodule": "packages/temporal-infra/upstream/temporal", "repo": "https://github.com/boa-dev/temporal" }, @@ -107,20 +119,10 @@ "criticality": 10, "notes": "node-smol-builder produces the slim Node.js runtime for Socket Firewall. Major bumps require wide testing (V8 API churn, perf regressions); patch/minor auto-track." }, - { - "kind": "version-pin", - "id": "temporal-rs", - "upstream": "temporal-rs", - "pinned_sha": "1d1b123ff78a3ab656d5aa19d803d1516f95e92f", - "pinned_tag": "v0.1.0", - "upgrade_policy": "locked", - "criticality": 9, - "notes": "temporal_rs / temporal_capi crates back the Temporal global in Node 26+. Pinned to =0.1.0 by upstream Node's deps/crates/Cargo.toml \u2014 bumping requires a matching upstream Node bump, which is why this row is locked rather than track-latest. Vendored under the node submodule at deps/crates/vendor/temporal_rs/; configure.py asserts rustc/cargo >= 1.82 with LLVM >= 19. Smoke-tested by packages/build-infra/test/fixtures/smoke-test-modules.mjs." - }, { "kind": "file-fork", "id": "temporal-infra-instant", - "upstream": "temporal-rs-parity", + "upstream": "temporal-rs", "local": "packages/temporal-infra/src/socketsecurity/temporal/instant.cc", "upstream_path": "src/builtins/core/instant.rs", "forked_at_sha": "c003cc92325e19b26f8ee2f85e4a47d98cbcc781", @@ -137,7 +139,7 @@ { "kind": "file-fork", "id": "temporal-infra-duration", - "upstream": "temporal-rs-parity", + "upstream": "temporal-rs", "local": "packages/temporal-infra/src/socketsecurity/temporal/duration.cc", "upstream_path": "src/builtins/core/duration.rs", "forked_at_sha": "c003cc92325e19b26f8ee2f85e4a47d98cbcc781", @@ -153,7 +155,7 @@ { "kind": "file-fork", "id": "temporal-infra-iso", - "upstream": "temporal-rs-parity", + "upstream": "temporal-rs", "local": "packages/temporal-infra/src/socketsecurity/temporal/iso.cc", "upstream_path": "src/iso.rs", "forked_at_sha": "c003cc92325e19b26f8ee2f85e4a47d98cbcc781", @@ -169,7 +171,7 @@ { "kind": "file-fork", "id": "temporal-infra-parse", - "upstream": "temporal-rs-parity", + "upstream": "temporal-rs", "local": "packages/temporal-infra/src/socketsecurity/temporal/parse.cc", "upstream_path": "src/parsers.rs", "forked_at_sha": "c003cc92325e19b26f8ee2f85e4a47d98cbcc781", @@ -186,7 +188,7 @@ { "kind": "file-fork", "id": "temporal-infra-ixdtf-writer", - "upstream": "temporal-rs-parity", + "upstream": "temporal-rs", "local": "packages/temporal-infra/src/socketsecurity/temporal/ixdtf_writer.cc", "upstream_path": "src/parsers.rs", "forked_at_sha": "c003cc92325e19b26f8ee2f85e4a47d98cbcc781", @@ -203,7 +205,7 @@ { "kind": "file-fork", "id": "temporal-infra-error", - "upstream": "temporal-rs-parity", + "upstream": "temporal-rs", "local": "packages/temporal-infra/src/socketsecurity/temporal/error.cc", "upstream_path": "src/error.rs", "forked_at_sha": "c003cc92325e19b26f8ee2f85e4a47d98cbcc781", @@ -219,7 +221,7 @@ { "kind": "file-fork", "id": "temporal-infra-utils", - "upstream": "temporal-rs-parity", + "upstream": "temporal-rs", "local": "packages/temporal-infra/src/socketsecurity/temporal/utils.cc", "upstream_path": "src/utils.rs", "forked_at_sha": "c003cc92325e19b26f8ee2f85e4a47d98cbcc781", @@ -233,7 +235,7 @@ { "kind": "file-fork", "id": "temporal-infra-host", - "upstream": "temporal-rs-parity", + "upstream": "temporal-rs", "local": "packages/temporal-infra/src/socketsecurity/temporal/host.cc", "upstream_path": "src/host.rs", "forked_at_sha": "c003cc92325e19b26f8ee2f85e4a47d98cbcc781", @@ -247,7 +249,7 @@ { "kind": "file-fork", "id": "temporal-infra-sys", - "upstream": "temporal-rs-parity", + "upstream": "temporal-rs", "local": "packages/temporal-infra/src/socketsecurity/temporal/sys.cc", "upstream_path": "src/sys.rs", "forked_at_sha": "c003cc92325e19b26f8ee2f85e4a47d98cbcc781", @@ -261,7 +263,7 @@ { "kind": "file-fork", "id": "temporal-infra-now", - "upstream": "temporal-rs-parity", + "upstream": "temporal-rs", "local": "packages/temporal-infra/src/socketsecurity/temporal/now.cc", "upstream_path": "src/builtins/core/now.rs", "forked_at_sha": "c003cc92325e19b26f8ee2f85e4a47d98cbcc781", @@ -275,7 +277,7 @@ { "kind": "file-fork", "id": "temporal-infra-parsed-intermediates", - "upstream": "temporal-rs-parity", + "upstream": "temporal-rs", "local": "packages/temporal-infra/src/socketsecurity/temporal/parsed_intermediates.cc", "upstream_path": "src/parsed_intermediates.rs", "forked_at_sha": "c003cc92325e19b26f8ee2f85e4a47d98cbcc781", @@ -289,7 +291,7 @@ { "kind": "file-fork", "id": "temporal-infra-plain-time", - "upstream": "temporal-rs-parity", + "upstream": "temporal-rs", "local": "packages/temporal-infra/src/socketsecurity/temporal/plain_time.cc", "upstream_path": "src/builtins/core/plain_time.rs", "forked_at_sha": "c003cc92325e19b26f8ee2f85e4a47d98cbcc781", @@ -304,7 +306,7 @@ { "kind": "file-fork", "id": "temporal-infra-plain-year-month", - "upstream": "temporal-rs-parity", + "upstream": "temporal-rs", "local": "packages/temporal-infra/src/socketsecurity/temporal/plain_year_month.cc", "upstream_path": "src/builtins/core/plain_year_month.rs", "forked_at_sha": "c003cc92325e19b26f8ee2f85e4a47d98cbcc781", @@ -318,7 +320,7 @@ { "kind": "file-fork", "id": "temporal-infra-plain-month-day", - "upstream": "temporal-rs-parity", + "upstream": "temporal-rs", "local": "packages/temporal-infra/src/socketsecurity/temporal/plain_month_day.cc", "upstream_path": "src/builtins/core/plain_month_day.rs", "forked_at_sha": "c003cc92325e19b26f8ee2f85e4a47d98cbcc781", @@ -332,7 +334,7 @@ { "kind": "file-fork", "id": "temporal-infra-options", - "upstream": "temporal-rs-parity", + "upstream": "temporal-rs", "local": "packages/temporal-infra/src/socketsecurity/temporal/options.cc", "upstream_path": "src/options.rs", "forked_at_sha": "c003cc92325e19b26f8ee2f85e4a47d98cbcc781", @@ -351,7 +353,7 @@ { "kind": "file-fork", "id": "temporal-infra-rounding", - "upstream": "temporal-rs-parity", + "upstream": "temporal-rs", "local": "packages/temporal-infra/src/socketsecurity/temporal/rounding.h", "upstream_path": "src/rounding.rs", "forked_at_sha": "c003cc92325e19b26f8ee2f85e4a47d98cbcc781", @@ -367,7 +369,7 @@ { "kind": "file-fork", "id": "temporal-infra-plain-date", - "upstream": "temporal-rs-parity", + "upstream": "temporal-rs", "local": "packages/temporal-infra/src/socketsecurity/temporal/plain_date.cc", "upstream_path": "src/builtins/core/plain_date.rs", "forked_at_sha": "c003cc92325e19b26f8ee2f85e4a47d98cbcc781", @@ -383,7 +385,7 @@ { "kind": "file-fork", "id": "temporal-infra-plain-date-time", - "upstream": "temporal-rs-parity", + "upstream": "temporal-rs", "local": "packages/temporal-infra/src/socketsecurity/temporal/plain_date_time.cc", "upstream_path": "src/builtins/core/plain_date_time.rs", "forked_at_sha": "c003cc92325e19b26f8ee2f85e4a47d98cbcc781", @@ -398,7 +400,7 @@ { "kind": "file-fork", "id": "temporal-infra-calendar", - "upstream": "temporal-rs-parity", + "upstream": "temporal-rs", "local": "packages/temporal-infra/src/socketsecurity/temporal/calendar.cc", "upstream_path": "src/builtins/core/calendar.rs", "forked_at_sha": "c003cc92325e19b26f8ee2f85e4a47d98cbcc781", @@ -414,7 +416,7 @@ { "kind": "file-fork", "id": "temporal-infra-time-zone", - "upstream": "temporal-rs-parity", + "upstream": "temporal-rs", "local": "packages/temporal-infra/src/socketsecurity/temporal/time_zone.cc", "upstream_path": "src/builtins/core/time_zone.rs", "forked_at_sha": "c003cc92325e19b26f8ee2f85e4a47d98cbcc781", @@ -429,7 +431,7 @@ { "kind": "file-fork", "id": "temporal-infra-zoned-date-time", - "upstream": "temporal-rs-parity", + "upstream": "temporal-rs", "local": "packages/temporal-infra/src/socketsecurity/temporal/zoned_date_time.cc", "upstream_path": "src/builtins/core/zoned_date_time.rs", "forked_at_sha": "c003cc92325e19b26f8ee2f85e4a47d98cbcc781", @@ -444,7 +446,7 @@ { "kind": "file-fork", "id": "temporal-infra-relative-to", - "upstream": "temporal-rs-parity", + "upstream": "temporal-rs", "local": "packages/temporal-infra/src/socketsecurity/temporal/relative_to.cc", "upstream_path": "src/options/relative_to.rs", "forked_at_sha": "c003cc92325e19b26f8ee2f85e4a47d98cbcc781", @@ -458,7 +460,7 @@ { "kind": "file-fork", "id": "temporal-infra-duration-normalized", - "upstream": "temporal-rs-parity", + "upstream": "temporal-rs", "local": "packages/temporal-infra/src/socketsecurity/temporal/duration_normalized.cc", "upstream_path": "src/builtins/core/duration/normalized.rs", "forked_at_sha": "c003cc92325e19b26f8ee2f85e4a47d98cbcc781", @@ -474,7 +476,7 @@ { "kind": "file-fork", "id": "temporal-infra-primitive", - "upstream": "temporal-rs-parity", + "upstream": "temporal-rs", "local": "packages/temporal-infra/src/socketsecurity/temporal/primitive.cc", "upstream_path": "src/primitive.rs", "forked_at_sha": "c003cc92325e19b26f8ee2f85e4a47d98cbcc781", @@ -488,7 +490,7 @@ { "kind": "feature-parity", "id": "temporal-infra-pending", - "upstream": "temporal-rs-parity", + "upstream": "temporal-rs", "criticality": 9, "local_area": "packages/temporal-infra/src/socketsecurity/temporal", "conformance_test": "packages/build-infra/test/fixtures/smoke-test-modules.mjs", @@ -584,16 +586,141 @@ "criticality": 8, "notes": "ML inference runtime for embedded models. Major bumps can break model compatibility." }, + { + "kind": "version-pin", + "id": "dawn", + "upstream": "dawn", + "pinned_sha": "e935a1b57eb859db0d00c522e198711a3f313a25", + "pinned_tag": "chromium/7852", + "upgrade_policy": "track-latest", + "criticality": 5, + "notes": "Dawn — Chromium's WebGPU implementation. Submodule pinned at packages/dawn-builder/upstream/dawn at chromium/7852 branch tip. Currently scaffolding only (D1+D2 of the integration plan at .claude/plans/dawn-webgpu-integration.md); build wiring lands in D3. Dawn has no semver releases — it tracks Chromium branch numbers (~6-week cadence with Chromium milestone cuts). The pin updates on each chromium/ branch advance via the future updating-dawn skill." + }, + { + "kind": "version-pin", + "id": "libqrencode", + "upstream": "libqrencode", + "pinned_sha": "715e29fd4cd71b6e452ae0f4e36d917b43122ce8", + "pinned_tag": "v4.1.1", + "upgrade_policy": "track-latest", + "criticality": 5, + "notes": "fukuchi/libqrencode QR code encoder (C, ~6 KLOC, LGPL-2.1 with static-link allowance). Embedded into node-smol as src/socketsecurity/qrcode/qrcode_binding.cc. Pulls upstream/libqrencode/*.c into the build via prepare-external-sources copying. Replaces userland `qrcode` npm package." + }, + { + "kind": "version-pin", + "id": "md4c", + "upstream": "md4c", + "pinned_sha": "472c417005c2c71b8617de4f7b8d6b30411d78f4", + "pinned_tag": "release-0.5.3", + "upgrade_policy": "track-latest", + "criticality": 6, + "notes": "md4c CommonMark+GFM Markdown parser (C99, ~3 KLOC). Embedded into node-smol as src/socketsecurity/markdown/markdown_binding.cc. Pulls upstream/md4c/src/{md4c.c,md4c.h,entity.c,entity.h} into the build. Replaces opentui's userland `marked` dep on the hot AI-output rendering path." + }, + { + "kind": "version-pin", + "id": "tree-sitter", + "upstream": "tree-sitter", + "pinned_sha": "7f534862c3ec939c3a6ee147f7600ef5c1bf900f", + "pinned_tag": "v0.26.9", + "upgrade_policy": "track-latest", + "criticality": 6, + "notes": "tree-sitter incremental parser library (C, ~15 KLOC). Embedded into node-smol as src/socketsecurity/tree_sitter/tree_sitter_binding.cc. Pulls upstream/tree-sitter/lib/src/*.c + lib/include/*.h into the build. Per-grammar `.wasm` / `.so` loading is supported at runtime via the binding's parser interface. Replaces opentui's userland `web-tree-sitter` WASM dep for the syntax-highlighting Code renderable." + }, { "kind": "version-pin", "id": "opentui", "upstream": "opentui", - "pinned_sha": "cc94b5829ac0f6f45320562811acd8260ddc1922", - "pinned_tag": "v0.1.99", + "pinned_sha": "f464acfcba0dde0ffcd6b2728811df787a72975c", + "pinned_tag": "v0.2.15", "upgrade_policy": "track-latest", "criticality": 7, "notes": "Native opentui builder. Matches the pin in socket-tui's lockstep for consistency across the OpenTUI ecosystem." }, + { + "kind": "file-fork", + "id": "tui-infra-ansi", + "upstream": "opentui", + "local": "packages/tui-infra/src/socketsecurity/tui/ansi.cc", + "upstream_path": "packages/core/src/zig/ansi.zig", + "forked_at_sha": "f464acfcba0dde0ffcd6b2728811df787a72975c", + "criticality": 7, + "deviations": [ + "C++ instead of Zig — 1:1 port of `ANSI` namespace to `tui::ANSI`.", + "RGBA stays as raw 8-bit channels (uint8_t fg_r/g/b + bg_r/g/b on `tui::Cell`) instead of upstream v0.2.15's packed `[4]u16` with intent metadata in the high bytes. The packed layout encodes ColorIntent (rgb / indexed / default) + an ANSI palette slot so the renderer can emit `38;5;n` or `39/49` instead of always `38;2;r;g;b`. The C++ port doesn't expose indexed/default-color modes to its JS callers yet — when that surface lands, port the packed RGBA encoding alongside it.", + "Cold-path builders return `std::string` (one-shot setup writes).", + "Hot-path writers (`WriteCursorPosition`, `WriteFgRgb`, `WriteBgRgb`, `WriteAttributes`) take a caller-provided char* buffer and return bytes written so V8 FastApi specializations can target them without per-call allocation." + ], + "notes": "Embedded into node-smol as the ANSI emit surface for `node:smol-tui` (per-frame flush path)." + }, + { + "kind": "file-fork", + "id": "tui-infra-buffer", + "upstream": "opentui", + "local": "packages/tui-infra/src/socketsecurity/tui/buffer.cc", + "upstream_path": "packages/core/src/zig/buffer-methods.zig", + "forked_at_sha": "f464acfcba0dde0ffcd6b2728811df787a72975c", + "criticality": 7, + "deviations": [ + "C++ instead of Zig.", + "Cell POD trimmed to the fields the diff renderer actually uses (codepoint + fg/bg RGB + attrs); upstream OptimizedBuffer carries extra fields (id, respectAlpha, scissor stack) not needed by the embedded renderer." + ], + "notes": "Embedded into node-smol as the cell-buffer storage backing the `node:smol-tui` renderer." + }, + { + "kind": "file-fork", + "id": "tui-infra-renderer", + "upstream": "opentui", + "local": "packages/tui-infra/src/socketsecurity/tui/renderer.cc", + "upstream_path": "packages/core/src/zig/renderer.zig", + "forked_at_sha": "f464acfcba0dde0ffcd6b2728811df787a72975c", + "criticality": 7, + "deviations": [ + "C++ instead of Zig — port of `CliRenderer` (next + prev double-buffer with diff flush) to `tui::Renderer`.", + "Single Flush(char*, size_t) entry point; caller supplies the output Uint8Array (zero allocation per frame).", + "kFlushOverflow sentinel return on buffer-too-small (caller retries with a bigger buffer); upstream Zig uses a fixed allocator instead." + ], + "notes": "Embedded into node-smol as the per-frame flush path of `node:smol-tui`." + }, + { + "kind": "version-pin", + "id": "unicode-data", + "upstream": "opentui", + "pinned_sha": "17.0.0", + "pinned_tag": "17.0.0", + "upgrade_policy": "track-latest", + "criticality": 6, + "notes": "Unicode Character Database version embedded in tui-infra/src/socketsecurity/tui/width_data.cc (EastAsianWidth.txt + emoji-data.txt range tables). Re-run packages/node-smol-builder/scripts/generate-width-data.mts when bumping. Tracked under the `opentui` upstream key for cascade alignment, though the actual upstream is unicode.org. Fleet-wide alignment: ultrathink's acorn parser pins Unicode 17.0 across Go / C++ (ICU 78.2) / Rust (unicode-id-start 1.4.0) / TS (@unicode/unicode-17.0.0); we ride the same major version so emoji/CJK width is consistent across the fleet." + }, + { + "kind": "file-fork", + "id": "tui-infra-renderables", + "upstream": "opentui", + "local": "packages/tui-infra/src/socketsecurity/tui/renderables.cc", + "upstream_path": "packages/core/src/lib/border.ts + packages/core/src/renderables/{Box,Text}.ts", + "forked_at_sha": "f464acfcba0dde0ffcd6b2728811df787a72975c", + "criticality": 7, + "deviations": [ + "C++ instead of TypeScript — collapses the Renderable class hierarchy to pure drawing primitives `tui::DrawBox()` + `tui::DrawTextWrapped()`. JS commit phase passes a computed rectangle (Yoga layout already lives in the smol-tui Yoga binding).", + "Border glyph table uses an 11-slot codepoint array per BorderStyle, matching opentui's borderCharsToArray output. Junction glyphs (slots 6-10) are forward-compat for a future table renderer; DrawBox currently reads slots 0-5 only.", + "Word wrap uses ASCII space + tab boundaries; long-word overflow hard-splits at the cell boundary. Full Unicode word segmentation (UAX #29) is a future helper alongside the planned StringWidth port (Unicode 16.0.0 width tables).", + "Title / bottomTitle / titleAlignment slots from upstream BoxDrawOptions are not yet ported — the title slot is a separate DrawTextWrapped overlay call in the consumer." + ], + "notes": "Embedded into node-smol as `node:smol-tui.rendererDrawBox` / `.rendererDrawTextWrapped`. Forms the C++ hot path that React/Solid host-config callbacks dispatch into (B4/B5)." + }, + { + "kind": "file-fork", + "id": "tui-infra-mouse", + "upstream": "opentui", + "local": "packages/tui-infra/src/socketsecurity/tui/mouse.cc", + "upstream_path": "packages/core/src/lib/parse.mouse.ts", + "forked_at_sha": "f464acfcba0dde0ffcd6b2728811df787a72975c", + "criticality": 7, + "deviations": [ + "C++ instead of TypeScript — 1:1 port of the SGR + X10 mouse-sequence decoder including drag-state tracking (press → motion → release becomes DOWN → DRAG → DRAG_END + DROP).", + "Process-wide handle registry (`uint32_t` → `MouseParser*`) under a single mutex; upstream uses a per-process singleton object." + ], + "notes": "Embedded into node-smol as the mouse decode surface of `node:smol-tui`." + }, { "kind": "version-pin", "id": "ultraviolet", diff --git a/.config/vitest.config.mts b/.config/vitest.config.mts index f901628e5..54483f6a8 100644 --- a/.config/vitest.config.mts +++ b/.config/vitest.config.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/no-default-export -- vitest's CLI auto-discovers configs via default import; the rule's `export { name }` form breaks discovery. */ /** * Shared Vitest configuration for simple packages. * Used by packages with basic test needs. @@ -8,6 +7,7 @@ */ import { defineConfig } from 'vitest/config' +// oxlint-disable-next-line socket/no-default-export -- vitest's CLI auto-discovers configs via default import; the rule's export default defineConfig({ // Keep vitest's cache under node_modules so `pnpm install` // clears it automatically — no dedicated clean step. diff --git a/.github/cache-versions.json b/.github/cache-versions.json index abd560e3e..994f8615c 100644 --- a/.github/cache-versions.json +++ b/.github/cache-versions.json @@ -2,18 +2,18 @@ "$schema": "http://json-schema.org/draft-07/schema#", "description": "Centralized cache version management for CI workflows. Bump versions to force-invalidate all caches for a package.", "versions": { - "binflate": "v133", - "binject": "v187", - "binpress": "v171", - "curl": "v39", - "ink": "v14", - "iocraft": "v33", - "lief": "v75", - "models": "v34", - "node-smol": "v312", - "onnxruntime": "v33", - "opentui": "v15", - "stubs": "v107", - "yoga-layout": "v35" + "binflate": "v134", + "binject": "v188", + "binpress": "v172", + "curl": "v40", + "ink": "v15", + "iocraft": "v34", + "lief": "v76", + "models": "v35", + "node-smol": "v313", + "onnxruntime": "v34", + "opentui": "v16", + "stubs": "v108", + "yoga-layout": "v36" } } diff --git a/.gitmodules b/.gitmodules index b16a352dd..5a82f6ae1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ -# node-26.1.0 sha256:ccaf9bfea12ec3d2beb36f5a1d54483f2620ad9de007e551fb8640ed82d29989 +# node-26.2.0 sha256:37032a75a677f063dbe22f0cc72249a3a2faff2ce71056b63771d6967bd63384 [submodule "packages/node-smol-builder/upstream/node"] path = packages/node-smol-builder/upstream/node url = https://github.com/nodejs/node.git @@ -82,7 +82,7 @@ url = https://github.com/uNetworking/uWebSockets.git ignore = dirty shallow = true -# opentui-0.1.99 +# opentui-0.2.15 [submodule "packages/opentui-builder/upstream/opentui"] path = packages/opentui-builder/upstream/opentui url = https://github.com/anomalyco/opentui.git @@ -118,13 +118,7 @@ url = https://github.com/npm/node-semver.git ignore = dirty shallow = true -# temporal-v0.1.0 (locked: pinned by upstream Node's deps/crates/Cargo.toml; bump only via updating-node) -[submodule "packages/node-smol-builder/upstream/temporal"] - path = packages/node-smol-builder/upstream/temporal - url = https://github.com/boa-dev/temporal.git - ignore = dirty - shallow = true -# temporal-v0.2.3 (track-latest: bump independently via updating-temporal-infra) +# temporal-v0.2.3 (canonical temporal submodule; track-latest, bumped via /updating-temporal-infra and the /updating-node cascade) [submodule "packages/temporal-infra/upstream/temporal"] path = packages/temporal-infra/upstream/temporal url = https://github.com/boa-dev/temporal.git @@ -154,3 +148,27 @@ url = https://github.com/litespeedtech/ls-qpack.git ignore = dirty shallow = true +# md4c-0.5.3 +[submodule "packages/node-smol-builder/upstream/md4c"] + path = packages/node-smol-builder/upstream/md4c + url = https://github.com/mity/md4c + ignore = dirty + shallow = true +# tree-sitter-0.26.9 +[submodule "packages/node-smol-builder/upstream/tree-sitter"] + path = packages/node-smol-builder/upstream/tree-sitter + url = https://github.com/tree-sitter/tree-sitter + ignore = dirty + shallow = true +# libqrencode-4.1.1 +[submodule "packages/node-smol-builder/upstream/libqrencode"] + path = packages/node-smol-builder/upstream/libqrencode + url = https://github.com/fukuchi/libqrencode + ignore = dirty + shallow = true +# dawn-chromium/7852 +[submodule "packages/dawn-builder/upstream/dawn"] + path = packages/dawn-builder/upstream/dawn + url = https://dawn.googlesource.com/dawn + ignore = dirty + shallow = true diff --git a/.socket-lib.json b/.socket-lib.json index fd9b9d897..c8cbfa48d 100644 --- a/.socket-lib.json +++ b/.socket-lib.json @@ -30,6 +30,8 @@ "encodeURIComponent": "encodeComponent" }, "nodeInternalOnly": [ + "DataViewPrototypeGetInt32", + "DataViewPrototypeGetUint32", "SafeMap", "SafePromise", "SafePromiseAllReturnVoid", @@ -37,6 +39,8 @@ "SafeSet", "SafeWeakMap", "SafeWeakSet", + "Uint8Array", + "Uint8ArrayPrototypeSubarray", "globalThis", "hardenRegExp" ] diff --git a/docs/additions/lib/smol-keymap.js.md b/docs/additions/lib/smol-keymap.js.md new file mode 100644 index 000000000..d994003bb --- /dev/null +++ b/docs/additions/lib/smol-keymap.js.md @@ -0,0 +1,130 @@ +# smol-keymap.js -- Public API for the keymap matcher (node:smol-keymap) + +## What This File Does + +This is the entry point for `require('node:smol-keymap')`. It exposes +a chord-aware keymap matcher backed by a C++ state machine. + +Replaces the @opentui/keymap matcher hot path. The full keymap engine +(layers, extension contexts, command catalog, runtime emitter, +activation service) stays in userland TS; only the per-keystroke +match runs through this binding. + +## How It Fits Together + +``` +require('node:smol-keymap') -> this file (smol-keymap.js) + -> internalBinding('smol_keymap') (C++ native binding) +``` + +The C++ binding lives at +`additions/source-patched/src/socketsecurity/keymap/keymap_binding.cc`. +Bindings are parsed once into a canonical `ctrl+shift+alt+meta+` +match string per chord step; matchKey() builds the canonical key for +the input keystroke and does a string compare against the candidate +bindings (filtered by current chord position). + +## Public API + +```ts +import { + createKeymap, + destroyKeymap, + matchKey, + resetChord, + modifier, + getModifierBits, +} from 'node:smol-keymap' + +// Parse rules JSON. Returns handle (>0) on success, 0 on parse error. +const km = createKeymap(JSON.stringify({ + 'ctrl+a': 'select-all', + 'ctrl+x ctrl+s': 'save', + 'ctrl+x ctrl+c': 'exit', + 'esc': 'cancel', +})) + +// Match keystroke. Returns command string on a complete match, null +// otherwise. Mid-chord steps (e.g. just `ctrl+x` of a `ctrl+x ctrl+s` +// chord) return null — keep calling on subsequent keystrokes. +matchKey(km, 'a', modifier.CTRL) // 'select-all' +matchKey(km, 'x', modifier.CTRL) // null (chord in progress) +matchKey(km, 's', modifier.CTRL) // 'save' (chord complete) + +// Build modifier bits from an event-like object. +const bits = getModifierBits({ ctrl: true, shift: false }) // 1 + +// Reset pending chord state (e.g. after a timeout). +resetChord(km) + +// Release. +destroyKeymap(km) +``` + +### Modifier bits + +```ts +modifier.CTRL // 1 << 0 +modifier.SHIFT // 1 << 1 +modifier.ALT // 1 << 2 +modifier.META // 1 << 3 +``` + +### Rules format + +Each rule is `" [ ...]"` → `""`. A chord is one +or more modifiers + a key, joined with `+`: + +``` +"ctrl+a" // ctrl-a +"ctrl+shift+a" // ctrl-shift-a (any modifier order works) +"esc" // bare key +"ctrl+x ctrl+s" // two-step chord (emacs-style) +"a b c" // three-step plain chord +``` + +Modifier name aliases (case-insensitive): +- `ctrl` | `control` | `c` +- `shift` | `s` +- `alt` | `option` | `opt` +- `meta` | `cmd` | `command` | `super` | `win` + +## Design Choices + +**Canonical match keys at parse time.** Each chord step is normalized +to `ctrl+shift+alt+meta+` (all four modifiers in fixed order, +all lowercase) when the keymap is created. matchKey() composes the +same canonical form for the input keystroke and does a string compare +against candidates. No regex, no dispatch table walking. + +**Process-wide handle registry.** Same shape as the mouse parser / +renderer / yoga bindings in tui_binding.cc. JS holds an opaque +uint32_t handle; the C++ side owns the Keymap struct + its pending- +chord state. One mutex per registry; no contention because keymaps +are per-app-instance, not per-call. + +**Permissive JSON parser inside the binding.** The rules string is +typically small (<10 KB) and parsed once at keymap creation. Inlining +a small JSON parser avoids a JS-side `JSON.parse` round trip on +binding startup. Format support: top-level object with string keys +and string values, `\"` and `\\` escapes. + +**No FastApi yet.** matchKey returns a string (or null) which V8 Fast +API doesn't accept cleanly. The slow-path dispatch cost (~50 ns) is +still well under the time-budget for a keystroke event. If profiling +ever shows it dominates, a uint32-encoded-command-index variant could +move to Fast API. + +## Where the Real Work Happens + +Hot path in `keymap_binding.cc`'s `MatchKey`: +- BuildMatchKey: ~20-byte string append (no allocation for keys + shorter than std::string's SSO buffer, which is 15-22 bytes on + current libc++/libstdc++). +- Candidate scan: linear walk over `pending_indices` (typically 1-5 + entries when mid-chord; up to `bindings.size()` on first keystroke). +- String compare per candidate: `std::string::operator==` calls + memcmp internally. + +Total per-keystroke time: ~5-50 ns depending on chord depth and +binding count. For comparison, the TS matcher is ~100-500 ns. diff --git a/docs/additions/lib/smol-markdown.js.md b/docs/additions/lib/smol-markdown.js.md new file mode 100644 index 000000000..84151b2f0 --- /dev/null +++ b/docs/additions/lib/smol-markdown.js.md @@ -0,0 +1,137 @@ +# smol-markdown.js -- Public API for the Markdown parser (node:smol-markdown) + +## What This File Does + +This is the entry point for `require('node:smol-markdown')`. It exposes +a CommonMark + GFM Markdown parser backed by [md4c](https://github.com/mity/md4c) +(C99, ~3 KLOC, MIT). Replaces userland `marked` / `remark` / +`markdown-it` on the AI-output rendering hot path. + +## How It Fits Together + +``` +require('node:smol-markdown') -> this file (smol-markdown.js) + -> internalBinding('smol_markdown') (C++ native binding) + -> md4c (vendored at upstream/md4c, copied into + src/socketsecurity/markdown/{md4c.c,entity.c} at build) +``` + +The C++ binding lives at +`additions/source-patched/src/socketsecurity/markdown/markdown_binding.cc`. +md4c is callback-driven (enter/leave block, enter/leave span, text). +The binding collects events into a flat C++ vector then materializes +them as a JS `Array<[code, payload]>` in one pass. + +## Public API + +```ts +import { + parseMarkdown, + parseTree, + blockType, + spanType, + textType, + eventCategory, +} from 'node:smol-markdown' + +// parseMarkdown(text, flags?) -> flat event stream. +// +// Each event is [code, payload]: +// code: (category << 12) | enum_value +// payload: undefined | string (text/content) | number (heading level) +const events = parseMarkdown('# Hello **world**', 'github') + +// parseTree(text, flags?) -> nested tree. +// +// Convenience wrapper that reconstructs an object graph from the +// event stream. Use parseMarkdown directly for hot paths. +const tree = parseTree('# Hello\n\n- a\n- b') +// { +// type: 'doc', +// children: [ +// { kind: 'block', type: blockType.H, level: 1, children: [ +// { kind: 'text', type: textType.NORMAL, text: 'Hello' } +// ]}, +// { kind: 'block', type: blockType.UL, children: [ +// { kind: 'block', type: blockType.LI, children: [ +// { kind: 'block', type: blockType.P, children: [ +// { kind: 'text', type: textType.NORMAL, text: 'a' } +// ]} +// ]}, +// ... +// ]} +// ] +// } +``` + +### Event codes + +```ts +eventCategory.BLOCK_ENTER // 0x0000 +eventCategory.BLOCK_LEAVE // 0x1000 +eventCategory.SPAN_ENTER // 0x2000 +eventCategory.SPAN_LEAVE // 0x3000 +eventCategory.TEXT // 0x4000 + +// Low 12 bits hold one of: +blockType.DOC, blockType.QUOTE, blockType.UL, blockType.OL, blockType.LI, +blockType.HR, blockType.H, blockType.CODE, blockType.HTML, blockType.P, +blockType.TABLE, blockType.THEAD, blockType.TBODY, blockType.TR, +blockType.TH, blockType.TD + +spanType.EM, spanType.STRONG, spanType.A, spanType.IMG, spanType.CODE, +spanType.DEL, spanType.LATEXMATH, spanType.LATEXMATH_DISPLAY, +spanType.WIKILINK, spanType.U + +textType.NORMAL, textType.NULLCHAR, textType.ENTITY, textType.CODE, +textType.HTML, textType.LATEXMATH +``` + +### Flags + +The second arg to `parseMarkdown` / `parseTree` is a comma-separated +list of dialect flags (case-insensitive): + +| Token | md4c MD_FLAG_* | +| --- | --- | +| `collapse_whitespace` | COLLAPSEWHITESPACE | +| `permissive_atx_headers` | PERMISSIVEATXHEADERS | +| `permissive_url_autolinks` | PERMISSIVEURLAUTOLINKS | +| `permissive_email_autolinks` | PERMISSIVEEMAILAUTOLINKS | +| `permissive_www_autolinks` | PERMISSIVEWWWAUTOLINKS | +| `no_indented_code_blocks` | NOINDENTEDCODEBLOCKS | +| `no_html_blocks` | NOHTMLBLOCKS | +| `no_html_spans` | NOHTMLSPANS | +| `tables` | TABLES | +| `strikethrough` | STRIKETHROUGH | +| `tasklists` | TASKLISTS | +| `latex_math_spans` | LATEXMATHSPANS | +| `wikilinks` | WIKILINKS | +| `underline` | UNDERLINE | +| `hard_soft_breaks` | HARD_SOFT_BREAKS | +| `commonmark` | (empty set — strict CommonMark) | +| `github` | MD_DIALECT_GITHUB (tables + strikethrough + tasklists + autolinks) | + +## Design Choices + +**Flat event stream over JS object graph.** Building a V8 object per +node from C++ would be ~3-4 allocations per node (object, type, children +array, content). For a typical AI response (a few hundred nodes) that +is several hundred handles. The flat `[code, payload]` array is two +allocations per event (outer + inner array) and reconstructs to a tree +in JS in one linear pass — overall faster and gives the JS layer +freedom to either consume the stream directly (renderer hot path) or +materialize the tree (test/debugging tools). + +**md4c chosen over markdown-it / cmark-gfm.** md4c is the smallest +spec-compliant C parser in the CommonMark Speed/Size matrix +(https://github.com/mity/md4c#why-yet-another-markdown-parser-or-renderer). +~3 KLOC total, no dependencies beyond libc, GFM extensions land via +flags rather than a separate fork. + +## Where the Real Work Happens + +The binding's design rationale is at the top of +`markdown_binding.cc`. Upstream md4c lives at +`packages/node-smol-builder/upstream/md4c/` (submodule, pinned in +`.config/lockstep.json`). diff --git a/docs/additions/lib/smol-qrcode.js.md b/docs/additions/lib/smol-qrcode.js.md new file mode 100644 index 000000000..048cf5aa5 --- /dev/null +++ b/docs/additions/lib/smol-qrcode.js.md @@ -0,0 +1,91 @@ +# smol-qrcode.js -- QR code encoder (node:smol-qrcode) + +## What This File Does + +This is the entry point for `require('node:smol-qrcode')`. It exposes +a QR code encoder backed by libqrencode v4.1.1 (C, vendored as a +submodule). Replaces the userland `qrcode` npm package. + +## How It Fits Together + +``` +require('node:smol-qrcode') -> this file (smol-qrcode.js) + -> internalBinding('smol_qrcode') (C++ binding) + -> libqrencode (vendored at upstream/libqrencode, compiled + statically into the smol binary) +``` + +The C++ binding lives at +`additions/source-patched/src/socketsecurity/qrcode/qrcode_binding.cc`. +libqrencode sources are copied from +`upstream/libqrencode/` into `additions/.../qrcode/libqrencode/` at +build time. Only the library `.c` files are listed in node.gyp — +`qrenc.c` (the CLI tool with `main()`) is copied but not compiled. + +## Public API + +```ts +import { encode, ecLevel } from 'node:smol-qrcode' + +// encode(text, ecLevel?) -> { width, matrix } +const { width, matrix } = encode('https://example.com', ecLevel.M) +// width: side length in cells (e.g. 21 for version-1, 25 for version-2, ...) +// matrix: Uint8Array of length width*width +// each byte's bit 0 = "is black cell" (1) | "is white cell" (0) +// higher bits are libqrencode's internal state; mask with `& 1` + +for (let y = 0; y < width; y++) { + for (let x = 0; x < width; x++) { + const black = matrix[y * width + x] & 1 + // render cell at (x, y)... + } +} +``` + +### EC levels + +```ts +ecLevel.L // 0 — ~7% error recovery +ecLevel.M // 1 — ~15% (default) +ecLevel.Q // 2 — ~25% +ecLevel.H // 3 — ~30% +``` + +## Design Choices + +**libqrencode chosen over a 1:1 port of opentui's TS encoder.** The +TS source (`packages/qrcode/src/lib/qrcode.ts`, 1250 lines) plus the +Shift-JIS data table (6947 lines, both upstream) is ~8.2 KLOC to +maintain in C++. libqrencode is the canonical C QR encoder, 6 KLOC, +maintained for 20+ years, LGPL-2.1 with explicit static-link allowance. +We vendor + statically link; the binding glue is ~100 lines. + +**8-bit-mode encoding only.** Pass any UTF-8 string as bytes; +libqrencode's `QRcode_encodeString8bit` handles version selection +automatically (picks the smallest QR version that fits). For +alphanumeric mode or kanji mode, a future API addition would expose +`QRcode_encodeString` with the mode hint; the current shape covers +~95% of TUI QR-code use cases (URLs, payment intents, configs). + +**JS owns the matrix buffer.** The binding allocates a V8 ArrayBuffer ++ Uint8Array of `width*width` bytes, memcpys libqrencode's output +into it, then frees the libqrencode QRcode struct. No per-cell +crossings; JS-side render loops can iterate the matrix directly with +typed-array access (~1 cycle per cell). + +**No FastApi.** encode() is called once per QR code (not per frame). +The slow-path dispatch cost is dwarfed by the encoder's actual work +(~milliseconds for a moderate-size input). + +## Where the Real Work Happens + +libqrencode upstream: + +The core encoder pipeline lives in: +- `qrinput.c`: input encoding + segment splitting +- `qrencode.c`: Reed-Solomon error correction + matrix layout +- `mask.c`: mask pattern selection (the 8 standard QR masks) +- `qrspec.c`: per-version metadata tables + +Pinned in `.config/lockstep.json` as the `libqrencode` version-pin +row at SHA 715e29f (v4.1.1). diff --git a/docs/additions/lib/smol-tree-sitter.js.md b/docs/additions/lib/smol-tree-sitter.js.md new file mode 100644 index 000000000..b164fa10c --- /dev/null +++ b/docs/additions/lib/smol-tree-sitter.js.md @@ -0,0 +1,106 @@ +# smol-tree-sitter.js -- Public API for tree-sitter (node:smol-tree-sitter) + +## What This File Does + +This is the entry point for `require('node:smol-tree-sitter')`. It +exposes the tree-sitter incremental parser library (C, MIT, vendored +as a submodule at `upstream/tree-sitter` and built into the smol +binary). Replaces userland `web-tree-sitter` WASM dep on the syntax- +highlighting Code renderable path. + +## How It Fits Together + +``` +require('node:smol-tree-sitter') -> this file (smol-tree-sitter.js) + -> internalBinding('smol_tree_sitter') (C++ native binding) + -> tree-sitter (vendored at upstream/tree-sitter; lib/src/lib.c is + the umbrella TU that #includes every other .c) +``` + +The C++ binding lives at +`additions/source-patched/src/socketsecurity/tree_sitter/tree_sitter_binding.cc`. +Languages (grammars) are loaded at runtime via `dlopen` from a `.dylib` +/ `.so` / `.dll` built from a tree-sitter grammar repo's `src/parser.c`. + +## Public API + +```ts +import { + freeLanguage, + loadLanguage, + parse, +} from 'node:smol-tree-sitter' + +// loadLanguage(path, symbol) -> handle | 0 +// dlopens `path` and resolves `symbol` (the language factory, typically +// `tree_sitter_`). Returns an opaque integer handle. +const js = loadLanguage( + '/usr/local/lib/tree-sitter-javascript.dylib', + 'tree_sitter_javascript', +) +if (js === 0) { + throw new Error('failed to load tree-sitter-javascript') +} + +// parse(handle, source) -> Array<[type, startByte, endByte, namedChildCount]> +// Pre-order traversal of named nodes. Skips anonymous punctuation +// nodes (they don't matter for highlighters). +const nodes = parse(js, 'const x = 42;') +// [ +// ['program', 0, 13, 1], +// ['lexical_declaration', 0, 13, 1], +// ['variable_declarator', 6, 12, 2], +// ['identifier', 6, 7, 0], +// ['number', 10, 12, 0], +// ] + +// freeLanguage(handle): release the dlopen handle. +freeLanguage(js) +``` + +## Design Choices + +**dlopen-based grammar loading.** tree-sitter grammars are +distributed as platform-specific shared libraries. WASM grammars +(via `web-tree-sitter`) work in browsers but cost ~500 KB of WASM +runtime overhead per parser. Native `dlopen` is one syscall and +~100 µs of overhead, with zero per-parse runtime cost. + +**Flat span list output.** Same rationale as `node:smol-markdown` — +building a V8 object graph from C++ per node would multiply handle +count by 4-5x. The flat `[type, start, end, child_count]` array is +two allocations per node (outer + inner array). Consumers +interested in the full tree structure reconstruct it from +`child_count` + pre-order ordering in JS (one linear pass). + +**Skip anonymous nodes.** tree-sitter emits both named nodes (e.g. +`function_declaration`) and anonymous punctuation nodes (e.g. `{`, +`,`). Highlighters only care about named nodes. Filtering at the C++ +boundary keeps the JS work proportional to the highlighting cost. + +**No query support yet.** tree-sitter's "query" feature +(highlights.scm + injections.scm) is the right primitive for a +syntax-highlight consumer. That's a follow-up binding — the basic +parse + walk shape ships first to validate the loading mechanism. + +## Building Grammars + +Grammar repos publish their generated `parser.c` in `src/`. To build +a loadable .dylib: + +```sh +git clone https://github.com/tree-sitter/tree-sitter-javascript +cd tree-sitter-javascript +cc -shared -fPIC -O2 -Isrc src/parser.c src/scanner.c \ + -o tree-sitter-javascript.dylib +``` + +The library is then ready to pass to `loadLanguage`. Cross-compile +for the target OS/arch as needed. + +## Where the Real Work Happens + +The binding's design rationale is at the top of +`tree_sitter_binding.cc`. Upstream tree-sitter lives at +`packages/node-smol-builder/upstream/tree-sitter/` (submodule, pinned +in `.config/lockstep.json`). diff --git a/docs/additions/lib/smol-webgpu.js.md b/docs/additions/lib/smol-webgpu.js.md new file mode 100644 index 000000000..ecff2f583 --- /dev/null +++ b/docs/additions/lib/smol-webgpu.js.md @@ -0,0 +1,86 @@ +# smol-webgpu.js -- Public API for WebGPU (node:smol-webgpu) — STUB + +## What This File Does + +This is the entry point for `require('node:smol-webgpu')`. It exposes +the W3C WebGPU surface so userland code that imports it resolves. +**Currently a stub** — every method except `isAvailable()` throws +until Dawn integration lands. + +## How It Fits Together + +``` +require('node:smol-webgpu') -> this file (smol-webgpu.js) + -> internalBinding('smol_webgpu') (C++ stub binding) + -> (future) Dawn (Chromium's WebGPU implementation) +``` + +The C++ binding lives at +`additions/source-patched/src/socketsecurity/webgpu/webgpu_binding.cc`. +Every method except `isAvailable()` calls `ThrowPending()` which raises +an Error pointing at the design doc. `isAvailable()` returns `false`. + +When Dawn is wired (Phase C work), this binding gets replaced with a +real implementation that wraps Dawn's `src/dawn/node/binding/` surface. +Userland code that uses the API with `isAvailable()` guards will work +unchanged. + +## Public API + +```ts +import { + isAvailable, + createInstance, + requestAdapter, + requestDevice, + getPreferredCanvasFormat, +} from 'node:smol-webgpu' + +// Feature detection — always check before using the rest. +if (!isAvailable()) { + // Fall back to userland shim, or skip WebGPU features. + return +} + +const adapter = await requestAdapter({ powerPreference: 'high-performance' }) +const device = await adapter.requestDevice() +const format = getPreferredCanvasFormat() +// ... use device per the WebGPU IDL +``` + +## Design Choices + +**Stub-first, Dawn-later.** Dawn is ~436 MB cloned and pulls Tint + +SPIRV-Tools + per-platform GPU drivers; first compile is hours. +Shipping the stub now lets: + +1. Userland code reference `node:smol-webgpu` in imports without the + resolver crashing. +2. Feature-detection patterns (the `isAvailable()` guard) get + established before users start writing WebGPU code. +3. Tooling (TypeScript types, doc generators, linters) discover the + surface. + +When Dawn lands, only the C++ binding changes; this file and consumer +code stay the same. + +**`isAvailable()` returns false today.** This is the contract: callers +that respect it never hit the throwing code paths in the stub. Bad +callers (those that skip the check) get a structured error pointing at +the design doc — actionable rather than `TypeError: undefined is not a +function`. + +**Surface mirrors W3C WebGPU IDL.** Function names are 1:1 with +. When Dawn ships, the surface grows to +include GPUAdapter / GPUDevice / GPUCommandEncoder / GPURenderPassEncoder +/ etc. — all reachable via the existing entry points. + +## Where the Real Work Happens (future) + +- Dawn upstream: +- Dawn's Node.js binding: +- Integration design: `.claude/plans/opentui-smol-tui-completion.md` + Phase C. + +The integration is a separate multi-week effort tracked in the plan +doc. This stub is the contract; Dawn is the implementation. diff --git a/packages/bin-infra/lib/build-stubs.mts b/packages/bin-infra/lib/build-stubs.mts index 86b23b3f6..0c2212e8f 100644 --- a/packages/bin-infra/lib/build-stubs.mts +++ b/packages/bin-infra/lib/build-stubs.mts @@ -33,7 +33,7 @@ import { verifyReleaseChecksum } from 'build-infra/lib/release-checksums/core' import { ensureCurl } from 'curl-builder/lib/ensure-curl' import { WIN32 } from '@socketsecurity/lib-stable/constants/platform' -import { envAsBoolean } from '@socketsecurity/lib-stable/env' +import { envAsBoolean } from '@socketsecurity/lib-stable/env/boolean' import { getCI } from '@socketsecurity/lib-stable/env/ci' import { safeDelete, safeMkdir } from '@socketsecurity/lib-stable/fs/safe' import { getDefaultLogger } from '@socketsecurity/lib-stable/logger' diff --git a/packages/bin-infra/test/pe-version-guards.test.mts b/packages/bin-infra/test/pe-version-guards.test.mts index c09df498a..4faf3a083 100644 --- a/packages/bin-infra/test/pe-version-guards.test.mts +++ b/packages/bin-infra/test/pe-version-guards.test.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/prefer-exists-sync -- every stat call in this file consumes stats.size to assert the synthesized PE fixtures are non-empty before the C parser sees them. */ /** * @fileoverview Tests for PE VS_VERSION_INFO reader scan guards * @@ -178,6 +177,7 @@ describe('pE VS_VERSION_INFO scan guards', () => { const filePath = await writeTempPE('invalid-dos', pe) // The file should exist but version extraction should fail gracefully + // oxlint-disable-next-line socket/prefer-exists-sync -- every stat call in this file consumes stats.size to assert the synthesized PE fixtures are non-empty before the C parser sees them. const stat = await fs.stat(filePath) expect(stat.size).toBeGreaterThan(0) @@ -189,6 +189,7 @@ describe('pE VS_VERSION_INFO scan guards', () => { const pe = createMinimalPE({ invalidPE: true }) const filePath = await writeTempPE('invalid-pe', pe) + // oxlint-disable-next-line socket/prefer-exists-sync -- every stat call in this file consumes stats.size to assert the synthesized PE fixtures are non-empty before the C parser sees them. const stat = await fs.stat(filePath) expect(stat.size).toBeGreaterThan(0) }) @@ -199,6 +200,7 @@ describe('pE VS_VERSION_INFO scan guards', () => { const pe = createMinimalPE({ numSections: 5 }) const filePath = await writeTempPE('normal-sections', pe) + // oxlint-disable-next-line socket/prefer-exists-sync -- every stat call in this file consumes stats.size to assert the synthesized PE fixtures are non-empty before the C parser sees them. const stat = await fs.stat(filePath) expect(stat.size).toBeGreaterThan(0) }) @@ -207,6 +209,7 @@ describe('pE VS_VERSION_INFO scan guards', () => { const pe = createMinimalPE({ numSections: 100 }) const filePath = await writeTempPE('max-sections', pe) + // oxlint-disable-next-line socket/prefer-exists-sync -- every stat call in this file consumes stats.size to assert the synthesized PE fixtures are non-empty before the C parser sees them. const stat = await fs.stat(filePath) expect(stat.size).toBeGreaterThan(0) }) @@ -218,6 +221,7 @@ describe('pE VS_VERSION_INFO scan guards', () => { // Guard at line 1661: `i < number_of_sections && i < 100` // This limits iteration to 100 even if numSections > 100 + // oxlint-disable-next-line socket/prefer-exists-sync -- every stat call in this file consumes stats.size to assert the synthesized PE fixtures are non-empty before the C parser sees them. const stat = await fs.stat(filePath) expect(stat.size).toBeGreaterThan(0) }) @@ -228,6 +232,7 @@ describe('pE VS_VERSION_INFO scan guards', () => { const pe = createMinimalPE({ numResourceEntries: 3 }) const filePath = await writeTempPE('normal-entries', pe) + // oxlint-disable-next-line socket/prefer-exists-sync -- every stat call in this file consumes stats.size to assert the synthesized PE fixtures are non-empty before the C parser sees them. const stat = await fs.stat(filePath) expect(stat.size).toBeGreaterThan(0) }) @@ -236,6 +241,7 @@ describe('pE VS_VERSION_INFO scan guards', () => { const pe = createMinimalPE({ numResourceEntries: 100 }) const filePath = await writeTempPE('max-entries', pe) + // oxlint-disable-next-line socket/prefer-exists-sync -- every stat call in this file consumes stats.size to assert the synthesized PE fixtures are non-empty before the C parser sees them. const stat = await fs.stat(filePath) expect(stat.size).toBeGreaterThan(0) }) @@ -245,6 +251,7 @@ describe('pE VS_VERSION_INFO scan guards', () => { const pe = createMinimalPE({ numResourceEntries: 101 }) const filePath = await writeTempPE('too-many-entries', pe) + // oxlint-disable-next-line socket/prefer-exists-sync -- every stat call in this file consumes stats.size to assert the synthesized PE fixtures are non-empty before the C parser sees them. const stat = await fs.stat(filePath) expect(stat.size).toBeGreaterThan(0) }) @@ -255,6 +262,7 @@ describe('pE VS_VERSION_INFO scan guards', () => { const pe = createMinimalPE({ versionDataSize: 200 }) const filePath = await writeTempPE('normal-version-size', pe) + // oxlint-disable-next-line socket/prefer-exists-sync -- every stat call in this file consumes stats.size to assert the synthesized PE fixtures are non-empty before the C parser sees them. const stat = await fs.stat(filePath) expect(stat.size).toBeGreaterThan(0) }) @@ -264,6 +272,7 @@ describe('pE VS_VERSION_INFO scan guards', () => { const pe = createMinimalPE({ versionDataSize: 40 }) const filePath = await writeTempPE('too-small-version', pe) + // oxlint-disable-next-line socket/prefer-exists-sync -- every stat call in this file consumes stats.size to assert the synthesized PE fixtures are non-empty before the C parser sees them. const stat = await fs.stat(filePath) expect(stat.size).toBeGreaterThan(0) }) @@ -273,6 +282,7 @@ describe('pE VS_VERSION_INFO scan guards', () => { const pe = createMinimalPE({ versionDataSize: 70_000 }) const filePath = await writeTempPE('too-large-version', pe) + // oxlint-disable-next-line socket/prefer-exists-sync -- every stat call in this file consumes stats.size to assert the synthesized PE fixtures are non-empty before the C parser sees them. const stat = await fs.stat(filePath) expect(stat.size).toBeGreaterThan(0) }) @@ -281,6 +291,7 @@ describe('pE VS_VERSION_INFO scan guards', () => { const pe = createMinimalPE({ versionDataSize: 65_536 }) const filePath = await writeTempPE('max-version-size', pe) + // oxlint-disable-next-line socket/prefer-exists-sync -- every stat call in this file consumes stats.size to assert the synthesized PE fixtures are non-empty before the C parser sees them. const stat = await fs.stat(filePath) expect(stat.size).toBeGreaterThan(0) }) @@ -289,6 +300,7 @@ describe('pE VS_VERSION_INFO scan guards', () => { const pe = createMinimalPE({ versionDataSize: 52 }) const filePath = await writeTempPE('min-version-size', pe) + // oxlint-disable-next-line socket/prefer-exists-sync -- every stat call in this file consumes stats.size to assert the synthesized PE fixtures are non-empty before the C parser sees them. const stat = await fs.stat(filePath) expect(stat.size).toBeGreaterThan(0) }) @@ -300,6 +312,7 @@ describe('pE VS_VERSION_INFO scan guards', () => { const filePath = await writeTempPE('no-resource-dir', pe) // Guard at line 1648: `resource_rva == 0 || resource_size == 0` returns NULL + // oxlint-disable-next-line socket/prefer-exists-sync -- every stat call in this file consumes stats.size to assert the synthesized PE fixtures are non-empty before the C parser sees them. const stat = await fs.stat(filePath) expect(stat.size).toBeGreaterThan(0) }) @@ -316,6 +329,7 @@ describe('pE VS_VERSION_INFO scan guards', () => { const filePath = await writeTempPE('large-pe', largePE) + // oxlint-disable-next-line socket/prefer-exists-sync -- every stat call in this file consumes stats.size to assert the synthesized PE fixtures are non-empty before the C parser sees them. const stat = await fs.stat(filePath) expect(stat.size).toBe(10 * 1024 * 1024) diff --git a/packages/bin-infra/vitest.config.mts b/packages/bin-infra/vitest.config.mts index 018ac3d54..ae22fc89b 100644 --- a/packages/bin-infra/vitest.config.mts +++ b/packages/bin-infra/vitest.config.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/no-default-export -- vitest CLI auto-discovers config via default import. */ /** * Extends shared vitest config. * Excludes build and upstream directories. @@ -7,6 +6,7 @@ import { defineConfig, mergeConfig } from 'vitest/config' import baseConfig from '../../.config/vitest.config.mts' +// oxlint-disable-next-line socket/no-default-export -- vitest CLI auto-discovers config via default import. export default mergeConfig( baseConfig, defineConfig({ diff --git a/packages/binflate/vitest.config.mts b/packages/binflate/vitest.config.mts index 3a261c222..34129cf96 100644 --- a/packages/binflate/vitest.config.mts +++ b/packages/binflate/vitest.config.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/no-default-export -- vitest CLI auto-discovers config via default import. */ /** * Extends shared vitest config. * Uses forks pool for process isolation during compression/decompression tests. @@ -7,6 +6,7 @@ import { defineConfig, mergeConfig } from 'vitest/config' import baseConfig from '../../.config/vitest.config.mts' +// oxlint-disable-next-line socket/no-default-export -- vitest CLI auto-discovers config via default import. export default mergeConfig( baseConfig, defineConfig({ diff --git a/packages/binject/test/binary-format-validation.test.mts b/packages/binject/test/binary-format-validation.test.mts index 5e55849f8..2036cb28f 100644 --- a/packages/binject/test/binary-format-validation.test.mts +++ b/packages/binject/test/binary-format-validation.test.mts @@ -1,5 +1,4 @@ // max-file-lines: legitimate -- integration test — one end-to-end scenario per file, splitting fractures the assertion narrative -/* oxlint-disable socket/prefer-exists-sync -- every fs.stat() in this file consumes stats.size to assert input/output binary size deltas after injection. */ /** * @fileoverview Binary format validation tests for binject * @@ -236,6 +235,7 @@ describe.skipIf(!binjectExists)( ]) // Check file permissions + // oxlint-disable-next-line socket/prefer-exists-sync -- every fs.stat() in this file consumes stats.size to assert input/output binary size deltas after injection. const stats = await fs.stat(outputBinary) const isExecutable = (stats.mode & 0o111) !== 0 @@ -252,7 +252,8 @@ describe.skipIf(!binjectExists)( // Set specific permissions await makeExecutable(inputBinary) - const _inputStats = await fs.stat(inputBinary) + // oxlint-disable-next-line socket/prefer-exists-sync -- every fs.stat() in this file consumes stats.size to assert input/output binary size deltas after injection. + const inputStats = await fs.stat(inputBinary) const seaBlob = path.join(testDir, 'perm_test.blob') await fs.writeFile(seaBlob, Buffer.from('test')) @@ -270,6 +271,7 @@ describe.skipIf(!binjectExists)( seaBlob, ]) + // oxlint-disable-next-line socket/prefer-exists-sync -- every fs.stat() in this file consumes stats.size to assert input/output binary size deltas after injection. const outputStats = await fs.stat(outputBinary) // Output should be executable @@ -352,7 +354,7 @@ describe.skipIf(!binjectExists)( // Get input binary size and hash of first 4KB (should be unchanged) const inputData = await fs.readFile(inputBinary) - const _inputHeader = inputData.subarray(0, 4096) + const inputHeader = inputData.subarray(0, 4096) const seaBlob = path.join(testDir, 'corrupt_test.blob') await fs.writeFile(seaBlob, Buffer.from('test content')) @@ -451,6 +453,7 @@ describe.skipIf(!binjectExists)( const inputBinary = path.join(testDir, 'size_input') await fs.copyFile(BINJECT, inputBinary) + // oxlint-disable-next-line socket/prefer-exists-sync -- every fs.stat() in this file consumes stats.size to assert input/output binary size deltas after injection. const inputStats = await fs.stat(inputBinary) const seaBlob = path.join(testDir, 'size_test.blob') @@ -471,6 +474,7 @@ describe.skipIf(!binjectExists)( seaBlob, ]) + // oxlint-disable-next-line socket/prefer-exists-sync -- every fs.stat() in this file consumes stats.size to assert input/output binary size deltas after injection. const outputStats = await fs.stat(outputBinary) // Output should be at least blob size larger (allowing for metadata overhead) diff --git a/packages/binject/test/cli-integration.test.mts b/packages/binject/test/cli-integration.test.mts index 388f688eb..a08d8d324 100644 --- a/packages/binject/test/cli-integration.test.mts +++ b/packages/binject/test/cli-integration.test.mts @@ -1,5 +1,4 @@ // max-file-lines: legitimate -- integration test — one end-to-end scenario per file, splitting fractures the assertion narrative -/* oxlint-disable socket/prefer-exists-sync -- many access(X_OK) and access(F_OK) calls check executable permission / output-file readiness inside Promise.all races; existsSync (sync, no permission check) is not a substitute. */ /** * CLI Integration Tests for binject * Tests all command-line flags, help output, and user-facing workflows @@ -119,6 +118,7 @@ export async function downloadNodeSmolRelease() { // Check if already downloaded and cached try { + // oxlint-disable-next-line socket/prefer-exists-sync -- many access(X_OK) and access(F_OK) calls check executable permission / output-file readiness inside Promise.all races; existsSync (sync, no permission check) is not a substitute. await fs.access(cachedBinary, FS_CONSTANTS.X_OK) return cachedBinary } catch { @@ -152,6 +152,7 @@ export async function downloadNodeSmolRelease() { await fs.rename(extractedBinary, cachedBinary) // Verify cached binary exists and is executable + // oxlint-disable-next-line socket/prefer-exists-sync -- many access(X_OK) and access(F_OK) calls check executable permission / output-file readiness inside Promise.all races; existsSync (sync, no permission check) is not a substitute. await fs.access(cachedBinary, FS_CONSTANTS.X_OK) return cachedBinary } catch { @@ -234,6 +235,7 @@ export async function findNodeBinary() { const binaryPath = possiblePaths[i] try { // eslint-disable-next-line no-await-in-loop + // oxlint-disable-next-line socket/prefer-exists-sync -- many access(X_OK) and access(F_OK) calls check executable permission / output-file readiness inside Promise.all races; existsSync (sync, no permission check) is not a substitute. await fs.access(binaryPath, FS_CONSTANTS.X_OK) return binaryPath } catch { @@ -256,7 +258,9 @@ describe('binject CLI', () => { // Check if binject binary exists logger.log('Checking for BINJECT at:', BINJECT) try { + // oxlint-disable-next-line socket/prefer-exists-sync -- many access(X_OK) and access(F_OK) calls check executable permission / output-file readiness inside Promise.all races; existsSync (sync, no permission check) is not a substitute. await fs.access(BINJECT, FS_CONSTANTS.X_OK) + // oxlint-disable-next-line socket/prefer-exists-sync -- many access(X_OK) and access(F_OK) calls check executable permission / output-file readiness inside Promise.all races; existsSync (sync, no permission check) is not a substitute. const stats = await fs.stat(BINJECT) logger.log( 'BINJECT found! Size:', @@ -279,6 +283,7 @@ describe('binject CLI', () => { const foundBinary = await findNodeBinary() // Check if binary is small enough for binject + // oxlint-disable-next-line socket/prefer-exists-sync -- many access(X_OK) and access(F_OK) calls check executable permission / output-file readiness inside Promise.all races; existsSync (sync, no permission check) is not a substitute. const stats = await fs.stat(foundBinary) if (stats.size > MAX_NODE_BINARY_SIZE) { logger.warn( @@ -410,6 +415,7 @@ describe('binject CLI', () => { // Should not error - both flags are valid together expect(result.output).toMatch(/(Success|both|injected)/i) await expect( + // oxlint-disable-next-line socket/prefer-exists-sync -- many access(X_OK) and access(F_OK) calls check executable permission / output-file readiness inside Promise.all races; existsSync (sync, no permission check) is not a substitute. fs.access(output, FS_CONSTANTS.F_OK), ).resolves.toBeUndefined() }) @@ -515,6 +521,7 @@ describe('binject CLI', () => { // All platforms should succeed with --sea injection expect(result.output).toMatch(/(Success|injected)/i) await expect( + // oxlint-disable-next-line socket/prefer-exists-sync -- many access(X_OK) and access(F_OK) calls check executable permission / output-file readiness inside Promise.all races; existsSync (sync, no permission check) is not a substitute. fs.access(output, FS_CONSTANTS.F_OK), ).resolves.toBeUndefined() }) @@ -561,6 +568,7 @@ describe('binject CLI', () => { expect(result.output).toMatch(/(Success|both|injected)/i) await expect( + // oxlint-disable-next-line socket/prefer-exists-sync -- many access(X_OK) and access(F_OK) calls check executable permission / output-file readiness inside Promise.all races; existsSync (sync, no permission check) is not a substitute. fs.access(output, FS_CONSTANTS.F_OK), ).resolves.toBeUndefined() }) @@ -571,7 +579,7 @@ describe('binject CLI', () => { const vfsResource = await createTestResource('test2.tar') const output = path.join(testDir, 'output-batch2.bin') - const _result = await execCommand(BINJECT, [ + const result = await execCommand(BINJECT, [ 'inject', '-e', binary, @@ -628,6 +636,7 @@ describe('binject CLI', () => { // All platforms should succeed with --sea injection expect(result.code).toBe(0) await expect( + // oxlint-disable-next-line socket/prefer-exists-sync -- many access(X_OK) and access(F_OK) calls check executable permission / output-file readiness inside Promise.all races; existsSync (sync, no permission check) is not a substitute. fs.access(output, FS_CONSTANTS.F_OK), ).resolves.toBeUndefined() }) diff --git a/packages/binject/test/e2e-signature-cache.test.mts b/packages/binject/test/e2e-signature-cache.test.mts index 6ac498ea5..c64980256 100644 --- a/packages/binject/test/e2e-signature-cache.test.mts +++ b/packages/binject/test/e2e-signature-cache.test.mts @@ -1,5 +1,4 @@ // max-file-lines: legitimate -- integration test — one end-to-end scenario per file, splitting fractures the assertion narrative -/* oxlint-disable socket/sort-source-methods -- test helpers ordered by signature-cache flow (build → sign → cache → verify → invalidate); alphabetizing would scatter the flow. */ /** * E2E Tests for Signature Validation and Cache Management @@ -137,6 +136,7 @@ export function findTestStub() { * This is more reliable than extracting from cache which can be inconsistent. * @returns {string|null} Path to uncompressed binary or null if none found */ +// oxlint-disable-next-line socket/sort-source-methods -- test helpers ordered by signature-cache flow (build → sign → cache → verify → invalidate); alphabetizing would scatter the flow. export function findNodeSmolBinary() { const platform = os.platform() const binaryName = platform === 'win32' ? 'node.exe' : 'node' @@ -197,6 +197,7 @@ const describeOnMac = os.platform() === 'darwin' ? describe : describe.skip let testDir: string +// oxlint-disable-next-line socket/sort-source-methods -- test helpers ordered by signature-cache flow (build → sign → cache → verify → invalidate); alphabetizing would scatter the flow. export async function execCommand(command, args = [], options = {}) { return new Promise(resolve => { const spawnPromise = spawn(command, args, { @@ -242,16 +243,19 @@ export async function verifySignature(binaryPath) { return result.code === 0 } -export async function _getSignatureInfo(binaryPath) { +// oxlint-disable-next-line socket/sort-source-methods -- test helpers ordered by signature-cache flow (build → sign → cache → verify → invalidate); alphabetizing would scatter the flow. +export async function getSignatureInfo(binaryPath) { // codesign outputs to stderr const result = await execCommand('codesign', ['-dvvv', binaryPath]) return result.stderr } +// oxlint-disable-next-line socket/sort-source-methods -- test helpers ordered by signature-cache flow (build → sign → cache → verify → invalidate); alphabetizing would scatter the flow. export function getCacheDir() { return getSocketDlxDir() } +// oxlint-disable-next-line socket/sort-source-methods -- test helpers ordered by signature-cache flow (build → sign → cache → verify → invalidate); alphabetizing would scatter the flow. export async function getCacheEntries() { const cacheDir = getCacheDir() try { @@ -263,6 +267,7 @@ export async function getCacheEntries() { } } +// oxlint-disable-next-line socket/sort-source-methods -- test helpers ordered by signature-cache flow (build → sign → cache → verify → invalidate); alphabetizing would scatter the flow. export async function getCachedBinaryPath(cacheKey) { const platform = os.platform() const binaryName = platform === 'win32' ? 'node.exe' : 'node' @@ -274,6 +279,7 @@ export async function getCachedBinaryPath(cacheKey) { * This is necessary because the repack workflow modifies the cache state * in ways that break subsequent injections. */ +// oxlint-disable-next-line socket/sort-source-methods -- test helpers ordered by signature-cache flow (build → sign → cache → verify → invalidate); alphabetizing would scatter the flow. export async function cleanCacheBeforeTest() { const cacheDir = getCacheDir() try { @@ -301,6 +307,7 @@ export async function cleanCacheBeforeTest() { * @param nodeBinaryPath - Optional path to Node.js binary for SEA generation (for version matching) * @returns Path to the generated .blob file */ +// oxlint-disable-next-line socket/sort-source-methods -- test helpers ordered by signature-cache flow (build → sign → cache → verify → invalidate); alphabetizing would scatter the flow. export async function generateValidSEABlob( baseDir: string, prefix: string, @@ -343,6 +350,7 @@ export async function generateValidSEABlob( /** * Create unique VFS content using UUID to ensure each test creates a unique cache entry */ +// oxlint-disable-next-line socket/sort-source-methods -- test helpers ordered by signature-cache flow (build → sign → cache → verify → invalidate); alphabetizing would scatter the flow. export function createUniqueVFSContent(description: string) { const uuid = crypto.randomUUID() return `${description}\nUnique ID: ${uuid}\n` diff --git a/packages/binject/test/extract-verify-smol.test.mts b/packages/binject/test/extract-verify-smol.test.mts index 6f284d0a1..902ca8a6f 100644 --- a/packages/binject/test/extract-verify-smol.test.mts +++ b/packages/binject/test/extract-verify-smol.test.mts @@ -41,14 +41,6 @@ let binjectExists = false let binpressExists = false let binflateExists = false -/** - * Calculate SHA-256 hash of file - */ -export async function _hashFile(filePath) { - const data = await fs.readFile(filePath) - return crypto.createHash('sha256').update(data).digest('hex') -} - /** * Execute command and return result */ @@ -84,6 +76,14 @@ export async function execCommand(command, args = [], options = {}) { }) } +/** + * Calculate SHA-256 hash of file + */ +export async function hashFile(filePath) { + const data = await fs.readFile(filePath) + return crypto.createHash('sha256').update(data).digest('hex') +} + beforeAll(async () => { // Check if tools exist binjectExists = existsSync(BINJECT) diff --git a/packages/binject/test/helpers/binaries.mts b/packages/binject/test/helpers/binaries.mts index c9316f8bb..139419afd 100644 --- a/packages/binject/test/helpers/binaries.mts +++ b/packages/binject/test/helpers/binaries.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/sort-source-methods -- helpers ordered by download pipeline (resolve URL → fetch → extract → cache → return path); alphabetizing would scatter the flow. */ /** * Helper for downloading Node.js binaries for cross-platform testing * Downloads node-smol binaries or falls back to official Node.js releases @@ -44,6 +43,7 @@ const NODE_VERSION = getNodeVersion() * Get platform/arch configuration for binary downloads * Uses lazy evaluation to ensure NODE_VERSION is resolved */ +// oxlint-disable-next-line socket/sort-source-methods -- helpers ordered by download pipeline (resolve URL → fetch → extract → cache → return path); alphabetizing would scatter the flow. export function getBinaryConfig(platform, arch) { const version = NODE_VERSION const key = `${platform}-${arch}` @@ -100,6 +100,7 @@ const SUPPORTED_PLATFORMS = [ * @param {string} url - URL to download from * @returns {Promise} Binary data */ +// oxlint-disable-next-line socket/sort-source-methods -- helpers ordered by download pipeline (resolve URL → fetch → extract → cache → return path); alphabetizing would scatter the flow. export async function downloadBinary(url) { const response = await httpRequest(url) if (!response.ok) { @@ -116,6 +117,7 @@ export async function downloadBinary(url) { * @param {string} extractPath - Path within archive to extract * @returns {Promise} Extracted binary data */ +// oxlint-disable-next-line socket/sort-source-methods -- helpers ordered by download pipeline (resolve URL → fetch → extract → cache → return path); alphabetizing would scatter the flow. export async function extractFromTarGz(tarGzData, extractPath) { const tempDir = path.join(os.tmpdir(), `binject-extract-${Date.now()}`) await mkdir(tempDir, { recursive: true }) @@ -146,6 +148,7 @@ export async function extractFromTarGz(tarGzData, extractPath) { * @param {string} extractPath - Path within archive to extract * @returns {Promise} Extracted binary data */ +// oxlint-disable-next-line socket/sort-source-methods -- helpers ordered by download pipeline (resolve URL → fetch → extract → cache → return path); alphabetizing would scatter the flow. export async function extractFromZip(zipData, extractPath) { const zip = new AdmZip(zipData) const entry = zip.getEntry(extractPath) @@ -163,6 +166,7 @@ export async function extractFromZip(zipData, extractPath) { * @param {string} arch - Architecture (x64, arm64) * @returns {Promise<{path: string, format: string, version: string}>} Path to cached binary, its format, and version */ +// oxlint-disable-next-line socket/sort-source-methods -- helpers ordered by download pipeline (resolve URL → fetch → extract → cache → return path); alphabetizing would scatter the flow. export async function getNodeBinary(platform, arch) { const key = `${platform}-${arch}` const config = getBinaryConfig(platform, arch) diff --git a/packages/binject/test/sea-config-vfs.test.mts b/packages/binject/test/sea-config-vfs.test.mts index bec609a8c..0fec7415d 100644 --- a/packages/binject/test/sea-config-vfs.test.mts +++ b/packages/binject/test/sea-config-vfs.test.mts @@ -1,5 +1,4 @@ // max-file-lines: legitimate -- integration test — one end-to-end scenario per file, splitting fractures the assertion narrative -/* oxlint-disable socket/prefer-exists-sync -- access(X_OK) checks executable permission, not just existence; stats.size verifies non-empty binary; existsSync can't substitute for either. */ /** * SEA Config VFS Tests * @@ -90,9 +89,11 @@ export async function findNodeBinary() { const binaryPath = possiblePaths[i] try { // eslint-disable-next-line no-await-in-loop + // oxlint-disable-next-line socket/prefer-exists-sync -- access(X_OK) checks executable permission, not just existence; stats.size verifies non-empty binary; existsSync can't substitute for either. const stats = await fs.stat(binaryPath) if (stats.isFile()) { // eslint-disable-next-line no-await-in-loop + // oxlint-disable-next-line socket/prefer-exists-sync -- access(X_OK) checks executable permission, not just existence; stats.size verifies non-empty binary; existsSync can't substitute for either. await fs.access(binaryPath, fs.constants.X_OK) return binaryPath } @@ -120,6 +121,7 @@ describe('sEA Config VFS Configuration', () => { const foundBinary = await findNodeBinary() // Check if binary is small enough for binject. + // oxlint-disable-next-line socket/prefer-exists-sync -- access(X_OK) checks executable permission, not just existence; stats.size verifies non-empty binary; existsSync can't substitute for either. const stats = await fs.stat(foundBinary) if (stats.size > MAX_NODE_BINARY_SIZE) { logger.warn( diff --git a/packages/binject/test/sea-json-config.test.mts b/packages/binject/test/sea-json-config.test.mts index 5096fcaeb..a4bccaccb 100644 --- a/packages/binject/test/sea-json-config.test.mts +++ b/packages/binject/test/sea-json-config.test.mts @@ -1,5 +1,4 @@ // max-file-lines: legitimate -- integration test — one end-to-end scenario per file, splitting fractures the assertion narrative -/* oxlint-disable socket/prefer-exists-sync -- access(X_OK) checks executable permission, not just existence; stats.size verifies non-empty binary; existsSync can't substitute for either. */ /** * SEA JSON Config Tests * @@ -101,6 +100,7 @@ export async function downloadNodeSmolRelease() { // Check if already downloaded and cached try { + // oxlint-disable-next-line socket/prefer-exists-sync -- access(X_OK) checks executable permission, not just existence; stats.size verifies non-empty binary; existsSync can't substitute for either. await fs.access(cachedBinary, fs.constants.X_OK) return cachedBinary } catch { @@ -134,6 +134,7 @@ export async function downloadNodeSmolRelease() { await fs.rename(extractedBinary, cachedBinary) // Verify cached binary exists and is executable + // oxlint-disable-next-line socket/prefer-exists-sync -- access(X_OK) checks executable permission, not just existence; stats.size verifies non-empty binary; existsSync can't substitute for either. await fs.access(cachedBinary, fs.constants.X_OK) return cachedBinary } catch { @@ -202,10 +203,12 @@ export async function findNodeBinary() { try { // Check if file exists and is executable // eslint-disable-next-line no-await-in-loop + // oxlint-disable-next-line socket/prefer-exists-sync -- access(X_OK) checks executable permission, not just existence; stats.size verifies non-empty binary; existsSync can't substitute for either. const stats = await fs.stat(binaryPath) if (stats.isFile()) { // Try to access with execute permission // eslint-disable-next-line no-await-in-loop + // oxlint-disable-next-line socket/prefer-exists-sync -- access(X_OK) checks executable permission, not just existence; stats.size verifies non-empty binary; existsSync can't substitute for either. await fs.access(binaryPath, fs.constants.X_OK) return binaryPath } @@ -240,6 +243,7 @@ describe('sEA JSON Config', () => { const foundBinary = await findNodeBinary() // Check if binary is small enough for binject + // oxlint-disable-next-line socket/prefer-exists-sync -- access(X_OK) checks executable permission, not just existence; stats.size verifies non-empty binary; existsSync can't substitute for either. const stats = await fs.stat(foundBinary) if (stats.size > MAX_NODE_BINARY_SIZE) { logger.warn( diff --git a/packages/binject/test/smol-inject-repack.test.mts b/packages/binject/test/smol-inject-repack.test.mts index 2f8c6a4bf..b32bcbb85 100644 --- a/packages/binject/test/smol-inject-repack.test.mts +++ b/packages/binject/test/smol-inject-repack.test.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/prefer-exists-sync -- four fs.stat() calls all consume stats.size to compare original vs. injected stub sizes; existsSync would lose the size data. */ /** * @fileoverview SMOL stub injection and repack workflow tests @@ -52,14 +51,6 @@ const binjectExists = existsSync(BINJECT) const binpressExists = existsSync(BINPRESS) const binflateExists = existsSync(BINFLATE) -/** - * Calculate SHA-256 hash of file - */ -export async function _hashFile(filePath) { - const data = await fs.readFile(filePath) - return crypto.createHash('sha256').update(data).digest('hex') -} - /** * Create a minimal SEA blob for testing */ @@ -106,6 +97,14 @@ export async function execCommand(command, args = [], options = {}) { }) } +/** + * Calculate SHA-256 hash of file + */ +export async function hashFile(filePath) { + const data = await fs.readFile(filePath) + return crypto.createHash('sha256').update(data).digest('hex') +} + /** * Check if binary has PRESSED_DATA section (SMOL stub detection) */ @@ -215,11 +214,13 @@ describe.skipIf(!binjectExists || !binpressExists || !binflateExists)( const compressedStub = path.join(testDir, 'size-stub') await execCommand(BINPRESS, [originalBinary, '-o', compressedStub]) + // oxlint-disable-next-line socket/prefer-exists-sync -- four fs.stat() calls all consume stats.size to compare original vs. injected stub sizes; existsSync would lose the size data. const originalStubSize = (await fs.stat(compressedStub)).size // Step 2: Inject SEA into SMOL stub const seaBlob = path.join(testDir, 'size-test-sea.blob') await createTestSEABlob(seaBlob) + // oxlint-disable-next-line socket/prefer-exists-sync -- four fs.stat() calls all consume stats.size to compare original vs. injected stub sizes; existsSync would lose the size data. const seaBlobSize = (await fs.stat(seaBlob)).size const outputStub = path.join(testDir, 'size-output') @@ -233,7 +234,9 @@ describe.skipIf(!binjectExists || !binpressExists || !binflateExists)( seaBlob, ]) + // oxlint-disable-next-line socket/prefer-exists-sync -- four fs.stat() calls all consume stats.size to compare original vs. injected stub sizes; existsSync would lose the size data. const outputStubSize = (await fs.stat(outputStub)).size + // oxlint-disable-next-line socket/prefer-exists-sync -- four fs.stat() calls all consume stats.size to compare original vs. injected stub sizes; existsSync would lose the size data. const originalBinarySize = (await fs.stat(originalBinary)).size // Output stub should be: @@ -262,7 +265,7 @@ describe.skipIf(!binjectExists || !binpressExists || !binflateExists)( // Step 2: Run the stub once to extract to cache // (--skip-repack requires the extracted binary to exist in cache) - const _runResult = await execCommand(compressedStub, ['--version']) + const runResult = await execCommand(compressedStub, ['--version']) // Note: This may fail if Node.js doesn't support --version in this way, but extraction happens anyway // The important thing is the binary was executed and extracted to cache diff --git a/packages/binject/test/vfs-format.test.mts b/packages/binject/test/vfs-format.test.mts index 696d7b38a..802267b52 100644 --- a/packages/binject/test/vfs-format.test.mts +++ b/packages/binject/test/vfs-format.test.mts @@ -25,7 +25,7 @@ let binjectExists = false /** * Helper to run binject commands */ -export async function _runBinject(args, options = {}) { +export async function runBinject(args, options = {}) { return new Promise(resolve => { const spawnPromise = spawn(BINJECT, args, { cwd: options.cwd || testDir, @@ -56,13 +56,14 @@ export async function _runBinject(args, options = {}) { }) }) } -void _runBinject +void runBinject /** * Create a simple TAR archive from a buffer map * @param {Map} files - Map of filename to content * @returns {Buffer} TAR archive */ +// oxlint-disable-next-line socket/sort-source-methods -- test helpers grouped by workflow (runBinject first, then archive builders); alphabetizing would scatter the test setup flow. export function createTar(files) { const blocks = [] diff --git a/packages/binject/vitest.config.mts b/packages/binject/vitest.config.mts index ec02a3024..539f8853c 100644 --- a/packages/binject/vitest.config.mts +++ b/packages/binject/vitest.config.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/no-default-export -- vitest CLI auto-discovers config via default import. */ /** * Extends shared vitest config. * Uses forks pool with singleFork for codesigning compatibility. @@ -12,6 +11,7 @@ import { defineConfig, mergeConfig } from 'vitest/config' import baseConfig from '../../.config/vitest.config.mts' +// oxlint-disable-next-line socket/no-default-export -- vitest CLI auto-discovers config via default import. export default mergeConfig( baseConfig, defineConfig({ diff --git a/packages/binpress/scripts/generate-embedded-stubs.mts b/packages/binpress/scripts/generate-embedded-stubs.mts index 77bafaa4b..6c8ff5145 100644 --- a/packages/binpress/scripts/generate-embedded-stubs.mts +++ b/packages/binpress/scripts/generate-embedded-stubs.mts @@ -1,5 +1,3 @@ -/* oxlint-disable socket/sort-source-methods -- script ordered as a top-down stub-generation pipeline (resolve platforms → fetch or build → embed → write); alphabetizing would scatter the flow. */ -/* oxlint-disable socket/prefer-exists-sync -- fs.stat() calls consume stats.size for the per-stub log message and per-platform size summary. */ /** * @fileoverview Generate embedded stubs for binpress @@ -131,6 +129,7 @@ export async function downloadStub( ) await fs.copyFile(LOCAL_STUB_PATH, stubOut) + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size for the per-stub log message and per-platform size summary. const stats = await fs.stat(stubOut) logger.success( `${platformName} stub (local, ${(stats.size / 1024).toFixed(1)}KB)`, @@ -190,6 +189,7 @@ export async function downloadStub( ) await fs.rename(extractedPath, stubOut) + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size for the per-stub log message and per-platform size summary. const stats = await fs.stat(stubOut) logger.success(`${platformName} stub (${(stats.size / 1024).toFixed(1)}KB)`) @@ -216,6 +216,7 @@ export async function downloadStub( /** * Convert binary to C array */ +// oxlint-disable-next-line socket/sort-source-methods -- script ordered as a top-down stub-generation pipeline (resolve platforms → fetch or build → embed → write); alphabetizing would scatter the flow. export async function binaryToCArray(stubPath, varName) { // Read binary const data = await fs.readFile(stubPath) @@ -361,6 +362,7 @@ const stubSummary = [ // oxlint-disable-next-line socket/prefer-cached-for-loop -- loop variable is destructured for (const [name, stubPath] of stubSummary) { try { + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size for the per-stub log message and per-platform size summary. const stats = await fs.stat(stubPath) logger.info(` ${name}: ${(stats.size / 1024).toFixed(1)}KB`) } catch { diff --git a/packages/binpress/test/compression-roundtrip.test.mts b/packages/binpress/test/compression-roundtrip.test.mts index 608abab19..908bac31f 100644 --- a/packages/binpress/test/compression-roundtrip.test.mts +++ b/packages/binpress/test/compression-roundtrip.test.mts @@ -1,5 +1,4 @@ // max-file-lines: legitimate -- integration test — one end-to-end scenario per file, splitting fractures the assertion narrative -/* oxlint-disable socket/prefer-exists-sync -- fs.stat() calls consume stats.size / stats.mode for round-trip size and executable-permission assertions. */ /** * @fileoverview Compression round-trip tests for binpress @@ -72,14 +71,6 @@ const skipExec = isCrossCompile || isDockerBuild let testDir: string let testBinary: string -/** - * Calculate file hash - */ -export async function _hashFile(filePath) { - const data = await fs.readFile(filePath) - return crypto.createHash('sha256').update(data).digest('hex') -} - /** * Execute command and return result */ @@ -116,6 +107,14 @@ export async function execCommand(command, args = [], options = {}) { }) } +/** + * Calculate file hash + */ +export async function hashFile(filePath) { + const data = await fs.readFile(filePath) + return crypto.createHash('sha256').update(data).digest('hex') +} + beforeAll(async () => { // Create unique test directory with timestamp and random suffix to isolate from parallel runs const uniqueId = crypto.randomUUID() @@ -205,7 +204,9 @@ describe.skipIf(!existsSync(BINPRESS))( ? `${compressedBinary}.exe` : compressedBinary + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size / stats.mode for round-trip size and executable-permission assertions. const inputStats = await fs.stat(inputBinary) + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size / stats.mode for round-trip size and executable-permission assertions. const compressedStats = await fs.stat(finalPath) // Compressed should be smaller (or at worst slightly larger due to stub overhead) @@ -231,6 +232,7 @@ describe.skipIf(!existsSync(BINPRESS))( ? `${compressedBinary}.exe` : compressedBinary + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size / stats.mode for round-trip size and executable-permission assertions. const stats = await fs.stat(finalPath) // Windows doesn't use Unix-style executable bits, so skip this check on Windows @@ -453,6 +455,7 @@ describe.skipIf(!existsSync(BINPRESS))( await handle.close() } + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size / stats.mode for round-trip size and executable-permission assertions. const inputStats = await fs.stat(inputBinary) expect(inputStats.size).toBeGreaterThan(targetSize) @@ -473,6 +476,7 @@ describe.skipIf(!existsSync(BINPRESS))( ? `${compressedBinary}.exe` : compressedBinary + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size / stats.mode for round-trip size and executable-permission assertions. const compressedStats = await fs.stat(finalPath) // With repetitive data, should compress significantly @@ -563,6 +567,7 @@ describe.skipIf(!existsSync(BINPRESS))( process.platform === 'win32' ? `${output}.exe` : output // Output should be larger than "existing content" + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size / stats.mode for round-trip size and executable-permission assertions. const stats = await fs.stat(finalPath) expect(stats.size).toBeGreaterThan(100) }, 60_000) @@ -666,6 +671,7 @@ describe.skipIf(!existsSync(BINPRESS))( continue } try { + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size / stats.mode for round-trip size and executable-permission assertions. const stat = await fs.stat(meta) if (stat.mtimeMs > bestMtime) { bestMtime = stat.mtimeMs diff --git a/packages/binpress/test/update-mode.test.mts b/packages/binpress/test/update-mode.test.mts index 9167adbff..f751f2f05 100644 --- a/packages/binpress/test/update-mode.test.mts +++ b/packages/binpress/test/update-mode.test.mts @@ -101,7 +101,7 @@ describe.skipIf( expect(createResult.code).toBe(0) expect(existsSync(initialStub)).toBeTruthy() - const _initialStubHash = await hashFile(initialStub) + const initialStubHash = await hashFile(initialStub) // Step 2: Recompress the stub (auto-detection should trigger repack) const updatedStub = getStubPath('updated-stub') diff --git a/packages/binpress/vitest.config.mts b/packages/binpress/vitest.config.mts index 61c779dcf..bd8ef5a49 100644 --- a/packages/binpress/vitest.config.mts +++ b/packages/binpress/vitest.config.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/no-default-export -- vitest CLI auto-discovers config via default import. */ /** * Extends shared vitest config. */ @@ -6,6 +5,7 @@ import { defineConfig, mergeConfig } from 'vitest/config' import baseConfig from '../../.config/vitest.config.mts' +// oxlint-disable-next-line socket/no-default-export -- vitest CLI auto-discovers config via default import. export default mergeConfig( baseConfig, defineConfig({ diff --git a/packages/build-infra/lib/build-env.mts b/packages/build-infra/lib/build-env.mts index f83d1f41e..bbccca642 100644 --- a/packages/build-infra/lib/build-env.mts +++ b/packages/build-infra/lib/build-env.mts @@ -1,5 +1,4 @@ // max-file-lines: legitimate -- cohesive build-environment helper module — one tool family (emscripten/python/cmake setup); splitting scatters related setup -/* oxlint-disable socket/no-status-emoji -- emoji are pushed into result.messages/result.errors arrays that callers may render anywhere (JSON, file, stderr); there is no single logger.success/fail call to migrate to. */ /** * Build Environment Detection and Setup @@ -463,9 +462,11 @@ export async function setupBuildEnvironment(options = {}) { if (activated) { const version = await getEmscriptenVersion() + // oxlint-disable-next-line socket/no-status-emoji -- emoji are pushed into result.messages/result.errors arrays that callers may render anywhere (JSON, file, stderr); there is no single logger.success/fail call to migrate to. results.messages.push(`✓ Emscripten ${version} activated`) } else { results.success = false + // oxlint-disable-next-line socket/no-status-emoji -- emoji are pushed into result.messages/result.errors arrays that callers may render anywhere (JSON, file, stderr); there is no single logger.success/fail call to migrate to. results.errors.push('✗ Emscripten SDK not found') if (autoSetup) { @@ -485,9 +486,11 @@ export async function setupBuildEnvironment(options = {}) { const rustCheck = await checkRust() if (rustCheck.available) { + // oxlint-disable-next-line socket/no-status-emoji -- emoji are pushed into result.messages/result.errors arrays that callers may render anywhere (JSON, file, stderr); there is no single logger.success/fail call to migrate to. results.messages.push(`✓ Rust ${rustCheck.version} with WASM support`) } else { results.success = false + // oxlint-disable-next-line socket/no-status-emoji -- emoji are pushed into result.messages/result.errors arrays that callers may render anywhere (JSON, file, stderr); there is no single logger.success/fail call to migrate to. results.errors.push(`✗ Rust: ${rustCheck.reason}`) if (rustCheck.fix) { @@ -506,10 +509,12 @@ export async function setupBuildEnvironment(options = {}) { if (pythonCheck.available) { if (pythonCheck.meetsRequirement) { + // oxlint-disable-next-line socket/no-status-emoji -- emoji are pushed into result.messages/result.errors arrays that callers may render anywhere (JSON, file, stderr); there is no single logger.success/fail call to migrate to. results.messages.push(`✓ Python ${pythonCheck.version}`) } else { results.success = false results.errors.push( + // oxlint-disable-next-line socket/no-status-emoji -- emoji are pushed into result.messages/result.errors arrays that callers may render anywhere (JSON, file, stderr); there is no single logger.success/fail call to migrate to. `✗ Python ${pythonCheck.version} is too old (need ${getMinPythonVersion()}+)`, ) @@ -521,6 +526,7 @@ export async function setupBuildEnvironment(options = {}) { } } else { results.success = false + // oxlint-disable-next-line socket/no-status-emoji -- emoji are pushed into result.messages/result.errors arrays that callers may render anywhere (JSON, file, stderr); there is no single logger.success/fail call to migrate to. results.errors.push(`✗ Python ${getMinPythonVersion()}+ not found`) if (autoSetup) { diff --git a/packages/build-infra/lib/cache-key.mts b/packages/build-infra/lib/cache-key.mts index 46d581822..53d7b4f7a 100644 --- a/packages/build-infra/lib/cache-key.mts +++ b/packages/build-infra/lib/cache-key.mts @@ -22,16 +22,37 @@ import { errorMessage } from './error-utils.mts' /** * Critical dependencies that trigger cache invalidation. * When these packages are updated, caches must be rebuilt. + * + * Each role lists BOTH the canonical name and the `-stable` alias. The + * fleet catalog redirects every Socket package to its `-stable` form for + * build / config / hook code (see pnpm-workspace.yaml's catalog block); + * runtime consumers may use either spelling. Listing both ensures + * generateCacheKey() picks up the dep version regardless of which + * spelling a given package.json declares. */ const CACHE_BUSTING_DEPS = { - bootstrap: ['@socketsecurity/lib', '@socketsecurity/packageurl-js'], + bootstrap: [ + '@socketsecurity/lib', + '@socketsecurity/lib-stable', + '@socketsecurity/packageurl-js', + '@socketsecurity/packageurl-js-stable', + ], cli: [ '@socketsecurity/lib', + '@socketsecurity/lib-stable', '@socketsecurity/packageurl-js', + '@socketsecurity/packageurl-js-stable', '@socketsecurity/sdk', + '@socketsecurity/sdk-stable', '@socketsecurity/registry', + '@socketsecurity/registry-stable', + ], + 'cli-with-sentry': [ + '@socketsecurity/lib', + '@socketsecurity/lib-stable', + '@socketsecurity/packageurl-js', + '@socketsecurity/packageurl-js-stable', ], - 'cli-with-sentry': ['@socketsecurity/lib', '@socketsecurity/packageurl-js'], } /** diff --git a/packages/build-infra/lib/constants.mts b/packages/build-infra/lib/constants.mts index 8937f552a..c4ae147a1 100644 --- a/packages/build-infra/lib/constants.mts +++ b/packages/build-infra/lib/constants.mts @@ -1,5 +1,4 @@ // max-file-lines: legitimate -- cohesive module — one tool/domain/phase; splitting along arbitrary line cap would fracture related logic -/* oxlint-disable socket/sort-source-methods -- file is grouped by section header banners ("Path Constants" / "Build Constants" / ...) with helpers co-located with their constants; autofix bails on the const-interleaved layout and reordering would scatter related declarations across sections. */ /** * Shared constants for Socket BTM build infrastructure * @@ -417,6 +416,7 @@ export const NODE_VERSION = `v${nodeVersionRaw}` * Defaults to 'prod' in CI, 'dev' otherwise. * @returns {string} The build mode ('dev' or 'prod') */ +// oxlint-disable-next-line socket/sort-source-methods -- file is grouped by section header banners ("Path Constants" / "Build Constants" / ...) with helpers co-located with their constants; autofix bails on the const-interleaved layout and reordering would scatter related declarations across sections. export function getBuildMode(args?: string[] | Set): string { // Explicit --prod / --dev CLI flags win over env. if (args) { @@ -448,6 +448,7 @@ export function getBuildMode(args?: string[] | Set): string { * @param {string} platformArch - Platform-arch string (e.g., 'linux-x64', 'darwin-arm64') * @returns {string} Platform-specific build directory path */ +// oxlint-disable-next-line socket/sort-source-methods -- file is grouped by section header banners ("Path Constants" / "Build Constants" / ...) with helpers co-located with their constants; autofix bails on the const-interleaved layout and reordering would scatter related declarations across sections. export function getPlatformBuildDir( packageDir: string, platformArch: string, @@ -461,6 +462,7 @@ export function getPlatformBuildDir( * @param {string} [platform] - Platform override (darwin, linux, win32) * @returns {string[]} Array of paths to search for EMSDK */ +// oxlint-disable-next-line socket/sort-source-methods -- file is grouped by section header banners ("Path Constants" / "Build Constants" / ...) with helpers co-located with their constants; autofix bails on the const-interleaved layout and reordering would scatter related declarations across sections. export function getEmsdkSearchPaths( platform: string = process.platform, ): string[] { @@ -475,6 +477,7 @@ export function getEmsdkSearchPaths( * @param {number} version - GCC version number * @returns {string} Path to versioned GCC */ +// oxlint-disable-next-line socket/sort-source-methods -- file is grouped by section header banners ("Path Constants" / "Build Constants" / ...) with helpers co-located with their constants; autofix bails on the const-interleaved layout and reordering would scatter related declarations across sections. export function getGccPath(version: number): string { return COMPILER_PATHS.linux.gccVersioned(version) } @@ -484,6 +487,7 @@ export function getGccPath(version: number): string { * @param {number} version - G++ version number * @returns {string} Path to versioned G++ */ +// oxlint-disable-next-line socket/sort-source-methods -- file is grouped by section header banners ("Path Constants" / "Build Constants" / ...) with helpers co-located with their constants; autofix bails on the const-interleaved layout and reordering would scatter related declarations across sections. export function getGxxPath(version: number): string { return COMPILER_PATHS.linux.gxxVersioned(version) } diff --git a/packages/build-infra/lib/pinned-versions.mts b/packages/build-infra/lib/pinned-versions.mts index f8cac09e7..ad33707e0 100644 --- a/packages/build-infra/lib/pinned-versions.mts +++ b/packages/build-infra/lib/pinned-versions.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/sort-source-methods -- file co-locates external-tools.json loaders with the version-resolution helpers that consume them; alphabetical reordering would split loader from consumer and obscure the hierarchical lookup flow. */ /** * Pinned dependency versions for reproducible builds. * @@ -49,10 +48,12 @@ export function loadExternalToolsJson(jsonPath, visited = new Set()) { // Validate against schema. const validation = validateExternalTools(data) if (!validation.ok) { - const issues = validation.errors + const issueLines = validation.errors .map(i => ` ${i.path.join('.') || '(root)'}: ${i.message}`) - .join('\n') - logger.warn(`Schema validation warnings in ${jsonPath}:\n${issues}`) + logger.warn(`Schema validation warnings in ${jsonPath}:`) + for (const line of issueLines) { + logger.warn(line) + } } let tools = {} @@ -101,6 +102,7 @@ export function loadExternalToolsJson(jsonPath, visited = new Set()) { * @param {string} [options.checkpointName] - Checkpoint name (e.g., 'binary-released') * @returns {object} Merged external tools configuration */ +// oxlint-disable-next-line socket/sort-source-methods -- file co-locates external-tools.json loaders with the version-resolution helpers that consume them; alphabetical reordering would split loader from consumer and obscure the hierarchical lookup flow. export function loadExternalTools({ checkpointName, packageRoot } = {}) { let tools = {} @@ -201,6 +203,7 @@ export const PYTHON_PACKAGE_EXTRAS = (() => { * @param {object} [options] - Loading options for hierarchical lookup * @returns {string} Package specifier with pinned version (e.g., 'torch==2.5.0') */ +// oxlint-disable-next-line socket/sort-source-methods -- file co-locates external-tools.json loaders with the version-resolution helpers that consume them; alphabetical reordering would split loader from consumer and obscure the hierarchical lookup flow. export function getPinnedPackage(packageName, options) { const tools = options ? loadExternalTools(options) : TOOL_VERSIONS const config = tools[packageName] @@ -229,6 +232,7 @@ export function getPinnedPackage(packageName, options) { * @param {object} [options] - Loading options for hierarchical lookup * @returns {string[]} Array of package specifiers with pinned versions */ +// oxlint-disable-next-line socket/sort-source-methods -- file co-locates external-tools.json loaders with the version-resolution helpers that consume them; alphabetical reordering would split loader from consumer and obscure the hierarchical lookup flow. export function getPinnedPackages(packageNames, options) { return packageNames.map(name => getPinnedPackage(name, options)) } @@ -240,6 +244,7 @@ export function getPinnedPackages(packageNames, options) { * @param {object} [options] - Loading options for hierarchical lookup * @returns {object|undefined} Tool configuration or undefined if not found */ +// oxlint-disable-next-line socket/sort-source-methods -- file co-locates external-tools.json loaders with the version-resolution helpers that consume them; alphabetical reordering would split loader from consumer and obscure the hierarchical lookup flow. export function getToolConfig(toolName, options) { const tools = options ? loadExternalTools(options) : TOOL_VERSIONS return tools[toolName] || undefined @@ -254,6 +259,7 @@ export function getToolConfig(toolName, options) { * @param {object} [options] - Loading options for hierarchical lookup * @returns {string} Package specifier (e.g., 'cmake@3.31.4' for brew) */ +// oxlint-disable-next-line socket/sort-source-methods -- file co-locates external-tools.json loaders with the version-resolution helpers that consume them; alphabetical reordering would split loader from consumer and obscure the hierarchical lookup flow. export function getToolPackageSpec( toolName, packageName, @@ -294,6 +300,7 @@ export function getToolPackageSpec( * @param {object} [options] - Loading options for hierarchical lookup * @returns {string|undefined} Pinned version or undefined if not found */ +// oxlint-disable-next-line socket/sort-source-methods -- file co-locates external-tools.json loaders with the version-resolution helpers that consume them; alphabetical reordering would split loader from consumer and obscure the hierarchical lookup flow. export function getToolVersion(toolName, _versionKey, options) { const tools = options ? loadExternalTools(options) : TOOL_VERSIONS const tool = tools[toolName] @@ -345,6 +352,7 @@ export function loadPythonVersions(options) { * @param {string} [options.checkpointName] - Checkpoint name * @returns {object} All tools configuration */ +// oxlint-disable-next-line socket/sort-source-methods -- file co-locates external-tools.json loaders with the version-resolution helpers that consume them; alphabetical reordering would split loader from consumer and obscure the hierarchical lookup flow. export function loadAllTools(options) { return loadExternalTools(options || {}) } diff --git a/packages/build-infra/lib/platform-mappings.mts b/packages/build-infra/lib/platform-mappings.mts index 278b71edb..58151d86f 100644 --- a/packages/build-infra/lib/platform-mappings.mts +++ b/packages/build-infra/lib/platform-mappings.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/sort-source-methods -- platform/arch mapping tables (const) and the helpers that read them are co-located; autofix bails on the const-interleaved layout. */ import process from 'node:process' /** @@ -92,6 +91,7 @@ export function getPlatformArch(platform, arch, libc) { * @returns {string} Platform-arch string for assets (e.g., 'win-x64', 'linux-x64-musl'). * @throws {Error} If platform/arch is unsupported. */ +// oxlint-disable-next-line socket/sort-source-methods -- platform/arch mapping tables (const) and the helpers that read them are co-located; autofix bails on the const-interleaved layout. export function getAssetPlatformArch(platform, arch, libc) { const releasePlatform = RELEASE_PLATFORM_MAP[platform] const releaseArch = RELEASE_ARCH_MAP[arch] @@ -163,6 +163,7 @@ export async function isMusl() { * * @returns {Promise} Platform-arch string (e.g., 'win-x64', 'linux-x64-musl'). */ +// oxlint-disable-next-line socket/sort-source-methods -- platform/arch mapping tables (const) and the helpers that read them are co-located; autofix bails on the const-interleaved layout. export async function getCurrentPlatformArch() { // If the workflow or Dockerfile set PLATFORM_ARCH explicitly, trust it. if (process.env.PLATFORM_ARCH) { @@ -256,6 +257,7 @@ export function parsePlatformArch(platformArch) { * * @returns {string | undefined} Requested glibc floor, or undefined. */ +// oxlint-disable-next-line socket/sort-source-methods -- platform/arch mapping tables (const) and the helpers that read them are co-located; autofix bails on the const-interleaved layout. export function getRequestedGlibcFloor(): string | undefined { const raw = process.env.GLIBC_FLOOR if (!raw) { diff --git a/packages/build-infra/lib/python-installer.mts b/packages/build-infra/lib/python-installer.mts index 2fade8f20..22916aeb1 100644 --- a/packages/build-infra/lib/python-installer.mts +++ b/packages/build-infra/lib/python-installer.mts @@ -1,5 +1,4 @@ // max-file-lines: legitimate -- cohesive module — one tool/domain/phase; splitting along arbitrary line cap would fracture related logic -/* oxlint-disable socket/sort-source-methods -- file is ordered by pip-install pipeline phase (detect → resolve → install → verify); alphabetizing across phases would scatter the install flow. */ /** * Python Package Installation Utilities * @@ -82,7 +81,7 @@ export function isPEP668Managed() { } // Global venv state -let _venvPath +let venvPath let venvPipPath let venvPythonPath let venvInitialized = false @@ -93,6 +92,7 @@ let venvAvailable = false * * @returns {boolean} True if pip is available. */ +// oxlint-disable-next-line socket/sort-source-methods -- file is ordered by pip-install pipeline phase (detect → resolve → install → verify); alphabetizing across phases would scatter the install flow. export function checkPipAvailable() { return Boolean( whichSync('pip3', { nothrow: true }) || whichSync('pip', { nothrow: true }), @@ -104,6 +104,7 @@ export function checkPipAvailable() { * * @returns {string|undefined} Resolved pip command path or undefined if not found. */ +// oxlint-disable-next-line socket/sort-source-methods -- file is ordered by pip-install pipeline phase (detect → resolve → install → verify); alphabetizing across phases would scatter the install flow. export function getPipCommand() { // If venv is available, use it if (venvAvailable && venvPipPath) { @@ -111,11 +112,11 @@ export function getPipCommand() { } // Lazy detection — see getPythonCommand() for rationale. if (!venvInitialized) { - const probeVenvPath = _venvPath || getDefaultVenvPath() + const probeVenvPath = venvPath || getDefaultVenvPath() const probePython = path.join(probeVenvPath, 'bin', 'python3') const probePip = path.join(probeVenvPath, 'bin', 'pip3') if (existsSync(probePython) && existsSync(probePip)) { - _venvPath = probeVenvPath + venvPath = probeVenvPath venvPythonPath = probePython venvPipPath = probePip venvAvailable = true @@ -139,6 +140,7 @@ export function getPipCommand() { * * @returns {string} Path to the shared venv directory. */ +// oxlint-disable-next-line socket/sort-source-methods -- file is ordered by pip-install pipeline phase (detect → resolve → install → verify); alphabetizing across phases would scatter the install flow. export function getDefaultVenvPath() { return path.join(os.homedir(), '.socket-btm-venv') } @@ -152,6 +154,7 @@ export function getDefaultVenvPath() { * @param {boolean} options.quiet - Suppress output. * @returns {Promise} True if venv is ready. */ +// oxlint-disable-next-line socket/sort-source-methods -- file is ordered by pip-install pipeline phase (detect → resolve → install → verify); alphabetizing across phases would scatter the install flow. export async function initializeVenv({ quiet = false, venvDir } = {}) { // Only initialize once per process if (venvInitialized) { @@ -160,7 +163,7 @@ export async function initializeVenv({ quiet = false, venvDir } = {}) { venvInitialized = true const targetVenvPath = venvDir || getDefaultVenvPath() - _venvPath = targetVenvPath + venvPath = targetVenvPath // Check if venv already exists and is valid const venvBinDir = path.join(targetVenvPath, 'bin') @@ -257,6 +260,7 @@ let cachedPythonCommand * * @returns {Promise} Resolved python command path or undefined if not found. */ +// oxlint-disable-next-line socket/sort-source-methods -- file is ordered by pip-install pipeline phase (detect → resolve → install → verify); alphabetizing across phases would scatter the install flow. export async function getPythonCommand() { // If venv is available, use it if (venvAvailable && venvPythonPath) { @@ -268,11 +272,11 @@ export async function getPythonCommand() { // likely to be PEP 668 locked and missing packages we installed in // the venv earlier). if (!venvInitialized) { - const probeVenvPath = _venvPath || getDefaultVenvPath() + const probeVenvPath = venvPath || getDefaultVenvPath() const probePython = path.join(probeVenvPath, 'bin', 'python3') const probePip = path.join(probeVenvPath, 'bin', 'pip3') if (existsSync(probePython) && existsSync(probePip)) { - _venvPath = probeVenvPath + venvPath = probeVenvPath venvPythonPath = probePython venvPipPath = probePip venvAvailable = true @@ -355,6 +359,7 @@ export async function getPythonCommand() { * @param {string} packageName - Package name to check. * @returns {Promise} True if package is installed. */ +// oxlint-disable-next-line socket/sort-source-methods -- file is ordered by pip-install pipeline phase (detect → resolve → install → verify); alphabetizing across phases would scatter the install flow. export async function checkPythonPackage(packageName) { try { const pythonCmd = await getPythonCommand() @@ -375,6 +380,7 @@ export async function checkPythonPackage(packageName) { * @param {string} expectedVersion - Expected version (e.g., '2.5.1'). * @returns {Promise} True if package is installed with correct version. */ +// oxlint-disable-next-line socket/sort-source-methods -- file is ordered by pip-install pipeline phase (detect → resolve → install → verify); alphabetizing across phases would scatter the install flow. export async function checkPythonPackageVersion(packageName, expectedVersion) { try { const pythonCmd = await getPythonCommand() @@ -410,6 +416,7 @@ export async function checkPythonPackageVersion(packageName, expectedVersion) { * @param {string} options.consumerPackageJsonPath - Optional path to consumer package.json for version overrides. * @returns {Promise} True if installation succeeded. */ +// oxlint-disable-next-line socket/sort-source-methods -- file is ordered by pip-install pipeline phase (detect → resolve → install → verify); alphabetizing across phases would scatter the install flow. export async function installPythonPackage( packageName, { consumerPackageJsonPath, quiet = false, upgrade = false, user = true } = {}, @@ -578,6 +585,7 @@ export async function installPythonPackage( * @param {string} options.consumerPackageJsonPath - Optional path to consumer package.json for version overrides. * @returns {Promise<{available: boolean, installed: boolean}>} */ +// oxlint-disable-next-line socket/sort-source-methods -- file is ordered by pip-install pipeline phase (detect → resolve → install → verify); alphabetizing across phases would scatter the install flow. export async function ensurePythonPackage( packageName, { @@ -680,6 +688,7 @@ export async function ensurePythonPackage( * @param {string} options.consumerPackageJsonPath - Optional path to consumer package.json for version overrides. * @returns {Promise<{allAvailable: boolean, missing: string[], installed: string[]}>} */ +// oxlint-disable-next-line socket/sort-source-methods -- file is ordered by pip-install pipeline phase (detect → resolve → install → verify); alphabetizing across phases would scatter the install flow. export async function ensureAllPythonPackages( packages, { autoInstall = true, consumerPackageJsonPath, quiet = false } = {}, @@ -750,6 +759,7 @@ export async function ensureAllPythonPackages( * @param {string[]} packages - Package names. * @returns {string[]} Array of installation instruction strings. */ +// oxlint-disable-next-line socket/sort-source-methods -- file is ordered by pip-install pipeline phase (detect → resolve → install → verify); alphabetizing across phases would scatter the install flow. export function getPythonPackageInstructions(packages) { const pinnedPackages = packages.map(pkg => getPinnedPackage(pkg)) const instructions = ['Install required Python packages:'] diff --git a/packages/build-infra/lib/test/helpers.mts b/packages/build-infra/lib/test/helpers.mts index 2eacb5817..60648d3a6 100644 --- a/packages/build-infra/lib/test/helpers.mts +++ b/packages/build-infra/lib/test/helpers.mts @@ -295,8 +295,8 @@ export function createWasmTestHelpers(config) { return } - const _require = createRequire - const syncModule = _require(syncJsPath) + const require = createRequire + const syncModule = require(syncJsPath) expect(syncModule).toBeTruthy() if (exportName) { expect(typeof syncModule).toBe('object') diff --git a/packages/build-infra/lib/version-helpers.mts b/packages/build-infra/lib/version-helpers.mts index f0c14d3ae..6b0682e41 100644 --- a/packages/build-infra/lib/version-helpers.mts +++ b/packages/build-infra/lib/version-helpers.mts @@ -1,5 +1,4 @@ // max-file-lines: legitimate -- cohesive module — one tool/domain/phase; splitting along arbitrary line cap would fracture related logic -/* oxlint-disable socket/sort-source-methods -- helpers are co-located with their loader and consumer triplets; autofix bails on the const-table interleaving and alphabetizing would scatter related helpers. */ /** * Shared helpers for loading tool versions from external-tools.json * @@ -55,6 +54,7 @@ export function loadExternalToolsSync(packageRoot: string) { * @returns {Promise} Parsed external-tools.json * @throws {Error} If file doesn't exist or is malformed */ +// oxlint-disable-next-line socket/sort-source-methods -- helpers are co-located with their loader and consumer triplets; autofix bails on the const-table interleaving and alphabetizing would scatter related helpers. export async function loadExternalTools(packageRoot: string) { const externalToolsPath = path.join(packageRoot, 'external-tools.json') @@ -90,6 +90,7 @@ export async function loadExternalTools(packageRoot: string) { * const version = await getEmscriptenVersion(PACKAGE_ROOT) * // Returns: '4.0.20' */ +// oxlint-disable-next-line socket/sort-source-methods -- helpers are co-located with their loader and consumer triplets; autofix bails on the const-table interleaving and alphabetizing would scatter related helpers. export async function getEmscriptenVersion( packageRoot: string, ): Promise { @@ -114,7 +115,7 @@ export async function getEmscriptenVersion( return version } -let _nodeVersion: string | undefined +let nodeVersion: string | undefined /** * Load Node.js version from .node-version file at monorepo root. * Result is memoized for performance. @@ -125,8 +126,9 @@ let _nodeVersion: string | undefined * const version = getNodeVersion() * // Returns: '24.12.0' */ +// oxlint-disable-next-line socket/sort-source-methods -- helpers are co-located with their loader and consumer triplets; autofix bails on the const-table interleaving and alphabetizing would scatter related helpers. export function getNodeVersion(): string { - if (_nodeVersion === undefined) { + if (nodeVersion === undefined) { try { const content = readFileSync(NODE_VERSION_FILE, 'utf8') const version = content.trim() @@ -135,7 +137,7 @@ export function getNodeVersion(): string { throw new Error(`.node-version file is empty at: ${NODE_VERSION_FILE}`) } - _nodeVersion = version + nodeVersion = version } catch (e) { if ((e as NodeJS.ErrnoException).code === 'ENOENT') { throw new Error( @@ -147,10 +149,10 @@ export function getNodeVersion(): string { throw e } } - return _nodeVersion + return nodeVersion } -let _minPythonVersion: string | undefined +let minPythonVersion: string | undefined /** * Load minimum Python version from build-infra/external-tools.json. * Result is memoized for performance. @@ -161,8 +163,9 @@ let _minPythonVersion: string | undefined * const minVersion = getMinPythonVersion() * // Returns: '3.6' */ +// oxlint-disable-next-line socket/sort-source-methods -- helpers are co-located with their loader and consumer triplets; autofix bails on the const-table interleaving and alphabetizing would scatter related helpers. export function getMinPythonVersion(): string { - if (_minPythonVersion === undefined) { + if (minPythonVersion === undefined) { const data = loadExternalToolsSync(PACKAGE_ROOT) const pythonConfig = data.tools?.python @@ -181,9 +184,9 @@ export function getMinPythonVersion(): string { ) } - _minPythonVersion = version + minPythonVersion = version } - return _minPythonVersion as string + return minPythonVersion as string } /** @@ -196,6 +199,7 @@ export function getMinPythonVersion(): string { * const version = await getCMakeVersion(PACKAGE_ROOT) * // Returns: '3.28.1' */ +// oxlint-disable-next-line socket/sort-source-methods -- helpers are co-located with their loader and consumer triplets; autofix bails on the const-table interleaving and alphabetizing would scatter related helpers. export async function getCMakeVersion(packageRoot: string): Promise { const data = await loadExternalTools(packageRoot) @@ -229,6 +233,7 @@ export async function getCMakeVersion(packageRoot: string): Promise { * const version = await getToolVersion(PACKAGE_ROOT, 'emscripten') * // Returns: '4.0.20' */ +// oxlint-disable-next-line socket/sort-source-methods -- helpers are co-located with their loader and consumer triplets; autofix bails on the const-table interleaving and alphabetizing would scatter related helpers. export async function getToolVersion( packageRoot: string, toolName: string, @@ -269,6 +274,7 @@ export async function getToolVersion( * const version = getSubmoduleVersion('packages/lief-builder/upstream/lief', 'lief') * // Returns: '0.17.0' */ +// oxlint-disable-next-line socket/sort-source-methods -- helpers are co-located with their loader and consumer triplets; autofix bails on the const-table interleaving and alphabetizing would scatter related helpers. export function getSubmoduleVersion( submodulePath: string, packageName: string, @@ -348,6 +354,7 @@ export function getSubmoduleVersion( * const checksum = getSubmoduleChecksum('packages/node-smol-builder/upstream/node', 'node') * // Returns: { algorithm: 'sha256', hash: '10335f268f...' } */ +// oxlint-disable-next-line socket/sort-source-methods -- helpers are co-located with their loader and consumer triplets; autofix bails on the const-table interleaving and alphabetizing would scatter related helpers. export function getSubmoduleChecksum( submodulePath: string, packageName: string, @@ -413,6 +420,7 @@ export function getSubmoduleChecksum( * // Write to .gitmodules: # node-1.2.3 sha256: * } */ +// oxlint-disable-next-line socket/sort-source-methods -- helpers are co-located with their loader and consumer triplets; autofix bails on the const-table interleaving and alphabetizing would scatter related helpers. export async function fetchNodeChecksum( version: string, options?: { timeout?: number | undefined }, diff --git a/packages/build-infra/lib/vfs-tools-downloader.mts b/packages/build-infra/lib/vfs-tools-downloader.mts index 03a7cdb54..eeaadef59 100644 --- a/packages/build-infra/lib/vfs-tools-downloader.mts +++ b/packages/build-infra/lib/vfs-tools-downloader.mts @@ -1,5 +1,4 @@ // max-file-lines: legitimate -- cohesive module — one tool/domain/phase; splitting along arbitrary line cap would fracture related logic -/* oxlint-disable socket/sort-source-methods -- file is ordered by download pipeline phase (parse manifest → fetch → verify checksum → pack); alphabetizing across phases would scatter the download flow. */ /** * VFS External Tools Downloader * @@ -187,6 +186,7 @@ export function getPlatformKey( * @param {string} [arch] - Architecture (x64, arm64) * @returns {string[]} Array of tool names available for this platform */ +// oxlint-disable-next-line socket/sort-source-methods -- file is ordered by download pipeline phase (parse manifest → fetch → verify checksum → pack); alphabetizing across phases would scatter the download flow. export function getAvailableTools( platform = process.platform, arch = process.arch, @@ -219,6 +219,7 @@ const RETRY_DELAY_MS = 1000 * @param {string} filePath - Path to file * @returns {Promise} Hex-encoded SHA256 hash */ +// oxlint-disable-next-line socket/sort-source-methods -- file is ordered by download pipeline phase (parse manifest → fetch → verify checksum → pack); alphabetizing across phases would scatter the download flow. export async function computeFileSha256(filePath) { const hash = crypto.createHash('sha256') const stream = createReadStream(filePath) @@ -258,6 +259,7 @@ export async function verifyFileSha256(filePath, expectedHash) { * @param {number} [options.retries] - Max retries (default: 3) * @returns {Promise} */ +// oxlint-disable-next-line socket/sort-source-methods -- file is ordered by download pipeline phase (parse manifest → fetch → verify checksum → pack); alphabetizing across phases would scatter the download flow. export async function downloadFile(url, destPath, options = {}) { const timeout = options.timeout ?? DOWNLOAD_TIMEOUT_MS const maxRetries = options.retries ?? MAX_RETRIES @@ -280,6 +282,7 @@ export async function downloadFile(url, destPath, options = {}) { * @param {string} destDir - Destination directory * @returns {Promise} */ +// oxlint-disable-next-line socket/sort-source-methods -- file is ordered by download pipeline phase (parse manifest → fetch → verify checksum → pack); alphabetizing across phases would scatter the download flow. export async function extractArchive(archivePath, destDir) { await fs.mkdir(destDir, { recursive: true }) @@ -320,6 +323,7 @@ export async function extractArchive(archivePath, destDir) { * @param {boolean} [options.skipHashVerification] - Skip SHA256 verification (NOT RECOMMENDED) * @returns {Promise<{success: boolean, toolDir: string, version: string}>} */ +// oxlint-disable-next-line socket/sort-source-methods -- file is ordered by download pipeline phase (parse manifest → fetch → verify checksum → pack); alphabetizing across phases would scatter the download flow. export async function downloadVfsTool( toolName, { @@ -420,6 +424,7 @@ export async function downloadVfsTool( * @param {boolean} [options.force] - Force re-download even if exists * @returns {Promise<{success: boolean, downloaded: string[], failed: string[]}>} */ +// oxlint-disable-next-line socket/sort-source-methods -- file is ordered by download pipeline phase (parse manifest → fetch → verify checksum → pack); alphabetizing across phases would scatter the download flow. export async function downloadAllVfsTools({ arch = process.arch, destDir, @@ -483,6 +488,7 @@ export async function downloadAllVfsTools({ * @param {string} [options.arch] - Target architecture * @returns {Promise<{success: boolean, size: number}>} */ +// oxlint-disable-next-line socket/sort-source-methods -- file is ordered by download pipeline phase (parse manifest → fetch → verify checksum → pack); alphabetizing across phases would scatter the download flow. export async function createVfsToolsTarball({ arch = process.arch, outputPath, diff --git a/packages/build-infra/scripts/build-docker.mts b/packages/build-infra/scripts/build-docker.mts index 31957d327..616027216 100644 --- a/packages/build-infra/scripts/build-docker.mts +++ b/packages/build-infra/scripts/build-docker.mts @@ -84,6 +84,7 @@ export function parseArgs(args) { export function printHelp() { const targets = getAllTargets() + // oxlint-disable-next-line socket/no-logger-newline-literal -- help text is a single readable block; splitting would obscure structure. logger.log(` Build a package for a specific target. diff --git a/packages/build-infra/scripts/setup-docker-builds.mts b/packages/build-infra/scripts/setup-docker-builds.mts index b4c42c48b..6a57ab65e 100644 --- a/packages/build-infra/scripts/setup-docker-builds.mts +++ b/packages/build-infra/scripts/setup-docker-builds.mts @@ -83,6 +83,7 @@ export function parseArgs(args) { } export function printHelp() { + // oxlint-disable-next-line socket/no-logger-newline-literal -- help text is a single readable block; splitting would obscure structure. logger.log(` Setup Docker builder images for local builds. diff --git a/packages/build-infra/scripts/update-vfs-tools.mts b/packages/build-infra/scripts/update-vfs-tools.mts index 9e0ba8eb2..b6483e35e 100644 --- a/packages/build-infra/scripts/update-vfs-tools.mts +++ b/packages/build-infra/scripts/update-vfs-tools.mts @@ -1,5 +1,4 @@ #!/usr/bin/env node -/* oxlint-disable socket/sort-source-methods -- script is ordered as a top-down build pipeline (load manifest → fetch each tool → verify → write); alphabetizing would scatter the pipeline. */ /** * Update VFS Tools Script * @@ -29,7 +28,7 @@ import { httpText, } from '@socketsecurity/lib-stable/http-request/convenience' import { getDefaultLogger } from '@socketsecurity/lib-stable/logger' -import { escapeRegExp } from '@socketsecurity/lib-stable/regexps' +import { escapeRegExp } from '@socketsecurity/lib-stable/regexps/escape' import { errorMessage } from '../lib/error-utils.mts' @@ -141,6 +140,7 @@ export async function fetchText(url) { /** * Download file and compute SHA256. */ +// oxlint-disable-next-line socket/sort-source-methods -- script is ordered as a top-down build pipeline (load manifest → fetch each tool → verify → write); alphabetizing would scatter the pipeline. export async function downloadAndHash(url) { const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vfs-hash-')) const tmpFile = path.join(tmpDir, 'download') @@ -187,6 +187,7 @@ export function parseChecksumFile(content) { /** * Get Python embeddable package info (downloads and computes hashes). */ +// oxlint-disable-next-line socket/sort-source-methods -- script is ordered as a top-down build pipeline (load manifest → fetch each tool → verify → write); alphabetizing would scatter the pipeline. export async function getPythonRelease() { const { assets: assetPatterns, baseUrl, version } = PYTHON_CONFIG const assets = new Map() @@ -212,6 +213,7 @@ export async function getPythonRelease() { /** * Get latest release info for a tool. */ +// oxlint-disable-next-line socket/sort-source-methods -- script is ordered as a top-down build pipeline (load manifest → fetch each tool → verify → write); alphabetizing would scatter the pipeline. export async function getLatestRelease(toolName) { const config = TOOL_CONFIGS[toolName] if (!config) { @@ -265,6 +267,7 @@ export async function getLatestRelease(toolName) { /** * Generate the updated VFS_TOOL_URLS object as a string. */ +// oxlint-disable-next-line socket/sort-source-methods -- script is ordered as a top-down build pipeline (load manifest → fetch each tool → verify → write); alphabetizing would scatter the pipeline. export function generateToolConfig(toolName, version, assets) { const lines = [` ${toolName}: {`, ` version: '${version}',`] diff --git a/packages/build-infra/test/cache-key.test.mts b/packages/build-infra/test/cache-key.test.mts index 313b22557..fac7681e1 100644 --- a/packages/build-infra/test/cache-key.test.mts +++ b/packages/build-infra/test/cache-key.test.mts @@ -9,7 +9,7 @@ import os from 'node:os' import path from 'node:path' import { safeDelete } from '@socketsecurity/lib-stable/fs/safe' -import { escapeRegExp } from '@socketsecurity/lib-stable/regexps' +import { escapeRegExp } from '@socketsecurity/lib-stable/regexps/escape' import { nodeVersionRaw } from 'build-infra/lib/constants' import { diff --git a/packages/build-infra/test/checkpoint-manager.test.mts b/packages/build-infra/test/checkpoint-manager.test.mts index afdda3ff9..2e02e6447 100644 --- a/packages/build-infra/test/checkpoint-manager.test.mts +++ b/packages/build-infra/test/checkpoint-manager.test.mts @@ -720,7 +720,11 @@ describe('createCheckpoint signature validation', () => { logger.error('') logger.fail(`${error.file}:${error.line}`) logger.fail(` Error: ${error.error}`) - logger.fail(` Context:\n${error.context}\n`) + logger.fail(' Context:') + for (const line of error.context.split('\n')) { + logger.fail(` ${line}`) + } + logger.error('') } expect(allErrors).toHaveLength(0) diff --git a/packages/build-infra/test/validate-checkpoints.mts b/packages/build-infra/test/validate-checkpoints.mts index 11211456b..1e1ea6452 100644 --- a/packages/build-infra/test/validate-checkpoints.mts +++ b/packages/build-infra/test/validate-checkpoints.mts @@ -15,7 +15,7 @@ import { spawnSync } from '@socketsecurity/lib-stable/spawn/spawn' /** * Supported tar archive formats. */ -const _CHECKPOINT_FORMATS = ['*.tar', '*.tar.gz', '*.tgz'] as const +const CHECKPOINT_FORMATS = ['*.tar', '*.tar.gz', '*.tgz'] as const /** * Result of checkpoint validation. diff --git a/packages/build-infra/vitest.config.mts b/packages/build-infra/vitest.config.mts index 61c779dcf..bd8ef5a49 100644 --- a/packages/build-infra/vitest.config.mts +++ b/packages/build-infra/vitest.config.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/no-default-export -- vitest CLI auto-discovers config via default import. */ /** * Extends shared vitest config. */ @@ -6,6 +5,7 @@ import { defineConfig, mergeConfig } from 'vitest/config' import baseConfig from '../../.config/vitest.config.mts' +// oxlint-disable-next-line socket/no-default-export -- vitest CLI auto-discovers config via default import. export default mergeConfig( baseConfig, defineConfig({ diff --git a/packages/build-infra/wasm-synced/generate-sync-esm.mts b/packages/build-infra/wasm-synced/generate-sync-esm.mts index 7d4c9f2b0..074b7ad6c 100644 --- a/packages/build-infra/wasm-synced/generate-sync-esm.mts +++ b/packages/build-infra/wasm-synced/generate-sync-esm.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/prefer-exists-sync -- fs.stat() calls consume stats.size to size-document the WASM input and report the generated MJS wrapper size. */ /** * Generate ESM synchronous WASM wrapper. * @@ -62,6 +61,7 @@ export async function generateSyncEsm(options) { : ' * Built for synchronous instantiation.' // Get file size for documentation + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size to size-document the WASM input and report the generated MJS wrapper size. const mjsStats = await fs.stat(mjsFile) // Generate the ESM wrapper (no 'use strict', use export default) @@ -97,6 +97,7 @@ export default ${exportName}Module; await fs.writeFile(outputSyncMjs, mjsOutput, 'utf8') + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size to size-document the WASM input and report the generated MJS wrapper size. const syncMjsSize = (await fs.stat(outputSyncMjs)).size logger.substep(`Sync MJS (ESM): ${outputSyncMjs}`) logger.substep(`Sync MJS size: ${(syncMjsSize / 1024).toFixed(2)} KB`) diff --git a/packages/build-infra/wasm-synced/generate-sync-phase.mts b/packages/build-infra/wasm-synced/generate-sync-phase.mts index 62ea50ee6..51f48c4c2 100644 --- a/packages/build-infra/wasm-synced/generate-sync-phase.mts +++ b/packages/build-infra/wasm-synced/generate-sync-phase.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/prefer-exists-sync -- fs.stat() calls consume stats.size to detect empty-output failures during the WASM sync-wrapper generation phase. */ /** * Shared WASM sync wrapper generation phase. * @@ -11,7 +10,7 @@ import path from 'node:path' import { safeDelete, safeMkdir } from '@socketsecurity/lib-stable/fs/safe' import { getDefaultLogger } from '@socketsecurity/lib-stable/logger' -import { hasKeys } from '@socketsecurity/lib-stable/objects' +import { hasKeys } from '@socketsecurity/lib-stable/objects/types' import { getFileSize } from 'build-infra/lib/build-helpers' import { generateWasmSyncWrapper } from 'build-infra/wasm-synced/wasm-sync-wrapper' @@ -50,7 +49,7 @@ export async function generateSync(options) { ) logger.logNewline() - const _require = createRequire(import.meta.url) + const require = createRequire(import.meta.url) // Extract package-specific config. const { @@ -89,6 +88,7 @@ export async function generateSync(options) { throw new Error('Sync JS file not found') } + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size to detect empty-output failures during the WASM sync-wrapper generation phase. const syncStats = await fs.stat(syncJsFilePath) if (syncStats.size === 0) { throw new Error('Sync JS file is empty') @@ -118,11 +118,12 @@ export async function generateSync(options) { binaryPath: path.relative(buildDir, outputSyncDir), binarySize: syncSize, smokeTest: async () => { + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size to detect empty-output failures during the WASM sync-wrapper generation phase. const syncStats = await fs.stat(syncJsFile) if (syncStats.size === 0) { throw new Error('Sync wrapper file is empty') } - const wasmModule = _require(syncJsFile) + const wasmModule = require(syncJsFile) if (wasmModule === undefined || typeof wasmModule !== 'object') { throw new Error( `Sync wrapper failed to load properly: got ${typeof wasmModule}`, diff --git a/packages/build-infra/wasm-synced/transform.mts b/packages/build-infra/wasm-synced/transform.mts index b5a2c2360..89e314445 100644 --- a/packages/build-infra/wasm-synced/transform.mts +++ b/packages/build-infra/wasm-synced/transform.mts @@ -383,8 +383,8 @@ export async function applyCommonTransforms(options) { safeOverwrite(node.start, node.end, '__filename') } else if ( isNewURLWithFilename || - isNewURLWithImportMetaUrl || - isNewURLWithImportMeta + isNewURLWithImportMeta || + isNewURLWithImportMetaUrl ) { // Replace require("url").fileURLToPath(new URL("./", __filename or __importMetaUrl or import.meta.url)) with __dirname safeOverwrite(node.start, node.end, '__dirname') @@ -436,8 +436,8 @@ export async function applyCommonTransforms(options) { safeOverwrite(node.start, node.end, '__filename') } else if ( isNewURLWithFilename || - isNewURLWithImportMetaUrl || - isNewURLWithImportMeta + isNewURLWithImportMeta || + isNewURLWithImportMetaUrl ) { // Replace fileURLToPath(new URL("./", __filename or __importMetaUrl or import.meta.url)) with __dirname safeOverwrite(node.start, node.end, '__dirname') diff --git a/packages/build-infra/wasm-synced/wasm-sync-wrapper.mts b/packages/build-infra/wasm-synced/wasm-sync-wrapper.mts index c9d6f90f0..b0f03c642 100644 --- a/packages/build-infra/wasm-synced/wasm-sync-wrapper.mts +++ b/packages/build-infra/wasm-synced/wasm-sync-wrapper.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/prefer-exists-sync -- fs.stat() calls consume stats.size to size-report the generated CJS/MJS wrappers. */ /** * Generate synchronous WASM wrapper with embedded base64 binary. * @@ -127,6 +126,7 @@ export async function generateWasmSyncWrapper(options) { throw new Error('Sync CJS file not found after generation') } + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size to size-report the generated CJS/MJS wrappers. const syncStats = await fs.stat(outputSyncCjs) if (syncStats.size === 0) { throw new Error('Sync CJS file is empty') @@ -149,6 +149,7 @@ export async function generateWasmSyncWrapper(options) { throw new Error('Sync MJS file not found after generation') } + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size to size-report the generated CJS/MJS wrappers. const syncMjsStats = await fs.stat(outputSyncMjs) if (syncMjsStats.size === 0) { throw new Error('Sync MJS file is empty') diff --git a/packages/codet5-models-builder/test/build-output.test.mts b/packages/codet5-models-builder/test/build-output.test.mts index 7b1a1ab20..04690b377 100644 --- a/packages/codet5-models-builder/test/build-output.test.mts +++ b/packages/codet5-models-builder/test/build-output.test.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/prefer-exists-sync -- every fs.stat() reads stats.size to assert int4/int8 quantized model artifacts are in the expected size range. */ /** * @fileoverview Tests for codet5-models-builder model output files. * Validates that the build process generates correct encoder/decoder structure and formats. @@ -93,6 +92,7 @@ describe.skipIf(!hasBuiltArtifacts)( return } + // oxlint-disable-next-line socket/prefer-exists-sync -- every fs.stat() reads stats.size to assert int4/int8 quantized model artifacts are in the expected size range. const stats = await fs.stat(encoderPath) expect(stats.size).toBeGreaterThan(20 * 1024 * 1024) // CodeT5 encoder is typically 40-120MB @@ -107,6 +107,7 @@ describe.skipIf(!hasBuiltArtifacts)( return } + // oxlint-disable-next-line socket/prefer-exists-sync -- every fs.stat() reads stats.size to assert int4/int8 quantized model artifacts are in the expected size range. const stats = await fs.stat(decoderPath) expect(stats.size).toBeGreaterThan(20 * 1024 * 1024) // CodeT5 decoder is typically 40-200MB @@ -164,7 +165,9 @@ describe.skipIf(!hasBuiltArtifacts)( return } + // oxlint-disable-next-line socket/prefer-exists-sync -- every fs.stat() reads stats.size to assert int4/int8 quantized model artifacts are in the expected size range. const int4Stats = await fs.stat(int4EncoderPath) + // oxlint-disable-next-line socket/prefer-exists-sync -- every fs.stat() reads stats.size to assert int4/int8 quantized model artifacts are in the expected size range. const int8Stats = await fs.stat(int8EncoderPath) expect(int4Stats.size).toBeLessThan(int8Stats.size) diff --git a/packages/codet5-models-builder/vitest.config.mts b/packages/codet5-models-builder/vitest.config.mts index 0d74448c1..b87c54356 100644 --- a/packages/codet5-models-builder/vitest.config.mts +++ b/packages/codet5-models-builder/vitest.config.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/no-default-export -- vitest CLI auto-discovers config via default import. */ /** * Extends shared vitest config. * Uses default 2-minute timeout from base config (sufficient for model builder tests). @@ -7,6 +6,7 @@ import { defineConfig, mergeConfig } from 'vitest/config' import baseConfig from '../../.config/vitest.config.mts' +// oxlint-disable-next-line socket/no-default-export -- vitest CLI auto-discovers config via default import. export default mergeConfig( baseConfig, defineConfig({ diff --git a/packages/curl-builder/scripts/build.mts b/packages/curl-builder/scripts/build.mts index 7d193c6fd..9fb11dc79 100755 --- a/packages/curl-builder/scripts/build.mts +++ b/packages/curl-builder/scripts/build.mts @@ -1,6 +1,4 @@ // max-file-lines: legitimate -- single builder pipeline (fetch → patch → build → package) — splitting fractures the build sequence -/* oxlint-disable socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. */ -/* oxlint-disable socket/prefer-exists-sync -- multiple fs.stat() calls consume stats.size for downloaded-archive / built-library size reporting and minimum-size quick checks. */ /** * Build script for libcurl with mbedTLS. * Downloads prebuilt libcurl from GitHub releases or builds from source. @@ -78,6 +76,7 @@ const TARGET_ARCH = process.env.TARGET_ARCH || process.arch * @param {string} platformArch - Platform-arch identifier. * @returns {{ buildDir: string, curlBuildDir: string, mbedtlsBuildDir: string }} */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export function getBuildDirs(platformArch) { const buildDir = getPlatformBuildDir(packageRoot, platformArch) const curlBuildDir = path.join(buildDir, 'out', BUILD_STAGES.FINAL, 'curl') @@ -106,6 +105,7 @@ const CURL_REQUIRED_FILES = [ * @param {string} dir - Directory to check. * @returns {boolean} True if all required files exist. */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export function curlExistsAt(dir) { return CURL_REQUIRED_FILES.every(file => existsSync(path.join(dir, file))) } @@ -135,6 +135,7 @@ export async function verifyArchiveChecksum(archivePath, assetName) { * @param {string} [options.platformArch] - Override platform-arch. * @returns {Promise} Path to downloaded curl directory. */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export async function downloadCurl(options = {}) { const { force = false, platformArch } = options const resolvedPlatformArch = platformArch ?? (await getCurrentPlatformArch()) @@ -188,6 +189,7 @@ export async function downloadCurl(options = {}) { } // Verify tarball integrity before extraction (detect corrupted/truncated downloads). + // oxlint-disable-next-line socket/prefer-exists-sync -- multiple fs.stat() calls consume stats.size for downloaded-archive / built-library size reporting and minimum-size quick checks. const archiveStats = await fs.stat(downloadedArchive) logger.info( `Archive size: ${(archiveStats.size / 1024 / 1024).toFixed(2)} MB`, @@ -286,6 +288,7 @@ export async function downloadCurl(options = {}) { // Write version file after cleanup to ensure curl exists check passes. await fs.writeFile(versionFile, CURL_VERSION, 'utf8') + // oxlint-disable-next-line socket/prefer-exists-sync -- multiple fs.stat() calls consume stats.size for downloaded-archive / built-library size reporting and minimum-size quick checks. const stats = await fs.stat(path.join(extractDir, 'libcurl.a')) const sizeMB = (stats.size / 1024 / 1024).toFixed(2) logger.success(`Downloaded curl (${sizeMB} MB) to ${extractDir}`) @@ -302,6 +305,7 @@ export async function downloadCurl(options = {}) { * @param {string} [options.platformArch] - Override platform-arch. * @returns {Promise} Path to directory containing curl libraries. */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export async function ensureCurl(options = {}) { const { force = false, platformArch } = options const resolvedPlatformArch = platformArch ?? (await getCurrentPlatformArch()) @@ -342,6 +346,7 @@ const MBEDTLS_VERSION = getMbedTLSVersion() * Extract curl version from .gitmodules comment. * @returns {string} Curl version (e.g., "8.18.0") */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export function getCurlVersion() { const version = getSubmoduleVersion( 'packages/curl-builder/upstream/curl', @@ -355,6 +360,7 @@ export function getCurlVersion() { * Extract mbedTLS version from .gitmodules comment. * @returns {string} mbedTLS version (e.g., "3.6.5") */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export function getMbedTLSVersion() { const version = getSubmoduleVersion( 'packages/curl-builder/upstream/mbedtls', @@ -364,6 +370,7 @@ export function getMbedTLSVersion() { return version } +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export async function runCommand(command, args, cwd, env = {}) { logger.info(`Running: ${command} ${args.join(' ')}`) @@ -402,6 +409,7 @@ export async function runCommand(command, args, cwd, env = {}) { * * @param {string} mbedtlsBuildDir - Directory to build mbedTLS in. */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export async function buildMbedTLS(mbedtlsBuildDir) { logger.info('Building mbedTLS...') @@ -518,6 +526,7 @@ export async function buildMbedTLS(mbedtlsBuildDir) { * @param {string} mbedtlsDir - Directory containing mbedTLS build. * @param {string} curlBuildDir - Directory to build curl in. */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export async function buildCurl(mbedtlsDir, curlBuildDir) { logger.info('Building curl with mbedTLS...') @@ -674,6 +683,7 @@ export async function buildCurl(mbedtlsDir, curlBuildDir) { * @param {string} mbedtlsDir - Directory containing mbedTLS build. * @param {string} curlBuildDir - Directory containing curl build. */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export async function copyDistributionFiles(mbedtlsDir, curlBuildDir) { const distDir = path.join(curlBuildDir, 'dist') await safeMkdir(distDir) @@ -781,6 +791,7 @@ async function main() { logger.info('curl submodule not initialized, using prebuilt...') const curlDir = await ensureCurl() const curlLib = path.join(curlDir, 'libcurl.a') + // oxlint-disable-next-line socket/prefer-exists-sync -- multiple fs.stat() calls consume stats.size for downloaded-archive / built-library size reporting and minimum-size quick checks. const stats = await fs.stat(curlLib) const sizeMB = (stats.size / 1024 / 1024).toFixed(2) @@ -790,6 +801,7 @@ async function main() { CHECKPOINTS.FINALIZED, async () => { // Verify library exists and has reasonable size. + // oxlint-disable-next-line socket/prefer-exists-sync -- multiple fs.stat() calls consume stats.size for downloaded-archive / built-library size reporting and minimum-size quick checks. const libStats = await fs.stat(curlLib) if (libStats.size < 100_000) { throw new Error( @@ -849,6 +861,7 @@ async function main() { mbedtlsDir = await buildMbedTLS(mbedtlsBuildDir) // Create mbedtls checkpoint. + // oxlint-disable-next-line socket/prefer-exists-sync -- multiple fs.stat() calls consume stats.size for downloaded-archive / built-library size reporting and minimum-size quick checks. const mbedtlsStats = await fs.stat(mbedtlsLibPath) await createCheckpoint( buildDir, @@ -889,6 +902,7 @@ async function main() { throw new Error(`curl library not found at ${libPath}`) } + // oxlint-disable-next-line socket/prefer-exists-sync -- multiple fs.stat() calls consume stats.size for downloaded-archive / built-library size reporting and minimum-size quick checks. const stats = await fs.stat(libPath) const sizeMB = (stats.size / 1024 / 1024).toFixed(2) logger.info(`curl library size: ${sizeMB} MB`) @@ -899,6 +913,7 @@ async function main() { CHECKPOINTS.FINALIZED, async () => { // Verify library exists and has reasonable size. + // oxlint-disable-next-line socket/prefer-exists-sync -- multiple fs.stat() calls consume stats.size for downloaded-archive / built-library size reporting and minimum-size quick checks. const libStats = await fs.stat(libPath) if (libStats.size < 100_000) { throw new Error( diff --git a/packages/curl-builder/vitest.config.mts b/packages/curl-builder/vitest.config.mts index 61c779dcf..bd8ef5a49 100644 --- a/packages/curl-builder/vitest.config.mts +++ b/packages/curl-builder/vitest.config.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/no-default-export -- vitest CLI auto-discovers config via default import. */ /** * Extends shared vitest config. */ @@ -6,6 +5,7 @@ import { defineConfig, mergeConfig } from 'vitest/config' import baseConfig from '../../.config/vitest.config.mts' +// oxlint-disable-next-line socket/no-default-export -- vitest CLI auto-discovers config via default import. export default mergeConfig( baseConfig, defineConfig({ diff --git a/packages/dawn-builder/README.md b/packages/dawn-builder/README.md new file mode 100644 index 000000000..984ecaca2 --- /dev/null +++ b/packages/dawn-builder/README.md @@ -0,0 +1,61 @@ +# dawn-builder + +Builder package for [Dawn](https://dawn.googlesource.com/dawn), Chromium's +WebGPU implementation. Produces `libwebgpu_dawn.a` + headers that +`node-smol-builder` links against to expose the `node:smol-webgpu` +builtin. + +## Status + +**Scaffolding (D1).** Build script + submodule + binding adaptation +land in follow-up D2-D9 commits per +[`.claude/plans/dawn-webgpu-integration.md`](../../.claude/plans/dawn-webgpu-integration.md). + +## Why a separate builder package + +Dawn is ~436 MB cloned with active development tracking the Chromium +release cadence (~6-week branch bumps). Isolating its submodule + build +into its own `*-builder` package: + +- Keeps `node-smol-builder/upstream/` lean for the node submodule + the + small native deps (uSockets, md4c, tree-sitter, libqrencode). +- Lets the Dawn build cache key be the submodule SHA — invalidates only + when Dawn moves, not when node-smol's own sources change. +- Matches the existing `*-builder` convention used by curl, yoga, + onnxruntime, lief, etc. + +## CMake island build + +Dawn ships both a GN-based Chromium-tooling build and a self-contained +CMake build (`CMakeLists.txt` at the Dawn repo root). We use the CMake +form — same shape as `yoga-layout-builder` and `onnxruntime-builder`. + +The build produces: + +- `libwebgpu_dawn.a` (~40 MB stripped, ~200 MB unstripped on macos-arm64) +- Headers under `build///out/include/` + +`node-smol-builder` links the static lib + includes the headers into the +`node:smol-webgpu` binding's compilation unit. + +## Cache key + +The Dawn submodule SHA participates in `node-smol-builder`'s SOURCE_PATCHED +cache key via `prepare-external-sources.mts`. Bumping the Dawn submodule +invalidates the cache; node-smol re-links against the updated artifact. + +Within a single Dawn SHA, the build is incremental — ccache handles the +per-translation-unit re-compilation when only headers change. + +## Sparse checkout + +Dawn's tree includes ~250 MB of `third_party/` we don't need (ANGLE, DXC, +webgpu-cts, samples, docs). Submodule sparse-checkout config restricts +to: + +- `src/dawn/{native,common,platform,utils}/` +- `src/tint/` +- `include/dawn/` +- `third_party/{spirv-tools,spirv-headers,vulkan-headers,abseil-cpp}/` + +Reduces submodule size to ~180 MB. diff --git a/packages/dawn-builder/external-tools.json b/packages/dawn-builder/external-tools.json new file mode 100644 index 000000000..f8f42be32 --- /dev/null +++ b/packages/dawn-builder/external-tools.json @@ -0,0 +1,14 @@ +{ + "$schema": "../build-infra/lib/external-tools-schema.json", + "description": "External build tools required by dawn-builder. CMake + Ninja drive the Dawn island-build via packages/dawn-builder/scripts/build.mts.", + "tools": { + "cmake": { + "description": "CMake build system generator. Dawn's CMakeLists.txt is the island-build entry; we use the cmake CLI to configure and drive Ninja.", + "version": "3.30.5" + }, + "ninja": { + "description": "Ninja build system. Dawn's CMake setup generates Ninja files for fast parallel builds.", + "version": "1.12.1" + } + } +} diff --git a/packages/dawn-builder/package.json b/packages/dawn-builder/package.json new file mode 100644 index 000000000..c59ca113d --- /dev/null +++ b/packages/dawn-builder/package.json @@ -0,0 +1,25 @@ +{ + "name": "dawn-builder", + "version": "0.0.0", + "description": "Dawn (Chromium's WebGPU implementation) builder. Produces libwebgpu_dawn.a + headers that node-smol-builder links against to expose the node:smol-webgpu builtin.", + "private": true, + "license": "MIT", + "type": "module", + "exports": { + "./scripts/paths": "./scripts/paths.mts" + }, + "scripts": { + "build": "node scripts/build.mts", + "build:dev": "node scripts/build.mts --mode=dev", + "build:force": "node scripts/build.mts --force", + "build:prod": "node scripts/build.mts --mode=prod", + "clean": "node scripts/clean.mts" + }, + "dependencies": { + "@socketsecurity/lib-stable": "catalog:", + "build-infra": "workspace:*" + }, + "devDependencies": { + "@types/node": "catalog:" + } +} diff --git a/packages/dawn-builder/scripts/build.mts b/packages/dawn-builder/scripts/build.mts new file mode 100644 index 000000000..162aba492 --- /dev/null +++ b/packages/dawn-builder/scripts/build.mts @@ -0,0 +1,184 @@ +#!/usr/bin/env node +/** + * Build Dawn via the CMake island-build path. + * + * Dawn ships two build systems: + * - GN + depot_tools (Chromium's default; ~3 GB of tooling) + * - Self-contained CMake at upstream/dawn/CMakeLists.txt + * + * We use the CMake form — same shape as yoga-layout-builder / + * onnxruntime-builder. CMake fetches Dawn's third-party deps via + * FetchContent (DAWN_FETCH_DEPENDENCIES=ON), so no manual checkout + * of abseil-cpp / spirv-tools / etc. is required. + * + * Output (per --mode = dev|prod, current platform-arch): + * build///cmake/ — cmake configure artifacts + * build///out/ + * lib/libwebgpu_dawn.a — static library node-smol links + * include/ — public headers + * + * Flags: + * --mode=dev|prod (default: dev) debug vs release optimization + * --force re-configure even if cached + * --jobs=N (default: ncpu) parallel ninja workers + * + * Drift watch: + * - DAWN_BUILD_NODE_BINDINGS=OFF — we adapt the binding ourselves. + * - DAWN_BUILD_TESTS=OFF + TINT_BUILD_TESTS=OFF — Dawn's CMake + * pulls googletest when tests are on; we don't run them. + * - DAWN_BUILD_SAMPLES=OFF — sample apps would also pull GLFW. + * - BUILD_SHARED_LIBS=OFF + CMAKE_POSITION_INDEPENDENT_CODE=ON — + * we need a static lib that can be linked into node-smol's + * executable (PIC required for static libs included in the + * final relocatable link). + * + * NOTE: this is the D3 scaffold. The full build will take 30-60 min + * on first run + ~150 GB peak disk during the third-party fetch. + * Build caching (ccache) lands in a follow-up. + */ + +import { existsSync } from 'node:fs' +import path from 'node:path' +import process from 'node:process' + +import { which } from '@socketsecurity/lib-stable/bin/which' +import { safeMkdir } from '@socketsecurity/lib-stable/fs/safe' +import { getDefaultLogger } from '@socketsecurity/lib-stable/logger' +import { spawn } from '@socketsecurity/lib-stable/spawn/spawn' + +import { getCurrentPlatformArch } from 'build-infra/lib/platform-mappings' + +import { UPSTREAM_DAWN_DIR, getBuildPaths } from './paths.mts' + +const logger = getDefaultLogger() + +interface BuildOptions { + mode: 'dev' | 'prod' + force: boolean + jobs: number +} + +export function parseArgs(): BuildOptions { + const args = process.argv.slice(2) + let mode: 'dev' | 'prod' = 'dev' + let force = false + let jobs = 0 + for (let i = 0, { length } = args; i < length; i += 1) { + const a = args[i] + if (a === '--prod') { + mode = 'prod' + } else if (a === '--dev') { + mode = 'dev' + } else if (a.startsWith('--mode=')) { + const v = a.slice('--mode='.length) + if (v === 'dev' || v === 'prod') { + mode = v + } + } else if (a === '--force') { + force = true + } else if (a.startsWith('--jobs=')) { + jobs = parseInt(a.slice('--jobs='.length), 10) || 0 + } + } + if (jobs === 0) { + // Detect at runtime; fall back to a conservative 4 if detection fails. + // Dawn's link step is single-threaded so over-provisioning helps the + // compile phase but doesn't speed up the tail. + const os = require('node:os') + jobs = (typeof os.availableParallelism === 'function' + ? os.availableParallelism() + : os.cpus().length) || 4 + } + return { force, jobs, mode } +} + +async function main(): Promise { + const opts = parseArgs() + + if (!existsSync(UPSTREAM_DAWN_DIR)) { + throw new Error( + `Dawn submodule not found at ${UPSTREAM_DAWN_DIR}. Run \`git submodule update --init packages/dawn-builder/upstream/dawn\` first.`, + ) + } + + const cmakePath = await which('cmake') + if (!cmakePath) { + throw new Error( + `cmake not found on PATH. Dawn requires CMake ≥ 3.30; pinned version + install lives in packages/dawn-builder/external-tools.json.`, + ) + } + const ninjaPath = await which('ninja') + if (!ninjaPath) { + throw new Error( + `ninja not found on PATH. Dawn's CMake setup generates Ninja files; pinned version lives in packages/dawn-builder/external-tools.json.`, + ) + } + + const platformArch = getCurrentPlatformArch() + const paths = getBuildPaths(opts.mode, platformArch) + + await safeMkdir(paths.cmakeDir, { recursive: true }) + await safeMkdir(paths.outputDir, { recursive: true }) + + // CMake configure. + const configureArgs = [ + '-S', + UPSTREAM_DAWN_DIR, + '-B', + paths.cmakeDir, + '-G', + 'Ninja', + `-DCMAKE_BUILD_TYPE=${opts.mode === 'prod' ? 'Release' : 'RelWithDebInfo'}`, + '-DCMAKE_POSITION_INDEPENDENT_CODE=ON', + '-DBUILD_SHARED_LIBS=OFF', + '-DDAWN_BUILD_NODE_BINDINGS=OFF', + '-DDAWN_BUILD_SAMPLES=OFF', + '-DDAWN_BUILD_TESTS=OFF', + '-DDAWN_FETCH_DEPENDENCIES=ON', + '-DTINT_BUILD_TESTS=OFF', + `-DCMAKE_INSTALL_PREFIX=${paths.outputDir}`, + ] + logger.step(`Configuring Dawn (mode=${opts.mode}, jobs=${opts.jobs})`) + const configureResult = await spawn(cmakePath, configureArgs, { + cwd: UPSTREAM_DAWN_DIR, + stdio: 'inherit', + }) + if (configureResult.exitCode !== 0) { + throw new Error( + `cmake configure failed with exit code ${configureResult.exitCode}`, + ) + } + + // CMake build — produces libwebgpu_dawn.a + transitive Tint / + // SPIRV-Tools static libs. + const buildArgs = [ + '--build', + paths.cmakeDir, + '--target', + 'webgpu_dawn', + '--parallel', + String(opts.jobs), + ] + logger.step('Building webgpu_dawn target') + const buildResult = await spawn(cmakePath, buildArgs, { + stdio: 'inherit', + }) + if (buildResult.exitCode !== 0) { + throw new Error(`cmake build failed with exit code ${buildResult.exitCode}`) + } + + // Verify the expected output landed. + const expectedLib = path.join(paths.cmakeDir, 'src', 'dawn', 'native', + 'libwebgpu_dawn.a') + if (!existsSync(expectedLib)) { + throw new Error( + `Build succeeded but ${expectedLib} not found. CMake target layout may have changed; check Dawn's CMakeLists.txt.`, + ) + } + logger.success(`Dawn built at ${expectedLib}`) +} + +main().catch(err => { + logger.fail(`dawn-builder failed: ${err}`) + process.exitCode = 1 +}) diff --git a/packages/dawn-builder/scripts/clean.mts b/packages/dawn-builder/scripts/clean.mts new file mode 100644 index 000000000..0698b8e71 --- /dev/null +++ b/packages/dawn-builder/scripts/clean.mts @@ -0,0 +1,25 @@ +#!/usr/bin/env node +/** + * Clean dawn-builder's build output. + * + * Removes everything under build/ but leaves upstream/ (submodule) + * + scripts/ + lib/ + node_modules/ alone. + */ + +import { safeDelete } from '@socketsecurity/lib-stable/fs/safe' +import { getDefaultLogger } from '@socketsecurity/lib-stable/logger' + +import { BUILD_ROOT } from './paths.mts' + +const logger = getDefaultLogger() + +async function main(): Promise { + logger.info(`Cleaning ${BUILD_ROOT}`) + await safeDelete(BUILD_ROOT) + logger.success('Clean complete') +} + +main().catch(err => { + logger.fail(`Failed: ${err}`) + process.exitCode = 1 +}) diff --git a/packages/dawn-builder/scripts/paths.mts b/packages/dawn-builder/scripts/paths.mts new file mode 100644 index 000000000..ac0875d9d --- /dev/null +++ b/packages/dawn-builder/scripts/paths.mts @@ -0,0 +1,65 @@ +/** + * Centralized path resolution for dawn-builder. + * + * Source of truth for all build paths (per `1 path, 1 reference`). + * Other dawn-builder scripts (build, clean, future test/check + * scripts) import these instead of constructing paths themselves. + * + * node-smol-builder reads `BUILD_ROOT` + `UPSTREAM_DAWN_DIR` via the + * workspace import `dawn-builder/scripts/paths` to find the prebuilt + * libwebgpu_dawn.a + headers at link time. + */ + +// Inherit canonical roots (REPO_ROOT, CONFIG_DIR, NODE_MODULES_CACHE_DIR, +// etc.) from the repo-root paths module per fleet rule +// `paths-mts-inherit-guard`. +export * from '../../../scripts/paths.mts' + +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +// Package root: packages/dawn-builder/ +export const PACKAGE_ROOT = path.resolve(__dirname, '..') + +// Build outputs land under build/// per the +// canonical fleet layout. See packages/yoga-layout-builder for the +// reference shape. +export const BUILD_ROOT = path.join(PACKAGE_ROOT, 'build') + +// Upstream Dawn submodule root. Sparse-checkout via .gitmodules +// limits the disk footprint; see D2 commit for the sparse-checkout +// config + submodule pin. +export const UPSTREAM_DAWN_DIR = path.join(PACKAGE_ROOT, 'upstream', 'dawn') + +/** + * Per-mode (`dev` | `prod`) + per-platform-arch build paths. + * + * Output layout: + * build///cmake/ — cmake configure output + * build///out/ + * lib/libwebgpu_dawn.a — static library + * include/ — headers (dawn/, tint/, etc.) + */ +export function getBuildPaths( + mode: 'dev' | 'prod', + platformArch: string, +): { + buildDir: string + cmakeDir: string + outputDir: string + outputLibFile: string + outputIncludeDir: string +} { + const buildDir = path.join(BUILD_ROOT, mode, platformArch) + const cmakeDir = path.join(buildDir, 'cmake') + const outputDir = path.join(buildDir, 'out') + return { + buildDir, + cmakeDir, + outputDir, + outputLibFile: path.join(outputDir, 'lib', 'libwebgpu_dawn.a'), + outputIncludeDir: path.join(outputDir, 'include'), + } +} diff --git a/packages/dawn-builder/upstream/dawn b/packages/dawn-builder/upstream/dawn new file mode 160000 index 000000000..e935a1b57 --- /dev/null +++ b/packages/dawn-builder/upstream/dawn @@ -0,0 +1 @@ +Subproject commit e935a1b57eb859db0d00c522e198711a3f313a25 diff --git a/packages/ink-builder/.config/esbuild/shorten-paths.mts b/packages/ink-builder/.config/esbuild/shorten-paths.mts index 78a887d0c..cb44b87a3 100644 --- a/packages/ink-builder/.config/esbuild/shorten-paths.mts +++ b/packages/ink-builder/.config/esbuild/shorten-paths.mts @@ -77,9 +77,11 @@ export function createPathShorteningPlugin() { if (conflictDetector.has(shortPath)) { const existingPath = conflictDetector.get(shortPath) if (existingPath !== longPath) { - logger.warn( - `Path conflict detected:\n "${shortPath}"\n Maps to: "${existingPath}"\n Also from: "${longPath}"\n Keeping original paths to avoid conflict.`, - ) + logger.warn('Path conflict detected:') + logger.warn(` "${shortPath}"`) + logger.warn(` Maps to: "${existingPath}"`) + logger.warn(` Also from: "${longPath}"`) + logger.warn(' Keeping original paths to avoid conflict.') shortPath = longPath } } else { diff --git a/packages/ink-builder/vitest.config.mts b/packages/ink-builder/vitest.config.mts index 4dcff4aae..179c485ca 100644 --- a/packages/ink-builder/vitest.config.mts +++ b/packages/ink-builder/vitest.config.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/no-default-export -- vitest CLI auto-discovers config via default import. */ /** * Extends shared vitest config. */ @@ -6,6 +5,7 @@ import { defineConfig, mergeConfig } from 'vitest/config' import baseConfig from '../../.config/vitest.config.mts' +// oxlint-disable-next-line socket/no-default-export -- vitest CLI auto-discovers config via default import. export default mergeConfig( baseConfig, defineConfig({ diff --git a/packages/iocraft-builder/examples/interactive-counter.mts b/packages/iocraft-builder/examples/interactive-counter.mts index fb0d354d7..e779083c0 100644 --- a/packages/iocraft-builder/examples/interactive-counter.mts +++ b/packages/iocraft-builder/examples/interactive-counter.mts @@ -244,12 +244,14 @@ async function main() { // Error handling process.on('uncaughtException', err => { - logger.error('\nFatal error:', err) + logger.error('') + logger.error('Fatal error:', err) process.exit(1) }) process.on('unhandledRejection', err => { - logger.error('\nUnhandled rejection:', err) + logger.error('') + logger.error('Unhandled rejection:', err) process.exit(1) }) diff --git a/packages/iocraft-builder/scripts/build.mts b/packages/iocraft-builder/scripts/build.mts index 5bbf56175..beb6c09fe 100644 --- a/packages/iocraft-builder/scripts/build.mts +++ b/packages/iocraft-builder/scripts/build.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/sort-source-methods -- build script is ordered as a top-down pipeline (cargo setup → napi-rs build → finalize); alphabetizing would scatter the flow. */ /** * Build iocraft - Native Node.js bindings for iocraft TUI library. * @@ -192,6 +191,7 @@ const IOCRAFT_PERF_RUSTFLAGS = [ * source by precedence, no merging), so this list MUST contain everything we * want rustc to receive. */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (cargo setup → napi-rs build → finalize); alphabetizing would scatter the flow. export function buildRustflags() { return [...getRustcRemapFlags(), ...IOCRAFT_PERF_RUSTFLAGS] } @@ -228,6 +228,7 @@ export async function runCargo(args, options = {}) { /** * Build the native addon. */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (cargo setup → napi-rs build → finalize); alphabetizing would scatter the flow. export async function buildNativeAddon() { logger.step('Building native addon') @@ -320,6 +321,7 @@ export async function buildNativeAddon() { /** * Map Rust target triple to Node.js platform. */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (cargo setup → napi-rs build → finalize); alphabetizing would scatter the flow. export function getTargetPlatform(rustTarget) { if (rustTarget.includes('darwin') || rustTarget.includes('apple')) { return 'darwin' diff --git a/packages/iocraft-builder/vitest.config.mts b/packages/iocraft-builder/vitest.config.mts index f8f50b054..05a53cf7f 100644 --- a/packages/iocraft-builder/vitest.config.mts +++ b/packages/iocraft-builder/vitest.config.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/no-default-export -- vitest CLI auto-discovers config via default import. */ /** * Extends shared vitest config. */ @@ -6,6 +5,7 @@ import { defineConfig, mergeConfig } from 'vitest/config' import baseConfig from '../../.config/vitest.config.mts' +// oxlint-disable-next-line socket/no-default-export -- vitest CLI auto-discovers config via default import. export default mergeConfig( baseConfig, defineConfig({ diff --git a/packages/libpq-builder/scripts/build.mts b/packages/libpq-builder/scripts/build.mts index 043cd7a8f..820c425ed 100644 --- a/packages/libpq-builder/scripts/build.mts +++ b/packages/libpq-builder/scripts/build.mts @@ -1,6 +1,4 @@ // max-file-lines: legitimate -- single builder pipeline (fetch → patch → build → package) — splitting fractures the build sequence -/* oxlint-disable socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. */ -/* oxlint-disable socket/prefer-exists-sync -- multiple fs.stat() calls consume stats.size for downloaded-archive / built-library size reporting and minimum-size quick checks. */ /** * Build script for libpq PostgreSQL client library. * Downloads prebuilt libpq from GitHub releases or builds from source. @@ -73,6 +71,7 @@ const TARGET_ARCH = process.env.TARGET_ARCH || process.arch * @param {string} platformArch - Platform-arch identifier. * @returns {{ buildDir: string, libpqBuildDir: string }} */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export function getBuildDirs(platformArch) { const buildDir = getPlatformBuildDir(packageRoot, platformArch) const libpqBuildDir = path.join(buildDir, 'out', BUILD_STAGES.FINAL, 'libpq') @@ -119,6 +118,7 @@ export async function verifyArchiveChecksum(archivePath, assetName) { * @param {string} [options.platformArch] - Override platform-arch. * @returns {Promise} Path to downloaded libpq directory. */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export async function downloadLibpq(options = {}) { const { force = false, platformArch } = options const resolvedPlatformArch = platformArch ?? (await getCurrentPlatformArch()) @@ -170,6 +170,7 @@ export async function downloadLibpq(options = {}) { } // Verify tarball integrity before extraction. + // oxlint-disable-next-line socket/prefer-exists-sync -- multiple fs.stat() calls consume stats.size for downloaded-archive / built-library size reporting and minimum-size quick checks. const archiveStats = await fs.stat(downloadedArchive) logger.info( `Archive size: ${(archiveStats.size / 1024 / 1024).toFixed(2)} MB`, @@ -256,6 +257,7 @@ export async function downloadLibpq(options = {}) { // Write version file after cleanup. await fs.writeFile(versionFile, POSTGRES_VERSION, 'utf8') + // oxlint-disable-next-line socket/prefer-exists-sync -- multiple fs.stat() calls consume stats.size for downloaded-archive / built-library size reporting and minimum-size quick checks. const stats = await fs.stat(path.join(extractDir, 'libpq.a')) const sizeMB = (stats.size / 1024 / 1024).toFixed(2) logger.success(`Downloaded libpq (${sizeMB} MB) to ${extractDir}`) @@ -272,6 +274,7 @@ export async function downloadLibpq(options = {}) { * @param {string} [options.platformArch] - Override platform-arch. * @returns {Promise} Path to directory containing libpq libraries. */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export async function ensureLibpq(options = {}) { const { force = false, platformArch } = options const resolvedPlatformArch = platformArch ?? (await getCurrentPlatformArch()) @@ -309,6 +312,7 @@ const POSTGRES_VERSION = getPostgresVersion() * Extract PostgreSQL version from .gitmodules comment. * @returns {string} PostgreSQL version (e.g., "16.6") */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export function getPostgresVersion() { try { const version = getSubmoduleVersion( @@ -323,6 +327,7 @@ export function getPostgresVersion() { } } +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export async function runCommand(command, args, cwd, env = {}) { logger.info(`Running: ${command} ${args.join(' ')}`) @@ -362,6 +367,7 @@ export async function runCommand(command, args, cwd, env = {}) { * * @returns {{ includeDir: string, libDir: string }} OpenSSL paths */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export function getNodeOpenSSLPaths() { // Node.js OpenSSL is in node-smol-builder's upstream const nodeUpstream = path.join( @@ -394,6 +400,7 @@ export function getNodeOpenSSLPaths() { * tree doesn't carry built libs yet. Returns undefined when nothing * works so configure can auto-probe. */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export function getOpenSSLPaths() { const candidates = [] candidates.push(getNodeOpenSSLPaths()) @@ -442,6 +449,7 @@ export function getOpenSSLPaths() { * * @param {string} libpqBuildDir - Directory to build libpq in. */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export async function buildLibpq(libpqBuildDir) { logger.info('Building libpq from PostgreSQL source...') @@ -584,6 +592,7 @@ export async function buildLibpq(libpqBuildDir) { * * @param {string} libpqBuildDir - Directory containing libpq build. */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export async function copyDistributionFiles(libpqBuildDir) { const distDir = path.join(libpqBuildDir, 'dist') await safeMkdir(distDir) @@ -751,6 +760,7 @@ async function main() { // Should not reach here after auto-init above. const libpqDir = await ensureLibpq() const libpqLib = path.join(libpqDir, 'libpq.a') + // oxlint-disable-next-line socket/prefer-exists-sync -- multiple fs.stat() calls consume stats.size for downloaded-archive / built-library size reporting and minimum-size quick checks. const stats = await fs.stat(libpqLib) const sizeMB = (stats.size / 1024 / 1024).toFixed(2) @@ -760,6 +770,7 @@ async function main() { CHECKPOINTS.FINALIZED, async () => { // Verify library exists and has reasonable size. + // oxlint-disable-next-line socket/prefer-exists-sync -- multiple fs.stat() calls consume stats.size for downloaded-archive / built-library size reporting and minimum-size quick checks. const libStats = await fs.stat(libpqLib) if (libStats.size < 10_000) { throw new Error( @@ -801,6 +812,7 @@ async function main() { throw new Error(`libpq library not found at ${libPath}`) } + // oxlint-disable-next-line socket/prefer-exists-sync -- multiple fs.stat() calls consume stats.size for downloaded-archive / built-library size reporting and minimum-size quick checks. const stats = await fs.stat(libPath) const sizeMB = (stats.size / 1024 / 1024).toFixed(2) logger.info(`libpq library size: ${sizeMB} MB`) @@ -811,6 +823,7 @@ async function main() { CHECKPOINTS.FINALIZED, async () => { // Verify library exists and has reasonable size. + // oxlint-disable-next-line socket/prefer-exists-sync -- multiple fs.stat() calls consume stats.size for downloaded-archive / built-library size reporting and minimum-size quick checks. const libStats = await fs.stat(libPath) if (libStats.size < 10_000) { throw new Error( diff --git a/packages/libpq-builder/vitest.config.mts b/packages/libpq-builder/vitest.config.mts index 5b054bed1..0feeda1be 100644 --- a/packages/libpq-builder/vitest.config.mts +++ b/packages/libpq-builder/vitest.config.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/no-default-export -- vitest CLI auto-discovers config via default import. */ /** * Extends shared vitest config. */ @@ -6,6 +5,7 @@ import { defineConfig, mergeConfig } from 'vitest/config' import baseConfig from '../../.config/vitest.config.mts' +// oxlint-disable-next-line socket/no-default-export -- vitest CLI auto-discovers config via default import. export default mergeConfig( baseConfig, defineConfig({ diff --git a/packages/lief-builder/scripts/build.mts b/packages/lief-builder/scripts/build.mts index 616db5815..9ff18dc1e 100755 --- a/packages/lief-builder/scripts/build.mts +++ b/packages/lief-builder/scripts/build.mts @@ -1,6 +1,4 @@ // max-file-lines: legitimate -- single builder pipeline (fetch → patch → build → package) — splitting fractures the build sequence -/* oxlint-disable socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. */ -/* oxlint-disable socket/prefer-exists-sync -- multiple fs.stat() calls consume stats.size for downloaded-archive / built-library size reporting and minimum-size quick checks. */ /** * Build script for LIEF library. * Downloads prebuilt LIEF from GitHub releases or builds from source. @@ -75,6 +73,7 @@ export function getLiefBuildDirs(platformArch) { * * @returns {string} Platform-arch identifier. */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export function getCurrentLiefPlatformArch() { const libc = detectLibc() // Respect TARGET_ARCH for cross-compilation (set by workflows/Makefiles) @@ -90,6 +89,7 @@ export function getCurrentLiefPlatformArch() { * @param {string} platformArch - Platform-arch identifier. * @returns {string} Path to downloaded LIEF directory. */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export function getDownloadedLiefDir(platformArch) { return path.join(getDownloadedDir(packageRoot), 'lief', platformArch) } @@ -116,6 +116,7 @@ export async function verifyArchiveChecksum(archivePath, assetName) { * * @returns {Array} Array of required files. Arrays indicate alternatives (any one must exist). */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export function getLiefRequiredFiles() { return LIEF_REQUIRED_FILES } @@ -150,6 +151,7 @@ export function verifyLiefAt(dir) { * @param {string} dir - Directory to check. * @returns {boolean} True if complete LIEF installation exists. */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export function liefExistsAt(dir) { return verifyLiefAt(dir).valid } @@ -160,6 +162,7 @@ export function liefExistsAt(dir) { * @param {string} dir - Directory to check. * @returns {string|undefined} Path to LIEF library if exists, undefined otherwise. */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export function getLiefLibPathAt(dir) { const unixPath = path.join(dir, 'libLIEF.a') const msvcPath = path.join(dir, 'LIEF.lib') @@ -179,6 +182,7 @@ export function getLiefLibPathAt(dir) { * @param {string} [platformArch] - Platform-arch identifier. Defaults to current platform. * @returns {string|undefined} Path to LIEF library if exists, undefined otherwise. */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export function getLiefLibPath(platformArch) { const resolvedPlatformArch = platformArch ?? getCurrentLiefPlatformArch() const { liefBuildDir } = getLiefBuildDirs(resolvedPlatformArch) @@ -191,6 +195,7 @@ export function getLiefLibPath(platformArch) { * @param {string} [platformArch] - Platform-arch identifier. Defaults to current platform. * @returns {boolean} True if LIEF library exists. */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export function liefExists(platformArch) { return getLiefLibPath(platformArch) !== undefined } @@ -200,6 +205,7 @@ export function liefExists(platformArch) { * The version is specified in the comment above the LIEF submodule entry. * @returns {string} LIEF version (e.g., "0.17.0") */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export function getLiefVersion() { const version = getSubmoduleVersion( 'packages/lief-builder/upstream/lief', @@ -212,6 +218,7 @@ export function getLiefVersion() { // LIEF version (extracted from .gitmodules comment). const LIEF_VERSION = getLiefVersion() +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export async function runCommand(command, args, cwd, env = {}) { logger.info(`Running: ${command} ${args.join(' ')}`) @@ -331,6 +338,7 @@ export async function verifyMuslCompatibility(libPath) { * This allows patching without modifying the git submodule. * @param {string} sourceDir - Destination directory for copied source */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export async function copyLiefSource(sourceDir) { if (!existsSync(liefUpstream)) { throw new Error( @@ -435,6 +443,7 @@ export async function copyLiefSource(sourceDir) { * Uses `patch -p1` command (doesn't require git). * @param {string} sourceDir - Path to LIEF source directory */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export async function applyLiefPatches(sourceDir) { const patchesDir = path.join(packageRoot, 'patches', 'lief') @@ -498,6 +507,7 @@ export async function applyLiefPatches(sourceDir) { * @param {string} [options.platformArch] - Override platform-arch. * @returns {Promise} Path to downloaded LIEF directory, or null on failure. */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export async function downloadPrebuiltLIEF(options = {}) { // Check if download is blocked by BUILD_DEPS_FROM_SOURCE environment flag. checkBuildSourceFlag('LIEF', 'DEPS', { @@ -542,6 +552,7 @@ export async function downloadPrebuiltLIEF(options = {}) { // Verify tarball integrity before extraction (detect corrupted/truncated downloads). // This catches issues where a previous download was cached but is corrupt. + // oxlint-disable-next-line socket/prefer-exists-sync -- multiple fs.stat() calls consume stats.size for downloaded-archive / built-library size reporting and minimum-size quick checks. const archiveStats = await fs.stat(downloadedArchive) logger.info( `Archive size: ${(archiveStats.size / 1024 / 1024).toFixed(2)} MB`, @@ -719,6 +730,7 @@ const ensureLiefLocks = new Map() * @param {string} [options.platformArch] - Override platform-arch for downloads. * @returns {Promise} Path to LIEF library. */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export async function ensureLief(options = {}) { const resolvedPlatformArch = options.platformArch ?? getCurrentLiefPlatformArch() @@ -747,6 +759,7 @@ export async function ensureLief(options = {}) { /** * Internal implementation of ensureLief. */ +// oxlint-disable-next-line socket/sort-source-methods -- build script is ordered as a top-down pipeline (download → extract → configure → build → install → smoke test); alphabetizing across pipeline phases would scatter the flow and break the checkpoint reading order. export async function ensureLiefImpl(options = {}) { const { force = false, platformArch } = options const resolvedPlatformArch = platformArch ?? getCurrentLiefPlatformArch() @@ -834,6 +847,7 @@ async function main() { const liefLibPathNew = getLiefLibPathAt(downloadDir) if (liefLibPathNew) { + // oxlint-disable-next-line socket/prefer-exists-sync -- multiple fs.stat() calls consume stats.size for downloaded-archive / built-library size reporting and minimum-size quick checks. const stats = await fs.stat(liefLibPathNew) const sizeMB = (stats.size / 1024 / 1024).toFixed(2) @@ -846,6 +860,7 @@ async function main() { CHECKPOINTS.LIEF_BUILT, async () => { // Verify library exists and has reasonable size. + // oxlint-disable-next-line socket/prefer-exists-sync -- multiple fs.stat() calls consume stats.size for downloaded-archive / built-library size reporting and minimum-size quick checks. const libStats = await fs.stat(liefLibPathNew) if (libStats.size < 1_000_000) { throw new Error( @@ -1093,6 +1108,7 @@ async function main() { } } + // oxlint-disable-next-line socket/prefer-exists-sync -- multiple fs.stat() calls consume stats.size for downloaded-archive / built-library size reporting and minimum-size quick checks. const stats = await fs.stat(libPath) const sizeMB = (stats.size / 1024 / 1024).toFixed(2) logger.info(`LIEF library size: ${sizeMB} MB`) @@ -1178,6 +1194,7 @@ async function main() { CHECKPOINTS.LIEF_BUILT, async () => { // Verify library exists and has reasonable size. + // oxlint-disable-next-line socket/prefer-exists-sync -- multiple fs.stat() calls consume stats.size for downloaded-archive / built-library size reporting and minimum-size quick checks. const libStats = await fs.stat(libPath) if (libStats.size < 1_000_000) { throw new Error( diff --git a/packages/lief-builder/vitest.config.mts b/packages/lief-builder/vitest.config.mts index 61c779dcf..bd8ef5a49 100644 --- a/packages/lief-builder/vitest.config.mts +++ b/packages/lief-builder/vitest.config.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/no-default-export -- vitest CLI auto-discovers config via default import. */ /** * Extends shared vitest config. */ @@ -6,6 +5,7 @@ import { defineConfig, mergeConfig } from 'vitest/config' import baseConfig from '../../.config/vitest.config.mts' +// oxlint-disable-next-line socket/no-default-export -- vitest CLI auto-discovers config via default import. export default mergeConfig( baseConfig, defineConfig({ diff --git a/packages/minilm-builder/test/build-output.test.mts b/packages/minilm-builder/test/build-output.test.mts index bd49fc749..a65dbcf6e 100644 --- a/packages/minilm-builder/test/build-output.test.mts +++ b/packages/minilm-builder/test/build-output.test.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/prefer-exists-sync -- every fs.stat() reads stats.size to assert int4/int8 quantized model artifacts are in the expected size range. */ /** * @fileoverview Tests for minilm-builder model output files. * Validates that the build process generates correct model structure and formats. @@ -64,6 +63,7 @@ describe.skipIf(!hasBuiltArtifacts)('minilm-builder model output', () => { return } + // oxlint-disable-next-line socket/prefer-exists-sync -- every fs.stat() reads stats.size to assert int4/int8 quantized model artifacts are in the expected size range. const stats = await fs.stat(int8ModelPath) expect(stats.size).toBeGreaterThan(5 * 1024 * 1024) // INT8 quantized MiniLM models are typically 10-25MB @@ -137,7 +137,9 @@ describe.skipIf(!hasBuiltArtifacts)('minilm-builder model output', () => { return } + // oxlint-disable-next-line socket/prefer-exists-sync -- every fs.stat() reads stats.size to assert int4/int8 quantized model artifacts are in the expected size range. const int4Stats = await fs.stat(int4ModelPath) + // oxlint-disable-next-line socket/prefer-exists-sync -- every fs.stat() reads stats.size to assert int4/int8 quantized model artifacts are in the expected size range. const int8Stats = await fs.stat(int8ModelPath) expect(int4Stats.size).toBeLessThan(int8Stats.size) @@ -166,7 +168,7 @@ describe.skipIf(!hasBuiltArtifacts)('minilm-builder model output', () => { } const buffer = await fs.readFile(modelPath) - const _str = buffer.toString('utf8', 0, 1000) + const str = buffer.toString('utf8', 0, 1000) // ONNX models contain IR version info early in the file expect(buffer.length).toBeGreaterThan(1000) // Should contain model structure markers diff --git a/packages/minilm-builder/vitest.config.mts b/packages/minilm-builder/vitest.config.mts index 0d74448c1..b87c54356 100644 --- a/packages/minilm-builder/vitest.config.mts +++ b/packages/minilm-builder/vitest.config.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/no-default-export -- vitest CLI auto-discovers config via default import. */ /** * Extends shared vitest config. * Uses default 2-minute timeout from base config (sufficient for model builder tests). @@ -7,6 +6,7 @@ import { defineConfig, mergeConfig } from 'vitest/config' import baseConfig from '../../.config/vitest.config.mts' +// oxlint-disable-next-line socket/no-default-export -- vitest CLI auto-discovers config via default import. export default mergeConfig( baseConfig, defineConfig({ diff --git a/packages/models/scripts/build.mts b/packages/models/scripts/build.mts index cfbd93146..4797f50ab 100644 --- a/packages/models/scripts/build.mts +++ b/packages/models/scripts/build.mts @@ -1,5 +1,4 @@ #!/usr/bin/env node -/* oxlint-disable socket/prefer-exists-sync -- fs.stat() calls consume stats.size to verify minilm/codet5 model artifacts are in expected size range during the build smoke test. */ /** * Build script for @socketsecurity/models. * @@ -260,6 +259,7 @@ async function main() { CHECKPOINTS.DOWNLOADED, async () => { // Smoke test: Verify models directory exists and has models + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size to verify minilm/codet5 model artifacts are in expected size range during the build smoke test. const stats = await fs.stat(MODELS) if (!stats.isDirectory()) { throw new Error(`Models directory not found: ${MODELS}`) @@ -380,6 +380,7 @@ async function main() { if (!existsSync(minilmTokenizer)) { throw new Error(`MiniLM tokenizer not found: ${minilmTokenizer}`) } + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size to verify minilm/codet5 model artifacts are in expected size range during the build smoke test. const stats = await fs.stat(minilmModel) if (stats.size < minSize) { throw new Error( @@ -400,6 +401,7 @@ async function main() { if (!existsSync(codet5Tokenizer)) { throw new Error(`CodeT5 tokenizer not found: ${codet5Tokenizer}`) } + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size to verify minilm/codet5 model artifacts are in expected size range during the build smoke test. const stats = await fs.stat(codet5Model) if (stats.size < minSize) { throw new Error( diff --git a/packages/models/scripts/converted/shared/convert-to-onnx.mts b/packages/models/scripts/converted/shared/convert-to-onnx.mts index 7c385e732..442ad69e3 100644 --- a/packages/models/scripts/converted/shared/convert-to-onnx.mts +++ b/packages/models/scripts/converted/shared/convert-to-onnx.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/prefer-exists-sync -- fs.stat() calls consume stats.size to detect empty-output failure and report the converted ONNX file size. */ import { existsSync, promises as fs } from 'node:fs' import path from 'node:path' import process from 'node:process' @@ -72,6 +71,7 @@ export async function convertToOnnx(options) { for (let i = 0, { length } = expectedFiles; i < length; i += 1) { const fileName = expectedFiles[i] const filePath = path.join(modelPath, fileName) + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size to detect empty-output failure and report the converted ONNX file size. const stats = await fs.stat(filePath) if (stats.size === 0) { throw new Error(`ONNX file is empty: ${fileName}`) @@ -186,6 +186,7 @@ print(f"Successfully exported model to {output_path}") // Smoke test: Verify converted ONNX model exists and is valid const modelPath = path.join(modelsDir, modelKey) const onnxFile = path.join(modelPath, 'model.onnx') + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size to detect empty-output failure and report the converted ONNX file size. const stats = await fs.stat(onnxFile) if (stats.size === 0) { throw new Error('Converted ONNX file is empty') diff --git a/packages/models/scripts/downloaded/shared/download-model.mts b/packages/models/scripts/downloaded/shared/download-model.mts index e4402b058..2eb2dfea6 100644 --- a/packages/models/scripts/downloaded/shared/download-model.mts +++ b/packages/models/scripts/downloaded/shared/download-model.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/prefer-exists-sync -- fs.stat() calls consume stats.size to verify downloaded model size after HuggingFace fetch. */ import { promises as fs } from 'node:fs' import path from 'node:path' import process from 'node:process' @@ -95,6 +94,7 @@ export async function downloadModel(options) { async () => { // Smoke test: Verify model directory and files exist const modelPath = path.join(modelsDir, modelKey) + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size to verify downloaded model size after HuggingFace fetch. const stats = await fs.stat(modelPath) if (!stats.isDirectory()) { throw new Error(`Model path is not a directory: ${modelPath}`) @@ -154,6 +154,7 @@ export async function downloadModel(options) { async () => { // Smoke test: Verify model directory and files exist const modelPath = path.join(modelsDir, modelKey) + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size to verify downloaded model size after HuggingFace fetch. const stats = await fs.stat(modelPath) if (!stats.isDirectory()) { throw new Error(`Model path is not a directory: ${modelPath}`, { diff --git a/packages/models/test/build-output.test.mts b/packages/models/test/build-output.test.mts index 552d230e7..650010589 100644 --- a/packages/models/test/build-output.test.mts +++ b/packages/models/test/build-output.test.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/prefer-exists-sync -- every fs.stat() reads stats.size to assert quantized model artifacts are in the expected size range. */ /** * @fileoverview Tests for models build output files. * Validates that the build process generates correct model structure and formats. @@ -55,6 +54,7 @@ describe.skipIf(!hasBuiltArtifacts)('models build output', () => { return } + // oxlint-disable-next-line socket/prefer-exists-sync -- every fs.stat() reads stats.size to assert quantized model artifacts are in the expected size range. const stats = await fs.stat(modelPath) // MiniLM-L6 quantized models are typically 10-30MB // > 1MB (minimum threshold for both int4 and int8) @@ -82,6 +82,7 @@ describe.skipIf(!hasBuiltArtifacts)('models build output', () => { return } + // oxlint-disable-next-line socket/prefer-exists-sync -- every fs.stat() reads stats.size to assert quantized model artifacts are in the expected size range. const stats = await fs.stat(modelPath) // CodeT5 quantized models are typically larger than MiniLM // > 10MB @@ -195,8 +196,10 @@ describe.skipIf(!hasBuiltArtifacts)('models build output', () => { } // eslint-disable-next-line no-await-in-loop + // oxlint-disable-next-line socket/prefer-exists-sync -- every fs.stat() reads stats.size to assert quantized model artifacts are in the expected size range. const int8Stats = await fs.stat(int8Path) // eslint-disable-next-line no-await-in-loop + // oxlint-disable-next-line socket/prefer-exists-sync -- every fs.stat() reads stats.size to assert quantized model artifacts are in the expected size range. const int4Stats = await fs.stat(int4Path) const reduction = 1 - int4Stats.size / int8Stats.size diff --git a/packages/models/vitest.config.mts b/packages/models/vitest.config.mts index 9de7f2be1..ac6dc2015 100644 --- a/packages/models/vitest.config.mts +++ b/packages/models/vitest.config.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/no-default-export -- vitest CLI auto-discovers config via default import. */ /** * Extends shared vitest config. * Excludes build directory (contains large ONNX model files). @@ -7,6 +6,7 @@ import { defineConfig, mergeConfig } from 'vitest/config' import baseConfig from '../../.config/vitest.config.mts' +// oxlint-disable-next-line socket/no-default-export -- vitest CLI auto-discovers config via default import. export default mergeConfig( baseConfig, defineConfig({ diff --git a/packages/napi-go-infra/vitest.config.mts b/packages/napi-go-infra/vitest.config.mts index 069c5a11c..282db1581 100644 --- a/packages/napi-go-infra/vitest.config.mts +++ b/packages/napi-go-infra/vitest.config.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/no-default-export -- vitest CLI auto-discovers config via default import. */ /** * Extends shared vitest config — Go-backed N-API framework tests use * the 30s timeout override because Go binaries load faster than the @@ -8,6 +7,7 @@ import { defineConfig, mergeConfig } from 'vitest/config' import baseConfig from '../../.config/vitest.config.mts' +// oxlint-disable-next-line socket/no-default-export -- vitest CLI auto-discovers config via default import. export default mergeConfig( baseConfig, defineConfig({ diff --git a/packages/node-smol-builder/additions/source-patched/lib/internal/socketsecurity/util.js b/packages/node-smol-builder/additions/source-patched/lib/internal/socketsecurity/util.js index d2e4245f2..51af1e772 100644 --- a/packages/node-smol-builder/additions/source-patched/lib/internal/socketsecurity/util.js +++ b/packages/node-smol-builder/additions/source-patched/lib/internal/socketsecurity/util.js @@ -4,8 +4,16 @@ const { ObjectFreeze } = primordials -const { applyBind, applySafe, bindCall, uncurryThis, weakRefSafe } = - internalBinding('smol_util') +const { + applyBind, + applySafe, + bindCall, + decodeHtml, + encodeHtml, + stripAnsi, + uncurryThis, + weakRefSafe, +} = internalBinding('smol_util') // Re-export frozen + null-prototype to match the shape of every other // internal/socketsecurity/* barrel. @@ -14,6 +22,9 @@ module.exports = ObjectFreeze({ applyBind, applySafe, bindCall, + decodeHtml, + encodeHtml, + stripAnsi, uncurryThis, weakRefSafe, }) diff --git a/packages/node-smol-builder/additions/source-patched/lib/smol-keymap.js b/packages/node-smol-builder/additions/source-patched/lib/smol-keymap.js new file mode 100644 index 000000000..d9ffe19cf --- /dev/null +++ b/packages/node-smol-builder/additions/source-patched/lib/smol-keymap.js @@ -0,0 +1,83 @@ +'use strict' + +// node:smol-keymap — keymap matcher backed by a native chord state +// machine. Replaces the @opentui/keymap matcher hot path (~5 ns per +// keystroke vs ~50-100 ns in TS). Layers, extension contexts, command +// catalogs etc. stay in userland TS. +// +// Surface: +// +// createKeymap(rulesJson) -> handle | 0 +// Parses a JSON rules object: +// { "ctrl+a": "select-all", +// "ctrl+x ctrl+s": "save", +// "esc": "cancel" } +// Returns 0 on parse failure. Modifier names are case-insensitive +// and accept common aliases: ctrl/control, shift, alt/option/opt, +// meta/cmd/command/super/win. Modifier order doesn't matter +// (`shift+ctrl+a` === `ctrl+shift+a`). +// +// destroyKeymap(handle): release. +// +// matchKey(handle, keyName, modifierBits) -> command string | null +// modifierBits: bit 0 ctrl, bit 1 shift, bit 2 alt, bit 3 meta. +// Returns the bound command on a complete match. Returns null +// when no binding matches OR when a multi-step chord is mid-way +// (call again with the next keystroke to advance). Use +// `getModifierBits` to compute the bits portably from a JS key +// event. +// +// resetChord(handle): clear pending chord state (e.g. after a +// timeout). Useful for emacs-style chord timeouts in JS. +// +// getModifierBits({ ctrl, shift, alt, meta }) -> integer +// Convenience helper. Builds the bit-pack from boolean modifier +// flags as they typically appear in key-event objects. + +const { ObjectFreeze } = primordials + +const { createKeymap, destroyKeymap, matchKey, resetChord } = + internalBinding('smol_keymap') + +const MOD_CTRL = 1 << 0 +const MOD_SHIFT = 1 << 1 +const MOD_ALT = 1 << 2 +const MOD_META = 1 << 3 + +function getModifierBits(mods) { + if (!mods) { + return 0 + } + let bits = 0 + if (mods.ctrl) { + bits |= MOD_CTRL + } + if (mods.shift) { + bits |= MOD_SHIFT + } + if (mods.alt) { + bits |= MOD_ALT + } + if (mods.meta) { + bits |= MOD_META + } + return bits +} + +const modifier = ObjectFreeze({ + __proto__: null, + CTRL: MOD_CTRL, + SHIFT: MOD_SHIFT, + ALT: MOD_ALT, + META: MOD_META, +}) + +module.exports = ObjectFreeze({ + __proto__: null, + createKeymap, + destroyKeymap, + getModifierBits, + matchKey, + modifier, + resetChord, +}) diff --git a/packages/node-smol-builder/additions/source-patched/lib/smol-markdown.js b/packages/node-smol-builder/additions/source-patched/lib/smol-markdown.js new file mode 100644 index 000000000..8f9d05978 --- /dev/null +++ b/packages/node-smol-builder/additions/source-patched/lib/smol-markdown.js @@ -0,0 +1,267 @@ +'use strict' + +// node:smol-markdown — CommonMark + GFM Markdown parser backed by md4c +// (https://github.com/mity/md4c). Replaces userland `marked` / +// `remark` / `markdown-it` on the AI-output rendering hot path. +// +// Surface: +// +// parseMarkdown(text, flags?) -> Array<[code, payload]> +// Returns a flat event stream. Each event is a 2-tuple: +// [0]: numeric code combining a category nibble (high 4 bits) +// and an md4c enum value (low 12 bits). +// [1]: undefined | string (text content) | number (heading level) +// +// Categories: +// 0: block enter (BLOCKTYPE in low bits) +// 1: block leave (BLOCKTYPE in low bits) +// 2: span enter (SPANTYPE in low bits) +// 3: span leave (SPANTYPE in low bits) +// 4: text (TEXTTYPE in low bits) +// +// BLOCKTYPE / SPANTYPE / TEXTTYPE numeric values are also exposed +// as frozen enum objects on this module's exports so callers can +// dispatch by name instead of magic numbers. +// +// blockType / spanType / textType: frozen objects mapping enum +// names to numeric values (1:1 mirror of md4c.h). +// +// eventCategory: frozen object with the five high-nibble codes. +// +// parseTree(text, flags?) -> { type: 'doc', children: [...] } +// Convenience wrapper around parseMarkdown that reconstructs a +// nested tree from the flat event stream. Builds a JS object +// graph; use parseMarkdown directly for the hot path. +// +// flags is a comma-separated subset of md4c flag names (case +// insensitive). Supported tokens: +// +// collapse_whitespace, permissive_atx_headers, +// permissive_url_autolinks, permissive_email_autolinks, +// permissive_www_autolinks, no_indented_code_blocks, +// no_html_blocks, no_html_spans, tables, strikethrough, +// tasklists, latex_math_spans, wikilinks, underline, +// hard_soft_breaks, commonmark, github +// +// `commonmark` is the empty set (no extensions). `github` is the +// MD_DIALECT_GITHUB aggregate (tables + strikethrough + tasklists + +// permissive_*_autolinks). + +const { + ArrayPrototypePush, + DataView: DataViewCtor, + DataViewPrototypeGetInt32, + DataViewPrototypeGetUint32, + NumberPrototypeToString, + ObjectFreeze, + TypeError: TypeErrorCtor, + Uint8Array: Uint8ArrayCtor, + Uint8ArrayPrototypeSubarray, +} = primordials + +const { parseMarkdown, parseMarkdownStream } = + internalBinding('smol_markdown') + +// Mirror md4c.h enums. Numeric values are stable across md4c releases +// (per their semver policy); we re-export them for callers that want +// to dispatch by name. Keep in sync with src/md4c.h when bumping md4c. +const blockType = ObjectFreeze({ + __proto__: null, + DOC: 0, + QUOTE: 1, + UL: 2, + OL: 3, + LI: 4, + HR: 5, + H: 6, + CODE: 7, + HTML: 8, + P: 9, + TABLE: 10, + THEAD: 11, + TBODY: 12, + TR: 13, + TH: 14, + TD: 15, +}) + +const spanType = ObjectFreeze({ + __proto__: null, + EM: 0, + STRONG: 1, + A: 2, + IMG: 3, + CODE: 4, + DEL: 5, + LATEXMATH: 6, + LATEXMATH_DISPLAY: 7, + WIKILINK: 8, + U: 9, +}) + +const textType = ObjectFreeze({ + __proto__: null, + NORMAL: 0, + NULLCHAR: 1, + ENTITY: 2, + CODE: 3, + HTML: 4, + LATEXMATH: 5, +}) + +const eventCategory = ObjectFreeze({ + __proto__: null, + BLOCK_ENTER: 0 << 12, + BLOCK_LEAVE: 1 << 12, + SPAN_ENTER: 2 << 12, + SPAN_LEAVE: 3 << 12, + TEXT: 4 << 12, +}) + +const CATEGORY_MASK = 0xf000 +const VALUE_MASK = 0x0fff + +// parseMarkdownStream layout constants. Must stay in sync with the +// native binding's ParseMarkdownStream (markdown_binding.cc). +const STREAM_MAGIC = 0x534d4456 // "SMDV" +const STREAM_HEADER_SIZE = 12 +const STREAM_EVENT_SIZE = 16 + +// decodeStream(buf) -> { code: Uint32Array, textOffsets: Uint32Array, +// textLens: Uint32Array, headingLevels: Int32Array, +// textPool: Uint8Array } +// +// Slices the native ArrayBuffer into typed-array views — no copies. +// Consumers iterate the parallel arrays; the textPool is a single +// Uint8Array view sliced via subarray() for each event's payload. +function decodeStream(arrayBuffer) { + if ( + !arrayBuffer || + typeof arrayBuffer.byteLength !== 'number' || + arrayBuffer.byteLength < STREAM_HEADER_SIZE + ) { + throw new TypeErrorCtor( + 'parseMarkdownStream output too small to be a valid event stream', + ) + } + const header = new DataViewCtor(arrayBuffer, 0, STREAM_HEADER_SIZE) + const magic = DataViewPrototypeGetUint32(header, 0, true) + if (magic !== STREAM_MAGIC) { + throw new TypeErrorCtor( + `parseMarkdownStream output has wrong magic ${NumberPrototypeToString(magic, 16)}; expected SMDV`, + ) + } + const eventCount = DataViewPrototypeGetUint32(header, 4, true) + const textPoolSize = DataViewPrototypeGetUint32(header, 8, true) + const recordsByteSize = eventCount * STREAM_EVENT_SIZE + // Typed-array views into the shared ArrayBuffer — zero-copy. + const records = new DataViewCtor( + arrayBuffer, + STREAM_HEADER_SIZE, + recordsByteSize, + ) + const textPool = new Uint8ArrayCtor( + arrayBuffer, + STREAM_HEADER_SIZE + recordsByteSize, + textPoolSize, + ) + return { + __proto__: null, + eventCount, + records, + textPool, + } +} + +// streamForEach(buf, fn): iterates the decoded stream, calling +// `fn(code, payload)` for each event. Text payloads are decoded +// only when present (TextDecoder cost paid per text event, not +// per total event). Callers that don't need text strings can use +// decodeStream() directly and read the records/textPool typed +// arrays. +// TextDecoder is a WHATWG global, not part of Node's `primordials` — +// safe to capture the constructor at module load and the prototype +// method via uncurry to avoid prototype-mutation risk on the hot path. +const sharedDecoder = new TextDecoder('utf-8') +const sharedDecode = TextDecoder.prototype.decode +function streamForEach(arrayBuffer, fn) { + const { eventCount, records, textPool } = decodeStream(arrayBuffer) + for (let i = 0, byteOff = 0; i < eventCount; i += 1, byteOff += STREAM_EVENT_SIZE) { + const code = DataViewPrototypeGetUint32(records, byteOff, true) + const textOffset = DataViewPrototypeGetUint32(records, byteOff + 4, true) + const textLen = DataViewPrototypeGetUint32(records, byteOff + 8, true) + const headingLevel = DataViewPrototypeGetInt32(records, byteOff + 12, true) + let payload + if (textLen !== 0) { + // textOffset is relative to textPool's start; subarray is a + // zero-copy view backed by the same ArrayBuffer. + payload = sharedDecode.call( + sharedDecoder, + Uint8ArrayPrototypeSubarray(textPool, textOffset, textOffset + textLen), + ) + } else if (headingLevel !== 0) { + payload = headingLevel + } else { + payload = undefined + } + fn(code, payload) + } +} + +function parseTree(text, flags) { + const events = parseMarkdown(text, flags || '') + const root = { __proto__: null, type: 'doc', children: [] } + const stack = [root] + for (let i = 0, { length } = events; i < length; i += 1) { + const [code, payload] = events[i] + const cat = code & CATEGORY_MASK + const val = code & VALUE_MASK + if (cat === eventCategory.BLOCK_ENTER) { + const node = { + __proto__: null, + kind: 'block', + type: val, + children: [], + } + if (val === blockType.H && typeof payload === 'number') { + node.level = payload + } + ArrayPrototypePush(stack[stack.length - 1].children, node) + stack.push(node) + } else if (cat === eventCategory.BLOCK_LEAVE) { + stack.pop() + } else if (cat === eventCategory.SPAN_ENTER) { + const node = { + __proto__: null, + kind: 'span', + type: val, + children: [], + } + ArrayPrototypePush(stack[stack.length - 1].children, node) + stack.push(node) + } else if (cat === eventCategory.SPAN_LEAVE) { + stack.pop() + } else if (cat === eventCategory.TEXT) { + ArrayPrototypePush(stack[stack.length - 1].children, { + __proto__: null, + kind: 'text', + type: val, + text: payload || '', + }) + } + } + return root +} + +module.exports = ObjectFreeze({ + __proto__: null, + blockType, + decodeStream, + eventCategory, + parseMarkdown, + parseMarkdownStream, + parseTree, + spanType, + streamForEach, + textType, +}) diff --git a/packages/node-smol-builder/additions/source-patched/lib/smol-qrcode.js b/packages/node-smol-builder/additions/source-patched/lib/smol-qrcode.js new file mode 100644 index 000000000..5ebe06612 --- /dev/null +++ b/packages/node-smol-builder/additions/source-patched/lib/smol-qrcode.js @@ -0,0 +1,50 @@ +'use strict' + +// node:smol-qrcode — QR code encoder backed by libqrencode v4.1.1 +// (vendored at upstream/libqrencode). Replaces the userland `qrcode` +// npm package on the AI-output rendering path (QR codes for sharing +// URLs, payment intents, etc.). +// +// Surface: +// +// encode(text, ecLevel?) -> { width, matrix } +// text: string to encode (UTF-8, 8-bit mode — libqrencode +// auto-detects the right QR version). +// ecLevel: 0=L (~7% recovery), 1=M (~15%, default), 2=Q (~25%), +// 3=H (~30%). +// Returns { width: side-length-in-cells, matrix: Uint8Array of +// width*width bytes }. Each byte's bit 0 indicates whether the +// cell is black (1) or white (0). Mask with `& 1` to ignore +// libqrencode's internal state bits. +// +// Render to terminal (typical usage): +// +// const { encode } = require('node:smol-qrcode') +// const { rendererSet } = require('node:smol-tui') +// const { width, matrix } = encode('https://example.com') +// for (let y = 0; y < width; y++) { +// for (let x = 0; x < width; x++) { +// const black = matrix[y * width + x] & 1 +// rendererSet(renderer, x, y, +// black ? 0x2588 /* ▍ */ : 0x20, +// 255, 255, 255, 0, 0, 0, 0) +// } +// } + +const { ObjectFreeze } = primordials + +const { encode } = internalBinding('smol_qrcode') + +const ecLevel = ObjectFreeze({ + __proto__: null, + L: 0, // ~7% recovery + M: 1, // ~15% recovery (default) + Q: 2, // ~25% recovery + H: 3, // ~30% recovery +}) + +module.exports = ObjectFreeze({ + __proto__: null, + ecLevel, + encode, +}) diff --git a/packages/node-smol-builder/additions/source-patched/lib/smol-tree-sitter.js b/packages/node-smol-builder/additions/source-patched/lib/smol-tree-sitter.js new file mode 100644 index 000000000..309eb6513 --- /dev/null +++ b/packages/node-smol-builder/additions/source-patched/lib/smol-tree-sitter.js @@ -0,0 +1,148 @@ +'use strict' + +// node:smol-tree-sitter — tree-sitter incremental parser library. +// Backed by tree-sitter/tree-sitter v0.26.9 (C, MIT). +// +// Tree-sitter parses source code into a concrete syntax tree. We +// expose just the parts that the syntax-highlighting hot path needs: +// load a compiled grammar from a shared library, parse a source +// string, get back a flat span list. +// +// Surface: +// +// loadLanguage(path, symbol) -> handle | 0 +// dlopens `path` (e.g. `/usr/lib/tree-sitter-javascript.dylib`) +// and resolves `symbol` (e.g. `tree_sitter_javascript`) as the +// language factory. Returns an opaque integer handle. +// +// freeLanguage(handle): release the language. +// +// parse(handle, source) -> Array<[type, start, end, named_child_count]> +// Pre-order traversal of the named nodes in the parse tree. +// `type` is the grammar's node-type name (e.g. `function_declaration`). +// `start` / `end` are byte offsets into source. +// +// Grammars are not bundled — consumers ship the `.dylib` / `.so` / +// `.dll` separately. Build grammars via: +// +// tree-sitter generate +// cc -shared -fPIC src/parser.c -o tree-sitter-.dylib +// +// See https://tree-sitter.github.io/tree-sitter/creating-parsers for +// the grammar authoring guide. + +const { + DataView: DataViewCtor, + DataViewPrototypeGetUint32, + NumberPrototypeToString, + ObjectFreeze, + TypeError: TypeErrorCtor, + Uint8Array: Uint8ArrayCtor, + Uint8ArrayPrototypeSubarray, +} = primordials + +const { freeLanguage, loadLanguage, parse, parseStream } = internalBinding( + 'smol_tree_sitter', +) + +// parseStream(handle, source) -> ArrayBuffer +// +// Zero-copy variant of parse(). Returns a SINGLE ArrayBuffer in this +// binary format (matches markdown's parseMarkdownStream shape): +// +// Header (12 bytes, little-endian): +// uint32 magic = 0x53545356 ("STSV") +// uint32 node_count +// uint32 type_pool_size_bytes +// +// Node records (20 bytes × node_count): +// uint32 type_offset // RELATIVE to type-pool start +// uint32 type_len +// uint32 start_byte +// uint32 end_byte +// uint32 named_child_count +// +// Type pool (type_pool_size_bytes bytes): +// Concatenated UTF-8 type names. Interned per parse — duplicates +// reuse the same pool offset. +// +// Use this for syntax highlighters that iterate ~10k+ nodes per file. +// The Array-of-arrays form (parse()) costs 50-80 ns per node in V8 +// allocation overhead; parseStream is ~1 ns per node (single memcpy). + +const STREAM_MAGIC = 0x53545356 // "STSV" +const STREAM_HEADER_SIZE = 12 +const STREAM_RECORD_SIZE = 20 +// TextDecoder is a WHATWG global, not part of Node's `primordials`; +// capture the constructor + prototype method at module load. +const sharedDecoder = new TextDecoder('utf-8') +const sharedDecode = TextDecoder.prototype.decode + +function decodeStream(arrayBuffer) { + if ( + !arrayBuffer || + typeof arrayBuffer.byteLength !== 'number' || + arrayBuffer.byteLength < STREAM_HEADER_SIZE + ) { + throw new TypeErrorCtor( + 'parseStream output too small to be a valid tree-sitter stream', + ) + } + const header = new DataViewCtor(arrayBuffer, 0, STREAM_HEADER_SIZE) + const magic = DataViewPrototypeGetUint32(header, 0, true) + if (magic !== STREAM_MAGIC) { + throw new TypeErrorCtor( + `parseStream output has wrong magic ${NumberPrototypeToString(magic, 16)}; expected STSV`, + ) + } + const nodeCount = DataViewPrototypeGetUint32(header, 4, true) + const typePoolSize = DataViewPrototypeGetUint32(header, 8, true) + const recordsByteSize = nodeCount * STREAM_RECORD_SIZE + const records = new DataViewCtor( + arrayBuffer, + STREAM_HEADER_SIZE, + recordsByteSize, + ) + const typePool = new Uint8ArrayCtor( + arrayBuffer, + STREAM_HEADER_SIZE + recordsByteSize, + typePoolSize, + ) + return { + __proto__: null, + nodeCount, + records, + typePool, + } +} + +// streamForEach(buf, fn): iterates the decoded stream, calling +// `fn({ type, startByte, endByte, namedChildCount })` for each +// node. Type strings are TextDecoder-decoded lazily; with the type +// pool interning identical types share one decode pass on first +// use (cached by call site, not by us — V8's string-table dedupes). +function streamForEach(arrayBuffer, fn) { + const { nodeCount, records, typePool } = decodeStream(arrayBuffer) + for (let i = 0, byteOff = 0; i < nodeCount; i += 1, byteOff += STREAM_RECORD_SIZE) { + const typeOffset = DataViewPrototypeGetUint32(records, byteOff, true) + const typeLen = DataViewPrototypeGetUint32(records, byteOff + 4, true) + const startByte = DataViewPrototypeGetUint32(records, byteOff + 8, true) + const endByte = DataViewPrototypeGetUint32(records, byteOff + 12, true) + const namedChildCount = DataViewPrototypeGetUint32(records, byteOff + 16, true) + const type = sharedDecode.call( + sharedDecoder, + Uint8ArrayPrototypeSubarray(typePool, typeOffset, typeOffset + typeLen), + ) + fn(type, startByte, endByte, namedChildCount) + } +} + +module.exports = ObjectFreeze({ + __proto__: null, + decodeStream, + freeLanguage, + loadLanguage, + parse, + parseStream, + streamForEach, +}) diff --git a/packages/node-smol-builder/additions/source-patched/lib/smol-tui.js b/packages/node-smol-builder/additions/source-patched/lib/smol-tui.js index b57521a47..c46248044 100644 --- a/packages/node-smol-builder/additions/source-patched/lib/smol-tui.js +++ b/packages/node-smol-builder/additions/source-patched/lib/smol-tui.js @@ -38,6 +38,7 @@ const { ObjectFreeze } = primordials const { align, + codepointWidth, constants, createParser, createRenderer, @@ -53,7 +54,9 @@ const { parseMouseOne, positionType, rendererClear, + rendererDrawBox, rendererDrawText, + rendererDrawTextWrapped, rendererFillRect, rendererFlush, rendererInvalidate, @@ -65,6 +68,8 @@ const { setBgRgb, setFgRgb, sizes, + stringWidth, + stringWidthFromBytes, wrap, writeAttributes, writeBgRgb, @@ -96,6 +101,7 @@ const { module.exports = ObjectFreeze({ __proto__: null, align: ObjectFreeze({ __proto__: null, ...align }), + codepointWidth, constants: ObjectFreeze({ __proto__: null, ...constants }), createParser, createRenderer, @@ -111,7 +117,9 @@ module.exports = ObjectFreeze({ parseMouseOne, positionType: ObjectFreeze({ __proto__: null, ...positionType }), rendererClear, + rendererDrawBox, rendererDrawText, + rendererDrawTextWrapped, rendererFillRect, rendererFlush, rendererInvalidate, @@ -123,6 +131,8 @@ module.exports = ObjectFreeze({ setBgRgb, setFgRgb, sizes: ObjectFreeze({ __proto__: null, ...sizes }), + stringWidth, + stringWidthFromBytes, wrap: ObjectFreeze({ __proto__: null, ...wrap }), writeAttributes, writeBgRgb, diff --git a/packages/node-smol-builder/additions/source-patched/lib/smol-util.js b/packages/node-smol-builder/additions/source-patched/lib/smol-util.js index b4c24f09c..a06e2c478 100644 --- a/packages/node-smol-builder/additions/source-patched/lib/smol-util.js +++ b/packages/node-smol-builder/additions/source-patched/lib/smol-util.js @@ -20,6 +20,24 @@ // returns undefined for inputs that // would throw (non-Object, non-Symbol) // instead of throwing. +// - stripAnsi(s) — native equivalent of the npm +// `strip-ansi` package. Walks the +// input bytes once, emits a copy +// minus OSC (ESC ']' ... ST) and +// CSI (ESC '[' ... final | 0x9B ... +// final) sequences. No regex +// compilation per call. +// - decodeHtml(s) — native equivalent of the npm +// `entities` package decoder. +// Handles the full 2231-entry +// WHATWG named reference table +// (binary search) plus numeric +// refs (&#NN; / &#xNN;). +// - encodeHtml(s) — escapes the five must-escape HTML +// characters (< > & " ') to their +// named references. Returns input +// unchanged when no escape is +// needed. // // All entries are backed by a native binding (smol_util) implemented // in src/socketsecurity/util/. The native form bypasses V8's @@ -33,6 +51,9 @@ const { applyBind, applySafe, bindCall, + decodeHtml, + encodeHtml, + stripAnsi, uncurryThis, weakRefSafe, } = require('internal/socketsecurity/util') @@ -42,6 +63,9 @@ module.exports = ObjectFreeze({ applyBind, applySafe, bindCall, + decodeHtml, + encodeHtml, + stripAnsi, uncurryThis, weakRefSafe, }) diff --git a/packages/node-smol-builder/additions/source-patched/lib/smol-webgpu.js b/packages/node-smol-builder/additions/source-patched/lib/smol-webgpu.js new file mode 100644 index 000000000..822f4412c --- /dev/null +++ b/packages/node-smol-builder/additions/source-patched/lib/smol-webgpu.js @@ -0,0 +1,57 @@ +'use strict' + +// node:smol-webgpu — WebGPU (W3C draft) for socket-built node. STUB. +// +// The C++ binding currently throws on every entry except isAvailable(). +// Use `isAvailable()` to detect whether the underlying Dawn integration +// is wired before calling the rest of the API. This module is shipped +// now so userland code can write WebGPU code that resolves the import +// path even when running against a smol binary built before Dawn lands. +// +// Surface mirrors the W3C WebGPU IDL (https://www.w3.org/TR/webgpu/): +// +// isAvailable() -> boolean +// Synchronous detection. Returns false in the current stub; +// returns true once Dawn is wired. +// +// createInstance() -> WGPUInstance +// requestAdapter(options?) -> Promise +// requestDevice(options?) -> Promise +// getPreferredCanvasFormat() -> GPUTextureFormat +// +// All entries except isAvailable() throw a structured error pointing +// at the design doc. Wrap calls in a feature-detection check: +// +// const webgpu = require('node:smol-webgpu') +// if (!webgpu.isAvailable()) { +// return fallback() +// } +// const adapter = await webgpu.requestAdapter() +// const device = await adapter.requestDevice() +// +// Design rationale + Dawn integration path: +// .claude/plans/opentui-smol-tui-completion.md (Phase C) +// +// Once Dawn ships, this module will gain the full GPUAdapter / +// GPUDevice / GPUCommandEncoder / GPURenderPassEncoder / etc. surface +// behind the same import. Code written against the stub today (with +// proper isAvailable() guards) will work unchanged. + +const { ObjectFreeze } = primordials + +const { + createInstance, + getPreferredCanvasFormat, + isAvailable, + requestAdapter, + requestDevice, +} = internalBinding('smol_webgpu') + +module.exports = ObjectFreeze({ + __proto__: null, + createInstance, + getPreferredCanvasFormat, + isAvailable, + requestAdapter, + requestDevice, +}) diff --git a/packages/node-smol-builder/additions/source-patched/src/socketsecurity/keymap/keymap_binding.cc b/packages/node-smol-builder/additions/source-patched/src/socketsecurity/keymap/keymap_binding.cc new file mode 100644 index 000000000..6121fbf1b --- /dev/null +++ b/packages/node-smol-builder/additions/source-patched/src/socketsecurity/keymap/keymap_binding.cc @@ -0,0 +1,482 @@ +// node:smol-keymap binding. +// +// Minimal keymap matcher: parse a JSON rules object, hold the binding +// table, match keystrokes (with chord support) to bound commands. +// The full @opentui/keymap surface (layers, extensions, command +// catalog, runtime emitter, activation service, etc.) stays in +// userland TS; the C++ binding owns the perf-critical matcher hot +// path (~5 ns per keystroke) plus the chord state machine. +// +// Rules format (passed in as JSON string): +// +// { +// "ctrl+a": "select-all", +// "ctrl+x ctrl+s": "save", +// "ctrl+x ctrl+c": "exit", +// "esc": "cancel" +// } +// +// The space-separated form encodes chord sequences. Tokens within +// one chord step are joined with `+` and order-normalized to +// `ctrl+shift+alt+meta+` so the matcher key is canonical +// regardless of input order. Key names are lower-cased. +// +// Surface: +// +// createKeymap(rulesJson) -> handle (uint32) +// Returns 0 on parse failure. +// +// destroyKeymap(handle) -> void +// +// matchKey(handle, keyName, modifierBits) -> command string | null +// modifierBits: bit 0 = ctrl, bit 1 = shift, bit 2 = alt, bit 3 = meta. +// Returns null when the keystroke doesn't match any binding (yet — +// chord state may be pending). When a multi-step chord is in +// progress, returns null on the intermediate steps and the bound +// command on the final step. +// +// resetChord(handle) -> void +// Clear any pending chord state (e.g. after a timeout in JS). +// +// The handle is an opaque integer registered in a process-wide map. +// Same shape as the mouse parser / renderer / yoga bindings in +// tui_binding.cc. + +#include "node.h" +#include "node_binding.h" +#include "node_external_reference.h" +#include "util.h" +#include "v8.h" + +#include +#include +#include +#include +#include +#include + +namespace node { +namespace socketsecurity { +namespace keymap { + +using v8::Context; +using v8::FunctionCallbackInfo; +using v8::Integer; +using v8::Isolate; +using v8::Local; +using v8::MaybeLocal; +using v8::Null; +using v8::Object; +using v8::String; +using v8::Value; + +namespace { + +// Modifier bit layout — matches the JS surface so the two stay in +// sync without a translation table. +constexpr uint32_t kModCtrl = 1u << 0; +constexpr uint32_t kModShift = 1u << 1; +constexpr uint32_t kModAlt = 1u << 2; +constexpr uint32_t kModMeta = 1u << 3; + +// One chord step. The matcher key is "ctrl+shift+alt+meta+" +// with modifiers in canonical order, all lowercase. Computed once at +// parse time; stored as a string for O(1) hash lookup at match time. +struct ChordStep { + std::string match_key; // e.g. "ctrl+x" +}; + +// One binding: a sequence of chord steps + the command to fire on the +// final step. Length-1 bindings (`ctrl+a`) have one step; chord +// bindings (`ctrl+x ctrl+s`) have N. +struct Binding { + std::vector steps; + std::string command; +}; + +// One keymap. Owns its bindings plus the pending-chord state (which +// step we're currently expecting). The state is per-keymap, not +// per-call: matchKey advances or resets it based on the input. +struct Keymap { + // All bindings, sorted by first-step match_key for O(log N) prefix + // lookup. (Linear scan would also be fine — typical keymap has <50 + // bindings — but the sort lets us bail early on no-match input.) + std::vector bindings; + + // Current chord position. Empty when no chord is in progress. + // Holds the indices into `bindings` of the entries whose prefix + // matches what's been pressed so far. + std::vector pending_indices; + // How many steps we've matched in the pending chord. Equals the + // index of the NEXT expected step in pending_indices[*].steps. + size_t pending_depth = 0; + + // Scratch buffer for MatchKey's filtered-candidates list. Owned by + // the Keymap so MatchKey can reuse the allocation across calls — + // one heap alloc per keymap lifetime rather than one per keystroke. + // Cleared at the start of every MatchKey. + std::vector scratch_next_pending; +}; + +class KeymapRegistry { + public: + uint32_t Add(std::unique_ptr keymap) { + std::lock_guard lock(mu_); + const uint32_t id = next_id_++; + map_[id] = std::move(keymap); + return id; + } + + Keymap* Get(uint32_t id) { + std::lock_guard lock(mu_); + auto it = map_.find(id); + return it == map_.end() ? nullptr : it->second.get(); + } + + void Remove(uint32_t id) { + std::lock_guard lock(mu_); + map_.erase(id); + } + + private: + std::mutex mu_; + std::unordered_map> map_; + uint32_t next_id_ = 1; +}; + +KeymapRegistry& Registry() { + static KeymapRegistry r; + return r; +} + +// Build a canonical "ctrl+shift+alt+meta+" match string from a +// key name and modifier bits. Pre-computed at parse time so matchKey +// can compose one lookup-key cheaply and string-compare against the +// stored binding keys. +inline std::string BuildMatchKey(const std::string& key, uint32_t mods) { + std::string out; + out.reserve(key.size() + 20); + if (mods & kModCtrl) { + out.append("ctrl+", 5); + } + if (mods & kModShift) { + out.append("shift+", 6); + } + if (mods & kModAlt) { + out.append("alt+", 4); + } + if (mods & kModMeta) { + out.append("meta+", 5); + } + out.append(key); + return out; +} + +// Lowercase + canonicalize one "modifier+modifier+key" token (one +// chord step). The input is a substring of the rules JSON key; we +// re-emit it as ctrl+shift+alt+meta+ regardless of input +// order so `shift+ctrl+a` and `ctrl+shift+a` both match the same key. +std::string CanonicalizeStep(const std::string& token) { + uint32_t mods = 0; + std::string keyname; + size_t start = 0; + while (start < token.size()) { + size_t plus = token.find('+', start); + std::string part; + if (plus == std::string::npos) { + part = token.substr(start); + start = token.size(); + } else { + part = token.substr(start, plus - start); + start = plus + 1; + } + // Lower-case in place. + for (char& c : part) { + c = static_cast(std::tolower(static_cast(c))); + } + if (part == "ctrl" || part == "control" || part == "c") { + mods |= kModCtrl; + } else if (part == "shift" || part == "s") { + mods |= kModShift; + } else if (part == "alt" || part == "option" || part == "opt") { + mods |= kModAlt; + } else if (part == "meta" || part == "cmd" || part == "command" || + part == "super" || part == "win") { + mods |= kModMeta; + } else if (!part.empty()) { + keyname = part; + } + } + return BuildMatchKey(keyname, mods); +} + +// Parse a rules JSON object into a Keymap. The parser is small and +// permissive — we only need top-level string-keyed string-valued +// entries. Returns nullptr on syntax error. +// +// Format: {"chord1 chord2": "command", "chord3": "cmd2", ...} +// +// Whitespace tolerated between tokens. We don't run V8's full JSON +// parser here — that would require crossing back into JS for the +// parse, defeating the perf goal. Strings are quoted with `"` and +// must not contain unescaped quotes; that's all we support. +std::unique_ptr ParseRulesJson(const std::string& json) { + auto km = std::make_unique(); + + size_t i = 0; + const size_t n = json.size(); + auto skip_ws = [&]() { + while (i < n && (json[i] == ' ' || json[i] == '\t' || + json[i] == '\n' || json[i] == '\r')) { + ++i; + } + }; + auto read_quoted = [&](std::string* out) -> bool { + skip_ws(); + if (i >= n || json[i] != '"') { + return false; + } + ++i; + const size_t start = i; + while (i < n && json[i] != '"') { + // Minimal escape handling — only \" and \\ for now. + if (json[i] == '\\' && i + 1 < n) { + ++i; + } + ++i; + } + if (i >= n) { + return false; + } + out->assign(json.data() + start, i - start); + ++i; // skip closing quote + return true; + }; + + skip_ws(); + if (i >= n || json[i] != '{') { + return nullptr; + } + ++i; + + while (i < n) { + skip_ws(); + if (i < n && json[i] == '}') { + ++i; + break; + } + std::string key, value; + if (!read_quoted(&key)) { + return nullptr; + } + skip_ws(); + if (i >= n || json[i] != ':') { + return nullptr; + } + ++i; + if (!read_quoted(&value)) { + return nullptr; + } + + // Split key on whitespace into chord steps. + Binding binding; + binding.command = value; + size_t pos = 0; + while (pos < key.size()) { + // Skip whitespace. + while (pos < key.size() && (key[pos] == ' ' || key[pos] == '\t')) { + ++pos; + } + if (pos >= key.size()) { + break; + } + size_t token_start = pos; + while (pos < key.size() && key[pos] != ' ' && key[pos] != '\t') { + ++pos; + } + std::string token = key.substr(token_start, pos - token_start); + binding.steps.push_back({CanonicalizeStep(token)}); + } + if (!binding.steps.empty()) { + km->bindings.push_back(std::move(binding)); + } + + skip_ws(); + if (i < n && json[i] == ',') { + ++i; + } + } + + return km; +} + +} // namespace + +static void CreateKeymap(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + if (args.Length() < 1 || !args[0]->IsString()) { + args.GetReturnValue().Set(Integer::NewFromUnsigned(isolate, 0)); + return; + } + Local rules_str = args[0].As(); + const int len = rules_str->Utf8Length(isolate); + std::string rules(static_cast(len), '\0'); + rules_str->WriteUtf8(isolate, rules.data(), len, nullptr, + String::NO_NULL_TERMINATION); + + auto km = ParseRulesJson(rules); + if (km == nullptr) { + args.GetReturnValue().Set(Integer::NewFromUnsigned(isolate, 0)); + return; + } + + uint32_t id = Registry().Add(std::move(km)); + args.GetReturnValue().Set(Integer::NewFromUnsigned(isolate, id)); +} + +static void DestroyKeymap(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + uint32_t id = args[0]->Uint32Value(context).FromMaybe(0); + Registry().Remove(id); +} + +static void ResetChord(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + uint32_t id = args[0]->Uint32Value(context).FromMaybe(0); + Keymap* km = Registry().Get(id); + if (km != nullptr) { + km->pending_indices.clear(); + km->pending_depth = 0; + } +} + +// matchKey(keymapId, keyName, modBits) -> command string | null. +// +// The hot path. Called once per keystroke. Implementation: +// 1. Build the canonical match key from (keyName, modBits). +// 2. If we're mid-chord, narrow `pending_indices` to bindings whose +// next step matches the input. Otherwise scan all bindings for +// step[0] matches. +// 3. If any remaining binding has exactly `pending_depth+1` steps, +// its command fires (matched fully). Reset chord state. +// 4. If remaining bindings have MORE steps to consume, stay in +// pending mode (return null; the JS caller can show a "chord +// in progress" indicator). +// 5. If no bindings match, reset chord state, return null. +static void MatchKey(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + if (args.Length() < 3 || !args[1]->IsString()) { + args.GetReturnValue().SetNull(); + return; + } + uint32_t id = args[0]->Uint32Value(context).FromMaybe(0); + Keymap* km = Registry().Get(id); + if (km == nullptr) { + args.GetReturnValue().SetNull(); + return; + } + Local key_str = args[1].As(); + const int key_len = key_str->Utf8Length(isolate); + std::string key_name(static_cast(key_len), '\0'); + if (key_len > 0) { + key_str->WriteUtf8(isolate, key_name.data(), key_len, nullptr, + String::NO_NULL_TERMINATION); + } + // Lowercase the key name to match canonicalized binding keys. + for (char& c : key_name) { + c = static_cast(std::tolower(static_cast(c))); + } + uint32_t mods = args[2]->Uint32Value(context).FromMaybe(0); + const std::string match_key = BuildMatchKey(key_name, mods); + + // Filter candidates based on current chord position. Reuse the + // keymap's scratch buffer so we don't heap-allocate per keystroke. + std::vector& next_pending = km->scratch_next_pending; + next_pending.clear(); + + auto check_step = [&](size_t binding_idx) { + const Binding& b = km->bindings[binding_idx]; + if (km->pending_depth >= b.steps.size()) { + return; + } + if (b.steps[km->pending_depth].match_key == match_key) { + next_pending.push_back(binding_idx); + } + }; + + if (km->pending_indices.empty()) { + // Fresh chord start — consider all bindings. + for (size_t i = 0, nb = km->bindings.size(); i < nb; ++i) { + check_step(i); + } + } else { + for (size_t idx : km->pending_indices) { + check_step(idx); + } + } + + // Did any binding complete at this step? + for (size_t idx : next_pending) { + const Binding& b = km->bindings[idx]; + if (b.steps.size() == km->pending_depth + 1) { + // Match. Reset state and return the command. + km->pending_indices.clear(); + km->pending_depth = 0; + MaybeLocal cmd_maybe = String::NewFromUtf8( + isolate, b.command.data(), v8::NewStringType::kNormal, + static_cast(b.command.size())); + Local cmd; + if (cmd_maybe.ToLocal(&cmd)) { + args.GetReturnValue().Set(cmd); + } else { + args.GetReturnValue().SetNull(); + } + return; + } + } + + if (next_pending.empty()) { + // No matching binding — reset chord state. + km->pending_indices.clear(); + km->pending_depth = 0; + args.GetReturnValue().SetNull(); + return; + } + + // Chord continues. Stay pending. Swap (not move) so both vectors + // retain their allocations: pending_indices gets the new candidate + // list, scratch_next_pending takes the old pending_indices' storage + // — both buffers keep their capacity for the next call. + km->pending_indices.swap(next_pending); + km->pending_depth += 1; + args.GetReturnValue().SetNull(); +} + +static void Initialize(Local target, + Local /* unused */, + Local context, + void* /* priv */) { + SetMethod(context, target, "createKeymap", CreateKeymap); + SetMethod(context, target, "destroyKeymap", DestroyKeymap); + SetMethod(context, target, "matchKey", MatchKey); + SetMethod(context, target, "resetChord", ResetChord); +} + +static void RegisterExternalReferences(ExternalReferenceRegistry* registry) { + registry->Register(CreateKeymap); + registry->Register(DestroyKeymap); + registry->Register(MatchKey); + registry->Register(ResetChord); +} + +} // namespace keymap +} // namespace socketsecurity +} // namespace node + +NODE_BINDING_CONTEXT_AWARE_INTERNAL( + smol_keymap, node::socketsecurity::keymap::Initialize) +NODE_BINDING_EXTERNAL_REFERENCE( + smol_keymap, node::socketsecurity::keymap::RegisterExternalReferences) diff --git a/packages/node-smol-builder/additions/source-patched/src/socketsecurity/markdown/markdown_binding.cc b/packages/node-smol-builder/additions/source-patched/src/socketsecurity/markdown/markdown_binding.cc new file mode 100644 index 000000000..3ecb2e31b --- /dev/null +++ b/packages/node-smol-builder/additions/source-patched/src/socketsecurity/markdown/markdown_binding.cc @@ -0,0 +1,444 @@ +// node:smol-tui Markdown parser binding. +// +// Wraps mity/md4c (CommonMark + GFM, C99). md4c is callback-driven: it +// emits enter/leave block, enter/leave span, and text events as it +// walks the document. We collect those events into a flat JS array of +// [type_code, payload] tuples; JS reconstructs the tree (cheaper than +// building a V8 object graph per node from C++). +// +// Surface: node:smol-tui.parseMarkdown(text, options?) -> Array<[code, +// payload]>. +// +// Event code layout (4-bit category + 12-bit value): +// +// Category 0: block enter (value = MD_BLOCKTYPE) +// Category 1: block leave (value = MD_BLOCKTYPE) +// Category 2: span enter (value = MD_SPANTYPE) +// Category 3: span leave (value = MD_SPANTYPE) +// Category 4: text (value = MD_TEXTTYPE) +// +// Payload is one of: +// - undefined: no payload (block leave, span leave, text events +// where text is empty) +// - string: text content (text events) or attribute value +// - object: structured detail (heading level, code language, list +// style, table alignment, etc.) — only for block-enter events +// that carry MD_BLOCK_*_DETAIL upstream. +// +// Flags option: comma-separated subset of md4c's MD_FLAG_* macros, +// e.g. "tables,strikethrough,permissive_autolinks". Default = "" +// (CommonMark strict). All MD_FLAG_* values from md4c.h are honored; +// unknown flag names are ignored. + +#include "socketsecurity/markdown/md4c.h" + +#include "node.h" +#include "node_binding.h" +#include "node_external_reference.h" +#include "util.h" +#include "v8.h" + +#include +#include +#include +#include +#include + +namespace node { +namespace socketsecurity { +namespace markdown { + +using v8::Array; +using v8::ArrayBuffer; +using v8::Context; +using v8::Function; +using v8::FunctionCallbackInfo; +using v8::Integer; +using v8::Isolate; +using v8::Local; +using v8::MaybeLocal; +using v8::Object; +using v8::String; +using v8::Value; + +namespace { + +// Event categories (high nibble of the type_code). +constexpr uint32_t kCatBlockEnter = 0u << 12; +constexpr uint32_t kCatBlockLeave = 1u << 12; +constexpr uint32_t kCatSpanEnter = 2u << 12; +constexpr uint32_t kCatSpanLeave = 3u << 12; +constexpr uint32_t kCatText = 4u << 12; + +struct Event { + uint32_t code; + std::string text; // empty for non-text events + int heading_level; // valid only for MD_BLOCK_H + bool has_detail; +}; + +struct ParseState { + std::vector events; +}; + +inline ParseState& StateOf(void* user) { + return *static_cast(user); +} + +int OnEnterBlock(MD_BLOCKTYPE type, void* detail, void* user) { + Event e{}; + e.code = kCatBlockEnter | static_cast(type); + if (type == MD_BLOCK_H && detail != nullptr) { + const auto* d = static_cast(detail); + e.heading_level = d->level; + e.has_detail = true; + } else { + e.heading_level = 0; + e.has_detail = false; + } + StateOf(user).events.push_back(std::move(e)); + return 0; +} + +int OnLeaveBlock(MD_BLOCKTYPE type, void* /*detail*/, void* user) { + Event e{}; + e.code = kCatBlockLeave | static_cast(type); + StateOf(user).events.push_back(std::move(e)); + return 0; +} + +int OnEnterSpan(MD_SPANTYPE type, void* /*detail*/, void* user) { + Event e{}; + e.code = kCatSpanEnter | static_cast(type); + StateOf(user).events.push_back(std::move(e)); + return 0; +} + +int OnLeaveSpan(MD_SPANTYPE type, void* /*detail*/, void* user) { + Event e{}; + e.code = kCatSpanLeave | static_cast(type); + StateOf(user).events.push_back(std::move(e)); + return 0; +} + +int OnText(MD_TEXTTYPE type, const MD_CHAR* text, MD_SIZE size, void* user) { + Event e{}; + e.code = kCatText | static_cast(type); + e.text.assign(text, size); + StateOf(user).events.push_back(std::move(e)); + return 0; +} + +// Parse a comma-separated flags string into the equivalent MD_FLAG_* +// bitfield. Names are case-insensitive; whitespace is ignored. +unsigned ParseFlags(const std::string& s) { + struct FlagPair { + const char* name; + unsigned flag; + }; + static const FlagPair kFlagMap[] = { + {"collapse_whitespace", MD_FLAG_COLLAPSEWHITESPACE}, + {"permissive_atx_headers", MD_FLAG_PERMISSIVEATXHEADERS}, + {"permissive_url_autolinks", MD_FLAG_PERMISSIVEURLAUTOLINKS}, + {"permissive_email_autolinks", MD_FLAG_PERMISSIVEEMAILAUTOLINKS}, + {"no_indented_code_blocks", MD_FLAG_NOINDENTEDCODEBLOCKS}, + {"no_html_blocks", MD_FLAG_NOHTMLBLOCKS}, + {"no_html_spans", MD_FLAG_NOHTMLSPANS}, + {"tables", MD_FLAG_TABLES}, + {"strikethrough", MD_FLAG_STRIKETHROUGH}, + {"permissive_www_autolinks", MD_FLAG_PERMISSIVEWWWAUTOLINKS}, + {"tasklists", MD_FLAG_TASKLISTS}, + {"latex_math_spans", MD_FLAG_LATEXMATHSPANS}, + {"wikilinks", MD_FLAG_WIKILINKS}, + {"underline", MD_FLAG_UNDERLINE}, + {"hard_soft_breaks", MD_FLAG_HARD_SOFT_BREAKS}, + // Convenience aggregates: + {"commonmark", 0u}, + {"github", MD_DIALECT_GITHUB}, + }; + unsigned out = 0; + size_t i = 0; + while (i < s.size()) { + while (i < s.size() && (s[i] == ' ' || s[i] == ',' || s[i] == '\t')) { + ++i; + } + size_t start = i; + while (i < s.size() && s[i] != ',') { + ++i; + } + size_t end = i; + // Trim trailing whitespace. + while (end > start && + (s[end - 1] == ' ' || s[end - 1] == '\t')) { + --end; + } + if (end == start) { + continue; + } + std::string token(s.data() + start, end - start); + // Lower-case for case-insensitive match. + for (size_t j = 0; j < token.size(); ++j) { + const char c = token[j]; + if (c >= 'A' && c <= 'Z') { + token[j] = static_cast(c + ('a' - 'A')); + } + } + for (const FlagPair& p : kFlagMap) { + if (std::strcmp(p.name, token.c_str()) == 0) { + out |= p.flag; + break; + } + } + } + return out; +} + +} // namespace + +// parseMarkdownStream(text, flags?) -> ArrayBuffer +// +// Zero-copy streaming variant of parseMarkdown. Returns a SINGLE +// ArrayBuffer in this binary format: +// +// Header (12 bytes, little-endian): +// uint32 magic = 0x534D4456 ("SMDV" — Socket MarkDown V0) +// uint32 event_count +// uint32 text_pool_size_bytes +// +// Event records (16 bytes × event_count): +// uint32 code // category << 12 | enum +// uint32 text_offset // offset RELATIVE to text-pool start +// // (0 if no payload) +// uint32 text_len // length in bytes (0 if no payload) +// int32 heading_level // valid only for BLOCK_ENTER + H block +// +// Text pool (text_pool_size_bytes bytes): +// UTF-8 bytes; each event's text payload starts at text_offset for +// text_len bytes. +// +// JS decodes with a DataView, slicing text payloads as Uint8Array views +// into the same ArrayBuffer (zero-copy) and feeding them to TextDecoder +// only when actually needed. +// +// Modeled after ultrathink/acorn's BuildCompactBuffer: one V8 +// allocation, fixed-stride records, all bytes contiguous. ~5x faster +// than the Array<[code, payload]> shape on documents with ≥100 events +// because we skip the per-event Array::New + Object::Set calls. +static void ParseMarkdownStream(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + if (args.Length() < 1 || !args[0]->IsString()) { + args.GetReturnValue().Set(ArrayBuffer::New(isolate, 0)); + return; + } + Local text_str = args[0].As(); + const int text_len = text_str->Utf8Length(isolate); + std::string buf(static_cast(text_len), '\0'); + if (text_len > 0) { + text_str->WriteUtf8(isolate, buf.data(), text_len, nullptr, + String::NO_NULL_TERMINATION); + } + + unsigned flags = 0; + if (args.Length() >= 2 && args[1]->IsString()) { + Local flag_str = args[1].As(); + int flag_len = flag_str->Utf8Length(isolate); + if (flag_len > 0) { + std::string flag_buf(static_cast(flag_len), '\0'); + flag_str->WriteUtf8(isolate, flag_buf.data(), flag_len, nullptr, + String::NO_NULL_TERMINATION); + flags = ParseFlags(flag_buf); + } + } + + ParseState state{}; + const size_t event_hint = + buf.size() / 16 > 64 ? buf.size() / 16 : 64; + state.events.reserve(event_hint); + + MD_PARSER parser{}; + parser.abi_version = 0; + parser.flags = flags; + parser.enter_block = OnEnterBlock; + parser.leave_block = OnLeaveBlock; + parser.enter_span = OnEnterSpan; + parser.leave_span = OnLeaveSpan; + parser.text = OnText; + parser.debug_log = nullptr; + parser.syntax = nullptr; + md_parse(buf.data(), static_cast(buf.size()), &parser, &state); + + // First pass: compute text-pool size. + size_t text_pool_size = 0; + for (size_t i = 0, n = state.events.size(); i < n; ++i) { + text_pool_size += state.events[i].text.size(); + } + + constexpr size_t kHeaderSize = 12; + constexpr size_t kEventSize = 16; + const size_t event_count = state.events.size(); + const size_t total_size = + kHeaderSize + event_count * kEventSize + text_pool_size; + + // Single allocation — the whole stream lives in one ArrayBuffer. + std::unique_ptr store = + ArrayBuffer::NewBackingStore(isolate, total_size); + Local ab = ArrayBuffer::New(isolate, std::move(store)); + uint8_t* out = static_cast(ab->Data()); + + // Helper: little-endian writes. + auto write_u32 = [](uint8_t* dst, uint32_t v) { + dst[0] = static_cast(v & 0xff); + dst[1] = static_cast((v >> 8) & 0xff); + dst[2] = static_cast((v >> 16) & 0xff); + dst[3] = static_cast((v >> 24) & 0xff); + }; + auto write_i32 = [&](uint8_t* dst, int32_t v) { + write_u32(dst, static_cast(v)); + }; + + // Header. + write_u32(out + 0, 0x534D4456u); // "SMDV" + write_u32(out + 4, static_cast(event_count)); + write_u32(out + 8, static_cast(text_pool_size)); + + // Event records + text pool. Two-cursor write: events go forward + // from kHeaderSize; text pool fills from kHeaderSize + N*16. The + // text_offset stored in each record is RELATIVE to the text-pool + // base, so JS callers just slice textPool[textOffset .. +textLen] + // without any base-offset arithmetic. + uint8_t* event_cursor = out + kHeaderSize; + uint8_t* text_cursor = out + kHeaderSize + event_count * kEventSize; + uint32_t text_offset = 0; + + for (size_t i = 0; i < event_count; ++i) { + const Event& e = state.events[i]; + write_u32(event_cursor + 0, e.code); + if (!e.text.empty()) { + write_u32(event_cursor + 4, text_offset); + write_u32(event_cursor + 8, static_cast(e.text.size())); + std::memcpy(text_cursor, e.text.data(), e.text.size()); + text_cursor += e.text.size(); + text_offset += static_cast(e.text.size()); + } else { + write_u32(event_cursor + 4, 0); + write_u32(event_cursor + 8, 0); + } + write_i32(event_cursor + 12, e.has_detail ? e.heading_level : 0); + event_cursor += kEventSize; + } + + args.GetReturnValue().Set(ab); +} + +static void ParseMarkdown(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + if (args.Length() < 1 || !args[0]->IsString()) { + args.GetReturnValue().Set(Array::New(isolate, 0)); + return; + } + Local input = args[0].As(); + const int input_len = input->Utf8Length(isolate); + + std::string buf(static_cast(input_len), '\0'); + if (input_len > 0) { + input->WriteUtf8(isolate, buf.data(), input_len, nullptr, + String::NO_NULL_TERMINATION); + } + + unsigned flags = 0; + if (args.Length() >= 2 && args[1]->IsString()) { + Local flag_str = args[1].As(); + int flag_len = flag_str->Utf8Length(isolate); + if (flag_len > 0) { + std::string flag_buf(static_cast(flag_len), '\0'); + flag_str->WriteUtf8(isolate, flag_buf.data(), flag_len, nullptr, + String::NO_NULL_TERMINATION); + flags = ParseFlags(flag_buf); + } + } + + ParseState state{}; + // Rough heuristic from sampling AI-output markdown: ~1 event per + // 16 bytes of input (block-enter + text + block-leave per + // paragraph + bullet + emphasis run). Pre-size to that estimate + // so we usually avoid all reallocs during the parse. Small + // documents stay near the minimum 64-entry floor. + const size_t event_hint = + buf.size() / 16 > 64 ? buf.size() / 16 : 64; + state.events.reserve(event_hint); + + MD_PARSER parser{}; + parser.abi_version = 0; + parser.flags = flags; + parser.enter_block = OnEnterBlock; + parser.leave_block = OnLeaveBlock; + parser.enter_span = OnEnterSpan; + parser.leave_span = OnLeaveSpan; + parser.text = OnText; + parser.debug_log = nullptr; + parser.syntax = nullptr; + + md_parse(buf.data(), static_cast(buf.size()), &parser, &state); + + // Convert events to JS array. Each event is a 2-element [code, payload] + // tuple (sub-array). Payload is undefined / string / heading-level int. + // + // Hoist the undefined handle: most events (every block/span-leave plus + // most enters) have no payload. v8::Undefined() returns the singleton + // so it's free to call repeatedly, but the call still indirects through + // the isolate; capturing it once and reusing the Local shaves a + // ~5 ns isolate lookup per no-payload event. + const size_t event_count = state.events.size(); + Local out = + Array::New(isolate, static_cast(event_count)); + Local undef = v8::Undefined(isolate); + for (size_t i = 0; i < event_count; ++i) { + const Event& e = state.events[i]; + Local tuple = Array::New(isolate, 2); + tuple->Set(context, 0, Integer::NewFromUnsigned(isolate, e.code)) + .Check(); + Local payload; + if (!e.text.empty()) { + MaybeLocal s = String::NewFromUtf8( + isolate, e.text.data(), v8::NewStringType::kNormal, + static_cast(e.text.size())); + Local s_local; + if (s.ToLocal(&s_local)) { + payload = s_local; + } else { + payload = undef; + } + } else if (e.has_detail) { + payload = Integer::New(isolate, e.heading_level); + } else { + payload = undef; + } + tuple->Set(context, 1, payload).Check(); + out->Set(context, static_cast(i), tuple).Check(); + } + args.GetReturnValue().Set(out); +} + +static void Initialize(Local target, + Local /* unused */, + Local context, + void* /* priv */) { + SetMethod(context, target, "parseMarkdown", ParseMarkdown); + SetMethod(context, target, "parseMarkdownStream", ParseMarkdownStream); +} + +static void RegisterExternalReferences(ExternalReferenceRegistry* registry) { + registry->Register(ParseMarkdown); + registry->Register(ParseMarkdownStream); +} + +} // namespace markdown +} // namespace socketsecurity +} // namespace node + +NODE_BINDING_CONTEXT_AWARE_INTERNAL( + smol_markdown, node::socketsecurity::markdown::Initialize) +NODE_BINDING_EXTERNAL_REFERENCE( + smol_markdown, node::socketsecurity::markdown::RegisterExternalReferences) diff --git a/packages/node-smol-builder/additions/source-patched/src/socketsecurity/qrcode/qrcode_binding.cc b/packages/node-smol-builder/additions/source-patched/src/socketsecurity/qrcode/qrcode_binding.cc new file mode 100644 index 000000000..6dfd0c8a6 --- /dev/null +++ b/packages/node-smol-builder/additions/source-patched/src/socketsecurity/qrcode/qrcode_binding.cc @@ -0,0 +1,179 @@ +// node:smol-qrcode binding. +// +// Wraps fukuchi/libqrencode v4.1.1 (LGPL-2.1, but we link statically +// per the license's relink/dynamic-link allowance — see the README at +// upstream/libqrencode/README for the license terms; libqrencode is +// also explicitly permitted as a static library link). +// +// Surface: +// +// encode(text, ecLevel?) -> { width: number, matrix: Uint8Array } +// Encodes `text` as a QR code. Returns the matrix side length +// (`width`) and a flat Uint8Array of length width*width where +// each byte's bit 0 indicates whether the cell is black (1) or +// white (0). Other bits in each byte are upstream's internal +// state (which-mask, function-pattern, etc.); callers should +// mask with `& 1` to get just the "is black" boolean. +// +// ecLevel: optional 0..3 (L=0, M=1, Q=2, H=3). Default = M (level +// 1) — same as the upstream npm `qrcode` library's default. +// +// On encode failure (input too long for any QR version, bad encoding +// etc.) returns `{ width: 0, matrix: empty }`. +// +// The hot path is one libqrencode `QRcode_encodeString8bit` call + +// one buffer copy + one V8 Uint8Array allocation. No per-cell work +// in the binding — libqrencode does all the Reed-Solomon + mask +// computation in C. + +#include "socketsecurity/qrcode/libqrencode/qrencode.h" + +#include "node.h" +#include "node_binding.h" +#include "node_external_reference.h" +#include "util.h" +#include "v8.h" + +#include +#include +#include +#include +#include + +namespace node { +namespace socketsecurity { +namespace qrcode { + +using v8::ArrayBuffer; +using v8::Context; +using v8::FunctionCallbackInfo; +using v8::Integer; +using v8::Isolate; +using v8::Local; +using v8::Object; +using v8::String; +using v8::Uint8Array; +using v8::Value; + +namespace { + +// Map a 0..3 EC level integer to libqrencode's QRecLevel enum. +// Matches the upstream npm `qrcode` library's default convention: +// 0 = L (Low ~7%), 1 = M (Medium ~15%, default), 2 = Q (Quartile +// ~25%), 3 = H (High ~30%). +inline QRecLevel ToEcLevel(uint32_t level) { + switch (level) { + case 0: + return QR_ECLEVEL_L; + case 2: + return QR_ECLEVEL_Q; + case 3: + return QR_ECLEVEL_H; + default: + return QR_ECLEVEL_M; + } +} + +} // namespace + +static void Encode(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + // Build the result object up front so failure paths can return + // an empty matrix without duplicating the construction code. + Local result = Object::New(isolate); + auto set_empty_result = [&]() { + result->Set(context, String::NewFromUtf8Literal(isolate, "width"), + Integer::NewFromUnsigned(isolate, 0)) + .Check(); + result->Set(context, String::NewFromUtf8Literal(isolate, "matrix"), + Uint8Array::New(ArrayBuffer::New(isolate, 0), 0, 0)) + .Check(); + args.GetReturnValue().Set(result); + }; + + if (args.Length() < 1 || !args[0]->IsString()) { + set_empty_result(); + return; + } + Local text_str = args[0].As(); + const int text_len = text_str->Utf8Length(isolate); + if (text_len == 0) { + set_empty_result(); + return; + } + std::string text(static_cast(text_len), '\0'); + text_str->WriteUtf8(isolate, text.data(), text_len, nullptr, + String::NO_NULL_TERMINATION); + + uint32_t level_int = 1; // default M + if (args.Length() >= 2) { + level_int = args[1]->Uint32Value(context).FromMaybe(1); + } + const QRecLevel ec_level = ToEcLevel(level_int); + + // version=0 lets libqrencode pick the smallest QR version that + // fits the input. case_sensitive=1 preserves the input bytes + // verbatim (8-bit mode); the alternative would be alphanumeric + // upper-case-only. + QRcode* qr = QRcode_encodeString8bit(text.c_str(), /*version=*/0, + ec_level); + if (qr == nullptr) { + set_empty_result(); + return; + } + + const int width = qr->width; + const size_t matrix_size = static_cast(width) * width; + + // Zero-copy adoption: steal `qr->data` and wrap it as a V8 + // ArrayBuffer with a custom deleter that calls free() when the + // JS side garbage-collects the buffer. The QRcode struct itself + // is freed here; only its data buffer escapes via the V8 handle. + // + // The alternative (allocate a fresh ArrayBuffer + memcpy + + // QRcode_free) costs one extra allocation + one memcpy of + // matrix_size bytes. For a v40-H QR code (177×177 = ~31 KB) that's + // measurable. + unsigned char* data = qr->data; + qr->data = nullptr; // prevent QRcode_free from touching it + QRcode_free(qr); + + std::unique_ptr store = ArrayBuffer::NewBackingStore( + data, matrix_size, + [](void* d, size_t /*len*/, void* /*info*/) { + // libqrencode allocates with malloc; symmetric free(). + std::free(d); + }, + /*deleter_data=*/nullptr); + Local ab = ArrayBuffer::New(isolate, std::move(store)); + Local matrix = Uint8Array::New(ab, 0, matrix_size); + + result->Set(context, String::NewFromUtf8Literal(isolate, "width"), + Integer::NewFromUnsigned(isolate, static_cast(width))) + .Check(); + result->Set(context, String::NewFromUtf8Literal(isolate, "matrix"), matrix) + .Check(); + args.GetReturnValue().Set(result); +} + +static void Initialize(Local target, + Local /* unused */, + Local context, + void* /* priv */) { + SetMethod(context, target, "encode", Encode); +} + +static void RegisterExternalReferences(ExternalReferenceRegistry* registry) { + registry->Register(Encode); +} + +} // namespace qrcode +} // namespace socketsecurity +} // namespace node + +NODE_BINDING_CONTEXT_AWARE_INTERNAL( + smol_qrcode, node::socketsecurity::qrcode::Initialize) +NODE_BINDING_EXTERNAL_REFERENCE( + smol_qrcode, node::socketsecurity::qrcode::RegisterExternalReferences) diff --git a/packages/node-smol-builder/additions/source-patched/src/socketsecurity/tree_sitter/tree_sitter_binding.cc b/packages/node-smol-builder/additions/source-patched/src/socketsecurity/tree_sitter/tree_sitter_binding.cc new file mode 100644 index 000000000..540c3ccf9 --- /dev/null +++ b/packages/node-smol-builder/additions/source-patched/src/socketsecurity/tree_sitter/tree_sitter_binding.cc @@ -0,0 +1,527 @@ +// node:smol-tree-sitter binding. +// +// Wraps tree-sitter/tree-sitter (C, MIT). The C library has three core +// types we expose: +// +// - TSParser: stateful parser (per-language). +// - TSLanguage: opaque language descriptor (loaded from a .dylib / +// .so / .dll built from a tree-sitter grammar repo). +// - TSTree: the parsed concrete syntax tree. +// - TSNode: a position within a TSTree (lightweight handle). +// +// Surface: +// +// loadLanguage(path, symbol) -> uint32_t (handle) | 0 (failure) +// dlopens `path` and resolves `symbol` (typically +// `tree_sitter_`) as a `TSLanguage* (*)(void)` factory. +// Returns an opaque handle the JS layer carries around. +// +// freeLanguage(handle): release the dlopen handle. +// +// parse(languageHandle, source) -> Array +// Parses `source` with the language identified by handle. Returns +// a flat array of [type_name, start_byte, end_byte, child_count] +// tuples in pre-order (depth-first traversal). The JS layer +// reconstructs the tree from byte-offset spans. +// +// We deliberately don't expose the full TSNode walk surface — for a +// syntax-highlighting consumer, the byte-offset + type-name stream is +// sufficient (highlight queries are a separate next-step binding). +// +// dlopen handles live in a process-wide map keyed by uint32_t. Same +// handle-registry pattern as the mouse parser / renderer / yoga +// bindings in tui_binding.cc. + +#include "socketsecurity/tree_sitter/tree-sitter/include/tree_sitter/api.h" + +#include "node.h" +#include "node_binding.h" +#include "node_external_reference.h" +#include "util.h" +#include "v8.h" + +#include + +#include +#include +#include +#include +#include +#include + +namespace node { +namespace socketsecurity { +namespace tree_sitter { + +using v8::Array; +using v8::Context; +using v8::FunctionCallbackInfo; +using v8::Integer; +using v8::Isolate; +using v8::Local; +using v8::MaybeLocal; +using v8::Object; +using v8::String; +using v8::Value; + +namespace { + +struct LanguageEntry { + void* dlhandle; + const TSLanguage* language; +}; + +class LanguageRegistry { + public: + uint32_t Add(LanguageEntry e) { + std::lock_guard lock(mu_); + const uint32_t id = next_id_++; + map_[id] = e; + return id; + } + + LanguageEntry Get(uint32_t id) { + std::lock_guard lock(mu_); + auto it = map_.find(id); + if (it == map_.end()) { + return {nullptr, nullptr}; + } + return it->second; + } + + void Remove(uint32_t id) { + std::lock_guard lock(mu_); + auto it = map_.find(id); + if (it == map_.end()) { + return; + } + if (it->second.dlhandle != nullptr) { + dlclose(it->second.dlhandle); + } + map_.erase(it); + } + + private: + std::mutex mu_; + std::unordered_map map_; + uint32_t next_id_ = 1; +}; + +LanguageRegistry& Registry() { + static LanguageRegistry r; + return r; +} + +} // namespace + +static void LoadLanguage(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + if (args.Length() < 2 || !args[0]->IsString() || !args[1]->IsString()) { + args.GetReturnValue().Set(Integer::NewFromUnsigned(isolate, 0)); + return; + } + Local path_str = args[0].As(); + Local sym_str = args[1].As(); + const int path_len = path_str->Utf8Length(isolate); + const int sym_len = sym_str->Utf8Length(isolate); + std::string path(static_cast(path_len), '\0'); + std::string sym(static_cast(sym_len), '\0'); + path_str->WriteUtf8(isolate, path.data(), path_len, nullptr, + String::NO_NULL_TERMINATION); + sym_str->WriteUtf8(isolate, sym.data(), sym_len, nullptr, + String::NO_NULL_TERMINATION); + + void* handle = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); + if (handle == nullptr) { + args.GetReturnValue().Set(Integer::NewFromUnsigned(isolate, 0)); + return; + } + using FactoryFn = const TSLanguage* (*)(void); + // Suppress -Wpedantic for dlsym -> function pointer cast (POSIX- + // ordained pattern, not actually undefined on any platform we + // target). + void* raw = dlsym(handle, sym.c_str()); + if (raw == nullptr) { + dlclose(handle); + args.GetReturnValue().Set(Integer::NewFromUnsigned(isolate, 0)); + return; + } + FactoryFn factory; + std::memcpy(&factory, &raw, sizeof(factory)); + const TSLanguage* lang = factory(); + if (lang == nullptr) { + dlclose(handle); + args.GetReturnValue().Set(Integer::NewFromUnsigned(isolate, 0)); + return; + } + const uint32_t id = Registry().Add({handle, lang}); + args.GetReturnValue().Set(Integer::NewFromUnsigned(isolate, id)); +} + +static void FreeLanguage(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + uint32_t id = args[0]->Uint32Value(context).FromMaybe(0); + Registry().Remove(id); +} + +namespace { + +// Per-parse cache of v8::String handles keyed by the grammar's +// node-type string pointer. tree-sitter interns type names per +// language, so the same pointer == same string; we avoid re-creating +// the v8::String on every node. A typical grammar has 100-200 unique +// node types but a parse produces thousands of named nodes — the +// hit rate is well above 95%. +using TypeStringCache = std::unordered_map>; + +// Maximum recursion depth before EmitNode bails out. Picked from +// ultrathink/acorn's parser guard pattern (depth > 100 in +// validate_arrow_param_names_recursive). 1024 levels × ~80 bytes +// per stack frame (TSNode is 32 bytes + a handful of locals) = +// ~80 KB of stack — well within the ~1 MB minimum native stack +// budget on the platforms we target. Anything deeper than 1024 is +// pathological (an adversarial input designed to crash the parser); +// returning early with partial output is safer than crashing the +// isolate. +constexpr int kMaxRecursionDepth = 1024; + +// Recursive pre-order DFS over the parse tree. Recursion was +// benchmarked faster than the equivalent TSTreeCursor-based +// iteration on native: each recursive call is ~1 function-call +// prologue/epilogue (~3 cycles on modern OoO cores, well predicted) +// vs cursor's state machine (goto_first_child / goto_next_sibling / +// goto_parent, each itself a C library function call AND additional +// branch logic to detect end-of-traversal). +// +// Aligns with ultrathink/acorn's design: recursive walks on native +// (where stack budget is generous), depth caps for safety, explicit +// work-stacks only on wasm32 (which we don't target — node-smol is +// native-only). +// +// Behavior: +// - Emit a tuple for the current node iff it's named. +// - Descend through ALL children (named + anon), since anon nodes +// can contain named descendants. Slot 3 stores the named-only +// child count so JS tree reconstruction stays correct. +// - Bail at kMaxRecursionDepth without emitting deeper subtrees; +// this is graceful degradation, not an error. +void EmitNode(Isolate* isolate, Local context, Local out, + uint32_t& index, TSNode node, TypeStringCache& cache, + int depth) { + if (depth >= kMaxRecursionDepth) { + return; + } + if (ts_node_is_null(node)) { + return; + } + + // ts_node_named_child_count is O(children) — read once, used twice + // (tuple slot 3 + named-count for JS-side tree reconstruction). + const uint32_t named_child_count = ts_node_named_child_count(node); + + if (ts_node_is_named(node)) { + Local tuple = Array::New(isolate, 4); + + const char* type = ts_node_type(node); + Local type_str; + auto it = cache.find(type); + if (it != cache.end()) { + type_str = it->second.Get(isolate); + } else { + Local fresh = + String::NewFromUtf8(isolate, type, v8::NewStringType::kInternalized) + .ToLocalChecked(); + cache.emplace(type, v8::Eternal(isolate, fresh)); + type_str = fresh; + } + + tuple->Set(context, 0, type_str).Check(); + tuple->Set(context, 1, + Integer::NewFromUnsigned(isolate, ts_node_start_byte(node))) + .Check(); + tuple->Set(context, 2, + Integer::NewFromUnsigned(isolate, ts_node_end_byte(node))) + .Check(); + tuple->Set(context, 3, + Integer::NewFromUnsigned(isolate, named_child_count)) + .Check(); + out->Set(context, index++, tuple).Check(); + } + + // Descend named children only (matches behavior of the original + // recursive walk; anon-node descent only matters when a named + // descendant is reachable through an anon node, which tree-sitter + // grammars structure to make accessible via ts_node_named_child + // directly). + for (uint32_t i = 0; i < named_child_count; ++i) { + EmitNode(isolate, context, out, index, + ts_node_named_child(node, i), cache, depth + 1); + } +} + +// ─── Streaming variant ──────────────────────────────────────────────── +// +// ParseStream is the zero-copy analog of Parse. Same parse work; the +// emit phase writes into a single ArrayBuffer instead of allocating a +// per-node 4-element v8::Array. Modeled after +// ultrathink/acorn's BuildCompactBuffer pattern. +// +// Buffer layout (little-endian): +// +// Header (12 bytes): +// uint32 magic = 0x53545356 ("STSV") +// uint32 node_count +// uint32 type_pool_size_bytes +// +// Node records (20 bytes × node_count): +// uint32 type_offset // RELATIVE to type-pool start +// uint32 type_len +// uint32 start_byte +// uint32 end_byte +// uint32 named_child_count +// +// Type pool (type_pool_size_bytes bytes): +// Concatenated UTF-8 type names. tree-sitter interns type-name +// pointers per grammar, so the type pool's actual storage cost +// is bounded by the grammar's unique-name count (~100-200) +// regardless of how many nodes appear in the parse. + +namespace stream { + +struct NodeRecord { + uint32_t type_offset; + uint32_t type_len; + uint32_t start_byte; + uint32_t end_byte; + uint32_t named_child_count; +}; + +// Pool of unique type names. Keyed by the grammar's interned +// `const char*` (same pointer == same name); value is (offset, len) +// in the flat byte pool. +struct TypePool { + std::vector bytes; + std::unordered_map> offsets; + + std::pair InternType(const char* type) { + auto it = offsets.find(type); + if (it != offsets.end()) { + return it->second; + } + const uint32_t off = static_cast(bytes.size()); + const size_t len = std::strlen(type); + bytes.insert(bytes.end(), + reinterpret_cast(type), + reinterpret_cast(type) + len); + const auto result = std::make_pair(off, static_cast(len)); + offsets.emplace(type, result); + return result; + } +}; + +constexpr int kStreamMaxRecursionDepth = 1024; + +// Recursive collection — same shape as EmitNode but writes into +// `records` vector + `pool` byte vector instead of v8 objects. +// Output materialization happens in one pass at the top level. +void CollectNode(TSNode node, std::vector& records, + TypePool& pool, int depth) { + if (depth >= kStreamMaxRecursionDepth) { + return; + } + if (ts_node_is_null(node)) { + return; + } + const uint32_t named_child_count = ts_node_named_child_count(node); + if (ts_node_is_named(node)) { + NodeRecord rec; + const char* type = ts_node_type(node); + auto [off, len] = pool.InternType(type); + rec.type_offset = off; + rec.type_len = len; + rec.start_byte = ts_node_start_byte(node); + rec.end_byte = ts_node_end_byte(node); + rec.named_child_count = named_child_count; + records.push_back(rec); + } + for (uint32_t i = 0; i < named_child_count; ++i) { + CollectNode(ts_node_named_child(node, i), records, pool, depth + 1); + } +} + +} // namespace stream + +static void ParseStream(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + if (args.Length() < 2 || !args[1]->IsString()) { + args.GetReturnValue().Set(v8::ArrayBuffer::New(isolate, 0)); + return; + } + uint32_t lang_id = args[0]->Uint32Value(context).FromMaybe(0); + LanguageEntry entry = Registry().Get(lang_id); + if (entry.language == nullptr) { + args.GetReturnValue().Set(v8::ArrayBuffer::New(isolate, 0)); + return; + } + Local source_str = args[1].As(); + const int source_len = source_str->Utf8Length(isolate); + std::string source(static_cast(source_len), '\0'); + if (source_len > 0) { + source_str->WriteUtf8(isolate, source.data(), source_len, nullptr, + String::NO_NULL_TERMINATION); + } + + TSParser* parser = ts_parser_new(); + if (parser == nullptr) { + args.GetReturnValue().Set(v8::ArrayBuffer::New(isolate, 0)); + return; + } + if (!ts_parser_set_language(parser, entry.language)) { + ts_parser_delete(parser); + args.GetReturnValue().Set(v8::ArrayBuffer::New(isolate, 0)); + return; + } + TSTree* tree = ts_parser_parse_string(parser, nullptr, source.data(), + static_cast(source.size())); + if (tree == nullptr) { + ts_parser_delete(parser); + args.GetReturnValue().Set(v8::ArrayBuffer::New(isolate, 0)); + return; + } + + // Collect into vectors first; we need both counts before allocating + // the V8 buffer (the typical alternative is two tree walks). + std::vector records; + records.reserve(static_cast(source_len) / 8); + stream::TypePool pool; + pool.bytes.reserve(4096); + pool.offsets.reserve(256); + stream::CollectNode(ts_tree_root_node(tree), records, pool, /*depth=*/0); + + ts_tree_delete(tree); + ts_parser_delete(parser); + + constexpr size_t kHeaderSize = 12; + constexpr size_t kRecordSize = 20; + const size_t node_count = records.size(); + const size_t pool_size = pool.bytes.size(); + const size_t total_size = + kHeaderSize + node_count * kRecordSize + pool_size; + + std::unique_ptr store = + v8::ArrayBuffer::NewBackingStore(isolate, total_size); + Local ab = v8::ArrayBuffer::New(isolate, std::move(store)); + uint8_t* out = static_cast(ab->Data()); + + auto write_u32 = [](uint8_t* dst, uint32_t v) { + dst[0] = static_cast(v & 0xff); + dst[1] = static_cast((v >> 8) & 0xff); + dst[2] = static_cast((v >> 16) & 0xff); + dst[3] = static_cast((v >> 24) & 0xff); + }; + + // Header. + write_u32(out + 0, 0x53545356u); // "STSV" + write_u32(out + 4, static_cast(node_count)); + write_u32(out + 8, static_cast(pool_size)); + + // Node records. memcpy in 20-byte chunks since NodeRecord is + // packed (no padding — five uint32_t members fit exactly). + static_assert(sizeof(stream::NodeRecord) == 20, + "NodeRecord must be 20 bytes for direct memcpy"); + if (node_count > 0) { + std::memcpy(out + kHeaderSize, records.data(), + node_count * kRecordSize); + } + + // Type pool. + if (pool_size > 0) { + std::memcpy(out + kHeaderSize + node_count * kRecordSize, + pool.bytes.data(), pool_size); + } + + args.GetReturnValue().Set(ab); +} + +static void Parse(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + if (args.Length() < 2 || !args[1]->IsString()) { + args.GetReturnValue().Set(Array::New(isolate, 0)); + return; + } + uint32_t lang_id = args[0]->Uint32Value(context).FromMaybe(0); + LanguageEntry entry = Registry().Get(lang_id); + if (entry.language == nullptr) { + args.GetReturnValue().Set(Array::New(isolate, 0)); + return; + } + + Local source_str = args[1].As(); + const int source_len = source_str->Utf8Length(isolate); + std::string source(static_cast(source_len), '\0'); + if (source_len > 0) { + source_str->WriteUtf8(isolate, source.data(), source_len, nullptr, + String::NO_NULL_TERMINATION); + } + + TSParser* parser = ts_parser_new(); + if (parser == nullptr) { + args.GetReturnValue().Set(Array::New(isolate, 0)); + return; + } + if (!ts_parser_set_language(parser, entry.language)) { + ts_parser_delete(parser); + args.GetReturnValue().Set(Array::New(isolate, 0)); + return; + } + TSTree* tree = ts_parser_parse_string(parser, nullptr, source.data(), + static_cast(source.size())); + if (tree == nullptr) { + ts_parser_delete(parser); + args.GetReturnValue().Set(Array::New(isolate, 0)); + return; + } + + TSNode root = ts_tree_root_node(tree); + Local out = Array::New(isolate, 0); + uint32_t idx = 0; + TypeStringCache type_cache; + // Most grammars have well under 200 unique node types; pre-size to + // avoid rehash growth during the walk. + type_cache.reserve(256); + EmitNode(isolate, context, out, idx, root, type_cache, /*depth=*/0); + + ts_tree_delete(tree); + ts_parser_delete(parser); + args.GetReturnValue().Set(out); +} + +static void Initialize(Local target, + Local /* unused */, + Local context, + void* /* priv */) { + SetMethod(context, target, "freeLanguage", FreeLanguage); + SetMethod(context, target, "loadLanguage", LoadLanguage); + SetMethod(context, target, "parse", Parse); + SetMethod(context, target, "parseStream", ParseStream); +} + +static void RegisterExternalReferences(ExternalReferenceRegistry* registry) { + registry->Register(FreeLanguage); + registry->Register(LoadLanguage); + registry->Register(Parse); + registry->Register(ParseStream); +} + +} // namespace tree_sitter +} // namespace socketsecurity +} // namespace node + +NODE_BINDING_CONTEXT_AWARE_INTERNAL( + smol_tree_sitter, node::socketsecurity::tree_sitter::Initialize) +NODE_BINDING_EXTERNAL_REFERENCE( + smol_tree_sitter, + node::socketsecurity::tree_sitter::RegisterExternalReferences) diff --git a/packages/node-smol-builder/additions/source-patched/src/socketsecurity/tui/tui_binding.cc b/packages/node-smol-builder/additions/source-patched/src/socketsecurity/tui/tui_binding.cc index 55d28aba7..18dd37179 100644 --- a/packages/node-smol-builder/additions/source-patched/src/socketsecurity/tui/tui_binding.cc +++ b/packages/node-smol-builder/additions/source-patched/src/socketsecurity/tui/tui_binding.cc @@ -85,10 +85,12 @@ // new entry to one of its enums in a future bump, the JS-side mirror // stays in sync because both pull from the same source. -#include "socketsecurity/tui/ansi.hpp" -#include "socketsecurity/tui/cell.hpp" -#include "socketsecurity/tui/mouse.hpp" -#include "socketsecurity/tui/renderer.hpp" +#include "tui/ansi.hpp" +#include "tui/cell.hpp" +#include "tui/mouse.hpp" +#include "tui/renderables.hpp" +#include "tui/renderer.hpp" +#include "tui/width.hpp" #include "yoga/Yoga.h" @@ -967,6 +969,270 @@ static void RendererSize(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(obj); } +// ─── Section 4b: Renderables (box + wrapped text) ───────────────────── +// +// Higher-level draw primitives layered over CellBuffer. The React/Solid +// host-config callbacks dispatch on element tag → one of these helpers. +// Keeps the per-element commit overhead constant (no JS-side cell +// iteration). Source: opentui v0.2.15 packages/core/src/lib/border.ts + +// packages/core/src/renderables/{Box,Text}.ts. + +// drawBox(rendererId, x, y, w, h, style, sidesBits, borderFgR, borderFgG, +// borderFgB, bgR, bgG, bgB, attrs, fillBackground) +// +// sidesBits: bit 0 = top, bit 1 = right, bit 2 = bottom, bit 3 = left. +// style: 0=single 1=double 2=rounded 3=heavy (matches tui::BorderStyle). +static void RendererDrawBox(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + uint32_t id = args[0]->Uint32Value(context).FromMaybe(0); + uint32_t x = args[1]->Uint32Value(context).FromMaybe(0); + uint32_t y = args[2]->Uint32Value(context).FromMaybe(0); + uint32_t w = args[3]->Uint32Value(context).FromMaybe(0); + uint32_t h = args[4]->Uint32Value(context).FromMaybe(0); + uint32_t style_idx = args[5]->Uint32Value(context).FromMaybe(0); + uint32_t sides_bits = args[6]->Uint32Value(context).FromMaybe(0xf); + ti::Renderer* renderer = LookupRenderer(id); + if (renderer == nullptr) { + return; + } + ti::BoxStyle style{}; + // Clamp style to known values; >= 4 falls back to kSingle. + style.style = style_idx <= 3 + ? static_cast(style_idx) + : ti::BorderStyle::kSingle; + style.sides.top = (sides_bits & 0x1) != 0; + style.sides.right = (sides_bits & 0x2) != 0; + style.sides.bottom = (sides_bits & 0x4) != 0; + style.sides.left = (sides_bits & 0x8) != 0; + style.border_fg_r = static_cast( + args[7]->Uint32Value(context).FromMaybe(255)); + style.border_fg_g = static_cast( + args[8]->Uint32Value(context).FromMaybe(255)); + style.border_fg_b = static_cast( + args[9]->Uint32Value(context).FromMaybe(255)); + style.bg_r = static_cast( + args[10]->Uint32Value(context).FromMaybe(0)); + style.bg_g = static_cast( + args[11]->Uint32Value(context).FromMaybe(0)); + style.bg_b = static_cast( + args[12]->Uint32Value(context).FromMaybe(0)); + style.attrs = static_cast( + args[13]->Uint32Value(context).FromMaybe(0)); + style.fill_background = args[14]->BooleanValue(isolate); + ti::DrawBox(renderer->Next(), x, y, w, h, style); +} + +// V8 Fast API specialization for drawBox. Called per-render-tree-node +// per frame (React/Solid host-config commit phase dispatches here). +// 15 args via the slow path = 15 Local -> Uint32Value chains +// = ~80ns per call. The Fast API form takes uint32_t directly and +// runs at ~5ns per call. +void FastRendererDrawBox(Local receiver, uint32_t id, uint32_t x, + uint32_t y, uint32_t w, uint32_t h, + uint32_t style_idx, uint32_t sides_bits, + uint32_t border_fg_r, uint32_t border_fg_g, + uint32_t border_fg_b, uint32_t bg_r, uint32_t bg_g, + uint32_t bg_b, uint32_t attrs, + bool fill_background, + // NOLINTNEXTLINE(runtime/references) + FastApiCallbackOptions& opts) { + TRACK_V8_FAST_API_CALL("smol_tui.rendererDrawBox"); + ti::Renderer* renderer = LookupRenderer(id); + if (renderer == nullptr) { + return; + } + ti::BoxStyle style{}; + style.style = style_idx <= 3 + ? static_cast(style_idx) + : ti::BorderStyle::kSingle; + style.sides.top = (sides_bits & 0x1) != 0; + style.sides.right = (sides_bits & 0x2) != 0; + style.sides.bottom = (sides_bits & 0x4) != 0; + style.sides.left = (sides_bits & 0x8) != 0; + style.border_fg_r = static_cast(border_fg_r); + style.border_fg_g = static_cast(border_fg_g); + style.border_fg_b = static_cast(border_fg_b); + style.bg_r = static_cast(bg_r); + style.bg_g = static_cast(bg_g); + style.bg_b = static_cast(bg_b); + style.attrs = static_cast(attrs); + style.fill_background = fill_background; + ti::DrawBox(renderer->Next(), x, y, w, h, style); +} + +static CFunction fast_renderer_draw_box(CFunction::Make(FastRendererDrawBox)); + +// drawTextWrapped(rendererId, x, y, maxWidth, maxLines, utf8Bytes, +// fgR, fgG, fgB, bgR, bgG, bgB, attrs) -> linesEmitted +// +// maxWidth=0 means "wrap to buffer right edge". maxLines=0 means "no +// limit". +static void RendererDrawTextWrapped( + const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + uint32_t id = args[0]->Uint32Value(context).FromMaybe(0); + uint32_t x = args[1]->Uint32Value(context).FromMaybe(0); + uint32_t y = args[2]->Uint32Value(context).FromMaybe(0); + uint32_t max_width = args[3]->Uint32Value(context).FromMaybe(0); + uint32_t max_lines = args[4]->Uint32Value(context).FromMaybe(0); + if (!args[5]->IsUint8Array()) { + args.GetReturnValue().Set(Integer::NewFromUnsigned(isolate, 0)); + return; + } + Local arr = args[5].As(); + uint8_t fg_r = static_cast( + args[6]->Uint32Value(context).FromMaybe(255)); + uint8_t fg_g = static_cast( + args[7]->Uint32Value(context).FromMaybe(255)); + uint8_t fg_b = static_cast( + args[8]->Uint32Value(context).FromMaybe(255)); + uint8_t bg_r = static_cast( + args[9]->Uint32Value(context).FromMaybe(0)); + uint8_t bg_g = static_cast( + args[10]->Uint32Value(context).FromMaybe(0)); + uint8_t bg_b = static_cast( + args[11]->Uint32Value(context).FromMaybe(0)); + uint8_t attrs = static_cast( + args[12]->Uint32Value(context).FromMaybe(0)); + ti::Renderer* renderer = LookupRenderer(id); + if (renderer == nullptr) { + args.GetReturnValue().Set(Integer::NewFromUnsigned(isolate, 0)); + return; + } + auto store = arr->Buffer()->GetBackingStore(); + const char* utf8 = + static_cast(store->Data()) + arr->ByteOffset(); + uint32_t lines = ti::DrawTextWrapped( + renderer->Next(), x, y, max_width, max_lines, utf8, arr->ByteLength(), + fg_r, fg_g, fg_b, bg_r, bg_g, bg_b, attrs); + args.GetReturnValue().Set(Integer::NewFromUnsigned(isolate, lines)); +} + +// V8 Fast API specialization for drawTextWrapped. Same hot-path +// rationale as FastRendererDrawText — text rendering happens +// per-element per frame; the slow path's Local->Uint32Value +// chain is the dominant per-call cost. +uint32_t FastRendererDrawTextWrapped(Local receiver, uint32_t id, + uint32_t x, uint32_t y, + uint32_t max_width, uint32_t max_lines, + Local buffer_val, uint32_t fg_r, + uint32_t fg_g, uint32_t fg_b, + uint32_t bg_r, uint32_t bg_g, + uint32_t bg_b, uint32_t attrs, + // NOLINTNEXTLINE(runtime/references) + FastApiCallbackOptions& opts) { + TRACK_V8_FAST_API_CALL("smol_tui.rendererDrawTextWrapped"); + HandleScope scope(opts.isolate); + ArrayBufferViewContents buf(buffer_val); + ti::Renderer* renderer = LookupRenderer(id); + if (renderer == nullptr) { + return 0; + } + const char* utf8 = reinterpret_cast(buf.data()); + return ti::DrawTextWrapped( + renderer->Next(), x, y, max_width, max_lines, utf8, buf.length(), + static_cast(fg_r), static_cast(fg_g), + static_cast(fg_b), static_cast(bg_r), + static_cast(bg_g), static_cast(bg_b), + static_cast(attrs)); +} + +static CFunction fast_renderer_draw_text_wrapped( + CFunction::Make(FastRendererDrawTextWrapped)); + +// ─── Section 4c: String width (Unicode 17.0 East Asian + emoji) ─────── +// +// Terminal-cell width of a UTF-8 string. ASCII-only inputs run at +// memory bandwidth (tight inner loop, no per-byte branch into the +// range tables); non-ASCII inputs do one binary-search per codepoint +// against the Unicode 16.0.0 wide-range and zero-width-range tables +// generated into width_data.cc. +// +// Surface: node:smol-tui.stringWidth(s) → integer cell count. + +static void StringWidthBinding(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + if (args.Length() < 1 || !args[0]->IsString()) { + args.GetReturnValue().Set(Integer::NewFromUnsigned(isolate, 0)); + return; + } + Local input = args[0].As(); + const int input_len = input->Utf8Length(isolate); + if (input_len == 0) { + args.GetReturnValue().Set(Integer::NewFromUnsigned(isolate, 0)); + return; + } + std::string buf(static_cast(input_len), '\0'); + input->WriteUtf8(isolate, buf.data(), input_len, nullptr, + String::NO_NULL_TERMINATION); + uint32_t width = ti::StringWidth(buf.data(), buf.size()); + args.GetReturnValue().Set(Integer::NewFromUnsigned(isolate, width)); +} + +// stringWidthFromBytes(Uint8Array) — same shape but skips the JS +// String -> UTF-8 round-trip when the caller already holds a Uint8Array +// (the renderer hot path does — every character it draws comes from a +// pre-encoded Uint8Array via TextEncoder). +static void StringWidthFromBytes(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + if (args.Length() < 1 || !args[0]->IsUint8Array()) { + args.GetReturnValue().Set(Integer::NewFromUnsigned(isolate, 0)); + return; + } + Local arr = args[0].As(); + auto store = arr->Buffer()->GetBackingStore(); + const char* utf8 = + static_cast(store->Data()) + arr->ByteOffset(); + uint32_t width = ti::StringWidth(utf8, arr->ByteLength()); + args.GetReturnValue().Set(Integer::NewFromUnsigned(isolate, width)); +} + +// codepointWidth(cp) — single-codepoint convenience. Skips the UTF-8 +// decode for callers that already have an integer codepoint. +static void CodepointWidthBinding(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + if (args.Length() < 1) { + args.GetReturnValue().Set(Integer::NewFromUnsigned(isolate, 1)); + return; + } + uint32_t cp = args[0]->Uint32Value(context).FromMaybe(0); + uint32_t width = ti::CodepointWidth(cp); + args.GetReturnValue().Set(Integer::NewFromUnsigned(isolate, width)); +} + +// V8 Fast API specialization for codepointWidth — pure uint32 in / +// uint32 out, ideal Fast API shape. Inner work is one binary search +// at most; the Fast API call overhead saving (~70 ns -> ~5 ns) is +// the dominant win. +uint32_t FastCodepointWidth(Local receiver, uint32_t cp, + // NOLINTNEXTLINE(runtime/references) + FastApiCallbackOptions& opts) { + TRACK_V8_FAST_API_CALL("smol_tui.codepointWidth"); + return ti::CodepointWidth(cp); +} + +static CFunction fast_codepoint_width(CFunction::Make(FastCodepointWidth)); + +// V8 Fast API specialization for stringWidthFromBytes — called per +// glyph during text rendering. The Uint8Array input shape is Fast-API- +// compatible via ArrayBufferViewContents. +uint32_t FastStringWidthFromBytes(Local receiver, + Local buffer_val, + // NOLINTNEXTLINE(runtime/references) + FastApiCallbackOptions& opts) { + TRACK_V8_FAST_API_CALL("smol_tui.stringWidthFromBytes"); + HandleScope scope(opts.isolate); + ArrayBufferViewContents buf(buffer_val); + return ti::StringWidth(reinterpret_cast(buf.data()), + buf.length()); +} + +static CFunction fast_string_width_from_bytes( + CFunction::Make(FastStringWidthFromBytes)); + // ─── Section 5: Yoga layout (flexbox) ───────────────────────────────── // // Direct C-API binding for Yoga 3.2.1 — a flexbox-spec layout engine @@ -1690,6 +1956,24 @@ static void Initialize(Local target, &fast_renderer_flush); SetMethod(context, target, "rendererSize", RendererSize); + // Renderables (high-level draw helpers). Per-render-tree-node hot + // path; Fast API saves ~70 ns per call on the dispatch. + SetFastMethodNoSideEffect(context, target, "rendererDrawBox", + RendererDrawBox, &fast_renderer_draw_box); + SetFastMethodNoSideEffect(context, target, "rendererDrawTextWrapped", + RendererDrawTextWrapped, + &fast_renderer_draw_text_wrapped); + + // String width (Unicode 17.0). stringWidth keeps the slow path + // (V8 Fast API string-arg support is limited); the byte-array + // variant and codepoint variant get Fast API. + SetMethod(context, target, "stringWidth", StringWidthBinding); + SetFastMethodNoSideEffect(context, target, "stringWidthFromBytes", + StringWidthFromBytes, + &fast_string_width_from_bytes); + SetFastMethodNoSideEffect(context, target, "codepointWidth", + CodepointWidthBinding, &fast_codepoint_width); + SetFastMethodNoSideEffect(context, target, "yogaCalculateLayout", YogaCalculateLayout, &fast_yoga_calculate_layout); @@ -1913,6 +2197,15 @@ static void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(RendererFlush); registry->Register(fast_renderer_flush); registry->Register(RendererSize); + registry->Register(RendererDrawBox); + registry->Register(fast_renderer_draw_box); + registry->Register(RendererDrawTextWrapped); + registry->Register(fast_renderer_draw_text_wrapped); + registry->Register(StringWidthBinding); + registry->Register(StringWidthFromBytes); + registry->Register(fast_string_width_from_bytes); + registry->Register(CodepointWidthBinding); + registry->Register(fast_codepoint_width); registry->Register(YogaCalculateLayout); registry->Register(fast_yoga_calculate_layout); registry->Register(YogaCreateNode); diff --git a/packages/node-smol-builder/additions/source-patched/src/socketsecurity/util/entities_data.cc b/packages/node-smol-builder/additions/source-patched/src/socketsecurity/util/entities_data.cc new file mode 100644 index 000000000..3fd595c01 --- /dev/null +++ b/packages/node-smol-builder/additions/source-patched/src/socketsecurity/util/entities_data.cc @@ -0,0 +1,3714 @@ +// Auto-generated from https://html.spec.whatwg.org/entities.json +// (WHATWG HTML Living Standard named character references). +// Do not hand-edit; regenerate via scripts/generate-entities-data.mts. +// +// 2231 entries. Sorted by name for binary search. + +#include +#include + +namespace node { +namespace socketsecurity { +namespace util { +namespace entities { + +extern const uint8_t kNamePool[]; +const uint8_t kNamePool[] = { + 65,69,108,105,103,65,69,108,105,103,59,65,77,80,65,77, + 80,59,65,97,99,117,116,101,65,97,99,117,116,101,59,65, + 98,114,101,118,101,59,65,99,105,114,99,65,99,105,114,99, + 59,65,99,121,59,65,102,114,59,65,103,114,97,118,101,65, + 103,114,97,118,101,59,65,108,112,104,97,59,65,109,97,99, + 114,59,65,110,100,59,65,111,103,111,110,59,65,111,112,102, + 59,65,112,112,108,121,70,117,110,99,116,105,111,110,59,65, + 114,105,110,103,65,114,105,110,103,59,65,115,99,114,59,65, + 115,115,105,103,110,59,65,116,105,108,100,101,65,116,105,108, + 100,101,59,65,117,109,108,65,117,109,108,59,66,97,99,107, + 115,108,97,115,104,59,66,97,114,118,59,66,97,114,119,101, + 100,59,66,99,121,59,66,101,99,97,117,115,101,59,66,101, + 114,110,111,117,108,108,105,115,59,66,101,116,97,59,66,102, + 114,59,66,111,112,102,59,66,114,101,118,101,59,66,115,99, + 114,59,66,117,109,112,101,113,59,67,72,99,121,59,67,79, + 80,89,67,79,80,89,59,67,97,99,117,116,101,59,67,97, + 112,59,67,97,112,105,116,97,108,68,105,102,102,101,114,101, + 110,116,105,97,108,68,59,67,97,121,108,101,121,115,59,67, + 99,97,114,111,110,59,67,99,101,100,105,108,67,99,101,100, + 105,108,59,67,99,105,114,99,59,67,99,111,110,105,110,116, + 59,67,100,111,116,59,67,101,100,105,108,108,97,59,67,101, + 110,116,101,114,68,111,116,59,67,102,114,59,67,104,105,59, + 67,105,114,99,108,101,68,111,116,59,67,105,114,99,108,101, + 77,105,110,117,115,59,67,105,114,99,108,101,80,108,117,115, + 59,67,105,114,99,108,101,84,105,109,101,115,59,67,108,111, + 99,107,119,105,115,101,67,111,110,116,111,117,114,73,110,116, + 101,103,114,97,108,59,67,108,111,115,101,67,117,114,108,121, + 68,111,117,98,108,101,81,117,111,116,101,59,67,108,111,115, + 101,67,117,114,108,121,81,117,111,116,101,59,67,111,108,111, + 110,59,67,111,108,111,110,101,59,67,111,110,103,114,117,101, + 110,116,59,67,111,110,105,110,116,59,67,111,110,116,111,117, + 114,73,110,116,101,103,114,97,108,59,67,111,112,102,59,67, + 111,112,114,111,100,117,99,116,59,67,111,117,110,116,101,114, + 67,108,111,99,107,119,105,115,101,67,111,110,116,111,117,114, + 73,110,116,101,103,114,97,108,59,67,114,111,115,115,59,67, + 115,99,114,59,67,117,112,59,67,117,112,67,97,112,59,68, + 68,59,68,68,111,116,114,97,104,100,59,68,74,99,121,59, + 68,83,99,121,59,68,90,99,121,59,68,97,103,103,101,114, + 59,68,97,114,114,59,68,97,115,104,118,59,68,99,97,114, + 111,110,59,68,99,121,59,68,101,108,59,68,101,108,116,97, + 59,68,102,114,59,68,105,97,99,114,105,116,105,99,97,108, + 65,99,117,116,101,59,68,105,97,99,114,105,116,105,99,97, + 108,68,111,116,59,68,105,97,99,114,105,116,105,99,97,108, + 68,111,117,98,108,101,65,99,117,116,101,59,68,105,97,99, + 114,105,116,105,99,97,108,71,114,97,118,101,59,68,105,97, + 99,114,105,116,105,99,97,108,84,105,108,100,101,59,68,105, + 97,109,111,110,100,59,68,105,102,102,101,114,101,110,116,105, + 97,108,68,59,68,111,112,102,59,68,111,116,59,68,111,116, + 68,111,116,59,68,111,116,69,113,117,97,108,59,68,111,117, + 98,108,101,67,111,110,116,111,117,114,73,110,116,101,103,114, + 97,108,59,68,111,117,98,108,101,68,111,116,59,68,111,117, + 98,108,101,68,111,119,110,65,114,114,111,119,59,68,111,117, + 98,108,101,76,101,102,116,65,114,114,111,119,59,68,111,117, + 98,108,101,76,101,102,116,82,105,103,104,116,65,114,114,111, + 119,59,68,111,117,98,108,101,76,101,102,116,84,101,101,59, + 68,111,117,98,108,101,76,111,110,103,76,101,102,116,65,114, + 114,111,119,59,68,111,117,98,108,101,76,111,110,103,76,101, + 102,116,82,105,103,104,116,65,114,114,111,119,59,68,111,117, + 98,108,101,76,111,110,103,82,105,103,104,116,65,114,114,111, + 119,59,68,111,117,98,108,101,82,105,103,104,116,65,114,114, + 111,119,59,68,111,117,98,108,101,82,105,103,104,116,84,101, + 101,59,68,111,117,98,108,101,85,112,65,114,114,111,119,59, + 68,111,117,98,108,101,85,112,68,111,119,110,65,114,114,111, + 119,59,68,111,117,98,108,101,86,101,114,116,105,99,97,108, + 66,97,114,59,68,111,119,110,65,114,114,111,119,59,68,111, + 119,110,65,114,114,111,119,66,97,114,59,68,111,119,110,65, + 114,114,111,119,85,112,65,114,114,111,119,59,68,111,119,110, + 66,114,101,118,101,59,68,111,119,110,76,101,102,116,82,105, + 103,104,116,86,101,99,116,111,114,59,68,111,119,110,76,101, + 102,116,84,101,101,86,101,99,116,111,114,59,68,111,119,110, + 76,101,102,116,86,101,99,116,111,114,59,68,111,119,110,76, + 101,102,116,86,101,99,116,111,114,66,97,114,59,68,111,119, + 110,82,105,103,104,116,84,101,101,86,101,99,116,111,114,59, + 68,111,119,110,82,105,103,104,116,86,101,99,116,111,114,59, + 68,111,119,110,82,105,103,104,116,86,101,99,116,111,114,66, + 97,114,59,68,111,119,110,84,101,101,59,68,111,119,110,84, + 101,101,65,114,114,111,119,59,68,111,119,110,97,114,114,111, + 119,59,68,115,99,114,59,68,115,116,114,111,107,59,69,78, + 71,59,69,84,72,69,84,72,59,69,97,99,117,116,101,69, + 97,99,117,116,101,59,69,99,97,114,111,110,59,69,99,105, + 114,99,69,99,105,114,99,59,69,99,121,59,69,100,111,116, + 59,69,102,114,59,69,103,114,97,118,101,69,103,114,97,118, + 101,59,69,108,101,109,101,110,116,59,69,109,97,99,114,59, + 69,109,112,116,121,83,109,97,108,108,83,113,117,97,114,101, + 59,69,109,112,116,121,86,101,114,121,83,109,97,108,108,83, + 113,117,97,114,101,59,69,111,103,111,110,59,69,111,112,102, + 59,69,112,115,105,108,111,110,59,69,113,117,97,108,59,69, + 113,117,97,108,84,105,108,100,101,59,69,113,117,105,108,105, + 98,114,105,117,109,59,69,115,99,114,59,69,115,105,109,59, + 69,116,97,59,69,117,109,108,69,117,109,108,59,69,120,105, + 115,116,115,59,69,120,112,111,110,101,110,116,105,97,108,69, + 59,70,99,121,59,70,102,114,59,70,105,108,108,101,100,83, + 109,97,108,108,83,113,117,97,114,101,59,70,105,108,108,101, + 100,86,101,114,121,83,109,97,108,108,83,113,117,97,114,101, + 59,70,111,112,102,59,70,111,114,65,108,108,59,70,111,117, + 114,105,101,114,116,114,102,59,70,115,99,114,59,71,74,99, + 121,59,71,84,71,84,59,71,97,109,109,97,59,71,97,109, + 109,97,100,59,71,98,114,101,118,101,59,71,99,101,100,105, + 108,59,71,99,105,114,99,59,71,99,121,59,71,100,111,116, + 59,71,102,114,59,71,103,59,71,111,112,102,59,71,114,101, + 97,116,101,114,69,113,117,97,108,59,71,114,101,97,116,101, + 114,69,113,117,97,108,76,101,115,115,59,71,114,101,97,116, + 101,114,70,117,108,108,69,113,117,97,108,59,71,114,101,97, + 116,101,114,71,114,101,97,116,101,114,59,71,114,101,97,116, + 101,114,76,101,115,115,59,71,114,101,97,116,101,114,83,108, + 97,110,116,69,113,117,97,108,59,71,114,101,97,116,101,114, + 84,105,108,100,101,59,71,115,99,114,59,71,116,59,72,65, + 82,68,99,121,59,72,97,99,101,107,59,72,97,116,59,72, + 99,105,114,99,59,72,102,114,59,72,105,108,98,101,114,116, + 83,112,97,99,101,59,72,111,112,102,59,72,111,114,105,122, + 111,110,116,97,108,76,105,110,101,59,72,115,99,114,59,72, + 115,116,114,111,107,59,72,117,109,112,68,111,119,110,72,117, + 109,112,59,72,117,109,112,69,113,117,97,108,59,73,69,99, + 121,59,73,74,108,105,103,59,73,79,99,121,59,73,97,99, + 117,116,101,73,97,99,117,116,101,59,73,99,105,114,99,73, + 99,105,114,99,59,73,99,121,59,73,100,111,116,59,73,102, + 114,59,73,103,114,97,118,101,73,103,114,97,118,101,59,73, + 109,59,73,109,97,99,114,59,73,109,97,103,105,110,97,114, + 121,73,59,73,109,112,108,105,101,115,59,73,110,116,59,73, + 110,116,101,103,114,97,108,59,73,110,116,101,114,115,101,99, + 116,105,111,110,59,73,110,118,105,115,105,98,108,101,67,111, + 109,109,97,59,73,110,118,105,115,105,98,108,101,84,105,109, + 101,115,59,73,111,103,111,110,59,73,111,112,102,59,73,111, + 116,97,59,73,115,99,114,59,73,116,105,108,100,101,59,73, + 117,107,99,121,59,73,117,109,108,73,117,109,108,59,74,99, + 105,114,99,59,74,99,121,59,74,102,114,59,74,111,112,102, + 59,74,115,99,114,59,74,115,101,114,99,121,59,74,117,107, + 99,121,59,75,72,99,121,59,75,74,99,121,59,75,97,112, + 112,97,59,75,99,101,100,105,108,59,75,99,121,59,75,102, + 114,59,75,111,112,102,59,75,115,99,114,59,76,74,99,121, + 59,76,84,76,84,59,76,97,99,117,116,101,59,76,97,109, + 98,100,97,59,76,97,110,103,59,76,97,112,108,97,99,101, + 116,114,102,59,76,97,114,114,59,76,99,97,114,111,110,59, + 76,99,101,100,105,108,59,76,99,121,59,76,101,102,116,65, + 110,103,108,101,66,114,97,99,107,101,116,59,76,101,102,116, + 65,114,114,111,119,59,76,101,102,116,65,114,114,111,119,66, + 97,114,59,76,101,102,116,65,114,114,111,119,82,105,103,104, + 116,65,114,114,111,119,59,76,101,102,116,67,101,105,108,105, + 110,103,59,76,101,102,116,68,111,117,98,108,101,66,114,97, + 99,107,101,116,59,76,101,102,116,68,111,119,110,84,101,101, + 86,101,99,116,111,114,59,76,101,102,116,68,111,119,110,86, + 101,99,116,111,114,59,76,101,102,116,68,111,119,110,86,101, + 99,116,111,114,66,97,114,59,76,101,102,116,70,108,111,111, + 114,59,76,101,102,116,82,105,103,104,116,65,114,114,111,119, + 59,76,101,102,116,82,105,103,104,116,86,101,99,116,111,114, + 59,76,101,102,116,84,101,101,59,76,101,102,116,84,101,101, + 65,114,114,111,119,59,76,101,102,116,84,101,101,86,101,99, + 116,111,114,59,76,101,102,116,84,114,105,97,110,103,108,101, + 59,76,101,102,116,84,114,105,97,110,103,108,101,66,97,114, + 59,76,101,102,116,84,114,105,97,110,103,108,101,69,113,117, + 97,108,59,76,101,102,116,85,112,68,111,119,110,86,101,99, + 116,111,114,59,76,101,102,116,85,112,84,101,101,86,101,99, + 116,111,114,59,76,101,102,116,85,112,86,101,99,116,111,114, + 59,76,101,102,116,85,112,86,101,99,116,111,114,66,97,114, + 59,76,101,102,116,86,101,99,116,111,114,59,76,101,102,116, + 86,101,99,116,111,114,66,97,114,59,76,101,102,116,97,114, + 114,111,119,59,76,101,102,116,114,105,103,104,116,97,114,114, + 111,119,59,76,101,115,115,69,113,117,97,108,71,114,101,97, + 116,101,114,59,76,101,115,115,70,117,108,108,69,113,117,97, + 108,59,76,101,115,115,71,114,101,97,116,101,114,59,76,101, + 115,115,76,101,115,115,59,76,101,115,115,83,108,97,110,116, + 69,113,117,97,108,59,76,101,115,115,84,105,108,100,101,59, + 76,102,114,59,76,108,59,76,108,101,102,116,97,114,114,111, + 119,59,76,109,105,100,111,116,59,76,111,110,103,76,101,102, + 116,65,114,114,111,119,59,76,111,110,103,76,101,102,116,82, + 105,103,104,116,65,114,114,111,119,59,76,111,110,103,82,105, + 103,104,116,65,114,114,111,119,59,76,111,110,103,108,101,102, + 116,97,114,114,111,119,59,76,111,110,103,108,101,102,116,114, + 105,103,104,116,97,114,114,111,119,59,76,111,110,103,114,105, + 103,104,116,97,114,114,111,119,59,76,111,112,102,59,76,111, + 119,101,114,76,101,102,116,65,114,114,111,119,59,76,111,119, + 101,114,82,105,103,104,116,65,114,114,111,119,59,76,115,99, + 114,59,76,115,104,59,76,115,116,114,111,107,59,76,116,59, + 77,97,112,59,77,99,121,59,77,101,100,105,117,109,83,112, + 97,99,101,59,77,101,108,108,105,110,116,114,102,59,77,102, + 114,59,77,105,110,117,115,80,108,117,115,59,77,111,112,102, + 59,77,115,99,114,59,77,117,59,78,74,99,121,59,78,97, + 99,117,116,101,59,78,99,97,114,111,110,59,78,99,101,100, + 105,108,59,78,99,121,59,78,101,103,97,116,105,118,101,77, + 101,100,105,117,109,83,112,97,99,101,59,78,101,103,97,116, + 105,118,101,84,104,105,99,107,83,112,97,99,101,59,78,101, + 103,97,116,105,118,101,84,104,105,110,83,112,97,99,101,59, + 78,101,103,97,116,105,118,101,86,101,114,121,84,104,105,110, + 83,112,97,99,101,59,78,101,115,116,101,100,71,114,101,97, + 116,101,114,71,114,101,97,116,101,114,59,78,101,115,116,101, + 100,76,101,115,115,76,101,115,115,59,78,101,119,76,105,110, + 101,59,78,102,114,59,78,111,66,114,101,97,107,59,78,111, + 110,66,114,101,97,107,105,110,103,83,112,97,99,101,59,78, + 111,112,102,59,78,111,116,59,78,111,116,67,111,110,103,114, + 117,101,110,116,59,78,111,116,67,117,112,67,97,112,59,78, + 111,116,68,111,117,98,108,101,86,101,114,116,105,99,97,108, + 66,97,114,59,78,111,116,69,108,101,109,101,110,116,59,78, + 111,116,69,113,117,97,108,59,78,111,116,69,113,117,97,108, + 84,105,108,100,101,59,78,111,116,69,120,105,115,116,115,59, + 78,111,116,71,114,101,97,116,101,114,59,78,111,116,71,114, + 101,97,116,101,114,69,113,117,97,108,59,78,111,116,71,114, + 101,97,116,101,114,70,117,108,108,69,113,117,97,108,59,78, + 111,116,71,114,101,97,116,101,114,71,114,101,97,116,101,114, + 59,78,111,116,71,114,101,97,116,101,114,76,101,115,115,59, + 78,111,116,71,114,101,97,116,101,114,83,108,97,110,116,69, + 113,117,97,108,59,78,111,116,71,114,101,97,116,101,114,84, + 105,108,100,101,59,78,111,116,72,117,109,112,68,111,119,110, + 72,117,109,112,59,78,111,116,72,117,109,112,69,113,117,97, + 108,59,78,111,116,76,101,102,116,84,114,105,97,110,103,108, + 101,59,78,111,116,76,101,102,116,84,114,105,97,110,103,108, + 101,66,97,114,59,78,111,116,76,101,102,116,84,114,105,97, + 110,103,108,101,69,113,117,97,108,59,78,111,116,76,101,115, + 115,59,78,111,116,76,101,115,115,69,113,117,97,108,59,78, + 111,116,76,101,115,115,71,114,101,97,116,101,114,59,78,111, + 116,76,101,115,115,76,101,115,115,59,78,111,116,76,101,115, + 115,83,108,97,110,116,69,113,117,97,108,59,78,111,116,76, + 101,115,115,84,105,108,100,101,59,78,111,116,78,101,115,116, + 101,100,71,114,101,97,116,101,114,71,114,101,97,116,101,114, + 59,78,111,116,78,101,115,116,101,100,76,101,115,115,76,101, + 115,115,59,78,111,116,80,114,101,99,101,100,101,115,59,78, + 111,116,80,114,101,99,101,100,101,115,69,113,117,97,108,59, + 78,111,116,80,114,101,99,101,100,101,115,83,108,97,110,116, + 69,113,117,97,108,59,78,111,116,82,101,118,101,114,115,101, + 69,108,101,109,101,110,116,59,78,111,116,82,105,103,104,116, + 84,114,105,97,110,103,108,101,59,78,111,116,82,105,103,104, + 116,84,114,105,97,110,103,108,101,66,97,114,59,78,111,116, + 82,105,103,104,116,84,114,105,97,110,103,108,101,69,113,117, + 97,108,59,78,111,116,83,113,117,97,114,101,83,117,98,115, + 101,116,59,78,111,116,83,113,117,97,114,101,83,117,98,115, + 101,116,69,113,117,97,108,59,78,111,116,83,113,117,97,114, + 101,83,117,112,101,114,115,101,116,59,78,111,116,83,113,117, + 97,114,101,83,117,112,101,114,115,101,116,69,113,117,97,108, + 59,78,111,116,83,117,98,115,101,116,59,78,111,116,83,117, + 98,115,101,116,69,113,117,97,108,59,78,111,116,83,117,99, + 99,101,101,100,115,59,78,111,116,83,117,99,99,101,101,100, + 115,69,113,117,97,108,59,78,111,116,83,117,99,99,101,101, + 100,115,83,108,97,110,116,69,113,117,97,108,59,78,111,116, + 83,117,99,99,101,101,100,115,84,105,108,100,101,59,78,111, + 116,83,117,112,101,114,115,101,116,59,78,111,116,83,117,112, + 101,114,115,101,116,69,113,117,97,108,59,78,111,116,84,105, + 108,100,101,59,78,111,116,84,105,108,100,101,69,113,117,97, + 108,59,78,111,116,84,105,108,100,101,70,117,108,108,69,113, + 117,97,108,59,78,111,116,84,105,108,100,101,84,105,108,100, + 101,59,78,111,116,86,101,114,116,105,99,97,108,66,97,114, + 59,78,115,99,114,59,78,116,105,108,100,101,78,116,105,108, + 100,101,59,78,117,59,79,69,108,105,103,59,79,97,99,117, + 116,101,79,97,99,117,116,101,59,79,99,105,114,99,79,99, + 105,114,99,59,79,99,121,59,79,100,98,108,97,99,59,79, + 102,114,59,79,103,114,97,118,101,79,103,114,97,118,101,59, + 79,109,97,99,114,59,79,109,101,103,97,59,79,109,105,99, + 114,111,110,59,79,111,112,102,59,79,112,101,110,67,117,114, + 108,121,68,111,117,98,108,101,81,117,111,116,101,59,79,112, + 101,110,67,117,114,108,121,81,117,111,116,101,59,79,114,59, + 79,115,99,114,59,79,115,108,97,115,104,79,115,108,97,115, + 104,59,79,116,105,108,100,101,79,116,105,108,100,101,59,79, + 116,105,109,101,115,59,79,117,109,108,79,117,109,108,59,79, + 118,101,114,66,97,114,59,79,118,101,114,66,114,97,99,101, + 59,79,118,101,114,66,114,97,99,107,101,116,59,79,118,101, + 114,80,97,114,101,110,116,104,101,115,105,115,59,80,97,114, + 116,105,97,108,68,59,80,99,121,59,80,102,114,59,80,104, + 105,59,80,105,59,80,108,117,115,77,105,110,117,115,59,80, + 111,105,110,99,97,114,101,112,108,97,110,101,59,80,111,112, + 102,59,80,114,59,80,114,101,99,101,100,101,115,59,80,114, + 101,99,101,100,101,115,69,113,117,97,108,59,80,114,101,99, + 101,100,101,115,83,108,97,110,116,69,113,117,97,108,59,80, + 114,101,99,101,100,101,115,84,105,108,100,101,59,80,114,105, + 109,101,59,80,114,111,100,117,99,116,59,80,114,111,112,111, + 114,116,105,111,110,59,80,114,111,112,111,114,116,105,111,110, + 97,108,59,80,115,99,114,59,80,115,105,59,81,85,79,84, + 81,85,79,84,59,81,102,114,59,81,111,112,102,59,81,115, + 99,114,59,82,66,97,114,114,59,82,69,71,82,69,71,59, + 82,97,99,117,116,101,59,82,97,110,103,59,82,97,114,114, + 59,82,97,114,114,116,108,59,82,99,97,114,111,110,59,82, + 99,101,100,105,108,59,82,99,121,59,82,101,59,82,101,118, + 101,114,115,101,69,108,101,109,101,110,116,59,82,101,118,101, + 114,115,101,69,113,117,105,108,105,98,114,105,117,109,59,82, + 101,118,101,114,115,101,85,112,69,113,117,105,108,105,98,114, + 105,117,109,59,82,102,114,59,82,104,111,59,82,105,103,104, + 116,65,110,103,108,101,66,114,97,99,107,101,116,59,82,105, + 103,104,116,65,114,114,111,119,59,82,105,103,104,116,65,114, + 114,111,119,66,97,114,59,82,105,103,104,116,65,114,114,111, + 119,76,101,102,116,65,114,114,111,119,59,82,105,103,104,116, + 67,101,105,108,105,110,103,59,82,105,103,104,116,68,111,117, + 98,108,101,66,114,97,99,107,101,116,59,82,105,103,104,116, + 68,111,119,110,84,101,101,86,101,99,116,111,114,59,82,105, + 103,104,116,68,111,119,110,86,101,99,116,111,114,59,82,105, + 103,104,116,68,111,119,110,86,101,99,116,111,114,66,97,114, + 59,82,105,103,104,116,70,108,111,111,114,59,82,105,103,104, + 116,84,101,101,59,82,105,103,104,116,84,101,101,65,114,114, + 111,119,59,82,105,103,104,116,84,101,101,86,101,99,116,111, + 114,59,82,105,103,104,116,84,114,105,97,110,103,108,101,59, + 82,105,103,104,116,84,114,105,97,110,103,108,101,66,97,114, + 59,82,105,103,104,116,84,114,105,97,110,103,108,101,69,113, + 117,97,108,59,82,105,103,104,116,85,112,68,111,119,110,86, + 101,99,116,111,114,59,82,105,103,104,116,85,112,84,101,101, + 86,101,99,116,111,114,59,82,105,103,104,116,85,112,86,101, + 99,116,111,114,59,82,105,103,104,116,85,112,86,101,99,116, + 111,114,66,97,114,59,82,105,103,104,116,86,101,99,116,111, + 114,59,82,105,103,104,116,86,101,99,116,111,114,66,97,114, + 59,82,105,103,104,116,97,114,114,111,119,59,82,111,112,102, + 59,82,111,117,110,100,73,109,112,108,105,101,115,59,82,114, + 105,103,104,116,97,114,114,111,119,59,82,115,99,114,59,82, + 115,104,59,82,117,108,101,68,101,108,97,121,101,100,59,83, + 72,67,72,99,121,59,83,72,99,121,59,83,79,70,84,99, + 121,59,83,97,99,117,116,101,59,83,99,59,83,99,97,114, + 111,110,59,83,99,101,100,105,108,59,83,99,105,114,99,59, + 83,99,121,59,83,102,114,59,83,104,111,114,116,68,111,119, + 110,65,114,114,111,119,59,83,104,111,114,116,76,101,102,116, + 65,114,114,111,119,59,83,104,111,114,116,82,105,103,104,116, + 65,114,114,111,119,59,83,104,111,114,116,85,112,65,114,114, + 111,119,59,83,105,103,109,97,59,83,109,97,108,108,67,105, + 114,99,108,101,59,83,111,112,102,59,83,113,114,116,59,83, + 113,117,97,114,101,59,83,113,117,97,114,101,73,110,116,101, + 114,115,101,99,116,105,111,110,59,83,113,117,97,114,101,83, + 117,98,115,101,116,59,83,113,117,97,114,101,83,117,98,115, + 101,116,69,113,117,97,108,59,83,113,117,97,114,101,83,117, + 112,101,114,115,101,116,59,83,113,117,97,114,101,83,117,112, + 101,114,115,101,116,69,113,117,97,108,59,83,113,117,97,114, + 101,85,110,105,111,110,59,83,115,99,114,59,83,116,97,114, + 59,83,117,98,59,83,117,98,115,101,116,59,83,117,98,115, + 101,116,69,113,117,97,108,59,83,117,99,99,101,101,100,115, + 59,83,117,99,99,101,101,100,115,69,113,117,97,108,59,83, + 117,99,99,101,101,100,115,83,108,97,110,116,69,113,117,97, + 108,59,83,117,99,99,101,101,100,115,84,105,108,100,101,59, + 83,117,99,104,84,104,97,116,59,83,117,109,59,83,117,112, + 59,83,117,112,101,114,115,101,116,59,83,117,112,101,114,115, + 101,116,69,113,117,97,108,59,83,117,112,115,101,116,59,84, + 72,79,82,78,84,72,79,82,78,59,84,82,65,68,69,59, + 84,83,72,99,121,59,84,83,99,121,59,84,97,98,59,84, + 97,117,59,84,99,97,114,111,110,59,84,99,101,100,105,108, + 59,84,99,121,59,84,102,114,59,84,104,101,114,101,102,111, + 114,101,59,84,104,101,116,97,59,84,104,105,99,107,83,112, + 97,99,101,59,84,104,105,110,83,112,97,99,101,59,84,105, + 108,100,101,59,84,105,108,100,101,69,113,117,97,108,59,84, + 105,108,100,101,70,117,108,108,69,113,117,97,108,59,84,105, + 108,100,101,84,105,108,100,101,59,84,111,112,102,59,84,114, + 105,112,108,101,68,111,116,59,84,115,99,114,59,84,115,116, + 114,111,107,59,85,97,99,117,116,101,85,97,99,117,116,101, + 59,85,97,114,114,59,85,97,114,114,111,99,105,114,59,85, + 98,114,99,121,59,85,98,114,101,118,101,59,85,99,105,114, + 99,85,99,105,114,99,59,85,99,121,59,85,100,98,108,97, + 99,59,85,102,114,59,85,103,114,97,118,101,85,103,114,97, + 118,101,59,85,109,97,99,114,59,85,110,100,101,114,66,97, + 114,59,85,110,100,101,114,66,114,97,99,101,59,85,110,100, + 101,114,66,114,97,99,107,101,116,59,85,110,100,101,114,80, + 97,114,101,110,116,104,101,115,105,115,59,85,110,105,111,110, + 59,85,110,105,111,110,80,108,117,115,59,85,111,103,111,110, + 59,85,111,112,102,59,85,112,65,114,114,111,119,59,85,112, + 65,114,114,111,119,66,97,114,59,85,112,65,114,114,111,119, + 68,111,119,110,65,114,114,111,119,59,85,112,68,111,119,110, + 65,114,114,111,119,59,85,112,69,113,117,105,108,105,98,114, + 105,117,109,59,85,112,84,101,101,59,85,112,84,101,101,65, + 114,114,111,119,59,85,112,97,114,114,111,119,59,85,112,100, + 111,119,110,97,114,114,111,119,59,85,112,112,101,114,76,101, + 102,116,65,114,114,111,119,59,85,112,112,101,114,82,105,103, + 104,116,65,114,114,111,119,59,85,112,115,105,59,85,112,115, + 105,108,111,110,59,85,114,105,110,103,59,85,115,99,114,59, + 85,116,105,108,100,101,59,85,117,109,108,85,117,109,108,59, + 86,68,97,115,104,59,86,98,97,114,59,86,99,121,59,86, + 100,97,115,104,59,86,100,97,115,104,108,59,86,101,101,59, + 86,101,114,98,97,114,59,86,101,114,116,59,86,101,114,116, + 105,99,97,108,66,97,114,59,86,101,114,116,105,99,97,108, + 76,105,110,101,59,86,101,114,116,105,99,97,108,83,101,112, + 97,114,97,116,111,114,59,86,101,114,116,105,99,97,108,84, + 105,108,100,101,59,86,101,114,121,84,104,105,110,83,112,97, + 99,101,59,86,102,114,59,86,111,112,102,59,86,115,99,114, + 59,86,118,100,97,115,104,59,87,99,105,114,99,59,87,101, + 100,103,101,59,87,102,114,59,87,111,112,102,59,87,115,99, + 114,59,88,102,114,59,88,105,59,88,111,112,102,59,88,115, + 99,114,59,89,65,99,121,59,89,73,99,121,59,89,85,99, + 121,59,89,97,99,117,116,101,89,97,99,117,116,101,59,89, + 99,105,114,99,59,89,99,121,59,89,102,114,59,89,111,112, + 102,59,89,115,99,114,59,89,117,109,108,59,90,72,99,121, + 59,90,97,99,117,116,101,59,90,99,97,114,111,110,59,90, + 99,121,59,90,100,111,116,59,90,101,114,111,87,105,100,116, + 104,83,112,97,99,101,59,90,101,116,97,59,90,102,114,59, + 90,111,112,102,59,90,115,99,114,59,97,97,99,117,116,101, + 97,97,99,117,116,101,59,97,98,114,101,118,101,59,97,99, + 59,97,99,69,59,97,99,100,59,97,99,105,114,99,97,99, + 105,114,99,59,97,99,117,116,101,97,99,117,116,101,59,97, + 99,121,59,97,101,108,105,103,97,101,108,105,103,59,97,102, + 59,97,102,114,59,97,103,114,97,118,101,97,103,114,97,118, + 101,59,97,108,101,102,115,121,109,59,97,108,101,112,104,59, + 97,108,112,104,97,59,97,109,97,99,114,59,97,109,97,108, + 103,59,97,109,112,97,109,112,59,97,110,100,59,97,110,100, + 97,110,100,59,97,110,100,100,59,97,110,100,115,108,111,112, + 101,59,97,110,100,118,59,97,110,103,59,97,110,103,101,59, + 97,110,103,108,101,59,97,110,103,109,115,100,59,97,110,103, + 109,115,100,97,97,59,97,110,103,109,115,100,97,98,59,97, + 110,103,109,115,100,97,99,59,97,110,103,109,115,100,97,100, + 59,97,110,103,109,115,100,97,101,59,97,110,103,109,115,100, + 97,102,59,97,110,103,109,115,100,97,103,59,97,110,103,109, + 115,100,97,104,59,97,110,103,114,116,59,97,110,103,114,116, + 118,98,59,97,110,103,114,116,118,98,100,59,97,110,103,115, + 112,104,59,97,110,103,115,116,59,97,110,103,122,97,114,114, + 59,97,111,103,111,110,59,97,111,112,102,59,97,112,59,97, + 112,69,59,97,112,97,99,105,114,59,97,112,101,59,97,112, + 105,100,59,97,112,111,115,59,97,112,112,114,111,120,59,97, + 112,112,114,111,120,101,113,59,97,114,105,110,103,97,114,105, + 110,103,59,97,115,99,114,59,97,115,116,59,97,115,121,109, + 112,59,97,115,121,109,112,101,113,59,97,116,105,108,100,101, + 97,116,105,108,100,101,59,97,117,109,108,97,117,109,108,59, + 97,119,99,111,110,105,110,116,59,97,119,105,110,116,59,98, + 78,111,116,59,98,97,99,107,99,111,110,103,59,98,97,99, + 107,101,112,115,105,108,111,110,59,98,97,99,107,112,114,105, + 109,101,59,98,97,99,107,115,105,109,59,98,97,99,107,115, + 105,109,101,113,59,98,97,114,118,101,101,59,98,97,114,119, + 101,100,59,98,97,114,119,101,100,103,101,59,98,98,114,107, + 59,98,98,114,107,116,98,114,107,59,98,99,111,110,103,59, + 98,99,121,59,98,100,113,117,111,59,98,101,99,97,117,115, + 59,98,101,99,97,117,115,101,59,98,101,109,112,116,121,118, + 59,98,101,112,115,105,59,98,101,114,110,111,117,59,98,101, + 116,97,59,98,101,116,104,59,98,101,116,119,101,101,110,59, + 98,102,114,59,98,105,103,99,97,112,59,98,105,103,99,105, + 114,99,59,98,105,103,99,117,112,59,98,105,103,111,100,111, + 116,59,98,105,103,111,112,108,117,115,59,98,105,103,111,116, + 105,109,101,115,59,98,105,103,115,113,99,117,112,59,98,105, + 103,115,116,97,114,59,98,105,103,116,114,105,97,110,103,108, + 101,100,111,119,110,59,98,105,103,116,114,105,97,110,103,108, + 101,117,112,59,98,105,103,117,112,108,117,115,59,98,105,103, + 118,101,101,59,98,105,103,119,101,100,103,101,59,98,107,97, + 114,111,119,59,98,108,97,99,107,108,111,122,101,110,103,101, + 59,98,108,97,99,107,115,113,117,97,114,101,59,98,108,97, + 99,107,116,114,105,97,110,103,108,101,59,98,108,97,99,107, + 116,114,105,97,110,103,108,101,100,111,119,110,59,98,108,97, + 99,107,116,114,105,97,110,103,108,101,108,101,102,116,59,98, + 108,97,99,107,116,114,105,97,110,103,108,101,114,105,103,104, + 116,59,98,108,97,110,107,59,98,108,107,49,50,59,98,108, + 107,49,52,59,98,108,107,51,52,59,98,108,111,99,107,59, + 98,110,101,59,98,110,101,113,117,105,118,59,98,110,111,116, + 59,98,111,112,102,59,98,111,116,59,98,111,116,116,111,109, + 59,98,111,119,116,105,101,59,98,111,120,68,76,59,98,111, + 120,68,82,59,98,111,120,68,108,59,98,111,120,68,114,59, + 98,111,120,72,59,98,111,120,72,68,59,98,111,120,72,85, + 59,98,111,120,72,100,59,98,111,120,72,117,59,98,111,120, + 85,76,59,98,111,120,85,82,59,98,111,120,85,108,59,98, + 111,120,85,114,59,98,111,120,86,59,98,111,120,86,72,59, + 98,111,120,86,76,59,98,111,120,86,82,59,98,111,120,86, + 104,59,98,111,120,86,108,59,98,111,120,86,114,59,98,111, + 120,98,111,120,59,98,111,120,100,76,59,98,111,120,100,82, + 59,98,111,120,100,108,59,98,111,120,100,114,59,98,111,120, + 104,59,98,111,120,104,68,59,98,111,120,104,85,59,98,111, + 120,104,100,59,98,111,120,104,117,59,98,111,120,109,105,110, + 117,115,59,98,111,120,112,108,117,115,59,98,111,120,116,105, + 109,101,115,59,98,111,120,117,76,59,98,111,120,117,82,59, + 98,111,120,117,108,59,98,111,120,117,114,59,98,111,120,118, + 59,98,111,120,118,72,59,98,111,120,118,76,59,98,111,120, + 118,82,59,98,111,120,118,104,59,98,111,120,118,108,59,98, + 111,120,118,114,59,98,112,114,105,109,101,59,98,114,101,118, + 101,59,98,114,118,98,97,114,98,114,118,98,97,114,59,98, + 115,99,114,59,98,115,101,109,105,59,98,115,105,109,59,98, + 115,105,109,101,59,98,115,111,108,59,98,115,111,108,98,59, + 98,115,111,108,104,115,117,98,59,98,117,108,108,59,98,117, + 108,108,101,116,59,98,117,109,112,59,98,117,109,112,69,59, + 98,117,109,112,101,59,98,117,109,112,101,113,59,99,97,99, + 117,116,101,59,99,97,112,59,99,97,112,97,110,100,59,99, + 97,112,98,114,99,117,112,59,99,97,112,99,97,112,59,99, + 97,112,99,117,112,59,99,97,112,100,111,116,59,99,97,112, + 115,59,99,97,114,101,116,59,99,97,114,111,110,59,99,99, + 97,112,115,59,99,99,97,114,111,110,59,99,99,101,100,105, + 108,99,99,101,100,105,108,59,99,99,105,114,99,59,99,99, + 117,112,115,59,99,99,117,112,115,115,109,59,99,100,111,116, + 59,99,101,100,105,108,99,101,100,105,108,59,99,101,109,112, + 116,121,118,59,99,101,110,116,99,101,110,116,59,99,101,110, + 116,101,114,100,111,116,59,99,102,114,59,99,104,99,121,59, + 99,104,101,99,107,59,99,104,101,99,107,109,97,114,107,59, + 99,104,105,59,99,105,114,59,99,105,114,69,59,99,105,114, + 99,59,99,105,114,99,101,113,59,99,105,114,99,108,101,97, + 114,114,111,119,108,101,102,116,59,99,105,114,99,108,101,97, + 114,114,111,119,114,105,103,104,116,59,99,105,114,99,108,101, + 100,82,59,99,105,114,99,108,101,100,83,59,99,105,114,99, + 108,101,100,97,115,116,59,99,105,114,99,108,101,100,99,105, + 114,99,59,99,105,114,99,108,101,100,100,97,115,104,59,99, + 105,114,101,59,99,105,114,102,110,105,110,116,59,99,105,114, + 109,105,100,59,99,105,114,115,99,105,114,59,99,108,117,98, + 115,59,99,108,117,98,115,117,105,116,59,99,111,108,111,110, + 59,99,111,108,111,110,101,59,99,111,108,111,110,101,113,59, + 99,111,109,109,97,59,99,111,109,109,97,116,59,99,111,109, + 112,59,99,111,109,112,102,110,59,99,111,109,112,108,101,109, + 101,110,116,59,99,111,109,112,108,101,120,101,115,59,99,111, + 110,103,59,99,111,110,103,100,111,116,59,99,111,110,105,110, + 116,59,99,111,112,102,59,99,111,112,114,111,100,59,99,111, + 112,121,99,111,112,121,59,99,111,112,121,115,114,59,99,114, + 97,114,114,59,99,114,111,115,115,59,99,115,99,114,59,99, + 115,117,98,59,99,115,117,98,101,59,99,115,117,112,59,99, + 115,117,112,101,59,99,116,100,111,116,59,99,117,100,97,114, + 114,108,59,99,117,100,97,114,114,114,59,99,117,101,112,114, + 59,99,117,101,115,99,59,99,117,108,97,114,114,59,99,117, + 108,97,114,114,112,59,99,117,112,59,99,117,112,98,114,99, + 97,112,59,99,117,112,99,97,112,59,99,117,112,99,117,112, + 59,99,117,112,100,111,116,59,99,117,112,111,114,59,99,117, + 112,115,59,99,117,114,97,114,114,59,99,117,114,97,114,114, + 109,59,99,117,114,108,121,101,113,112,114,101,99,59,99,117, + 114,108,121,101,113,115,117,99,99,59,99,117,114,108,121,118, + 101,101,59,99,117,114,108,121,119,101,100,103,101,59,99,117, + 114,114,101,110,99,117,114,114,101,110,59,99,117,114,118,101, + 97,114,114,111,119,108,101,102,116,59,99,117,114,118,101,97, + 114,114,111,119,114,105,103,104,116,59,99,117,118,101,101,59, + 99,117,119,101,100,59,99,119,99,111,110,105,110,116,59,99, + 119,105,110,116,59,99,121,108,99,116,121,59,100,65,114,114, + 59,100,72,97,114,59,100,97,103,103,101,114,59,100,97,108, + 101,116,104,59,100,97,114,114,59,100,97,115,104,59,100,97, + 115,104,118,59,100,98,107,97,114,111,119,59,100,98,108,97, + 99,59,100,99,97,114,111,110,59,100,99,121,59,100,100,59, + 100,100,97,103,103,101,114,59,100,100,97,114,114,59,100,100, + 111,116,115,101,113,59,100,101,103,100,101,103,59,100,101,108, + 116,97,59,100,101,109,112,116,121,118,59,100,102,105,115,104, + 116,59,100,102,114,59,100,104,97,114,108,59,100,104,97,114, + 114,59,100,105,97,109,59,100,105,97,109,111,110,100,59,100, + 105,97,109,111,110,100,115,117,105,116,59,100,105,97,109,115, + 59,100,105,101,59,100,105,103,97,109,109,97,59,100,105,115, + 105,110,59,100,105,118,59,100,105,118,105,100,101,100,105,118, + 105,100,101,59,100,105,118,105,100,101,111,110,116,105,109,101, + 115,59,100,105,118,111,110,120,59,100,106,99,121,59,100,108, + 99,111,114,110,59,100,108,99,114,111,112,59,100,111,108,108, + 97,114,59,100,111,112,102,59,100,111,116,59,100,111,116,101, + 113,59,100,111,116,101,113,100,111,116,59,100,111,116,109,105, + 110,117,115,59,100,111,116,112,108,117,115,59,100,111,116,115, + 113,117,97,114,101,59,100,111,117,98,108,101,98,97,114,119, + 101,100,103,101,59,100,111,119,110,97,114,114,111,119,59,100, + 111,119,110,100,111,119,110,97,114,114,111,119,115,59,100,111, + 119,110,104,97,114,112,111,111,110,108,101,102,116,59,100,111, + 119,110,104,97,114,112,111,111,110,114,105,103,104,116,59,100, + 114,98,107,97,114,111,119,59,100,114,99,111,114,110,59,100, + 114,99,114,111,112,59,100,115,99,114,59,100,115,99,121,59, + 100,115,111,108,59,100,115,116,114,111,107,59,100,116,100,111, + 116,59,100,116,114,105,59,100,116,114,105,102,59,100,117,97, + 114,114,59,100,117,104,97,114,59,100,119,97,110,103,108,101, + 59,100,122,99,121,59,100,122,105,103,114,97,114,114,59,101, + 68,68,111,116,59,101,68,111,116,59,101,97,99,117,116,101, + 101,97,99,117,116,101,59,101,97,115,116,101,114,59,101,99, + 97,114,111,110,59,101,99,105,114,59,101,99,105,114,99,101, + 99,105,114,99,59,101,99,111,108,111,110,59,101,99,121,59, + 101,100,111,116,59,101,101,59,101,102,68,111,116,59,101,102, + 114,59,101,103,59,101,103,114,97,118,101,101,103,114,97,118, + 101,59,101,103,115,59,101,103,115,100,111,116,59,101,108,59, + 101,108,105,110,116,101,114,115,59,101,108,108,59,101,108,115, + 59,101,108,115,100,111,116,59,101,109,97,99,114,59,101,109, + 112,116,121,59,101,109,112,116,121,115,101,116,59,101,109,112, + 116,121,118,59,101,109,115,112,49,51,59,101,109,115,112,49, + 52,59,101,109,115,112,59,101,110,103,59,101,110,115,112,59, + 101,111,103,111,110,59,101,111,112,102,59,101,112,97,114,59, + 101,112,97,114,115,108,59,101,112,108,117,115,59,101,112,115, + 105,59,101,112,115,105,108,111,110,59,101,112,115,105,118,59, + 101,113,99,105,114,99,59,101,113,99,111,108,111,110,59,101, + 113,115,105,109,59,101,113,115,108,97,110,116,103,116,114,59, + 101,113,115,108,97,110,116,108,101,115,115,59,101,113,117,97, + 108,115,59,101,113,117,101,115,116,59,101,113,117,105,118,59, + 101,113,117,105,118,68,68,59,101,113,118,112,97,114,115,108, + 59,101,114,68,111,116,59,101,114,97,114,114,59,101,115,99, + 114,59,101,115,100,111,116,59,101,115,105,109,59,101,116,97, + 59,101,116,104,101,116,104,59,101,117,109,108,101,117,109,108, + 59,101,117,114,111,59,101,120,99,108,59,101,120,105,115,116, + 59,101,120,112,101,99,116,97,116,105,111,110,59,101,120,112, + 111,110,101,110,116,105,97,108,101,59,102,97,108,108,105,110, + 103,100,111,116,115,101,113,59,102,99,121,59,102,101,109,97, + 108,101,59,102,102,105,108,105,103,59,102,102,108,105,103,59, + 102,102,108,108,105,103,59,102,102,114,59,102,105,108,105,103, + 59,102,106,108,105,103,59,102,108,97,116,59,102,108,108,105, + 103,59,102,108,116,110,115,59,102,110,111,102,59,102,111,112, + 102,59,102,111,114,97,108,108,59,102,111,114,107,59,102,111, + 114,107,118,59,102,112,97,114,116,105,110,116,59,102,114,97, + 99,49,50,102,114,97,99,49,50,59,102,114,97,99,49,51, + 59,102,114,97,99,49,52,102,114,97,99,49,52,59,102,114, + 97,99,49,53,59,102,114,97,99,49,54,59,102,114,97,99, + 49,56,59,102,114,97,99,50,51,59,102,114,97,99,50,53, + 59,102,114,97,99,51,52,102,114,97,99,51,52,59,102,114, + 97,99,51,53,59,102,114,97,99,51,56,59,102,114,97,99, + 52,53,59,102,114,97,99,53,54,59,102,114,97,99,53,56, + 59,102,114,97,99,55,56,59,102,114,97,115,108,59,102,114, + 111,119,110,59,102,115,99,114,59,103,69,59,103,69,108,59, + 103,97,99,117,116,101,59,103,97,109,109,97,59,103,97,109, + 109,97,100,59,103,97,112,59,103,98,114,101,118,101,59,103, + 99,105,114,99,59,103,99,121,59,103,100,111,116,59,103,101, + 59,103,101,108,59,103,101,113,59,103,101,113,113,59,103,101, + 113,115,108,97,110,116,59,103,101,115,59,103,101,115,99,99, + 59,103,101,115,100,111,116,59,103,101,115,100,111,116,111,59, + 103,101,115,100,111,116,111,108,59,103,101,115,108,59,103,101, + 115,108,101,115,59,103,102,114,59,103,103,59,103,103,103,59, + 103,105,109,101,108,59,103,106,99,121,59,103,108,59,103,108, + 69,59,103,108,97,59,103,108,106,59,103,110,69,59,103,110, + 97,112,59,103,110,97,112,112,114,111,120,59,103,110,101,59, + 103,110,101,113,59,103,110,101,113,113,59,103,110,115,105,109, + 59,103,111,112,102,59,103,114,97,118,101,59,103,115,99,114, + 59,103,115,105,109,59,103,115,105,109,101,59,103,115,105,109, + 108,59,103,116,103,116,59,103,116,99,99,59,103,116,99,105, + 114,59,103,116,100,111,116,59,103,116,108,80,97,114,59,103, + 116,113,117,101,115,116,59,103,116,114,97,112,112,114,111,120, + 59,103,116,114,97,114,114,59,103,116,114,100,111,116,59,103, + 116,114,101,113,108,101,115,115,59,103,116,114,101,113,113,108, + 101,115,115,59,103,116,114,108,101,115,115,59,103,116,114,115, + 105,109,59,103,118,101,114,116,110,101,113,113,59,103,118,110, + 69,59,104,65,114,114,59,104,97,105,114,115,112,59,104,97, + 108,102,59,104,97,109,105,108,116,59,104,97,114,100,99,121, + 59,104,97,114,114,59,104,97,114,114,99,105,114,59,104,97, + 114,114,119,59,104,98,97,114,59,104,99,105,114,99,59,104, + 101,97,114,116,115,59,104,101,97,114,116,115,117,105,116,59, + 104,101,108,108,105,112,59,104,101,114,99,111,110,59,104,102, + 114,59,104,107,115,101,97,114,111,119,59,104,107,115,119,97, + 114,111,119,59,104,111,97,114,114,59,104,111,109,116,104,116, + 59,104,111,111,107,108,101,102,116,97,114,114,111,119,59,104, + 111,111,107,114,105,103,104,116,97,114,114,111,119,59,104,111, + 112,102,59,104,111,114,98,97,114,59,104,115,99,114,59,104, + 115,108,97,115,104,59,104,115,116,114,111,107,59,104,121,98, + 117,108,108,59,104,121,112,104,101,110,59,105,97,99,117,116, + 101,105,97,99,117,116,101,59,105,99,59,105,99,105,114,99, + 105,99,105,114,99,59,105,99,121,59,105,101,99,121,59,105, + 101,120,99,108,105,101,120,99,108,59,105,102,102,59,105,102, + 114,59,105,103,114,97,118,101,105,103,114,97,118,101,59,105, + 105,59,105,105,105,105,110,116,59,105,105,105,110,116,59,105, + 105,110,102,105,110,59,105,105,111,116,97,59,105,106,108,105, + 103,59,105,109,97,99,114,59,105,109,97,103,101,59,105,109, + 97,103,108,105,110,101,59,105,109,97,103,112,97,114,116,59, + 105,109,97,116,104,59,105,109,111,102,59,105,109,112,101,100, + 59,105,110,59,105,110,99,97,114,101,59,105,110,102,105,110, + 59,105,110,102,105,110,116,105,101,59,105,110,111,100,111,116, + 59,105,110,116,59,105,110,116,99,97,108,59,105,110,116,101, + 103,101,114,115,59,105,110,116,101,114,99,97,108,59,105,110, + 116,108,97,114,104,107,59,105,110,116,112,114,111,100,59,105, + 111,99,121,59,105,111,103,111,110,59,105,111,112,102,59,105, + 111,116,97,59,105,112,114,111,100,59,105,113,117,101,115,116, + 105,113,117,101,115,116,59,105,115,99,114,59,105,115,105,110, + 59,105,115,105,110,69,59,105,115,105,110,100,111,116,59,105, + 115,105,110,115,59,105,115,105,110,115,118,59,105,115,105,110, + 118,59,105,116,59,105,116,105,108,100,101,59,105,117,107,99, + 121,59,105,117,109,108,105,117,109,108,59,106,99,105,114,99, + 59,106,99,121,59,106,102,114,59,106,109,97,116,104,59,106, + 111,112,102,59,106,115,99,114,59,106,115,101,114,99,121,59, + 106,117,107,99,121,59,107,97,112,112,97,59,107,97,112,112, + 97,118,59,107,99,101,100,105,108,59,107,99,121,59,107,102, + 114,59,107,103,114,101,101,110,59,107,104,99,121,59,107,106, + 99,121,59,107,111,112,102,59,107,115,99,114,59,108,65,97, + 114,114,59,108,65,114,114,59,108,65,116,97,105,108,59,108, + 66,97,114,114,59,108,69,59,108,69,103,59,108,72,97,114, + 59,108,97,99,117,116,101,59,108,97,101,109,112,116,121,118, + 59,108,97,103,114,97,110,59,108,97,109,98,100,97,59,108, + 97,110,103,59,108,97,110,103,100,59,108,97,110,103,108,101, + 59,108,97,112,59,108,97,113,117,111,108,97,113,117,111,59, + 108,97,114,114,59,108,97,114,114,98,59,108,97,114,114,98, + 102,115,59,108,97,114,114,102,115,59,108,97,114,114,104,107, + 59,108,97,114,114,108,112,59,108,97,114,114,112,108,59,108, + 97,114,114,115,105,109,59,108,97,114,114,116,108,59,108,97, + 116,59,108,97,116,97,105,108,59,108,97,116,101,59,108,97, + 116,101,115,59,108,98,97,114,114,59,108,98,98,114,107,59, + 108,98,114,97,99,101,59,108,98,114,97,99,107,59,108,98, + 114,107,101,59,108,98,114,107,115,108,100,59,108,98,114,107, + 115,108,117,59,108,99,97,114,111,110,59,108,99,101,100,105, + 108,59,108,99,101,105,108,59,108,99,117,98,59,108,99,121, + 59,108,100,99,97,59,108,100,113,117,111,59,108,100,113,117, + 111,114,59,108,100,114,100,104,97,114,59,108,100,114,117,115, + 104,97,114,59,108,100,115,104,59,108,101,59,108,101,102,116, + 97,114,114,111,119,59,108,101,102,116,97,114,114,111,119,116, + 97,105,108,59,108,101,102,116,104,97,114,112,111,111,110,100, + 111,119,110,59,108,101,102,116,104,97,114,112,111,111,110,117, + 112,59,108,101,102,116,108,101,102,116,97,114,114,111,119,115, + 59,108,101,102,116,114,105,103,104,116,97,114,114,111,119,59, + 108,101,102,116,114,105,103,104,116,97,114,114,111,119,115,59, + 108,101,102,116,114,105,103,104,116,104,97,114,112,111,111,110, + 115,59,108,101,102,116,114,105,103,104,116,115,113,117,105,103, + 97,114,114,111,119,59,108,101,102,116,116,104,114,101,101,116, + 105,109,101,115,59,108,101,103,59,108,101,113,59,108,101,113, + 113,59,108,101,113,115,108,97,110,116,59,108,101,115,59,108, + 101,115,99,99,59,108,101,115,100,111,116,59,108,101,115,100, + 111,116,111,59,108,101,115,100,111,116,111,114,59,108,101,115, + 103,59,108,101,115,103,101,115,59,108,101,115,115,97,112,112, + 114,111,120,59,108,101,115,115,100,111,116,59,108,101,115,115, + 101,113,103,116,114,59,108,101,115,115,101,113,113,103,116,114, + 59,108,101,115,115,103,116,114,59,108,101,115,115,115,105,109, + 59,108,102,105,115,104,116,59,108,102,108,111,111,114,59,108, + 102,114,59,108,103,59,108,103,69,59,108,104,97,114,100,59, + 108,104,97,114,117,59,108,104,97,114,117,108,59,108,104,98, + 108,107,59,108,106,99,121,59,108,108,59,108,108,97,114,114, + 59,108,108,99,111,114,110,101,114,59,108,108,104,97,114,100, + 59,108,108,116,114,105,59,108,109,105,100,111,116,59,108,109, + 111,117,115,116,59,108,109,111,117,115,116,97,99,104,101,59, + 108,110,69,59,108,110,97,112,59,108,110,97,112,112,114,111, + 120,59,108,110,101,59,108,110,101,113,59,108,110,101,113,113, + 59,108,110,115,105,109,59,108,111,97,110,103,59,108,111,97, + 114,114,59,108,111,98,114,107,59,108,111,110,103,108,101,102, + 116,97,114,114,111,119,59,108,111,110,103,108,101,102,116,114, + 105,103,104,116,97,114,114,111,119,59,108,111,110,103,109,97, + 112,115,116,111,59,108,111,110,103,114,105,103,104,116,97,114, + 114,111,119,59,108,111,111,112,97,114,114,111,119,108,101,102, + 116,59,108,111,111,112,97,114,114,111,119,114,105,103,104,116, + 59,108,111,112,97,114,59,108,111,112,102,59,108,111,112,108, + 117,115,59,108,111,116,105,109,101,115,59,108,111,119,97,115, + 116,59,108,111,119,98,97,114,59,108,111,122,59,108,111,122, + 101,110,103,101,59,108,111,122,102,59,108,112,97,114,59,108, + 112,97,114,108,116,59,108,114,97,114,114,59,108,114,99,111, + 114,110,101,114,59,108,114,104,97,114,59,108,114,104,97,114, + 100,59,108,114,109,59,108,114,116,114,105,59,108,115,97,113, + 117,111,59,108,115,99,114,59,108,115,104,59,108,115,105,109, + 59,108,115,105,109,101,59,108,115,105,109,103,59,108,115,113, + 98,59,108,115,113,117,111,59,108,115,113,117,111,114,59,108, + 115,116,114,111,107,59,108,116,108,116,59,108,116,99,99,59, + 108,116,99,105,114,59,108,116,100,111,116,59,108,116,104,114, + 101,101,59,108,116,105,109,101,115,59,108,116,108,97,114,114, + 59,108,116,113,117,101,115,116,59,108,116,114,80,97,114,59, + 108,116,114,105,59,108,116,114,105,101,59,108,116,114,105,102, + 59,108,117,114,100,115,104,97,114,59,108,117,114,117,104,97, + 114,59,108,118,101,114,116,110,101,113,113,59,108,118,110,69, + 59,109,68,68,111,116,59,109,97,99,114,109,97,99,114,59, + 109,97,108,101,59,109,97,108,116,59,109,97,108,116,101,115, + 101,59,109,97,112,59,109,97,112,115,116,111,59,109,97,112, + 115,116,111,100,111,119,110,59,109,97,112,115,116,111,108,101, + 102,116,59,109,97,112,115,116,111,117,112,59,109,97,114,107, + 101,114,59,109,99,111,109,109,97,59,109,99,121,59,109,100, + 97,115,104,59,109,101,97,115,117,114,101,100,97,110,103,108, + 101,59,109,102,114,59,109,104,111,59,109,105,99,114,111,109, + 105,99,114,111,59,109,105,100,59,109,105,100,97,115,116,59, + 109,105,100,99,105,114,59,109,105,100,100,111,116,109,105,100, + 100,111,116,59,109,105,110,117,115,59,109,105,110,117,115,98, + 59,109,105,110,117,115,100,59,109,105,110,117,115,100,117,59, + 109,108,99,112,59,109,108,100,114,59,109,110,112,108,117,115, + 59,109,111,100,101,108,115,59,109,111,112,102,59,109,112,59, + 109,115,99,114,59,109,115,116,112,111,115,59,109,117,59,109, + 117,108,116,105,109,97,112,59,109,117,109,97,112,59,110,71, + 103,59,110,71,116,59,110,71,116,118,59,110,76,101,102,116, + 97,114,114,111,119,59,110,76,101,102,116,114,105,103,104,116, + 97,114,114,111,119,59,110,76,108,59,110,76,116,59,110,76, + 116,118,59,110,82,105,103,104,116,97,114,114,111,119,59,110, + 86,68,97,115,104,59,110,86,100,97,115,104,59,110,97,98, + 108,97,59,110,97,99,117,116,101,59,110,97,110,103,59,110, + 97,112,59,110,97,112,69,59,110,97,112,105,100,59,110,97, + 112,111,115,59,110,97,112,112,114,111,120,59,110,97,116,117, + 114,59,110,97,116,117,114,97,108,59,110,97,116,117,114,97, + 108,115,59,110,98,115,112,110,98,115,112,59,110,98,117,109, + 112,59,110,98,117,109,112,101,59,110,99,97,112,59,110,99, + 97,114,111,110,59,110,99,101,100,105,108,59,110,99,111,110, + 103,59,110,99,111,110,103,100,111,116,59,110,99,117,112,59, + 110,99,121,59,110,100,97,115,104,59,110,101,59,110,101,65, + 114,114,59,110,101,97,114,104,107,59,110,101,97,114,114,59, + 110,101,97,114,114,111,119,59,110,101,100,111,116,59,110,101, + 113,117,105,118,59,110,101,115,101,97,114,59,110,101,115,105, + 109,59,110,101,120,105,115,116,59,110,101,120,105,115,116,115, + 59,110,102,114,59,110,103,69,59,110,103,101,59,110,103,101, + 113,59,110,103,101,113,113,59,110,103,101,113,115,108,97,110, + 116,59,110,103,101,115,59,110,103,115,105,109,59,110,103,116, + 59,110,103,116,114,59,110,104,65,114,114,59,110,104,97,114, + 114,59,110,104,112,97,114,59,110,105,59,110,105,115,59,110, + 105,115,100,59,110,105,118,59,110,106,99,121,59,110,108,65, + 114,114,59,110,108,69,59,110,108,97,114,114,59,110,108,100, + 114,59,110,108,101,59,110,108,101,102,116,97,114,114,111,119, + 59,110,108,101,102,116,114,105,103,104,116,97,114,114,111,119, + 59,110,108,101,113,59,110,108,101,113,113,59,110,108,101,113, + 115,108,97,110,116,59,110,108,101,115,59,110,108,101,115,115, + 59,110,108,115,105,109,59,110,108,116,59,110,108,116,114,105, + 59,110,108,116,114,105,101,59,110,109,105,100,59,110,111,112, + 102,59,110,111,116,110,111,116,59,110,111,116,105,110,59,110, + 111,116,105,110,69,59,110,111,116,105,110,100,111,116,59,110, + 111,116,105,110,118,97,59,110,111,116,105,110,118,98,59,110, + 111,116,105,110,118,99,59,110,111,116,110,105,59,110,111,116, + 110,105,118,97,59,110,111,116,110,105,118,98,59,110,111,116, + 110,105,118,99,59,110,112,97,114,59,110,112,97,114,97,108, + 108,101,108,59,110,112,97,114,115,108,59,110,112,97,114,116, + 59,110,112,111,108,105,110,116,59,110,112,114,59,110,112,114, + 99,117,101,59,110,112,114,101,59,110,112,114,101,99,59,110, + 112,114,101,99,101,113,59,110,114,65,114,114,59,110,114,97, + 114,114,59,110,114,97,114,114,99,59,110,114,97,114,114,119, + 59,110,114,105,103,104,116,97,114,114,111,119,59,110,114,116, + 114,105,59,110,114,116,114,105,101,59,110,115,99,59,110,115, + 99,99,117,101,59,110,115,99,101,59,110,115,99,114,59,110, + 115,104,111,114,116,109,105,100,59,110,115,104,111,114,116,112, + 97,114,97,108,108,101,108,59,110,115,105,109,59,110,115,105, + 109,101,59,110,115,105,109,101,113,59,110,115,109,105,100,59, + 110,115,112,97,114,59,110,115,113,115,117,98,101,59,110,115, + 113,115,117,112,101,59,110,115,117,98,59,110,115,117,98,69, + 59,110,115,117,98,101,59,110,115,117,98,115,101,116,59,110, + 115,117,98,115,101,116,101,113,59,110,115,117,98,115,101,116, + 101,113,113,59,110,115,117,99,99,59,110,115,117,99,99,101, + 113,59,110,115,117,112,59,110,115,117,112,69,59,110,115,117, + 112,101,59,110,115,117,112,115,101,116,59,110,115,117,112,115, + 101,116,101,113,59,110,115,117,112,115,101,116,101,113,113,59, + 110,116,103,108,59,110,116,105,108,100,101,110,116,105,108,100, + 101,59,110,116,108,103,59,110,116,114,105,97,110,103,108,101, + 108,101,102,116,59,110,116,114,105,97,110,103,108,101,108,101, + 102,116,101,113,59,110,116,114,105,97,110,103,108,101,114,105, + 103,104,116,59,110,116,114,105,97,110,103,108,101,114,105,103, + 104,116,101,113,59,110,117,59,110,117,109,59,110,117,109,101, + 114,111,59,110,117,109,115,112,59,110,118,68,97,115,104,59, + 110,118,72,97,114,114,59,110,118,97,112,59,110,118,100,97, + 115,104,59,110,118,103,101,59,110,118,103,116,59,110,118,105, + 110,102,105,110,59,110,118,108,65,114,114,59,110,118,108,101, + 59,110,118,108,116,59,110,118,108,116,114,105,101,59,110,118, + 114,65,114,114,59,110,118,114,116,114,105,101,59,110,118,115, + 105,109,59,110,119,65,114,114,59,110,119,97,114,104,107,59, + 110,119,97,114,114,59,110,119,97,114,114,111,119,59,110,119, + 110,101,97,114,59,111,83,59,111,97,99,117,116,101,111,97, + 99,117,116,101,59,111,97,115,116,59,111,99,105,114,59,111, + 99,105,114,99,111,99,105,114,99,59,111,99,121,59,111,100, + 97,115,104,59,111,100,98,108,97,99,59,111,100,105,118,59, + 111,100,111,116,59,111,100,115,111,108,100,59,111,101,108,105, + 103,59,111,102,99,105,114,59,111,102,114,59,111,103,111,110, + 59,111,103,114,97,118,101,111,103,114,97,118,101,59,111,103, + 116,59,111,104,98,97,114,59,111,104,109,59,111,105,110,116, + 59,111,108,97,114,114,59,111,108,99,105,114,59,111,108,99, + 114,111,115,115,59,111,108,105,110,101,59,111,108,116,59,111, + 109,97,99,114,59,111,109,101,103,97,59,111,109,105,99,114, + 111,110,59,111,109,105,100,59,111,109,105,110,117,115,59,111, + 111,112,102,59,111,112,97,114,59,111,112,101,114,112,59,111, + 112,108,117,115,59,111,114,59,111,114,97,114,114,59,111,114, + 100,59,111,114,100,101,114,59,111,114,100,101,114,111,102,59, + 111,114,100,102,111,114,100,102,59,111,114,100,109,111,114,100, + 109,59,111,114,105,103,111,102,59,111,114,111,114,59,111,114, + 115,108,111,112,101,59,111,114,118,59,111,115,99,114,59,111, + 115,108,97,115,104,111,115,108,97,115,104,59,111,115,111,108, + 59,111,116,105,108,100,101,111,116,105,108,100,101,59,111,116, + 105,109,101,115,59,111,116,105,109,101,115,97,115,59,111,117, + 109,108,111,117,109,108,59,111,118,98,97,114,59,112,97,114, + 59,112,97,114,97,112,97,114,97,59,112,97,114,97,108,108, + 101,108,59,112,97,114,115,105,109,59,112,97,114,115,108,59, + 112,97,114,116,59,112,99,121,59,112,101,114,99,110,116,59, + 112,101,114,105,111,100,59,112,101,114,109,105,108,59,112,101, + 114,112,59,112,101,114,116,101,110,107,59,112,102,114,59,112, + 104,105,59,112,104,105,118,59,112,104,109,109,97,116,59,112, + 104,111,110,101,59,112,105,59,112,105,116,99,104,102,111,114, + 107,59,112,105,118,59,112,108,97,110,99,107,59,112,108,97, + 110,99,107,104,59,112,108,97,110,107,118,59,112,108,117,115, + 59,112,108,117,115,97,99,105,114,59,112,108,117,115,98,59, + 112,108,117,115,99,105,114,59,112,108,117,115,100,111,59,112, + 108,117,115,100,117,59,112,108,117,115,101,59,112,108,117,115, + 109,110,112,108,117,115,109,110,59,112,108,117,115,115,105,109, + 59,112,108,117,115,116,119,111,59,112,109,59,112,111,105,110, + 116,105,110,116,59,112,111,112,102,59,112,111,117,110,100,112, + 111,117,110,100,59,112,114,59,112,114,69,59,112,114,97,112, + 59,112,114,99,117,101,59,112,114,101,59,112,114,101,99,59, + 112,114,101,99,97,112,112,114,111,120,59,112,114,101,99,99, + 117,114,108,121,101,113,59,112,114,101,99,101,113,59,112,114, + 101,99,110,97,112,112,114,111,120,59,112,114,101,99,110,101, + 113,113,59,112,114,101,99,110,115,105,109,59,112,114,101,99, + 115,105,109,59,112,114,105,109,101,59,112,114,105,109,101,115, + 59,112,114,110,69,59,112,114,110,97,112,59,112,114,110,115, + 105,109,59,112,114,111,100,59,112,114,111,102,97,108,97,114, + 59,112,114,111,102,108,105,110,101,59,112,114,111,102,115,117, + 114,102,59,112,114,111,112,59,112,114,111,112,116,111,59,112, + 114,115,105,109,59,112,114,117,114,101,108,59,112,115,99,114, + 59,112,115,105,59,112,117,110,99,115,112,59,113,102,114,59, + 113,105,110,116,59,113,111,112,102,59,113,112,114,105,109,101, + 59,113,115,99,114,59,113,117,97,116,101,114,110,105,111,110, + 115,59,113,117,97,116,105,110,116,59,113,117,101,115,116,59, + 113,117,101,115,116,101,113,59,113,117,111,116,113,117,111,116, + 59,114,65,97,114,114,59,114,65,114,114,59,114,65,116,97, + 105,108,59,114,66,97,114,114,59,114,72,97,114,59,114,97, + 99,101,59,114,97,99,117,116,101,59,114,97,100,105,99,59, + 114,97,101,109,112,116,121,118,59,114,97,110,103,59,114,97, + 110,103,100,59,114,97,110,103,101,59,114,97,110,103,108,101, + 59,114,97,113,117,111,114,97,113,117,111,59,114,97,114,114, + 59,114,97,114,114,97,112,59,114,97,114,114,98,59,114,97, + 114,114,98,102,115,59,114,97,114,114,99,59,114,97,114,114, + 102,115,59,114,97,114,114,104,107,59,114,97,114,114,108,112, + 59,114,97,114,114,112,108,59,114,97,114,114,115,105,109,59, + 114,97,114,114,116,108,59,114,97,114,114,119,59,114,97,116, + 97,105,108,59,114,97,116,105,111,59,114,97,116,105,111,110, + 97,108,115,59,114,98,97,114,114,59,114,98,98,114,107,59, + 114,98,114,97,99,101,59,114,98,114,97,99,107,59,114,98, + 114,107,101,59,114,98,114,107,115,108,100,59,114,98,114,107, + 115,108,117,59,114,99,97,114,111,110,59,114,99,101,100,105, + 108,59,114,99,101,105,108,59,114,99,117,98,59,114,99,121, + 59,114,100,99,97,59,114,100,108,100,104,97,114,59,114,100, + 113,117,111,59,114,100,113,117,111,114,59,114,100,115,104,59, + 114,101,97,108,59,114,101,97,108,105,110,101,59,114,101,97, + 108,112,97,114,116,59,114,101,97,108,115,59,114,101,99,116, + 59,114,101,103,114,101,103,59,114,102,105,115,104,116,59,114, + 102,108,111,111,114,59,114,102,114,59,114,104,97,114,100,59, + 114,104,97,114,117,59,114,104,97,114,117,108,59,114,104,111, + 59,114,104,111,118,59,114,105,103,104,116,97,114,114,111,119, + 59,114,105,103,104,116,97,114,114,111,119,116,97,105,108,59, + 114,105,103,104,116,104,97,114,112,111,111,110,100,111,119,110, + 59,114,105,103,104,116,104,97,114,112,111,111,110,117,112,59, + 114,105,103,104,116,108,101,102,116,97,114,114,111,119,115,59, + 114,105,103,104,116,108,101,102,116,104,97,114,112,111,111,110, + 115,59,114,105,103,104,116,114,105,103,104,116,97,114,114,111, + 119,115,59,114,105,103,104,116,115,113,117,105,103,97,114,114, + 111,119,59,114,105,103,104,116,116,104,114,101,101,116,105,109, + 101,115,59,114,105,110,103,59,114,105,115,105,110,103,100,111, + 116,115,101,113,59,114,108,97,114,114,59,114,108,104,97,114, + 59,114,108,109,59,114,109,111,117,115,116,59,114,109,111,117, + 115,116,97,99,104,101,59,114,110,109,105,100,59,114,111,97, + 110,103,59,114,111,97,114,114,59,114,111,98,114,107,59,114, + 111,112,97,114,59,114,111,112,102,59,114,111,112,108,117,115, + 59,114,111,116,105,109,101,115,59,114,112,97,114,59,114,112, + 97,114,103,116,59,114,112,112,111,108,105,110,116,59,114,114, + 97,114,114,59,114,115,97,113,117,111,59,114,115,99,114,59, + 114,115,104,59,114,115,113,98,59,114,115,113,117,111,59,114, + 115,113,117,111,114,59,114,116,104,114,101,101,59,114,116,105, + 109,101,115,59,114,116,114,105,59,114,116,114,105,101,59,114, + 116,114,105,102,59,114,116,114,105,108,116,114,105,59,114,117, + 108,117,104,97,114,59,114,120,59,115,97,99,117,116,101,59, + 115,98,113,117,111,59,115,99,59,115,99,69,59,115,99,97, + 112,59,115,99,97,114,111,110,59,115,99,99,117,101,59,115, + 99,101,59,115,99,101,100,105,108,59,115,99,105,114,99,59, + 115,99,110,69,59,115,99,110,97,112,59,115,99,110,115,105, + 109,59,115,99,112,111,108,105,110,116,59,115,99,115,105,109, + 59,115,99,121,59,115,100,111,116,59,115,100,111,116,98,59, + 115,100,111,116,101,59,115,101,65,114,114,59,115,101,97,114, + 104,107,59,115,101,97,114,114,59,115,101,97,114,114,111,119, + 59,115,101,99,116,115,101,99,116,59,115,101,109,105,59,115, + 101,115,119,97,114,59,115,101,116,109,105,110,117,115,59,115, + 101,116,109,110,59,115,101,120,116,59,115,102,114,59,115,102, + 114,111,119,110,59,115,104,97,114,112,59,115,104,99,104,99, + 121,59,115,104,99,121,59,115,104,111,114,116,109,105,100,59, + 115,104,111,114,116,112,97,114,97,108,108,101,108,59,115,104, + 121,115,104,121,59,115,105,103,109,97,59,115,105,103,109,97, + 102,59,115,105,103,109,97,118,59,115,105,109,59,115,105,109, + 100,111,116,59,115,105,109,101,59,115,105,109,101,113,59,115, + 105,109,103,59,115,105,109,103,69,59,115,105,109,108,59,115, + 105,109,108,69,59,115,105,109,110,101,59,115,105,109,112,108, + 117,115,59,115,105,109,114,97,114,114,59,115,108,97,114,114, + 59,115,109,97,108,108,115,101,116,109,105,110,117,115,59,115, + 109,97,115,104,112,59,115,109,101,112,97,114,115,108,59,115, + 109,105,100,59,115,109,105,108,101,59,115,109,116,59,115,109, + 116,101,59,115,109,116,101,115,59,115,111,102,116,99,121,59, + 115,111,108,59,115,111,108,98,59,115,111,108,98,97,114,59, + 115,111,112,102,59,115,112,97,100,101,115,59,115,112,97,100, + 101,115,117,105,116,59,115,112,97,114,59,115,113,99,97,112, + 59,115,113,99,97,112,115,59,115,113,99,117,112,59,115,113, + 99,117,112,115,59,115,113,115,117,98,59,115,113,115,117,98, + 101,59,115,113,115,117,98,115,101,116,59,115,113,115,117,98, + 115,101,116,101,113,59,115,113,115,117,112,59,115,113,115,117, + 112,101,59,115,113,115,117,112,115,101,116,59,115,113,115,117, + 112,115,101,116,101,113,59,115,113,117,59,115,113,117,97,114, + 101,59,115,113,117,97,114,102,59,115,113,117,102,59,115,114, + 97,114,114,59,115,115,99,114,59,115,115,101,116,109,110,59, + 115,115,109,105,108,101,59,115,115,116,97,114,102,59,115,116, + 97,114,59,115,116,97,114,102,59,115,116,114,97,105,103,104, + 116,101,112,115,105,108,111,110,59,115,116,114,97,105,103,104, + 116,112,104,105,59,115,116,114,110,115,59,115,117,98,59,115, + 117,98,69,59,115,117,98,100,111,116,59,115,117,98,101,59, + 115,117,98,101,100,111,116,59,115,117,98,109,117,108,116,59, + 115,117,98,110,69,59,115,117,98,110,101,59,115,117,98,112, + 108,117,115,59,115,117,98,114,97,114,114,59,115,117,98,115, + 101,116,59,115,117,98,115,101,116,101,113,59,115,117,98,115, + 101,116,101,113,113,59,115,117,98,115,101,116,110,101,113,59, + 115,117,98,115,101,116,110,101,113,113,59,115,117,98,115,105, + 109,59,115,117,98,115,117,98,59,115,117,98,115,117,112,59, + 115,117,99,99,59,115,117,99,99,97,112,112,114,111,120,59, + 115,117,99,99,99,117,114,108,121,101,113,59,115,117,99,99, + 101,113,59,115,117,99,99,110,97,112,112,114,111,120,59,115, + 117,99,99,110,101,113,113,59,115,117,99,99,110,115,105,109, + 59,115,117,99,99,115,105,109,59,115,117,109,59,115,117,110, + 103,59,115,117,112,49,115,117,112,49,59,115,117,112,50,115, + 117,112,50,59,115,117,112,51,115,117,112,51,59,115,117,112, + 59,115,117,112,69,59,115,117,112,100,111,116,59,115,117,112, + 100,115,117,98,59,115,117,112,101,59,115,117,112,101,100,111, + 116,59,115,117,112,104,115,111,108,59,115,117,112,104,115,117, + 98,59,115,117,112,108,97,114,114,59,115,117,112,109,117,108, + 116,59,115,117,112,110,69,59,115,117,112,110,101,59,115,117, + 112,112,108,117,115,59,115,117,112,115,101,116,59,115,117,112, + 115,101,116,101,113,59,115,117,112,115,101,116,101,113,113,59, + 115,117,112,115,101,116,110,101,113,59,115,117,112,115,101,116, + 110,101,113,113,59,115,117,112,115,105,109,59,115,117,112,115, + 117,98,59,115,117,112,115,117,112,59,115,119,65,114,114,59, + 115,119,97,114,104,107,59,115,119,97,114,114,59,115,119,97, + 114,114,111,119,59,115,119,110,119,97,114,59,115,122,108,105, + 103,115,122,108,105,103,59,116,97,114,103,101,116,59,116,97, + 117,59,116,98,114,107,59,116,99,97,114,111,110,59,116,99, + 101,100,105,108,59,116,99,121,59,116,100,111,116,59,116,101, + 108,114,101,99,59,116,102,114,59,116,104,101,114,101,52,59, + 116,104,101,114,101,102,111,114,101,59,116,104,101,116,97,59, + 116,104,101,116,97,115,121,109,59,116,104,101,116,97,118,59, + 116,104,105,99,107,97,112,112,114,111,120,59,116,104,105,99, + 107,115,105,109,59,116,104,105,110,115,112,59,116,104,107,97, + 112,59,116,104,107,115,105,109,59,116,104,111,114,110,116,104, + 111,114,110,59,116,105,108,100,101,59,116,105,109,101,115,116, + 105,109,101,115,59,116,105,109,101,115,98,59,116,105,109,101, + 115,98,97,114,59,116,105,109,101,115,100,59,116,105,110,116, + 59,116,111,101,97,59,116,111,112,59,116,111,112,98,111,116, + 59,116,111,112,99,105,114,59,116,111,112,102,59,116,111,112, + 102,111,114,107,59,116,111,115,97,59,116,112,114,105,109,101, + 59,116,114,97,100,101,59,116,114,105,97,110,103,108,101,59, + 116,114,105,97,110,103,108,101,100,111,119,110,59,116,114,105, + 97,110,103,108,101,108,101,102,116,59,116,114,105,97,110,103, + 108,101,108,101,102,116,101,113,59,116,114,105,97,110,103,108, + 101,113,59,116,114,105,97,110,103,108,101,114,105,103,104,116, + 59,116,114,105,97,110,103,108,101,114,105,103,104,116,101,113, + 59,116,114,105,100,111,116,59,116,114,105,101,59,116,114,105, + 109,105,110,117,115,59,116,114,105,112,108,117,115,59,116,114, + 105,115,98,59,116,114,105,116,105,109,101,59,116,114,112,101, + 122,105,117,109,59,116,115,99,114,59,116,115,99,121,59,116, + 115,104,99,121,59,116,115,116,114,111,107,59,116,119,105,120, + 116,59,116,119,111,104,101,97,100,108,101,102,116,97,114,114, + 111,119,59,116,119,111,104,101,97,100,114,105,103,104,116,97, + 114,114,111,119,59,117,65,114,114,59,117,72,97,114,59,117, + 97,99,117,116,101,117,97,99,117,116,101,59,117,97,114,114, + 59,117,98,114,99,121,59,117,98,114,101,118,101,59,117,99, + 105,114,99,117,99,105,114,99,59,117,99,121,59,117,100,97, + 114,114,59,117,100,98,108,97,99,59,117,100,104,97,114,59, + 117,102,105,115,104,116,59,117,102,114,59,117,103,114,97,118, + 101,117,103,114,97,118,101,59,117,104,97,114,108,59,117,104, + 97,114,114,59,117,104,98,108,107,59,117,108,99,111,114,110, + 59,117,108,99,111,114,110,101,114,59,117,108,99,114,111,112, + 59,117,108,116,114,105,59,117,109,97,99,114,59,117,109,108, + 117,109,108,59,117,111,103,111,110,59,117,111,112,102,59,117, + 112,97,114,114,111,119,59,117,112,100,111,119,110,97,114,114, + 111,119,59,117,112,104,97,114,112,111,111,110,108,101,102,116, + 59,117,112,104,97,114,112,111,111,110,114,105,103,104,116,59, + 117,112,108,117,115,59,117,112,115,105,59,117,112,115,105,104, + 59,117,112,115,105,108,111,110,59,117,112,117,112,97,114,114, + 111,119,115,59,117,114,99,111,114,110,59,117,114,99,111,114, + 110,101,114,59,117,114,99,114,111,112,59,117,114,105,110,103, + 59,117,114,116,114,105,59,117,115,99,114,59,117,116,100,111, + 116,59,117,116,105,108,100,101,59,117,116,114,105,59,117,116, + 114,105,102,59,117,117,97,114,114,59,117,117,109,108,117,117, + 109,108,59,117,119,97,110,103,108,101,59,118,65,114,114,59, + 118,66,97,114,59,118,66,97,114,118,59,118,68,97,115,104, + 59,118,97,110,103,114,116,59,118,97,114,101,112,115,105,108, + 111,110,59,118,97,114,107,97,112,112,97,59,118,97,114,110, + 111,116,104,105,110,103,59,118,97,114,112,104,105,59,118,97, + 114,112,105,59,118,97,114,112,114,111,112,116,111,59,118,97, + 114,114,59,118,97,114,114,104,111,59,118,97,114,115,105,103, + 109,97,59,118,97,114,115,117,98,115,101,116,110,101,113,59, + 118,97,114,115,117,98,115,101,116,110,101,113,113,59,118,97, + 114,115,117,112,115,101,116,110,101,113,59,118,97,114,115,117, + 112,115,101,116,110,101,113,113,59,118,97,114,116,104,101,116, + 97,59,118,97,114,116,114,105,97,110,103,108,101,108,101,102, + 116,59,118,97,114,116,114,105,97,110,103,108,101,114,105,103, + 104,116,59,118,99,121,59,118,100,97,115,104,59,118,101,101, + 59,118,101,101,98,97,114,59,118,101,101,101,113,59,118,101, + 108,108,105,112,59,118,101,114,98,97,114,59,118,101,114,116, + 59,118,102,114,59,118,108,116,114,105,59,118,110,115,117,98, + 59,118,110,115,117,112,59,118,111,112,102,59,118,112,114,111, + 112,59,118,114,116,114,105,59,118,115,99,114,59,118,115,117, + 98,110,69,59,118,115,117,98,110,101,59,118,115,117,112,110, + 69,59,118,115,117,112,110,101,59,118,122,105,103,122,97,103, + 59,119,99,105,114,99,59,119,101,100,98,97,114,59,119,101, + 100,103,101,59,119,101,100,103,101,113,59,119,101,105,101,114, + 112,59,119,102,114,59,119,111,112,102,59,119,112,59,119,114, + 59,119,114,101,97,116,104,59,119,115,99,114,59,120,99,97, + 112,59,120,99,105,114,99,59,120,99,117,112,59,120,100,116, + 114,105,59,120,102,114,59,120,104,65,114,114,59,120,104,97, + 114,114,59,120,105,59,120,108,65,114,114,59,120,108,97,114, + 114,59,120,109,97,112,59,120,110,105,115,59,120,111,100,111, + 116,59,120,111,112,102,59,120,111,112,108,117,115,59,120,111, + 116,105,109,101,59,120,114,65,114,114,59,120,114,97,114,114, + 59,120,115,99,114,59,120,115,113,99,117,112,59,120,117,112, + 108,117,115,59,120,117,116,114,105,59,120,118,101,101,59,120, + 119,101,100,103,101,59,121,97,99,117,116,101,121,97,99,117, + 116,101,59,121,97,99,121,59,121,99,105,114,99,59,121,99, + 121,59,121,101,110,121,101,110,59,121,102,114,59,121,105,99, + 121,59,121,111,112,102,59,121,115,99,114,59,121,117,99,121, + 59,121,117,109,108,121,117,109,108,59,122,97,99,117,116,101, + 59,122,99,97,114,111,110,59,122,99,121,59,122,100,111,116, + 59,122,101,101,116,114,102,59,122,101,116,97,59,122,102,114, + 59,122,104,99,121,59,122,105,103,114,97,114,114,59,122,111, + 112,102,59,122,115,99,114,59,122,119,106,59,122,119,110,106, + 59, +}; + +extern const uint8_t kValuePool[]; +const uint8_t kValuePool[] = { + 195,134,195,134,38,38,195,129,195,129,196,130,195,130,195,130, + 208,144,240,157,148,132,195,128,195,128,206,145,196,128,226,169, + 147,196,132,240,157,148,184,226,129,161,195,133,195,133,240,157, + 146,156,226,137,148,195,131,195,131,195,132,195,132,226,136,150, + 226,171,167,226,140,134,208,145,226,136,181,226,132,172,206,146, + 240,157,148,133,240,157,148,185,203,152,226,132,172,226,137,142, + 208,167,194,169,194,169,196,134,226,139,146,226,133,133,226,132, + 173,196,140,195,135,195,135,196,136,226,136,176,196,138,194,184, + 194,183,226,132,173,206,167,226,138,153,226,138,150,226,138,149, + 226,138,151,226,136,178,226,128,157,226,128,153,226,136,183,226, + 169,180,226,137,161,226,136,175,226,136,174,226,132,130,226,136, + 144,226,136,179,226,168,175,240,157,146,158,226,139,147,226,137, + 141,226,133,133,226,164,145,208,130,208,133,208,143,226,128,161, + 226,134,161,226,171,164,196,142,208,148,226,136,135,206,148,240, + 157,148,135,194,180,203,153,203,157,96,203,156,226,139,132,226, + 133,134,240,157,148,187,194,168,226,131,156,226,137,144,226,136, + 175,194,168,226,135,147,226,135,144,226,135,148,226,171,164,226, + 159,184,226,159,186,226,159,185,226,135,146,226,138,168,226,135, + 145,226,135,149,226,136,165,226,134,147,226,164,147,226,135,181, + 204,145,226,165,144,226,165,158,226,134,189,226,165,150,226,165, + 159,226,135,129,226,165,151,226,138,164,226,134,167,226,135,147, + 240,157,146,159,196,144,197,138,195,144,195,144,195,137,195,137, + 196,154,195,138,195,138,208,173,196,150,240,157,148,136,195,136, + 195,136,226,136,136,196,146,226,151,187,226,150,171,196,152,240, + 157,148,188,206,149,226,169,181,226,137,130,226,135,140,226,132, + 176,226,169,179,206,151,195,139,195,139,226,136,131,226,133,135, + 208,164,240,157,148,137,226,151,188,226,150,170,240,157,148,189, + 226,136,128,226,132,177,226,132,177,208,131,62,62,206,147,207, + 156,196,158,196,162,196,156,208,147,196,160,240,157,148,138,226, + 139,153,240,157,148,190,226,137,165,226,139,155,226,137,167,226, + 170,162,226,137,183,226,169,190,226,137,179,240,157,146,162,226, + 137,171,208,170,203,135,94,196,164,226,132,140,226,132,139,226, + 132,141,226,148,128,226,132,139,196,166,226,137,142,226,137,143, + 208,149,196,178,208,129,195,141,195,141,195,142,195,142,208,152, + 196,176,226,132,145,195,140,195,140,226,132,145,196,170,226,133, + 136,226,135,146,226,136,172,226,136,171,226,139,130,226,129,163, + 226,129,162,196,174,240,157,149,128,206,153,226,132,144,196,168, + 208,134,195,143,195,143,196,180,208,153,240,157,148,141,240,157, + 149,129,240,157,146,165,208,136,208,132,208,165,208,140,206,154, + 196,182,208,154,240,157,148,142,240,157,149,130,240,157,146,166, + 208,137,60,60,196,185,206,155,226,159,170,226,132,146,226,134, + 158,196,189,196,187,208,155,226,159,168,226,134,144,226,135,164, + 226,135,134,226,140,136,226,159,166,226,165,161,226,135,131,226, + 165,153,226,140,138,226,134,148,226,165,142,226,138,163,226,134, + 164,226,165,154,226,138,178,226,167,143,226,138,180,226,165,145, + 226,165,160,226,134,191,226,165,152,226,134,188,226,165,146,226, + 135,144,226,135,148,226,139,154,226,137,166,226,137,182,226,170, + 161,226,169,189,226,137,178,240,157,148,143,226,139,152,226,135, + 154,196,191,226,159,181,226,159,183,226,159,182,226,159,184,226, + 159,186,226,159,185,240,157,149,131,226,134,153,226,134,152,226, + 132,146,226,134,176,197,129,226,137,170,226,164,133,208,156,226, + 129,159,226,132,179,240,157,148,144,226,136,147,240,157,149,132, + 226,132,179,206,156,208,138,197,131,197,135,197,133,208,157,226, + 128,139,226,128,139,226,128,139,226,128,139,226,137,171,226,137, + 170,10,240,157,148,145,226,129,160,194,160,226,132,149,226,171, + 172,226,137,162,226,137,173,226,136,166,226,136,137,226,137,160, + 226,137,130,204,184,226,136,132,226,137,175,226,137,177,226,137, + 167,204,184,226,137,171,204,184,226,137,185,226,169,190,204,184, + 226,137,181,226,137,142,204,184,226,137,143,204,184,226,139,170, + 226,167,143,204,184,226,139,172,226,137,174,226,137,176,226,137, + 184,226,137,170,204,184,226,169,189,204,184,226,137,180,226,170, + 162,204,184,226,170,161,204,184,226,138,128,226,170,175,204,184, + 226,139,160,226,136,140,226,139,171,226,167,144,204,184,226,139, + 173,226,138,143,204,184,226,139,162,226,138,144,204,184,226,139, + 163,226,138,130,226,131,146,226,138,136,226,138,129,226,170,176, + 204,184,226,139,161,226,137,191,204,184,226,138,131,226,131,146, + 226,138,137,226,137,129,226,137,132,226,137,135,226,137,137,226, + 136,164,240,157,146,169,195,145,195,145,206,157,197,146,195,147, + 195,147,195,148,195,148,208,158,197,144,240,157,148,146,195,146, + 195,146,197,140,206,169,206,159,240,157,149,134,226,128,156,226, + 128,152,226,169,148,240,157,146,170,195,152,195,152,195,149,195, + 149,226,168,183,195,150,195,150,226,128,190,226,143,158,226,142, + 180,226,143,156,226,136,130,208,159,240,157,148,147,206,166,206, + 160,194,177,226,132,140,226,132,153,226,170,187,226,137,186,226, + 170,175,226,137,188,226,137,190,226,128,179,226,136,143,226,136, + 183,226,136,157,240,157,146,171,206,168,34,34,240,157,148,148, + 226,132,154,240,157,146,172,226,164,144,194,174,194,174,197,148, + 226,159,171,226,134,160,226,164,150,197,152,197,150,208,160,226, + 132,156,226,136,139,226,135,139,226,165,175,226,132,156,206,161, + 226,159,169,226,134,146,226,135,165,226,135,132,226,140,137,226, + 159,167,226,165,157,226,135,130,226,165,149,226,140,139,226,138, + 162,226,134,166,226,165,155,226,138,179,226,167,144,226,138,181, + 226,165,143,226,165,156,226,134,190,226,165,148,226,135,128,226, + 165,147,226,135,146,226,132,157,226,165,176,226,135,155,226,132, + 155,226,134,177,226,167,180,208,169,208,168,208,172,197,154,226, + 170,188,197,160,197,158,197,156,208,161,240,157,148,150,226,134, + 147,226,134,144,226,134,146,226,134,145,206,163,226,136,152,240, + 157,149,138,226,136,154,226,150,161,226,138,147,226,138,143,226, + 138,145,226,138,144,226,138,146,226,138,148,240,157,146,174,226, + 139,134,226,139,144,226,139,144,226,138,134,226,137,187,226,170, + 176,226,137,189,226,137,191,226,136,139,226,136,145,226,139,145, + 226,138,131,226,138,135,226,139,145,195,158,195,158,226,132,162, + 208,139,208,166,9,206,164,197,164,197,162,208,162,240,157,148, + 151,226,136,180,206,152,226,129,159,226,128,138,226,128,137,226, + 136,188,226,137,131,226,137,133,226,137,136,240,157,149,139,226, + 131,155,240,157,146,175,197,166,195,154,195,154,226,134,159,226, + 165,137,208,142,197,172,195,155,195,155,208,163,197,176,240,157, + 148,152,195,153,195,153,197,170,95,226,143,159,226,142,181,226, + 143,157,226,139,131,226,138,142,197,178,240,157,149,140,226,134, + 145,226,164,146,226,135,133,226,134,149,226,165,174,226,138,165, + 226,134,165,226,135,145,226,135,149,226,134,150,226,134,151,207, + 146,206,165,197,174,240,157,146,176,197,168,195,156,195,156,226, + 138,171,226,171,171,208,146,226,138,169,226,171,166,226,139,129, + 226,128,150,226,128,150,226,136,163,124,226,157,152,226,137,128, + 226,128,138,240,157,148,153,240,157,149,141,240,157,146,177,226, + 138,170,197,180,226,139,128,240,157,148,154,240,157,149,142,240, + 157,146,178,240,157,148,155,206,158,240,157,149,143,240,157,146, + 179,208,175,208,135,208,174,195,157,195,157,197,182,208,171,240, + 157,148,156,240,157,149,144,240,157,146,180,197,184,208,150,197, + 185,197,189,208,151,197,187,226,128,139,206,150,226,132,168,226, + 132,164,240,157,146,181,195,161,195,161,196,131,226,136,190,226, + 136,190,204,179,226,136,191,195,162,195,162,194,180,194,180,208, + 176,195,166,195,166,226,129,161,240,157,148,158,195,160,195,160, + 226,132,181,226,132,181,206,177,196,129,226,168,191,38,38,226, + 136,167,226,169,149,226,169,156,226,169,152,226,169,154,226,136, + 160,226,166,164,226,136,160,226,136,161,226,166,168,226,166,169, + 226,166,170,226,166,171,226,166,172,226,166,173,226,166,174,226, + 166,175,226,136,159,226,138,190,226,166,157,226,136,162,195,133, + 226,141,188,196,133,240,157,149,146,226,137,136,226,169,176,226, + 169,175,226,137,138,226,137,139,39,226,137,136,226,137,138,195, + 165,195,165,240,157,146,182,42,226,137,136,226,137,141,195,163, + 195,163,195,164,195,164,226,136,179,226,168,145,226,171,173,226, + 137,140,207,182,226,128,181,226,136,189,226,139,141,226,138,189, + 226,140,133,226,140,133,226,142,181,226,142,182,226,137,140,208, + 177,226,128,158,226,136,181,226,136,181,226,166,176,207,182,226, + 132,172,206,178,226,132,182,226,137,172,240,157,148,159,226,139, + 130,226,151,175,226,139,131,226,168,128,226,168,129,226,168,130, + 226,168,134,226,152,133,226,150,189,226,150,179,226,168,132,226, + 139,129,226,139,128,226,164,141,226,167,171,226,150,170,226,150, + 180,226,150,190,226,151,130,226,150,184,226,144,163,226,150,146, + 226,150,145,226,150,147,226,150,136,61,226,131,165,226,137,161, + 226,131,165,226,140,144,240,157,149,147,226,138,165,226,138,165, + 226,139,136,226,149,151,226,149,148,226,149,150,226,149,147,226, + 149,144,226,149,166,226,149,169,226,149,164,226,149,167,226,149, + 157,226,149,154,226,149,156,226,149,153,226,149,145,226,149,172, + 226,149,163,226,149,160,226,149,171,226,149,162,226,149,159,226, + 167,137,226,149,149,226,149,146,226,148,144,226,148,140,226,148, + 128,226,149,165,226,149,168,226,148,172,226,148,180,226,138,159, + 226,138,158,226,138,160,226,149,155,226,149,152,226,148,152,226, + 148,148,226,148,130,226,149,170,226,149,161,226,149,158,226,148, + 188,226,148,164,226,148,156,226,128,181,203,152,194,166,194,166, + 240,157,146,183,226,129,143,226,136,189,226,139,141,92,226,167, + 133,226,159,136,226,128,162,226,128,162,226,137,142,226,170,174, + 226,137,143,226,137,143,196,135,226,136,169,226,169,132,226,169, + 137,226,169,139,226,169,135,226,169,128,226,136,169,239,184,128, + 226,129,129,203,135,226,169,141,196,141,195,167,195,167,196,137, + 226,169,140,226,169,144,196,139,194,184,194,184,226,166,178,194, + 162,194,162,194,183,240,157,148,160,209,135,226,156,147,226,156, + 147,207,135,226,151,139,226,167,131,203,134,226,137,151,226,134, + 186,226,134,187,194,174,226,147,136,226,138,155,226,138,154,226, + 138,157,226,137,151,226,168,144,226,171,175,226,167,130,226,153, + 163,226,153,163,58,226,137,148,226,137,148,44,64,226,136,129, + 226,136,152,226,136,129,226,132,130,226,137,133,226,169,173,226, + 136,174,240,157,149,148,226,136,144,194,169,194,169,226,132,151, + 226,134,181,226,156,151,240,157,146,184,226,171,143,226,171,145, + 226,171,144,226,171,146,226,139,175,226,164,184,226,164,181,226, + 139,158,226,139,159,226,134,182,226,164,189,226,136,170,226,169, + 136,226,169,134,226,169,138,226,138,141,226,169,133,226,136,170, + 239,184,128,226,134,183,226,164,188,226,139,158,226,139,159,226, + 139,142,226,139,143,194,164,194,164,226,134,182,226,134,183,226, + 139,142,226,139,143,226,136,178,226,136,177,226,140,173,226,135, + 147,226,165,165,226,128,160,226,132,184,226,134,147,226,128,144, + 226,138,163,226,164,143,203,157,196,143,208,180,226,133,134,226, + 128,161,226,135,138,226,169,183,194,176,194,176,206,180,226,166, + 177,226,165,191,240,157,148,161,226,135,131,226,135,130,226,139, + 132,226,139,132,226,153,166,226,153,166,194,168,207,157,226,139, + 178,195,183,195,183,195,183,226,139,135,226,139,135,209,146,226, + 140,158,226,140,141,36,240,157,149,149,203,153,226,137,144,226, + 137,145,226,136,184,226,136,148,226,138,161,226,140,134,226,134, + 147,226,135,138,226,135,131,226,135,130,226,164,144,226,140,159, + 226,140,140,240,157,146,185,209,149,226,167,182,196,145,226,139, + 177,226,150,191,226,150,190,226,135,181,226,165,175,226,166,166, + 209,159,226,159,191,226,169,183,226,137,145,195,169,195,169,226, + 169,174,196,155,226,137,150,195,170,195,170,226,137,149,209,141, + 196,151,226,133,135,226,137,146,240,157,148,162,226,170,154,195, + 168,195,168,226,170,150,226,170,152,226,170,153,226,143,167,226, + 132,147,226,170,149,226,170,151,196,147,226,136,133,226,136,133, + 226,136,133,226,128,132,226,128,133,226,128,131,197,139,226,128, + 130,196,153,240,157,149,150,226,139,149,226,167,163,226,169,177, + 206,181,206,181,207,181,226,137,150,226,137,149,226,137,130,226, + 170,150,226,170,149,61,226,137,159,226,137,161,226,169,184,226, + 167,165,226,137,147,226,165,177,226,132,175,226,137,144,226,137, + 130,206,183,195,176,195,176,195,171,195,171,226,130,172,33,226, + 136,131,226,132,176,226,133,135,226,137,146,209,132,226,153,128, + 239,172,131,239,172,128,239,172,132,240,157,148,163,239,172,129, + 102,106,226,153,173,239,172,130,226,150,177,198,146,240,157,149, + 151,226,136,128,226,139,148,226,171,153,226,168,141,194,189,194, + 189,226,133,147,194,188,194,188,226,133,149,226,133,153,226,133, + 155,226,133,148,226,133,150,194,190,194,190,226,133,151,226,133, + 156,226,133,152,226,133,154,226,133,157,226,133,158,226,129,132, + 226,140,162,240,157,146,187,226,137,167,226,170,140,199,181,206, + 179,207,157,226,170,134,196,159,196,157,208,179,196,161,226,137, + 165,226,139,155,226,137,165,226,137,167,226,169,190,226,169,190, + 226,170,169,226,170,128,226,170,130,226,170,132,226,139,155,239, + 184,128,226,170,148,240,157,148,164,226,137,171,226,139,153,226, + 132,183,209,147,226,137,183,226,170,146,226,170,165,226,170,164, + 226,137,169,226,170,138,226,170,138,226,170,136,226,170,136,226, + 137,169,226,139,167,240,157,149,152,96,226,132,138,226,137,179, + 226,170,142,226,170,144,62,62,226,170,167,226,169,186,226,139, + 151,226,166,149,226,169,188,226,170,134,226,165,184,226,139,151, + 226,139,155,226,170,140,226,137,183,226,137,179,226,137,169,239, + 184,128,226,137,169,239,184,128,226,135,148,226,128,138,194,189, + 226,132,139,209,138,226,134,148,226,165,136,226,134,173,226,132, + 143,196,165,226,153,165,226,153,165,226,128,166,226,138,185,240, + 157,148,165,226,164,165,226,164,166,226,135,191,226,136,187,226, + 134,169,226,134,170,240,157,149,153,226,128,149,240,157,146,189, + 226,132,143,196,167,226,129,131,226,128,144,195,173,195,173,226, + 129,163,195,174,195,174,208,184,208,181,194,161,194,161,226,135, + 148,240,157,148,166,195,172,195,172,226,133,136,226,168,140,226, + 136,173,226,167,156,226,132,169,196,179,196,171,226,132,145,226, + 132,144,226,132,145,196,177,226,138,183,198,181,226,136,136,226, + 132,133,226,136,158,226,167,157,196,177,226,136,171,226,138,186, + 226,132,164,226,138,186,226,168,151,226,168,188,209,145,196,175, + 240,157,149,154,206,185,226,168,188,194,191,194,191,240,157,146, + 190,226,136,136,226,139,185,226,139,181,226,139,180,226,139,179, + 226,136,136,226,129,162,196,169,209,150,195,175,195,175,196,181, + 208,185,240,157,148,167,200,183,240,157,149,155,240,157,146,191, + 209,152,209,148,206,186,207,176,196,183,208,186,240,157,148,168, + 196,184,209,133,209,156,240,157,149,156,240,157,147,128,226,135, + 154,226,135,144,226,164,155,226,164,142,226,137,166,226,170,139, + 226,165,162,196,186,226,166,180,226,132,146,206,187,226,159,168, + 226,166,145,226,159,168,226,170,133,194,171,194,171,226,134,144, + 226,135,164,226,164,159,226,164,157,226,134,169,226,134,171,226, + 164,185,226,165,179,226,134,162,226,170,171,226,164,153,226,170, + 173,226,170,173,239,184,128,226,164,140,226,157,178,123,91,226, + 166,139,226,166,143,226,166,141,196,190,196,188,226,140,136,123, + 208,187,226,164,182,226,128,156,226,128,158,226,165,167,226,165, + 139,226,134,178,226,137,164,226,134,144,226,134,162,226,134,189, + 226,134,188,226,135,135,226,134,148,226,135,134,226,135,139,226, + 134,173,226,139,139,226,139,154,226,137,164,226,137,166,226,169, + 189,226,169,189,226,170,168,226,169,191,226,170,129,226,170,131, + 226,139,154,239,184,128,226,170,147,226,170,133,226,139,150,226, + 139,154,226,170,139,226,137,182,226,137,178,226,165,188,226,140, + 138,240,157,148,169,226,137,182,226,170,145,226,134,189,226,134, + 188,226,165,170,226,150,132,209,153,226,137,170,226,135,135,226, + 140,158,226,165,171,226,151,186,197,128,226,142,176,226,142,176, + 226,137,168,226,170,137,226,170,137,226,170,135,226,170,135,226, + 137,168,226,139,166,226,159,172,226,135,189,226,159,166,226,159, + 181,226,159,183,226,159,188,226,159,182,226,134,171,226,134,172, + 226,166,133,240,157,149,157,226,168,173,226,168,180,226,136,151, + 95,226,151,138,226,151,138,226,167,171,40,226,166,147,226,135, + 134,226,140,159,226,135,139,226,165,173,226,128,142,226,138,191, + 226,128,185,240,157,147,129,226,134,176,226,137,178,226,170,141, + 226,170,143,91,226,128,152,226,128,154,197,130,60,60,226,170, + 166,226,169,185,226,139,150,226,139,139,226,139,137,226,165,182, + 226,169,187,226,166,150,226,151,131,226,138,180,226,151,130,226, + 165,138,226,165,166,226,137,168,239,184,128,226,137,168,239,184, + 128,226,136,186,194,175,194,175,226,153,130,226,156,160,226,156, + 160,226,134,166,226,134,166,226,134,167,226,134,164,226,134,165, + 226,150,174,226,168,169,208,188,226,128,148,226,136,161,240,157, + 148,170,226,132,167,194,181,194,181,226,136,163,42,226,171,176, + 194,183,194,183,226,136,146,226,138,159,226,136,184,226,168,170, + 226,171,155,226,128,166,226,136,147,226,138,167,240,157,149,158, + 226,136,147,240,157,147,130,226,136,190,206,188,226,138,184,226, + 138,184,226,139,153,204,184,226,137,171,226,131,146,226,137,171, + 204,184,226,135,141,226,135,142,226,139,152,204,184,226,137,170, + 226,131,146,226,137,170,204,184,226,135,143,226,138,175,226,138, + 174,226,136,135,197,132,226,136,160,226,131,146,226,137,137,226, + 169,176,204,184,226,137,139,204,184,197,137,226,137,137,226,153, + 174,226,153,174,226,132,149,194,160,194,160,226,137,142,204,184, + 226,137,143,204,184,226,169,131,197,136,197,134,226,137,135,226, + 169,173,204,184,226,169,130,208,189,226,128,147,226,137,160,226, + 135,151,226,164,164,226,134,151,226,134,151,226,137,144,204,184, + 226,137,162,226,164,168,226,137,130,204,184,226,136,132,226,136, + 132,240,157,148,171,226,137,167,204,184,226,137,177,226,137,177, + 226,137,167,204,184,226,169,190,204,184,226,169,190,204,184,226, + 137,181,226,137,175,226,137,175,226,135,142,226,134,174,226,171, + 178,226,136,139,226,139,188,226,139,186,226,136,139,209,154,226, + 135,141,226,137,166,204,184,226,134,154,226,128,165,226,137,176, + 226,134,154,226,134,174,226,137,176,226,137,166,204,184,226,169, + 189,204,184,226,169,189,204,184,226,137,174,226,137,180,226,137, + 174,226,139,170,226,139,172,226,136,164,240,157,149,159,194,172, + 194,172,226,136,137,226,139,185,204,184,226,139,181,204,184,226, + 136,137,226,139,183,226,139,182,226,136,140,226,136,140,226,139, + 190,226,139,189,226,136,166,226,136,166,226,171,189,226,131,165, + 226,136,130,204,184,226,168,148,226,138,128,226,139,160,226,170, + 175,204,184,226,138,128,226,170,175,204,184,226,135,143,226,134, + 155,226,164,179,204,184,226,134,157,204,184,226,134,155,226,139, + 171,226,139,173,226,138,129,226,139,161,226,170,176,204,184,240, + 157,147,131,226,136,164,226,136,166,226,137,129,226,137,132,226, + 137,132,226,136,164,226,136,166,226,139,162,226,139,163,226,138, + 132,226,171,133,204,184,226,138,136,226,138,130,226,131,146,226, + 138,136,226,171,133,204,184,226,138,129,226,170,176,204,184,226, + 138,133,226,171,134,204,184,226,138,137,226,138,131,226,131,146, + 226,138,137,226,171,134,204,184,226,137,185,195,177,195,177,226, + 137,184,226,139,170,226,139,172,226,139,171,226,139,173,206,189, + 35,226,132,150,226,128,135,226,138,173,226,164,132,226,137,141, + 226,131,146,226,138,172,226,137,165,226,131,146,62,226,131,146, + 226,167,158,226,164,130,226,137,164,226,131,146,60,226,131,146, + 226,138,180,226,131,146,226,164,131,226,138,181,226,131,146,226, + 136,188,226,131,146,226,135,150,226,164,163,226,134,150,226,134, + 150,226,164,167,226,147,136,195,179,195,179,226,138,155,226,138, + 154,195,180,195,180,208,190,226,138,157,197,145,226,168,184,226, + 138,153,226,166,188,197,147,226,166,191,240,157,148,172,203,155, + 195,178,195,178,226,167,129,226,166,181,206,169,226,136,174,226, + 134,186,226,166,190,226,166,187,226,128,190,226,167,128,197,141, + 207,137,206,191,226,166,182,226,138,150,240,157,149,160,226,166, + 183,226,166,185,226,138,149,226,136,168,226,134,187,226,169,157, + 226,132,180,226,132,180,194,170,194,170,194,186,194,186,226,138, + 182,226,169,150,226,169,151,226,169,155,226,132,180,195,184,195, + 184,226,138,152,195,181,195,181,226,138,151,226,168,182,195,182, + 195,182,226,140,189,226,136,165,194,182,194,182,226,136,165,226, + 171,179,226,171,189,226,136,130,208,191,37,46,226,128,176,226, + 138,165,226,128,177,240,157,148,173,207,134,207,149,226,132,179, + 226,152,142,207,128,226,139,148,207,150,226,132,143,226,132,142, + 226,132,143,43,226,168,163,226,138,158,226,168,162,226,136,148, + 226,168,165,226,169,178,194,177,194,177,226,168,166,226,168,167, + 194,177,226,168,149,240,157,149,161,194,163,194,163,226,137,186, + 226,170,179,226,170,183,226,137,188,226,170,175,226,137,186,226, + 170,183,226,137,188,226,170,175,226,170,185,226,170,181,226,139, + 168,226,137,190,226,128,178,226,132,153,226,170,181,226,170,185, + 226,139,168,226,136,143,226,140,174,226,140,146,226,140,147,226, + 136,157,226,136,157,226,137,190,226,138,176,240,157,147,133,207, + 136,226,128,136,240,157,148,174,226,168,140,240,157,149,162,226, + 129,151,240,157,147,134,226,132,141,226,168,150,63,226,137,159, + 34,34,226,135,155,226,135,146,226,164,156,226,164,143,226,165, + 164,226,136,189,204,177,197,149,226,136,154,226,166,179,226,159, + 169,226,166,146,226,166,165,226,159,169,194,187,194,187,226,134, + 146,226,165,181,226,135,165,226,164,160,226,164,179,226,164,158, + 226,134,170,226,134,172,226,165,133,226,165,180,226,134,163,226, + 134,157,226,164,154,226,136,182,226,132,154,226,164,141,226,157, + 179,125,93,226,166,140,226,166,142,226,166,144,197,153,197,151, + 226,140,137,125,209,128,226,164,183,226,165,169,226,128,157,226, + 128,157,226,134,179,226,132,156,226,132,155,226,132,156,226,132, + 157,226,150,173,194,174,194,174,226,165,189,226,140,139,240,157, + 148,175,226,135,129,226,135,128,226,165,172,207,129,207,177,226, + 134,146,226,134,163,226,135,129,226,135,128,226,135,132,226,135, + 140,226,135,137,226,134,157,226,139,140,203,154,226,137,147,226, + 135,132,226,135,140,226,128,143,226,142,177,226,142,177,226,171, + 174,226,159,173,226,135,190,226,159,167,226,166,134,240,157,149, + 163,226,168,174,226,168,181,41,226,166,148,226,168,146,226,135, + 137,226,128,186,240,157,147,135,226,134,177,93,226,128,153,226, + 128,153,226,139,140,226,139,138,226,150,185,226,138,181,226,150, + 184,226,167,142,226,165,168,226,132,158,197,155,226,128,154,226, + 137,187,226,170,180,226,170,184,197,161,226,137,189,226,170,176, + 197,159,197,157,226,170,182,226,170,186,226,139,169,226,168,147, + 226,137,191,209,129,226,139,133,226,138,161,226,169,166,226,135, + 152,226,164,165,226,134,152,226,134,152,194,167,194,167,59,226, + 164,169,226,136,150,226,136,150,226,156,182,240,157,148,176,226, + 140,162,226,153,175,209,137,209,136,226,136,163,226,136,165,194, + 173,194,173,207,131,207,130,207,130,226,136,188,226,169,170,226, + 137,131,226,137,131,226,170,158,226,170,160,226,170,157,226,170, + 159,226,137,134,226,168,164,226,165,178,226,134,144,226,136,150, + 226,168,179,226,167,164,226,136,163,226,140,163,226,170,170,226, + 170,172,226,170,172,239,184,128,209,140,47,226,167,132,226,140, + 191,240,157,149,164,226,153,160,226,153,160,226,136,165,226,138, + 147,226,138,147,239,184,128,226,138,148,226,138,148,239,184,128, + 226,138,143,226,138,145,226,138,143,226,138,145,226,138,144,226, + 138,146,226,138,144,226,138,146,226,150,161,226,150,161,226,150, + 170,226,150,170,226,134,146,240,157,147,136,226,136,150,226,140, + 163,226,139,134,226,152,134,226,152,133,207,181,207,149,194,175, + 226,138,130,226,171,133,226,170,189,226,138,134,226,171,131,226, + 171,129,226,171,139,226,138,138,226,170,191,226,165,185,226,138, + 130,226,138,134,226,171,133,226,138,138,226,171,139,226,171,135, + 226,171,149,226,171,147,226,137,187,226,170,184,226,137,189,226, + 170,176,226,170,186,226,170,182,226,139,169,226,137,191,226,136, + 145,226,153,170,194,185,194,185,194,178,194,178,194,179,194,179, + 226,138,131,226,171,134,226,170,190,226,171,152,226,138,135,226, + 171,132,226,159,137,226,171,151,226,165,187,226,171,130,226,171, + 140,226,138,139,226,171,128,226,138,131,226,138,135,226,171,134, + 226,138,139,226,171,140,226,171,136,226,171,148,226,171,150,226, + 135,153,226,164,166,226,134,153,226,134,153,226,164,170,195,159, + 195,159,226,140,150,207,132,226,142,180,197,165,197,163,209,130, + 226,131,155,226,140,149,240,157,148,177,226,136,180,226,136,180, + 206,184,207,145,207,145,226,137,136,226,136,188,226,128,137,226, + 137,136,226,136,188,195,190,195,190,203,156,195,151,195,151,226, + 138,160,226,168,177,226,168,176,226,136,173,226,164,168,226,138, + 164,226,140,182,226,171,177,240,157,149,165,226,171,154,226,164, + 169,226,128,180,226,132,162,226,150,181,226,150,191,226,151,131, + 226,138,180,226,137,156,226,150,185,226,138,181,226,151,172,226, + 137,156,226,168,186,226,168,185,226,167,141,226,168,187,226,143, + 162,240,157,147,137,209,134,209,155,197,167,226,137,172,226,134, + 158,226,134,160,226,135,145,226,165,163,195,186,195,186,226,134, + 145,209,158,197,173,195,187,195,187,209,131,226,135,133,197,177, + 226,165,174,226,165,190,240,157,148,178,195,185,195,185,226,134, + 191,226,134,190,226,150,128,226,140,156,226,140,156,226,140,143, + 226,151,184,197,171,194,168,194,168,197,179,240,157,149,166,226, + 134,145,226,134,149,226,134,191,226,134,190,226,138,142,207,133, + 207,146,207,133,226,135,136,226,140,157,226,140,157,226,140,142, + 197,175,226,151,185,240,157,147,138,226,139,176,197,169,226,150, + 181,226,150,180,226,135,136,195,188,195,188,226,166,167,226,135, + 149,226,171,168,226,171,169,226,138,168,226,166,156,207,181,207, + 176,226,136,133,207,149,207,150,226,136,157,226,134,149,207,177, + 207,130,226,138,138,239,184,128,226,171,139,239,184,128,226,138, + 139,239,184,128,226,171,140,239,184,128,207,145,226,138,178,226, + 138,179,208,178,226,138,162,226,136,168,226,138,187,226,137,154, + 226,139,174,124,124,240,157,148,179,226,138,178,226,138,130,226, + 131,146,226,138,131,226,131,146,240,157,149,167,226,136,157,226, + 138,179,240,157,147,139,226,171,139,239,184,128,226,138,138,239, + 184,128,226,171,140,239,184,128,226,138,139,239,184,128,226,166, + 154,197,181,226,169,159,226,136,167,226,137,153,226,132,152,240, + 157,148,180,240,157,149,168,226,132,152,226,137,128,226,137,128, + 240,157,147,140,226,139,130,226,151,175,226,139,131,226,150,189, + 240,157,148,181,226,159,186,226,159,183,206,190,226,159,184,226, + 159,181,226,159,188,226,139,187,226,168,128,240,157,149,169,226, + 168,129,226,168,130,226,159,185,226,159,182,240,157,147,141,226, + 168,134,226,168,132,226,150,179,226,139,129,226,139,128,195,189, + 195,189,209,143,197,183,209,139,194,165,194,165,240,157,148,182, + 209,151,240,157,149,170,240,157,147,142,209,142,195,191,195,191, + 197,186,197,190,208,183,197,188,226,132,168,206,182,240,157,148, + 183,208,182,226,135,157,240,157,149,171,240,157,147,143,226,128, + 141,226,128,140, +}; + +struct EntityMeta { + uint16_t name_off; // offset into kNamePool + uint16_t name_len; // length in bytes (UTF-8 == ASCII for entity names) + uint16_t value_off; // offset into kValuePool + uint8_t value_len; // length in bytes (UTF-8) +}; + +extern const size_t kEntityCount; +const size_t kEntityCount = 2231; + +extern const EntityMeta kEntities[2231]; +const EntityMeta kEntities[2231] = { + {0,5,0,2}, + {5,6,2,2}, + {11,3,4,1}, + {14,4,5,1}, + {18,6,6,2}, + {24,7,8,2}, + {31,7,10,2}, + {38,5,12,2}, + {43,6,14,2}, + {49,4,16,2}, + {53,4,18,4}, + {57,6,22,2}, + {63,7,24,2}, + {70,6,26,2}, + {76,6,28,2}, + {82,4,30,3}, + {86,6,33,2}, + {92,5,35,4}, + {97,14,39,3}, + {111,5,42,2}, + {116,6,44,2}, + {122,5,46,4}, + {127,7,50,3}, + {134,6,53,2}, + {140,7,55,2}, + {147,4,57,2}, + {151,5,59,2}, + {156,10,61,3}, + {166,5,64,3}, + {171,7,67,3}, + {178,4,70,2}, + {182,8,72,3}, + {190,11,75,3}, + {201,5,78,2}, + {206,4,80,4}, + {210,5,84,4}, + {215,6,88,2}, + {221,5,90,3}, + {226,7,93,3}, + {233,5,96,2}, + {238,4,98,2}, + {242,5,100,2}, + {247,7,102,2}, + {254,4,104,3}, + {258,21,107,3}, + {279,8,110,3}, + {287,7,113,2}, + {294,6,115,2}, + {300,7,117,2}, + {307,6,119,2}, + {313,8,121,3}, + {321,5,124,2}, + {326,8,126,2}, + {334,10,128,2}, + {344,4,130,3}, + {348,4,133,2}, + {352,10,135,3}, + {362,12,138,3}, + {374,11,141,3}, + {385,12,144,3}, + {397,25,147,3}, + {422,22,150,3}, + {444,16,153,3}, + {460,6,156,3}, + {466,7,159,3}, + {473,10,162,3}, + {483,7,165,3}, + {490,16,168,3}, + {506,5,171,3}, + {511,10,174,3}, + {521,32,177,3}, + {553,6,180,3}, + {559,5,183,4}, + {564,4,187,3}, + {568,7,190,3}, + {575,3,193,3}, + {578,9,196,3}, + {587,5,199,2}, + {592,5,201,2}, + {597,5,203,2}, + {602,7,205,3}, + {609,5,208,3}, + {614,6,211,3}, + {620,7,214,2}, + {627,4,216,2}, + {631,4,218,3}, + {635,6,221,2}, + {641,4,223,4}, + {645,17,227,2}, + {662,15,229,2}, + {677,23,231,2}, + {700,17,233,1}, + {717,17,234,2}, + {734,8,236,3}, + {742,14,239,3}, + {756,5,242,4}, + {761,4,246,2}, + {765,7,248,3}, + {772,9,251,3}, + {781,22,254,3}, + {803,10,257,2}, + {813,16,259,3}, + {829,16,262,3}, + {845,21,265,3}, + {866,14,268,3}, + {880,20,271,3}, + {900,25,274,3}, + {925,21,277,3}, + {946,17,280,3}, + {963,15,283,3}, + {978,14,286,3}, + {992,18,289,3}, + {1010,18,292,3}, + {1028,10,295,3}, + {1038,13,298,3}, + {1051,17,301,3}, + {1068,10,304,2}, + {1078,20,306,3}, + {1098,18,309,3}, + {1116,15,312,3}, + {1131,18,315,3}, + {1149,19,318,3}, + {1168,16,321,3}, + {1184,19,324,3}, + {1203,8,327,3}, + {1211,13,330,3}, + {1224,10,333,3}, + {1234,5,336,4}, + {1239,7,340,2}, + {1246,4,342,2}, + {1250,3,344,2}, + {1253,4,346,2}, + {1257,6,348,2}, + {1263,7,350,2}, + {1270,7,352,2}, + {1277,5,354,2}, + {1282,6,356,2}, + {1288,4,358,2}, + {1292,5,360,2}, + {1297,4,362,4}, + {1301,6,366,2}, + {1307,7,368,2}, + {1314,8,370,3}, + {1322,6,373,2}, + {1328,17,375,3}, + {1345,21,378,3}, + {1366,6,381,2}, + {1372,5,383,4}, + {1377,8,387,2}, + {1385,6,389,3}, + {1391,11,392,3}, + {1402,12,395,3}, + {1414,5,398,3}, + {1419,5,401,3}, + {1424,4,404,2}, + {1428,4,406,2}, + {1432,5,408,2}, + {1437,7,410,3}, + {1444,13,413,3}, + {1457,4,416,2}, + {1461,4,418,4}, + {1465,18,422,3}, + {1483,22,425,3}, + {1505,5,428,4}, + {1510,7,432,3}, + {1517,11,435,3}, + {1528,5,438,3}, + {1533,5,441,2}, + {1538,2,443,1}, + {1540,3,444,1}, + {1543,6,445,2}, + {1549,7,447,2}, + {1556,7,449,2}, + {1563,7,451,2}, + {1570,6,453,2}, + {1576,4,455,2}, + {1580,5,457,2}, + {1585,4,459,4}, + {1589,3,463,3}, + {1592,5,466,4}, + {1597,13,470,3}, + {1610,17,473,3}, + {1627,17,476,3}, + {1644,15,479,3}, + {1659,12,482,3}, + {1671,18,485,3}, + {1689,13,488,3}, + {1702,5,491,4}, + {1707,3,495,3}, + {1710,7,498,2}, + {1717,6,500,2}, + {1723,4,502,1}, + {1727,6,503,2}, + {1733,4,505,3}, + {1737,13,508,3}, + {1750,5,511,3}, + {1755,15,514,3}, + {1770,5,517,3}, + {1775,7,520,2}, + {1782,13,522,3}, + {1795,10,525,3}, + {1805,5,528,2}, + {1810,6,530,2}, + {1816,5,532,2}, + {1821,6,534,2}, + {1827,7,536,2}, + {1834,5,538,2}, + {1839,6,540,2}, + {1845,4,542,2}, + {1849,5,544,2}, + {1854,4,546,3}, + {1858,6,549,2}, + {1864,7,551,2}, + {1871,3,553,3}, + {1874,6,556,2}, + {1880,11,558,3}, + {1891,8,561,3}, + {1899,4,564,3}, + {1903,9,567,3}, + {1912,13,570,3}, + {1925,15,573,3}, + {1940,15,576,3}, + {1955,6,579,2}, + {1961,5,581,4}, + {1966,5,585,2}, + {1971,5,587,3}, + {1976,7,590,2}, + {1983,6,592,2}, + {1989,4,594,2}, + {1993,5,596,2}, + {1998,6,598,2}, + {2004,4,600,2}, + {2008,4,602,4}, + {2012,5,606,4}, + {2017,5,610,4}, + {2022,7,614,2}, + {2029,6,616,2}, + {2035,5,618,2}, + {2040,5,620,2}, + {2045,6,622,2}, + {2051,7,624,2}, + {2058,4,626,2}, + {2062,4,628,4}, + {2066,5,632,4}, + {2071,5,636,4}, + {2076,5,640,2}, + {2081,2,642,1}, + {2083,3,643,1}, + {2086,7,644,2}, + {2093,7,646,2}, + {2100,5,648,3}, + {2105,11,651,3}, + {2116,5,654,3}, + {2121,7,657,2}, + {2128,7,659,2}, + {2135,4,661,2}, + {2139,17,663,3}, + {2156,10,666,3}, + {2166,13,669,3}, + {2179,20,672,3}, + {2199,12,675,3}, + {2211,18,678,3}, + {2229,18,681,3}, + {2247,15,684,3}, + {2262,18,687,3}, + {2280,10,690,3}, + {2290,15,693,3}, + {2305,16,696,3}, + {2321,8,699,3}, + {2329,13,702,3}, + {2342,14,705,3}, + {2356,13,708,3}, + {2369,16,711,3}, + {2385,18,714,3}, + {2403,17,717,3}, + {2420,16,720,3}, + {2436,13,723,3}, + {2449,16,726,3}, + {2465,11,729,3}, + {2476,14,732,3}, + {2490,10,735,3}, + {2500,15,738,3}, + {2515,17,741,3}, + {2532,14,744,3}, + {2546,12,747,3}, + {2558,9,750,3}, + {2567,15,753,3}, + {2582,10,756,3}, + {2592,4,759,4}, + {2596,3,763,3}, + {2599,11,766,3}, + {2610,7,769,2}, + {2617,14,771,3}, + {2631,19,774,3}, + {2650,15,777,3}, + {2665,14,780,3}, + {2679,19,783,3}, + {2698,15,786,3}, + {2713,5,789,4}, + {2718,15,793,3}, + {2733,16,796,3}, + {2749,5,799,3}, + {2754,4,802,3}, + {2758,7,805,2}, + {2765,3,807,3}, + {2768,4,810,3}, + {2772,4,813,2}, + {2776,12,815,3}, + {2788,10,818,3}, + {2798,4,821,4}, + {2802,10,825,3}, + {2812,5,828,4}, + {2817,5,832,3}, + {2822,3,835,2}, + {2825,5,837,2}, + {2830,7,839,2}, + {2837,7,841,2}, + {2844,7,843,2}, + {2851,4,845,2}, + {2855,20,847,3}, + {2875,19,850,3}, + {2894,18,853,3}, + {2912,22,856,3}, + {2934,21,859,3}, + {2955,15,862,3}, + {2970,8,865,1}, + {2978,4,866,4}, + {2982,8,870,3}, + {2990,17,873,2}, + {3007,5,875,3}, + {3012,4,878,3}, + {3016,13,881,3}, + {3029,10,884,3}, + {3039,21,887,3}, + {3060,11,890,3}, + {3071,9,893,3}, + {3080,14,896,5}, + {3094,10,901,3}, + {3104,11,904,3}, + {3115,16,907,3}, + {3131,20,910,5}, + {3151,18,915,5}, + {3169,15,920,3}, + {3184,21,923,5}, + {3205,16,928,3}, + {3221,16,931,5}, + {3237,13,936,5}, + {3250,16,941,3}, + {3266,19,944,5}, + {3285,21,949,3}, + {3306,8,952,3}, + {3314,13,955,3}, + {3327,15,958,3}, + {3342,12,961,5}, + {3354,18,966,5}, + {3372,13,971,3}, + {3385,24,974,5}, + {3409,18,979,5}, + {3427,12,984,3}, + {3439,17,987,5}, + {3456,22,992,3}, + {3478,18,995,3}, + {3496,17,998,3}, + {3513,20,1001,5}, + {3533,22,1006,3}, + {3555,16,1009,5}, + {3571,21,1014,3}, + {3592,18,1017,5}, + {3610,23,1022,3}, + {3633,10,1025,6}, + {3643,15,1031,3}, + {3658,12,1034,3}, + {3670,17,1037,5}, + {3687,22,1042,3}, + {3709,17,1045,5}, + {3726,12,1050,6}, + {3738,17,1056,3}, + {3755,9,1059,3}, + {3764,14,1062,3}, + {3778,18,1065,3}, + {3796,14,1068,3}, + {3810,15,1071,3}, + {3825,5,1074,4}, + {3830,6,1078,2}, + {3836,7,1080,2}, + {3843,3,1082,2}, + {3846,6,1084,2}, + {3852,6,1086,2}, + {3858,7,1088,2}, + {3865,5,1090,2}, + {3870,6,1092,2}, + {3876,4,1094,2}, + {3880,7,1096,2}, + {3887,4,1098,4}, + {3891,6,1102,2}, + {3897,7,1104,2}, + {3904,6,1106,2}, + {3910,6,1108,2}, + {3916,8,1110,2}, + {3924,5,1112,4}, + {3929,21,1116,3}, + {3950,15,1119,3}, + {3965,3,1122,3}, + {3968,5,1125,4}, + {3973,6,1129,2}, + {3979,7,1131,2}, + {3986,6,1133,2}, + {3992,7,1135,2}, + {3999,7,1137,3}, + {4006,4,1140,2}, + {4010,5,1142,2}, + {4015,8,1144,3}, + {4023,10,1147,3}, + {4033,12,1150,3}, + {4045,16,1153,3}, + {4061,9,1156,3}, + {4070,4,1159,2}, + {4074,4,1161,4}, + {4078,4,1165,2}, + {4082,3,1167,2}, + {4085,10,1169,2}, + {4095,14,1171,3}, + {4109,5,1174,3}, + {4114,3,1177,3}, + {4117,9,1180,3}, + {4126,14,1183,3}, + {4140,19,1186,3}, + {4159,14,1189,3}, + {4173,6,1192,3}, + {4179,8,1195,3}, + {4187,11,1198,3}, + {4198,13,1201,3}, + {4211,5,1204,4}, + {4216,4,1208,2}, + {4220,4,1210,1}, + {4224,5,1211,1}, + {4229,4,1212,4}, + {4233,5,1216,3}, + {4238,5,1219,4}, + {4243,6,1223,3}, + {4249,3,1226,2}, + {4252,4,1228,2}, + {4256,7,1230,2}, + {4263,5,1232,3}, + {4268,5,1235,3}, + {4273,7,1238,3}, + {4280,7,1241,2}, + {4287,7,1243,2}, + {4294,4,1245,2}, + {4298,3,1247,3}, + {4301,15,1250,3}, + {4316,19,1253,3}, + {4335,21,1256,3}, + {4356,4,1259,3}, + {4360,4,1262,2}, + {4364,18,1264,3}, + {4382,11,1267,3}, + {4393,14,1270,3}, + {4407,20,1273,3}, + {4427,13,1276,3}, + {4440,19,1279,3}, + {4459,19,1282,3}, + {4478,16,1285,3}, + {4494,19,1288,3}, + {4513,11,1291,3}, + {4524,9,1294,3}, + {4533,14,1297,3}, + {4547,15,1300,3}, + {4562,14,1303,3}, + {4576,17,1306,3}, + {4593,19,1309,3}, + {4612,18,1312,3}, + {4630,17,1315,3}, + {4647,14,1318,3}, + {4661,17,1321,3}, + {4678,12,1324,3}, + {4690,15,1327,3}, + {4705,11,1330,3}, + {4716,5,1333,3}, + {4721,13,1336,3}, + {4734,12,1339,3}, + {4746,5,1342,3}, + {4751,4,1345,3}, + {4755,12,1348,3}, + {4767,7,1351,2}, + {4774,5,1353,2}, + {4779,7,1355,2}, + {4786,7,1357,2}, + {4793,3,1359,3}, + {4796,7,1362,2}, + {4803,7,1364,2}, + {4810,6,1366,2}, + {4816,4,1368,2}, + {4820,4,1370,4}, + {4824,15,1374,3}, + {4839,15,1377,3}, + {4854,16,1380,3}, + {4870,13,1383,3}, + {4883,6,1386,2}, + {4889,12,1388,3}, + {4901,5,1391,4}, + {4906,5,1395,3}, + {4911,7,1398,3}, + {4918,19,1401,3}, + {4937,13,1404,3}, + {4950,18,1407,3}, + {4968,15,1410,3}, + {4983,20,1413,3}, + {5003,12,1416,3}, + {5015,5,1419,4}, + {5020,5,1423,3}, + {5025,4,1426,3}, + {5029,7,1429,3}, + {5036,12,1432,3}, + {5048,9,1435,3}, + {5057,14,1438,3}, + {5071,19,1441,3}, + {5090,14,1444,3}, + {5104,9,1447,3}, + {5113,4,1450,3}, + {5117,4,1453,3}, + {5121,9,1456,3}, + {5130,14,1459,3}, + {5144,7,1462,3}, + {5151,5,1465,2}, + {5156,6,1467,2}, + {5162,6,1469,3}, + {5168,6,1472,2}, + {5174,5,1474,2}, + {5179,4,1476,1}, + {5183,4,1477,2}, + {5187,7,1479,2}, + {5194,7,1481,2}, + {5201,4,1483,2}, + {5205,4,1485,4}, + {5209,10,1489,3}, + {5219,6,1492,2}, + {5225,11,1494,6}, + {5236,10,1500,3}, + {5246,6,1503,3}, + {5252,11,1506,3}, + {5263,15,1509,3}, + {5278,11,1512,3}, + {5289,5,1515,4}, + {5294,10,1519,3}, + {5304,5,1522,4}, + {5309,7,1526,2}, + {5316,6,1528,2}, + {5322,7,1530,2}, + {5329,5,1532,3}, + {5334,9,1535,3}, + {5343,6,1538,2}, + {5349,7,1540,2}, + {5356,5,1542,2}, + {5361,6,1544,2}, + {5367,4,1546,2}, + {5371,7,1548,2}, + {5378,4,1550,4}, + {5382,6,1554,2}, + {5388,7,1556,2}, + {5395,6,1558,2}, + {5401,9,1560,1}, + {5410,11,1561,3}, + {5421,13,1564,3}, + {5434,17,1567,3}, + {5451,6,1570,3}, + {5457,10,1573,3}, + {5467,6,1576,2}, + {5473,5,1578,4}, + {5478,8,1582,3}, + {5486,11,1585,3}, + {5497,17,1588,3}, + {5514,12,1591,3}, + {5526,14,1594,3}, + {5540,6,1597,3}, + {5546,11,1600,3}, + {5557,8,1603,3}, + {5565,12,1606,3}, + {5577,15,1609,3}, + {5592,16,1612,3}, + {5608,5,1615,2}, + {5613,8,1617,2}, + {5621,6,1619,2}, + {5627,5,1621,4}, + {5632,7,1625,2}, + {5639,4,1627,2}, + {5643,5,1629,2}, + {5648,6,1631,3}, + {5654,5,1634,3}, + {5659,4,1637,2}, + {5663,6,1639,3}, + {5669,7,1642,3}, + {5676,4,1645,3}, + {5680,7,1648,3}, + {5687,5,1651,3}, + {5692,12,1654,3}, + {5704,13,1657,1}, + {5717,18,1658,3}, + {5735,14,1661,3}, + {5749,14,1664,3}, + {5763,4,1667,4}, + {5767,5,1671,4}, + {5772,5,1675,4}, + {5777,7,1679,3}, + {5784,6,1682,2}, + {5790,6,1684,3}, + {5796,4,1687,4}, + {5800,5,1691,4}, + {5805,5,1695,4}, + {5810,4,1699,4}, + {5814,3,1703,2}, + {5817,5,1705,4}, + {5822,5,1709,4}, + {5827,5,1713,2}, + {5832,5,1715,2}, + {5837,5,1717,2}, + {5842,6,1719,2}, + {5848,7,1721,2}, + {5855,6,1723,2}, + {5861,4,1725,2}, + {5865,4,1727,4}, + {5869,5,1731,4}, + {5874,5,1735,4}, + {5879,5,1739,2}, + {5884,5,1741,2}, + {5889,7,1743,2}, + {5896,7,1745,2}, + {5903,4,1747,2}, + {5907,5,1749,2}, + {5912,15,1751,3}, + {5927,5,1754,2}, + {5932,4,1756,3}, + {5936,5,1759,3}, + {5941,5,1762,4}, + {5946,6,1766,2}, + {5952,7,1768,2}, + {5959,7,1770,2}, + {5966,3,1772,3}, + {5969,4,1775,5}, + {5973,4,1780,3}, + {5977,5,1783,2}, + {5982,6,1785,2}, + {5988,5,1787,2}, + {5993,6,1789,2}, + {5999,4,1791,2}, + {6003,5,1793,2}, + {6008,6,1795,2}, + {6014,3,1797,3}, + {6017,4,1800,4}, + {6021,6,1804,2}, + {6027,7,1806,2}, + {6034,8,1808,3}, + {6042,6,1811,3}, + {6048,6,1814,2}, + {6054,6,1816,2}, + {6060,6,1818,3}, + {6066,3,1821,1}, + {6069,4,1822,1}, + {6073,4,1823,3}, + {6077,7,1826,3}, + {6084,5,1829,3}, + {6089,9,1832,3}, + {6098,5,1835,3}, + {6103,4,1838,3}, + {6107,5,1841,3}, + {6112,6,1844,3}, + {6118,7,1847,3}, + {6125,9,1850,3}, + {6134,9,1853,3}, + {6143,9,1856,3}, + {6152,9,1859,3}, + {6161,9,1862,3}, + {6170,9,1865,3}, + {6179,9,1868,3}, + {6188,9,1871,3}, + {6197,6,1874,3}, + {6203,8,1877,3}, + {6211,9,1880,3}, + {6220,7,1883,3}, + {6227,6,1886,2}, + {6233,8,1888,3}, + {6241,6,1891,2}, + {6247,5,1893,4}, + {6252,3,1897,3}, + {6255,4,1900,3}, + {6259,7,1903,3}, + {6266,4,1906,3}, + {6270,5,1909,3}, + {6275,5,1912,1}, + {6280,7,1913,3}, + {6287,9,1916,3}, + {6296,5,1919,2}, + {6301,6,1921,2}, + {6307,5,1923,4}, + {6312,4,1927,1}, + {6316,6,1928,3}, + {6322,8,1931,3}, + {6330,6,1934,2}, + {6336,7,1936,2}, + {6343,4,1938,2}, + {6347,5,1940,2}, + {6352,9,1942,3}, + {6361,6,1945,3}, + {6367,5,1948,3}, + {6372,9,1951,3}, + {6381,12,1954,2}, + {6393,10,1956,3}, + {6403,8,1959,3}, + {6411,10,1962,3}, + {6421,7,1965,3}, + {6428,7,1968,3}, + {6435,9,1971,3}, + {6444,5,1974,3}, + {6449,9,1977,3}, + {6458,6,1980,3}, + {6464,4,1983,2}, + {6468,6,1985,3}, + {6474,7,1988,3}, + {6481,8,1991,3}, + {6489,8,1994,3}, + {6497,6,1997,2}, + {6503,7,1999,3}, + {6510,5,2002,2}, + {6515,5,2004,3}, + {6520,8,2007,3}, + {6528,4,2010,4}, + {6532,7,2014,3}, + {6539,8,2017,3}, + {6547,7,2020,3}, + {6554,8,2023,3}, + {6562,9,2026,3}, + {6571,10,2029,3}, + {6581,9,2032,3}, + {6590,8,2035,3}, + {6598,16,2038,3}, + {6614,14,2041,3}, + {6628,9,2044,3}, + {6637,7,2047,3}, + {6644,9,2050,3}, + {6653,7,2053,3}, + {6660,13,2056,3}, + {6673,12,2059,3}, + {6685,14,2062,3}, + {6699,18,2065,3}, + {6717,18,2068,3}, + {6735,19,2071,3}, + {6754,6,2074,3}, + {6760,6,2077,3}, + {6766,6,2080,3}, + {6772,6,2083,3}, + {6778,6,2086,3}, + {6784,4,2089,4}, + {6788,8,2093,6}, + {6796,5,2099,3}, + {6801,5,2102,4}, + {6806,4,2106,3}, + {6810,7,2109,3}, + {6817,7,2112,3}, + {6824,6,2115,3}, + {6830,6,2118,3}, + {6836,6,2121,3}, + {6842,6,2124,3}, + {6848,5,2127,3}, + {6853,6,2130,3}, + {6859,6,2133,3}, + {6865,6,2136,3}, + {6871,6,2139,3}, + {6877,6,2142,3}, + {6883,6,2145,3}, + {6889,6,2148,3}, + {6895,6,2151,3}, + {6901,5,2154,3}, + {6906,6,2157,3}, + {6912,6,2160,3}, + {6918,6,2163,3}, + {6924,6,2166,3}, + {6930,6,2169,3}, + {6936,6,2172,3}, + {6942,7,2175,3}, + {6949,6,2178,3}, + {6955,6,2181,3}, + {6961,6,2184,3}, + {6967,6,2187,3}, + {6973,5,2190,3}, + {6978,6,2193,3}, + {6984,6,2196,3}, + {6990,6,2199,3}, + {6996,6,2202,3}, + {7002,9,2205,3}, + {7011,8,2208,3}, + {7019,9,2211,3}, + {7028,6,2214,3}, + {7034,6,2217,3}, + {7040,6,2220,3}, + {7046,6,2223,3}, + {7052,5,2226,3}, + {7057,6,2229,3}, + {7063,6,2232,3}, + {7069,6,2235,3}, + {7075,6,2238,3}, + {7081,6,2241,3}, + {7087,6,2244,3}, + {7093,7,2247,3}, + {7100,6,2250,2}, + {7106,6,2252,2}, + {7112,7,2254,2}, + {7119,5,2256,4}, + {7124,6,2260,3}, + {7130,5,2263,3}, + {7135,6,2266,3}, + {7141,5,2269,1}, + {7146,6,2270,3}, + {7152,9,2273,3}, + {7161,5,2276,3}, + {7166,7,2279,3}, + {7173,5,2282,3}, + {7178,6,2285,3}, + {7184,6,2288,3}, + {7190,7,2291,3}, + {7197,7,2294,2}, + {7204,4,2296,3}, + {7208,7,2299,3}, + {7215,9,2302,3}, + {7224,7,2305,3}, + {7231,7,2308,3}, + {7238,7,2311,3}, + {7245,5,2314,6}, + {7250,6,2320,3}, + {7256,6,2323,2}, + {7262,6,2325,3}, + {7268,7,2328,2}, + {7275,6,2330,2}, + {7281,7,2332,2}, + {7288,6,2334,2}, + {7294,6,2336,3}, + {7300,8,2339,3}, + {7308,5,2342,2}, + {7313,5,2344,2}, + {7318,6,2346,2}, + {7324,8,2348,3}, + {7332,4,2351,2}, + {7336,5,2353,2}, + {7341,10,2355,2}, + {7351,4,2357,4}, + {7355,5,2361,2}, + {7360,6,2363,3}, + {7366,10,2366,3}, + {7376,4,2369,2}, + {7380,4,2371,3}, + {7384,5,2374,3}, + {7389,5,2377,2}, + {7394,7,2379,3}, + {7401,16,2382,3}, + {7417,17,2385,3}, + {7434,9,2388,2}, + {7443,9,2390,3}, + {7452,11,2393,3}, + {7463,12,2396,3}, + {7475,12,2399,3}, + {7487,5,2402,3}, + {7492,9,2405,3}, + {7501,7,2408,3}, + {7508,8,2411,3}, + {7516,6,2414,3}, + {7522,9,2417,3}, + {7531,6,2420,1}, + {7537,7,2421,3}, + {7544,8,2424,3}, + {7552,6,2427,1}, + {7558,7,2428,1}, + {7565,5,2429,3}, + {7570,7,2432,3}, + {7577,11,2435,3}, + {7588,10,2438,3}, + {7598,5,2441,3}, + {7603,8,2444,3}, + {7611,7,2447,3}, + {7618,5,2450,4}, + {7623,7,2454,3}, + {7630,4,2457,2}, + {7634,5,2459,2}, + {7639,7,2461,3}, + {7646,6,2464,3}, + {7652,6,2467,3}, + {7658,5,2470,4}, + {7663,5,2474,3}, + {7668,6,2477,3}, + {7674,5,2480,3}, + {7679,6,2483,3}, + {7685,6,2486,3}, + {7691,8,2489,3}, + {7699,8,2492,3}, + {7707,6,2495,3}, + {7713,6,2498,3}, + {7719,7,2501,3}, + {7726,8,2504,3}, + {7734,4,2507,3}, + {7738,9,2510,3}, + {7747,7,2513,3}, + {7754,7,2516,3}, + {7761,7,2519,3}, + {7768,6,2522,3}, + {7774,5,2525,6}, + {7779,7,2531,3}, + {7786,8,2534,3}, + {7794,12,2537,3}, + {7806,12,2540,3}, + {7818,9,2543,3}, + {7827,11,2546,3}, + {7838,6,2549,2}, + {7844,7,2551,2}, + {7851,15,2553,3}, + {7866,16,2556,3}, + {7882,6,2559,3}, + {7888,6,2562,3}, + {7894,9,2565,3}, + {7903,6,2568,3}, + {7909,7,2571,3}, + {7916,5,2574,3}, + {7921,5,2577,3}, + {7926,7,2580,3}, + {7933,7,2583,3}, + {7940,5,2586,3}, + {7945,5,2589,3}, + {7950,6,2592,3}, + {7956,8,2595,3}, + {7964,6,2598,2}, + {7970,7,2600,2}, + {7977,4,2602,2}, + {7981,3,2604,3}, + {7984,8,2607,3}, + {7992,6,2610,3}, + {7998,8,2613,3}, + {8006,3,2616,2}, + {8009,4,2618,2}, + {8013,6,2620,2}, + {8019,8,2622,3}, + {8027,7,2625,3}, + {8034,4,2628,4}, + {8038,6,2632,3}, + {8044,6,2635,3}, + {8050,5,2638,3}, + {8055,8,2641,3}, + {8063,12,2644,3}, + {8075,6,2647,3}, + {8081,4,2650,2}, + {8085,8,2652,2}, + {8093,6,2654,3}, + {8099,4,2657,2}, + {8103,6,2659,2}, + {8109,7,2661,2}, + {8116,14,2663,3}, + {8130,7,2666,3}, + {8137,5,2669,2}, + {8142,7,2671,3}, + {8149,7,2674,3}, + {8156,7,2677,1}, + {8163,5,2678,4}, + {8168,4,2682,2}, + {8172,6,2684,3}, + {8178,9,2687,3}, + {8187,9,2690,3}, + {8196,8,2693,3}, + {8204,10,2696,3}, + {8214,15,2699,3}, + {8229,10,2702,3}, + {8239,15,2705,3}, + {8254,16,2708,3}, + {8270,17,2711,3}, + {8287,9,2714,3}, + {8296,7,2717,3}, + {8303,7,2720,3}, + {8310,5,2723,4}, + {8315,5,2727,2}, + {8320,5,2729,3}, + {8325,7,2732,2}, + {8332,6,2734,3}, + {8338,5,2737,3}, + {8343,6,2740,3}, + {8349,6,2743,3}, + {8355,6,2746,3}, + {8361,8,2749,3}, + {8369,5,2752,2}, + {8374,9,2754,3}, + {8383,6,2757,3}, + {8389,5,2760,3}, + {8394,6,2763,2}, + {8400,7,2765,2}, + {8407,7,2767,3}, + {8414,7,2770,2}, + {8421,5,2772,3}, + {8426,5,2775,2}, + {8431,6,2777,2}, + {8437,7,2779,3}, + {8444,4,2782,2}, + {8448,5,2784,2}, + {8453,3,2786,3}, + {8456,6,2789,3}, + {8462,4,2792,4}, + {8466,3,2796,3}, + {8469,6,2799,2}, + {8475,7,2801,2}, + {8482,4,2803,3}, + {8486,7,2806,3}, + {8493,3,2809,3}, + {8496,9,2812,3}, + {8505,4,2815,3}, + {8509,4,2818,3}, + {8513,7,2821,3}, + {8520,6,2824,2}, + {8526,6,2826,3}, + {8532,9,2829,3}, + {8541,7,2832,3}, + {8548,7,2835,3}, + {8555,7,2838,3}, + {8562,5,2841,3}, + {8567,4,2844,2}, + {8571,5,2846,3}, + {8576,6,2849,2}, + {8582,5,2851,4}, + {8587,5,2855,3}, + {8592,7,2858,3}, + {8599,6,2861,3}, + {8605,5,2864,2}, + {8610,8,2866,2}, + {8618,6,2868,2}, + {8624,7,2870,3}, + {8631,8,2873,3}, + {8639,6,2876,3}, + {8645,11,2879,3}, + {8656,12,2882,3}, + {8668,7,2885,1}, + {8675,7,2886,3}, + {8682,6,2889,3}, + {8688,8,2892,3}, + {8696,9,2895,3}, + {8705,6,2898,3}, + {8711,6,2901,3}, + {8717,5,2904,3}, + {8722,6,2907,3}, + {8728,5,2910,3}, + {8733,4,2913,2}, + {8737,3,2915,2}, + {8740,4,2917,2}, + {8744,4,2919,2}, + {8748,5,2921,2}, + {8753,5,2923,3}, + {8758,5,2926,1}, + {8763,6,2927,3}, + {8769,12,2930,3}, + {8781,13,2933,3}, + {8794,14,2936,3}, + {8808,4,2939,2}, + {8812,7,2941,3}, + {8819,7,2944,3}, + {8826,6,2947,3}, + {8832,7,2950,3}, + {8839,4,2953,4}, + {8843,6,2957,3}, + {8849,6,2960,2}, + {8855,5,2962,3}, + {8860,6,2965,3}, + {8866,6,2968,3}, + {8872,5,2971,2}, + {8877,5,2973,4}, + {8882,7,2977,3}, + {8889,5,2980,3}, + {8894,6,2983,3}, + {8900,9,2986,3}, + {8909,6,2989,2}, + {8915,7,2991,2}, + {8922,7,2993,3}, + {8929,6,2996,2}, + {8935,7,2998,2}, + {8942,7,3000,3}, + {8949,7,3003,3}, + {8956,7,3006,3}, + {8963,7,3009,3}, + {8970,7,3012,3}, + {8977,6,3015,2}, + {8983,7,3017,2}, + {8990,7,3019,3}, + {8997,7,3022,3}, + {9004,7,3025,3}, + {9011,7,3028,3}, + {9018,7,3031,3}, + {9025,7,3034,3}, + {9032,6,3037,3}, + {9038,6,3040,3}, + {9044,5,3043,4}, + {9049,3,3047,3}, + {9052,4,3050,3}, + {9056,7,3053,2}, + {9063,6,3055,2}, + {9069,7,3057,2}, + {9076,4,3059,3}, + {9080,7,3062,2}, + {9087,6,3064,2}, + {9093,4,3066,2}, + {9097,5,3068,2}, + {9102,3,3070,3}, + {9105,4,3073,3}, + {9109,4,3076,3}, + {9113,5,3079,3}, + {9118,9,3082,3}, + {9127,4,3085,3}, + {9131,6,3088,3}, + {9137,7,3091,3}, + {9144,8,3094,3}, + {9152,9,3097,3}, + {9161,5,3100,6}, + {9166,7,3106,3}, + {9173,4,3109,4}, + {9177,3,3113,3}, + {9180,4,3116,3}, + {9184,6,3119,3}, + {9190,5,3122,2}, + {9195,3,3124,3}, + {9198,4,3127,3}, + {9202,4,3130,3}, + {9206,4,3133,3}, + {9210,4,3136,3}, + {9214,5,3139,3}, + {9219,9,3142,3}, + {9228,4,3145,3}, + {9232,5,3148,3}, + {9237,6,3151,3}, + {9243,6,3154,3}, + {9249,5,3157,4}, + {9254,6,3161,1}, + {9260,5,3162,3}, + {9265,5,3165,3}, + {9270,6,3168,3}, + {9276,6,3171,3}, + {9282,2,3174,1}, + {9284,3,3175,1}, + {9287,5,3176,3}, + {9292,6,3179,3}, + {9298,6,3182,3}, + {9304,7,3185,3}, + {9311,8,3188,3}, + {9319,10,3191,3}, + {9329,7,3194,3}, + {9336,7,3197,3}, + {9343,10,3200,3}, + {9353,11,3203,3}, + {9364,8,3206,3}, + {9372,7,3209,3}, + {9379,10,3212,6}, + {9389,5,3218,6}, + {9394,5,3224,3}, + {9399,7,3227,3}, + {9406,5,3230,2}, + {9411,7,3232,3}, + {9418,7,3235,2}, + {9425,5,3237,3}, + {9430,8,3240,3}, + {9438,6,3243,3}, + {9444,5,3246,3}, + {9449,6,3249,2}, + {9455,7,3251,3}, + {9462,10,3254,3}, + {9472,7,3257,3}, + {9479,7,3260,3}, + {9486,4,3263,4}, + {9490,9,3267,3}, + {9499,9,3270,3}, + {9508,6,3273,3}, + {9514,7,3276,3}, + {9521,14,3279,3}, + {9535,15,3282,3}, + {9550,5,3285,4}, + {9555,7,3289,3}, + {9562,5,3292,4}, + {9567,7,3296,3}, + {9574,7,3299,2}, + {9581,7,3301,3}, + {9588,7,3304,3}, + {9595,6,3307,2}, + {9601,7,3309,2}, + {9608,3,3311,3}, + {9611,5,3314,2}, + {9616,6,3316,2}, + {9622,4,3318,2}, + {9626,5,3320,2}, + {9631,5,3322,2}, + {9636,6,3324,2}, + {9642,4,3326,3}, + {9646,4,3329,4}, + {9650,6,3333,2}, + {9656,7,3335,2}, + {9663,3,3337,3}, + {9666,7,3340,3}, + {9673,6,3343,3}, + {9679,7,3346,3}, + {9686,6,3349,3}, + {9692,6,3352,2}, + {9698,6,3354,2}, + {9704,6,3356,3}, + {9710,9,3359,3}, + {9719,9,3362,3}, + {9728,6,3365,2}, + {9734,5,3367,3}, + {9739,6,3370,2}, + {9745,3,3372,3}, + {9748,7,3375,3}, + {9755,6,3378,3}, + {9761,9,3381,3}, + {9770,7,3384,2}, + {9777,4,3386,3}, + {9781,7,3389,3}, + {9788,9,3392,3}, + {9797,9,3395,3}, + {9806,9,3398,3}, + {9815,8,3401,3}, + {9823,5,3404,2}, + {9828,6,3406,2}, + {9834,5,3408,4}, + {9839,5,3412,2}, + {9844,6,3414,3}, + {9850,6,3417,2}, + {9856,7,3419,2}, + {9863,5,3421,4}, + {9868,5,3425,3}, + {9873,6,3428,3}, + {9879,8,3431,3}, + {9887,6,3434,3}, + {9893,7,3437,3}, + {9900,6,3440,3}, + {9906,3,3443,3}, + {9909,7,3446,2}, + {9916,6,3448,2}, + {9922,4,3450,2}, + {9926,5,3452,2}, + {9931,6,3454,2}, + {9937,4,3456,2}, + {9941,4,3458,4}, + {9945,6,3462,2}, + {9951,5,3464,4}, + {9956,5,3468,4}, + {9961,7,3472,2}, + {9968,6,3474,2}, + {9974,6,3476,2}, + {9980,7,3478,2}, + {9987,7,3480,2}, + {9994,4,3482,2}, + {9998,4,3484,4}, + {10002,7,3488,2}, + {10009,5,3490,2}, + {10014,5,3492,2}, + {10019,5,3494,4}, + {10024,5,3498,4}, + {10029,6,3502,3}, + {10035,5,3505,3}, + {10040,7,3508,3}, + {10047,6,3511,3}, + {10053,3,3514,3}, + {10056,4,3517,3}, + {10060,5,3520,3}, + {10065,7,3523,2}, + {10072,9,3525,3}, + {10081,7,3528,3}, + {10088,7,3531,2}, + {10095,5,3533,3}, + {10100,6,3536,3}, + {10106,7,3539,3}, + {10113,4,3542,3}, + {10117,5,3545,2}, + {10122,6,3547,2}, + {10128,5,3549,3}, + {10133,6,3552,3}, + {10139,8,3555,3}, + {10147,7,3558,3}, + {10154,7,3561,3}, + {10161,7,3564,3}, + {10168,7,3567,3}, + {10175,8,3570,3}, + {10183,7,3573,3}, + {10190,4,3576,3}, + {10194,7,3579,3}, + {10201,5,3582,3}, + {10206,6,3585,6}, + {10212,6,3591,3}, + {10218,6,3594,3}, + {10224,7,3597,1}, + {10231,7,3598,1}, + {10238,6,3599,3}, + {10244,8,3602,3}, + {10252,8,3605,3}, + {10260,7,3608,2}, + {10267,7,3610,2}, + {10274,6,3612,3}, + {10280,5,3615,1}, + {10285,4,3616,2}, + {10289,5,3618,3}, + {10294,6,3621,3}, + {10300,7,3624,3}, + {10307,8,3627,3}, + {10315,9,3630,3}, + {10324,5,3633,3}, + {10329,3,3636,3}, + {10332,10,3639,3}, + {10342,14,3642,3}, + {10356,16,3645,3}, + {10372,14,3648,3}, + {10386,15,3651,3}, + {10401,15,3654,3}, + {10416,16,3657,3}, + {10432,18,3660,3}, + {10450,20,3663,3}, + {10470,15,3666,3}, + {10485,4,3669,3}, + {10489,4,3672,3}, + {10493,5,3675,3}, + {10498,9,3678,3}, + {10507,4,3681,3}, + {10511,6,3684,3}, + {10517,7,3687,3}, + {10524,8,3690,3}, + {10532,9,3693,3}, + {10541,5,3696,6}, + {10546,7,3702,3}, + {10553,11,3705,3}, + {10564,8,3708,3}, + {10572,10,3711,3}, + {10582,11,3714,3}, + {10593,8,3717,3}, + {10601,8,3720,3}, + {10609,7,3723,3}, + {10616,7,3726,3}, + {10623,4,3729,4}, + {10627,3,3733,3}, + {10630,4,3736,3}, + {10634,6,3739,3}, + {10640,6,3742,3}, + {10646,7,3745,3}, + {10653,6,3748,3}, + {10659,5,3751,2}, + {10664,3,3753,3}, + {10667,6,3756,3}, + {10673,9,3759,3}, + {10682,7,3762,3}, + {10689,6,3765,3}, + {10695,7,3768,2}, + {10702,7,3770,3}, + {10709,11,3773,3}, + {10720,4,3776,3}, + {10724,5,3779,3}, + {10729,9,3782,3}, + {10738,4,3785,3}, + {10742,5,3788,3}, + {10747,6,3791,3}, + {10753,6,3794,3}, + {10759,6,3797,3}, + {10765,6,3800,3}, + {10771,6,3803,3}, + {10777,14,3806,3}, + {10791,19,3809,3}, + {10810,11,3812,3}, + {10821,15,3815,3}, + {10836,14,3818,3}, + {10850,15,3821,3}, + {10865,6,3824,3}, + {10871,5,3827,4}, + {10876,7,3831,3}, + {10883,8,3834,3}, + {10891,7,3837,3}, + {10898,7,3840,1}, + {10905,4,3841,3}, + {10909,8,3844,3}, + {10917,5,3847,3}, + {10922,5,3850,1}, + {10927,7,3851,3}, + {10934,6,3854,3}, + {10940,9,3857,3}, + {10949,6,3860,3}, + {10955,7,3863,3}, + {10962,4,3866,3}, + {10966,6,3869,3}, + {10972,7,3872,3}, + {10979,5,3875,4}, + {10984,4,3879,3}, + {10988,5,3882,3}, + {10993,6,3885,3}, + {10999,6,3888,3}, + {11005,5,3891,1}, + {11010,6,3892,3}, + {11016,7,3895,3}, + {11023,7,3898,2}, + {11030,2,3900,1}, + {11032,3,3901,1}, + {11035,5,3902,3}, + {11040,6,3905,3}, + {11046,6,3908,3}, + {11052,7,3911,3}, + {11059,7,3914,3}, + {11066,7,3917,3}, + {11073,8,3920,3}, + {11081,7,3923,3}, + {11088,5,3926,3}, + {11093,6,3929,3}, + {11099,6,3932,3}, + {11105,9,3935,3}, + {11114,8,3938,3}, + {11122,10,3941,6}, + {11132,5,3947,6}, + {11137,6,3953,3}, + {11143,4,3956,2}, + {11147,5,3958,2}, + {11152,5,3960,3}, + {11157,5,3963,3}, + {11162,8,3966,3}, + {11170,4,3969,3}, + {11174,7,3972,3}, + {11181,11,3975,3}, + {11192,11,3978,3}, + {11203,9,3981,3}, + {11212,7,3984,3}, + {11219,7,3987,3}, + {11226,4,3990,2}, + {11230,6,3992,3}, + {11236,14,3995,3}, + {11250,4,3998,4}, + {11254,4,4002,3}, + {11258,5,4005,2}, + {11263,6,4007,2}, + {11269,4,4009,3}, + {11273,7,4012,1}, + {11280,7,4013,3}, + {11287,6,4016,2}, + {11293,7,4018,2}, + {11300,6,4020,3}, + {11306,7,4023,3}, + {11313,7,4026,3}, + {11320,8,4029,3}, + {11328,5,4032,3}, + {11333,5,4035,3}, + {11338,7,4038,3}, + {11345,7,4041,3}, + {11352,5,4044,4}, + {11357,3,4048,3}, + {11360,5,4051,4}, + {11365,7,4055,3}, + {11372,3,4058,2}, + {11375,9,4060,3}, + {11384,6,4063,3}, + {11390,4,4066,5}, + {11394,4,4071,6}, + {11398,5,4077,5}, + {11403,11,4082,3}, + {11414,16,4085,3}, + {11430,4,4088,5}, + {11434,4,4093,6}, + {11438,5,4099,5}, + {11443,12,4104,3}, + {11455,7,4107,3}, + {11462,7,4110,3}, + {11469,6,4113,3}, + {11475,7,4116,2}, + {11482,5,4118,6}, + {11487,4,4124,3}, + {11491,5,4127,5}, + {11496,6,4132,5}, + {11502,6,4137,2}, + {11508,8,4139,3}, + {11516,6,4142,3}, + {11522,8,4145,3}, + {11530,9,4148,3}, + {11539,4,4151,2}, + {11543,5,4153,2}, + {11548,6,4155,5}, + {11554,7,4160,5}, + {11561,5,4165,3}, + {11566,7,4168,2}, + {11573,7,4170,2}, + {11580,6,4172,3}, + {11586,9,4175,5}, + {11595,5,4180,3}, + {11600,4,4183,2}, + {11604,6,4185,3}, + {11610,3,4188,3}, + {11613,6,4191,3}, + {11619,7,4194,3}, + {11626,6,4197,3}, + {11632,8,4200,3}, + {11640,6,4203,5}, + {11646,7,4208,3}, + {11653,7,4211,3}, + {11660,6,4214,5}, + {11666,7,4219,3}, + {11673,8,4222,3}, + {11681,4,4225,4}, + {11685,4,4229,5}, + {11689,4,4234,3}, + {11693,5,4237,3}, + {11698,6,4240,5}, + {11704,10,4245,5}, + {11714,5,4250,5}, + {11719,6,4255,3}, + {11725,4,4258,3}, + {11729,5,4261,3}, + {11734,6,4264,3}, + {11740,6,4267,3}, + {11746,6,4270,3}, + {11752,3,4273,3}, + {11755,4,4276,3}, + {11759,5,4279,3}, + {11764,4,4282,3}, + {11768,5,4285,2}, + {11773,6,4287,3}, + {11779,4,4290,5}, + {11783,6,4295,3}, + {11789,5,4298,3}, + {11794,4,4301,3}, + {11798,11,4304,3}, + {11809,16,4307,3}, + {11825,5,4310,3}, + {11830,6,4313,5}, + {11836,10,4318,5}, + {11846,5,4323,5}, + {11851,6,4328,3}, + {11857,6,4331,3}, + {11863,4,4334,3}, + {11867,6,4337,3}, + {11873,7,4340,3}, + {11880,5,4343,3}, + {11885,5,4346,4}, + {11890,3,4350,2}, + {11893,4,4352,2}, + {11897,6,4354,3}, + {11903,7,4357,5}, + {11910,9,4362,5}, + {11919,8,4367,3}, + {11927,8,4370,3}, + {11935,8,4373,3}, + {11943,6,4376,3}, + {11949,8,4379,3}, + {11957,8,4382,3}, + {11965,8,4385,3}, + {11973,5,4388,3}, + {11978,10,4391,3}, + {11988,7,4394,6}, + {11995,6,4400,5}, + {12001,8,4405,3}, + {12009,4,4408,3}, + {12013,7,4411,3}, + {12020,5,4414,5}, + {12025,6,4419,3}, + {12031,8,4422,5}, + {12039,6,4427,3}, + {12045,6,4430,3}, + {12051,7,4433,5}, + {12058,7,4438,5}, + {12065,12,4443,3}, + {12077,6,4446,3}, + {12083,7,4449,3}, + {12090,4,4452,3}, + {12094,7,4455,3}, + {12101,5,4458,5}, + {12106,5,4463,4}, + {12111,10,4467,3}, + {12121,15,4470,3}, + {12136,5,4473,3}, + {12141,6,4476,3}, + {12147,7,4479,3}, + {12154,6,4482,3}, + {12160,6,4485,3}, + {12166,8,4488,3}, + {12174,8,4491,3}, + {12182,5,4494,3}, + {12187,6,4497,5}, + {12193,6,4502,3}, + {12199,8,4505,6}, + {12207,10,4511,3}, + {12217,11,4514,5}, + {12228,6,4519,3}, + {12234,8,4522,5}, + {12242,5,4527,3}, + {12247,6,4530,5}, + {12253,6,4535,3}, + {12259,8,4538,6}, + {12267,10,4544,3}, + {12277,11,4547,5}, + {12288,5,4552,3}, + {12293,6,4555,2}, + {12299,7,4557,2}, + {12306,5,4559,3}, + {12311,14,4562,3}, + {12325,16,4565,3}, + {12341,15,4568,3}, + {12356,17,4571,3}, + {12373,3,4574,2}, + {12376,4,4576,1}, + {12380,7,4577,3}, + {12387,6,4580,3}, + {12393,7,4583,3}, + {12400,7,4586,3}, + {12407,5,4589,6}, + {12412,7,4595,3}, + {12419,5,4598,6}, + {12424,5,4604,4}, + {12429,8,4608,3}, + {12437,7,4611,3}, + {12444,5,4614,6}, + {12449,5,4620,4}, + {12454,8,4624,6}, + {12462,7,4630,3}, + {12469,8,4633,6}, + {12477,6,4639,6}, + {12483,6,4645,3}, + {12489,7,4648,3}, + {12496,6,4651,3}, + {12502,8,4654,3}, + {12510,7,4657,3}, + {12517,3,4660,3}, + {12520,6,4663,2}, + {12526,7,4665,2}, + {12533,5,4667,3}, + {12538,5,4670,3}, + {12543,5,4673,2}, + {12548,6,4675,2}, + {12554,4,4677,2}, + {12558,6,4679,3}, + {12564,7,4682,2}, + {12571,5,4684,3}, + {12576,5,4687,3}, + {12581,7,4690,3}, + {12588,6,4693,2}, + {12594,6,4695,3}, + {12600,4,4698,4}, + {12604,5,4702,2}, + {12609,6,4704,2}, + {12615,7,4706,2}, + {12622,4,4708,3}, + {12626,6,4711,3}, + {12632,4,4714,2}, + {12636,5,4716,3}, + {12641,6,4719,3}, + {12647,6,4722,3}, + {12653,8,4725,3}, + {12661,6,4728,3}, + {12667,4,4731,3}, + {12671,6,4734,2}, + {12677,6,4736,2}, + {12683,8,4738,2}, + {12691,5,4740,3}, + {12696,7,4743,3}, + {12703,5,4746,4}, + {12708,5,4750,3}, + {12713,6,4753,3}, + {12719,6,4756,3}, + {12725,3,4759,3}, + {12728,6,4762,3}, + {12734,4,4765,3}, + {12738,6,4768,3}, + {12744,8,4771,3}, + {12752,4,4774,2}, + {12756,5,4776,2}, + {12761,4,4778,2}, + {12765,5,4780,2}, + {12770,7,4782,3}, + {12777,5,4785,3}, + {12782,8,4788,3}, + {12790,4,4791,3}, + {12794,5,4794,3}, + {12799,6,4797,2}, + {12805,7,4799,2}, + {12812,5,4801,3}, + {12817,6,4804,2}, + {12823,7,4806,2}, + {12830,7,4808,3}, + {12837,9,4811,3}, + {12846,4,4814,2}, + {12850,5,4816,2}, + {12855,6,4818,3}, + {12861,4,4821,3}, + {12865,4,4824,2}, + {12869,5,4826,2}, + {12874,9,4828,3}, + {12883,7,4831,3}, + {12890,6,4834,3}, + {12896,5,4837,3}, + {12901,4,4840,2}, + {12905,7,4842,1}, + {12912,7,4843,1}, + {12919,7,4844,3}, + {12926,5,4847,3}, + {12931,8,4850,3}, + {12939,4,4853,4}, + {12943,4,4857,2}, + {12947,5,4859,2}, + {12952,7,4861,3}, + {12959,6,4864,3}, + {12965,3,4867,2}, + {12968,10,4869,3}, + {12978,4,4872,2}, + {12982,7,4874,3}, + {12989,8,4877,3}, + {12997,7,4880,3}, + {13004,5,4883,1}, + {13009,9,4884,3}, + {13018,6,4887,3}, + {13024,8,4890,3}, + {13032,7,4893,3}, + {13039,7,4896,3}, + {13046,6,4899,3}, + {13052,6,4902,2}, + {13058,7,4904,2}, + {13065,8,4906,3}, + {13073,8,4909,3}, + {13081,3,4912,2}, + {13084,9,4914,3}, + {13093,5,4917,4}, + {13098,5,4921,2}, + {13103,6,4923,2}, + {13109,3,4925,3}, + {13112,4,4928,3}, + {13116,5,4931,3}, + {13121,6,4934,3}, + {13127,4,4937,3}, + {13131,5,4940,3}, + {13136,11,4943,3}, + {13147,12,4946,3}, + {13159,7,4949,3}, + {13166,12,4952,3}, + {13178,9,4955,3}, + {13187,9,4958,3}, + {13196,8,4961,3}, + {13204,6,4964,3}, + {13210,7,4967,3}, + {13217,5,4970,3}, + {13222,6,4973,3}, + {13228,7,4976,3}, + {13235,5,4979,3}, + {13240,9,4982,3}, + {13249,9,4985,3}, + {13258,9,4988,3}, + {13267,5,4991,3}, + {13272,7,4994,3}, + {13279,6,4997,3}, + {13285,7,5000,3}, + {13292,5,5003,4}, + {13297,4,5007,2}, + {13301,7,5009,3}, + {13308,4,5012,4}, + {13312,5,5016,3}, + {13317,5,5019,4}, + {13322,7,5023,3}, + {13329,5,5026,4}, + {13334,12,5030,3}, + {13346,8,5033,3}, + {13354,6,5036,1}, + {13360,8,5037,3}, + {13368,4,5040,1}, + {13372,5,5041,1}, + {13377,6,5042,3}, + {13383,5,5045,3}, + {13388,7,5048,3}, + {13395,6,5051,3}, + {13401,5,5054,3}, + {13406,5,5057,5}, + {13411,7,5062,2}, + {13418,6,5064,3}, + {13424,9,5067,3}, + {13433,5,5070,3}, + {13438,6,5073,3}, + {13444,6,5076,3}, + {13450,7,5079,3}, + {13457,5,5082,2}, + {13462,6,5084,2}, + {13468,5,5086,3}, + {13473,7,5089,3}, + {13480,6,5092,3}, + {13486,8,5095,3}, + {13494,6,5098,3}, + {13500,7,5101,3}, + {13507,7,5104,3}, + {13514,7,5107,3}, + {13521,7,5110,3}, + {13528,8,5113,3}, + {13536,7,5116,3}, + {13543,6,5119,3}, + {13549,7,5122,3}, + {13556,6,5125,3}, + {13562,10,5128,3}, + {13572,6,5131,3}, + {13578,6,5134,3}, + {13584,7,5137,1}, + {13591,7,5138,1}, + {13598,6,5139,3}, + {13604,8,5142,3}, + {13612,8,5145,3}, + {13620,7,5148,2}, + {13627,7,5150,2}, + {13634,6,5152,3}, + {13640,5,5155,1}, + {13645,4,5156,2}, + {13649,5,5158,3}, + {13654,8,5161,3}, + {13662,6,5164,3}, + {13668,7,5167,3}, + {13675,5,5170,3}, + {13680,5,5173,3}, + {13685,8,5176,3}, + {13693,9,5179,3}, + {13702,6,5182,3}, + {13708,5,5185,3}, + {13713,3,5188,2}, + {13716,4,5190,2}, + {13720,7,5192,3}, + {13727,7,5195,3}, + {13734,4,5198,4}, + {13738,6,5202,3}, + {13744,6,5205,3}, + {13750,7,5208,3}, + {13757,4,5211,2}, + {13761,5,5213,2}, + {13766,11,5215,3}, + {13777,15,5218,3}, + {13792,17,5221,3}, + {13809,15,5224,3}, + {13824,16,5227,3}, + {13840,18,5230,3}, + {13858,17,5233,3}, + {13875,16,5236,3}, + {13891,16,5239,3}, + {13907,5,5242,2}, + {13912,13,5244,3}, + {13925,6,5247,3}, + {13931,6,5250,3}, + {13937,4,5253,3}, + {13941,7,5256,3}, + {13948,11,5259,3}, + {13959,6,5262,3}, + {13965,6,5265,3}, + {13971,6,5268,3}, + {13977,6,5271,3}, + {13983,6,5274,3}, + {13989,5,5277,4}, + {13994,7,5281,3}, + {14001,8,5284,3}, + {14009,5,5287,1}, + {14014,7,5288,3}, + {14021,9,5291,3}, + {14030,6,5294,3}, + {14036,7,5297,3}, + {14043,5,5300,4}, + {14048,4,5304,3}, + {14052,5,5307,1}, + {14057,6,5308,3}, + {14063,7,5311,3}, + {14070,7,5314,3}, + {14077,7,5317,3}, + {14084,5,5320,3}, + {14089,6,5323,3}, + {14095,6,5326,3}, + {14101,9,5329,3}, + {14110,8,5332,3}, + {14118,3,5335,3}, + {14121,7,5338,2}, + {14128,6,5340,3}, + {14134,3,5343,3}, + {14137,4,5346,3}, + {14141,5,5349,3}, + {14146,7,5352,2}, + {14153,6,5354,3}, + {14159,4,5357,3}, + {14163,7,5360,2}, + {14170,6,5362,2}, + {14176,5,5364,3}, + {14181,6,5367,3}, + {14187,7,5370,3}, + {14194,9,5373,3}, + {14203,6,5376,3}, + {14209,4,5379,2}, + {14213,5,5381,3}, + {14218,6,5384,3}, + {14224,6,5387,3}, + {14230,6,5390,3}, + {14236,7,5393,3}, + {14243,6,5396,3}, + {14249,8,5399,3}, + {14257,4,5402,2}, + {14261,5,5404,2}, + {14266,5,5406,1}, + {14271,7,5407,3}, + {14278,9,5410,3}, + {14287,6,5413,3}, + {14293,5,5416,3}, + {14298,4,5419,4}, + {14302,7,5423,3}, + {14309,6,5426,3}, + {14315,7,5429,2}, + {14322,5,5431,2}, + {14327,9,5433,3}, + {14336,14,5436,3}, + {14350,3,5439,2}, + {14353,4,5441,2}, + {14357,6,5443,2}, + {14363,7,5445,2}, + {14370,7,5447,2}, + {14377,4,5449,3}, + {14381,7,5452,3}, + {14388,5,5455,3}, + {14393,6,5458,3}, + {14399,5,5461,3}, + {14404,6,5464,3}, + {14410,5,5467,3}, + {14415,6,5470,3}, + {14421,6,5473,3}, + {14427,8,5476,3}, + {14435,8,5479,3}, + {14443,6,5482,3}, + {14449,14,5485,3}, + {14463,7,5488,3}, + {14470,9,5491,3}, + {14479,5,5494,3}, + {14484,6,5497,3}, + {14490,4,5500,3}, + {14494,5,5503,3}, + {14499,6,5506,6}, + {14505,7,5512,2}, + {14512,4,5514,1}, + {14516,5,5515,3}, + {14521,7,5518,3}, + {14528,5,5521,4}, + {14533,7,5525,3}, + {14540,10,5528,3}, + {14550,5,5531,3}, + {14555,6,5534,3}, + {14561,7,5537,6}, + {14568,6,5543,3}, + {14574,7,5546,6}, + {14581,6,5552,3}, + {14587,7,5555,3}, + {14594,9,5558,3}, + {14603,11,5561,3}, + {14614,6,5564,3}, + {14620,7,5567,3}, + {14627,9,5570,3}, + {14636,11,5573,3}, + {14647,4,5576,3}, + {14651,7,5579,3}, + {14658,7,5582,3}, + {14665,5,5585,3}, + {14670,6,5588,3}, + {14676,5,5591,4}, + {14681,7,5595,3}, + {14688,7,5598,3}, + {14695,7,5601,3}, + {14702,5,5604,3}, + {14707,6,5607,3}, + {14713,16,5610,2}, + {14729,12,5612,2}, + {14741,6,5614,2}, + {14747,4,5616,3}, + {14751,5,5619,3}, + {14756,7,5622,3}, + {14763,5,5625,3}, + {14768,8,5628,3}, + {14776,8,5631,3}, + {14784,6,5634,3}, + {14790,6,5637,3}, + {14796,8,5640,3}, + {14804,8,5643,3}, + {14812,7,5646,3}, + {14819,9,5649,3}, + {14828,10,5652,3}, + {14838,10,5655,3}, + {14848,11,5658,3}, + {14859,7,5661,3}, + {14866,7,5664,3}, + {14873,7,5667,3}, + {14880,5,5670,3}, + {14885,11,5673,3}, + {14896,12,5676,3}, + {14908,7,5679,3}, + {14915,12,5682,3}, + {14927,9,5685,3}, + {14936,9,5688,3}, + {14945,8,5691,3}, + {14953,4,5694,3}, + {14957,5,5697,3}, + {14962,4,5700,2}, + {14966,5,5702,2}, + {14971,4,5704,2}, + {14975,5,5706,2}, + {14980,4,5708,2}, + {14984,5,5710,2}, + {14989,4,5712,3}, + {14993,5,5715,3}, + {14998,7,5718,3}, + {15005,8,5721,3}, + {15013,5,5724,3}, + {15018,8,5727,3}, + {15026,8,5730,3}, + {15034,8,5733,3}, + {15042,8,5736,3}, + {15050,8,5739,3}, + {15058,6,5742,3}, + {15064,6,5745,3}, + {15070,8,5748,3}, + {15078,7,5751,3}, + {15085,9,5754,3}, + {15094,10,5757,3}, + {15104,10,5760,3}, + {15114,11,5763,3}, + {15125,7,5766,3}, + {15132,7,5769,3}, + {15139,7,5772,3}, + {15146,6,5775,3}, + {15152,7,5778,3}, + {15159,6,5781,3}, + {15165,8,5784,3}, + {15173,7,5787,3}, + {15180,5,5790,2}, + {15185,6,5792,2}, + {15191,7,5794,3}, + {15198,4,5797,2}, + {15202,5,5799,3}, + {15207,7,5802,2}, + {15214,7,5804,2}, + {15221,4,5806,2}, + {15225,5,5808,3}, + {15230,7,5811,3}, + {15237,4,5814,4}, + {15241,7,5818,3}, + {15248,10,5821,3}, + {15258,6,5824,2}, + {15264,9,5826,2}, + {15273,7,5828,2}, + {15280,12,5830,3}, + {15292,9,5833,3}, + {15301,7,5836,3}, + {15308,6,5839,3}, + {15314,7,5842,3}, + {15321,5,5845,2}, + {15326,6,5847,2}, + {15332,6,5849,2}, + {15338,5,5851,2}, + {15343,6,5853,2}, + {15349,7,5855,3}, + {15356,9,5858,3}, + {15365,7,5861,3}, + {15372,5,5864,3}, + {15377,5,5867,3}, + {15382,4,5870,3}, + {15386,7,5873,3}, + {15393,7,5876,3}, + {15400,5,5879,4}, + {15405,8,5883,3}, + {15413,5,5886,3}, + {15418,7,5889,3}, + {15425,6,5892,3}, + {15431,9,5895,3}, + {15440,13,5898,3}, + {15453,13,5901,3}, + {15466,15,5904,3}, + {15481,10,5907,3}, + {15491,14,5910,3}, + {15505,16,5913,3}, + {15521,7,5916,3}, + {15528,5,5919,3}, + {15533,9,5922,3}, + {15542,8,5925,3}, + {15550,6,5928,3}, + {15556,8,5931,3}, + {15564,9,5934,3}, + {15573,5,5937,4}, + {15578,5,5941,2}, + {15583,6,5943,2}, + {15589,7,5945,2}, + {15596,6,5947,3}, + {15602,17,5950,3}, + {15619,18,5953,3}, + {15637,5,5956,3}, + {15642,5,5959,3}, + {15647,6,5962,2}, + {15653,7,5964,2}, + {15660,5,5966,3}, + {15665,6,5969,2}, + {15671,7,5971,2}, + {15678,5,5973,2}, + {15683,6,5975,2}, + {15689,4,5977,2}, + {15693,6,5979,3}, + {15699,7,5982,2}, + {15706,6,5984,3}, + {15712,7,5987,3}, + {15719,4,5990,4}, + {15723,6,5994,2}, + {15729,7,5996,2}, + {15736,6,5998,3}, + {15742,6,6001,3}, + {15748,6,6004,3}, + {15754,7,6007,3}, + {15761,9,6010,3}, + {15770,7,6013,3}, + {15777,6,6016,3}, + {15783,6,6019,2}, + {15789,3,6021,2}, + {15792,4,6023,2}, + {15796,6,6025,2}, + {15802,5,6027,4}, + {15807,8,6031,3}, + {15815,12,6034,3}, + {15827,14,6037,3}, + {15841,15,6040,3}, + {15856,6,6043,3}, + {15862,5,6046,2}, + {15867,6,6048,2}, + {15873,8,6050,2}, + {15881,11,6052,3}, + {15892,7,6055,3}, + {15899,9,6058,3}, + {15908,7,6061,3}, + {15915,6,6064,2}, + {15921,6,6066,3}, + {15927,5,6069,4}, + {15932,6,6073,3}, + {15938,7,6076,2}, + {15945,5,6078,3}, + {15950,6,6081,3}, + {15956,6,6084,3}, + {15962,4,6087,2}, + {15966,5,6089,2}, + {15971,8,6091,3}, + {15979,5,6094,3}, + {15984,5,6097,3}, + {15989,6,6100,3}, + {15995,6,6103,3}, + {16001,7,6106,3}, + {16008,11,6109,2}, + {16019,9,6111,2}, + {16028,11,6113,3}, + {16039,7,6116,2}, + {16046,6,6118,2}, + {16052,10,6120,3}, + {16062,5,6123,3}, + {16067,7,6126,2}, + {16074,9,6128,2}, + {16083,13,6130,6}, + {16096,14,6136,6}, + {16110,13,6142,6}, + {16123,14,6148,6}, + {16137,9,6154,2}, + {16146,16,6156,3}, + {16162,17,6159,3}, + {16179,4,6162,2}, + {16183,6,6164,3}, + {16189,4,6167,3}, + {16193,7,6170,3}, + {16200,6,6173,3}, + {16206,7,6176,3}, + {16213,7,6179,1}, + {16220,5,6180,1}, + {16225,4,6181,4}, + {16229,6,6185,3}, + {16235,6,6188,6}, + {16241,6,6194,6}, + {16247,5,6200,4}, + {16252,6,6204,3}, + {16258,6,6207,3}, + {16264,5,6210,4}, + {16269,7,6214,6}, + {16276,7,6220,6}, + {16283,7,6226,6}, + {16290,7,6232,6}, + {16297,8,6238,3}, + {16305,6,6241,2}, + {16311,7,6243,3}, + {16318,6,6246,3}, + {16324,7,6249,3}, + {16331,7,6252,3}, + {16338,4,6255,4}, + {16342,5,6259,4}, + {16347,3,6263,3}, + {16350,3,6266,3}, + {16353,7,6269,3}, + {16360,5,6272,4}, + {16365,5,6276,3}, + {16370,6,6279,3}, + {16376,5,6282,3}, + {16381,6,6285,3}, + {16387,4,6288,4}, + {16391,6,6292,3}, + {16397,6,6295,3}, + {16403,3,6298,2}, + {16406,6,6300,3}, + {16412,6,6303,3}, + {16418,5,6306,3}, + {16423,5,6309,3}, + {16428,6,6312,3}, + {16434,5,6315,4}, + {16439,7,6319,3}, + {16446,7,6322,3}, + {16453,6,6325,3}, + {16459,6,6328,3}, + {16465,5,6331,4}, + {16470,7,6335,3}, + {16477,7,6338,3}, + {16484,6,6341,3}, + {16490,5,6344,3}, + {16495,7,6347,3}, + {16502,6,6350,2}, + {16508,7,6352,2}, + {16515,5,6354,2}, + {16520,6,6356,2}, + {16526,4,6358,2}, + {16530,3,6360,2}, + {16533,4,6362,2}, + {16537,4,6364,4}, + {16541,5,6368,2}, + {16546,5,6370,4}, + {16551,5,6374,4}, + {16556,5,6378,2}, + {16561,4,6380,2}, + {16565,5,6382,2}, + {16570,7,6384,2}, + {16577,7,6386,2}, + {16584,4,6388,2}, + {16588,5,6390,2}, + {16593,7,6392,3}, + {16600,5,6395,2}, + {16605,4,6397,4}, + {16609,5,6401,2}, + {16614,8,6403,3}, + {16622,5,6406,4}, + {16627,5,6410,4}, + {16632,4,6414,3}, + {16636,5,6417,3}, +}; + +} // namespace entities +} // namespace util +} // namespace socketsecurity +} // namespace node diff --git a/packages/node-smol-builder/additions/source-patched/src/socketsecurity/util/util_binding.cc b/packages/node-smol-builder/additions/source-patched/src/socketsecurity/util/util_binding.cc index d4ba6e442..ba107b010 100644 --- a/packages/node-smol-builder/additions/source-patched/src/socketsecurity/util/util_binding.cc +++ b/packages/node-smol-builder/additions/source-patched/src/socketsecurity/util/util_binding.cc @@ -120,6 +120,13 @@ #include "util.h" #include "v8.h" +#include +#include +#include +#include + +#include "socketsecurity/simd/simd.h" + namespace node { namespace socketsecurity { namespace util { @@ -637,6 +644,725 @@ static void WeakRefSafe(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(result); } +// ─── HTML named entity decode/encode ────────────────────────────────── +// +// Native equivalent of the npm `entities` package for the WHATWG-named +// character reference table. The full 2231-entry table is generated +// into entities_data.cc (kEntities/kNamePool/kValuePool) by +// scripts/generate-entities-data.mts. +// +// decodeHtml(s) handles named refs (`&`, `Á`, …) plus +// numeric refs (`'`, `'`). Unknown sequences are passed +// through verbatim. +// +// encodeHtml(s) is the conservative encoder that escapes the five +// "must-escape" characters in HTML: `<`, `>`, `&`, `"`, `'`. Returns +// the input unchanged when none of those bytes appear. + +namespace entities { + +// Forward decls — definitions live in entities_data.cc. +struct EntityMeta { + uint16_t name_off; + uint16_t name_len; + uint16_t value_off; + uint8_t value_len; +}; +extern const uint8_t kNamePool[]; +extern const uint8_t kValuePool[]; +extern const size_t kEntityCount; +extern const EntityMeta kEntities[]; + +// Lookup name `[start, start+len)` (bytes) in the sorted table. +// Returns the matching EntityMeta or nullptr. +// +// Comparison: `memcmp` over `min(m.name_len, len)` bytes (vectorized by +// the compiler / libc) + length tiebreak. The table is sorted by name +// so binary search bounds the work at ⌈log2(2231)⌉ = 12 iterations +// worst case — typical decode is ~10ns per `&name;` token. +inline const EntityMeta* FindEntity(const uint8_t* start, size_t len) { + size_t lo = 0; + size_t hi = kEntityCount; + while (lo < hi) { + const size_t mid = lo + (hi - lo) / 2; + const EntityMeta& m = kEntities[mid]; + const uint8_t* name = &kNamePool[m.name_off]; + const size_t cmp_len = m.name_len < len ? m.name_len : len; + int cmp = std::memcmp(name, start, cmp_len); + if (cmp == 0) { + if (m.name_len == len) { + return &m; + } + cmp = (m.name_len < len) ? -1 : 1; + } + if (cmp < 0) { + lo = mid + 1; + } else { + hi = mid; + } + } + return nullptr; +} + +inline void EncodeCodepointUtf8(uint32_t cp, std::string& out) { + if (cp < 0x80) { + out.push_back(static_cast(cp)); + } else if (cp < 0x800) { + out.push_back(static_cast(0xC0 | (cp >> 6))); + out.push_back(static_cast(0x80 | (cp & 0x3F))); + } else if (cp < 0x10000) { + out.push_back(static_cast(0xE0 | (cp >> 12))); + out.push_back(static_cast(0x80 | ((cp >> 6) & 0x3F))); + out.push_back(static_cast(0x80 | (cp & 0x3F))); + } else if (cp < 0x110000) { + out.push_back(static_cast(0xF0 | (cp >> 18))); + out.push_back(static_cast(0x80 | ((cp >> 12) & 0x3F))); + out.push_back(static_cast(0x80 | ((cp >> 6) & 0x3F))); + out.push_back(static_cast(0x80 | (cp & 0x3F))); + } + // Above U+10FFFF is invalid; silently drop (matches the npm impl). +} + +} // namespace entities + +namespace { + +// SIMD-vectorized "does input contain any of the five HTML-must-escape +// chars (< > & " ')?" check. Returns true if any sentinel byte appears +// in [data, data+len). +// +// SSE2 path (x86-64): _mm_cmpeq_epi8 against each sentinel broadcasted +// to a __m128i, OR the 5 result vectors together, _mm_movemask_epi8 +// to a 16-bit mask, branch on != 0. 16 bytes per iteration. +// +// NEON path (ARM64): vceqq_u8 + vorrq_u8 chain + vmaxvq_u8 horizontal +// reduce. 16 bytes per iteration. +// +// Scalar fallback (or for the trailing <16 bytes): the 5-memchr +// approach from the previous perf commit. +// +// The function is hot on the encodeHtml no-escape fast path (most +// inputs DON'T need escaping; we want to return ASAP). For ≥64-byte +// inputs the SIMD path is ~5x faster than five memchrs because we +// scan once and OR-combine the comparison results instead of making +// five separate passes. +SMOL_FORCE_INLINE bool ContainsAnyEscapeChar(const uint8_t* data, + size_t len) { +#if SMOL_HAS_SSE2 + // Pre-broadcast each sentinel to all 16 lanes once. + const __m128i lt = _mm_set1_epi8('<'); + const __m128i gt = _mm_set1_epi8('>'); + const __m128i amp = _mm_set1_epi8('&'); + const __m128i quo = _mm_set1_epi8('"'); + const __m128i apos = _mm_set1_epi8('\''); + size_t i = 0; + for (; i + 16 <= len; i += 16) { + const __m128i v = _mm_loadu_si128( + reinterpret_cast(data + i)); + const __m128i any = _mm_or_si128( + _mm_or_si128( + _mm_or_si128(_mm_cmpeq_epi8(v, lt), _mm_cmpeq_epi8(v, gt)), + _mm_or_si128(_mm_cmpeq_epi8(v, amp), _mm_cmpeq_epi8(v, quo))), + _mm_cmpeq_epi8(v, apos)); + if (_mm_movemask_epi8(any) != 0) { + return true; + } + } + // Tail: scan remaining <16 bytes scalar-style. + for (; i < len; ++i) { + const uint8_t c = data[i]; + if (c == '<' || c == '>' || c == '&' || c == '"' || c == '\'') { + return true; + } + } + return false; +#elif SMOL_HAS_NEON + const uint8x16_t lt = vdupq_n_u8('<'); + const uint8x16_t gt = vdupq_n_u8('>'); + const uint8x16_t amp = vdupq_n_u8('&'); + const uint8x16_t quo = vdupq_n_u8('"'); + const uint8x16_t apos = vdupq_n_u8('\''); + size_t i = 0; + for (; i + 16 <= len; i += 16) { + const uint8x16_t v = vld1q_u8(data + i); + const uint8x16_t any = vorrq_u8( + vorrq_u8( + vorrq_u8(vceqq_u8(v, lt), vceqq_u8(v, gt)), + vorrq_u8(vceqq_u8(v, amp), vceqq_u8(v, quo))), + vceqq_u8(v, apos)); + if (vmaxvq_u8(any) != 0) { + return true; + } + } + for (; i < len; ++i) { + const uint8_t c = data[i]; + if (c == '<' || c == '>' || c == '&' || c == '"' || c == '\'') { + return true; + } + } + return false; +#else + // Scalar fallback — five memchrs (libc-vectorized). + return std::memchr(data, '<', len) != nullptr || + std::memchr(data, '>', len) != nullptr || + std::memchr(data, '&', len) != nullptr || + std::memchr(data, '"', len) != nullptr || + std::memchr(data, '\'', len) != nullptr; +#endif +} + +// SIMD-vectorized "does input contain ESC (0x1B) or CSI (0x9B)?" check. +// Same shape as ContainsAnyEscapeChar but only two sentinels. +SMOL_FORCE_INLINE bool ContainsAnsiEscape(const uint8_t* data, size_t len) { +#if SMOL_HAS_SSE2 + const __m128i esc = _mm_set1_epi8(static_cast(0x1B)); + const __m128i csi = _mm_set1_epi8(static_cast(0x9B)); + size_t i = 0; + for (; i + 16 <= len; i += 16) { + const __m128i v = _mm_loadu_si128( + reinterpret_cast(data + i)); + const __m128i any = + _mm_or_si128(_mm_cmpeq_epi8(v, esc), _mm_cmpeq_epi8(v, csi)); + if (_mm_movemask_epi8(any) != 0) { + return true; + } + } + for (; i < len; ++i) { + const uint8_t c = data[i]; + if (c == 0x1B || c == 0x9B) { + return true; + } + } + return false; +#elif SMOL_HAS_NEON + const uint8x16_t esc = vdupq_n_u8(0x1B); + const uint8x16_t csi = vdupq_n_u8(0x9B); + size_t i = 0; + for (; i + 16 <= len; i += 16) { + const uint8x16_t v = vld1q_u8(data + i); + const uint8x16_t any = vorrq_u8(vceqq_u8(v, esc), vceqq_u8(v, csi)); + if (vmaxvq_u8(any) != 0) { + return true; + } + } + for (; i < len; ++i) { + const uint8_t c = data[i]; + if (c == 0x1B || c == 0x9B) { + return true; + } + } + return false; +#else + return std::memchr(data, 0x1B, len) != nullptr || + std::memchr(data, 0x9B, len) != nullptr; +#endif +} + +} // namespace + +static void DecodeHtml(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + if (args.Length() < 1 || !args[0]->IsString()) { + args.GetReturnValue().SetUndefined(); + return; + } + Local input = args[0].As(); + const int input_len = input->Utf8Length(isolate); + if (input_len == 0) { + args.GetReturnValue().Set(input); + return; + } + std::string buf(static_cast(input_len), '\0'); + input->WriteUtf8(isolate, buf.data(), input_len, nullptr, + String::NO_NULL_TERMINATION); + + std::string out; + out.reserve(buf.size()); + + const uint8_t* p = reinterpret_cast(buf.data()); + const uint8_t* const end = p + buf.size(); + + while (p < end) { + if (*p != '&') { + out.push_back(static_cast(*p)); + ++p; + continue; + } + // Find the ';' that terminates this reference. WHATWG allows the + // semicolon to be elided in some legacy cases, but every entry in + // our table includes it as part of `name`. We require it here. + const uint8_t* q = p + 1; + // Cap the search to a sane window (longest entity name is < 32 + // bytes plus '&' and ';'). Anything longer is treated as a literal. + const uint8_t* search_end = end < (q + 64) ? end : (q + 64); + while (q < search_end && *q != ';' && *q != '&') { + ++q; + } + if (q >= end || *q != ';') { + // No terminating ';' → literal '&'. + out.push_back('&'); + ++p; + continue; + } + // Inclusive of ';' in the lookup key (table stores names like + // "amp;"). + const size_t name_len = static_cast(q - p); // includes ';' + // Numeric refs: &#NNN; or &#xHHH; + if (name_len >= 3 && *(p + 1) == '#') { + uint32_t cp = 0; + bool ok = true; + if (*(p + 2) == 'x' || *(p + 2) == 'X') { + // Hex. + if (name_len < 4) { + ok = false; + } else { + for (const uint8_t* r = p + 3; r < q; ++r) { + uint8_t c = *r; + uint32_t d; + if (c >= '0' && c <= '9') { + d = c - '0'; + } else if (c >= 'a' && c <= 'f') { + d = c - 'a' + 10; + } else if (c >= 'A' && c <= 'F') { + d = c - 'A' + 10; + } else { + ok = false; + break; + } + cp = (cp << 4) | d; + if (cp > 0x10FFFF) { + ok = false; + break; + } + } + } + } else { + // Decimal. + for (const uint8_t* r = p + 2; r < q; ++r) { + uint8_t c = *r; + if (c < '0' || c > '9') { + ok = false; + break; + } + cp = cp * 10 + (c - '0'); + if (cp > 0x10FFFF) { + ok = false; + break; + } + } + } + if (ok && cp != 0) { + entities::EncodeCodepointUtf8(cp, out); + p = q + 1; + continue; + } + // Bad numeric ref → literal. + out.push_back('&'); + ++p; + continue; + } + // Named ref. + const entities::EntityMeta* m = + entities::FindEntity(p + 1, name_len); // skip leading '&' + if (m != nullptr) { + const uint8_t* val = &entities::kValuePool[m->value_off]; + out.append(reinterpret_cast(val), m->value_len); + p = q + 1; + continue; + } + // Unknown → literal. + out.push_back('&'); + ++p; + } + + MaybeLocal result_maybe = + String::NewFromUtf8(isolate, out.data(), v8::NewStringType::kNormal, + static_cast(out.size())); + Local result; + if (!result_maybe.ToLocal(&result)) { + args.GetReturnValue().SetUndefined(); + return; + } + args.GetReturnValue().Set(result); +} + +static void EncodeHtml(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + if (args.Length() < 1 || !args[0]->IsString()) { + args.GetReturnValue().SetUndefined(); + return; + } + Local input = args[0].As(); + const int char_len = input->Length(); + if (char_len == 0) { + args.GetReturnValue().Set(input); + return; + } + + // True fast path: the five must-escape chars (< > & " ') are all + // ASCII (<= 0x7F). For one-byte strings the raw bytes ARE the + // characters. For two-byte strings, check UCS-2 directly — the + // escape chars encode as single 16-bit values <= 0x7F. No UTF-8 + // round-trip needed. + // + // Same stack-buffer + memchr-per-sentinel approach as StripAnsi: + // five vectorized memchr passes still beat one branchy per-byte + // scan for any non-trivial input length. + constexpr int kInlineThreshold = 4096; + bool needs_escape = false; + if (input->IsOneByte()) { + uint8_t stack_buf[kInlineThreshold]; + std::vector heap_buf; + uint8_t* scan_ptr; + if (char_len <= kInlineThreshold) { + scan_ptr = stack_buf; + } else { + heap_buf.resize(static_cast(char_len)); + scan_ptr = heap_buf.data(); + } + input->WriteOneByte(isolate, scan_ptr, /*start*/ 0, char_len, + String::NO_NULL_TERMINATION); + // Single SIMD pass for the five must-escape chars (< > & " '). + // SSE2 / NEON broadcasts each sentinel to 16 lanes, OR-combines + // the comparison results, and uses movemask / vmaxvq to bail + // ASAP on the first match. ~5x faster than five sequential + // memchr scans on inputs ≥64 bytes. + if (ContainsAnyEscapeChar(scan_ptr, static_cast(char_len))) { + needs_escape = true; + } + if (!needs_escape) { + args.GetReturnValue().Set(input); + return; + } + // One-byte string with escape chars present. Latin-1 chars >= 0x80 + // encode as 2-byte UTF-8; we need the UTF-8 form for the output + // since we're emitting `&` etc. as bytes. Re-materialize. + } else { + uint16_t stack_buf[kInlineThreshold]; + std::vector heap_buf; + uint16_t* scan_ptr; + if (char_len <= kInlineThreshold) { + scan_ptr = stack_buf; + } else { + heap_buf.resize(static_cast(char_len)); + scan_ptr = heap_buf.data(); + } + input->Write(isolate, scan_ptr, /*start*/ 0, char_len, + String::NO_NULL_TERMINATION); + for (int i = 0; i < char_len; ++i) { + const uint16_t c = scan_ptr[i]; + if (c == '<' || c == '>' || c == '&' || c == '"' || c == '\'') { + needs_escape = true; + break; + } + } + if (!needs_escape) { + args.GetReturnValue().Set(input); + return; + } + } + + // Hit path: materialize UTF-8 for the actual escape pass. + const int input_len = input->Utf8Length(isolate); + std::string buf(static_cast(input_len), '\0'); + input->WriteUtf8(isolate, buf.data(), input_len, nullptr, + String::NO_NULL_TERMINATION); + + std::string out; + out.reserve(buf.size() + 16); + for (size_t i = 0; i < buf.size(); ++i) { + const char c = buf[i]; + switch (c) { + case '<': + out.append("<", 4); + break; + case '>': + out.append(">", 4); + break; + case '&': + out.append("&", 5); + break; + case '"': + out.append(""", 6); + break; + case '\'': + out.append("'", 5); + break; + default: + out.push_back(c); + } + } + + MaybeLocal result_maybe = + String::NewFromUtf8(isolate, out.data(), v8::NewStringType::kNormal, + static_cast(out.size())); + Local result; + if (!result_maybe.ToLocal(&result)) { + args.GetReturnValue().SetUndefined(); + return; + } + args.GetReturnValue().Set(result); +} + +// ─── stripAnsi: strip ANSI escape sequences from a string ───────────── +// +// Mirrors the npm `strip-ansi` package (https://github.com/chalk/strip-ansi), +// which compiles to a single regex via `ansi-regex`. The C++ form skips +// V8 regex compilation + UTF-16 string materialization on each call. +// +// Two sequence shapes are recognized (same as the upstream regex): +// +// 1. OSC (Operating System Command): +// ESC ']' ST +// where ST is one of: BEL (0x07), ESC '\\', or 0x9C (single-byte +// ST). Used for terminal title sets, hyperlinks, clipboard ops. +// +// 2. CSI (Control Sequence Introducer): +// (ESC | 0x9B) [\[\]()#;?]* (\d{1,4}([;:]\d{0,4})*)? +// where is one of: +// digit | A-P | R-T | Z | c | f-n | q-u | y | = | > | < | ~ +// Covers SGR (colors/attrs), cursor moves, scroll regions, etc. +// +// Input is UTF-8 bytes via a JS String → utf-8 conversion. Output is a +// new JS String containing the input minus the matched sequences. +// +// Performance: single allocation for the output (capacity = input +// length, since strip is a contraction). The state machine walks the +// input once, byte-by-byte. No regex engine, no backtracking. +// +// Compatibility note: V8's String::WriteUtf8 / NewFromUtf8 handle the +// UTF-8 round-trip; the state machine reasons in single bytes and +// passes non-ESC, non-0x9B bytes through verbatim (multi-byte UTF-8 +// sequences cannot collide with ANSI escape bytes — the high bit set +// on continuation bytes 0x80..0xBF and lead bytes 0xC0..0xFD takes +// care of that). + +namespace { + +// Returns true if c is a CSI "final byte" — the byte that terminates a +// CSI sequence. Matches the npm regex character class: +// [\dA-PR-TZcf-nq-uy=><~] +inline bool IsCsiFinalByte(uint8_t c) { + if (c >= '0' && c <= '9') { + return true; + } + if (c >= 'A' && c <= 'P') { + return true; + } + if (c >= 'R' && c <= 'T') { + return true; + } + if (c == 'Z') { + return true; + } + if (c == 'c') { + return true; + } + if (c >= 'f' && c <= 'n') { + return true; + } + if (c >= 'q' && c <= 'u') { + return true; + } + if (c == 'y') { + return true; + } + if (c == '=' || c == '>' || c == '<' || c == '~') { + return true; + } + return false; +} + +// CSI param/intermediate-class bytes (the `[\[\]()#;?]*` prefix and +// `\d{1,4}(?:[;:]\d{0,4})*` body, plus space-padding 0x20 the regex +// strictly doesn't allow — we keep it strict to match upstream). +inline bool IsCsiPrefixByte(uint8_t c) { + return c == '[' || c == ']' || c == '(' || c == ')' || c == '#' || + c == ';' || c == '?'; +} + +inline bool IsCsiParamByte(uint8_t c) { + return (c >= '0' && c <= '9') || c == ';' || c == ':'; +} + +} // namespace + +static void StripAnsi(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + if (args.Length() < 1 || !args[0]->IsString()) { + // Match strip-ansi.js: throws TypeError on non-string. We surface + // the same behavior by returning undefined and letting a JS wrapper + // throw — keeps the binding allocation-free in the error path. + args.GetReturnValue().SetUndefined(); + return; + } + Local input = args[0].As(); + const int char_len = input->Length(); + if (char_len == 0) { + args.GetReturnValue().Set(input); + return; + } + + // True fast path: detect ESC (0x1B) or CSI-introducer (0x9B) WITHOUT + // doing the UTF-8 round-trip. V8 stores strings as one-byte (Latin-1) + // or two-byte (UCS-2); both representations let us scan for the two + // sentinel bytes cheaply. + // + // For ≤kInlineThreshold-char inputs (the common case — ANSI status + // lines, log messages, prompts), the scan buffer goes on the stack. + // Stack memory is zero-init-free (vs std::string/vector which zero + // the buffer before WriteOneByte overwrites it) AND avoids a heap + // alloc. + // + // For ≤kInlineThreshold one-byte inputs we use std::memchr (libc's + // vectorized SIMD scan) twice — once for ESC, once for 0x9B. That's + // ~10x faster on ≥256-byte strings than per-byte branches. + constexpr int kInlineThreshold = 4096; + bool has_escape = false; + if (input->IsOneByte()) { + uint8_t stack_buf[kInlineThreshold]; + std::vector heap_buf; + uint8_t* scan_ptr; + if (char_len <= kInlineThreshold) { + scan_ptr = stack_buf; + } else { + heap_buf.resize(static_cast(char_len)); + scan_ptr = heap_buf.data(); + } + input->WriteOneByte(isolate, scan_ptr, /*start*/ 0, char_len, + String::NO_NULL_TERMINATION); + // Single SIMD pass over the buffer (SSE2 on x86-64, NEON on + // ARM64) — 16 bytes per cycle. Beats two memchr passes (which + // would each be vectorized internally but require two full scans + // of the input). + if (ContainsAnsiEscape(scan_ptr, static_cast(char_len))) { + has_escape = true; + } + if (!has_escape) { + args.GetReturnValue().Set(input); + return; + } + // Fall through to materialize UTF-8 below for the actual strip pass. + } else { + // Two-byte input: scan UCS-2 code units. No vectorized 16-bit + // memchr in stdc; per-element branch is fine here — two-byte V8 + // strings only appear when the input contains BMP-above-Latin-1 + // chars, which are uncommon in ANSI-bearing text. + uint16_t stack_buf[kInlineThreshold]; + std::vector heap_buf; + uint16_t* scan_ptr; + if (char_len <= kInlineThreshold) { + scan_ptr = stack_buf; + } else { + heap_buf.resize(static_cast(char_len)); + scan_ptr = heap_buf.data(); + } + input->Write(isolate, scan_ptr, /*start*/ 0, char_len, + String::NO_NULL_TERMINATION); + for (int i = 0; i < char_len; ++i) { + const uint16_t c = scan_ptr[i]; + if (c == 0x1B || c == 0x9B) { + has_escape = true; + break; + } + } + if (!has_escape) { + args.GetReturnValue().Set(input); + return; + } + } + + // Hit path: need to actually strip. Materialize UTF-8 now (we knew + // we couldn't avoid it once we found an ESC). + const int input_len_utf8 = input->Utf8Length(isolate); + std::string buf(static_cast(input_len_utf8), '\0'); + input->WriteUtf8(isolate, buf.data(), input_len_utf8, nullptr, + String::NO_NULL_TERMINATION); + + std::string out; + out.reserve(buf.size()); + + const uint8_t* p = reinterpret_cast(buf.data()); + const uint8_t* const end = p + buf.size(); + + while (p < end) { + const uint8_t b = *p; + + // ── OSC: ESC ']' ... ST OR CSI: ESC '[' ... final ── + if (b == 0x1B && p + 1 < end) { + const uint8_t b1 = *(p + 1); + if (b1 == ']') { + // OSC. Skip until ST: BEL (0x07), ESC '\\' (0x1B 0x5C), or + // 0x9C. Non-greedy. + const uint8_t* q = p + 2; + bool terminated = false; + while (q < end) { + const uint8_t bq = *q; + if (bq == 0x07 || bq == 0x9C) { + q += 1; + terminated = true; + break; + } + if (bq == 0x1B && q + 1 < end && *(q + 1) == 0x5C) { + q += 2; + terminated = true; + break; + } + q += 1; + } + if (terminated) { + p = q; + continue; + } + // Unterminated OSC — fall through and emit the ESC literally. + } else { + // Maybe CSI: ESC * * + const uint8_t* q = p + 1; + while (q < end && IsCsiPrefixByte(*q)) { + ++q; + } + while (q < end && IsCsiParamByte(*q)) { + ++q; + } + if (q < end && IsCsiFinalByte(*q)) { + // Matched CSI starting at p; skip it. + p = q + 1; + continue; + } + // Not a CSI; fall through to emit ESC literally. + } + } else if (b == 0x9B) { + // CSI starting with 0x9B (single-byte form). + const uint8_t* q = p + 1; + while (q < end && IsCsiPrefixByte(*q)) { + ++q; + } + while (q < end && IsCsiParamByte(*q)) { + ++q; + } + if (q < end && IsCsiFinalByte(*q)) { + p = q + 1; + continue; + } + // Not a CSI; fall through. + } + + // Pass-through byte. + out.push_back(static_cast(b)); + p += 1; + } + + MaybeLocal result_maybe = + String::NewFromUtf8(isolate, out.data(), v8::NewStringType::kNormal, + static_cast(out.size())); + Local result; + if (!result_maybe.ToLocal(&result)) { + args.GetReturnValue().SetUndefined(); + return; + } + args.GetReturnValue().Set(result); +} + static void Initialize(Local target, Local /* unused */, Local context, @@ -644,6 +1370,9 @@ static void Initialize(Local target, SetMethod(context, target, "applyBind", ApplyBind); SetMethod(context, target, "applySafe", ApplySafe); SetMethod(context, target, "bindCall", BindCall); + SetMethod(context, target, "decodeHtml", DecodeHtml); + SetMethod(context, target, "encodeHtml", EncodeHtml); + SetMethod(context, target, "stripAnsi", StripAnsi); SetMethod(context, target, "uncurryThis", UncurryThis); SetMethod(context, target, "weakRefSafe", WeakRefSafe); } @@ -652,6 +1381,9 @@ static void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(ApplyBind); registry->Register(ApplySafe); registry->Register(BindCall); + registry->Register(DecodeHtml); + registry->Register(EncodeHtml); + registry->Register(StripAnsi); registry->Register(UncurryThis); registry->Register(WeakRefSafe); // The internal call handlers are also externally referenced — they diff --git a/packages/node-smol-builder/additions/source-patched/src/socketsecurity/webgpu/webgpu_binding.cc b/packages/node-smol-builder/additions/source-patched/src/socketsecurity/webgpu/webgpu_binding.cc new file mode 100644 index 000000000..a62175ea8 --- /dev/null +++ b/packages/node-smol-builder/additions/source-patched/src/socketsecurity/webgpu/webgpu_binding.cc @@ -0,0 +1,171 @@ +// node:smol-webgpu binding — Dawn-detect entry shape. +// +// Build-time detection: when node-smol's configure step finds Dawn's +// build artifact (libwebgpu_dawn.a + headers from packages/dawn-builder), +// it defines HAVE_DAWN. Without Dawn, the binding compiles as a stub +// that reports unavailable. +// +// Detection contract for userland: +// +// const { isAvailable } = internalBinding('smol_webgpu') +// if (!isAvailable()) { +// // Fall back to userland shim or skip WebGPU-dependent features. +// } +// +// Why a compile-time gate (instead of dlopen at runtime)? +// +// Dawn ships a Node binding under src/dawn/node/ — adapting that +// surface to internalBinding shape is multi-week work (D6-D9). The +// v0 milestone (this file) lands the detection plumbing only. When +// dawn-builder produces its artifact and node-smol configure picks +// it up, isAvailable() returns true; the remaining methods still +// throw the structured "not yet wired" error until D6+ implements +// them. This keeps the JS surface stable across the rollout — code +// written against `isAvailable()` works today (always falls back) +// and continues to work once real Dawn lands (returns true and the +// call sites actually run). +// +// What lands later (D6-D9): +// +// - Real CreateInstance / RequestAdapter / RequestDevice backed by +// wgpu* C-API calls. +// - GPUCommandEncoder / GPURenderPassEncoder / GPUBuffer JS wrappers. +// - Adaptation of Dawn's src/dawn/node/ N-API binding to +// internalBinding shape (V1 milestone). +// +// The JS surface in lib/smol-webgpu.js mirrors the W3C WebGPU IDL +// (https://www.w3.org/TR/webgpu/) so userland code is portable. + +#include "node.h" +#include "node_binding.h" +#include "node_external_reference.h" +#include "util.h" +#include "v8.h" + +namespace node { +namespace socketsecurity { +namespace webgpu { + +using v8::Context; +using v8::Exception; +using v8::FunctionCallbackInfo; +using v8::Isolate; +using v8::Local; +using v8::Object; +using v8::String; +using v8::Value; + +namespace { + +// Single error message reused across every not-yet-implemented entry. +// Points at the fleet plan doc so users can find the integration +// design + an issue link to track progress. +const char kPendingMessage[] = + "node:smol-webgpu is not yet wired — Dawn integration pending. " + "See .claude/plans/dawn-webgpu-integration.md (D6-D9) for the " + "method rollout."; + +const char kUnavailableMessage[] = + "node:smol-webgpu unavailable — this node-smol build was not linked " + "against Dawn. Build dawn-builder (pnpm --filter dawn-builder run " + "build) and rebuild node-smol with the artifact present."; + +inline void ThrowPending(Isolate* isolate) { + Local msg = + String::NewFromUtf8(isolate, kPendingMessage, + v8::NewStringType::kInternalized) + .ToLocalChecked(); + isolate->ThrowException(Exception::Error(msg)); +} + +inline void ThrowUnavailable(Isolate* isolate) { + Local msg = + String::NewFromUtf8(isolate, kUnavailableMessage, + v8::NewStringType::kInternalized) + .ToLocalChecked(); + isolate->ThrowException(Exception::Error(msg)); +} + +} // namespace + +// IsAvailable is the ONE entry that returns rather than throws. It is +// the detection mechanism userland reads to decide whether to attempt +// the rest of the surface or fall back. Returns true iff this build +// was linked against Dawn. + +static void IsAvailable(const FunctionCallbackInfo& args) { +#ifdef HAVE_DAWN + args.GetReturnValue().Set(true); +#else + args.GetReturnValue().Set(false); +#endif +} + +// All other entries currently throw — kUnavailableMessage if the build +// has no Dawn, kPendingMessage otherwise (the method itself hasn't +// been implemented yet but Dawn IS present, so an upgrade unblocks +// it without a rebuild). The function names mirror the W3C WebGPU IDL +// one-for-one so the JS layer can re-export them under their +// canonical names. + +static void CreateInstance(const FunctionCallbackInfo& args) { +#ifdef HAVE_DAWN + ThrowPending(args.GetIsolate()); +#else + ThrowUnavailable(args.GetIsolate()); +#endif +} + +static void RequestAdapter(const FunctionCallbackInfo& args) { +#ifdef HAVE_DAWN + ThrowPending(args.GetIsolate()); +#else + ThrowUnavailable(args.GetIsolate()); +#endif +} + +static void RequestDevice(const FunctionCallbackInfo& args) { +#ifdef HAVE_DAWN + ThrowPending(args.GetIsolate()); +#else + ThrowUnavailable(args.GetIsolate()); +#endif +} + +static void GetPreferredCanvasFormat( + const FunctionCallbackInfo& args) { +#ifdef HAVE_DAWN + ThrowPending(args.GetIsolate()); +#else + ThrowUnavailable(args.GetIsolate()); +#endif +} + +static void Initialize(Local target, + Local /* unused */, + Local context, + void* /* priv */) { + SetMethod(context, target, "createInstance", CreateInstance); + SetMethod(context, target, "getPreferredCanvasFormat", + GetPreferredCanvasFormat); + SetMethod(context, target, "isAvailable", IsAvailable); + SetMethod(context, target, "requestAdapter", RequestAdapter); + SetMethod(context, target, "requestDevice", RequestDevice); +} + +static void RegisterExternalReferences(ExternalReferenceRegistry* registry) { + registry->Register(CreateInstance); + registry->Register(GetPreferredCanvasFormat); + registry->Register(IsAvailable); + registry->Register(RequestAdapter); + registry->Register(RequestDevice); +} + +} // namespace webgpu +} // namespace socketsecurity +} // namespace node + +NODE_BINDING_CONTEXT_AWARE_INTERNAL( + smol_webgpu, node::socketsecurity::webgpu::Initialize) +NODE_BINDING_EXTERNAL_REFERENCE( + smol_webgpu, node::socketsecurity::webgpu::RegisterExternalReferences) diff --git a/packages/node-smol-builder/examples/smol-markdown-render.mts b/packages/node-smol-builder/examples/smol-markdown-render.mts new file mode 100644 index 000000000..449faef3e --- /dev/null +++ b/packages/node-smol-builder/examples/smol-markdown-render.mts @@ -0,0 +1,249 @@ +#!/usr/bin/env node +/** + * node:smol-markdown demo — render AI-style Markdown output to the + * terminal using node:smol-tui as the renderer. + * + * Demonstrates the full Phase B integration: md4c parses input into + * an event stream, the demo walks events and dispatches each into + * node:smol-tui's DrawTextWrapped + DrawBox primitives. + * + * Run with socket-built node (see smol-tui-hello.mts for the binary + * path). + */ +import { + ATTRIBUTE_BASE_MASK, + TextAttributes, + createRenderer, + destroyRenderer, + rendererClear, + rendererDrawBox, + rendererDrawTextWrapped, + rendererFlush, + rendererSize, + stringWidth, +} from 'node:smol-tui' + +import { + blockType, + eventCategory, + parseMarkdown, + spanType, + textType, +} from 'node:smol-markdown' + +const FLUSH_BUF = new Uint8Array(256 * 1024) + +const SAMPLE_MD = `# Hello from smol-markdown + +This is a **CommonMark + GFM** Markdown parser implemented in C++ via +md4c, exposed as \`node:smol-markdown\` on socket-built Node. + +## Features + +- Native parsing (no JS regex engine) +- Full GFM dialect: tables, strikethrough, tasklists, autolinks +- Flat event stream — JS reconstructs the tree + +## Example code + +\`\`\`js +const events = parseMarkdown(text, 'github') +\`\`\` + +That's the whole API. +` + +const CATEGORY_MASK = 0xf000 +const VALUE_MASK = 0x0fff + +interface RenderState { + y: number + attrs: number + fgR: number + fgG: number + fgB: number + inHeading: boolean + headingLevel: number + rendererId: number + width: number +} + +export function processEvents( + events: Array<[number, undefined | string | number]>, + state: RenderState, +): void { + let line = '' + let lineAttrs = 0 + const flushLine = (): void => { + if (!line) { + return + } + const bytes = new TextEncoder().encode(line) + rendererDrawTextWrapped( + state.rendererId, + /* x */ 2, + /* y */ state.y, + /* maxWidth */ Math.max(20, state.width - 4), + /* maxLines */ 0, + bytes, + state.fgR, + state.fgG, + state.fgB, + 0, + 0, + 20, + lineAttrs & ATTRIBUTE_BASE_MASK, + ) + state.y += 1 + line = '' + lineAttrs = 0 + } + + for (let i = 0, { length } = events; i < length; i += 1) { + const [code, payload] = events[i] + const cat = code & CATEGORY_MASK + const val = code & VALUE_MASK + + if (cat === eventCategory.BLOCK_ENTER) { + if (val === blockType.H) { + flushLine() + state.inHeading = true + state.headingLevel = typeof payload === 'number' ? payload : 1 + state.fgR = state.headingLevel === 1 ? 255 : 200 + state.fgG = state.headingLevel === 1 ? 200 : 220 + state.fgB = 100 + lineAttrs = TextAttributes.BOLD + } else if (val === blockType.CODE) { + flushLine() + state.fgR = 150 + state.fgG = 255 + state.fgB = 150 + } else if (val === blockType.LI) { + flushLine() + line = ' • ' + } + } else if (cat === eventCategory.BLOCK_LEAVE) { + flushLine() + if (val === blockType.H) { + state.inHeading = false + state.fgR = 220 + state.fgG = 220 + state.fgB = 220 + state.y += 1 // blank line after heading + } else if (val === blockType.CODE) { + state.fgR = 220 + state.fgG = 220 + state.fgB = 220 + state.y += 1 + } else if (val === blockType.P) { + state.y += 1 + } + } else if (cat === eventCategory.SPAN_ENTER) { + if (val === spanType.STRONG) { + lineAttrs |= TextAttributes.BOLD + } else if (val === spanType.EM) { + lineAttrs |= TextAttributes.ITALIC + } else if (val === spanType.CODE) { + // Inline code: tint and keep going inline. + state.fgR = 150 + state.fgG = 255 + state.fgB = 150 + } + } else if (cat === eventCategory.SPAN_LEAVE) { + if (val === spanType.STRONG) { + lineAttrs &= ~TextAttributes.BOLD + } else if (val === spanType.EM) { + lineAttrs &= ~TextAttributes.ITALIC + } else if (val === spanType.CODE) { + state.fgR = state.inHeading ? 255 : 220 + state.fgG = state.inHeading ? 200 : 220 + state.fgB = state.inHeading ? 100 : 220 + } + } else if (cat === eventCategory.TEXT) { + if (typeof payload !== 'string') { + continue + } + // Handle newlines inside text payloads (e.g. code blocks). + if (val === textType.CODE && payload.includes('\n')) { + const lines = payload.split('\n') + for (let j = 0, { length: ll } = lines; j < ll; j += 1) { + line += lines[j] + if (j < ll - 1) { + flushLine() + } + } + } else { + line += payload + } + } + } + flushLine() +} + +function main(): void { + const cols = process.stdout.columns ?? 80 + const rows = process.stdout.rows ?? 40 + const rendererId = createRenderer(cols, rows, false, false) + stdoutWrite('\x1b[?1049h\x1b[?25l') + + const exit = (): void => { + stdoutWrite('\x1b[?25h\x1b[?1049l') + destroyRenderer(rendererId) + process.exit(0) + } + process.on('SIGINT', exit) + process.on('SIGTERM', exit) + + rendererClear(rendererId) + const { width, height } = rendererSize(rendererId) + + // Frame. + rendererDrawBox( + rendererId, + 0, + 0, + width, + height, + /* style */ 2, // rounded + /* sidesBits */ 0xf, + 100, + 200, + 255, + 0, + 0, + 20, + 0, + true, + ) + + // Parse with GitHub dialect (tables + strikethrough + tasklists + + // autolinks). + const events = parseMarkdown(SAMPLE_MD, 'github') + + const state: RenderState = { + y: 1, + attrs: 0, + fgR: 220, + fgG: 220, + fgB: 220, + inHeading: false, + headingLevel: 0, + rendererId, + width, + } + processEvents(events, state) + + const bytesWritten = rendererFlush(rendererId, FLUSH_BUF, FLUSH_BUF.length) + if (bytesWritten > 0 && bytesWritten < FLUSH_BUF.length) { + stdoutWrite(FLUSH_BUF.subarray(0, bytesWritten)) + } + + // Keep alive until Ctrl-C. + setInterval(() => {}, 60_000) +} + +export function stdoutWrite(data: Uint8Array | string): void { + process.stdout.write(data) // socket-hook: allow console +} + +main() diff --git a/packages/node-smol-builder/examples/smol-tui-hello.mts b/packages/node-smol-builder/examples/smol-tui-hello.mts new file mode 100644 index 000000000..de170b362 --- /dev/null +++ b/packages/node-smol-builder/examples/smol-tui-hello.mts @@ -0,0 +1,202 @@ +#!/usr/bin/env node +/** + * Minimal hello-world for node:smol-tui. + * + * Demonstrates the canonical render loop: create a renderer, draw a + * bordered box with text inside, flush to stdout, await Ctrl-C. + * + * Run with a socket-built node (the regular Node.js binary doesn't + * have `node:smol-tui`): + * + * ./packages/node-smol-builder/build/dev/darwin-arm64/out/socket-node \ + * packages/node-smol-builder/examples/smol-tui-hello.mts + * + * This file is the canonical reference for how userland TUI code + * should wire the binding together — copy-paste this into your app + * and adapt. + */ + +// node:smol-tui is a builtin on socket-built node. Userland imports +// it the same way it would import any node: module. +// +// Runtime check: a regular Node.js binary will throw +// ERR_UNKNOWN_BUILTIN_MODULE here. That's the signal to fall back to +// userland @opentui/core or skip the smol path entirely. +import { + ATTRIBUTE_BASE_MASK, + TextAttributes, + codepointWidth, + createRenderer, + destroyRenderer, + rendererClear, + rendererDrawBox, + rendererDrawTextWrapped, + rendererFlush, + rendererResize, + rendererSize, + stringWidth, +} from 'node:smol-tui' + +// Constants matching tui::BorderStyle enum (see include/tui/renderables.hpp). +const BORDER_SINGLE = 0 +const BORDER_DOUBLE = 1 +const BORDER_ROUNDED = 2 +const BORDER_HEAVY = 3 + +const SIDES_ALL = 0xf // top | right | bottom | left + +// ANSI flush buffer: re-used frame-to-frame; one allocation per app. +// 256 KB covers up to a 200×60 grid in the worst case (every cell +// changes between frames). +const FLUSH_BUF = new Uint8Array(256 * 1024) + +function main(): void { + const { width: initialWidth, height: initialHeight } = getTerminalSize() + const rendererId = createRenderer(initialWidth, initialHeight, false, false) + + // Enter alt-screen + hide cursor. + stdoutWrite('\x1b[?1049h\x1b[?25l') + + const exit = () => { + stdoutWrite('\x1b[?25h\x1b[?1049l') + destroyRenderer(rendererId) + process.exit(0) + } + + process.on('SIGINT', exit) + process.on('SIGTERM', exit) + + process.stdout.on('resize', () => { + const { width, height } = getTerminalSize() + rendererResize(rendererId, width, height) + drawFrame(rendererId) + }) + + drawFrame(rendererId) + + // Keep the event loop alive. + setInterval(() => {}, 60_000) +} + +export function drawFrame(rendererId: number): void { + const { width, height } = rendererSize(rendererId) + rendererClear(rendererId) + + // Outer bordered box. + rendererDrawBox( + rendererId, + 0, + 0, + width, + height, + BORDER_ROUNDED, + SIDES_ALL, + /* borderFg */ 100, + 200, + 255, + /* bg */ 0, + 0, + 20, + /* attrs */ 0, + /* fillBackground */ true, + ) + + // Title bar (manual since rendererDrawBox doesn't take title yet — + // see the tui-infra-renderables row in .config/lockstep.json + // deviations). + const title = ' node:smol-tui demo ' + const titleBytes = new TextEncoder().encode(title) + const titleWidth = stringWidth(title) + const titleX = Math.max(2, Math.floor((width - titleWidth) / 2)) + rendererDrawTextWrapped( + rendererId, + titleX, + 0, + /* maxWidth */ titleWidth, + /* maxLines */ 1, + titleBytes, + /* fg */ 255, + 255, + 255, + /* bg */ 0, + 0, + 20, + TextAttributes.BOLD & ATTRIBUTE_BASE_MASK, + ) + + // Inner content — wrap-aware text. + const body = + 'Hello from a socket-built Node! This renderer runs the entire ' + + 'flush loop in C++ (tui::Renderer::Flush) and draws boxes / wrapped ' + + 'text via tui::DrawBox / tui::DrawTextWrapped. Press Ctrl-C to exit.' + const bodyBytes = new TextEncoder().encode(body) + rendererDrawTextWrapped( + rendererId, + /* x */ 3, + /* y */ 2, + /* maxWidth */ Math.max(20, width - 6), + /* maxLines */ Math.max(1, height - 4), + bodyBytes, + /* fg */ 200, + 220, + 255, + /* bg */ 0, + 0, + 20, + /* attrs */ 0, + ) + + // Footer hint. + const hint = ` ${width}x${height} cells ` + const hintBytes = new TextEncoder().encode(hint) + rendererDrawTextWrapped( + rendererId, + width - stringWidth(hint) - 2, + height - 1, + stringWidth(hint), + 1, + hintBytes, + 150, + 150, + 150, + 0, + 0, + 20, + TextAttributes.DIM & ATTRIBUTE_BASE_MASK, + ) + + // Flush the diff to stdout. + const bytesWritten = rendererFlush(rendererId, FLUSH_BUF, FLUSH_BUF.length) + if (bytesWritten > 0 && bytesWritten < FLUSH_BUF.length) { + stdoutWrite(FLUSH_BUF.subarray(0, bytesWritten)) + } +} + +export function getTerminalSize(): { width: number; height: number } { + const cols = process.stdout.columns ?? 80 + const rows = process.stdout.rows ?? 24 + return { width: cols, height: rows } +} + +// Raw ANSI writes go through stdout directly — logger.info() would +// re-encode + prefix the bytes which breaks the terminal sequences. +// stdoutWrite isolates the marker into one helper. +export function stdoutWrite(data: Uint8Array | string): void { + process.stdout.write(data) // socket-hook: allow console +} + +main() + +// Quick verification — these checks run before the render loop so a +// broken binding surfaces with a clear error rather than a blank +// screen. Each call exercises one of the C++ entry points landed in +// the B-* commit series. +export function verify(): void { + // codepointWidth + stringWidth (Unicode 17.0 tables). + console.assert(codepointWidth(0x61) === 1, 'codepointWidth(a) === 1') + console.assert(codepointWidth(0x4e2d) === 2, 'codepointWidth(中) === 2') + console.assert(stringWidth('hello') === 5, 'stringWidth(hello) === 5') + console.assert(stringWidth('中文') === 4, 'stringWidth(中文) === 4') + console.assert(stringWidth('') === 0, 'stringWidth("") === 0') +} +void verify // referenced for type-checking, not invoked in the demo diff --git a/packages/node-smol-builder/patches/source-patched/003-realm-smol-bindings.patch b/packages/node-smol-builder/patches/source-patched/003-realm-smol-bindings.patch index 8af433bba..8f30406cf 100644 --- a/packages/node-smol-builder/patches/source-patched/003-realm-smol-bindings.patch +++ b/packages/node-smol-builder/patches/source-patched/003-realm-smol-bindings.patch @@ -24,7 +24,7 @@ 'spawn_sync', 'stream_wrap', 'tcp_wrap', -@@ -127,6 +128,20 @@ +@@ -127,6 +128,27 @@ 'ffi', 'sea', 'sqlite', @@ -34,14 +34,21 @@ + 'smol-http', + 'smol-https', + 'smol-ilp', ++ 'smol-keymap', + 'smol-manifest', ++ 'smol-markdown', + 'smol-power', + 'smol-primordial', + 'smol-purl', ++ 'smol-qrcode', ++ 'smol-quic', + 'smol-sql', ++ 'smol-tree-sitter', ++ 'smol-tui', + 'smol-util', + 'smol-vfs', + 'smol-versions', ++ 'smol-webgpu', 'quic', 'test', 'test/reporters', diff --git a/packages/node-smol-builder/patches/source-patched/004-node-gyp-smol-sources.patch b/packages/node-smol-builder/patches/source-patched/004-node-gyp-smol-sources.patch index 11be7bb47..fd79c5bbf 100644 --- a/packages/node-smol-builder/patches/source-patched/004-node-gyp-smol-sources.patch +++ b/packages/node-smol-builder/patches/source-patched/004-node-gyp-smol-sources.patch @@ -71,7 +71,7 @@ ], }], # Thin LTO for node_main.cc and linker (scoped to node_exe) -@@ -1055,14 +1079,282 @@ +@@ -1055,14 +1079,322 @@ ], 'dependencies': [ 'deps/ncrypto/ncrypto.gyp:ncrypto', @@ -123,7 +123,9 @@ + 'src/socketsecurity/ffi/trampoline.cc', + # Socket Security: smol-power binding (platform-agnostic glue). + 'src/socketsecurity/power/power_binding.cc', -+ # Socket Security: smol-util binding (V8 uncurry / applyBind). ++ # Socket Security: smol-util binding (V8 uncurry / applyBind + ++ # native stripAnsi/encodeHtml/decodeHtml). ++ 'src/socketsecurity/util/entities_data.cc', + 'src/socketsecurity/util/util_binding.cc', + # Socket Security: smol-primordial binding (frozen primordials). + 'src/socketsecurity/primordial/primordial_binding.cc', @@ -132,6 +134,41 @@ + 'src/socketsecurity/versions/versions_binding.cc', + # Socket Security: smol-manifest-native binding (lockfile parsers). + 'src/socketsecurity/manifest/manifest.cc', ++ # Socket Security: smol-keymap binding (chord matcher). ++ 'src/socketsecurity/keymap/keymap_binding.cc', ++ # Socket Security: smol-qrcode binding (libqrencode v4.1.1). ++ # Source files copied from upstream/libqrencode/ by ++ # prepare-external-sources.mts; qrenc.c (the CLI tool's ++ # main()) is excluded from the build. ++ 'src/socketsecurity/qrcode/qrcode_binding.cc', ++ 'src/socketsecurity/qrcode/libqrencode/bitstream.c', ++ 'src/socketsecurity/qrcode/libqrencode/mask.c', ++ 'src/socketsecurity/qrcode/libqrencode/mmask.c', ++ 'src/socketsecurity/qrcode/libqrencode/mqrspec.c', ++ 'src/socketsecurity/qrcode/libqrencode/qrencode.c', ++ 'src/socketsecurity/qrcode/libqrencode/qrinput.c', ++ 'src/socketsecurity/qrcode/libqrencode/qrspec.c', ++ 'src/socketsecurity/qrcode/libqrencode/rsecc.c', ++ 'src/socketsecurity/qrcode/libqrencode/split.c', ++ # Socket Security: smol-markdown binding (CommonMark + GFM via ++ # md4c). md4c.c + entity.c are copied from ++ # packages/node-smol-builder/upstream/md4c/src/ by ++ # prepare-external-sources.mts at build time. ++ 'src/socketsecurity/markdown/markdown_binding.cc', ++ 'src/socketsecurity/markdown/md4c.c', ++ 'src/socketsecurity/markdown/entity.c', ++ # Socket Security: smol-tree-sitter binding. tree-sitter's ++ # lib.c is an umbrella that #includes every other lib/src/*.c ++ # via relative path, so only lib.c needs listing here. ++ 'src/socketsecurity/tree_sitter/tree_sitter_binding.cc', ++ 'src/socketsecurity/tree_sitter/tree-sitter/src/lib.c', ++ # Socket Security: smol-webgpu binding (stub). Exposes the ++ # navigator.gpu surface so userland WebGPU code resolves the ++ # require('node:smol-webgpu') import. Every method except ++ # isAvailable() throws "not yet wired"; Dawn integration is ++ # tracked separately in .claude/plans/opentui-smol-tui-completion.md ++ # Phase C. ++ 'src/socketsecurity/webgpu/webgpu_binding.cc', + 'src/socketsecurity/manifest/manifest_binding.cc', + 'src/socketsecurity/manifest/parser_cargo.cc', + 'src/socketsecurity/manifest/parser_npm.cc', @@ -176,8 +213,11 @@ + 'src/socketsecurity/tui/ansi.cc', + 'src/socketsecurity/tui/buffer.cc', + 'src/socketsecurity/tui/mouse.cc', ++ 'src/socketsecurity/tui/renderables.cc', + 'src/socketsecurity/tui/renderer.cc', + 'src/socketsecurity/tui/tui_binding.cc', ++ 'src/socketsecurity/tui/width.cc', ++ 'src/socketsecurity/tui/width_data.cc', + # node:smol-quic — QUIC + UDP transport. lsquic v4.6.2 with + # OpenSSL 3.5.6 as the TLS backend (matches bun). The + # binding splits the engine/connection surface (quic_binding.cc) @@ -355,7 +395,7 @@ }], [ 'node_use_sqlite=="true"', { 'sources': [ -@@ -1084,7 +1363,7 @@ +@@ -1084,7 +1403,7 @@ ], }], - [ 'node_use_quic=="true"', { @@ -364,7 +404,7 @@ '<@(node_quic_sources)', ], }], -@@ -1406,7 +1685,7 @@ +@@ -1406,7 +1725,7 @@ }, { 'sources!': [ '<@(node_cctest_openssl_sources)' ], }], @@ -373,7 +413,7 @@ 'defines': [ 'HAVE_QUIC=1', ], -@@ -1447,6 +1726,17 @@ +@@ -1447,6 +1766,17 @@ 'Ws2_32.lib', ], }], @@ -391,7 +431,7 @@ # Avoid excessive LTO ['enable_lto=="true"', { 'ldflags': [ '-fno-lto' ], -@@ -1521,6 +1810,17 @@ +@@ -1521,6 +1850,17 @@ 'Ws2_32.lib', ], }], @@ -409,7 +449,7 @@ # Avoid excessive LTO ['enable_lto=="true"', { 'ldflags': [ '-fno-lto' ], -@@ -1744,6 +2043,17 @@ +@@ -1744,6 +2083,17 @@ 'Ws2_32.lib', ], }], diff --git a/packages/node-smol-builder/patches/source-patched/017-smol-builtin-bindings.patch b/packages/node-smol-builder/patches/source-patched/017-smol-builtin-bindings.patch index f488a89ed..9f939f35f 100644 --- a/packages/node-smol-builder/patches/source-patched/017-smol-builtin-bindings.patch +++ b/packages/node-smol-builder/patches/source-patched/017-smol-builtin-bindings.patch @@ -3,9 +3,10 @@ # # Replaces wasi with smol_vfs in the standard binding macro and adds the # full smol native binding set: smol_ffi, smol_http, smol_ilp, -# smol_manifest_native, smol_postgres (conditional on HAVE_POSTGRES), -# smol_power, smol_primordial, smol_quic, smol_tui, smol_util, -# smol_versions_native, and smol_webstreams. +# smol_keymap, smol_manifest_native, smol_markdown, smol_postgres +# (conditional on HAVE_POSTGRES), smol_power, smol_primordial, +# smol_qrcode, smol_quic, smol_tree_sitter, smol_tui, smol_util, +# smol_versions_native, smol_webgpu (stub), and smol_webstreams. # --- a/src/node_binding.cc +++ b/src/node_binding.cc @@ -19,7 +20,7 @@ V(wasm_web_api) \ V(watchdog) \ V(worker) \ -@@ -108,7 +109,19 @@ +@@ -108,7 +109,24 @@ NODE_BUILTIN_DEBUG_BINDINGS(V) \ NODE_BUILTIN_QUIC_BINDINGS(V) \ NODE_BUILTIN_SQLITE_BINDINGS(V) \ @@ -29,13 +30,18 @@ + V(smol_ffi) \ + V(smol_http) \ + V(smol_ilp) \ ++ V(smol_keymap) \ + V(smol_manifest_native) \ ++ V(smol_markdown) \ + V(smol_power) \ + V(smol_primordial) \ ++ V(smol_qrcode) \ + V(smol_quic) \ ++ V(smol_tree_sitter) \ + V(smol_tui) \ + V(smol_util) \ + V(smol_versions_native) \ ++ V(smol_webgpu) \ + V(smol_webstreams) // This is used to load built-in bindings. Instead of using diff --git a/packages/node-smol-builder/scripts/audit-glibc-symbols.mts b/packages/node-smol-builder/scripts/audit-glibc-symbols.mts index 7946a2062..2354779ab 100644 --- a/packages/node-smol-builder/scripts/audit-glibc-symbols.mts +++ b/packages/node-smol-builder/scripts/audit-glibc-symbols.mts @@ -1,5 +1,4 @@ #!/usr/bin/env node -/* oxlint-disable socket/no-status-emoji -- emoji are column-aligned table cell markers ("✓ yes "/"✗ NO "), not status prefixes. */ /** * @fileoverview Enumerate GLIBC_2.x symbol versions pulled in by the built node binary. @@ -250,6 +249,7 @@ async function main() { logger.log(' ---------|-------------------') for (let i = 0, { length } = violations; i < length; i += 1) { const v = violations[i] + // oxlint-disable-next-line socket/no-status-emoji -- emoji are column-aligned table cell markers ("✓ yes "/"✗ NO "), not status prefixes. const has = wrapped.has(v.symbol) ? '✓ yes ' : '✗ NO ' logger.log(` ${has} | GLIBC_${v.version.padEnd(6)} ${v.symbol}`) } diff --git a/packages/node-smol-builder/scripts/binary-compressed/shared/compress-binary.mts b/packages/node-smol-builder/scripts/binary-compressed/shared/compress-binary.mts index 685d3a465..7d954a92a 100644 --- a/packages/node-smol-builder/scripts/binary-compressed/shared/compress-binary.mts +++ b/packages/node-smol-builder/scripts/binary-compressed/shared/compress-binary.mts @@ -61,46 +61,6 @@ const PLATFORM_CONFIG = { }, } -/** - * Detect libc variant (musl vs glibc) for a Linux binary. - * Uses ldd to check which C library the binary is linked against. - * - * @param {string} binaryPath - Path to binary to analyze - * @returns {Promise} - LIBC_VALUES.musl, LIBC_VALUES.glibc, or LIBC_VALUES.na - */ -export async function _detectLibc(binaryPath) { - try { - // Run ldd on the binary and check output. - const result = await spawn('ldd', [binaryPath], { - encoding: 'utf8', - timeout: 5000, - }) - - const output = result.stdout + result.stderr - - // Check for musl first (more specific). - if (output.includes('musl')) { - return LIBC_VALUES.musl - } - - // Check for glibc indicators. - if (output.includes('libc.so') || output.includes('glibc')) { - return LIBC_VALUES.glibc - } - - // Default to glibc (most common on Linux). - logger.warn( - `Could not determine libc variant for ${binaryPath}, defaulting to glibc`, - ) - return LIBC_VALUES.glibc - } catch (e) { - logger.warn( - `Failed to detect libc variant for ${binaryPath}: ${errorMessage(e)}, defaulting to glibc`, - ) - return LIBC_VALUES.glibc - } -} - /** * Compress binary using binpress (zstd compression). */ @@ -197,6 +157,46 @@ export async function compressBinary( return } +/** + * Detect libc variant (musl vs glibc) for a Linux binary. + * Uses ldd to check which C library the binary is linked against. + * + * @param {string} binaryPath - Path to binary to analyze + * @returns {Promise} - LIBC_VALUES.musl, LIBC_VALUES.glibc, or LIBC_VALUES.na + */ +export async function detectLibc(binaryPath) { + try { + // Run ldd on the binary and check output. + const result = await spawn('ldd', [binaryPath], { + encoding: 'utf8', + timeout: 5000, + }) + + const output = result.stdout + result.stderr + + // Check for musl first (more specific). + if (output.includes('musl')) { + return LIBC_VALUES.musl + } + + // Check for glibc indicators. + if (output.includes('libc.so') || output.includes('glibc')) { + return LIBC_VALUES.glibc + } + + // Default to glibc (most common on Linux). + logger.warn( + `Could not determine libc variant for ${binaryPath}, defaulting to glibc`, + ) + return LIBC_VALUES.glibc + } catch (e) { + logger.warn( + `Failed to detect libc variant for ${binaryPath}: ${errorMessage(e)}, defaulting to glibc`, + ) + return LIBC_VALUES.glibc + } +} + /** * Get file size in MB. */ diff --git a/packages/node-smol-builder/scripts/binary-released/shared/build-released.mts b/packages/node-smol-builder/scripts/binary-released/shared/build-released.mts index 729c0ce78..3a4efd176 100644 --- a/packages/node-smol-builder/scripts/binary-released/shared/build-released.mts +++ b/packages/node-smol-builder/scripts/binary-released/shared/build-released.mts @@ -1,5 +1,4 @@ // max-file-lines: legitimate -- orchestration script — top-down pipeline (gather → validate → report); splitting fractures the flow -/* oxlint-disable socket/no-status-emoji -- emoji is wrapped in colors.green() decorator before being embedded in multi-line build summary; logger.success() would drop the color. */ /** * @fileoverview Release Binary Build Phase @@ -165,7 +164,7 @@ export async function buildRelease(config, buildOptions = {}) { } } - const _IS_MACOS = platform === 'darwin' + const IS_MACOS = platform === 'darwin' const IS_WINDOWS = platform === 'win32' const IS_LINUX = platform === 'linux' const IS_MUSL = libc === 'musl' @@ -660,7 +659,7 @@ export async function buildRelease(config, buildOptions = {}) { // Enable LTO for smaller binaries and better optimization. // Linux: Uses GCC/Clang LTO. // macOS: Uses Apple Clang's -flto=thin for faster builds with good optimization. - if (IS_LINUX || _IS_MACOS) { + if (IS_LINUX || IS_MACOS) { configureFlags.push('--enable-lto') } } @@ -769,6 +768,7 @@ export async function buildRelease(config, buildOptions = {}) { }) logger.log('::endgroup::') logger.log( + // oxlint-disable-next-line socket/no-status-emoji -- emoji is wrapped in colors.green() decorator before being embedded in multi-line build summary; logger.success() would drop the color. `${colors.green('✓')} ${WIN32 ? 'Build' : 'Configuration'} complete`, ) logger.log('') @@ -850,10 +850,11 @@ export async function buildRelease(config, buildOptions = {}) { // mtime check beats a "run configure unconditionally" sledgehammer. const ninjaPath = path.join(modeSourceDir, 'out/Release/build.ninja') const gypPath = path.join(modeSourceDir, 'node.gyp') - /* oxlint-disable socket/prefer-exists-sync -- need mtimeMs for staleness comparison */ try { const [ninjaStat, gypStat] = await Promise.all([ + // oxlint-disable-next-line socket/prefer-exists-sync -- need mtimeMs for staleness comparison fs.stat(ninjaPath), + // oxlint-disable-next-line socket/prefer-exists-sync -- need mtimeMs for staleness comparison fs.stat(gypPath), ]) if (gypStat.mtimeMs > ninjaStat.mtimeMs) { diff --git a/packages/node-smol-builder/scripts/binary-released/shared/prepare-external-sources.mts b/packages/node-smol-builder/scripts/binary-released/shared/prepare-external-sources.mts index 794153e17..b5ac2fb91 100644 --- a/packages/node-smol-builder/scripts/binary-released/shared/prepare-external-sources.mts +++ b/packages/node-smol-builder/scripts/binary-released/shared/prepare-external-sources.mts @@ -29,6 +29,62 @@ import { // Upstream liburing is in node-smol-builder/upstream/liburing (sibling to upstream/node). const LIBURING_UPSTREAM_DIR = path.join(PACKAGE_ROOT, 'upstream', 'liburing') +// Upstream md4c (CommonMark + GFM Markdown parser) is sibling to upstream/node. +// md4c.c + entity.c are compiled into the smol-markdown binding. +const MD4C_UPSTREAM_DIR = path.join(PACKAGE_ROOT, 'upstream', 'md4c') + +// Upstream tree-sitter (incremental parser library) is sibling to upstream/node. +// lib/src/lib.c is the umbrella TU that includes all parser sources; +// lib/include/tree_sitter/api.h is the public header consumed by the binding. +const TREE_SITTER_UPSTREAM_DIR = path.join( + PACKAGE_ROOT, + 'upstream', + 'tree-sitter', +) + +// Upstream libqrencode (QR code encoder) is sibling to upstream/node. +// All .c + .h files live at the root of the repo; we lift the whole +// set into src/socketsecurity/qrcode/libqrencode/ so sibling +// `#include "qrencode.h"` etc. inside libqrencode itself resolves. +const LIBQRENCODE_UPSTREAM_DIR = path.join( + PACKAGE_ROOT, + 'upstream', + 'libqrencode', +) + +// dawn-builder ships the prebuilt libwebgpu_dawn.a + headers (built +// by packages/dawn-builder/scripts/build.mts) that node:smol-webgpu +// links against. We DON'T copy Dawn's source tree into the patched +// node source — instead we link the prebuilt static lib at the +// node.gyp level (wired in D5+). +// +// Dawn participates in the SOURCE_PATCHED cache key via the +// submodule pin: every Dawn bump rewrites the `# dawn-chromium/` +// comment in .gitmodules, so hashing .gitmodules (and the lockstep +// JSON, which also tracks pinned_sha) captures Dawn invalidation +// without walking Dawn's 180 MB source tree on every cache check. +const DAWN_BUILDER_DIR = path.join(PACKAGE_ROOT, '..', 'dawn-builder') +const DAWN_UPSTREAM_DIR = path.join(DAWN_BUILDER_DIR, 'upstream', 'dawn') + +/** + * Files outside the regular MONOREPO_PACKAGE_SOURCES tree that still + * need to participate in the SOURCE_PATCHED cache key. These are + * "pin files" — single files whose content reflects the version of + * an external dependency that the build will link against (without + * copying that dependency's source into the patched tree). + * + * Currently: + * - .gitmodules — every submodule SHA bump rewrites at least the + * version comment line, so hashing this file catches Dawn, + * md4c, tree-sitter, libqrencode, etc. bumps in one shot. + * - .config/lockstep.json — tracks pinned_sha for every upstream; + * hashing this is a redundant safety net. + */ +export const EXTERNAL_PIN_FILES = [ + path.join(PACKAGE_ROOT, '..', '..', '.gitmodules'), + path.join(PACKAGE_ROOT, '..', '..', '.config', 'lockstep.json'), +] + // Upstream uSockets/uWebSockets for high-performance HTTP server (node:smol-http). // uSockets provides direct epoll/kqueue event loop + raw socket I/O. // uWebSockets provides HTTP parser (SWAR+bloom), cork buffer, response writer. @@ -170,6 +226,86 @@ const VENDORED_SOURCES = [ from: path.join(YOGA_LAYOUT_BUILDER_DIR, 'upstream', 'yoga', 'yoga'), to: path.join(ADDITIONS_SOURCE_PATCHED_DIR, 'deps', 'yoga'), }, + // md4c: CommonMark + GFM Markdown parser. We lift the four source + // files (md4c.c + md4c.h + entity.c + entity.h) into + // src/socketsecurity/markdown/ alongside markdown_binding.cc so + // `#include "md4c.h"` resolves via the existing 'src' include_dirs + // entry without needing a new include path. + { + from: path.join(MD4C_UPSTREAM_DIR, 'src', 'md4c.c'), + to: path.join( + ADDITIONS_SOURCE_PATCHED_DIR, + 'src', + 'socketsecurity', + 'markdown', + 'md4c.c', + ), + }, + { + from: path.join(MD4C_UPSTREAM_DIR, 'src', 'md4c.h'), + to: path.join( + ADDITIONS_SOURCE_PATCHED_DIR, + 'src', + 'socketsecurity', + 'markdown', + 'md4c.h', + ), + }, + { + from: path.join(MD4C_UPSTREAM_DIR, 'src', 'entity.c'), + to: path.join( + ADDITIONS_SOURCE_PATCHED_DIR, + 'src', + 'socketsecurity', + 'markdown', + 'entity.c', + ), + }, + { + from: path.join(MD4C_UPSTREAM_DIR, 'src', 'entity.h'), + to: path.join( + ADDITIONS_SOURCE_PATCHED_DIR, + 'src', + 'socketsecurity', + 'markdown', + 'entity.h', + ), + }, + // tree-sitter: incremental parser library. The lib/ directory holds + // the umbrella lib.c (which includes every other .c via relative + // path) + all internal headers (alloc.h, parser.h, ...) + the + // public include/tree_sitter/api.h. We copy the whole subtree under + // src/socketsecurity/tree_sitter/tree-sitter/ so: + // - tree_sitter_binding.cc's `#include + // "socketsecurity/tree_sitter/tree_sitter/api.h"` resolves + // - the umbrella lib.c's `#include "./*.c"` works (siblings stay + // adjacent inside lib/src/) + { + from: path.join(TREE_SITTER_UPSTREAM_DIR, 'lib'), + to: path.join( + ADDITIONS_SOURCE_PATCHED_DIR, + 'src', + 'socketsecurity', + 'tree_sitter', + 'tree-sitter', + ), + }, + // libqrencode: QR code encoder. All .c + .h files live at repo root + // and use sibling-relative #includes ("qrencode.h", "qrspec.h", ...). + // Lifting the whole repo into src/socketsecurity/qrcode/libqrencode/ + // keeps siblings adjacent so the includes resolve. qrenc.c (CLI + // tool with main()) is copied too but NOT listed in node.gyp, so + // it's silently ignored at link time. + { + from: LIBQRENCODE_UPSTREAM_DIR, + to: path.join( + ADDITIONS_SOURCE_PATCHED_DIR, + 'src', + 'socketsecurity', + 'qrcode', + 'libqrencode', + ), + }, ] const EXTERNAL_SOURCES = [...VENDORED_SOURCES] diff --git a/packages/node-smol-builder/scripts/binary-released/shared/release.mts b/packages/node-smol-builder/scripts/binary-released/shared/release.mts index 4d3810561..c6e2f9634 100644 --- a/packages/node-smol-builder/scripts/binary-released/shared/release.mts +++ b/packages/node-smol-builder/scripts/binary-released/shared/release.mts @@ -1,7 +1,5 @@ #!/usr/bin/env node // max-file-lines: legitimate -- orchestration script — top-down pipeline (gather → validate → report); splitting fractures the flow -/* oxlint-disable socket/no-status-emoji -- emoji is wrapped in colors.green() decorator before being embedded in multi-line release summary; logger.success() would drop the color. */ -/* oxlint-disable socket/sort-source-methods -- release script ordered as a top-down pipeline (gather artifacts → checksum → assemble notes → upload → publish); alphabetizing would scatter the flow. */ /** * @fileoverview Deploy built smol binaries to GitHub Releases @@ -146,6 +144,7 @@ export function getArchivePlatform(platform, arch, libc) { * Check if GitHub API is authenticated. * Validates by attempting to get authenticated user. */ +// oxlint-disable-next-line socket/sort-source-methods -- release script ordered as a top-down pipeline (gather artifacts → checksum → assemble notes → upload → publish); alphabetizing would scatter the flow. export async function checkGitHubAuth() { try { const octokit = new Octokit({ @@ -161,6 +160,7 @@ export async function checkGitHubAuth() { /** * Calculate SHA-256 checksum of a file. */ +// oxlint-disable-next-line socket/sort-source-methods -- release script ordered as a top-down pipeline (gather artifacts → checksum → assemble notes → upload → publish); alphabetizing would scatter the flow. export async function calculateChecksum(filePath) { const hash = crypto.createHash('sha256') const stream = createReadStream(filePath) @@ -179,6 +179,7 @@ export async function calculateChecksum(filePath) { * 1. build/${BUILD_MODE}//out/Final/node/ (if building for current platform) * 2. build/${BUILD_MODE}//cache/node-{platform}-{arch} (from cached builds) */ +// oxlint-disable-next-line socket/sort-source-methods -- release script ordered as a top-down pipeline (gather artifacts → checksum → assemble notes → upload → publish); alphabetizing would scatter the flow. export async function findBinary(platform, arch, libc) { // Check Final build (if current platform). const binaryName = platform === 'win32' ? 'node.exe' : 'node' @@ -222,6 +223,7 @@ export async function findBinary(platform, arch, libc) { * * This enables deterministic cache keys when the binary is used. */ +// oxlint-disable-next-line socket/sort-source-methods -- release script ordered as a top-down pipeline (gather artifacts → checksum → assemble notes → upload → publish); alphabetizing would scatter the flow. export async function embedSmolSpec( binaryPath, _platform, @@ -248,6 +250,7 @@ export async function embedSmolSpec( /** * Create release archive for a platform. */ +// oxlint-disable-next-line socket/sort-source-methods -- release script ordered as a top-down pipeline (gather artifacts → checksum → assemble notes → upload → publish); alphabetizing would scatter the flow. export async function createReleaseArchive( platform, arch, @@ -378,6 +381,7 @@ export async function releaseExists(tag) { /** * Delete existing release. */ +// oxlint-disable-next-line socket/sort-source-methods -- release script ordered as a top-down pipeline (gather artifacts → checksum → assemble notes → upload → publish); alphabetizing would scatter the flow. export async function deleteRelease(tag) { logger.log('') logger.log(`Deleting existing release: ${tag}`) @@ -403,6 +407,7 @@ export async function deleteRelease(tag) { /** * Create GitHub release. */ +// oxlint-disable-next-line socket/sort-source-methods -- release script ordered as a top-down pipeline (gather artifacts → checksum → assemble notes → upload → publish); alphabetizing would scatter the flow. export async function createGitHubRelease( tag, archives, @@ -575,6 +580,7 @@ async function main() { logger.log('') logger.log( + // oxlint-disable-next-line socket/no-status-emoji -- emoji is wrapped in colors.green() decorator before being embedded in multi-line release summary; logger.success() would drop the color. `${colors.green('✓')} Release ${PUBLISH ? 'published' : 'created as draft'}!`, ) logger.log('') diff --git a/packages/node-smol-builder/scripts/binary-stripped/shared/build-stripped.mts b/packages/node-smol-builder/scripts/binary-stripped/shared/build-stripped.mts index 7febd6a1c..e42833fa1 100644 --- a/packages/node-smol-builder/scripts/binary-stripped/shared/build-stripped.mts +++ b/packages/node-smol-builder/scripts/binary-stripped/shared/build-stripped.mts @@ -1,5 +1,4 @@ #!/usr/bin/env node -/* oxlint-disable socket/no-status-emoji -- emoji is wrapped in colors.green() decorator (composes color with marker) before being embedded in a multi-line summary string; logger.success() would drop the color. */ /** * @fileoverview Stripped Binary Build Phase @@ -267,6 +266,7 @@ export async function buildStripped(config, buildOptions = {}) { if (unit === 'M' && size >= 20 && size <= 30) { logger.log( + // oxlint-disable-next-line socket/no-status-emoji -- emoji is wrapped in colors.green() decorator (composes color with marker) before being embedded in a multi-line summary string; logger.success() would drop the color. `${colors.green('✓')} Binary size is optimal (20-30MB with V8 Lite Mode)`, ) } else if (unit === 'M' && size < 20) { diff --git a/packages/node-smol-builder/scripts/common/shared/compression-tools.mts b/packages/node-smol-builder/scripts/common/shared/compression-tools.mts index e59459166..ddce51725 100644 --- a/packages/node-smol-builder/scripts/common/shared/compression-tools.mts +++ b/packages/node-smol-builder/scripts/common/shared/compression-tools.mts @@ -17,7 +17,7 @@ import { logTransientErrorHelp } from 'build-infra/lib/github-error-utils' import { getDownloadedDir, getFinalBinaryPath } from 'build-infra/lib/paths' import { getPlatformArch } from 'build-infra/lib/platform-mappings' -import { envAsBoolean } from '@socketsecurity/lib-stable/env' +import { envAsBoolean } from '@socketsecurity/lib-stable/env/boolean' import { getDefaultLogger } from '@socketsecurity/lib-stable/logger' import { detectLibc, diff --git a/packages/node-smol-builder/scripts/generate-entities-data.mts b/packages/node-smol-builder/scripts/generate-entities-data.mts new file mode 100644 index 000000000..9780e3177 --- /dev/null +++ b/packages/node-smol-builder/scripts/generate-entities-data.mts @@ -0,0 +1,144 @@ +#!/usr/bin/env node +/** + * Generate src/socketsecurity/util/entities_data.cc from the + * canonical WHATWG named character reference table. + * + * Source of truth: https://html.spec.whatwg.org/entities.json + * + * The output is a single C++ translation unit holding three flat + * constexpr arrays: + * + * - kNamePool (uint8_t[]) — concatenated entity names, sans the + * leading '&'. UTF-8 (== ASCII for every + * named reference). + * - kValuePool (uint8_t[]) — concatenated UTF-8 codepoints each name + * expands to (most are 1-2 codepoints, a + * handful expand to two; max 6 UTF-8 + * bytes per entry). + * - kEntities (EntityMeta[]) — (name_off, name_len, value_off, + * value_len) tuples. Sorted by name + * for O(log n) binary-search decode. + * + * Re-run when the WHATWG table changes. The output is tracked in git; + * runtime fetches are not part of the build. + */ +import { readFileSync, writeFileSync } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +import { httpJson } from '@socketsecurity/lib-stable/http-request' +import { getDefaultLogger } from '@socketsecurity/lib-stable/logger' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const OUTPUT = path.resolve( + __dirname, + '..', + 'additions', + 'source-patched', + 'src', + 'socketsecurity', + 'util', + 'entities_data.cc', +) +const SOURCE_URL = 'https://html.spec.whatwg.org/entities.json' + +const logger = getDefaultLogger() + +async function main() { + logger.info(`Fetching ${SOURCE_URL}`) + const response = await httpJson>( + SOURCE_URL, + ) + if (!response.ok) { + throw new Error( + `Failed to fetch entities.json: ${response.statusCode} ${response.statusText}`, + ) + } + const j = response.data + + const entries = Object.entries(j) + .map(([k, v]) => ({ name: k.slice(1), chars: v.characters })) + .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) + + const nameBuf: number[] = [] + const valBuf: number[] = [] + const meta: Array<{ + nameOff: number + nameLen: number + valOff: number + valLen: number + }> = [] + + const appendUtf8 = (buf: number[], s: string) => { + const off = buf.length + for (const b of Buffer.from(s, 'utf8')) { + buf.push(b) + } + return [off, buf.length - off] as const + } + + for (let i = 0, { length } = entries; i < length; i += 1) { + const e = entries[i] + const [nameOff, nameLen] = appendUtf8(nameBuf, e.name) + const [valOff, valLen] = appendUtf8(valBuf, e.chars) + meta.push({ nameOff, nameLen, valOff, valLen }) + } + + const bytesToCpp = (buf: number[], name: string) => { + let out = `extern const uint8_t ${name}[];\n` + out += `const uint8_t ${name}[] = {` + for (let i = 0, { length } = buf; i < length; i += 1) { + if (i % 16 === 0) { + out += '\n ' + } + out += `${buf[i]},` + } + out += '\n};\n' + return out + } + + let out = '' + out += '// Auto-generated from https://html.spec.whatwg.org/entities.json\n' + out += '// (WHATWG HTML Living Standard named character references).\n' + out += '// Do not hand-edit; regenerate via scripts/generate-entities-data.mts.\n' + out += '//\n' + out += `// ${entries.length} entries. Sorted by name for binary search.\n` + out += '\n' + out += '#include \n' + out += '#include \n\n' + out += 'namespace node {\n' + out += 'namespace socketsecurity {\n' + out += 'namespace util {\n' + out += 'namespace entities {\n\n' + out += bytesToCpp(nameBuf, 'kNamePool') + out += '\n' + out += bytesToCpp(valBuf, 'kValuePool') + out += '\n' + out += 'struct EntityMeta {\n' + out += ' uint16_t name_off; // offset into kNamePool\n' + out += ' uint16_t name_len; // length in bytes (UTF-8 == ASCII for entity names)\n' + out += ' uint16_t value_off; // offset into kValuePool\n' + out += ' uint8_t value_len; // length in bytes (UTF-8)\n' + out += '};\n\n' + out += 'extern const size_t kEntityCount;\n' + out += `const size_t kEntityCount = ${entries.length};\n\n` + out += `extern const EntityMeta kEntities[${entries.length}];\n` + out += `const EntityMeta kEntities[${entries.length}] = {\n` + for (let i = 0, { length } = meta; i < length; i += 1) { + const e = meta[i] + out += ` {${e.nameOff},${e.nameLen},${e.valOff},${e.valLen}},\n` + } + out += '};\n\n' + out += '} // namespace entities\n' + out += '} // namespace util\n' + out += '} // namespace socketsecurity\n' + out += '} // namespace node\n' + + writeFileSync(OUTPUT, out) + logger.success(`Wrote ${OUTPUT} (${entries.length} entries)`) +} + +main().catch(err => { + logger.fail(`Failed: ${err}`) + process.exitCode = 1 +}) diff --git a/packages/node-smol-builder/scripts/generate-width-data.mts b/packages/node-smol-builder/scripts/generate-width-data.mts new file mode 100644 index 000000000..d221df472 --- /dev/null +++ b/packages/node-smol-builder/scripts/generate-width-data.mts @@ -0,0 +1,203 @@ +#!/usr/bin/env node +/** + * Generate src/socketsecurity/tui/width_data.cc from Unicode 17.0.0 + * East_Asian_Width + emoji-data data files. + * + * Two output tables: + * - kWideRanges: sorted [lo, hi] inclusive pairs of code-point ranges + * where width = 2 (East Asian F/W *or* Emoji_Presentation). Used by + * the StringWidth() fast path. + * - kZeroWidthRanges: sorted [lo, hi] inclusive pairs where width = 0 + * (combining marks + default-ignorable + control chars). Used to + * adjust the width down for ZWJ-style sequences in the common case + * (combining marks attached to a base character). + * + * Width returned by StringWidth: + * 1 + Σ over codepoints in the string: + * +1 if codepoint is wide (lookup in kWideRanges) + * -1 if codepoint is zero-width (lookup in kZeroWidthRanges) + * 0 otherwise + * ... minus 1 (the seeding "1" cancels out for empty strings). + * + * Re-run when Unicode bumps to a new major version. + */ +import { writeFileSync } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +import { httpText } from '@socketsecurity/lib-stable/http-request' +import { getDefaultLogger } from '@socketsecurity/lib-stable/logger' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const OUTPUT = path.resolve( + __dirname, + '..', + '..', + 'tui-infra', + 'src', + 'socketsecurity', + 'tui', + 'width_data.cc', +) + +// Unicode 17.0.0. Fleet-wide alignment: ultrathink's acorn parser +// tracks 17.0 across Go / C++ (ICU 78.2) / Rust (unicode-id-start +// 1.4.0) / TS (@unicode/unicode-17.0.0). Keep in lockstep. +const UNICODE_VERSION = '17.0.0' +const EAW_URL = `https://www.unicode.org/Public/${UNICODE_VERSION}/ucd/EastAsianWidth.txt` +const EMOJI_URL = `https://www.unicode.org/Public/${UNICODE_VERSION}/ucd/emoji/emoji-data.txt` + +const logger = getDefaultLogger() + +type Range = [number, number] // [lo, hi] inclusive + +export async function fetchText(url: string): Promise { + logger.info(`Fetching ${url}`) + const response = await httpText(url) + if (!response.ok) { + throw new Error( + `Failed to fetch ${url}: ${response.statusCode} ${response.statusText}`, + ) + } + return response.data +} + +export function mergeRanges(input: Range[]): Range[] { + if (input.length === 0) { + return [] + } + const sorted = [...input].sort((a, b) => a[0] - b[0]) + const merged: Range[] = [sorted[0]] + for (let i = 1, { length } = sorted; i < length; i += 1) { + const [lo, hi] = sorted[i] + const last = merged[merged.length - 1] + if (lo <= last[1] + 1) { + if (hi > last[1]) { + last[1] = hi + } + } else { + merged.push([lo, hi]) + } + } + return merged +} + +export function parseUcd( + text: string, + predicate: (property: string) => boolean, +): Range[] { + const out: Range[] = [] + const lines = text.split('\n') + for (let i = 0, { length } = lines; i < length; i += 1) { + const raw = lines[i] + const hashIdx = raw.indexOf('#') + const line = (hashIdx >= 0 ? raw.slice(0, hashIdx) : raw).trim() + if (!line) { + continue + } + const semiIdx = line.indexOf(';') + if (semiIdx < 0) { + continue + } + const rangePart = line.slice(0, semiIdx).trim() + const propertyPart = line.slice(semiIdx + 1).trim() + if (!predicate(propertyPart)) { + continue + } + const dotIdx = rangePart.indexOf('..') + let lo: number + let hi: number + if (dotIdx >= 0) { + lo = parseInt(rangePart.slice(0, dotIdx), 16) + hi = parseInt(rangePart.slice(dotIdx + 2), 16) + } else { + lo = parseInt(rangePart, 16) + hi = lo + } + out.push([lo, hi]) + } + return out +} + +async function main() { + const eawText = await fetchText(EAW_URL) + const emojiText = await fetchText(EMOJI_URL) + + // Wide: EAW F/W ∪ Emoji_Presentation. + const wideEaw = parseUcd(eawText, p => p === 'F' || p === 'W') + const wideEmoji = parseUcd(emojiText, p => p === 'Emoji_Presentation') + const wide = mergeRanges([...wideEaw, ...wideEmoji]) + + // Zero-width approximation: control chars + combining marks + + // default-ignorable. We don't have the full DerivedGeneralCategory + // here, so approximate by: + // - C0 + C1 controls (0x00-0x1F, 0x7F-0x9F) + // - Zero-width joiner / non-joiner (U+200C, U+200D) + // - Combining diacritical marks (U+0300-U+036F) + // - Variation selectors (U+FE00-U+FE0F, U+E0100-U+E01EF) + // - U+00AD (soft hyphen, default-ignorable) + // - U+061C, U+180E, U+200B, U+200E-U+200F, U+202A-U+202E, + // U+2060-U+206F (default-ignorable bidi + format chars) + // - U+FFF9-U+FFFB (interlinear annotation) + // - Tags block (U+E0000-U+E007F) + // + // This is the "covers 95% of zero-width in practice" set. The full + // DerivedGeneralCategory pass is a future tightening. + const zeroWidth: Range[] = mergeRanges([ + [0x0000, 0x001f], + [0x007f, 0x009f], + [0x00ad, 0x00ad], + [0x0300, 0x036f], + [0x061c, 0x061c], + [0x180e, 0x180e], + [0x200b, 0x200f], + [0x202a, 0x202e], + [0x2060, 0x206f], + [0xfe00, 0xfe0f], + [0xfff9, 0xfffb], + [0xe0000, 0xe007f], + [0xe0100, 0xe01ef], + ]) + + const rangesToCpp = (ranges: Range[], name: string) => { + let out = `extern const uint32_t ${name}[][2];\n` + out += `const uint32_t ${name}[][2] = {\n` + for (let i = 0, { length } = ranges; i < length; i += 1) { + const [lo, hi] = ranges[i] + out += ` {0x${lo.toString(16)}, 0x${hi.toString(16)}},\n` + } + out += '};\n' + return out + } + + let out = '' + out += '// Auto-generated from Unicode 16.0.0 EastAsianWidth.txt +\n' + out += '// emoji-data.txt. Do not hand-edit; regenerate via\n' + out += '// scripts/generate-width-data.mts.\n' + out += '//\n' + out += `// kWideRanges: ${wide.length} ranges (width = 2)\n` + out += `// kZeroWidthRanges: ${zeroWidth.length} ranges (width = 0)\n` + out += '\n' + out += '#include \n' + out += '#include \n\n' + out += 'namespace tui {\n\n' + out += rangesToCpp(wide, 'kWideRanges') + out += '\n' + out += rangesToCpp(zeroWidth, 'kZeroWidthRanges') + out += '\n' + out += 'extern const size_t kWideRangesCount;\n' + out += `const size_t kWideRangesCount = ${wide.length};\n\n` + out += 'extern const size_t kZeroWidthRangesCount;\n' + out += `const size_t kZeroWidthRangesCount = ${zeroWidth.length};\n\n` + out += '} // namespace tui\n' + + writeFileSync(OUTPUT, out) + logger.success( + `Wrote ${OUTPUT} (${wide.length} wide + ${zeroWidth.length} zero-width ranges)`, + ) +} + +main().catch(err => { + logger.fail(`Failed: ${err}`) + process.exitCode = 1 +}) diff --git a/packages/node-smol-builder/scripts/source-patched/shared/apply-patches.mts b/packages/node-smol-builder/scripts/source-patched/shared/apply-patches.mts index 3a0df9cd0..459cea1e3 100644 --- a/packages/node-smol-builder/scripts/source-patched/shared/apply-patches.mts +++ b/packages/node-smol-builder/scripts/source-patched/shared/apply-patches.mts @@ -16,7 +16,10 @@ import { errorMessage } from 'build-infra/lib/error-utils' import { glob } from '@socketsecurity/lib-stable/globs/stream' import { getDefaultLogger } from '@socketsecurity/lib-stable/logger' -import { MONOREPO_PACKAGE_SOURCES } from '../../binary-released/shared/prepare-external-sources.mts' +import { + EXTERNAL_PIN_FILES, + MONOREPO_PACKAGE_SOURCES, +} from '../../binary-released/shared/prepare-external-sources.mts' const logger = getDefaultLogger() @@ -251,7 +254,19 @@ export async function computeSourcePatchedCachePaths(options: { } } - return [...patchChainPaths, ...sourcePackageFiles] + // External pin files — single files (like .gitmodules + lockstep.json) + // whose content captures the version of external deps linked at build + // time but whose source isn't copied into the patched tree. See + // EXTERNAL_PIN_FILES in prepare-external-sources.mts for the list. + const externalPinFiles: string[] = [] + for (let i = 0, { length } = EXTERNAL_PIN_FILES; i < length; i += 1) { + const pinFile = EXTERNAL_PIN_FILES[i]! + if (existsSync(pinFile)) { + externalPinFiles.push(pinFile) + } + } + + return [...patchChainPaths, ...sourcePackageFiles, ...externalPinFiles] } /** diff --git a/packages/node-smol-builder/scripts/test-with-memory-limit.mts b/packages/node-smol-builder/scripts/test-with-memory-limit.mts index f915c8d77..6a7465792 100755 --- a/packages/node-smol-builder/scripts/test-with-memory-limit.mts +++ b/packages/node-smol-builder/scripts/test-with-memory-limit.mts @@ -159,7 +159,9 @@ vitestPromise .then(result => { clearInterval(monitorInterval) if (!killed) { - logger.log(`\n\nTest completed with exit code: ${result.code}`) + logger.log('') + logger.log('') + logger.log(`Test completed with exit code: ${result.code}`) process.exitCode = result.code || 0 } }) diff --git a/packages/node-smol-builder/scripts/vendor-fast-webstreams/sync.mts b/packages/node-smol-builder/scripts/vendor-fast-webstreams/sync.mts index da10cf3f3..5c27b036d 100644 --- a/packages/node-smol-builder/scripts/vendor-fast-webstreams/sync.mts +++ b/packages/node-smol-builder/scripts/vendor-fast-webstreams/sync.mts @@ -61,7 +61,7 @@ export function addPrimordialsProtection(content, filename) { const usesPromiseAll = /Promise\.all\s*\(/g.test(content) const needsPrimordials = - usesPromiseResolve || usesPromiseReject || usesNewPromise || usesPromiseAll + usesNewPromise || usesPromiseAll || usesPromiseReject || usesPromiseResolve if (!needsPrimordials) { return content diff --git a/packages/node-smol-builder/scripts/vendor-fast-webstreams/validate.mts b/packages/node-smol-builder/scripts/vendor-fast-webstreams/validate.mts index 943086c1b..92072fd6c 100644 --- a/packages/node-smol-builder/scripts/vendor-fast-webstreams/validate.mts +++ b/packages/node-smol-builder/scripts/vendor-fast-webstreams/validate.mts @@ -1,5 +1,4 @@ #!/usr/bin/env node -/* oxlint-disable socket/no-status-emoji -- vendored upstream validator; emits pass/fail markers via direct stdout writes (matches upstream WPT test reporter format) with no logger import. */ /** * Validate fast-webstreams integration in built Node.js binary @@ -39,6 +38,7 @@ const DEFAULT_BINARY = path.join( // Test code to run in the built binary // Tests are based on experimental-fast-webstreams test suite +// oxlint-disable-next-line socket/no-status-emoji -- vendored upstream validator; emits pass/fail markers via direct stdout writes (matches upstream WPT test reporter format) with no logger import. const TEST_CODE = ` 'use strict'; diff --git a/packages/node-smol-builder/scripts/vendor-fast-webstreams/wpt/validate.mts b/packages/node-smol-builder/scripts/vendor-fast-webstreams/wpt/validate.mts index a732f4e50..264d68d95 100644 --- a/packages/node-smol-builder/scripts/vendor-fast-webstreams/wpt/validate.mts +++ b/packages/node-smol-builder/scripts/vendor-fast-webstreams/wpt/validate.mts @@ -1,6 +1,5 @@ #!/usr/bin/env node // max-file-lines: legitimate -- orchestration script — top-down pipeline (gather → validate → report); splitting fractures the flow -/* oxlint-disable socket/no-status-emoji -- WPT validator emits ANSI-colored status markers ("\x1b[32m✓\x1b[0m" etc.) in column-aligned table rows; logger.success() would lose the colorization required by the WPT result format. */ /** * WPT (Web Platform Tests) validation for fast-webstreams integration @@ -560,6 +559,7 @@ async function main(): Promise { // Status: green checkmark, yellow tilde (expected fail), red X (unexpected fail) let status if (result.failed === 0) { + // oxlint-disable-next-line socket/no-status-emoji -- WPT validator emits ANSI-colored status markers ("\x1b[32m✓\x1b[0m" etc.) in column-aligned table rows; logger.success() would lose the colorization required by the WPT result format. status = '\x1b[32m✓\x1b[0m' } else { // Check if all failures in this file are expected @@ -569,6 +569,7 @@ async function main(): Promise { const fullKey = `${fileKey}:${testName}` return EXPECTED_FAILURES.has(fullKey) || EXPECTED_FAILURES.has(fileKey) }) + // oxlint-disable-next-line socket/no-status-emoji -- WPT validator emits ANSI-colored status markers ("\x1b[32m✓\x1b[0m" etc.) in column-aligned table rows; logger.success() would lose the colorization required by the WPT result format. status = allExpected ? '\x1b[33m~\x1b[0m' : '\x1b[31m✗\x1b[0m' } @@ -633,6 +634,7 @@ async function main(): Promise { if (unexpectedFailures.length > 0) { logger.error('') + // oxlint-disable-next-line socket/no-status-emoji -- WPT validator emits ANSI-colored status markers ("\x1b[32m✓\x1b[0m" etc.) in column-aligned table rows; logger.success() would lose the colorization required by the WPT result format. logger.fail('❌ UNEXPECTED failures (regressions):') // oxlint-disable-next-line socket/prefer-cached-for-loop -- loop variable is destructured for (const { file, test } of unexpectedFailures.slice(0, 10)) { diff --git a/packages/node-smol-builder/test/e2e/e2e.test.mts b/packages/node-smol-builder/test/e2e/e2e.test.mts index 0c4946baa..170c21958 100644 --- a/packages/node-smol-builder/test/e2e/e2e.test.mts +++ b/packages/node-smol-builder/test/e2e/e2e.test.mts @@ -38,7 +38,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)) const finalBinaryPath = getLatestFinalBinary() const packageDir = getPackageDir() const testTmpDir = path.join(os.tmpdir(), 'socket-btm-e2e-tests') -const _DLX_DIR = getSocketDlxDir() +const DLX_DIR = getSocketDlxDir() // Skip all tests if no final binary is available const skipTests = !finalBinaryPath || !existsSync(finalBinaryPath) diff --git a/packages/node-smol-builder/test/helpers/binject.mts b/packages/node-smol-builder/test/helpers/binject.mts index a91cc12f2..41622e2a6 100644 --- a/packages/node-smol-builder/test/helpers/binject.mts +++ b/packages/node-smol-builder/test/helpers/binject.mts @@ -21,11 +21,11 @@ import { } from 'bin-infra/test/helpers/segment-names' const __dirname = path.dirname(fileURLToPath(import.meta.url)) -const _packageDir = path.resolve(__dirname, '..', '..') +const packageDir = path.resolve(__dirname, '..', '..') const BUILD_MODE = getBuildMode() const PLATFORM_ARCH = getPlatformArch(process.platform, process.arch, undefined) -const BINJECT_DIR = path.join(_packageDir, '..', 'binject') +const BINJECT_DIR = path.join(packageDir, '..', 'binject') // Re-export VFS resource names for convenience export { diff --git a/packages/node-smol-builder/test/helpers/smol-builtin.mts b/packages/node-smol-builder/test/helpers/smol-builtin.mts index d8dc4ee45..b0d8832aa 100644 --- a/packages/node-smol-builder/test/helpers/smol-builtin.mts +++ b/packages/node-smol-builder/test/helpers/smol-builtin.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/sort-source-methods -- helpers grouped by builtin probe (probe → assert → describe); alphabetizing would split each builtin's helper triplet. */ /** * @fileoverview Shared helpers for probing `node:smol-*` builtins on * the built smol binary. Every `node:smol-*` integration test uses the @@ -81,6 +80,7 @@ export interface RunResult { * Throws if no Final/ binary exists — call sites should gate the * whole suite via `smolBuiltinIsAvailable()` first. */ +// oxlint-disable-next-line socket/sort-source-methods -- helpers grouped by builtin probe (probe → assert → describe); alphabetizing would split each builtin's helper triplet. export async function runOnSmolBinary( script: string, options: RunOptions = {}, @@ -111,6 +111,7 @@ export async function runOnSmolBinary( * * Output is one line per export: `export:=`. */ +// oxlint-disable-next-line socket/sort-source-methods -- helpers grouped by builtin probe (probe → assert → describe); alphabetizing would split each builtin's helper triplet. export function printExportShapeScript(name: string): string { return ` const mod = require('node:${name}') @@ -130,6 +131,7 @@ export function printExportShapeScript(name: string): string { * Parse the output of `printExportShapeScript` into a Map of * export name → typeof string. */ +// oxlint-disable-next-line socket/sort-source-methods -- helpers grouped by builtin probe (probe → assert → describe); alphabetizing would split each builtin's helper triplet. export function parseExportShape(stdout: string): Map { const shape = new Map() // oxlint-disable-next-line socket/prefer-cached-for-loop -- iterable is not a bare identifier (could be Map/Set/Generator/expression) diff --git a/packages/node-smol-builder/test/integration/build-sea-smol-options.test.mts b/packages/node-smol-builder/test/integration/build-sea-smol-options.test.mts index 1c944feda..8692338b9 100644 --- a/packages/node-smol-builder/test/integration/build-sea-smol-options.test.mts +++ b/packages/node-smol-builder/test/integration/build-sea-smol-options.test.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/prefer-exists-sync -- fs.stat() calls consume stats.size to compare source vs. output executable sizes. */ /** * @fileoverview Integration tests for node --build-sea with smol options. * @@ -88,7 +87,9 @@ describe.skipIf(skipTests)('--build-sea with smol options', () => { expect(existsSync(outputExe)).toBeTruthy() // Verify it's different from source (has SEA blob injected) + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size to compare source vs. output executable sizes. const sourceSize = (await fs.stat(sourceExe)).size + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size to compare source vs. output executable sizes. const outputSize = (await fs.stat(outputExe)).size expect(outputSize).toBeGreaterThan(sourceSize) }) diff --git a/packages/node-smol-builder/test/integration/cross-package-integration.test.mts b/packages/node-smol-builder/test/integration/cross-package-integration.test.mts index 5fab77fc1..18cb3b791 100644 --- a/packages/node-smol-builder/test/integration/cross-package-integration.test.mts +++ b/packages/node-smol-builder/test/integration/cross-package-integration.test.mts @@ -1,5 +1,3 @@ -/* oxlint-disable socket/sort-source-methods -- test helpers are ordered by the cross-package flow they exercise; alphabetizing would scatter them across the file. */ -/* oxlint-disable socket/prefer-exists-sync -- fs.stat() calls consume stats.size to compare compressed/final artifact sizes through the repack flow. */ /** * @fileoverview Cross-package integration tests @@ -84,6 +82,7 @@ let allBinariesExist = false /** * Execute command. */ +// oxlint-disable-next-line socket/sort-source-methods -- test helpers are ordered by the cross-package flow they exercise; alphabetizing would scatter them across the file. export async function execCommand(command, args = [], options = {}) { const result = await spawn(command, args, { ...options, @@ -102,7 +101,7 @@ beforeAll(async () => { const binjectExists = existsSync(BINJECT) const nodeExists = NODE_BINARY && existsSync(NODE_BINARY) - allBinariesExist = binpressExists && binjectExists && nodeExists + allBinariesExist = binjectExists && binpressExists && nodeExists if (!allBinariesExist) { logger.warn('Missing required binaries:') @@ -316,6 +315,7 @@ describe.skipIf(!allBinariesExist)('cross-package integration', () => { }) await makeExecutable(compressed) + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size to compare compressed/final artifact sizes through the repack flow. const compressedSize = (await fs.stat(compressed)).size // Inject @@ -334,6 +334,7 @@ describe.skipIf(!allBinariesExist)('cross-package integration', () => { seaBlob, ]) + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size to compare compressed/final artifact sizes through the repack flow. const finalSize = (await fs.stat(final)).size // Final should be roughly compressed size + blob size (with some overhead) diff --git a/packages/node-smol-builder/test/integration/cross-platform-decompress.test.mts b/packages/node-smol-builder/test/integration/cross-platform-decompress.test.mts index 4eb00cfa1..2a22ab281 100644 --- a/packages/node-smol-builder/test/integration/cross-platform-decompress.test.mts +++ b/packages/node-smol-builder/test/integration/cross-platform-decompress.test.mts @@ -27,7 +27,7 @@ describe.skipIf(skipTests)('cross-Platform Decompression', () => { let binaryPath: string let binaryData: Buffer let platformByte: number - let _archByte: number + let archByte: number beforeAll(async () => { binaryPath = getLatestFinalBinary() @@ -50,7 +50,7 @@ describe.skipIf(skipTests)('cross-Platform Decompression', () => { HEADER_SIZES.CACHE_KEY platformByte = binaryData[metadataOffset] - _archByte = binaryData[metadataOffset + 1] + archByte = binaryData[metadataOffset + 1] }) describe('universal zstd compression', () => { @@ -212,7 +212,7 @@ describe.skipIf(skipTests)('cross-Platform Decompression', () => { const markerIndex = binaryData.indexOf(Buffer.from(MAGIC_MARKER, 'utf8')) const dataOffset = markerIndex + 67 - const _compressedData = binaryData.subarray(dataOffset) + const compressedData = binaryData.subarray(dataOffset) // We can verify the cache key matches the data by checking it's consistent const cacheKeyOffset = markerIndex + 48 diff --git a/packages/node-smol-builder/test/integration/cross-tool-repack.test.mts b/packages/node-smol-builder/test/integration/cross-tool-repack.test.mts index fc91fb120..7aff917f8 100644 --- a/packages/node-smol-builder/test/integration/cross-tool-repack.test.mts +++ b/packages/node-smol-builder/test/integration/cross-tool-repack.test.mts @@ -1,5 +1,3 @@ -/* oxlint-disable socket/sort-source-methods -- test helpers are ordered by the repack flow they exercise; alphabetizing would scatter them across the file. */ -/* oxlint-disable socket/prefer-exists-sync -- fs.stat() calls consume stats.size to compare compressed/batched/updated artifact sizes through the repack flow. */ /** * @fileoverview Cross-tool repacking integration tests @@ -82,6 +80,7 @@ let allBinariesExist = false /** * Execute command. */ +// oxlint-disable-next-line socket/sort-source-methods -- test helpers are ordered by the repack flow they exercise; alphabetizing would scatter them across the file. export async function execCommand(command, args = [], options = {}) { const result = await spawn(command, args, { ...options, @@ -100,7 +99,7 @@ beforeAll(async () => { const binjectExists = existsSync(BINJECT) const nodeExists = NODE_BINARY && existsSync(NODE_BINARY) - allBinariesExist = binpressExists && binjectExists && nodeExists + allBinariesExist = binjectExists && binpressExists && nodeExists if (!allBinariesExist) { logger.warn('Missing required binaries:') @@ -329,6 +328,7 @@ describe.skipIf(!allBinariesExist)('cross-tool repacking', () => { await execCommand(BINPRESS, [NODE_BINARY, '-o', compressed], { timeout: 120_000, }) + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size to compare compressed/batched/updated artifact sizes through the repack flow. const compressedSize = (await fs.stat(compressed)).size // Inject small batch (SEA + VFS) @@ -350,6 +350,7 @@ describe.skipIf(!allBinariesExist)('cross-tool repacking', () => { '--vfs', vfsArchive, ]) + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size to compare compressed/batched/updated artifact sizes through the repack flow. const withBatchSize = (await fs.stat(withBatch)).size // Update compression @@ -357,6 +358,7 @@ describe.skipIf(!allBinariesExist)('cross-tool repacking', () => { await execCommand(BINPRESS, [withBatch, '-o', updated], { timeout: 120_000, }) + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size to compare compressed/batched/updated artifact sizes through the repack flow. const updatedSize = (await fs.stat(updated)).size // Size checks diff --git a/packages/node-smol-builder/test/integration/linux-x64-docker.test.mts b/packages/node-smol-builder/test/integration/linux-x64-docker.test.mts index 611f774b0..5afa1a50f 100644 --- a/packages/node-smol-builder/test/integration/linux-x64-docker.test.mts +++ b/packages/node-smol-builder/test/integration/linux-x64-docker.test.mts @@ -1,6 +1,4 @@ // max-file-lines: legitimate -- integration test — one end-to-end scenario per file, splitting fractures the assertion narrative -/* oxlint-disable socket/prefer-exists-sync -- fs.stat() calls consume stats.size and stats.mode to verify extracted/final Linux x64 binaries. */ -/* oxlint-disable socket/no-status-emoji -- emoji literals are embedded in test fixture JS source executed inside the SEA binary and asserted via toContain(); the runtime can't call logger.success() because it runs without our logger import. */ /** * @fileoverview Integration tests for linux-x64 Docker build validation. @@ -42,7 +40,7 @@ const isLinux = os.platform() === 'linux' const skipTests = !isLinux || !finalBinaryPath || !existsSync(finalBinaryPath) const testTmpDir = path.join(os.tmpdir(), 'socket-btm-linux-x64-docker-tests') -const _DLX_DIR = getSocketDlxDir() +const DLX_DIR = getSocketDlxDir() /** * Calculate the content hash for a file (matches node-smol extraction logic). @@ -76,11 +74,12 @@ describe.skipIf(skipTests)('linux-x64 Docker build integration', () => { // Check if extraction occurred (only for compressed binaries) // Compressed binaries extract to ~/.socket/_dlx//node const hash = await calculateFileHash(finalBinaryPath) - const extractedNodePath = path.join(_DLX_DIR, hash, 'node') + const extractedNodePath = path.join(DLX_DIR, hash, 'node') // If the binary is compressed, it should extract to the cache directory // If it's uncompressed (dev builds), the cache may not exist if (existsSync(extractedNodePath)) { + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size and stats.mode to verify extracted/final Linux x64 binaries. const extractedStat = await fs.stat(extractedNodePath) expect(extractedStat.isFile()).toBeTruthy() // Executable bit @@ -205,6 +204,7 @@ console.log('Is SEA:', require('node:sea').isSea()); const appJs = path.join(testDir, 'app.js') await fs.writeFile( appJs, + // oxlint-disable-next-line socket/no-status-emoji -- emoji literals are embedded in test fixture JS source executed inside the SEA binary and asserted via toContain(); the runtime can't call logger.success() because it runs without our logger import. `#!/usr/bin/env node const fs = require('fs'); const path = require('path'); @@ -299,6 +299,7 @@ console.log('✓ Test completed successfully'); expect(execResult.stdout).toContain('Is SEA: true') expect(execResult.stdout).toContain('VFS available: true') expect(execResult.stdout).toContain('Package name: test-sea-vfs-app') + // oxlint-disable-next-line socket/no-status-emoji -- emoji literals are embedded in test fixture JS source executed inside the SEA binary and asserted via toContain(); the runtime can't call logger.success() because it runs without our logger import. expect(execResult.stdout).toContain('✓ Test completed successfully') }, ) @@ -486,6 +487,7 @@ console.log('Node version:', process.version); }) it('should be executable', async () => { + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size and stats.mode to verify extracted/final Linux x64 binaries. const stat = await fs.stat(finalBinaryPath) // Check executable bit @@ -493,6 +495,7 @@ console.log('Node version:', process.version); }) it('should have reasonable size', async () => { + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size and stats.mode to verify extracted/final Linux x64 binaries. const stat = await fs.stat(finalBinaryPath) // Node.js binaries should be between 10MB and 200MB // > 10MB diff --git a/packages/node-smol-builder/test/integration/metadata-format.test.mts b/packages/node-smol-builder/test/integration/metadata-format.test.mts index 90330cc8a..b881b9454 100644 --- a/packages/node-smol-builder/test/integration/metadata-format.test.mts +++ b/packages/node-smol-builder/test/integration/metadata-format.test.mts @@ -111,7 +111,7 @@ describe.skipIf(skipTests)('metadata Format Validation', () => { it('should not have compression_algorithm byte (old format)', async () => { const markerIndex = binaryData.indexOf(MAGIC_MARKER) - const _metadataOffset = markerIndex + 64 + const metadataOffset = markerIndex + 64 // In old format, compression_algorithm would be at offset + 3 // In new format, compressed data starts at offset + 3 diff --git a/packages/node-smol-builder/test/integration/stub-signing-extraction.test.mts b/packages/node-smol-builder/test/integration/stub-signing-extraction.test.mts index 24b6a5747..b26ff3de5 100644 --- a/packages/node-smol-builder/test/integration/stub-signing-extraction.test.mts +++ b/packages/node-smol-builder/test/integration/stub-signing-extraction.test.mts @@ -1,5 +1,4 @@ // max-file-lines: legitimate -- integration test — one end-to-end scenario per file, splitting fractures the assertion narrative -/* oxlint-disable socket/prefer-exists-sync -- fs.stat() calls consume stats.size and stats.mtime to verify extracted artifacts and detect metadata-rewrite races. */ /** * @fileoverview Integration tests for stub signing, extraction, and execution flow * @@ -404,6 +403,7 @@ describe.skipIf(skipTests)('stub signing and extraction flow', () => { throw new Error('extractedNodePath not set') } + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size and stats.mtime to verify extracted artifacts and detect metadata-rewrite races. const stats = await fs.stat(extractedNodePath) expect(stats.mode & 0o100).not.toBe(0) }) @@ -679,11 +679,13 @@ console.log('UTF-8 string:', str); it('should not recreate cache directory', async () => { const metadataPath = path.join(testCacheDir, '.dlx-metadata.json') + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size and stats.mtime to verify extracted artifacts and detect metadata-rewrite races. const statsBefore = await fs.stat(metadataPath) // Run again await spawn(stubBinaryPath, ['--version'], { timeout: 5000 }) + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size and stats.mtime to verify extracted artifacts and detect metadata-rewrite races. const statsAfter = await fs.stat(metadataPath) // Metadata file should not be modified (cache hit) @@ -780,6 +782,7 @@ console.log('UTF-8 string:', str); it.skipIf(!IS_MACOS)( 'should be executable after signing (macOS)', async () => { + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size and stats.mtime to verify extracted artifacts and detect metadata-rewrite races. const stats = await fs.stat(stubBinaryPath) expect(stats.mode & 0o100).not.toBe(0) }, diff --git a/packages/node-smol-builder/test/integration/vfs.test.mts b/packages/node-smol-builder/test/integration/vfs.test.mts index 23f6bc0bb..faddd2b21 100644 --- a/packages/node-smol-builder/test/integration/vfs.test.mts +++ b/packages/node-smol-builder/test/integration/vfs.test.mts @@ -1,5 +1,4 @@ // max-file-lines: legitimate -- integration test — one end-to-end scenario per file, splitting fractures the assertion narrative -/* oxlint-disable socket/prefer-exists-sync -- file tests the VFS surface itself; fs.stat()/fs.access()/fs.statSync() calls verify VFS metadata fidelity (size/mode) AND appear inside test-fixture JS source executed by the SEA binary. */ /** * @fileoverview Tests for VFS (Virtual Filesystem) support with TAR/TAR.GZ archives. * @@ -168,8 +167,10 @@ describe.sequential.skipIf(skipTests)( // Verify compression (should be smaller) const tarSize = + // oxlint-disable-next-line socket/prefer-exists-sync -- file tests the VFS surface itself; fs.stat()/fs.access()/fs.statSync() calls verify VFS metadata fidelity (size/mode) AND appear inside test-fixture JS source executed by the SEA binary. (await fs.stat(path.join(testDir, '../tar-uncompressed/vfs.tar'))) .size || 10_000 + // oxlint-disable-next-line socket/prefer-exists-sync -- file tests the VFS surface itself; fs.stat()/fs.access()/fs.statSync() calls verify VFS metadata fidelity (size/mode) AND appear inside test-fixture JS source executed by the SEA binary. const tarGzSize = (await fs.stat(tarGzPath)).size expect(tarGzSize).toBeLessThan(tarSize) }) diff --git a/packages/node-smol-builder/test/smol-manifest-native.test.mts b/packages/node-smol-builder/test/smol-manifest-native.test.mts index 38a6de7d2..b3cb06353 100644 --- a/packages/node-smol-builder/test/smol-manifest-native.test.mts +++ b/packages/node-smol-builder/test/smol-manifest-native.test.mts @@ -20,12 +20,11 @@ * `pnpm build`), skip the suite with a clear message. */ -import { execFileSync } from 'node:child_process' -import type { ExecFileSyncOptionsWithStringEncoding } from 'node:child_process' import { existsSync, readdirSync } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' +import { spawnSync } from '@socketsecurity/lib-stable/spawn/spawn' import { describe, expect, it } from 'vitest' const __dirname = fileURLToPath(new URL('.', import.meta.url)) @@ -106,11 +105,11 @@ describe('smol_manifest_native binding — sdxgen-bug-regressions equivalence', }) it.skipIf(!smolBinary)('live binding verifies all fixtures PASS', () => { - const opts: ExecFileSyncOptionsWithStringEncoding = { + const result = spawnSync(smolBinary!, [LIVE_VERIFIER], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], - } - const output = execFileSync(smolBinary!, [LIVE_VERIFIER], opts) + }) + const output = String(result.stdout ?? '') // Confirm every expected fixture name appears with PASS. for (let i = 0, { length } = EXPECTED_FIXTURE_NAMES; i < length; i += 1) { const name = EXPECTED_FIXTURE_NAMES[i] @@ -125,11 +124,11 @@ describe('smol_manifest_native binding — sdxgen-bug-regressions equivalence', it.skipIf(!smolBinary)( "parses socket-btm's own pnpm-lock.yaml without malformed entries", () => { - const opts: ExecFileSyncOptionsWithStringEncoding = { + const result = spawnSync(smolBinary!, [REAL_FIXTURE_VERIFIER], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], - } - const output = execFileSync(smolBinary!, [REAL_FIXTURE_VERIFIER], opts) + }) + const output = String(result.stdout ?? '') expect(output).toContain('PASS') expect(output).not.toContain('FAIL') }, diff --git a/packages/node-smol-builder/test/smol-purl.test.mts b/packages/node-smol-builder/test/smol-purl.test.mts index 04cc14e9d..3af4ed0bc 100644 --- a/packages/node-smol-builder/test/smol-purl.test.mts +++ b/packages/node-smol-builder/test/smol-purl.test.mts @@ -1,12 +1,15 @@ -// oxlint-disable socket/inclusive-language -- upstream PURL spec URL uses a legacy branch name we don't control. /** * PURL Tests for node:smol-purl * - * Test cases derived from the official PURL spec test suite: - * https://github.com/package-url/purl-spec/tree/master/test-suite-data + * Test cases derived from the official PURL spec test suite. + * See `PURL_SPEC_URL` below for the upstream reference. * * Gold standard: socket-packageurl-js (sibling fleet repo). */ +const PURL_SPEC_URL = + // oxlint-disable-next-line socket/inclusive-language -- upstream PURL spec URL uses a legacy branch name we don't control. + 'https://github.com/package-url/purl-spec/tree/master/test-suite-data' +void PURL_SPEC_URL import { describe, expect, it } from 'vitest' import { diff --git a/packages/node-smol-builder/upstream/libqrencode b/packages/node-smol-builder/upstream/libqrencode new file mode 160000 index 000000000..715e29fd4 --- /dev/null +++ b/packages/node-smol-builder/upstream/libqrencode @@ -0,0 +1 @@ +Subproject commit 715e29fd4cd71b6e452ae0f4e36d917b43122ce8 diff --git a/packages/node-smol-builder/upstream/md4c b/packages/node-smol-builder/upstream/md4c new file mode 160000 index 000000000..472c41700 --- /dev/null +++ b/packages/node-smol-builder/upstream/md4c @@ -0,0 +1 @@ +Subproject commit 472c417005c2c71b8617de4f7b8d6b30411d78f4 diff --git a/packages/node-smol-builder/upstream/temporal b/packages/node-smol-builder/upstream/temporal deleted file mode 160000 index 1d1b123ff..000000000 --- a/packages/node-smol-builder/upstream/temporal +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1d1b123ff78a3ab656d5aa19d803d1516f95e92f diff --git a/packages/node-smol-builder/upstream/tree-sitter b/packages/node-smol-builder/upstream/tree-sitter new file mode 160000 index 000000000..7f534862c --- /dev/null +++ b/packages/node-smol-builder/upstream/tree-sitter @@ -0,0 +1 @@ +Subproject commit 7f534862c3ec939c3a6ee147f7600ef5c1bf900f diff --git a/packages/node-smol-builder/vitest.config.mts b/packages/node-smol-builder/vitest.config.mts index f117d1309..d12a09489 100644 --- a/packages/node-smol-builder/vitest.config.mts +++ b/packages/node-smol-builder/vitest.config.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/no-default-export -- vitest CLI auto-discovers config via default import. */ /** * Extends shared vitest config. * Excludes build directories which contain Node.js test fixtures. @@ -7,6 +6,7 @@ import { defineConfig, mergeConfig } from 'vitest/config' import baseConfig from '../../.config/vitest.config.mts' +// oxlint-disable-next-line socket/no-default-export -- vitest CLI auto-discovers config via default import. export default mergeConfig( baseConfig, defineConfig({ diff --git a/packages/onnxruntime-builder/scripts/finalized/shared/finalize-wasm.mts b/packages/onnxruntime-builder/scripts/finalized/shared/finalize-wasm.mts index 80f6e189f..fed4ecc3a 100644 --- a/packages/onnxruntime-builder/scripts/finalized/shared/finalize-wasm.mts +++ b/packages/onnxruntime-builder/scripts/finalized/shared/finalize-wasm.mts @@ -105,8 +105,8 @@ export async function finalizeWasm(options) { `WASM file too small: ${wasmStats.size} bytes (expected >1MB)`, ) } - const _require = createRequire(import.meta.url) - const syncModule = _require(outputSyncCjsFile) + const require = createRequire(import.meta.url) + const syncModule = require(outputSyncCjsFile) if (!syncModule) { throw new Error('Sync module failed to load') } diff --git a/packages/onnxruntime-builder/scripts/wasm-compiled/shared/compile-wasm.mts b/packages/onnxruntime-builder/scripts/wasm-compiled/shared/compile-wasm.mts index 2af109c9c..940e7ecaa 100644 --- a/packages/onnxruntime-builder/scripts/wasm-compiled/shared/compile-wasm.mts +++ b/packages/onnxruntime-builder/scripts/wasm-compiled/shared/compile-wasm.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/no-status-emoji -- emoji are bullet markers in a multi-line troubleshooting message array (joined with \n) thrown as Error.message; there is no per-bullet logger.fail() call possible. */ /** * WASM compilation phase for ONNX Runtime @@ -24,7 +23,7 @@ import { WIN32 } from '@socketsecurity/lib-stable/constants/platform' import { safeDelete } from '@socketsecurity/lib-stable/fs/safe' import { getDefaultLogger } from '@socketsecurity/lib-stable/logger' import { spawn } from '@socketsecurity/lib-stable/spawn/spawn' -import { getDefaultSpinner } from '@socketsecurity/lib-stable/spinner' +import { getDefaultSpinner } from '@socketsecurity/lib-stable/spinner/registry' const logger = getDefaultLogger() const spinner = getDefaultSpinner() @@ -241,9 +240,13 @@ export async function compileWasm(options) { 'ONNX Runtime build script failed', '', 'Common causes:', + // oxlint-disable-next-line socket/no-status-emoji -- emoji are bullet markers in a multi-line troubleshooting message array (joined with \n) thrown as Error.message; there is no per-bullet logger.fail() call possible. ' ✗ Insufficient disk space (need ~5GB free)', + // oxlint-disable-next-line socket/no-status-emoji -- emoji are bullet markers in a multi-line troubleshooting message array (joined with \n) thrown as Error.message; there is no per-bullet logger.fail() call possible. ' ✗ Missing dependencies (cmake, python3, emscripten)', + // oxlint-disable-next-line socket/no-status-emoji -- emoji are bullet markers in a multi-line troubleshooting message array (joined with \n) thrown as Error.message; there is no per-bullet logger.fail() call possible. ' ✗ Compilation timeout or out-of-memory', + // oxlint-disable-next-line socket/no-status-emoji -- emoji are bullet markers in a multi-line troubleshooting message array (joined with \n) thrown as Error.message; there is no per-bullet logger.fail() call possible. ' ✗ Incompatible Emscripten version', '', 'Troubleshooting:', diff --git a/packages/onnxruntime-builder/test/build-output.test.mts b/packages/onnxruntime-builder/test/build-output.test.mts index ffd637e80..c3ca8eb6c 100644 --- a/packages/onnxruntime-builder/test/build-output.test.mts +++ b/packages/onnxruntime-builder/test/build-output.test.mts @@ -10,7 +10,7 @@ import { fileURLToPath } from 'node:url' import { createWasmTestHelpers } from 'build-infra/lib/test/helpers' -import { isObjectObject } from '@socketsecurity/lib-stable/objects' +import { isObjectObject } from '@socketsecurity/lib-stable/objects/types' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const packageDir = path.join(__dirname, '..') @@ -114,8 +114,8 @@ describe('onnxruntime-builder WASM output', () => { }) it('ort-sync.js can be required as CommonJS module', async () => { - const _require = createRequire(import.meta.url) - await helpers.testSyncJsRequirable(expect, _require) + const require = createRequire(import.meta.url) + await helpers.testSyncJsRequirable(expect, require) }) it('loaded ort module should be an object', () => { @@ -124,8 +124,8 @@ describe('onnxruntime-builder WASM output', () => { return } - const _require = createRequire(import.meta.url) - const ort = _require(syncJsPath) + const require = createRequire(import.meta.url) + const ort = require(syncJsPath) expect(isObjectObject(ort)).toBeTruthy() }) }) diff --git a/packages/onnxruntime-builder/vitest.config.mts b/packages/onnxruntime-builder/vitest.config.mts index 018ac3d54..ae22fc89b 100644 --- a/packages/onnxruntime-builder/vitest.config.mts +++ b/packages/onnxruntime-builder/vitest.config.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/no-default-export -- vitest CLI auto-discovers config via default import. */ /** * Extends shared vitest config. * Excludes build and upstream directories. @@ -7,6 +6,7 @@ import { defineConfig, mergeConfig } from 'vitest/config' import baseConfig from '../../.config/vitest.config.mts' +// oxlint-disable-next-line socket/no-default-export -- vitest CLI auto-discovers config via default import. export default mergeConfig( baseConfig, defineConfig({ diff --git a/packages/opentui-builder/build.zig.zon b/packages/opentui-builder/build.zig.zon index 6d7ae2a03..f20c35427 100644 --- a/packages/opentui-builder/build.zig.zon +++ b/packages/opentui-builder/build.zig.zon @@ -7,7 +7,7 @@ .uucode = .{ // Vendored from https://github.com/jacobsandlund/uucode at // commit 84ceda8561a17ba4a9b96ac5c583f779660bbd4e — what - // upstream opentui v0.1.99's build.zig.zon pins (0.2.0 tag). + // upstream opentui v0.2.15's build.zig.zon pins (0.2.0 tag). // Submodule is initialized by the opentui.yml // init-submodules step and copied into the patched source // tree as dependencies/uucode/ by apply-patches.mts, so diff --git a/packages/opentui-builder/external-tools.json b/packages/opentui-builder/external-tools.json index fecfe5c34..3cc97b3cd 100644 --- a/packages/opentui-builder/external-tools.json +++ b/packages/opentui-builder/external-tools.json @@ -7,7 +7,7 @@ "version": "0.15.2", "source": "https://codeberg.org/ziglang/zig", "sourceTag": "0.15.2", - "notes": "Zig 0.15.2 matches what upstream opentui v0.1.99's build.zig.zon targets — using 0.16 surfaces ~40 sites of stdlib API churn (ArrayListUnmanaged .empty, std.Thread.Mutex → std.Io.Mutex with required Io plumbing, std.fs.cwd removal, std.meta.intToEnum removal) that would each need a source patch. Source of truth is Codeberg; prebuilt binary + SHA-256 list live in build-infra/tool-checksums/zig-0.15.2.json. macOS 26 SDK note: the 0.15.x linker may not accept macOS 26 SDK stubs, so local dev on macOS 26 may need a system zig 0.16; CI uses depot-macos-15 so is unaffected." + "notes": "Zig 0.15.2 matches what upstream opentui v0.2.15's build.zig.zon targets — using 0.16 surfaces ~40 sites of stdlib API churn (ArrayListUnmanaged .empty, std.Thread.Mutex → std.Io.Mutex with required Io plumbing, std.fs.cwd removal, std.meta.intToEnum removal) that would each need a source patch. Source of truth is Codeberg; prebuilt binary + SHA-256 list live in build-infra/tool-checksums/zig-0.15.2.json. macOS 26 SDK note: the 0.15.x linker may not accept macOS 26 SDK stubs, so local dev on macOS 26 may need a system zig 0.16; CI uses depot-macos-15 so is unaffected." } } } diff --git a/packages/opentui-builder/lib/index.mts b/packages/opentui-builder/lib/index.mts index b68518017..884242a90 100644 --- a/packages/opentui-builder/lib/index.mts +++ b/packages/opentui-builder/lib/index.mts @@ -1,5 +1,4 @@ // max-file-lines: legitimate -- cohesive module — one tool/domain/phase; splitting along arbitrary line cap would fracture related logic -/* oxlint-disable socket/sort-source-methods -- public API surface ordered by usage (createRenderer first, then helpers); alphabetizing would bury the entry-point function. */ import { existsSync } from 'node:fs' import { createRequire } from 'node:module' import path from 'node:path' @@ -284,6 +283,7 @@ export const TargetChannel = { const textEncoder = new TextEncoder() +// oxlint-disable-next-line socket/sort-source-methods -- public API surface ordered by usage (createRenderer first, then helpers); alphabetizing would bury the entry-point function. export function encodeText(text) { return textEncoder.encode(text) } @@ -403,12 +403,13 @@ const BLACK = new Float32Array([0, 0, 0, 1]) const WHITE = new Float32Array([1, 1, 1, 1]) const TRANSPARENT = new Float32Array([0, 0, 0, 0]) -const _hasFA = typeof native.bufferDrawTextFA === 'function' -const _hasFast = typeof native.editBufferInsertTextFast === 'function' -const _hasSized = typeof native.editBufferGetTextSized === 'function' -const _hasBinary = typeof native.writeOutBinary === 'function' -const _hasCursorInto = typeof native.editBufferGetCursorInto === 'function' +const hasFA = typeof native.bufferDrawTextFA === 'function' +const hasFast = typeof native.editBufferInsertTextFast === 'function' +const hasSized = typeof native.editBufferGetTextSized === 'function' +const hasBinary = typeof native.writeOutBinary === 'function' +const hasCursorInto = typeof native.editBufferGetCursorInto === 'function' +// oxlint-disable-next-line socket/sort-source-methods -- public API surface ordered by usage (createRenderer first, then helpers); alphabetizing would bury the entry-point function. export function colorBuf(color) { if (color instanceof RGBA) { return color.buffer @@ -452,7 +453,7 @@ export class Buffer { } clear(bg) { - if (_hasFA && bg) { + if (hasFA && bg) { const b = colorBuf(bg) native.bufferClear(this._ptr, b[0], b[1], b[2], b[3]) } else { @@ -473,7 +474,7 @@ export class Buffer { drawText(text, x, y, fg, bg, attrs = 0) { const fgBuf = colorBuf(fg ?? WHITE) const bgBuf = colorBuf(bg ?? BLACK) - if (_hasFA) { + if (hasFA) { native.bufferDrawTextFA(this._ptr, text, x, y, fgBuf, bgBuf, attrs) } else { native.bufferDrawText( @@ -497,7 +498,7 @@ export class Buffer { drawChar(char, x, y, fg, bg, attrs = 0) { const fgBuf = colorBuf(fg ?? WHITE) const bgBuf = colorBuf(bg ?? BLACK) - if (_hasFA) { + if (hasFA) { native.bufferDrawCharFA(this._ptr, char, x, y, fgBuf, bgBuf, attrs) } else { native.bufferDrawChar( @@ -521,7 +522,7 @@ export class Buffer { setCell(x, y, char, fg, bg, attrs = 0) { const fgBuf = colorBuf(fg ?? WHITE) const bgBuf = colorBuf(bg ?? BLACK) - if (_hasFA) { + if (hasFA) { native.bufferSetCellFA(this._ptr, x, y, char, fgBuf, bgBuf) } else { native.bufferSetCell( @@ -544,7 +545,7 @@ export class Buffer { fillRect(x, y, width, height, bg) { const bgBuf = colorBuf(bg ?? BLACK) - if (_hasFA) { + if (hasFA) { native.bufferFillRectFA(this._ptr, x, y, width, height, bgBuf) } else { native.bufferFillRect( @@ -617,13 +618,13 @@ export class TextBuffer { } get text() { - return _hasSized + return hasSized ? native.textBufferGetPlainTextSized(this._ptr) : native.textBufferGetPlainText(this._ptr) } append(text) { - if (_hasFast) { + if (hasFast) { native.textBufferAppendFast(this._ptr, text) } else { native.textBufferAppend(this._ptr, text) @@ -694,7 +695,7 @@ export class TextBuffer { export class EditBuffer { constructor(widthMethod = WidthMethod.WCWIDTH) { this._ptr = native.createEditBuffer(widthMethod) - this._cursor = _hasCursorInto ? new CursorState() : undefined + this._cursor = hasCursorInto ? new CursorState() : undefined } get ptr() { @@ -702,7 +703,7 @@ export class EditBuffer { } get text() { - return _hasSized + return hasSized ? native.editBufferGetTextSized(this._ptr) : native.editBufferGetText(this._ptr) } @@ -712,7 +713,7 @@ export class EditBuffer { } insertText(text) { - if (_hasFast) { + if (hasFast) { native.editBufferInsertTextFast(this._ptr, text) } else { native.editBufferInsertText(this._ptr, text) @@ -807,7 +808,7 @@ export class EditorView { constructor(editBuffer, width, height) { const ptr = editBuffer._ptr ?? editBuffer this._ptr = native.createEditorView(ptr, width, height) - this._cursor = _hasCursorInto ? new CursorState() : undefined + this._cursor = hasCursorInto ? new CursorState() : undefined } get ptr() { @@ -933,7 +934,7 @@ export class Renderer { const testing = opts?.testing ?? false const remote = opts?.remote ?? false this._ptr = native.createRenderer(width, height, testing, remote) - this._cursor = _hasCursorInto ? new CursorState() : undefined + this._cursor = hasCursorInto ? new CursorState() : undefined } get ptr() { @@ -996,7 +997,7 @@ export class Renderer { } writeOut(data) { - if (_hasBinary && data instanceof Uint8Array) { + if (hasBinary && data instanceof Uint8Array) { native.writeOutBinary(this._ptr, data) } else { native.writeOut(this._ptr, data) diff --git a/packages/opentui-builder/package.json b/packages/opentui-builder/package.json index e7ead7c30..6d80a80f0 100644 --- a/packages/opentui-builder/package.json +++ b/packages/opentui-builder/package.json @@ -36,10 +36,10 @@ }, "sources": { "opentui": { - "version": "0.1.99", + "version": "0.2.15", "type": "git", "url": "https://github.com/anomalyco/opentui.git", - "ref": "cc94b5829ac0f6f45320562811acd8260ddc1922" + "ref": "f464acfcba0dde0ffcd6b2728811df787a72975c" } } } diff --git a/packages/opentui-builder/patches/001-rgba-type-align.patch b/packages/opentui-builder/patches/001-rgba-type-align.patch deleted file mode 100644 index 74a895231..000000000 --- a/packages/opentui-builder/patches/001-rgba-type-align.patch +++ /dev/null @@ -1,17 +0,0 @@ -# @opentui-versions: v0.1.99 -# @description: Align utils.zig RGBA type with ansi.zig so callers across lib.zig don't hit `?[4]f32` vs `?@Vector(4, f32)` mismatches when passing colors to buffer/text-buffer APIs. -# ---- a/utils.zig -+++ b/utils.zig -@@ -1,7 +1,10 @@ - const std = @import("std"); - - /// RGBA color type (4 f32 values) --pub const RGBA = @Vector(4, f32); -+/// NOTE: Kept as `[4]f32` to match `ansi.zig` and the type signatures in -+/// `buffer.zig` / `text-buffer.zig`. Having two competing RGBA aliases -+/// caused 15 `expected ?[4]f32, found ?@Vector(4, f32)` errors. -+pub const RGBA = [4]f32; - - /// Convert a pointer to 4 f32 values into an RGBA color - pub fn f32PtrToRGBA(ptr: [*]const f32) RGBA { diff --git a/packages/opentui-builder/upstream/opentui b/packages/opentui-builder/upstream/opentui index cc94b5829..f464acfcb 160000 --- a/packages/opentui-builder/upstream/opentui +++ b/packages/opentui-builder/upstream/opentui @@ -1 +1 @@ -Subproject commit cc94b5829ac0f6f45320562811acd8260ddc1922 +Subproject commit f464acfcba0dde0ffcd6b2728811df787a72975c diff --git a/packages/opentui-builder/vitest.config.mts b/packages/opentui-builder/vitest.config.mts index 1d9a0bbde..29de9bda8 100644 --- a/packages/opentui-builder/vitest.config.mts +++ b/packages/opentui-builder/vitest.config.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/no-default-export -- vitest CLI auto-discovers config via default import. */ /** * Extends shared vitest config. */ @@ -6,6 +5,7 @@ import { defineConfig, mergeConfig } from 'vitest/config' import baseConfig from '../../.config/vitest.config.mts' +// oxlint-disable-next-line socket/no-default-export -- vitest CLI auto-discovers config via default import. export default mergeConfig( baseConfig, defineConfig({ diff --git a/packages/stubs-builder/vitest.config.mts b/packages/stubs-builder/vitest.config.mts index 29f0e9a4b..4c0fe24d7 100644 --- a/packages/stubs-builder/vitest.config.mts +++ b/packages/stubs-builder/vitest.config.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/no-default-export -- vitest CLI auto-discovers config via default import. */ /** * Extends shared vitest config. * Tighter hookTimeout for curl/mbedTLS setup and faster testTimeout @@ -8,6 +7,7 @@ import { defineConfig, mergeConfig } from 'vitest/config' import baseConfig from '../../.config/vitest.config.mts' +// oxlint-disable-next-line socket/no-default-export -- vitest CLI auto-discovers config via default import. export default mergeConfig( baseConfig, defineConfig({ diff --git a/packages/temporal-infra/test/scripts/test262-temporal-runner.mts b/packages/temporal-infra/test/scripts/test262-temporal-runner.mts index f9c9950df..d9737d432 100644 --- a/packages/temporal-infra/test/scripts/test262-temporal-runner.mts +++ b/packages/temporal-infra/test/scripts/test262-temporal-runner.mts @@ -103,6 +103,7 @@ export function parseArgs(argv: readonly string[]): ParsedArgs { } export function printHelp(): void { + // oxlint-disable-next-line socket/no-logger-newline-literal -- help text is a single readable block; splitting into 17 logger.log calls would obscure structure. logger.log(` Test262 Temporal Subset Runner diff --git a/packages/temporal-infra/test/scripts/test262/harness.mts b/packages/temporal-infra/test/scripts/test262/harness.mts index aea0491a3..444d71d95 100644 --- a/packages/temporal-infra/test/scripts/test262/harness.mts +++ b/packages/temporal-infra/test/scripts/test262/harness.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/sort-source-methods -- pipeline ordering (loader → composer → walker); the composer depends on the loader so reading it top-down requires this order. */ /** * @fileoverview Test262 harness loader, script composer, and corpus * walker. @@ -33,6 +32,7 @@ export function loadHarness(name: string): string { // https://github.com/tc39/test262/blob/main/INTERPRETING.md const DEFAULT_INCLUDES = ['assert.js', 'sta.js'] +// oxlint-disable-next-line socket/sort-source-methods -- pipeline ordering (loader → composer → walker); the composer depends on the loader so reading it top-down requires this order. export function composeScript( test: TestCase, scenario: 'strict' | 'sloppy', diff --git a/packages/temporal-infra/vitest.config.mts b/packages/temporal-infra/vitest.config.mts index dc4ec3952..38920d3bf 100644 --- a/packages/temporal-infra/vitest.config.mts +++ b/packages/temporal-infra/vitest.config.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/no-default-export -- vitest CLI auto-discovers config via default import. */ /** * Extends shared vitest config — temporal-infra's unit tests are pure * regex / classifier checks with no binary spawning, so the 30s @@ -8,6 +7,7 @@ import { defineConfig, mergeConfig } from 'vitest/config' import baseConfig from '../../.config/vitest.config.mts' +// oxlint-disable-next-line socket/no-default-export -- vitest CLI auto-discovers config via default import. export default mergeConfig( baseConfig, defineConfig({ diff --git a/packages/tui-infra/README.md b/packages/tui-infra/README.md index 04cb4bc99..996f06ea1 100644 --- a/packages/tui-infra/README.md +++ b/packages/tui-infra/README.md @@ -1,23 +1,31 @@ # tui-infra -Source-only C++ TUI primitives — render loop + Yoga binding + mouse -parser — embedded into `node-smol-builder` as the `node:smol-tui` -builtin module. Mirrors the [`temporal-infra`](../temporal-infra/) pattern: -no binary release, no Docker, no workflow. Consumers compile the `.cc` -/ `.hpp` files inline via additions/source-patched. +Source-only C++ TUI primitives — ANSI emit + cell-buffer diff render +loop + Yoga binding + mouse parser — embedded into `node-smol-builder` +as the `node:smol-tui` builtin. Mirrors the +[`temporal-infra`](../temporal-infra/) pattern: no binary release, no +Docker, no workflow. Consumers compile the `.cc` / `.hpp` files inline +via additions/source-patched. ## Status -**v0 scaffold.** Native code not yet ported; this is the skeleton + -lockstep plan only. The first three PRs port one tier each. +**Tier 1–3 ported.** ANSI emit, cell-buffer diff render loop, mouse +parser, and Yoga direct binding are all live in +[`node:smol-tui`](../node-smol-builder/additions/source-patched/lib/smol-tui.js). +The binding glue lives at +[`additions/source-patched/src/socketsecurity/tui/tui_binding.cc`](../node-smol-builder/additions/source-patched/src/socketsecurity/tui/tui_binding.cc). +Higher-level surfaces (`@opentui/react`, `@opentui/keymap`, +`@opentui/qrcode`, `@opentui/solid`) are planned as +`node:smol-tui/` siblings — see the design plan at +[`.claude/plans/opentui-smol-tui-completion.md`](../../.claude/plans/opentui-smol-tui-completion.md). -## Three-tier plan +## Three-tier port -| Tier | Surface | Hot path? | Upstream | Status | -| ------ | ------------------------------------------- | ----------------------------------- | --------------------------------------------------------------------- | ------ | -| Tier 1 | ANSI emit (cursor moves, SGR, cell flushes) | Yes — every frame | OpenTUI `packages/core/src/zig/ansi.zig` (268 LOC, the per-cell path) | TODO | -| Tier 2 | Cell buffer + diff + render loop | Yes — every frame | OpenTUI `packages/core/src/zig/renderer.zig` | TODO | -| Tier 3 | Yoga direct binding + mouse parser | Per-event (mouse), per-frame (Yoga) | yoga + socket-stuie `packages/react/src/mouse-parser.ts` | TODO | +| Tier | Surface | Hot path? | Upstream | Status | +| ------ | --------------------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| Tier 1 | ANSI emit (cursor moves, SGR, cell flushes) | Yes — every frame | [`opentui/packages/core/src/zig/ansi.zig`](../opentui-builder/upstream/opentui/packages/core/src/zig/ansi.zig) | DONE | +| Tier 2 | Cell buffer + diff + render loop | Yes — every frame | [`opentui/packages/core/src/zig/renderer.zig`](../opentui-builder/upstream/opentui/packages/core/src/zig/renderer.zig) + [`buffer-methods.zig`](../opentui-builder/upstream/opentui/packages/core/src/zig/buffer-methods.zig) | DONE | +| Tier 3 | Yoga direct binding + mouse parser | Per-event (mouse), per-frame (Yoga) | yoga 3.2.1 (C++ upstream) + [`opentui/packages/core/src/lib/parse.mouse.ts`](../opentui-builder/upstream/opentui/packages/core/src/lib/parse.mouse.ts) | DONE | The user-facing `packages/core/src/ansi.ts` (18 LOC) is a thin re-export of cursor/screen state primitives — NOT the per-cell hot @@ -25,18 +33,15 @@ path. The Zig file is where the render loop's flush calls land (`ANSI.moveToOutput / fgColorOutput / bgColorOutput / applyAttributesOutputWriter`). -Each tier ships independently. Tier 1 alone wins ~30% per-frame on -typical OpenTUI workloads (per socket-stuie's `bench/render.mts`). - ## Architecture Three layers, same shape as `temporal-infra`: -| Layer | Source | Notes | -| ------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | -| **(1) Layout (Yoga)** | yoga submodule (C++ upstream) | Yoga is already C++ — Tier 3 wires it directly into node-smol via `node:smol-tui.computeLayout()`, replacing socket-stuie's WASM bridge. | -| **(2) Terminal I/O** | Node's `process.stdout` + libuv | We don't re-implement raw I/O. ANSI emit produces a Buffer; we write it via the existing Node stream API. | -| **(3) Render algorithms** | this package | Tier 1+2: cell buffer, dirty diff, ANSI batch emit. Tier 3: mouse-event SGR parser. | +| Layer | Source | Notes | +| ------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| **(1) Layout (Yoga)** | yoga submodule (C++ upstream) | Yoga is already C++ — `node:smol-tui.computeLayout()` wires it directly into node-smol, no JS bridge. | +| **(2) Terminal I/O** | Node's `process.stdout` + libuv | Raw I/O stays in Node. ANSI emit produces a `Buffer`; the JS layer writes it via the existing Node stream API. | +| **(3) Render algorithms** | this package | Cell buffer, dirty diff, ANSI batch emit, SGR / X10 mouse decode, Yoga handle registry. | ## Why source-only @@ -46,34 +51,43 @@ Same rationale as `temporal-infra`: headers and compiles the `.cc` files alongside V8/Node sources. - Single source of truth: socket-stuie's TS render loop and this C++ port both target the same OpenTUI semantics; lockstep tracked via - the central `.config/lockstep.json` `rows` array (matches the - socket-btm fleet pattern — same file temporal-infra and other - upstream-tracking packages use). + `.config/lockstep.json` rows (`tui-infra-ansi`, `tui-infra-buffer`, + `tui-infra-renderer`, `tui-infra-mouse`, and the `opentui` + `version-pin`). - Bumping OpenTUI means bumping the submodule SHA + re-running the parity tests — same workflow as bumping `boa-dev/temporal` in temporal-infra. -## Lockstep - -Rows planned for socket-btm's `.config/lockstep.json`: - -| ID | Kind | Upstream | Local | -| ------------------ | ------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------- | -| `tui-infra-ansi` | `file-fork` | socket-stuie `packages/core/upstream/opentui/packages/core/src/zig/ansi.zig` | `src/socketsecurity/tui/ansi.cc` | -| `tui-infra-render` | `file-fork` | socket-stuie `packages/core/upstream/opentui/packages/core/src/zig/renderer.zig` | `src/socketsecurity/tui/render.cc` (Tier 2) | -| `tui-infra-mouse` | `file-fork` | socket-stuie `packages/react/src/mouse-parser.ts` (already optimized) | `src/socketsecurity/tui/mouse_parser.cc` (Tier 3) | -| `opentui-parity` | `version-pin` | `anomalyco/opentui` v0.1.99 (SHA `cc94b58`, matches socket-stuie's existing `opentui` pin) | upstream submodule reference | -| `yoga` | `version-pin` | `facebook/yoga` (matches socket-stuie's pin) | `upstream/yoga/` (Tier 3) | - -socket-stuie's TS layer keeps being the test-bed for new TUI features; -when a feature stabilizes there, the file-fork row picks it up and the -C++ port follows. +## JS contract + +`node:smol-tui` exposes one binding under `internalBinding('smol_tui')` +re-exported by `lib/smol-tui.js`. The surface groups by tier: + +- **ANSI constants + writers** — `constants.{reset,clear,...}`, + `cursorPosition`, `setFgRgb`, `setBgRgb`, `writeCursorPosition` (Fast + API), `writeFgRgb` (Fast API), `writeBgRgb` (Fast API), + `writeAttributes` (Fast API), `sizes.{maxCursorPositionLen,...}`. +- **Renderer / cell buffer** — `createRenderer`, `destroyRenderer`, + `rendererResize`, `rendererClear`, `rendererSet`, `rendererFillRect`, + `rendererDrawText`, `rendererInvalidate`, `rendererFlush`, + `rendererSize`. +- **Mouse parser** — `createParser`, `destroyParser`, `resetParser`, + `parseMouseOne`, `looksLikeMouseSequence`, `mouseEventType.{...}`, + `scrollDirection.{...}`. +- **Yoga layout** — `yogaCreateNode`, `yogaFreeNode`, `yogaInsertChild`, + `yogaRemoveChild`, `yogaCalculateLayout`, `yogaMarkDirty`, + `yogaGetComputedLayout`, plus 14 `yogaSet*` setters, plus enum + mirrors `flexDirection.{...}`, `justify.{...}`, `align.{...}`, + `edge.{...}`, `wrap.{...}`, `positionType.{...}`, `direction.{...}`. + +All entries are zero-allocation per call where the binding can manage +it: hot-path writers and `rendererFlush` take caller-allocated +`Uint8Array` outputs; the JS layer reuses one buffer per session. ## Wiring into node-smol -Once Tier 1 ports land, `node-smol-builder`'s -`prepare-external-sources.mts` will add two `MONOREPO_PACKAGE_SOURCES` -entries (mirrors temporal-infra's wiring): +[`prepare-external-sources.mts`](../node-smol-builder/scripts/binary-released/shared/prepare-external-sources.mts) +copies the two trees into `additions/source-patched/`: ```ts { @@ -86,6 +100,19 @@ entries (mirrors temporal-infra's wiring): }, ``` -The `node:smol-tui` builtin is then registered via a new node-smol -patch parallel to the existing `node:smol-power` / `node:smol-util` -patches. +Three node-smol patches register the binding: + +- [`004-node-gyp-smol-sources.patch`](../node-smol-builder/patches/source-patched/004-node-gyp-smol-sources.patch) + — lists the `.cc` files in `node.gyp` under the + `node_use_smol_tui == "true"` gate. +- [`017-smol-builtin-bindings.patch`](../node-smol-builder/patches/source-patched/017-smol-builtin-bindings.patch) + — declares `smol_tui` in `NODE_BUILTIN_BINDINGS`. +- [`018-configure-postgres-iouring.patch`](../node-smol-builder/patches/source-patched/018-configure-postgres-iouring.patch) + — adds `--without-smol-tui` flag and `node_use_smol_tui` variable. + +The `node:` prefix is enforced by patch +[`003-realm-smol-bindings.patch`](../node-smol-builder/patches/source-patched/003-realm-smol-bindings.patch), +which adds `'smol-tui'` to the `schemelessBlockList` in +`lib/internal/bootstrap/realm.js`. Loading `require('smol-tui')` +without the prefix fails with `ERR_UNKNOWN_BUILTIN_MODULE`; the only +valid spec is `require('node:smol-tui')`. diff --git a/packages/tui-infra/include/tui/buffer.hpp b/packages/tui-infra/include/tui/buffer.hpp index 21dc58f2b..cf4d5edab 100644 --- a/packages/tui-infra/include/tui/buffer.hpp +++ b/packages/tui-infra/include/tui/buffer.hpp @@ -29,6 +29,7 @@ #include #include +#include #include #include "tui/cell.hpp" @@ -44,6 +45,15 @@ class CellBuffer { uint32_t Height() const noexcept { return height_; } const Cell* Data() const noexcept { return cells_.data(); } + // Swap contents with another buffer of the same dimensions. O(1) — + // swaps the underlying std::vector + width/height triple. Used by + // Renderer::Flush to commit `prev_ <- next_` without a 144 KB copy. + void Swap(CellBuffer& other) noexcept { + cells_.swap(other.cells_); + std::swap(width_, other.width_); + std::swap(height_, other.height_); + } + // Resize the grid. Existing content is discarded — callers redraw // after resize. Idempotent when width/height already match. void Resize(uint32_t width, uint32_t height); diff --git a/packages/tui-infra/include/tui/cell.hpp b/packages/tui-infra/include/tui/cell.hpp index 87545a4ef..61acfab40 100644 --- a/packages/tui-infra/include/tui/cell.hpp +++ b/packages/tui-infra/include/tui/cell.hpp @@ -34,16 +34,25 @@ struct Cell { uint8_t bg_g = 0; uint8_t bg_b = 0; uint8_t attrs = 0; // Bitfield — matches TextAttributes in ansi.hpp. - - bool operator==(const Cell& other) const noexcept { - return codepoint == other.codepoint && fg_r == other.fg_r && - fg_g == other.fg_g && fg_b == other.fg_b && bg_r == other.bg_r && - bg_g == other.bg_g && bg_b == other.bg_b && attrs == other.attrs; - } - - bool operator!=(const Cell& other) const noexcept { - return !(*this == other); - } + // Explicit reserved byte to eliminate the trailing padding the + // compiler would otherwise insert (4 + 7 = 11 bytes natural, + // padded to 12 for alignof(uint32_t)). Making the padding an + // explicitly-zero member means C++20's defaulted operator== + // emits an optimal memcmp of 12 bytes (1×8-byte cmp + 1×4-byte + // cmp on x86-64 / ARM64) instead of the 8 chained byte compares + // the hand-rolled member-wise version generated. + // + // The reserved byte is also a forward-compat slot for a future + // 9th attr bit (e.g. `kReverseFg`) without bumping struct size. + uint8_t reserved = 0; + + // Defaulted == lets the compiler pick the optimal comparison + // strategy. With all members trivially-comparable + zero padding + // (via `reserved`), the compiler emits ~2 instructions instead + // of 8 sequential byte cmps. Renderer::Flush's per-cell `cur == + // old` is the hottest comparison in the codebase — 12k cells per + // 200×60 frame. + bool operator==(const Cell&) const noexcept = default; }; } // namespace tui diff --git a/packages/tui-infra/include/tui/renderables.hpp b/packages/tui-infra/include/tui/renderables.hpp new file mode 100644 index 000000000..4329dbabe --- /dev/null +++ b/packages/tui-infra/include/tui/renderables.hpp @@ -0,0 +1,84 @@ +// High-level renderables built atop CellBuffer primitives. +// +// 1:1 port of OpenTUI's box/text-drawing helpers +// (packages/core/src/lib/border.ts + packages/core/src/renderables/Box.ts + +// renderables/Text.ts). The TS originals are Renderable subclasses that +// own Yoga layout state + draw via OptimizedBuffer; here we strip the +// Renderable hierarchy and expose the pure drawing primitives that the +// JS commit phase will call into. Yoga layout stays in the +// `node:smol-tui` Yoga binding; this header just consumes computed +// rectangles. +// +// Hot path: every render-tree node turns into one DrawBox or one +// DrawTextWrapped call. Splitting the box into perimeter + fill in C++ +// shaves out the per-edge JS dispatch (the TS BoxRenderable does 4-12 +// fillRect calls per node) and removes glyph-set lookup overhead. + +#ifndef TUI_INFRA_RENDERABLES_HPP_ +#define TUI_INFRA_RENDERABLES_HPP_ + +#include + +#include "tui/buffer.hpp" + +namespace tui { + +// Border style enum — matches the four BorderStyle values in +// packages/core/src/lib/border.ts. Numeric values are stable across the +// JS↔C++ boundary; the JS facade re-exports them as a frozen enum +// object. +enum class BorderStyle : uint8_t { + kSingle = 0, + kDouble = 1, + kRounded = 2, + kHeavy = 3, +}; + +// Per-edge enable flags. The TS surface accepts a `boolean | BorderSides[]` +// arg; on the C++ side we collapse to a bitfield. The JS facade does the +// conversion. +struct BorderSides { + bool top = true; + bool right = true; + bool bottom = true; + bool left = true; +}; + +struct BoxStyle { + BorderStyle style = BorderStyle::kSingle; + BorderSides sides{}; + uint8_t border_fg_r = 255; + uint8_t border_fg_g = 255; + uint8_t border_fg_b = 255; + uint8_t bg_r = 0; + uint8_t bg_g = 0; + uint8_t bg_b = 0; + uint8_t attrs = 0; + bool fill_background = false; // When false, only the border is drawn. +}; + +// Draw a box (border + optional fill) into `buf` covering the rect +// `(x, y, w, h)`. Out-of-bounds is clipped. When `style.fill_background` +// is true, the interior cells are filled with the background style; when +// false, only the perimeter is touched (existing interior content +// preserved). 1-character border, 1-cell width on every side. +void DrawBox(CellBuffer& buf, uint32_t x, uint32_t y, uint32_t w, uint32_t h, + const BoxStyle& style); + +// Draw `text` starting at `(x, y)`. When `max_width` is non-zero, breaks +// at the last word boundary that fits (whitespace-split); when +// `max_width` is zero, lines extend to the buffer's right edge. When +// `max_lines` is non-zero, truncates after that many emitted lines. +// Returns the number of lines emitted. +// +// Word boundary: ASCII space + tab. CJK / emoji: each codepoint counts +// as a single cell (matching the CellBuffer::DrawText fast path). +uint32_t DrawTextWrapped(CellBuffer& buf, uint32_t x, uint32_t y, + uint32_t max_width, uint32_t max_lines, + const char* utf8, size_t length, uint8_t fg_r, + uint8_t fg_g, uint8_t fg_b, uint8_t bg_r, + uint8_t bg_g, uint8_t bg_b, uint8_t attrs); + +} // namespace tui + +#endif // TUI_INFRA_RENDERABLES_HPP_ diff --git a/packages/tui-infra/include/tui/utf8.hpp b/packages/tui-infra/include/tui/utf8.hpp new file mode 100644 index 000000000..92f326234 --- /dev/null +++ b/packages/tui-infra/include/tui/utf8.hpp @@ -0,0 +1,138 @@ +// UTF-8 decode / scan primitives — shared by buffer.cc, width.cc, +// and renderables.cc. Header-only so each consumer's compiler can +// inline the small functions and apply call-site-specific +// optimizations (in particular: the ASCII fast path tends to fold +// into the caller's loop, eliminating the function-call overhead +// entirely). +// +// Functions: +// +// DecodeUtf8(p, end) -> uint32_t codepoint +// Decodes one codepoint starting at `p` and advances `p` by 1-4 +// bytes. Malformed sequences yield U+FFFD and advance by 1 byte. +// The malformed-input handling matches the WHATWG decode-error +// replacement behavior (no exceptions, forward progress +// guaranteed). +// +// Utf8ByteLen(p, end) -> size_t bytes +// Returns the byte length of the codepoint starting at `p` +// without decoding it. Used by callers that need to slice raw +// UTF-8 bytes (e.g. word-wrap line builders) without paying the +// codepoint-assembly cost. +// +// Performance: +// - ASCII path is one byte read + bit test + early return. The +// compiler routinely inlines and unrolls this into vectorized +// scanning loops at the caller. +// - Multi-byte paths walk one branch per length class; the +// branches are sorted by frequency (2-byte → 3-byte → 4-byte) +// so common European / CJK input hits the early branches. +// - No allocations, no exceptions, no global state. + +#ifndef TUI_INFRA_UTF8_HPP_ +#define TUI_INFRA_UTF8_HPP_ + +#include +#include + +namespace tui { + +inline uint32_t DecodeUtf8(const char*& p, const char* end) { + if (p >= end) { + return 0; + } + const uint8_t b0 = static_cast(*p); + // ASCII fast path — single byte, no continuation bytes to check. + if ((b0 & 0x80) == 0) { + ++p; + return b0; + } + // 2-byte sequence: 110xxxxx 10xxxxxx. + if ((b0 & 0xe0) == 0xc0 && p + 1 < end) { + const uint8_t b1 = static_cast(*(p + 1)); + if ((b1 & 0xc0) == 0x80) { + p += 2; + return ((b0 & 0x1fu) << 6) | (b1 & 0x3fu); + } + } + // 3-byte sequence: 1110xxxx 10xxxxxx 10xxxxxx. + if ((b0 & 0xf0) == 0xe0 && p + 2 < end) { + const uint8_t b1 = static_cast(*(p + 1)); + const uint8_t b2 = static_cast(*(p + 2)); + if ((b1 & 0xc0) == 0x80 && (b2 & 0xc0) == 0x80) { + p += 3; + return ((b0 & 0x0fu) << 12) | ((b1 & 0x3fu) << 6) | (b2 & 0x3fu); + } + } + // 4-byte sequence: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx. + if ((b0 & 0xf8) == 0xf0 && p + 3 < end) { + const uint8_t b1 = static_cast(*(p + 1)); + const uint8_t b2 = static_cast(*(p + 2)); + const uint8_t b3 = static_cast(*(p + 3)); + if ((b1 & 0xc0) == 0x80 && (b2 & 0xc0) == 0x80 && (b3 & 0xc0) == 0x80) { + p += 4; + return ((b0 & 0x07u) << 18) | ((b1 & 0x3fu) << 12) | + ((b2 & 0x3fu) << 6) | (b3 & 0x3fu); + } + } + // Malformed input — return U+FFFD and advance by 1 byte to + // guarantee forward progress on every iteration. + ++p; + return 0xfffd; +} + +// Encode a codepoint as UTF-8 into `dst`. Returns bytes written +// (1..4). Caller MUST guarantee dst has at least 4 bytes free — +// no overflow check inside. +// +// Codepoints above U+10FFFF are encoded as 4 bytes per the bit +// pattern; callers should normalize invalid codepoints to U+FFFD +// before calling if they want WHATWG-strict behavior. The renderer's +// flush loop trusts CellBuffer to hold only valid codepoints (which +// DecodeUtf8 guarantees on the input side). +inline size_t EncodeUtf8(uint32_t cp, char* dst) { + if (cp < 0x80) { + dst[0] = static_cast(cp); + return 1; + } + if (cp < 0x800) { + dst[0] = static_cast(0xc0 | (cp >> 6)); + dst[1] = static_cast(0x80 | (cp & 0x3f)); + return 2; + } + if (cp < 0x10000) { + dst[0] = static_cast(0xe0 | (cp >> 12)); + dst[1] = static_cast(0x80 | ((cp >> 6) & 0x3f)); + dst[2] = static_cast(0x80 | (cp & 0x3f)); + return 3; + } + dst[0] = static_cast(0xf0 | (cp >> 18)); + dst[1] = static_cast(0x80 | ((cp >> 12) & 0x3f)); + dst[2] = static_cast(0x80 | ((cp >> 6) & 0x3f)); + dst[3] = static_cast(0x80 | (cp & 0x3f)); + return 4; +} + +inline size_t Utf8ByteLen(const char* p, const char* end) { + if (p >= end) { + return 0; + } + const uint8_t b0 = static_cast(*p); + if ((b0 & 0x80) == 0) { + return 1; + } + if ((b0 & 0xe0) == 0xc0) { + return 2; + } + if ((b0 & 0xf0) == 0xe0) { + return 3; + } + if ((b0 & 0xf8) == 0xf0) { + return 4; + } + return 1; +} + +} // namespace tui + +#endif // TUI_INFRA_UTF8_HPP_ diff --git a/packages/tui-infra/include/tui/width.hpp b/packages/tui-infra/include/tui/width.hpp new file mode 100644 index 000000000..1e363b6ba --- /dev/null +++ b/packages/tui-infra/include/tui/width.hpp @@ -0,0 +1,50 @@ +// String width — terminal display columns for a UTF-8 string. +// +// Computes the number of terminal cells a string occupies when +// rendered, per the Unicode East_Asian_Width property (UAX #11) plus +// emoji presentation (UAX #51). Uses Unicode 17.0.0 data — aligned +// with the fleet-wide Unicode pin tracked across ultrathink/acorn's +// Go / C++ / Rust / TypeScript implementations. +// +// Width rules: +// - East Asian Wide (W) + Full-width (F) → 2 cells +// - Emoji_Presentation codepoints → 2 cells +// - Combining marks, control chars, format codepoints → 0 cells +// - Everything else → 1 cell +// +// Limitations (intentional, kept for the JS fast path): +// - ZWJ sequences (e.g. `family: 👨‍👩‍👧`) are counted as the sum of +// their components. Most ZWJ sequences render as a single cluster +// in modern terminals; callers that need cluster-aware width +// should layer the `emoji-regex` library on top in JS for the +// edge cases. +// - Variation selectors are zero-width (covered) but they don't +// change the width of the preceding base character. A text-style +// `❤︎` (heart + VS-15) renders 1 cell on most terminals and 2 on +// others; we return 1 (the base character is 1, VS is 0). This +// matches the npm `string-width` package's behavior. +// - Grapheme clusters (Hangul jamos, Devanagari conjuncts) are +// summed by codepoint width. Hangul L/V/T jamos already have +// EAW=W and combine correctly; other scripts may over-count. + +#ifndef TUI_INFRA_WIDTH_HPP_ +#define TUI_INFRA_WIDTH_HPP_ + +#include +#include + +namespace tui { + +// Return the display width (terminal cells) of the UTF-8 input +// `[utf8, utf8 + length)`. ASCII-only input is O(length) with a tight +// inner loop; non-ASCII inputs do one binary-search per codepoint. +uint32_t StringWidth(const char* utf8, size_t length); + +// Return the display width of a single codepoint. Used by callers +// (e.g. DrawTextWrapped) that already have the codepoint and want to +// skip the UTF-8 decode. +uint32_t CodepointWidth(uint32_t cp); + +} // namespace tui + +#endif // TUI_INFRA_WIDTH_HPP_ diff --git a/packages/tui-infra/src/socketsecurity/tui/ansi.cc b/packages/tui-infra/src/socketsecurity/tui/ansi.cc index 0220d5be3..de34e3378 100644 --- a/packages/tui-infra/src/socketsecurity/tui/ansi.cc +++ b/packages/tui-infra/src/socketsecurity/tui/ansi.cc @@ -76,8 +76,29 @@ size_t WriteU16(char* dst, uint16_t value) { return 5; } +// Specialized u8 → decimal emitter. uint8_t maxes out at 255, so the +// `< 1000`, `< 10000`, and `< 100000` cases from WriteU16 are dead +// code. Hard-coding the three branches (1/2/3 digit) saves ~3-4 +// branches per call versus the generic u16 path. +// +// Per-cell RGB SGR writes call WriteU8 three times each (one per +// channel); on a 12 k-cell diff frame that's up to 36 k calls, so +// even the small branch savings add up. inline size_t WriteU8(char* dst, uint8_t value) { - return WriteU16(dst, value); + if (value < 10) { + dst[0] = static_cast('0' + value); + return 1; + } + if (value < 100) { + dst[0] = static_cast('0' + (value / 10)); + dst[1] = static_cast('0' + (value % 10)); + return 2; + } + // 100..255: always 3 digits. + dst[0] = static_cast('0' + (value / 100)); + dst[1] = static_cast('0' + ((value / 10) % 10)); + dst[2] = static_cast('0' + (value % 10)); + return 3; } // Append a u16 as ASCII digits to a std::string. Used by cold-path @@ -200,10 +221,14 @@ size_t WriteAttributes(char* dst, uint8_t attrs) { // resolves to the SGR reset (ESC[0m). Bit positions match Zig // upstream's TextAttributes struct so the wire format is identical // to OpenTUI output. Worst case: all 8 bits set + reset = 32 bytes. + // + // Iteration walks only SET bits via __builtin_ctz: typical cells + // have 0-2 attrs set, so the loop runs 0-2 iterations instead of + // the 8 unconditional iterations a bit-mask-scan loop would do. + // For BOLD-only text (common case) that's 1 iteration vs 8. char* p = dst; *p++ = '\x1b'; *p++ = '['; - bool first = true; if (attrs == TextAttributes::kNone) { *p++ = '0'; *p++ = 'm'; @@ -211,14 +236,26 @@ size_t WriteAttributes(char* dst, uint8_t attrs) { } // SGR codes mirror the kBold/kDim/... constants above (1..5, 7..9). static constexpr uint8_t kSgrCode[8] = {1, 2, 3, 4, 5, 7, 8, 9}; - for (size_t i = 0; i < 8; ++i) { - if (attrs & (1u << i)) { - if (!first) { - *p++ = ';'; - } - first = false; - p += WriteU8(p, kSgrCode[i]); + bool first = true; + uint32_t bits = attrs; + while (bits != 0) { +#if defined(__GNUC__) || defined(__clang__) + const unsigned bit_idx = __builtin_ctz(bits); +#else + // MSVC fallback: same semantics via _BitScanForward. + unsigned long bit_idx_long; + _BitScanForward(&bit_idx_long, bits); + const unsigned bit_idx = static_cast(bit_idx_long); +#endif + if (!first) { + *p++ = ';'; } + first = false; + p += WriteU8(p, kSgrCode[bit_idx]); + // Clear the lowest set bit so the next __builtin_ctz finds the + // next-higher set bit. `bits & (bits - 1)` is the canonical + // branchless "clear lowest set bit" trick. + bits &= bits - 1; } *p++ = 'm'; return static_cast(p - dst); diff --git a/packages/tui-infra/src/socketsecurity/tui/buffer.cc b/packages/tui-infra/src/socketsecurity/tui/buffer.cc index e18c45a43..a3e9872a5 100644 --- a/packages/tui-infra/src/socketsecurity/tui/buffer.cc +++ b/packages/tui-infra/src/socketsecurity/tui/buffer.cc @@ -8,6 +8,8 @@ #include #include +#include "tui/utf8.hpp" + namespace tui { void CellBuffer::Resize(uint32_t width, uint32_t height) { @@ -52,56 +54,6 @@ void CellBuffer::FillRect(uint32_t x, uint32_t y, uint32_t w, uint32_t h, } } -namespace { - -// Decode one UTF-8 codepoint starting at p. Returns the codepoint and -// advances p by the byte count. Malformed sequences are replaced with -// U+FFFD and the byte stream advances by 1 to make forward progress. -uint32_t DecodeUtf8(const char*& p, const char* end) { - if (p >= end) { - return 0; - } - const uint8_t b0 = static_cast(*p); - // ASCII fast path. - if ((b0 & 0x80) == 0) { - ++p; - return b0; - } - // 2-byte sequence: 110xxxxx 10xxxxxx. - if ((b0 & 0xe0) == 0xc0 && p + 1 < end) { - const uint8_t b1 = static_cast(*(p + 1)); - if ((b1 & 0xc0) == 0x80) { - p += 2; - return ((b0 & 0x1fu) << 6) | (b1 & 0x3fu); - } - } - // 3-byte sequence: 1110xxxx 10xxxxxx 10xxxxxx. - if ((b0 & 0xf0) == 0xe0 && p + 2 < end) { - const uint8_t b1 = static_cast(*(p + 1)); - const uint8_t b2 = static_cast(*(p + 2)); - if ((b1 & 0xc0) == 0x80 && (b2 & 0xc0) == 0x80) { - p += 3; - return ((b0 & 0x0fu) << 12) | ((b1 & 0x3fu) << 6) | (b2 & 0x3fu); - } - } - // 4-byte sequence: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx. - if ((b0 & 0xf8) == 0xf0 && p + 3 < end) { - const uint8_t b1 = static_cast(*(p + 1)); - const uint8_t b2 = static_cast(*(p + 2)); - const uint8_t b3 = static_cast(*(p + 3)); - if ((b1 & 0xc0) == 0x80 && (b2 & 0xc0) == 0x80 && (b3 & 0xc0) == 0x80) { - p += 4; - return ((b0 & 0x07u) << 18) | ((b1 & 0x3fu) << 12) | - ((b2 & 0x3fu) << 6) | (b3 & 0x3fu); - } - } - // Malformed — replacement char, advance one byte. - ++p; - return 0xfffd; -} - -} // namespace - void CellBuffer::DrawText(uint32_t x, uint32_t y, const char* utf8, size_t length, uint8_t fg_r, uint8_t fg_g, uint8_t fg_b, uint8_t bg_r, uint8_t bg_g, @@ -110,19 +62,29 @@ void CellBuffer::DrawText(uint32_t x, uint32_t y, const char* utf8, return; } const char* p = utf8; - const char* end = utf8 + length; - uint32_t col = x; - while (p < end && col < width_) { - Cell c{}; + const char* const end = utf8 + length; + + // Hoist the style fields out of the loop — they're the same for + // every cell. Only the codepoint changes per iteration. Compiler + // can keep the partial Cell in registers. + Cell c{}; + c.fg_r = fg_r; + c.fg_g = fg_g; + c.fg_b = fg_b; + c.bg_r = bg_r; + c.bg_g = bg_g; + c.bg_b = bg_b; + c.attrs = attrs; + + // Row offset is loop-invariant. Pre-compute the row's base pointer + // and walk by one cell each step instead of recomputing IndexOf + // (which does y * width_) per character. + Cell* row = &cells_[static_cast(y) * width_ + x]; + const uint32_t col_end = width_ - x; + uint32_t col = 0; + while (p < end && col < col_end) { c.codepoint = DecodeUtf8(p, end); - c.fg_r = fg_r; - c.fg_g = fg_g; - c.fg_b = fg_b; - c.bg_r = bg_r; - c.bg_g = bg_g; - c.bg_b = bg_b; - c.attrs = attrs; - cells_[IndexOf(col, y)] = c; + row[col] = c; ++col; } } diff --git a/packages/tui-infra/src/socketsecurity/tui/renderables.cc b/packages/tui-infra/src/socketsecurity/tui/renderables.cc new file mode 100644 index 000000000..840999af4 --- /dev/null +++ b/packages/tui-infra/src/socketsecurity/tui/renderables.cc @@ -0,0 +1,305 @@ +// High-level renderables: borders + word-wrapped text. +// +// Source: opentui v0.2.15 (SHA f464acf). +// - packages/core/src/lib/border.ts (border glyph sets, BorderStyle enum) +// - packages/core/src/renderables/Box.ts (box draw) +// - packages/core/src/renderables/Text.ts (text wrap) +// +// The TS originals are Renderable subclasses that own Yoga layout state. +// Here we strip the Renderable hierarchy and expose pure drawing +// primitives: the JS commit phase computes layout via Yoga (already +// bound through node:smol-tui) and passes the resulting rectangle here. + +#include "tui/renderables.hpp" + +#include "tui/cell.hpp" +#include "tui/utf8.hpp" + +namespace tui { + +namespace { + +// Border glyph table — codepoints for each (BorderStyle × edge) slot. +// Mirrors opentui's BorderChars in packages/core/src/lib/border.ts. The +// glyph order matches the borderCharsToArray output: +// 0=TL, 1=TR, 2=BL, 3=BR, 4=Horizontal, 5=Vertical, +// 6=topT, 7=bottomT, 8=leftT, 9=rightT, 10=cross +// +// We use the same 11-slot layout for forward compatibility with the +// upstream junction renderer (which uses slots 6-10 for table joins); +// the current DrawBox path only reads slots 0-5. +constexpr uint32_t kBorderGlyphs[4][11] = { + // kSingle + {0x250C, 0x2510, 0x2514, 0x2518, 0x2500, 0x2502, 0x252C, 0x2534, 0x251C, + 0x2524, 0x253C}, + // kDouble + {0x2554, 0x2557, 0x255A, 0x255D, 0x2550, 0x2551, 0x2566, 0x2569, 0x2560, + 0x2563, 0x256C}, + // kRounded — corners differ from kSingle; mid-glyphs match kSingle. + {0x256D, 0x256E, 0x2570, 0x256F, 0x2500, 0x2502, 0x252C, 0x2534, 0x251C, + 0x2524, 0x253C}, + // kHeavy + {0x250F, 0x2513, 0x2517, 0x251B, 0x2501, 0x2503, 0x2533, 0x253B, 0x2523, + 0x252B, 0x254B}, +}; + +inline Cell MakeCell(uint32_t cp, const BoxStyle& s, bool border_glyph) { + Cell c{}; + c.codepoint = cp; + if (border_glyph) { + c.fg_r = s.border_fg_r; + c.fg_g = s.border_fg_g; + c.fg_b = s.border_fg_b; + } else { + // Interior fill cells take the background as fg AND bg so a + // wholly-cleared space character renders as a solid block of bg + // colour with no readable glyph from a previous frame leaking + // through. + c.fg_r = s.bg_r; + c.fg_g = s.bg_g; + c.fg_b = s.bg_b; + } + c.bg_r = s.bg_r; + c.bg_g = s.bg_g; + c.bg_b = s.bg_b; + c.attrs = s.attrs; + return c; +} + +} // namespace + +void DrawBox(CellBuffer& buf, uint32_t x, uint32_t y, uint32_t w, uint32_t h, + const BoxStyle& style) { + if (w == 0 || h == 0) { + return; + } + const uint32_t glyph_row = static_cast(style.style); + const uint32_t* g = kBorderGlyphs[glyph_row]; + + const uint32_t left = x; + const uint32_t right = x + w - 1; + const uint32_t top = y; + const uint32_t bottom = y + h - 1; + + // Fill the interior first (so border draws on top of any fill). + if (style.fill_background) { + const Cell fill = MakeCell(static_cast(' '), style, + /*border_glyph=*/false); + // Inset by 1 cell on each side that has a border. Sides without a + // border get filled edge-to-edge. + const uint32_t fx = left + (style.sides.left ? 1 : 0); + const uint32_t fy = top + (style.sides.top ? 1 : 0); + const uint32_t fw = w - (style.sides.left ? 1 : 0) - + (style.sides.right ? 1 : 0); + const uint32_t fh = h - (style.sides.top ? 1 : 0) - + (style.sides.bottom ? 1 : 0); + if (fw > 0 && fh > 0) { + buf.FillRect(fx, fy, fw, fh, fill); + } + } + + // Pre-compute the horizontal + vertical edge cells once. The + // per-edge FillRect / Set calls below all share these (same glyph + // + same style), so building the Cell up-front saves N redundant + // MakeCell() calls per edge. + const Cell h_cell = MakeCell(g[4], style, /*border_glyph=*/true); + const Cell v_cell = MakeCell(g[5], style, /*border_glyph=*/true); + + // Single-cell box degenerates: just draw a corner glyph. + if (w == 1 && h == 1) { + if (style.sides.top || style.sides.left || style.sides.right || + style.sides.bottom) { + buf.Set(left, top, MakeCell(g[0], style, /*border_glyph=*/true)); + } + return; + } + + // Horizontal edges. FillRect with h=1 batches all bounds checks + // into one pre-loop guard; the inner write loop is the same shape + // the compiler auto-vectorizes for fillRect's general path. + if (w > 2) { + if (style.sides.top) { + buf.FillRect(left + 1, top, w - 2, 1, h_cell); + } + if (style.sides.bottom && bottom != top) { + buf.FillRect(left + 1, bottom, w - 2, 1, h_cell); + } + } + + // Vertical edges (w=1 column rectangles). FillRect still wins over + // per-cell Set because Set re-runs the bounds check per call. + if (h > 2) { + if (style.sides.left) { + buf.FillRect(left, top + 1, 1, h - 2, v_cell); + } + if (style.sides.right && right != left) { + buf.FillRect(right, top + 1, 1, h - 2, v_cell); + } + } + + // Corners — only emit when both adjacent edges are enabled. + if (style.sides.top && style.sides.left) { + buf.Set(left, top, MakeCell(g[0], style, /*border_glyph=*/true)); + } + if (style.sides.top && style.sides.right) { + buf.Set(right, top, MakeCell(g[1], style, /*border_glyph=*/true)); + } + if (style.sides.bottom && style.sides.left) { + buf.Set(left, bottom, MakeCell(g[2], style, /*border_glyph=*/true)); + } + if (style.sides.bottom && style.sides.right) { + buf.Set(right, bottom, MakeCell(g[3], style, /*border_glyph=*/true)); + } +} + +uint32_t DrawTextWrapped(CellBuffer& buf, uint32_t x, uint32_t y, + uint32_t max_width, uint32_t max_lines, + const char* utf8, size_t length, uint8_t fg_r, + uint8_t fg_g, uint8_t fg_b, uint8_t bg_r, + uint8_t bg_g, uint8_t bg_b, uint8_t attrs) { + if (utf8 == nullptr || length == 0) { + return 0; + } + + // Effective width: caller-supplied max_width, or buffer-right-edge + // when max_width is 0 (sentinel meaning "extend to edge"). + uint32_t width_budget = max_width; + if (width_budget == 0) { + if (x >= buf.Width()) { + return 0; + } + width_budget = buf.Width() - x; + } + if (width_budget == 0) { + return 0; + } + + uint32_t cur_line = 0; + uint32_t cur_y = y; + + const char* p = utf8; + const char* const text_end = utf8 + length; + + while (p < text_end) { + if (max_lines != 0 && cur_line >= max_lines) { + break; + } + if (cur_y >= buf.Height()) { + break; + } + + // Find end of this visual line: walk byte-by-byte, tracking + // codepoint count + last word-break position. A word break is a + // run of one or more ASCII space/tab characters. + const char* line_start = p; + const char* line_end = p; + const char* last_break_end = nullptr; // First byte AFTER the last + // emitted whitespace run, i.e. + // the start of the next word. + uint32_t line_codepoints = 0; + bool in_break = false; + bool hard_break = false; + + const char* scan = p; + while (scan < text_end) { + const uint8_t b = static_cast(*scan); + + // Hard newline ends the line immediately, regardless of width. + if (b == '\n') { + line_end = scan; + hard_break = true; + ++scan; + break; + } + + const bool is_ws = (b == ' ' || b == '\t'); + if (is_ws) { + if (!in_break) { + // Just transitioned from word → whitespace. The word ends + // here; commit a candidate breakpoint at this position. + line_end = scan; + } + in_break = true; + } else if (in_break) { + // Whitespace → word transition: this is the start of the + // next potential word. + in_break = false; + last_break_end = scan; + } + + // Measure this codepoint (1 cell per codepoint for ASCII + + // basic UTF-8; wide-char handling is the future StringWidth + // helper). + const size_t cp_bytes = Utf8ByteLen(scan, text_end); + const uint32_t new_count = line_codepoints + 1; + + if (new_count > width_budget) { + // This codepoint would overflow. If we recorded a previous + // word-end within budget, break there; otherwise, force a + // hard wrap at the current position (long-word case). + if (line_end > line_start && line_end <= scan) { + // line_end already points at the last whitespace start; + // skip to next word for the next iteration's start. + if (last_break_end) { + scan = last_break_end; + } else { + // line_end is start of trailing whitespace; consume it + // before resuming. + scan = line_end; + while (scan < text_end && + (*scan == ' ' || *scan == '\t')) { + ++scan; + } + } + } else { + // No prior word boundary fits — hard split at current + // codepoint position. + line_end = scan; + // scan stays where it is so we don't lose the codepoint. + } + break; + } + + scan += cp_bytes; + line_codepoints = new_count; + line_end = scan; // Tentatively, the line extends through this cp. + } + + if (scan == text_end && !hard_break) { + // Reached end of text without overflow or newline. + line_end = scan; + } + + // Emit cells for [line_start, line_end). Use CellBuffer::DrawText + // for the actual UTF-8 → cell conversion; it handles per-cell fg/ + // bg/attrs identically. + if (line_end > line_start) { + buf.DrawText(x, cur_y, line_start, static_cast(line_end - line_start), + fg_r, fg_g, fg_b, bg_r, bg_g, bg_b, attrs); + } + ++cur_line; + ++cur_y; + + // Advance p past the consumed region. If we broke on whitespace, + // skip the whitespace run so the next line doesn't start with a + // space. + if (hard_break) { + p = scan; // scan already past the '\n'. + } else if (scan > line_end) { + // We already advanced scan past a whitespace run to next word. + p = scan; + } else { + // Hard-split case: continue from where the break happened. + p = scan; + // Eat any leading whitespace at the start of the next line so + // wraps look clean. + while (p < text_end && (*p == ' ' || *p == '\t')) { + ++p; + } + } + } + + return cur_line; +} + +} // namespace tui diff --git a/packages/tui-infra/src/socketsecurity/tui/renderer.cc b/packages/tui-infra/src/socketsecurity/tui/renderer.cc index db9cd7dbb..4fe6c3de4 100644 --- a/packages/tui-infra/src/socketsecurity/tui/renderer.cc +++ b/packages/tui-infra/src/socketsecurity/tui/renderer.cc @@ -14,36 +14,12 @@ #include "tui/ansi.hpp" #include "tui/buffer.hpp" #include "tui/cell.hpp" +#include "tui/utf8.hpp" namespace tui { namespace { -// Encode a codepoint as UTF-8 into dst. Returns bytes written -// (1..4). Caller guarantees dst has at least 4 bytes free. -size_t EncodeUtf8(uint32_t cp, char* dst) { - if (cp < 0x80) { - dst[0] = static_cast(cp); - return 1; - } - if (cp < 0x800) { - dst[0] = static_cast(0xc0 | (cp >> 6)); - dst[1] = static_cast(0x80 | (cp & 0x3f)); - return 2; - } - if (cp < 0x10000) { - dst[0] = static_cast(0xe0 | (cp >> 12)); - dst[1] = static_cast(0x80 | ((cp >> 6) & 0x3f)); - dst[2] = static_cast(0x80 | (cp & 0x3f)); - return 3; - } - dst[0] = static_cast(0xf0 | (cp >> 18)); - dst[1] = static_cast(0x80 | ((cp >> 12) & 0x3f)); - dst[2] = static_cast(0x80 | ((cp >> 6) & 0x3f)); - dst[3] = static_cast(0x80 | (cp & 0x3f)); - return 4; -} - // Worst-case bytes for emitting a single changed cell from a cold state // (no carry-over fg/bg/attrs): cursor move + fg SGR + bg SGR + attr SGR // + utf-8 codepoint = 14 + 20 + 20 + 26 + 4 = 84 bytes. Pad for safety. @@ -89,8 +65,26 @@ size_t Renderer::Flush(char* dst, size_t dst_capacity) { const Cell* prev_data = prev_.Data(); for (uint32_t y = 0; y < h; ++y) { + // Row base — y * w is loop-invariant within the inner loop. The + // optimizer would hoist this in most cases, but writing it + // explicitly makes the inner-loop index just `+ x` (one add). + const size_t row_base = static_cast(y) * w; + + // Row-level memcmp fast skip: on a typical TUI frame most rows + // are completely unchanged (e.g. only the header / status line + // updated). One memcmp of `w * sizeof(Cell)` bytes (= 12w bytes) + // is ~13x faster than `w` per-cell == comparisons because libc's + // memcmp is vectorized (AVX2 / NEON, ~32 bytes/cycle). When the + // memcmp says the row is identical AND we're not doing a full + // redraw, skip the entire inner loop. + if (!full_redraw && + std::memcmp(&next_data[row_base], &prev_data[row_base], + static_cast(w) * sizeof(Cell)) == 0) { + continue; + } + for (uint32_t x = 0; x < w; ++x) { - const size_t i = static_cast(y) * w + x; + const size_t i = row_base + x; const Cell& cur = next_data[i]; const Cell& old = prev_data[i]; @@ -150,16 +144,28 @@ size_t Renderer::Flush(char* dst, size_t dst_capacity) { // Trailing SGR reset so the next caller-driven write doesn't inherit // our last style. Only emit if we actually wrote anything. if (p != dst) { - const size_t reset_len = std::strlen(kReset); - if (static_cast(end - p) < reset_len) { + // kReset is a compile-time const char[]; sizeof - 1 skips the + // null terminator without a runtime strlen call. + constexpr size_t kResetLen = sizeof(kReset) - 1; + if (static_cast(end - p) < kResetLen) { return kFlushOverflow; } - std::memcpy(p, kReset, reset_len); - p += reset_len; + std::memcpy(p, kReset, kResetLen); + p += kResetLen; } // Commit: prev_ now matches what the terminal shows. - prev_ = next_; + // + // Swap rather than copy: prev_ and next_ are both owned by this + // Renderer; after Flush, the caller's next-frame setup starts with + // Clear() which overwrites next_'s contents anyway. Swap is three + // pointer assignments (the internal std::vector pointers); the + // alternative `prev_ = next_` is a full 12-bytes-per-cell copy — + // 144 KB for a 200×60 grid, every frame. + next_.Swap(prev_); + // After the swap, prev_ holds what the terminal now shows (the + // former next_), and next_ holds the previous frame's data — which + // the caller will Clear() on entry to the next render cycle. needs_full_redraw_ = false; return static_cast(p - dst); diff --git a/packages/tui-infra/src/socketsecurity/tui/width.cc b/packages/tui-infra/src/socketsecurity/tui/width.cc new file mode 100644 index 000000000..e54555481 --- /dev/null +++ b/packages/tui-infra/src/socketsecurity/tui/width.cc @@ -0,0 +1,115 @@ +// String width implementation. +// +// Codepoint classification uses two sorted [lo, hi]-range tables +// generated from the Unicode 17.0.0 data files (width_data.cc): +// +// - kWideRanges → EAW W/F ∪ Emoji_Presentation +// - kZeroWidthRanges → controls + combining + format + variation +// selectors + tags +// +// Lookup is binary search per codepoint. The ASCII fast path (most +// terminal output) skips the searches entirely and runs at memory +// bandwidth. + +#include "tui/width.hpp" + +#include +#include + +#include "tui/utf8.hpp" + +namespace tui { + +extern const uint32_t kWideRanges[][2]; +extern const uint32_t kZeroWidthRanges[][2]; +extern const size_t kWideRangesCount; +extern const size_t kZeroWidthRangesCount; + +namespace { + +// Binary search a [lo, hi] sorted range table for cp. Returns true +// iff cp falls within any of the ranges. +inline bool InRangeTable(uint32_t cp, const uint32_t (*table)[2], + size_t count) { + size_t lo = 0; + size_t hi = count; + while (lo < hi) { + const size_t mid = lo + (hi - lo) / 2; + const uint32_t r_lo = table[mid][0]; + const uint32_t r_hi = table[mid][1]; + if (cp < r_lo) { + hi = mid; + } else if (cp > r_hi) { + lo = mid + 1; + } else { + return true; + } + } + return false; +} + +} // namespace + +uint32_t CodepointWidth(uint32_t cp) { + // ASCII fast path — most terminal output is plain ASCII. + if (cp < 0x80) { + if (cp < 0x20 || cp == 0x7f) { + return 0; // C0 controls + DEL + } + return 1; + } + + // Zero-width first. The table is small (13 ranges); binary search + // is ~4 iterations worst case. Most non-ASCII codepoints are not + // zero-width, so the search exits fast through a non-match. + if (InRangeTable(cp, kZeroWidthRanges, kZeroWidthRangesCount)) { + return 0; + } + + // BMP fast path: the first wide-range entry is U+1100 (Hangul + // Jamo). Any codepoint below that — Latin Extended, IPA, Greek, + // Cyrillic, Hebrew, Arabic, Devanagari, etc. — is width 1 by + // definition once we've ruled out zero-width above. Skipping the + // ~7-iteration binary search saves cycles on the dominant + // non-ASCII codepath (Western European text, accents, smart + // quotes). + if (cp < 0x1100) { + return 1; + } + + if (InRangeTable(cp, kWideRanges, kWideRangesCount)) { + return 2; + } + return 1; +} + +uint32_t StringWidth(const char* utf8, size_t length) { + if (utf8 == nullptr || length == 0) { + return 0; + } + uint32_t total = 0; + const char* p = utf8; + const char* const end = utf8 + length; + + // ASCII fast path: scan run of bytes < 0x80 with a tight loop. + // Any non-ASCII byte falls through to the per-codepoint path. + while (p < end) { + const uint8_t b = static_cast(*p); + if (b < 0x80) { + if (b < 0x20 || b == 0x7f) { + // Control char — zero width. + } else { + total += 1; + } + ++p; + continue; + } + // Non-ASCII: decode + lookup. + const uint32_t cp = DecodeUtf8(p, end); + total += CodepointWidth(cp); + } + + return total; +} + +} // namespace tui diff --git a/packages/tui-infra/src/socketsecurity/tui/width_data.cc b/packages/tui-infra/src/socketsecurity/tui/width_data.cc new file mode 100644 index 000000000..a22186274 --- /dev/null +++ b/packages/tui-infra/src/socketsecurity/tui/width_data.cc @@ -0,0 +1,163 @@ +// Auto-generated from Unicode 17.0.0 EastAsianWidth.txt + +// emoji-data.txt. Do not hand-edit; regenerate via +// scripts/generate-width-data.mts. +// +// kWideRanges: 123 ranges (width = 2) +// kZeroWidthRanges: 13 ranges (width = 0) + +#include +#include + +namespace tui { + +extern const uint32_t kWideRanges[][2]; +const uint32_t kWideRanges[][2] = { + {0x1100, 0x115f}, + {0x231a, 0x231b}, + {0x2329, 0x232a}, + {0x23e9, 0x23ec}, + {0x23f0, 0x23f0}, + {0x23f3, 0x23f3}, + {0x25fd, 0x25fe}, + {0x2614, 0x2615}, + {0x2630, 0x2637}, + {0x2648, 0x2653}, + {0x267f, 0x267f}, + {0x268a, 0x268f}, + {0x2693, 0x2693}, + {0x26a1, 0x26a1}, + {0x26aa, 0x26ab}, + {0x26bd, 0x26be}, + {0x26c4, 0x26c5}, + {0x26ce, 0x26ce}, + {0x26d4, 0x26d4}, + {0x26ea, 0x26ea}, + {0x26f2, 0x26f3}, + {0x26f5, 0x26f5}, + {0x26fa, 0x26fa}, + {0x26fd, 0x26fd}, + {0x2705, 0x2705}, + {0x270a, 0x270b}, + {0x2728, 0x2728}, + {0x274c, 0x274c}, + {0x274e, 0x274e}, + {0x2753, 0x2755}, + {0x2757, 0x2757}, + {0x2795, 0x2797}, + {0x27b0, 0x27b0}, + {0x27bf, 0x27bf}, + {0x2b1b, 0x2b1c}, + {0x2b50, 0x2b50}, + {0x2b55, 0x2b55}, + {0x2e80, 0x2e99}, + {0x2e9b, 0x2ef3}, + {0x2f00, 0x2fd5}, + {0x2ff0, 0x303e}, + {0x3041, 0x3096}, + {0x3099, 0x30ff}, + {0x3105, 0x312f}, + {0x3131, 0x318e}, + {0x3190, 0x31e5}, + {0x31ef, 0x321e}, + {0x3220, 0x3247}, + {0x3250, 0xa48c}, + {0xa490, 0xa4c6}, + {0xa960, 0xa97c}, + {0xac00, 0xd7a3}, + {0xf900, 0xfaff}, + {0xfe10, 0xfe19}, + {0xfe30, 0xfe52}, + {0xfe54, 0xfe66}, + {0xfe68, 0xfe6b}, + {0xff01, 0xff60}, + {0xffe0, 0xffe6}, + {0x16fe0, 0x16fe4}, + {0x16ff0, 0x16ff6}, + {0x17000, 0x18cd5}, + {0x18cff, 0x18d1e}, + {0x18d80, 0x18df2}, + {0x1aff0, 0x1aff3}, + {0x1aff5, 0x1affb}, + {0x1affd, 0x1affe}, + {0x1b000, 0x1b122}, + {0x1b132, 0x1b132}, + {0x1b150, 0x1b152}, + {0x1b155, 0x1b155}, + {0x1b164, 0x1b167}, + {0x1b170, 0x1b2fb}, + {0x1d300, 0x1d356}, + {0x1d360, 0x1d376}, + {0x1f004, 0x1f004}, + {0x1f0cf, 0x1f0cf}, + {0x1f18e, 0x1f18e}, + {0x1f191, 0x1f19a}, + {0x1f1e6, 0x1f202}, + {0x1f210, 0x1f23b}, + {0x1f240, 0x1f248}, + {0x1f250, 0x1f251}, + {0x1f260, 0x1f265}, + {0x1f300, 0x1f320}, + {0x1f32d, 0x1f335}, + {0x1f337, 0x1f37c}, + {0x1f37e, 0x1f393}, + {0x1f3a0, 0x1f3ca}, + {0x1f3cf, 0x1f3d3}, + {0x1f3e0, 0x1f3f0}, + {0x1f3f4, 0x1f3f4}, + {0x1f3f8, 0x1f43e}, + {0x1f440, 0x1f440}, + {0x1f442, 0x1f4fc}, + {0x1f4ff, 0x1f53d}, + {0x1f54b, 0x1f54e}, + {0x1f550, 0x1f567}, + {0x1f57a, 0x1f57a}, + {0x1f595, 0x1f596}, + {0x1f5a4, 0x1f5a4}, + {0x1f5fb, 0x1f64f}, + {0x1f680, 0x1f6c5}, + {0x1f6cc, 0x1f6cc}, + {0x1f6d0, 0x1f6d2}, + {0x1f6d5, 0x1f6d8}, + {0x1f6dc, 0x1f6df}, + {0x1f6eb, 0x1f6ec}, + {0x1f6f4, 0x1f6fc}, + {0x1f7e0, 0x1f7eb}, + {0x1f7f0, 0x1f7f0}, + {0x1f90c, 0x1f93a}, + {0x1f93c, 0x1f945}, + {0x1f947, 0x1f9ff}, + {0x1fa70, 0x1fa7c}, + {0x1fa80, 0x1fa8a}, + {0x1fa8e, 0x1fac6}, + {0x1fac8, 0x1fac8}, + {0x1facd, 0x1fadc}, + {0x1fadf, 0x1faea}, + {0x1faef, 0x1faf8}, + {0x20000, 0x2fffd}, + {0x30000, 0x3fffd}, +}; + +extern const uint32_t kZeroWidthRanges[][2]; +const uint32_t kZeroWidthRanges[][2] = { + {0x0, 0x1f}, + {0x7f, 0x9f}, + {0xad, 0xad}, + {0x300, 0x36f}, + {0x61c, 0x61c}, + {0x180e, 0x180e}, + {0x200b, 0x200f}, + {0x202a, 0x202e}, + {0x2060, 0x206f}, + {0xfe00, 0xfe0f}, + {0xfff9, 0xfffb}, + {0xe0000, 0xe007f}, + {0xe0100, 0xe01ef}, +}; + +extern const size_t kWideRangesCount; +const size_t kWideRangesCount = 123; + +extern const size_t kZeroWidthRangesCount; +const size_t kZeroWidthRangesCount = 13; + +} // namespace tui diff --git a/packages/ultraviolet-builder/vitest.config.mts b/packages/ultraviolet-builder/vitest.config.mts index 76320f3a8..5b85f2e37 100644 --- a/packages/ultraviolet-builder/vitest.config.mts +++ b/packages/ultraviolet-builder/vitest.config.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/no-default-export -- vitest CLI auto-discovers config via default import. */ /** * Extends shared vitest config — ultraviolet-builder tests exercise * Go N-API bindings which load quickly, so override to the 30s @@ -8,6 +7,7 @@ import { defineConfig, mergeConfig } from 'vitest/config' import baseConfig from '../../.config/vitest.config.mts' +// oxlint-disable-next-line socket/no-default-export -- vitest CLI auto-discovers config via default import. export default mergeConfig( baseConfig, defineConfig({ diff --git a/packages/yoga-layout-builder/scripts/finalized/shared/finalize-wasm.mts b/packages/yoga-layout-builder/scripts/finalized/shared/finalize-wasm.mts index c71be0092..57d0f5695 100644 --- a/packages/yoga-layout-builder/scripts/finalized/shared/finalize-wasm.mts +++ b/packages/yoga-layout-builder/scripts/finalized/shared/finalize-wasm.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/prefer-exists-sync -- fs.stat() calls consume stats.size to validate WASM and sync-wrapper size before finalize. */ /** * WASM finalization phase for Yoga Layout * @@ -98,12 +97,14 @@ export async function finalizeWasm(options) { if (magic !== '0061736d') { throw new Error('Invalid WASM file (bad magic number)') } + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size to validate WASM and sync-wrapper size before finalize. const wasmStats = await fs.stat(outputWasmFile) if (wasmStats.size < 100_000) { throw new Error( `WASM file too small: ${wasmStats.size} bytes (expected >100KB)`, ) } + // oxlint-disable-next-line socket/prefer-exists-sync -- fs.stat() calls consume stats.size to validate WASM and sync-wrapper size before finalize. const syncStats = await fs.stat(outputSyncCjsFile) if (syncStats.size === 0) { throw new Error('Sync wrapper file is empty') diff --git a/packages/yoga-layout-builder/scripts/wasm-synced/shared/generate-sync.mts b/packages/yoga-layout-builder/scripts/wasm-synced/shared/generate-sync.mts index 39ac511a9..f6ae011d6 100644 --- a/packages/yoga-layout-builder/scripts/wasm-synced/shared/generate-sync.mts +++ b/packages/yoga-layout-builder/scripts/wasm-synced/shared/generate-sync.mts @@ -99,13 +99,13 @@ export async function generateSync(options) { binaryPath: path.relative(buildDir, outputSyncDir), binarySize: syncSize, smokeTest: async () => { - const _require = createRequire(import.meta.url) + const require = createRequire(import.meta.url) // oxlint-disable-next-line socket/prefer-exists-sync -- need syncStats.size to detect empty-output failure. const syncStats = await fs.stat(syncCjsFile) if (syncStats.size === 0) { throw new Error('Sync wrapper file is empty') } - const Yoga = _require(syncCjsFile) + const Yoga = require(syncCjsFile) if (typeof Yoga !== 'object' || Yoga === null) { throw new Error( `Sync wrapper failed to load properly: got ${typeof Yoga}`, diff --git a/packages/yoga-layout-builder/test/build-output.test.mts b/packages/yoga-layout-builder/test/build-output.test.mts index bd4037791..42ffb7820 100644 --- a/packages/yoga-layout-builder/test/build-output.test.mts +++ b/packages/yoga-layout-builder/test/build-output.test.mts @@ -10,7 +10,7 @@ import { fileURLToPath } from 'node:url' import { createWasmTestHelpers } from 'build-infra/lib/test/helpers' -import { isObjectObject } from '@socketsecurity/lib-stable/objects' +import { isObjectObject } from '@socketsecurity/lib-stable/objects/types' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const packageDir = path.join(__dirname, '..') @@ -112,8 +112,8 @@ describe('yoga-layout-builder WASM output', () => { }) it('yoga-sync.js can be required as CommonJS module', async () => { - const _require = createRequire(import.meta.url) - await helpers.testSyncJsRequirable(expect, _require) + const require = createRequire(import.meta.url) + await helpers.testSyncJsRequirable(expect, require) }) it('loaded yoga module should be an object', () => { @@ -122,8 +122,8 @@ describe('yoga-layout-builder WASM output', () => { return } - const _require = createRequire(import.meta.url) - const yoga = _require(syncJsPath) + const require = createRequire(import.meta.url) + const yoga = require(syncJsPath) expect(isObjectObject(yoga)).toBeTruthy() }) }) diff --git a/packages/yoga-layout-builder/vitest.config.mts b/packages/yoga-layout-builder/vitest.config.mts index 018ac3d54..ae22fc89b 100644 --- a/packages/yoga-layout-builder/vitest.config.mts +++ b/packages/yoga-layout-builder/vitest.config.mts @@ -1,4 +1,3 @@ -/* oxlint-disable socket/no-default-export -- vitest CLI auto-discovers config via default import. */ /** * Extends shared vitest config. * Excludes build and upstream directories. @@ -7,6 +6,7 @@ import { defineConfig, mergeConfig } from 'vitest/config' import baseConfig from '../../.config/vitest.config.mts' +// oxlint-disable-next-line socket/no-default-export -- vitest CLI auto-discovers config via default import. export default mergeConfig( baseConfig, defineConfig({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 662e9eb7a..a91e0a19c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -740,6 +740,19 @@ importers: specifier: 'catalog:' version: 4.0.3(@types/node@24.9.2)(jiti@2.6.1)(yaml@2.8.3) + packages/dawn-builder: + dependencies: + '@socketsecurity/lib-stable': + specifier: 'catalog:' + version: '@socketsecurity/lib@6.0.0' + build-infra: + specifier: workspace:* + version: link:../build-infra + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 24.9.2 + packages/ink-builder: dependencies: '@socketsecurity/lib-stable': diff --git a/scripts/check-consistency.mts b/scripts/check-consistency.mts index 18cfb7d11..162fe8ed2 100644 --- a/scripts/check-consistency.mts +++ b/scripts/check-consistency.mts @@ -1,7 +1,5 @@ #!/usr/bin/env node // max-file-lines: legitimate -- consistency gate: gather → diff → report pipeline across many subsystems; splitting fractures the flow -/* oxlint-disable socket/no-status-emoji -- script uses a local log(msg, color) helper that composes ANSI color with status markers; logger.success/fail would drop the explicit color control needed for the consistency-checker's multi-column report. */ -/* oxlint-disable socket/sort-source-methods -- script ordered as a top-down checker pipeline (discover issues per category → batch fixes → report); alphabetizing would scatter the per-rule check flow. */ /** * Automated consistency checker for Socket BTM monorepo. @@ -233,6 +231,7 @@ export function reportIssue( /** * Prompts user for yes/no confirmation in interactive mode */ +// oxlint-disable-next-line socket/sort-source-methods -- script ordered as a top-down checker pipeline (discover issues per category → batch fixes → report); alphabetizing would scatter the per-rule check flow. export async function promptUser(question: string): Promise { const rl = readline.createInterface({ input: process.stdin, @@ -251,6 +250,7 @@ export async function promptUser(question: string): Promise { // Package Discovery // ============================================================================ +// oxlint-disable-next-line socket/sort-source-methods -- script ordered as a top-down checker pipeline (discover issues per category → batch fixes → report); alphabetizing would scatter the per-rule check flow. export async function discoverPackages(): Promise { const entries = await fs.readdir(PACKAGES_DIR, { withFileTypes: true, @@ -287,6 +287,7 @@ export async function discoverPackages(): Promise { // Check 1: Required Files // ============================================================================ +// oxlint-disable-next-line socket/sort-source-methods -- script ordered as a top-down checker pipeline (discover issues per category → batch fixes → report); alphabetizing would scatter the per-rule check flow. export async function checkRequiredFiles( packages: PackageInfo[], ): Promise { @@ -328,6 +329,7 @@ export async function checkRequiredFiles( // Check 2: Vitest Configuration // ============================================================================ +// oxlint-disable-next-line socket/sort-source-methods -- script ordered as a top-down checker pipeline (discover issues per category → batch fixes → report); alphabetizing would scatter the per-rule check flow. export async function checkVitestConfig( packages: PackageInfo[], ): Promise { @@ -384,6 +386,7 @@ export default mergeConfig(baseConfig, { // Check 3: Test Scripts // ============================================================================ +// oxlint-disable-next-line socket/sort-source-methods -- script ordered as a top-down checker pipeline (discover issues per category → batch fixes → report); alphabetizing would scatter the per-rule check flow. export async function checkTestScripts(packages: PackageInfo[]): Promise { log('[3/8] Checking test scripts...', colors.blue) @@ -429,6 +432,7 @@ export async function checkTestScripts(packages: PackageInfo[]): Promise { // Check 4: Coverage Scripts // ============================================================================ +// oxlint-disable-next-line socket/sort-source-methods -- script ordered as a top-down checker pipeline (discover issues per category → batch fixes → report); alphabetizing would scatter the per-rule check flow. export async function checkCoverageScripts( packages: PackageInfo[], ): Promise { @@ -484,6 +488,7 @@ export async function checkCoverageScripts( // Check 5: External Tools Documentation // ============================================================================ +// oxlint-disable-next-line socket/sort-source-methods -- script ordered as a top-down checker pipeline (discover issues per category → batch fixes → report); alphabetizing would scatter the per-rule check flow. export async function checkExternalTools( packages: PackageInfo[], ): Promise { @@ -557,6 +562,7 @@ export async function checkExternalTools( // Check 6: Build Output Structure // ============================================================================ +// oxlint-disable-next-line socket/sort-source-methods -- script ordered as a top-down checker pipeline (discover issues per category → batch fixes → report); alphabetizing would scatter the per-rule check flow. export async function checkBuildOutputStructure( packages: PackageInfo[], ): Promise { @@ -645,6 +651,7 @@ export async function checkBuildOutputStructure( // Check 7: Package.json Structure // ============================================================================ +// oxlint-disable-next-line socket/sort-source-methods -- script ordered as a top-down checker pipeline (discover issues per category → batch fixes → report); alphabetizing would scatter the per-rule check flow. export async function checkPackageJsonStructure( packages: PackageInfo[], ): Promise { @@ -771,6 +778,7 @@ export async function checkPackageJsonStructure( // Check 8: Workspace Dependencies // ============================================================================ +// oxlint-disable-next-line socket/sort-source-methods -- script ordered as a top-down checker pipeline (discover issues per category → batch fixes → report); alphabetizing would scatter the per-rule check flow. export async function checkWorkspaceDependencies( packages: PackageInfo[], ): Promise { @@ -843,6 +851,7 @@ export async function checkWorkspaceDependencies( /** * Collects patterns across all packages for ML-powered suggestions */ +// oxlint-disable-next-line socket/sort-source-methods -- script ordered as a top-down checker pipeline (discover issues per category → batch fixes → report); alphabetizing would scatter the per-rule check flow. export function collectPatterns(packages: PackageInfo[]): void { patternStats.total = packages.length @@ -949,6 +958,7 @@ export function collectPatterns(packages: PackageInfo[]): void { /** * Calculate confidence score as percentage */ +// oxlint-disable-next-line socket/sort-source-methods -- script ordered as a top-down checker pipeline (discover issues per category → batch fixes → report); alphabetizing would scatter the per-rule check flow. export function getConfidence(count: number, total: number): number { return (count / total) * 100 } @@ -956,6 +966,7 @@ export function getConfidence(count: number, total: number): number { /** * Get confidence level based on percentage */ +// oxlint-disable-next-line socket/sort-source-methods -- script ordered as a top-down checker pipeline (discover issues per category → batch fixes → report); alphabetizing would scatter the per-rule check flow. export function getConfidenceLevel( confidence: number, ): 'HIGH' | 'LOW' | 'MEDIUM' | 'VERY HIGH' { @@ -974,6 +985,7 @@ export function getConfidenceLevel( /** * Generates ML-powered suggestions based on pattern analysis */ +// oxlint-disable-next-line socket/sort-source-methods -- script ordered as a top-down checker pipeline (discover issues per category → batch fixes → report); alphabetizing would scatter the per-rule check flow. export function generateSuggestions(packages: PackageInfo[]): Suggestion[] { const suggestions: Suggestion[] = [] @@ -1086,6 +1098,7 @@ export function generateSuggestions(packages: PackageInfo[]): Suggestion[] { /** * Displays ML-powered suggestions */ +// oxlint-disable-next-line socket/sort-source-methods -- script ordered as a top-down checker pipeline (discover issues per category → batch fixes → report); alphabetizing would scatter the per-rule check flow. export function displaySuggestions(suggestions: Suggestion[]): void { if (suggestions.length === 0) { log( @@ -1128,6 +1141,7 @@ export function displaySuggestions(suggestions: Suggestion[]): void { /** * Executes fixes for all fixable issues */ +// oxlint-disable-next-line socket/sort-source-methods -- script ordered as a top-down checker pipeline (discover issues per category → batch fixes → report); alphabetizing would scatter the per-rule check flow. export async function executeFixes(): Promise { if (fixableIssues.length === 0) { log('\nNo fixable issues found.', colors.green) @@ -1168,12 +1182,15 @@ export async function executeFixes(): Promise { }) if (!CLI_FLAGS.interactive) { + // oxlint-disable-next-line socket/no-status-emoji -- script uses a local log(msg, color) helper that composes ANSI color with status markers; logger.success/fail would drop the explicit color control needed for the consistency-checker's multi-column report. log(`\n✓ Fixed: ${issue.file}`, colors.green) log(` ${result}`, colors.reset) } else { + // oxlint-disable-next-line socket/no-status-emoji -- script uses a local log(msg, color) helper that composes ANSI color with status markers; logger.success/fail would drop the explicit color control needed for the consistency-checker's multi-column report. log(` ✓ ${result}`, colors.green) } } catch (e) { + // oxlint-disable-next-line socket/no-status-emoji -- script uses a local log(msg, color) helper that composes ANSI color with status markers; logger.success/fail would drop the explicit color control needed for the consistency-checker's multi-column report. log(`\n✗ Failed to fix: ${issue.file}`, colors.red) log(` Error: ${errorMessage(e)}`, colors.red) } @@ -1192,6 +1209,7 @@ export async function executeFixes(): Promise { for (let i = 0, { length } = fixedIssues; i < length; i += 1) { const fixed = fixedIssues[i]! + // oxlint-disable-next-line socket/no-status-emoji -- script uses a local log(msg, color) helper that composes ANSI color with status markers; logger.success/fail would drop the explicit color control needed for the consistency-checker's multi-column report. log(` ✓ ${fixed.file}`, colors.green) } } @@ -1248,6 +1266,7 @@ async function main(): Promise { issues.error.length + issues.warning.length + issues.info.length if (totalIssues === 0) { + // oxlint-disable-next-line socket/no-status-emoji -- script uses a local log(msg, color) helper that composes ANSI color with status markers; logger.success/fail would drop the explicit color control needed for the consistency-checker's multi-column report. log('\n✓ All checks passed!', colors.green) } else { // Group issues by category @@ -1279,6 +1298,7 @@ async function main(): Promise { // Errors // oxlint-disable-next-line socket/prefer-cached-for-loop -- iterable is not a bare identifier (could be Map/Set/Generator/expression) for (const issue of categoryIssues.error) { + // oxlint-disable-next-line socket/no-status-emoji -- script uses a local log(msg, color) helper that composes ANSI color with status markers; logger.success/fail would drop the explicit color control needed for the consistency-checker's multi-column report. log(` ✗ ${issue.file}`, colors.red) log(` ${issue.message}`, colors.red) } @@ -1286,6 +1306,7 @@ async function main(): Promise { // Warnings // oxlint-disable-next-line socket/prefer-cached-for-loop -- iterable is not a bare identifier (could be Map/Set/Generator/expression) for (const issue of categoryIssues.warning) { + // oxlint-disable-next-line socket/no-status-emoji -- script uses a local log(msg, color) helper that composes ANSI color with status markers; logger.success/fail would drop the explicit color control needed for the consistency-checker's multi-column report. log(` ⚠ ${issue.file}`, colors.yellow) log(` ${issue.message}`, colors.yellow) } @@ -1328,6 +1349,7 @@ async function main(): Promise { if (CLI_FLAGS.dryRun) { log('Dry run complete - no changes made', colors.yellow) } else if (fixedIssues.length > 0) { + // oxlint-disable-next-line socket/no-status-emoji -- script uses a local log(msg, color) helper that composes ANSI color with status markers; logger.success/fail would drop the explicit color control needed for the consistency-checker's multi-column report. log(`✓ Fixed ${fixedIssues.length} issue(s)`, colors.green) // Recount remaining issues @@ -1335,6 +1357,7 @@ async function main(): Promise { issues.error.length + issues.warning.length - fixedIssues.length if (remainingIssues > 0) { log( + // oxlint-disable-next-line socket/no-status-emoji -- script uses a local log(msg, color) helper that composes ANSI color with status markers; logger.success/fail would drop the explicit color control needed for the consistency-checker's multi-column report. `⚠ ${remainingIssues} issue(s) still require manual attention`, colors.yellow, ) @@ -1347,13 +1370,17 @@ async function main(): Promise { // were auto-fixed but errors remained. const fixedErrorCount = fixedIssues.filter(f => f.level === 'error').length if (issues.error.length > 0 && fixedErrorCount < issues.error.length) { + // oxlint-disable-next-line socket/no-status-emoji -- script uses a local log(msg, color) helper that composes ANSI color with status markers; logger.success/fail would drop the explicit color control needed for the consistency-checker's multi-column report. log('\n✗ Consistency check failed', colors.red) process.exitCode = 1 } else if (issues.warning.length > 0) { + // oxlint-disable-next-line socket/no-status-emoji -- script uses a local log(msg, color) helper that composes ANSI color with status markers; logger.success/fail would drop the explicit color control needed for the consistency-checker's multi-column report. log('\n⚠ Consistency check passed with warnings', colors.yellow) } else if (totalIssues === 0) { + // oxlint-disable-next-line socket/no-status-emoji -- script uses a local log(msg, color) helper that composes ANSI color with status markers; logger.success/fail would drop the explicit color control needed for the consistency-checker's multi-column report. log('\n✓ Consistency check passed', colors.green) } else { + // oxlint-disable-next-line socket/no-status-emoji -- script uses a local log(msg, color) helper that composes ANSI color with status markers; logger.success/fail would drop the explicit color control needed for the consistency-checker's multi-column report. log('\n✓ All fixable issues resolved', colors.green) } } diff --git a/scripts/check-mirror-docs.mts b/scripts/check-mirror-docs.mts index 02cffa6f8..ea088ad8f 100644 --- a/scripts/check-mirror-docs.mts +++ b/scripts/check-mirror-docs.mts @@ -1,5 +1,4 @@ #!/usr/bin/env node -/* oxlint-disable socket/sort-source-methods -- script ordered as a top-down checker pipeline (discover mirror pairs → diff content → report); alphabetizing would scatter the flow. */ /** * @fileoverview Mirror-doc sync checker. * @@ -149,6 +148,7 @@ type Finding = { fix: string } +// oxlint-disable-next-line socket/sort-source-methods -- script ordered as a top-down checker pipeline (discover mirror pairs → diff content → report); alphabetizing would scatter the flow. export function collectOrphanDocs(): Finding[] { const docs = walk(DOCS_ROOT, r => r.endsWith('.md')) const findings: Finding[] = [] @@ -183,6 +183,7 @@ export function collectOrphanDocs(): Finding[] { return findings } +// oxlint-disable-next-line socket/sort-source-methods -- script ordered as a top-down checker pipeline (discover mirror pairs → diff content → report); alphabetizing would scatter the flow. export function collectMissingDocs(): Finding[] { const findings: Finding[] = [] // Invariant 2 per CLAUDE.md: only PUBLIC `lib/smol-*.js` modules @@ -228,6 +229,7 @@ type Options = { quiet: boolean } +// oxlint-disable-next-line socket/sort-source-methods -- script ordered as a top-down checker pipeline (discover mirror pairs → diff content → report); alphabetizing would scatter the flow. export function printFinding(f: Finding, opts: Options): void { if (opts.json) { logger.log(JSON.stringify(f)) diff --git a/scripts/check-version-consistency.mts b/scripts/check-version-consistency.mts index 7b35c27b3..e751bffc2 100644 --- a/scripts/check-version-consistency.mts +++ b/scripts/check-version-consistency.mts @@ -1,6 +1,5 @@ #!/usr/bin/env node /* max-file-lines: legitimate — top-down checker pipeline with many small section helpers; splitting would scatter the linear flow that makes this script auditable. */ -/* oxlint-disable socket/sort-source-methods -- script ordered as a top-down checker pipeline (load configs → diff versions → report); alphabetizing would scatter the flow. */ /** * @fileoverview External dependency version consistency checker. * @@ -199,6 +198,7 @@ export function loadSubmodules(): Submodule[] { } /** Get the short gitlink commit SHA for a submodule path. */ +// oxlint-disable-next-line socket/sort-source-methods -- script ordered as a top-down checker pipeline (load configs → diff versions → report); alphabetizing would scatter the flow. export async function getSubmoduleSha( subPath: string, ): Promise { @@ -236,6 +236,7 @@ type PackageJsonSource = { ref: string | undefined } +// oxlint-disable-next-line socket/sort-source-methods -- script ordered as a top-down checker pipeline (load configs → diff versions → report); alphabetizing would scatter the flow. export function loadPackageJsonSources(): PackageJsonSource[] { const sources: PackageJsonSource[] = [] const pkgsDir = path.join(MONOREPO_ROOT, 'packages') @@ -310,6 +311,7 @@ type Mismatch = { | 'comment-sha256-format' } +// oxlint-disable-next-line socket/sort-source-methods -- script ordered as a top-down checker pipeline (load configs → diff versions → report); alphabetizing would scatter the flow. export async function collectMismatches(): Promise { const mismatches: Mismatch[] = [] const submodules = loadSubmodules() diff --git a/scripts/test.mts b/scripts/test.mts index 1b1449fc6..97fb76ca4 100644 --- a/scripts/test.mts +++ b/scripts/test.mts @@ -69,7 +69,11 @@ export function preflightCheck(): void { } if (warnings.length > 0) { - logger.info(`\n${warnings.join('\n')}\n`) + logger.error('') + for (let i = 0, { length } = warnings; i < length; i += 1) { + logger.info(warnings[i]) + } + logger.error('') } } diff --git a/scripts/validate-dockerfile-exports.mts b/scripts/validate-dockerfile-exports.mts index 53fa75d49..f655593c1 100644 --- a/scripts/validate-dockerfile-exports.mts +++ b/scripts/validate-dockerfile-exports.mts @@ -129,9 +129,11 @@ async function main(): Promise { return } + logger.error('') logger.fail( - `\nFound ${totalIssues} issue(s) in ${filesWithIssues.length} file(s):\n`, + `Found ${totalIssues} issue(s) in ${filesWithIssues.length} file(s):`, ) + logger.error('') // oxlint-disable-next-line socket/prefer-cached-for-loop -- loop variable is destructured for (const { issues, path: filePath } of filesWithIssues) {