feat(8.0): visibility field (public/internal/system) on nouns + verbs

Adds a reserved, top-level `visibility` field (mirrors the subtype rollout):
'public' (default, surfaced) | 'internal' (developer app-internal — hidden from
default find/count/stats, opt-in via includeInternal) | 'system' (Brainy
plumbing, library-set only).

Fixes a real leak: the VFS root entity counted in getNounCount() and appeared in
find() (a fresh brain reported 1 entity). It is now visibility:'system' →
excluded from every user-facing surface. Developers also get a first-class
hidden-unless-asked tier (e.g. learned internals vs user-exposed data).

- Reserved (RESERVED_ENTITY_FIELDS / RESERVED_RELATION_FIELDS) — spoof-proof from metadata.
- Threaded through add/relate/update/transact; surfaced top-level on reads.
- Default exclusion in counts (baseStorage), find()/related() (hard candidate
  filter via excludeVisibility — keeps topK/limit correct), and stats;
  includeInternal/includeSystem opt-ins.
- VFS root marked 'system'.

Tests: visibility.test.ts 17/17 (fresh-brain getNounCount()===0, internal hidden
+ opt-in, verb symmetry, top-level surfacing, metadata-spoof rejection). Unit
1431 green; count-synchronization integration now passes (off-by-one fixed).
This commit is contained in:
David Snelling 2026-06-16 15:20:26 -07:00
parent 0ca0e5c6cc
commit f4dea80176
10 changed files with 777 additions and 41 deletions

View file

@ -5,6 +5,7 @@
*/
import { StorageAdapter, Vector } from '../coreTypes.js'
import type { EntityVisibility } from '../coreTypes.js'
import { NounType, VerbType } from './graphTypes.js'
import type {
EntityMetadataInput,
@ -39,6 +40,13 @@ export interface Entity<T = any> {
* rolled into per-NounType statistics.
*/
subtype?: string
/**
* Visibility tier (reserved top-level field). Absent === `'public'` (counted and
* returned everywhere). `'internal'` hides the entity from default `find()` / counts /
* `stats()` (opt back in with `find({ includeInternal: true })`); `'system'` is Brainy
* plumbing, hidden unless `find({ includeSystem: true })`. See {@link EntityVisibility}.
*/
visibility?: EntityVisibility
/** Opaque content — used for embeddings and semantic search. Not indexed by MetadataIndex. */
data?: any
/** User-defined structured fields — indexed and queryable via `where` filters. */
@ -90,6 +98,14 @@ export interface Relation<T = any> {
* the column-store directly.
*/
subtype?: string
/**
* Visibility tier (reserved top-level field), the verb mirror of `Entity.visibility`.
* Absent === `'public'` (counted and returned everywhere). `'internal'` hides the edge
* from default `related()` / counts / `stats()` (opt back in with
* `related({ includeInternal: true })`); `'system'` is Brainy plumbing. See
* {@link EntityVisibility}.
*/
visibility?: EntityVisibility
/** Connection strength (0-1, default: 1.0) */
weight?: number
/** Opaque content for the relationship (overrides auto-computed vector if provided) */
@ -135,6 +151,7 @@ export interface Result<T = any> {
// Convenience: Common entity fields flattened to top level
type?: NounType // Entity type (from entity.type)
subtype?: string // Per-product sub-classification (from entity.subtype)
visibility?: EntityVisibility // Visibility tier (from entity.visibility); absent === 'public'
metadata?: T // Entity metadata (from entity.metadata)
data?: any // Entity data (from entity.data)
confidence?: number // Type classification confidence (from entity.confidence)
@ -289,16 +306,25 @@ export interface AddParams<T = any> {
* aggregation on the standard-field fast path.
*/
subtype?: string
/**
* Visibility tier (reserved top-level field). Omit (or pass `'public'`) for normal
* data that is counted and returned everywhere. Pass `'internal'` for app-internal
* data that should be hidden from default `find()` / counts / `stats()` but stay
* retrievable via `find({ includeInternal: true })`. The `'system'` tier is reserved
* for Brainy's own plumbing and is intentionally not accepted here. Stored only when
* not `'public'`.
*/
visibility?: 'public' | 'internal'
/**
* Structured queryable fields indexed by MetadataIndex, used in `where` filters.
*
* Reserved entity fields (`RESERVED_ENTITY_FIELDS` `noun`, `subtype`, `createdAt`,
* `updatedAt`, `confidence`, `weight`, `service`, `data`, `createdBy`, `_rev`) may NOT
* appear here they have dedicated top-level params and the type makes a literal
* reserved key a compile error. Untyped (JavaScript) callers that pass one anyway are
* normalized at write time: user-settable fields remap to their top-level param
* (top-level wins when both are supplied), system-managed fields are dropped with a
* one-shot warning.
* Reserved entity fields (`RESERVED_ENTITY_FIELDS` `noun`, `subtype`, `visibility`,
* `createdAt`, `updatedAt`, `confidence`, `weight`, `service`, `data`, `createdBy`,
* `_rev`) may NOT appear here they have dedicated top-level params and the type makes
* a literal reserved key a compile error. Untyped (JavaScript) callers that pass one
* anyway are normalized at write time: user-settable fields remap to their top-level
* param (top-level wins when both are supplied), system-managed fields are dropped with
* a one-shot warning.
*/
metadata?: EntityMetadataInput<T>
/** Custom entity ID (auto-generated UUID v4 if not provided) */
@ -330,11 +356,18 @@ export interface UpdateParams<T = any> {
data?: any // New content to re-embed
type?: NounType // Change type
subtype?: string // Change subtype (set to '' or null-equivalent via dedicated unset is future work)
/**
* Change the visibility tier. Omit to preserve the existing value. Toggling between
* `'public'` and `'internal'` moves the entity in/out of the default-visible counts and
* `find()` results. The `'system'` tier is Brainy-internal (e.g. re-asserted on the VFS
* root during repair) consumer code should only ever use `'public'` / `'internal'`.
*/
visibility?: EntityVisibility
/**
* Metadata fields to merge (or replace when `merge: false`). Reserved entity
* fields (`RESERVED_ENTITY_FIELDS`) may NOT appear here `confidence` /
* `weight` / `subtype` have dedicated params on this call, and the rest are
* system-managed. A literal reserved key is a compile error; untyped callers
* `weight` / `subtype` / `visibility` have dedicated params on this call, and the rest
* are system-managed. A literal reserved key is a compile error; untyped callers
* are normalized at write time (remap user-settable, drop system-managed
* with a one-shot warning).
*/
@ -375,14 +408,23 @@ export interface RelateParams<T = any> {
* (`related({ verb, subtype })`) and aggregation (`groupBy:['subtype']`).
*/
subtype?: string
/**
* Visibility tier (reserved top-level field), the verb mirror of `AddParams.visibility`.
* Omit (or pass `'public'`) for normal edges that are counted and returned everywhere.
* Pass `'internal'` for app-internal edges hidden from default `related()` / counts /
* `stats()` but retrievable via `related({ includeInternal: true })`. The `'system'`
* tier is reserved for Brainy's own plumbing and is not accepted here. Stored only when
* not `'public'`.
*/
visibility?: 'public' | 'internal'
/** Connection strength (0-1, default: 1.0) */
weight?: number
/** Content for the relationship (optional — overrides auto-computed vector) */
data?: any
/**
* Structured queryable fields on the edge. Reserved relationship fields
* (`RESERVED_RELATION_FIELDS` `verb`, `subtype`, `createdAt`, `updatedAt`,
* `confidence`, `weight`, `service`, `data`, `createdBy`, `_rev`) may NOT
* (`RESERVED_RELATION_FIELDS` `verb`, `subtype`, `visibility`, `createdAt`,
* `updatedAt`, `confidence`, `weight`, `service`, `data`, `createdBy`, `_rev`) may NOT
* appear here they have dedicated params. A literal reserved key is a
* compile error; untyped callers are normalized at write time.
*/
@ -404,6 +446,12 @@ export interface UpdateRelationParams<T = any> {
id: string // Relation to update
type?: VerbType // Change verb type
subtype?: string // Change sub-classification (omit to preserve existing)
/**
* Change the visibility tier. Omit to preserve the existing value. The verb mirror of
* `UpdateParams.visibility`; `'system'` is Brainy-internal consumer code should only
* use `'public'` / `'internal'`.
*/
visibility?: EntityVisibility
weight?: number // New weight
confidence?: number // New confidence (0-1)
data?: any // New content
@ -448,7 +496,24 @@ export interface FindParams<T = any> {
subtype?: string | string[]
/** Metadata filters using BFO operators (e.g., `{ year: { greaterThan: 2020 } }`) */
where?: Partial<T>
// Visibility
/**
* Also return `visibility: 'internal'` entities. By default `find()` returns ONLY
* public entities (those with no `visibility` field, or `visibility: 'public'`);
* app-internal entities are hidden. Set `true` to include them. Applied as a hard
* candidate filter, so `limit` / `offset` stay correct. Does NOT include `'system'`
* entities use `includeSystem` for those.
*/
includeInternal?: boolean
/**
* Also return `visibility: 'system'` entities (Brainy's own plumbing, e.g. the VFS
* root). Hidden by default and even when `includeInternal` is set. Set `true` only
* when you specifically need to see system entities. Applied as a hard candidate
* filter, so `limit` / `offset` stay correct.
*/
includeSystem?: boolean
// Graph Intelligence
connected?: GraphConstraints
@ -609,6 +674,25 @@ export interface RelatedParams {
*/
subtype?: string | string[]
/**
* Also return `visibility: 'internal'` relationships.
*
* By default `related()` returns ONLY public relationships (those with no
* `visibility` field, or `visibility: 'public'`); app-internal edges are hidden.
* Set `true` to include them. Applied as a hard candidate filter so `limit` /
* `offset` stay correct. Does NOT include `'system'` edges use `includeSystem`.
*/
includeInternal?: boolean
/**
* Also return `visibility: 'system'` relationships (Brainy's own plumbing).
*
* Hidden by default and even when `includeInternal` is set. Set `true` only when
* you specifically need system edges. Applied as a hard candidate filter so
* `limit` / `offset` stay correct.
*/
includeSystem?: boolean
/**
* Maximum number of results to return
*

View file

@ -35,6 +35,7 @@
* |-----|----------------------|
* | `noun` | the `type` param of `add()` / `update()` (stored under the key `noun`) |
* | `subtype` | the `subtype` param |
* | `visibility` | the `visibility` param (`'public'` \| `'internal'`; `'system'` is Brainy-only) |
* | `createdAt` | system-managed set once at `add()` time |
* | `updatedAt` | system-managed set on every write |
* | `confidence` | the `confidence` param |
@ -52,6 +53,7 @@
export const RESERVED_ENTITY_FIELDS = [
'noun',
'subtype',
'visibility',
'createdAt',
'updatedAt',
'confidence',
@ -78,6 +80,7 @@ export type ReservedEntityField = (typeof RESERVED_ENTITY_FIELDS)[number]
* |-----|----------------------|
* | `verb` | the `type` param of `relate()` / `updateRelation()` (stored under the key `verb`) |
* | `subtype` | the `subtype` param |
* | `visibility` | the `visibility` param (`'public'` \| `'internal'`; `'system'` is Brainy-only) |
* | `createdAt` | system-managed set once at `relate()` time |
* | `updatedAt` | system-managed set on every write |
* | `confidence` | the `confidence` param |
@ -90,6 +93,7 @@ export type ReservedEntityField = (typeof RESERVED_ENTITY_FIELDS)[number]
export const RESERVED_RELATION_FIELDS = [
'verb',
'subtype',
'visibility',
'createdAt',
'updatedAt',
'confidence',