feat: guarded plugin auto-detection — installing @soulcraft/cor is the opt-in
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).
This commit is contained in:
parent
b37359e097
commit
588267be7f
6 changed files with 265 additions and 26 deletions
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
21
RELEASES.md
21
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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -331,6 +331,10 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
* 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<T = any> implements BrainyInterface<T> {
|
|||
} 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<T = any> implements BrainyInterface<T> {
|
|||
* 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<any> {
|
||||
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<void> {
|
||||
// plugins config:
|
||||
// undefined (default) → no auto-detection (safe default)
|
||||
// false → no auto-detection
|
||||
// [] → no auto-detection
|
||||
// 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<T = any> implements BrainyInterface<T> {
|
|||
)
|
||||
}
|
||||
}
|
||||
} 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
136
tests/unit/plugin-autodetect.test.ts
Normal file
136
tests/unit/plugin-autodetect.test.ts
Normal file
|
|
@ -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<any>): 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/)
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue