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:
David Snelling 2026-07-02 16:19:55 -07:00
parent b37359e097
commit 588267be7f
6 changed files with 265 additions and 26 deletions

View file

@ -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
// ['@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<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