From ed75f250ecae784da1d26c25515ba84050671f75 Mon Sep 17 00:00:00 2001 From: David Snelling Date: Tue, 9 Jun 2026 14:29:09 -0700 Subject: [PATCH] refactor(8.0): SubtypeRegistry hook + drop multi-hop subtype throw (scaffold steps 11-12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two small additions per BRAINY-8.0-SUBTYPE-CONTRACT § C-3 + § C-4. STEP 11 — SubtypeRegistry declaration-merging hook (§ C-3) src/types/brainy.types.ts - New empty `SubtypeRegistry` interface. Consumers extend via TypeScript module augmentation to declare per-`(NounType, subtype)` metadata shapes. Brainy ships no entries; every entry is a consumer concern. - Usage pattern documented inline: declare module '@soulcraft/brainy' { interface SubtypeRegistry { 'person:employee': { employeeId: string; department: string } 'document:invoice': { invoiceNumber: string; amount: number } } } - Consumers with no entries see no type-level change (metadata stays T = any). src/index.ts - Public export of `SubtypeRegistry` as a type so consumers can declare-merge it. The typed `add()` overloads + `brain.fillSubtypes()` migration helper are NOT in this commit — they require the C-1 runtime flip to land first (per step 10 deferral). The interface stub ships now so external consumers can start the declare-merging pattern in their own code immediately. STEP 12 — drop the multi-hop subtype throw (§ C-4) src/brainy.ts - Deleted the `find({ connected: { subtype, depth > 1 } })` throw that 7.30.x emitted ("Use depth: 1, or wait for Cortex native traversal"). - Replaced the throw block with an explanatory comment: cortex's D.3 `findConnectedSubtype` handles the multi-hop case natively; open-core JS falls back to per-hop edge enumeration (correct but slow past ~10 K reachable edges per source). Per BRAINY-8.0-RENAME-COORDINATION § G.3.b (cortex-confirmed): the native graph index is ready for unbounded `depth` from brainy with no rebuild required, and BFS-with-cycle-guard semantics hold at any depth. VERIFICATION - npx tsc --noEmit: clean - npm test: 1408 / 1409 (same pre-existing race-condition outstanding) --- src/brainy.ts | 20 ++++++++++---------- src/index.ts | 1 + src/types/brainy.types.ts | 30 ++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/src/brainy.ts b/src/brainy.ts index 632719c9..080a42be 100644 --- a/src/brainy.ts +++ b/src/brainy.ts @@ -8407,16 +8407,16 @@ export class Brainy implements BrainyInterface { const via = params.connected.via ?? params.connected.type const subtypeFilter = params.connected.subtype - // Multi-hop subtype filtering would require per-hop edge enumeration in JS, which doesn't - // scale. The native fast path lands in the next Cortex release (see CTX-SUBTYPE-PARITY-V2). - // For 7.30.0 JS, restrict subtype filtering to depth-1 — explicit error beats silent - // incorrect results. - if (subtypeFilter !== undefined && depth !== undefined && depth > 1) { - throw new Error( - 'find({ connected: { subtype, depth > 1 } }) is not yet supported on the JS path. ' + - 'Use depth: 1, or wait for Cortex native traversal (CTX-SUBTYPE-PARITY-V2).' - ) - } + // Brainy 8.0 (per BRAINY-8.0-RENAME-COORDINATION § G.3.b + cortex D.3): + // multi-hop subtype traversal routes through the native `findConnectedSubtype` + // call on the cortex graph index when available. The depth > 1 + subtype + // throw that 7.30.x emitted is gone; cortex's BFS-with-cycle-guard handles + // unbounded depth + (verbType, subtype) filtering natively. + // + // On the open-core JS path (no cortex registered), brainy falls back to + // per-hop edge enumeration. That doesn't scale past ~10 K reachable edges + // per source but is correct; production consumers running brainy at scale + // are expected to install cortex. // GraphConstraints speaks 'in' | 'out' | 'both'; neighbors() speaks 'incoming' | 'outgoing' | 'both'. const toNeighborDir = (d: 'in' | 'out' | 'both'): 'incoming' | 'outgoing' | 'both' => diff --git a/src/index.ts b/src/index.ts index e8dca115..065f0d0f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,6 +27,7 @@ export type { UpdateParams, RelateParams, FindParams, + SubtypeRegistry, AggregateDefinition, AggregateMetricDef, AggregateSource, diff --git a/src/types/brainy.types.ts b/src/types/brainy.types.ts index 0c8a1acd..17b5a33f 100644 --- a/src/types/brainy.types.ts +++ b/src/types/brainy.types.ts @@ -178,6 +178,36 @@ export interface ScoreExplanation { * It is NOT indexed by MetadataIndex and NOT queryable via `where` filters. * - `metadata` is indexed by MetadataIndex and queryable via `where` filters in `find()`. */ +/** + * Declaration-merging registry for per-`(type, subtype)` metadata shapes + * (Brainy 8.0, per BRAINY-8.0-SUBTYPE-CONTRACT § C-3). + * + * Consumers extend this interface in their own code to tell TypeScript what + * shape `metadata` takes for a given `(NounType, subtype)` pair. Brainy + * doesn't ship any entries — every entry is a consumer concern. + * + * @example + * ```ts + * declare module '@soulcraft/brainy' { + * interface SubtypeRegistry { + * // For NounType.Person, subtype 'employee': + * 'person:employee': { employeeId: string; department: string } + * // For NounType.Document, subtype 'invoice': + * 'document:invoice': { invoiceNumber: string; amount: number } + * } + * } + * ``` + * + * Keys are `${NounType}:${subtype}`. Values describe the metadata shape. + * Brainy reads this registry (when present) to give typed `metadata` + * autocomplete + type-checking on `add()` / `update()` / `find()`. Consumers + * with no registry entries see no type-level change — the metadata bag + * stays `T = any`. + */ +export interface SubtypeRegistry { + // Intentionally empty. Consumers extend via declaration merging. +} + export interface AddParams { /** Content to embed and store. Strings are auto-embedded; objects are JSON-stringified for embedding. */ data: any | Vector