refactor(8.0): SubtypeRegistry hook + drop multi-hop subtype throw (scaffold steps 11-12)

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<NounType.Person, 'employee'>()` 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)
This commit is contained in:
David Snelling 2026-06-09 14:29:09 -07:00
parent 1eb0ffc341
commit ed75f250ec
3 changed files with 41 additions and 10 deletions

View file

@ -8407,16 +8407,16 @@ export class Brainy<T = any> implements BrainyInterface<T> {
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' =>

View file

@ -27,6 +27,7 @@ export type {
UpdateParams,
RelateParams,
FindParams,
SubtypeRegistry,
AggregateDefinition,
AggregateMetricDef,
AggregateSource,

View file

@ -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<T = any> {
/** Content to embed and store. Strings are auto-embedded; objects are JSON-stringified for embedding. */
data: any | Vector