From 588267be7f1598593cb8be95d3f8d683393cd22c Mon Sep 17 00:00:00 2001 From: David Snelling Date: Thu, 2 Jul 2026 16:19:55 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20guarded=20plugin=20auto-detection=20?= =?UTF-8?q?=E2=80=94=20installing=20@soulcraft/cor=20is=20the=20opt-in?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With plugins unset (the default), init() probes for the first-party accelerator: not installed means plain brainy with zero noise; installed and healthy means it loads and announces itself; installed but broken (import failure, invalid shape, failed activation, version mismatch) makes init() THROW. An installed accelerator never silently vanishes behind the JS engines - the anti-drift posture of the explicit list, applied to detection. plugins: []/false stays a true opt-out (no probe); an explicit list keeps its required-and-loud semantics. The import runs through an importPluginPackage seam (variable specifier - bundlers cannot static-resolve the optional package; tests simulate all outcomes without it installed). Also: when a plugin activates but registers zero native providers (e.g. a licensing gate declining to engage), the provider summary now warns loudly instead of leaving every query silently on the JS engines. Supersedes 8.0.8's explicit-opt-in wording; README/PLUGINS/types now document the guarded contract. 7 new tests (tests/unit/plugin-autodetect.test.ts). --- README.md | 9 +- RELEASES.md | 21 +++++ docs/PLUGINS.md | 15 ++- src/brainy.ts | 99 +++++++++++++++++-- src/types/brainy.types.ts | 11 ++- tests/unit/plugin-autodetect.test.ts | 136 +++++++++++++++++++++++++++ 6 files changed, 265 insertions(+), 26 deletions(-) create mode 100644 tests/unit/plugin-autodetect.test.ts diff --git a/README.md b/README.md index 8d0516a2..d1342f52 100644 --- a/README.md +++ b/README.md @@ -181,14 +181,11 @@ npm install @soulcraft/cor ``` ```javascript -const brain = new Brainy({ - plugins: ['@soulcraft/cor'], // explicit opt-in — brainy never auto-loads a package - storage: { type: 'filesystem', path: './data' } -}) -await brain.init() // same code — native engines underneath +const brain = new Brainy({ storage: { type: 'filesystem', path: './data' } }) +await brain.init() // @soulcraft/cor detected — same code, native engines underneath ``` -Plugins are explicit: an engine swap is always visible in your config, and a listed plugin that fails to load fails **loud** — never a silent fallback to the JS engines. [`@soulcraft/cor`](https://www.npmjs.com/package/@soulcraft/cor) (Brainy 8.x ↔ Cor 3.x, version-matched) registers Rust implementations behind every provider seam: SIMD distance kernels, memory-mapped storage, a disk-native vector index that doesn't need your dataset in RAM, durable LSM field/graph indexes that serve cold opens instantly, and native aggregation. Recall@10 measured **0.99 / 0.96 / 0.96 at 1M / 10M / 100M vectors** in Cor's release gate. +Installing the package is the opt-in: if `@soulcraft/cor` is present, it loads and announces itself in the init log; if it's present but broken, `init()` **throws** — an installed accelerator never silently vanishes behind the JS engines. Opt out with `plugins: []`, or pin exactly what loads with `plugins: ['@soulcraft/cor']`. [`@soulcraft/cor`](https://www.npmjs.com/package/@soulcraft/cor) (Brainy 8.x ↔ Cor 3.x, version-matched) registers Rust implementations behind every provider seam: SIMD distance kernels, memory-mapped storage, a disk-native vector index that doesn't need your dataset in RAM, durable LSM field/graph indexes that serve cold opens instantly, and native aggregation. Recall@10 measured **0.99 / 0.96 / 0.96 at 1M / 10M / 100M vectors** in Cor's release gate. Open core, commercial accelerator: Brainy is MIT and complete on its own; Cor is licensed and funds both. diff --git a/RELEASES.md b/RELEASES.md index 13e38dac..591c8c8e 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -10,6 +10,27 @@ Full auto-generated changelog: `CHANGELOG.md` · Releases: https://github.com/so --- +## v8.0.9 — 2026-07-02 (guarded plugin auto-detection — the "install it and it's on" contract) + +With the default config (`plugins` unset), brainy now **auto-detects the first-party accelerator**: +installing `@soulcraft/cor` is the opt-in. The detection is guarded — everything except "not +installed" fails loud: + +- Not installed → plain brainy, silently (the free path — zero noise, zero cost). +- Installed and healthy → it loads and announces itself (`[brainy] Plugin activated`). +- Installed but broken (unresolvable, invalid shape, failed activation, version mismatch) → + **`init()` throws.** An installed accelerator never silently vanishes behind the JS engines. +- `plugins: []` / `false` = explicit opt-out; `plugins: ['@soulcraft/cor']` pins the exact list + (unchanged semantics). + +Also new: if a plugin activates but registers **zero** native providers (e.g. a licensing gate +declining to engage), brainy warns loudly instead of leaving you to discover every query is running +on the JS engines. + +> This supersedes v8.0.8's "plugins are explicit opt-in" wording, which documented the pre-GA +> loader accurately but contradicted the published product contract ("add the package and it +> activates"). 8.0.9 makes the contract true — with the loud-failure guarantees intact. + ## v8.0.8 — 2026-07-02 (docs-only patch on the GA) Corrects the README's scale-up section: the native provider is **not** auto-detected — plugins are diff --git a/docs/PLUGINS.md b/docs/PLUGINS.md index 1d590c81..6c4c1558 100644 --- a/docs/PLUGINS.md +++ b/docs/PLUGINS.md @@ -23,20 +23,19 @@ Brainy's plugin system uses **named providers** — string keys mapped to implem 3. The plugin calls `context.registerProvider(key, implementation)` for each subsystem it provides 4. Brainy checks each provider key and wires the implementation into its internal pipeline -Plugins are **opt-in** — brainy never auto-imports packages. You must explicitly list plugins in the config: +Installing the first-party accelerator is the opt-in: with the default config, brainy probes for `@soulcraft/cor` and loads it when present. Everything except "not installed" fails **loud** — a present-but-broken accelerator makes `init()` throw rather than silently degrading to the JS engines. ```typescript -const brain = new Brainy({ - plugins: ['@soulcraft/cor'] // explicitly load the native acceleration package -}) +const brain = new Brainy() // @soulcraft/cor auto-detected when installed +const pinned = new Brainy({ plugins: ['@soulcraft/cor'] }) // or pin exactly what loads +const plain = new Brainy({ plugins: [] }) // or opt out of detection entirely ``` | `plugins` value | Behavior | |---|---| -| `undefined` (default) | No plugins loaded | -| `false` | No plugins loaded | -| `[]` | No plugins loaded | -| `['@soulcraft/cor']` | Load only the listed packages | +| `undefined` (default) | Guarded auto-detection of `@soulcraft/cor`: not installed → no plugins, silently; installed → loads + announces; installed-but-broken → `init()` throws | +| `false` / `[]` | No plugins, no detection (explicit opt-out) | +| `['@soulcraft/cor']` | Load only the listed packages; a listed plugin that fails to load throws | Plugins registered programmatically via `brain.use(plugin)` are always activated regardless of the `plugins` config. diff --git a/src/brainy.ts b/src/brainy.ts index e435489d..5ba72b4d 100644 --- a/src/brainy.ts +++ b/src/brainy.ts @@ -331,6 +331,10 @@ export class Brainy implements BrainyInterface { * event-driven `whenMigrationComplete()` signal. See {@link awaitMigrationLock}. */ private static readonly MIGRATION_POLL_INTERVAL_MS = 250 + /** First-party accelerator packages probed by guarded auto-detection when + * `plugins` is undefined (installing one IS the opt-in). See {@link loadPlugins}. */ + private static readonly AUTO_DETECT_PLUGIN_PACKAGES = ['@soulcraft/cor'] + // Core components private index!: JsHnswVectorIndex private storage!: BaseStorage @@ -1041,6 +1045,17 @@ export class Brainy implements BrainyInterface { } else { console.log(`[brainy] Providers: ${native.length}/${wellKnownKeys.length} native (${plugins}) | default: ${fallback.join(', ')}`) } + // An activated accelerator that registered NOTHING is running as pure + // decoration — every query silently serves from the JS engines. Most + // often a licensing gate declining to engage. Say so, loudly, so an + // evaluator never concludes the accelerator "does nothing". + if (native.length === 0) { + console.warn( + `[brainy] ⚠ ${plugins} activated but registered 0 native providers — all queries are ` + + `running on the default JS engines. Check the plugin's requirements (e.g. a license ` + + `key) and its logs; see docs/PLUGINS.md.` + ) + } } // Mark as initialized BEFORE VFS init @@ -13870,23 +13885,59 @@ export class Brainy implements BrainyInterface { * Auto-detect and activate plugins. * Called internally during init(). */ + /** + * Import a plugin package by name. Isolated as a seam so tests can simulate + * the three auto-detect outcomes (not installed / broken install / valid) + * without the package being present. The specifier is a variable, so + * bundlers cannot statically resolve — and cannot force-include — the + * optional accelerator. + */ + private async importPluginPackage(pkg: string): Promise { + return import(pkg) + } + + /** + * True when a dynamic-import failure means "the package itself is not + * installed" (the silent free path), as opposed to "the package is present + * but broken" (which must fail loud). Node and Bun both name the missing + * package in the resolution error; a failure naming anything else — an + * internal file, a dependency of the plugin, a syntax error — is a broken + * install, never a not-installed. + */ + private static isPackageNotInstalledError(error: unknown, pkg: string): boolean { + const code = (error as { code?: string })?.code + const message = error instanceof Error ? error.message : String(error) + const namesPackage = + message.includes(`'${pkg}'`) || message.includes(`"${pkg}"`) || message.includes(` ${pkg}`) + const isResolutionFailure = + code === 'ERR_MODULE_NOT_FOUND' || + code === 'MODULE_NOT_FOUND' || + /cannot find (package|module)/i.test(message) || + /failed to resolve/i.test(message) // Bun's resolver phrasing + return isResolutionFailure && namesPackage + } + private async loadPlugins(): Promise { // plugins config: - // undefined (default) → no auto-detection (safe default) - // false → no auto-detection - // [] → no auto-detection - // ['@soulcraft/cor'] → load only these explicitly listed packages + // undefined (default) → guarded auto-detection of the first-party + // accelerator: installing @soulcraft/cor IS the + // opt-in. Not installed → plain brainy, silently. + // Installed → it loads and announces itself; if it + // is installed but broken, init() THROWS — an + // installed accelerator never silently vanishes. + // false / [] → no plugins, no detection (explicit opt-out) + // ['@soulcraft/cor'] → load only these explicitly listed packages // Note: plugins registered via brain.use() are always activated regardless of config const pluginConfig = this.config.plugins if (Array.isArray(pluginConfig) && pluginConfig.length > 0) { // Explicit list: import and register the specified packages. A package - // listed here is REQUIRED — brainy does no auto-detection, so a missing or - // invalid plugin must fail LOUD, never silently fall back to the default - // engine (the cross-repo drift this whole guard exists to prevent). + // listed here is REQUIRED — a missing or invalid plugin must fail LOUD, + // never silently fall back to the default engine (the cross-repo drift + // this whole guard exists to prevent). for (const pkg of pluginConfig) { let mod: any try { - mod = await import(pkg) + mod = await this.importPluginPackage(pkg) } catch (error) { throw new Error( `[brainy] Plugin "${pkg}" is listed in config.plugins but could not be loaded: ` + @@ -13904,6 +13955,38 @@ export class Brainy implements BrainyInterface { ) } } + } else if (pluginConfig === undefined) { + // Guarded auto-detection (default). Installing the first-party + // accelerator is the opt-in — probe for it, and apply the SAME loud + // posture as the explicit branch to everything except "not installed": + // a present-but-broken accelerator must never silently degrade to JS. + for (const pkg of Brainy.AUTO_DETECT_PLUGIN_PACKAGES) { + let mod: any + try { + mod = await this.importPluginPackage(pkg) + } catch (error) { + if (Brainy.isPackageNotInstalledError(error, pkg)) { + continue // the free path: not installed, nothing to load, no noise + } + throw new Error( + `[brainy] The accelerator "${pkg}" is installed but failed to load: ` + + `${error instanceof Error ? error.message : String(error)}. ` + + `brainy will NOT silently run the default JS engines in its place — ` + + `fix the install (npm i ${pkg}) or disable detection with plugins: [].` + ) + } + const plugin: BrainyPlugin = mod.default || mod + if (plugin && typeof plugin.activate === 'function' && plugin.name) { + this.pluginRegistry.register(plugin) + } else { + throw new Error( + `[brainy] The installed accelerator "${pkg}" is not a valid Brainy plugin ` + + `(missing { name, activate }) — a broken or incompatible install. ` + + `brainy will NOT silently run the default JS engines in its place — ` + + `fix the install (npm i ${pkg}) or disable detection with plugins: [].` + ) + } + } } // Create plugin context diff --git a/src/types/brainy.types.ts b/src/types/brainy.types.ts index 7d2c2552..c2344375 100644 --- a/src/types/brainy.types.ts +++ b/src/types/brainy.types.ts @@ -1804,10 +1804,13 @@ export interface BrainyConfig { eagerEmbeddings?: boolean // Plugin configuration - // Controls which plugins are loaded during init(). Plugins are explicit - // opt-in — brainy NEVER auto-imports a package that isn't listed here. - // - undefined (default): no plugins loaded (same as [] / false) - // - false / []: no plugins loaded + // Controls which plugins are loaded during init(). + // - undefined (default): guarded auto-detection of the first-party + // accelerator (@soulcraft/cor) — installing the package IS the opt-in. + // Not installed → plain brainy, silently. Installed → it loads and + // announces itself. Installed but broken → init() THROWS (an installed + // accelerator never silently vanishes behind the JS engines). + // - false / []: no plugins, no detection (explicit opt-out) // - ['@soulcraft/cor']: load exactly these packages; a listed plugin that // fails to load or is invalid THROWS (loud, never a silent JS fallback) plugins?: string[] | false diff --git a/tests/unit/plugin-autodetect.test.ts b/tests/unit/plugin-autodetect.test.ts new file mode 100644 index 00000000..37c181ba --- /dev/null +++ b/tests/unit/plugin-autodetect.test.ts @@ -0,0 +1,136 @@ +/** + * Guarded plugin auto-detection (8.0.9) — the "install it and it's on" contract. + * + * With `plugins` unset, brainy probes for the first-party accelerator + * (@soulcraft/cor). The probe applies the same loud posture as the explicit + * list to everything except "not installed": + * - not installed → plain brainy, silently (the free path) + * - installed + healthy → registered and activated + * - installed + broken → init() THROWS (import failure, invalid shape, + * or failed activation — an installed + * accelerator never silently vanishes) + * - plugins: [] / false → no probe at all (explicit opt-out) + * - explicit list → unchanged: required, loud on any failure + * + * The import is isolated behind the private `importPluginPackage` seam so the + * three outcomes are simulated without the package being present. + */ +import { describe, it, expect, afterEach } from 'vitest' +import { Brainy, NounType } from '../../src/index.js' + +const proto = Brainy.prototype as any +const origImport = proto.importPluginPackage + +afterEach(() => { + proto.importPluginPackage = origImport +}) + +/** Simulate the probe outcome per package name. */ +function stubImport(fn: (pkg: string) => Promise): string[] { + const probed: string[] = [] + proto.importPluginPackage = async function (pkg: string) { + probed.push(pkg) + return fn(pkg) + } + return probed +} + +function notInstalledError(pkg: string): Error { + // Node's exact shape for a missing package in a dynamic ESM import. + const e = new Error(`Cannot find package '${pkg}' imported from /app/index.js`) + ;(e as any).code = 'ERR_MODULE_NOT_FOUND' + return e +} + +/** A minimal valid plugin that registers one well-known provider. */ +function validPlugin(name: string) { + return { + name, + activate: async (context: any) => { + context.registerProvider('distance', { euclidean: (a: number[], b: number[]) => 0 }) + return true + } + } +} + +describe('Guarded plugin auto-detection (plugins: undefined)', () => { + it('not installed → plain brainy, no plugins, init succeeds silently', async () => { + const probed = stubImport(async (pkg) => { + throw notInstalledError(pkg) + }) + const brain: any = new Brainy({ requireSubtype: false, storage: { type: 'memory' }, silent: true }) + await brain.init() + expect(probed).toContain('@soulcraft/cor') // the probe ran… + expect(brain.pluginRegistry.hasActivePlugins()).toBe(false) // …and found nothing, quietly + const id = await brain.add({ data: 'works without any plugin', type: NounType.Concept }) + expect(id).toBeTruthy() + await brain.close() + }) + + it('installed + healthy → auto-registered and activated', async () => { + stubImport(async () => ({ default: validPlugin('@soulcraft/cor') })) + const brain: any = new Brainy({ requireSubtype: false, storage: { type: 'memory' }, silent: true }) + await brain.init() + expect(brain.pluginRegistry.getActivePlugins()).toContain('@soulcraft/cor') + expect(brain.pluginRegistry.hasProvider('distance')).toBe(true) + await brain.close() + }) + + it('installed but import fails (broken install) → init() throws, never a silent JS fallback', async () => { + stubImport(async () => { + // A resolution failure INSIDE the package (names a sub-path, not the + // package) — present but broken, must be loud. + const e = new Error( + "Cannot find module '/app/node_modules/@soulcraft/cor/dist/native.js' imported from " + + '/app/node_modules/@soulcraft/cor/dist/index.js' + ) + ;(e as any).code = 'ERR_MODULE_NOT_FOUND' + throw e + }) + const brain: any = new Brainy({ requireSubtype: false, storage: { type: 'memory' }, silent: true }) + await expect(brain.init()).rejects.toThrow(/installed but failed to load/) + }) + + it('installed but not a valid plugin (missing activate) → init() throws', async () => { + stubImport(async () => ({ default: { name: '@soulcraft/cor' } })) // no activate() + const brain: any = new Brainy({ requireSubtype: false, storage: { type: 'memory' }, silent: true }) + await expect(brain.init()).rejects.toThrow(/not a valid Brainy plugin/) + }) + + it('installed but activation fails → init() throws (activateAll posture applies)', async () => { + stubImport(async () => ({ + default: { + name: '@soulcraft/cor', + activate: async () => { + throw new Error('native binary missing for platform') + } + } + })) + const brain: any = new Brainy({ requireSubtype: false, storage: { type: 'memory' }, silent: true }) + await expect(brain.init()).rejects.toThrow(/failed to activate/) + }) + + it('plugins: [] and plugins: false → no probe at all (explicit opt-out)', async () => { + const probed = stubImport(async () => ({ default: validPlugin('@soulcraft/cor') })) + for (const plugins of [[], false] as const) { + const brain: any = new Brainy({ requireSubtype: false, storage: { type: 'memory' }, plugins: plugins as any, silent: true }) + await brain.init() + expect(brain.pluginRegistry.hasActivePlugins()).toBe(false) + await brain.close() + } + expect(probed).toEqual([]) // the seam was never touched + }) + + it('explicit list is unchanged: a listed-but-missing plugin throws with the config-pointing message', async () => { + stubImport(async (pkg) => { + throw notInstalledError(pkg) + }) + const brain: any = new Brainy({ + requireSubtype: false, + storage: { type: 'memory' }, + plugins: ['@soulcraft/cor'], + silent: true + }) + await expect(brain.init()).rejects.toThrow(/listed in config\.plugins but could not be loaded/) + }) +})