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