feat(8.0): version-coupling guard — a mismatched/failed native plugin fails loud, never silent JS fallback

A wrong or missing accelerator (@soulcraft/cor) used to silently degrade to the
default JS engine — the #1 source of invisible cross-repo drift. Brainy does no
plugin auto-detection (a registered plugin is always explicitly requested via
config.plugins or brain.use()), so any mismatch/failure is now fatal:

- BrainyPlugin gains optional `brainyRange` (semver range of brainy it supports);
  init() throws if the running brainy is outside it. New dep-free, prerelease-safe
  matcher pluginRangeSatisfies() (8.0.0-rc1 satisfies >=8.0.0).
- activateAll(): a plugin that THROWS during activate now re-throws (was swallowed
  to a JS fallback); a graceful decline (activate()→false) stays non-fatal but logs
  a loud warning.
- loadPlugins(): an explicitly-listed package that can't load — or isn't a valid
  plugin — throws (was a silent skip).
- Provider-key cliff: a pre-8.0 plugin that registers a legacy vector key
  ('hnsw'/'diskann') but not the 8.0 'vector' key throws (brainy 8.x reads only
  'vector', so it would otherwise run JS vectors invisibly).

Tests: tests/unit/plugin-version-coupling.test.ts (range matcher incl. rc1; range
mismatch / activate-throw / cliff / missing-package all throw; decline non-fatal).
Updated two plugin.test.ts cases that asserted the old silent-swallow behavior.
Build green; 1464 unit green. cor declares its brainyRange per handoff LV.1 #2.
This commit is contained in:
David Snelling 2026-06-19 10:13:04 -07:00
parent b198281ce1
commit 1264fec534
4 changed files with 271 additions and 32 deletions

View file

@ -10976,16 +10976,29 @@ export class Brainy<T = any> implements BrainyInterface<T> {
// 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
// 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).
for (const pkg of pluginConfig) {
let mod: any
try {
const mod = await import(pkg)
const plugin: BrainyPlugin = mod.default || mod
if (plugin && typeof plugin.activate === 'function' && plugin.name) {
this.pluginRegistry.register(plugin)
}
} catch {
// Package not found — skip
mod = await import(pkg)
} catch (error) {
throw new Error(
`[brainy] Plugin "${pkg}" is listed in config.plugins but could not be loaded: ` +
`${error instanceof Error ? error.message : String(error)}. ` +
`Install it (e.g. npm i ${pkg}) or remove it from config.plugins.`
)
}
const plugin: BrainyPlugin = mod.default || mod
if (plugin && typeof plugin.activate === 'function' && plugin.name) {
this.pluginRegistry.register(plugin)
} else {
throw new Error(
`[brainy] Package "${pkg}" (config.plugins) is not a valid Brainy plugin ` +
`(missing { name, activate }). Remove it from config.plugins.`
)
}
}
}
@ -10998,6 +11011,27 @@ export class Brainy<T = any> implements BrainyInterface<T> {
// Activate all registered plugins
const activated = await this.pluginRegistry.activateAll(context)
// Version-coupling guard (the provider-key cliff). A pre-8.0 native plugin
// (e.g. @soulcraft/cor < 3.0) registers its vector engine under a LEGACY key
// ('hnsw'/'diskann') instead of the 8.0 canonical 'vector'. brainy 8.x reads
// only 'vector', so without this check the native engine silently never
// engages and every query runs on the default JS index — exactly the
// invisible degrade we must prevent. brainy never registers these keys
// itself, so their presence can only come from a plugin.
const LEGACY_VECTOR_KEYS = ['hnsw', 'diskann', 'hnswIndex']
const legacyVector = LEGACY_VECTOR_KEYS.filter(
(k) => this.pluginRegistry.getProvider(k) !== undefined
)
if (legacyVector.length > 0 && this.pluginRegistry.getProvider('vector') === undefined) {
throw new Error(
`[brainy] A native acceleration plugin registered a legacy vector provider ` +
`(${legacyVector.join(', ')}) but not the 'vector' provider that brainy 8.x requires. ` +
`This is an incompatible (pre-8.0) accelerator — upgrade to @soulcraft/cor >= 3.0. ` +
`brainy will NOT silently run the default JS vector engine in its place.`
)
}
if (activated.length > 0) {
// Only log if not in silent mode
if (!this.config.silent) {