Skip to content

relayburn-sdk-node: shape conformance with TS @relayburn/sdk@1.x (#247c)#354

Merged
willwashburn merged 1 commit into
mainfrom
247c-shape-conformance
May 7, 2026
Merged

relayburn-sdk-node: shape conformance with TS @relayburn/sdk@1.x (#247c)#354
willwashburn merged 1 commit into
mainfrom
247c-shape-conformance

Conversation

@willwashburn

Copy link
Copy Markdown
Member

Summary

PR α of the burn 2.0 cutover (epic #240, blocking #249). Brings the napi-rs umbrella @relayburn/sdk@2.x (binding crate at crates/relayburn-sdk-node + facade at packages/sdk-node/src/) into shape-parity with the TS 1.x SDK at packages/sdk/index.d.ts, modulo the already-documented number | bigint widening on u64 token-count fields.

Punch-list

  1. Ledger class — JS-side wrapper class in src/index.{js,cjs} that awaits binding.ledgerOpen() and stashes the resolved home. No #[napi] class on the Rust side (would have forced Mutex<LedgerHandle> plumbing for no benefit). index.d.ts declares class Ledger { readonly home: string; static open(opts?: LedgerOpenOptions): Promise<Ledger> }.
  2. totalUSD casing#[napi(js_name = "totalUSD")] override on SessionCostResult.total_usd. Verified end-to-end against a freshly built .node: typeof sc.totalUSD === 'number', no totalUsd key.
  3. onLog?: (msg) => void on every read verb's options — declared in TS at index.d.ts (already present from prior commits). The napi boundary silently drops it — #[napi(object)]'s from_napi_value codegen reads only declared fields, so extra props (like a JS function) ride through without a TypeError. Forward-compat hook for when relayburn-sdk grows fallback-log threading; relayburn-sdk does not currently expose an OnLogCallback type for read verbs.
  4. IngestOptions shape{ sessionId, harness, ledgerHome } matches TS 1.x byte-for-byte. The binding routes to sdk::IngestRoots::default() and ignores sessionId / harness (TS 1.x's ingest() likewise routes to ingestAll() without filtering — confirmed at packages/sdk/index.js:127-129). Drops the IngestRoots napi struct.
  5. Read verbs return Promise<T> — every read verb is re-exported as an async function in the JS facade. The Rust side stays sync (returning Result<T, BurnError>) so the typed BurnErrorCode on e.code survives the wrapper. Verified: await m.compare({ minFidelity: 'bogus' }) rejects with e.code === BurnErrorCode.InvalidArgument. ingest's return type spelled Result<IngestReport, NapiError> so napi-derive's syntactic Result detection emits Promise<IngestReport> instead of treating the NapiResult alias as the literal return.
  6. top and minSample wideningOption<u32> instead of Option<BigInt>. Both are small counts (top is a recommendation cap, minSample is a turn-count threshold) and TS types them as number; u32 -> u64 is lossless on the way into the SDK.
  7. Re-export list — adds search, exportLedger, exportStamps, BurnErrorCode, OverheadFileKind, HotspotsGroupBy as 2.x extensions to index.{js,cjs,d.ts}. Ledger re-exported via the wrapper class; ledgerOpen is internal-only.

Out of scope

  • tests/fixtures/ledger/ seeding + flipping RELAYBURN_SDK_NAPI_BUILT=1 — PR β.
  • binding.cjs loader-path bug (the regenerated dispatcher looks for index.<target>.node next to __dirname = src/, but napi build drops the file at packages/sdk-node/ root) — also PR β.
  • .github/workflows/napi-build.yml — PR γ.

Test plan

  • cargo build -p relayburn-sdk-node --release clean.
  • cargo test -p relayburn-sdk-node — 10 tests pass (no regressions in the BIGINT_FIELDS membership / runtime invariant tests).
  • cd packages/sdk-node && pnpm run build:napi:debug clean. Regenerated binding.d.ts shows Promise<IngestReport>, totalUSD: number, top?: number, minSample?: number, and the new IngestHarness enum.
  • node --test test/esbuild-smoke.test.js clean (esbuild bundles the umbrella facade with all the new exports).
  • cargo build --workspace clean.
  • pnpm -r run build clean.
  • End-to-end smoke against a real .node: Ledger.open() returns a Promise, summary()'s totalTokens is bigint, sessionCost()'s totalUSD is correctly cased, typed e.code survives the async wrapper, onLog callbacks are silently accepted.

Notes for PR β / γ

  • The regenerated binding.cjs (which I left out of the diff per instructions) destructures the napi exports at the top: const { BurnErrorCode, summary, sessionCost, ..., IngestHarness, ingest, ledgerOpen } = nativeBinding. The hand-written stub on main does module.exports = nativeBinding; and works for both shapes — so reverting binding.cjs in this PR doesn't break the umbrella facade. The CI matrix's regen overwrites it.
  • The loader-path bug really does bite locally: napi-rs drops index.darwin-arm64.node at the package root (next to package.json), but the regenerated dispatcher does existsSync(join(__dirname, 'index.darwin-arm64.node')) where __dirname is src/. Worked around for this PR by copying the artifact into src/ for the smoke test; PR β should align the build output and the loader.

Refs #247.

Bring the napi-rs umbrella in line with packages/sdk/index.d.ts so
@relayburn/sdk@2.0 is a drop-in replacement for the 1.x TS SDK
modulo the documented number|bigint widening for u64 fields.

- SessionCostResult.totalUSD: pin js_name="totalUSD" so napi-rs
  doesn't camelCase it to totalUsd.
- IngestOptions: { sessionId, harness, ledgerHome } matches TS 1.x
  byte-for-byte; the binding routes to sdk::IngestRoots::default().
  Drops the IngestRoots napi struct (callers needing root overrides
  can drive the SDK directly until a typed escape hatch lands).
- top / minSample: widen from BigInt to plain u32. Both are small
  counts; TS source-of-truth types them as number.
- ingest() return type: spell `Result<IngestReport, NapiError>` so
  napi-derive emits Promise<IngestReport> instead of treating the
  NapiResult alias as the literal return type.
- IngestHarness: new string_enum so callers branch on the canonical
  wire values without stringly-typed literals.
- Umbrella facade (src/index.{js,cjs,d.ts}):
  - Ledger as a JS-side class wrapping binding.ledgerOpen() so the
    1.x Ledger.open() -> Promise<Ledger> signature lands without
    forcing Mutex<LedgerHandle> plumbing on the Rust side.
  - All read verbs re-exported as `async`, returning Promise<T> while
    preserving the typed BurnErrorCode on e.code (verified end-to-end
    via a real .node load with InvalidArgument).
  - Adds 2.x extensions: search, exportLedger, exportStamps,
    BurnErrorCode, OverheadFileKind, HotspotsGroupBy.
  - Carries onLog?: (msg: string) => void on every read verb's
    options. Currently dropped at the napi boundary (extra props on a
    #[napi(object)] are ignored) — a forward-compat hook for when the
    SDK grows the fallback-logging callback the TS SDK currently uses.

cargo build -p relayburn-sdk-node --release, cargo test -p
relayburn-sdk-node, pnpm run build:napi:debug, esbuild smoke test,
cargo build --workspace, and pnpm -r run build all clean. End-to-end
load test against a freshly built .node confirms typed e.code and
totalUSD casing survive the async wrapper.

Out of scope (other PRs):
- tests/fixtures/ledger seeding + RELAYBURN_SDK_NAPI_BUILT=1 flip
  (PR β / #247-b).
- napi-build CI workflow (PR γ).
- binding.cjs loader path (.node lands at packages/sdk-node/ root,
  loader looks under src/) — PR β.

Refs #247.
@coderabbitai

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Node SDK 2.x surface is updated to align with TypeScript 1.x contracts by restructuring N-API bindings for ingest options, wrapping async operations in both ESM and CommonJS facades, extending type definitions with new search/export APIs, and adjusting field types for numeric option parameters.

Changes

Node SDK 2.x API Surface Alignment

Layer / File(s) Summary
N-API Bindings
crates/relayburn-sdk-node/src/lib.rs
SessionCostResult.total_usd adds JS name totalUSD; OverheadTrimOptions.top and CompareOptions.min_sample change from Option<BigInt> to Option<u32> with u64 conversion; new IngestHarness enum exported; IngestOptions restructured to add optional session_id and harness, remove IngestRoots, and binding discards them before SDK call; ingest return type changes to Result<IngestReport, NapiError>.
TypeScript Type Definitions
packages/sdk-node/src/index.d.ts
LedgerOpenOptions adds contentHome?: string; Ledger class exposes home: string property; ingest return type narrows from Promise<unknown> to Promise<IngestReport> with number | bigint fields; new search, exportLedger, exportStamps functions declared; new enums BurnErrorCode, OverheadFileKind, HotspotsGroupBy added as readonly constants.
ESM Facade
packages/sdk-node/src/index.js
New Ledger wrapper class with static async open(opts) calling binding's ledgerOpen and returning Promise<Ledger>; existing query functions (ingest, summary, sessionCost, overhead, overheadTrim, hotspots, compare) converted to async wrappers; new async exports (search, exportLedger, exportStamps); binding enums re-exported as constants.
CommonJS Facade
packages/sdk-node/src/index.cjs
Parallel changes to ESM facade: new Ledger wrapper class with static async open, existing functions converted to async arrow wrappers delegating to bindings, structure mirrors ESM surface.
Documentation
packages/sdk-node/CHANGELOG.md
Unreleased section documents 2.x surface conformance with 1.x TS SDK, noting async returns, IngestOptions shape with sessionId and harness, callback handling, and new 2.x extensions (search, exports, enums).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related issues

Possibly related PRs

  • AgentWorkforce/burn#302: Modifies the ingest API surface with IngestOptions and free async ingest function changes in the core SDK, paralleling the Node bindings' ingest restructure.
  • AgentWorkforce/burn#306: Introduces the napi-rs bindings skeleton in crates/relayburn-sdk-node/src/lib.rs; this PR directly extends and refines those bindings' exported structs, enums, and async signatures.

Poem

🐰✨ A hop toward harmony!

From Rust to JS, our bindings dance,
Async wrappers take their chance,
Types align, facades gleam bright—
Two-point-oh shines TypeScript light! 🌟

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: aligning the napi-rs Node SDK bindings with the TypeScript 1.x SDK shape/API surface.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, detailing the specific shape-conformance updates, punch-list items, test verification, and out-of-scope follow-ups.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 247c-shape-conformance

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/sdk-node/CHANGELOG.md (1)

10-18: ⚡ Quick win

Trim this Unreleased entry to the user-visible delta.

This bullet currently mixes release notes, internal behavior notes, and issue bookkeeping. Please keep it to the surfaced API changes and drop details like the napi-boundary note and (#247 part c).

✂️ Suggested wording
-- Shape conformance with TS `@relayburn/sdk@1.x`: `Ledger.open()` returns
-  a `Promise<Ledger>` instance, `sessionCost()` emits `totalUSD`
-  (screaming USD), every read verb is `async` (`Promise<T>`),
-  `IngestOptions` is `{ sessionId, harness, ledgerHome }`, `top` and
-  `minSample` accept plain `number`, and `onLog` callbacks are accepted
-  on every read verb's options (silently dropped at the napi boundary
-  until the SDK wires fallback logging). Adds `search`, `exportLedger`,
-  `exportStamps`, `BurnErrorCode`, `OverheadFileKind`, and
-  `HotspotsGroupBy` as 2.x extensions over the 1.x surface. (`#247` part c)
+- Node SDK APIs now match the TS 1.x surface more closely: `Ledger.open()`
+  returns `Promise<Ledger>`, `sessionCost()` exposes `totalUSD`, read verbs
+  are async, `ingest()` accepts `{ sessionId, harness, ledgerHome }`, and
+  `overheadTrim()` / `compare()` accept numeric `top` / `minSample`.
+- Added Node SDK exports for `search()`, `exportLedger()`, `exportStamps()`,
+  `BurnErrorCode`, `OverheadFileKind`, and `HotspotsGroupBy`.

As per coding guidelines, "Curate [Unreleased] sections in packages/*/CHANGELOG.md as PRs land with concise, impact-first entries: name the command/API/schema touched and practical effect; drop issue/PR links, internal notes, backstory, and 'foundation for...' phrasing".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/sdk-node/CHANGELOG.md` around lines 10 - 18, Trim the Unreleased
bullet to only the user-visible API changes: list the surfaced signatures and
behavior changes (e.g., Ledger.open() now returns Promise<Ledger>, sessionCost()
emits totalUSD, all read verbs return Promise<T>, IngestOptions shape is {
sessionId, harness, ledgerHome }, top and minSample accept plain number,
read-verb options accept onLog callbacks) and the new API additions (search,
exportLedger, exportStamps, BurnErrorCode, OverheadFileKind, HotspotsGroupBy);
remove internal/backstory text such as the napi-boundary note, “screaming USD”,
and the "(`#247` part c)" bookkeeping reference so the entry is concise and
user-facing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/sdk-node/CHANGELOG.md`:
- Around line 10-18: Trim the Unreleased bullet to only the user-visible API
changes: list the surfaced signatures and behavior changes (e.g., Ledger.open()
now returns Promise<Ledger>, sessionCost() emits totalUSD, all read verbs return
Promise<T>, IngestOptions shape is { sessionId, harness, ledgerHome }, top and
minSample accept plain number, read-verb options accept onLog callbacks) and the
new API additions (search, exportLedger, exportStamps, BurnErrorCode,
OverheadFileKind, HotspotsGroupBy); remove internal/backstory text such as the
napi-boundary note, “screaming USD”, and the "(`#247` part c)" bookkeeping
reference so the entry is concise and user-facing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 76e38dca-284a-4405-b376-88adefe2c564

📥 Commits

Reviewing files that changed from the base of the PR and between cf68d8b and 5ccb4a0.

📒 Files selected for processing (5)
  • crates/relayburn-sdk-node/src/lib.rs
  • packages/sdk-node/CHANGELOG.md
  • packages/sdk-node/src/index.cjs
  • packages/sdk-node/src/index.d.ts
  • packages/sdk-node/src/index.js

@willwashburn willwashburn merged commit f32ee6b into main May 7, 2026
8 checks passed
willwashburn added a commit that referenced this pull request May 7, 2026
napi-rs serializes Rust u64/i64 as JS BigInt, but the TS 1.x
@relayburn/sdk shape (mirrored in src/index.d.ts) emits plain Number for
the same fields. PR alpha (#354) made the napi binding shape-conformant;
PR beta (#355) flipped the conformance gate in CI and surfaced 6/7 verbs
failing because of this BigInt vs Number wire-shape gap (and a separate
JSONL->SQLite read-side gap fixed by a sibling PR).

Add a coerceBigInts(value) helper that recursively walks a verb's return
value and downcasts BigInt to Number when the value fits in
[Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]; values outside that
range stay BigInt to avoid silent precision loss. Wrap each verb's
return in both the ESM facade (src/index.js) and the CJS mirror
(src/index.cjs): summary, sessionCost, overhead, overheadTrim, hotspots,
compare, ingest, search, exportLedger, exportStamps. The TS 1.x types
already declare number | bigint where this matters, so this is a
runtime-shape fix rather than a type-surface change.

Local conformance probe (cherry-picking beta's test against this fix):
3/7 verbs now pass (overhead, overheadTrim, ingest) vs the 1/7 beta
reported pre-fix (overhead). The remaining 4 failures are the
JSONL->SQLite read-side gap, which the sibling alpha-followup addresses.

Refs #247, #354, #355.
willwashburn added a commit that referenced this pull request May 7, 2026
#357)

The Rust SDK reads exclusively from `burn.sqlite` but never auto-built
that mirror from a `ledger.jsonl` sibling. Freshly-ingested or JSONL-only
ledgers (the cli-golden fixture, side-by-side TS/Rust tooling, users
upgrading from 1.x) returned empty rows on read until something else
populated the sqlite. The TS @relayburn/sdk@1.x didn't have this problem
because it treats sqlite as a derived view rebuilt on demand.

Lift the bootstrap algorithm from `tests/golden.rs::bootstrap_sqlite_from_jsonl`
into the production SDK so reads always see the latest data.

Algorithm — Option A, eager on `Ledger::open`. We snapshot the JSONL-vs-
sqlite mtime BEFORE `Connection::open` creates `burn.sqlite` as a side
effect (otherwise every fresh sqlite would look "current" relative to the
JSONL and we'd skip the rebuild). If the JSONL is newer (or the sqlite is
missing), wipe the derivable tables and replay the JSONL via the existing
`writer::append_*` paths. Stamps + archive_state are first-party and
preserved.

Concurrency: SQLite WAL plus the configured `busy_timeout` serialize
peer writers without a user-space lockfile — same design choice that
let us drop `lock.ts` from the Rust port (see #259). Two concurrent
opens both observing a stale sqlite would each attempt a rebuild; the
second sees an already-warm sqlite and skips.

Side effect: the cli-golden test helper no longer needs its own JSONL
parser. Replace `bootstrap_sqlite_from_jsonl` (~150 lines of duplicated
parse/replay logic) with a 30-line `reset_sqlite_for_fresh_bootstrap`
that just deletes any prior sqlite so the SDK does the rebuild on the
binary's first `Ledger::open`.

Followup to PRs #354 (napi shape conformance) and #355 (conformance gate
flip) which surfaced this gap. Verified manually against the cli-golden
fixture: `RELAYBURN_HOME=/tmp/probe-ledger RELAYBURN_ARCHIVE=0 burn summary`
on a JSONL-only ledger now returns 7 turns instead of 0.
@willwashburn willwashburn deleted the 247c-shape-conformance branch May 7, 2026 06:32
willwashburn added a commit that referenced this pull request May 7, 2026
…/sdk@1.x (#358)

The Rust SDK's `compare()` was pre-filtering the turn list by `opts.models`
*before* computing `analyzedTurns` and the fidelity summary. The TS contract
in `packages/sdk/index.js::compare()` does the opposite: `analyzedTurns =
filteredTurns.length` is taken AFTER the fidelity gate but BEFORE the
model allow-list, which is honored inside `buildCompareTable` (which also
pre-seeds requested-but-absent models as all-empty columns).

Net effect on the conformance fixture (`tests/fixtures/cli-golden`,
seven turns spanning sonnet-4-6 / haiku-4-5 / gpt-5-codex / sonnet-4-6):
calling `compare({ models: ['claude-sonnet-4-5', 'claude-opus-4-7'],
minFidelity: 'partial' })` — neither requested model is present in the
fixture — yielded `analyzedTurns: 0` and an all-zero fidelity summary on
the Rust side (every turn dropped at the early model filter), versus
`analyzedTurns: 7` plus a populated `byClass` / `byGranularity` /
`missingCoverage` block on TS. The conformance gate at
`packages/sdk-node/test/conformance.test.js` reduced this to a
`deepStrictEqual` failure on the only verb that hits this code path.

Fix: drop the early `requested_models` `retain` from `LedgerHandle::compare`.
Provider filtering and fidelity summarization now run on the full slice the
ledger query returned, matching the TS path; cell construction still
honors the model allow-list via `AnalyzeCompareOptions::models`. The
unused `compare_model_id` helper is removed.

Test deltas:
 - `compare_metadata_counts_requested_models_only` was asserting the buggy
   behavior. Renamed to `compare_metadata_counts_all_matched_turns_pre_models_filter`
   and updated to the TS-parity expectations: `analyzed_turns == 3` /
   `summary.total == 3` for a 3-turn fixture even when the requested models
   only match 2 of them.
 - New `compare_reports_full_fidelity_summary_when_no_requested_model_appears`
   regression covering the exact conformance scenario (request two models
   that are absent from the ledger; metadata still describes the slice).

Refs #240 (rust-port epic). Follows #354/#356/#357 and unblocks #355
(α-followup conformance gate). Local conformance now 7/7 green
(summary, sessionCost, overhead, overheadTrim, hotspots, compare, ingest).
willwashburn added a commit that referenced this pull request May 7, 2026
* relayburn-sdk-node: route napi build outputs into src/ so loader resolves

`napi build --js src/binding.cjs --dts src/binding.d.ts` regenerated the
loader at `packages/sdk-node/src/binding.cjs` but emitted the actual
`.node` artifact at `packages/sdk-node/index.<target>.node` (package
root). The generated dispatcher checks `existsSync(join(__dirname,
'index.<target>.node'))` with `__dirname = packages/sdk-node/src/`, so
the local-file branch never matched and it fell through to
`require('@relayburn/sdk-darwin-arm64')` — not installed in dev,
producing "native binding not found".

Pass `src` as the positional `[destDir]` argument with `--js
binding.cjs --dts binding.d.ts` (relative to destDir) so all three
outputs land in `src/` next to `index.js`. Update the CI artifact
upload glob and `.gitignore` to match the new path.

* relayburn-sdk-node: seed conformance fixture from cli-golden builder

The conformance suite required `tests/fixtures/ledger/` to exist with a
canonical `ledger.jsonl` + `content/` sidecar, but no such fixture was
ever committed (the file's header referenced a `prepare-fixture-ledger`
CI step that didn't land).

Rather than commit a binary-ish snapshot that drifts silently, drive the
suite from `tests/fixtures/cli-golden/scripts/build-ledger.mjs` — the
hand-curated, byte-deterministic builder already maintained by the
cli-golden tests. Each conformance run spawns the builder once into a
tmp dir, then `cpSync`s the produced ledger into per-impl tmp homes so
TS and napi reads see identical state. Keeps conformance self-contained
and in lock-step with the same fixture cli-golden owns.

Ingest is intentionally read-only (HOME pinned at an empty tmp tree on
both sides) so the verb returns the trivial empty report rather than
scanning the runner's real `~/.claude/projects/`. Deep-corpus ingest
conformance is tracked as an α follow-up.

* relayburn-sdk-node: flip RELAYBURN_SDK_NAPI_BUILT=1 to gate the matrix

With α (#354) shape-conformant and the loader + fixture seeding wired
up, the conformance suite can finally run for real. Flip the gate from
'0' to '1' so `node --test test/conformance.test.js` does a
`deepStrictEqual` check across all 7 verbs against TS
`@relayburn/sdk@1.x` instead of skipping itself.

Also adds a `pnpm run build` step before the conformance run — the TS
1.x SDK imports `@relayburn/{ledger,analyze,ingest,reader}`, which ship
as `dist/` and need a `tsc --build` pass.

Closes the β slice of #247; α follow-ups still required for the
BigInt-vs-Number divergences and the Rust SDK's JSONL→SQLite replay
gap.

* relayburn-sdk-node: hoist @relayburn/* so cli-golden seeder resolves

The conformance fixture seeder (`tests/fixtures/cli-golden/scripts/build-ledger.mjs`)
imports `@relayburn/ledger` directly. Node ESM resolution walks up
`node_modules/` from the script's location, but pnpm's default linking
puts `@relayburn/ledger` under each consumer's `packages/<pkg>/node_modules/`,
not the workspace root. So the seeder fails with `ERR_MODULE_NOT_FOUND`
in any clean checkout — including all four legs of `napi-build.yml`,
where it broke every conformance run before the deepStrictEqual phase.

Fix: add `.npmrc` with `public-hoist-pattern[]=@relayburn/*` so pnpm
hoists workspace siblings into the root `node_modules/`. The seeder's
import now resolves, conformance proceeds to actual shape diffs (the
remaining BigInt + JSONL→SQLite divergences are tracked as α follow-ups).
Also unbreaks `pnpm run golden:capture`, which had the same latent bug.

* napi-build: skip conformance step on aarch64-linux cross-compile leg

The aarch64-unknown-linux-gnu matrix leg cross-compiles on an x64 host
(`runs-on: ubuntu-latest`), so the only `.node` artifact present at the
end of the build step is the arm64 binary. The conformance step's
`node --test` then runs under the runner's x64 interpreter, which
cannot load the arm64 `.node` — `binding.cjs` resolves the binding by
`process.arch`, so it tries `index.linux-x64-gnu.node` (or the matching
`@relayburn/sdk-linux-x64-gnu` optionalDependency, which isn't installed
in dev) and crashes the import before the test body's `t.skip` can fire.
Result: every conformance test on this leg surfaces as a `MODULE_NOT_FOUND`
test failure instead of a skip.

Gate the "Build TS workspace" + "Conformance test" steps off this leg
via `if: matrix.target != 'aarch64-unknown-linux-gnu'`. The cross-compile
leg's job remains valuable — it validates the cross-build + artifact
upload — but the conformance contract is already exercised by the three
native legs (x64-darwin, arm64-darwin, x64-linux) that run on hosts
matching their target arch.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant