@soulcraft/cor 3.0.7 → 3.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,11 @@
8
8
  * Exports the unified NativeBindings interface that all consumers use.
9
9
  */
10
10
  import type { NativeBindings } from './types.js';
11
+ /**
12
+ * Diagnostic accessor for the native-module load failure. `null` when the
13
+ * module loaded successfully or no load has been attempted yet.
14
+ */
15
+ export declare function getNativeLoadError(): Error | null;
11
16
  /**
12
17
  * Open-file-limit raise result, captured once when the native module
13
18
  * first loads. `null` until then (or if the platform/loader doesn't
@@ -39,7 +44,10 @@ export declare function getOpenFileLimitInfo(): OpenFileLimitInfo | null;
39
44
  export declare function loadNativeModule(): NativeBindings;
40
45
  /**
41
46
  * Check if the native module is available.
42
- * Does not throw - returns false if loading fails.
47
+ * Does not throw returns false if loading fails, and logs the underlying
48
+ * load error ONCE per process. cor exists to accelerate; a native module
49
+ * that fails to load must never be a silent no (the dlopen cause — e.g. a
50
+ * glibc version mismatch — is the only actionable diagnostic).
43
51
  */
44
52
  export declare function isNativeAvailable(): boolean;
45
53
  export type { NativeBindings, NativeEmbeddingEngineInstance, NativeRoaringBitmap32Instance, EmbeddingResult, EngineStats, DeviceInfo, } from './types.js';
@@ -11,6 +11,25 @@ import { loadViaNapi } from './napi.js';
11
11
  import { loadViaFFI } from './ffi.js';
12
12
  // Cached module reference
13
13
  let cachedModule = null;
14
+ /**
15
+ * The error from the most recent failed native-module load, or `null` if the
16
+ * module loaded (or was never attempted). Carries the underlying dlopen
17
+ * message — e.g. a glibc version mismatch — which is the only actionable
18
+ * diagnostic when native acceleration silently fails to engage
19
+ * (ACC-COR-SILENT-NATIVE-FAIL).
20
+ */
21
+ let lastLoadError = null;
22
+ /** One-shot latch so the load-failure warning prints once per process, not
23
+ * once per {@link isNativeAvailable} probe (benchmarks and test gates call
24
+ * it repeatedly). */
25
+ let loadFailureWarned = false;
26
+ /**
27
+ * Diagnostic accessor for the native-module load failure. `null` when the
28
+ * module loaded successfully or no load has been attempted yet.
29
+ */
30
+ export function getNativeLoadError() {
31
+ return lastLoadError;
32
+ }
14
33
  let fileLimitInfo = null;
15
34
  /**
16
35
  * Raise the process's soft open-file limit toward its hard cap, once.
@@ -56,6 +75,7 @@ export function loadNativeModule() {
56
75
  try {
57
76
  cachedModule = loadViaFFI();
58
77
  raiseFileLimitOnce(cachedModule);
78
+ lastLoadError = null;
59
79
  return cachedModule;
60
80
  }
61
81
  catch {
@@ -63,20 +83,35 @@ export function loadNativeModule() {
63
83
  }
64
84
  }
65
85
  // Strategy 2: All platforms (napi-rs)
66
- cachedModule = loadViaNapi();
86
+ try {
87
+ cachedModule = loadViaNapi();
88
+ }
89
+ catch (err) {
90
+ lastLoadError = err instanceof Error ? err : new Error(String(err));
91
+ throw err;
92
+ }
67
93
  raiseFileLimitOnce(cachedModule);
94
+ lastLoadError = null;
68
95
  return cachedModule;
69
96
  }
70
97
  /**
71
98
  * Check if the native module is available.
72
- * Does not throw - returns false if loading fails.
99
+ * Does not throw returns false if loading fails, and logs the underlying
100
+ * load error ONCE per process. cor exists to accelerate; a native module
101
+ * that fails to load must never be a silent no (the dlopen cause — e.g. a
102
+ * glibc version mismatch — is the only actionable diagnostic).
73
103
  */
74
104
  export function isNativeAvailable() {
75
105
  try {
76
106
  loadNativeModule();
77
107
  return true;
78
108
  }
79
- catch {
109
+ catch (err) {
110
+ if (!loadFailureWarned) {
111
+ loadFailureWarned = true;
112
+ console.warn(`[cor] native module failed to load — acceleration unavailable: ` +
113
+ `${err instanceof Error ? err.message : String(err)}`);
114
+ }
80
115
  return false;
81
116
  }
82
117
  }
@@ -46,30 +46,33 @@ export function loadViaNapi() {
46
46
  // This file lives at dist/native/napi.js → package root is ../../
47
47
  const __dirname = dirname(fileURLToPath(import.meta.url));
48
48
  const nativeDir = join(__dirname, '..', '..', 'native');
49
+ // Each strategy's real failure is captured and carried in the final throw:
50
+ // the underlying dlopen error (e.g. "GLIBC_2.39 not found" on an older
51
+ // distro) is the ONLY actionable diagnostic, and swallowing it produced
52
+ // native-silently-off deployments (ACC-COR-SILENT-NATIVE-FAIL).
53
+ const failures = [];
49
54
  // Strategy 1: Bundled platform binary (shipped in npm package)
55
+ const binaryPath = join(nativeDir, `brainy-native.${platformKey}.node`);
50
56
  try {
51
- const binaryPath = join(nativeDir, `brainy-native.${platformKey}.node`);
52
57
  const bindings = require(binaryPath);
53
58
  cachedBindings = bindings;
54
59
  return bindings;
55
60
  }
56
- catch {
57
- // Fall through to strategy 2
61
+ catch (err) {
62
+ failures.push(` - ${binaryPath}: ${err instanceof Error ? err.message : String(err)}`);
58
63
  }
59
64
  // Strategy 2: Local dev build (no platform suffix)
65
+ const devPath = join(nativeDir, 'brainy-native.node');
60
66
  try {
61
- const devPath = join(nativeDir, 'brainy-native.node');
62
67
  const bindings = require(devPath);
63
68
  cachedBindings = bindings;
64
69
  return bindings;
65
70
  }
66
- catch {
67
- // Fall through to error
71
+ catch (err) {
72
+ failures.push(` - ${devPath}: ${err instanceof Error ? err.message : String(err)}`);
68
73
  }
69
- throw new Error(`Failed to load Brainy native module for ${key}.\n` +
70
- `Looked for:\n` +
71
- ` - native/brainy-native.${platformKey}.node (bundled binary)\n` +
72
- ` - native/brainy-native.node (local dev build)\n` +
74
+ throw new Error(`Failed to load Brainy native module for ${key}:\n` +
75
+ `${failures.join('\n')}\n` +
73
76
  `Build locally: cd native && npm run build`);
74
77
  }
75
78
  /**
package/dist/plugin.js CHANGED
@@ -42,12 +42,27 @@
42
42
  * with a single code path (in-memory / hybrid / on-disk modes
43
43
  * selected at brain-open time from observed memory).
44
44
  */
45
- import { loadNativeModule, isNativeAvailable } from './native/index.js';
45
+ import { loadNativeModule, isNativeAvailable, getNativeLoadError } from './native/index.js';
46
46
  import { validateLicense, readLicenseToken, readConfig, isShortCode } from './license.js';
47
47
  import { MigrationCoordinator } from './migration/MigrationCoordinator.js';
48
48
  import { prodLog } from '@soulcraft/brainy/internals';
49
49
  import { setEntityCountSource } from './license/onlineValidator.js';
50
50
  const PRICING_URL = 'https://soulcraft.com/pricing?focus=pro';
51
+ /**
52
+ * Resolve the RAW configured license value with the env-then-file precedence
53
+ * `readLicenseToken()` uses (env `COR_LICENSE_KEY` / legacy `CORTEX_LICENSE_KEY`
54
+ * wins over `.soulcraft.json`, dual-reading the new `cor` key and the legacy
55
+ * `cortex` key), WITHOUT exchanging short codes — callers need the raw shape
56
+ * (is anything configured at all? is it a short code?). `undefined` when no
57
+ * license is configured anywhere.
58
+ */
59
+ async function resolveRawLicenseValue() {
60
+ const envValue = (process.env.COR_LICENSE_KEY ?? process.env.CORTEX_LICENSE_KEY)?.trim();
61
+ if (envValue)
62
+ return envValue;
63
+ const config = await readConfig();
64
+ return config.cor ?? config.cortex;
65
+ }
51
66
  // `brainyRange` engages brainy 8.0's version-coupling gate (handoff LV.3): a
52
67
  // brainy outside this range fails loud at init() instead of silently dropping to
53
68
  // the JS engine. Typed via intersection until the devDependency moves to brainy
@@ -91,8 +106,21 @@ const corPlugin = {
91
106
  // storage. On a cloud adapter the compute providers fail at
92
107
  // init with a clear error rather than silently degrading.
93
108
  // Brainy continues to work unaccelerated on any adapter.
94
- // Gate: native module must be available for compute acceleration
109
+ // Gate: native module must be available for compute acceleration.
110
+ // A silent return here with a license configured is the worst failure
111
+ // shape cor has: the operator PAID for acceleration, the module didn't
112
+ // load (wrong glibc, missing binary), and every provider quietly ran
113
+ // brainy-JS with no journal line (ACC-COR-SILENT-NATIVE-FAIL — accord-vm
114
+ // ran 0/10 native for a day). isNativeAvailable() logs the load error
115
+ // once; this adds the license-aware alarm.
95
116
  if (!isNativeAvailable()) {
117
+ const configured = await resolveRawLicenseValue();
118
+ if (configured) {
119
+ const cause = getNativeLoadError();
120
+ console.warn(`[cor] a license is configured but the native module FAILED TO LOAD — ` +
121
+ `compute acceleration is DISABLED and brainy runs on its JS engines. ` +
122
+ `Cause: ${cause?.message ?? 'unknown (no load error recorded)'}`);
123
+ }
96
124
  return true;
97
125
  }
98
126
  // Gate: license tier determines compute access
@@ -108,23 +136,12 @@ const corPlugin = {
108
136
  // The CLI now writes JWTs directly (sc_cor_...), so short codes in .soulcraft.json
109
137
  // only appear in old or manually-written files. Exchange at runtime is a fallback path.
110
138
  //
111
- // Resolve the RAW license value with the same env-then-file precedence
112
- // readLicenseToken() uses (env var COR_LICENSE_KEY / legacy CORTEX_LICENSE_KEY
113
- // wins over .soulcraft.json). We can't reuse readLicenseToken() directly here
114
- // because it exchanges short codes into JWTs (or returns null on a failed
115
- // exchange), erasing the short-code shape this branch needs to test. Reading
116
- // the file alone would drop offline grace for an env-supplied short code on a
117
- // network blip — the exact gap this fixes.
118
- const envValue = (process.env.COR_LICENSE_KEY ?? process.env.CORTEX_LICENSE_KEY)?.trim();
119
- let licenseValue;
120
- if (envValue) {
121
- licenseValue = envValue;
122
- }
123
- else {
124
- const config = await readConfig();
125
- // Dual-read during the cortex→cor rename: new `cor` key, legacy `cortex`.
126
- licenseValue = config.cor ?? config.cortex;
127
- }
139
+ // We can't reuse readLicenseToken() here because it exchanges short codes
140
+ // into JWTs (or returns null on a failed exchange), erasing the
141
+ // short-code shape this branch needs to test. Reading the file alone
142
+ // would drop offline grace for an env-supplied short code on a network
143
+ // blip the exact gap this fixes.
144
+ const licenseValue = await resolveRawLicenseValue();
128
145
  if (!licenseValue) {
129
146
  return true; // No license configured — free tier, no warning
130
147
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulcraft/cor",
3
- "version": "3.0.7",
3
+ "version": "3.0.8",
4
4
  "description": "Native Rust acceleration for Brainy — SIMD distance, vector quantization, zero-copy mmap, native embeddings. Free tier for storage, Pro license for compute acceleration.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -22,7 +22,7 @@
22
22
  "build:native": "cd native && napi build --release",
23
23
  "build:native:dev": "cd native && napi build",
24
24
  "prepublishOnly": "npm run build && cd native && napi prepublish -t npm",
25
- "test": "vitest run",
25
+ "test": "vitest run --exclude src/native/lsmPerfGates.test.ts && vitest run --no-file-parallelism src/native/lsmPerfGates.test.ts",
26
26
  "bench": "vitest bench",
27
27
  "bench:compare": "npx tsx src/benchmarks/comparison.ts",
28
28
  "test:ci-native": "node --input-type=module -e \"import { loadNativeModule } from './dist/native/index.js'; const n = loadNativeModule(); console.log('Native module loaded:', Object.keys(n).length, 'exports');\"",
@@ -82,7 +82,7 @@
82
82
  },
83
83
  "devDependencies": {
84
84
  "@napi-rs/cli": "^3.0.0",
85
- "@soulcraft/brainy": "8.0.12",
85
+ "@soulcraft/brainy": "8.2.2",
86
86
  "@types/node": "^22.0.0",
87
87
  "pg": "^8.21.0",
88
88
  "tsx": "^4.21.0",