Skip to content

fix(embeddings): guard local ONNX runtime on macOS Intel before transformers.js import#1987

Merged
magyargergo merged 4 commits into
mainfrom
fix/1515-macos-intel-embedding-runtime-guard
Jun 3, 2026
Merged

fix(embeddings): guard local ONNX runtime on macOS Intel before transformers.js import#1987
magyargergo merged 4 commits into
mainfrom
fix/1515-macos-intel-embedding-runtime-guard

Conversation

@magyargergo

@magyargergo magyargergo commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator

Problem

gitnexus analyze --embeddings crashes on macOS Intel (darwin/x64) with:

Cannot find module '../bin/napi-v6/darwin/x64/onnxruntime_binding.node'

Both embedders imported @huggingface/transformers at module scope, which loads onnxruntime-node, which immediately resolves the platform-native .node binding. onnxruntime-node@1.24.x ships no darwin/x64 prebuild, so the process dies at import time — before any device/backend selection. That's why ONNX_WEB_BACKEND=wasm / --embedding-device wasm can't help (#1516), and why users previously saw the misleading "installation may be corrupt, reinstall" hint from the CLI's Cannot find module branch.

Closes #1515. Refs #1516.

Fix

Make GitNexus fail gracefully with a clear, GitNexus-authored message before importing transformers.js / onnxruntime-node on unsupported local runtimes.

  • New core/embeddings/runtime-support.tsgetLocalEmbeddingRuntimeBlocker({platform?, arch?}): string | null. Native-/transformers-free so it can be consulted before the import that would crash. Returns a clear blocker on darwin/x64, null elsewhere. The message names macOS Intel, the missing darwin/x64 ONNX native binding, explicitly states ONNX_WEB_BACKEND=wasm does not help, and lists alternatives (run without --embeddings, GITNEXUS_EMBEDDING_URL HTTP provider, Linux/Docker, Apple Silicon, future build). Plus isLocalEmbeddingRuntimeBlockerMessage() for clean CLI surfacing.
  • core/embeddings/embedder.ts + mcp/core/embedder.ts — top-level import {pipeline, env, …}import type {…} (erased at compile time); runtime values now via a guarded lazy await import('@huggingface/transformers') inside the init promise. The guard is thrown in initEmbedder right after the existing HTTP-mode check, before any transformers.js / onnxruntime-node resolution (including the require.resolve in hasOrtCudaProvider, which is path-only and safe).
  • cli/analyze.ts — present the blocker via cliError (clean, actionable, no stack trace) instead of the misleading module-not-found "reinstall" hint. New RecoveryHint tag local-embedding-unsupported.
  • cli/doctor.ts — the Embeddings section now shows a Support: line. On a blocked platform it prints the full guidance (reused from the guard) to stderr, so macOS Intel users see the limitation proactively via gitnexus doctor instead of only when analyze --embeddings fails. doctor stays import-safe (never loads transformers.js/onnxruntime).

Scope note

Both the analyze/CLI embedder (core/embeddings/embedder.ts) and the MCP search embedder (mcp/core/embedder.ts) had the identical top-level import; both are fixed (same npm package). Leaving the MCP one would still crash gitnexus mcp search on macOS Intel.

What this does / doesn't do

  • ✅ Eliminates the native module-load crash on macOS Intel (CLI and MCP paths).
  • ✅ Clear, actionable error instead of a confusing reinstall hint.
  • ✅ HTTP embedding mode (GITNEXUS_EMBEDDING_URL) remains fully usable on macOS Intel.
  • ✅ Supported platforms keep identical local-embedding behavior (model, dims, CUDA/DirectML/CPU fallback, HF endpoint, retry, progress).
  • ⛔ Does not enable local embeddings on macOS Intel — impossible without a shipped darwin/x64 binding; users are routed to working alternatives.

Dependency pinning

Deliberately avoided. The fix is lazy import + explicit guard. onnxruntime-node@^1.24.0 and the transformers→ORT override are unchanged; pinning an older ORT would risk the override strategy and CUDA-on-Linux behavior. No package.json / lockfile changes.

Validation

  • npx tsc --noEmit — clean.
  • New test/unit/embedding-runtime-support.test.ts13/13: guard DI (darwin/x64 blocks; arm64/linux/win32 → null; message content), lazy-import timing (spy proves guard/core/MCP modules don't load transformers, with a positive control), core+MCP initEmbedder darwin/x64 rejection before the lazy import (no raw Cannot find module), and HTTP-mode-not-blocked (no network).
  • Existing embedding tests (79) — pass, no regression.
  • New doctor support-status helper unit-tested (darwin/x64 blocked; arm64/linux/win32 + HTTP supported); verified end-to-end by running the real doctor command on linux and a simulated darwin/x64.
  • npm run test:unit — 7180 pass / 2 skipped; 2 failures are the pre-existing analyze-heap-respawn full-suite-load flake (9/9 in isolation, untouched code).
  • npm run test:cross-platform — 1245 pass / 0 failed.
  • Prettier clean; no bidi/hidden-Unicode controls; CHANGELOG.md untouched.

Not run: full npm test integration lane (expensive native-LadybugDB suite, unrelated to this import-ordering/CLI change). A real darwin/x64 host run isn't available in CI here; the Object.defineProperty(process, …) simulation is the deterministic proxy.

🤖 Generated with Claude Code

…formers.js import

macOS Intel (darwin/x64) crashed on `gitnexus analyze --embeddings` with a raw
`Cannot find module .../bin/napi-v6/darwin/x64/onnxruntime_binding.node`: both
embedders imported @huggingface/transformers at module scope, which loads
onnxruntime-node and resolves the (unshipped) native binding before any backend
could be selected. ONNX_WEB_BACKEND=wasm could not help (#1516).

- Add a native-free runtime-support guard (getLocalEmbeddingRuntimeBlocker) that
  returns a clear, actionable message on darwin/x64 and null elsewhere.
- Convert both the core and MCP embedders to type-only transformers imports plus
  a guarded lazy `await import()`; throw the blocker in initEmbedder before any
  transformers.js / onnxruntime-node resolution. HTTP mode is unaffected.
- Surface the blocker cleanly in the analyze CLI instead of the misleading
  "installation may be corrupt" module-not-found hint.
- Add unit tests: guard DI, lazy-import timing, core+MCP darwin/x64 rejection,
  and HTTP mode not blocked.

Refs #1515, #1516

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
gitnexus Ready Ready Preview, Comment Jun 3, 2026 8:21am

Request Review

`gitnexus doctor` now reports whether the local embedding runtime can load on
the current platform. macOS Intel (darwin/x64) users see up front that local
embeddings are unavailable — plus the recommended alternatives — instead of
only discovering it when `analyze --embeddings` fails (#1515).

The Embeddings section gains a "Support" line; on a blocked platform the full
guidance (reused from getLocalEmbeddingRuntimeBlocker, single source of truth)
is written to stderr. doctor stays import-safe — it never loads transformers.js
or onnxruntime-node, so it runs cleanly on macOS Intel.

Refs #1515

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@magyargergo magyargergo left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Tri-review — PR #1987 (macOS Intel embedding runtime guard)

Verdict: no blocking issues — the fix is correct and merge-safe. Codex (an independent engine) and all six Claude lanes converged on this. That's the strong form of the signal: the one independent engine agrees, not only correlated personas.

Methods (3). GitNexus swarm (risk-architect, test-ci-verifier) + Compound-Engineering personas (correctness, adversarial, maintainability, testing) — all Claude (6 lanes, correlated) — plus Codex (1 lane, independent engine, ran live). Read it accordingly: cross-engine = Codex and a Claude lane; Claude-only = "consistent across personas," not independent confirmation.

What the review validated (credit)

  • Lazy import is complete. A repo-wide sweep found no remaining module-scope import of @huggingface/transformers/onnxruntime-node on any runtime path (analyze→pipeline→embedder, MCP search, doctor); the only two references are import type (erased at compile time). (Codex + adversarial — cross-engine)
  • The guard precedes every native touch. It throws before the singleton state, before isCudaAvailable()/hasOrtCudaProvider()'s require.resolve (which is path-only — no dlopen), and before the lazy await import(). On darwin/x64 nothing reaches the native load. (Codex + correctness + adversarial + risk)
  • No supported-platform regression. Singleton/initPromise error-reset, device fallback, and ORT_LOG_LEVEL ordering are unchanged; HTTP mode (embedText/embedBatch/embedQuery) bypasses the guard. (risk + correctness + Codex)
  • Tests don't false-pass. The lazy-import spy has a positive control; vi.resetModules()+mockClear() prevent mock leakage; platform stubs restore in finally; a re-added top-level import would fail the timing tests. (Codex + testing + adversarial)

Inline findings — both are test-coverage gaps on otherwise-correct code (not bugs)

  1. [P2] analyze.ts:1220 — the new error branch has no dedicated test.
  2. [P2] embedding-runtime-support.test.ts — MCP embedQuery darwin/x64 path untested.

Lower priority (P3)

  • Message wasm-gap (runtime-support.ts:55): the blocker rebuts ONNX_WEB_BACKEND=wasm, but a user who set GITNEXUS_EMBEDDING_DEVICE=wasm (a valid device per config.ts) isn't directly addressed — consider naming that knob too. (adversarial)
  • Tautological test (embedding-runtime-support.test.ts:88): the "defaults platform/arch" assertion is null === null on the CI host and doesn't exercise the process.platform fallback — stub darwin/x64 to make it falsifiable. (testing)
  • Error-branch ordering (analyze.ts): the blocker check sits after isHfDownloadFailure; harmless (no substring overlap) but the explicit platform message could logically precede the network heuristics. (Codex)
  • doctor.ts redundancy: localEmbeddingDoctorStatus re-resolves platform/arch the guard already resolved — cosmetic. (maintainability)

Refuted / out of scope

  • Refuted: concurrency half-set singleton (guard throws before any state mutation); substring false-positive misrouting (lead string is unique — no overlap with HF/WAL patterns); require.resolve triggering a native load (path-only, and unreachable on darwin/x64).
  • Pre-existing, not this PR: MCP semantic search degrades silently to BM25 on darwin/x64 when an index already has embeddings (the catch{} predates this PR — worth a follow-up to surface the blocker once); the darwin/x64 block is a deliberate single-pair allowlist (won't catch a hypothetical future binding-drop on another platform); the analyze.ts error-dispatch chain keeps growing (extract a classifyAnalysisError later).

CI: MERGEABLE; required checks pending (branch-protection BLOCKED at review time). Coverage: full diff read (8 files, +420/-8) — no partial-coverage caveat.

Automated multi-tool digest (GitNexus swarm + Compound-Engineering personas + Codex). Verify before acting.

Comment thread gitnexus/src/cli/analyze.ts Outdated
Comment thread gitnexus/test/unit/embedding-runtime-support.test.ts
@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

CI Report

All checks passed

Pipeline Status

Stage Status Details
✅ Typecheck success tsc --noEmit
✅ Tests success unit tests, 3 platforms
✅ E2E success gitnexus-web changes only

Test Results

Tests Passed Failed Skipped Duration
10982 10969 0 13 674s

✅ All 10969 tests passed

13 test(s) skipped — expand for details
  • COBOL pipeline benchmark > scales with file count
  • C# pipeline benchmark > scales with file count — namespaces spread across the solution
  • C# pipeline benchmark > scales with file count — all types in one (global) namespace bucket
  • C# pipeline benchmark > scales with file count — all types in one (named) namespace bucket
  • Go pipeline benchmark > scales with file count (workers enabled)
  • Go pipeline benchmark — worker pool (issue Worker idle timeout kills long Go scope extraction and surfaces as Napi::Error during analyze #1848) > does not quarantine the large generated Go file on sub-batch idle timeout
  • Go structural interface detection benchmark > scales linearly with interface × struct count
  • Go structural interface detection split-phase benchmark > separates index-build and detection time
  • PHP pipeline benchmark > scales with file count (workers enabled)
  • Ruby pipeline benchmark > scales with file count (workers enabled)
  • Rust pipeline benchmark > scales with file count (workers enabled)
  • run.cjs direct-exec entrypoint (fix(cli): steer docs, skills, and hooks through a CLI-neutral project-local runner (#1939) #1945) > resolves a .cmd shim via the Windows shell branch, passing args and exit code
  • buildTypeEnv > known limitations (documented skip tests) > Ruby block parameter: users.each { |user| } — closure param inference, different feature

Code Coverage

Tests

Metric Coverage Covered Base Delta Status
Statements 80.31% 38275/47658 79.84% 📈 +0.5 🟢 ████████████████░░░░
Branches 68.87% 24344/35345 68.5% 📈 +0.4 🟢 █████████████░░░░░░░
Functions 85.5% 3983/4658 84.94% 📈 +0.6 🟢 █████████████████░░░
Lines 83.91% 34433/41033 83.36% 📈 +0.5 🟢 ████████████████░░░░

📋 View full run · Generated by CI

…olish

Resolves the maintainer tri-review feedback on PR #1987:

- Add the analyze error-branch test (new analyze-local-embedding-error.test.ts):
  a darwin/x64 blocker routes to the clean local-embedding-unsupported message
  (exit 1), not the module-not-found "installation may be corrupt" branch, and
  wins over isHfDownloadFailure even when both match (guards the reorder below).
- Cover the MCP embedQuery darwin/x64 paths — HTTP bypass via httpEmbedQuery
  without importing transformers, and local-mode rejection before the import.
- Make the "defaults platform/arch" guard test falsifiable by stubbing the
  platform, instead of asserting null === null on the CI host.
- analyze.ts: evaluate the blocker-message branch before the network-heuristic
  isHfDownloadFailure branch so the explicit platform message takes priority.
- runtime-support.ts: the blocker message now also notes GITNEXUS_EMBEDDING_DEVICE
  =wasm/cpu cannot help, not only ONNX_WEB_BACKEND=wasm.
- doctor.ts: resolve platform/arch once instead of re-resolving after the guard.

Refs #1515, #1516

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@magyargergo magyargergo merged commit fca3494 into main Jun 3, 2026
31 checks passed
@magyargergo magyargergo deleted the fix/1515-macos-intel-embedding-runtime-guard branch June 3, 2026 08:46
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.

GitNexus fails on macOS Intel (x86_64) with 'Cannot find module ../bin/napi-v6/darwin/x64/onnxruntime_binding.node'

1 participant