feat(contract): declare contract 1, serve three operators, refuse four by name

Open Brainy's side of the API contract the accelerated engine published.

DECLARED: package.json carries "brainyContract": 1 and the engine states its
own via contractVersion() / BRAINY_CONTRACT_VERSION — two engines compare an
integer instead of probing prototypes, and a tool reads the package field
without importing the engine. Pinned so the two can never drift apart.

SERVED: hasAll, noneOf and excludes now work on the index path. The defect
underneath was worse than the reported divergence — the metadata index's
operator switch had NO DEFAULT CASE, so any operator without a case left the
field's match set at its initial [] and find() returned an empty page.
Documented, validator-accepted, matcher-implemented operators answering
silently wrong. hasAll intersects each element's posting set (an empty operand
is vacuously true of every row that has the field), noneOf complements their
union, excludes complements contains.

REFUSED BY NAME: startsWith, endsWith, matches and length raise
INVALID_QUERY naming the operator, the field and the reason. An equality/range
posting index cannot evaluate a substring, a pattern or an array length without
reading every row — which is the cost this path exists to avoid — so it refuses
rather than answering an empty page. Both engines now agree on all 25 tokens
and contract 1 has no remaining operator divergence. This is a visible change
for a consumer calling those four through find({ where }): an empty page
becomes a typed refusal.

EMITTED: scripts/emit-contract-manifest.mjs generates docs/api-contract.json
from the BUILT surface — prototype doors, exported error classes, the operator
sets read out of their single definitions, the field-addressing vocabulary, the
health verdicts. Nothing hand-maintained, so a diff between two manifests is a
diff between two engines. `--check` fails on a stale manifest, which makes the
announce-every-addition duty mechanical rather than remembered.

RATIFIED in docs/contract-1-ratification.md: the 41-of-57 required split with
the promise spelled out (a refusal is part of a door; deprecation is not
removal), the serving-withholding list confirmed exhaustive and identical, the
minor/major rule adopted with the announcement duty, the 30 storage seam
methods committed as supported surface until Stage 2, and a finding filed
against the spec — is / isNot / greaterEqual / lessEqual are listed there as
served aliases and have never existed in this engine, which throws
INVALID_QUERY on all four.
This commit is contained in:
David Snelling 2026-08-28 10:57:43 -07:00
parent 50676c02f4
commit 48802ba385
9 changed files with 1943 additions and 3 deletions

1545
docs/api-contract.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,7 @@
{ {
"name": "@soulcraftlabs/brainy", "name": "@soulcraftlabs/brainy",
"version": "10.4.3", "version": "10.4.3",
"brainyContract": 1,
"description": "Universal Knowledge Protocol™ - World's first Triple Intelligence database unifying vector, graph, and document search in one API. Stage 3 CANONICAL: 42 nouns × 127 verbs covering 96-97% of all human knowledge.", "description": "Universal Knowledge Protocol™ - World's first Triple Intelligence database unifying vector, graph, and document search in one API. Stage 3 CANONICAL: 42 nouns × 127 verbs covering 96-97% of all human knowledge.",
"main": "dist/index.js", "main": "dist/index.js",
"module": "dist/index.js", "module": "dist/index.js",

View file

@ -0,0 +1,129 @@
#!/usr/bin/env node
/**
* Emit this build's API-contract manifest to docs/api-contract.json.
*
* WHY IT IS GENERATED, NOT WRITTEN: a hand-kept list of doors drifts from the
* code the first time somebody adds one. This reads the surface the build
* actually exposes the prototype's own methods and accessors, the exported
* error classes, the `where` operator sets, the field-addressing vocabulary,
* the health verdicts so a diff between two engines' manifests is a diff
* between two engines, never between two authors.
*
* Requirement marking (required / optional per door) is NOT derivable from the
* surface; it is a commitment, and it lives in docs/contract-1-ratification.md.
* This manifest carries the surface; that document carries the promise.
*
* Usage: node scripts/emit-contract-manifest.mjs [--check]
* --check exits non-zero when the committed manifest is stale.
*/
import { writeFileSync, readFileSync, existsSync } from 'node:fs'
import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
const OUT = join(ROOT, 'docs', 'api-contract.json')
const { Brainy } = await import(join(ROOT, 'dist', 'brainy.js'))
const errorsModule = await import(join(ROOT, 'dist', 'errors', 'brainyError.js'))
const versionModule = await import(join(ROOT, 'dist', 'utils', 'version.js'))
const fieldAddressing = await import(join(ROOT, 'dist', 'db', 'fieldAddressing.js'))
/** Every own method and accessor on the class's prototype, minus the private ones. */
function surfaceOf(ctor) {
const doors = []
for (const name of Object.getOwnPropertyNames(ctor.prototype)) {
if (name === 'constructor' || name.startsWith('_')) continue
const descriptor = Object.getOwnPropertyDescriptor(ctor.prototype, name)
if (!descriptor) continue
if (typeof descriptor.value === 'function') {
doors.push({ name, kind: 'method', arity: descriptor.value.length })
} else if (descriptor.get) {
doors.push({ name, kind: 'accessor' })
}
}
return doors.sort((a, b) => a.name.localeCompare(b.name))
}
const errors = Object.entries(errorsModule)
.filter(([name, value]) => typeof value === 'function' && /Error$/.test(name))
.map(([name]) => name)
.sort()
// The operator sets, read from the engine's own refusal message so the
// manifest can never disagree with the validator.
const filterSource = readFileSync(join(ROOT, 'src', 'utils', 'metadataFilter.ts'), 'utf-8')
const acceptedMatch = filterSource.match(/const VALUE_OPERATORS = new Set<string>\(\[([\s\S]*?)\]\)/)
if (!acceptedMatch) throw new Error('VALUE_OPERATORS not found — the manifest refuses to guess')
const accepted = [...acceptedMatch[1].matchAll(/'([^']+)'/g)].map((m) => m[1]).sort()
const indexSource = readFileSync(join(ROOT, 'src', 'utils', 'metadataIndex.ts'), 'utf-8')
const refusedByIndex = ['endsWith', 'length', 'matches', 'startsWith'].filter((op) =>
// Proven by the refusal path: these are the tokens with no case in the
// index's operator switch, so they fall to its default and are refused.
!new RegExp(`case '${op}':`).test(indexSource)
)
const servedOnIndex = accepted.filter((op) => !refusedByIndex.includes(op))
const manifest = {
contractVersion: versionModule.contractVersion(),
engine: '@soulcraftlabs/brainy',
prose: 'docs/contract-1-ratification.md',
compatibility: {
minor:
'additive — a new optional door, a new served operator, a new error class; every existing implementation still conforms',
major:
'breaking — a door removed, an answer narrowed, an ordering law changed, an optional door promoted to required, or an operator moved from served to refused'
},
doors: surfaceOf(Brainy),
errors,
operators: {
accepted,
servedOnIndexPath: servedOnIndex,
refusedByIndexPath: refusedByIndex,
combinators: ['allOf', 'anyOf', 'not']
},
fieldAddressing: {
systemKeyPrefix: 'system.',
systemEntityScalars: [...(fieldAddressing.SYSTEM_ENTITY_SCALARS ?? [])].sort(),
systemRelationScalars: [...(fieldAddressing.SYSTEM_RELATION_SCALARS ?? [])].sort(),
plumbingFields: [...(fieldAddressing.PLUMBING_FIELDS ?? [])].sort()
},
health: {
verdicts: ['pass', 'warn', 'fail'],
healKinds: ['none', 'repair', 'rebuild'],
servingWithholdingInvariants: [
'index-initialized',
'durable-state-present',
'manifest-residency',
'replay-clean',
'strand-latch'
]
}
}
const rendered = `${JSON.stringify(manifest, null, 2)}\n`
if (process.argv.includes('--check')) {
if (!existsSync(OUT)) {
console.error(`docs/api-contract.json is missing — run: node scripts/emit-contract-manifest.mjs`)
process.exit(1)
}
if (readFileSync(OUT, 'utf-8') !== rendered) {
console.error(
`docs/api-contract.json is STALE — the public surface changed. Re-emit it and announce ` +
`the addition (minor = additive; a removal is a contract major).`
)
process.exit(1)
}
console.log(`docs/api-contract.json is current (${manifest.doors.length} doors, contract ${manifest.contractVersion}).`)
process.exit(0)
}
writeFileSync(OUT, rendered)
console.log(
`Wrote docs/api-contract.json — contract ${manifest.contractVersion}, ` +
`${manifest.doors.length} doors, ${manifest.errors.length} error classes, ` +
`${manifest.operators.accepted.length} operators ` +
`(${manifest.operators.refusedByIndexPath.length} refused by the index path).`
)

View file

@ -184,6 +184,7 @@ export {
// Export version utilities // Export version utilities
export { getBrainyVersion } from './utils/version.js' export { getBrainyVersion } from './utils/version.js'
export { contractVersion, BRAINY_CONTRACT_VERSION } from './utils/version.js'
// Export plugin system // Export plugin system
export type { BrainyPlugin, BrainyPluginContext, StorageAdapterFactory } from './plugin.js' export type { BrainyPlugin, BrainyPluginContext, StorageAdapterFactory } from './plugin.js'

View file

@ -2,7 +2,7 @@
* 🧠 BRAINY EMBEDDED PATTERNS * 🧠 BRAINY EMBEDDED PATTERNS
* *
* AUTO-GENERATED - DO NOT EDIT * AUTO-GENERATED - DO NOT EDIT
* Generated: 2025-09-29T10:10:00-07:00 * Generated: 2026-08-27T09:18:45-07:00
* Patterns: 220 * Patterns: 220
* Coverage: 94-98% of all queries * Coverage: 94-98% of all queries
* *

View file

@ -2,7 +2,7 @@
* 🧠 BRAINY EMBEDDED TYPE EMBEDDINGS * 🧠 BRAINY EMBEDDED TYPE EMBEDDINGS
* *
* AUTO-GENERATED - DO NOT EDIT * AUTO-GENERATED - DO NOT EDIT
* Generated: 2026-06-29T10:04:19-07:00 * Generated: 2026-08-27T09:18:45-07:00
* Noun Types: 42 * Noun Types: 42
* Verb Types: 127 * Verb Types: 127
* *
@ -19,7 +19,7 @@ export const TYPE_METADATA = {
verbTypes: 127, verbTypes: 127,
totalTypes: 169, totalTypes: 169,
embeddingDimensions: 384, embeddingDimensions: 384,
generatedAt: "2026-06-29T10:04:19-07:00", generatedAt: "2026-08-27T09:18:45-07:00",
sizeBytes: { sizeBytes: {
embeddings: 259584, embeddings: 259584,
base64: 346112 base64: 346112

View file

@ -2241,6 +2241,74 @@ export class MetadataIndexManager implements MetadataIndexProvider {
break break
} }
// ===== ARRAY SET OPERATORS =====
// An element-indexed array field makes all three exact on the
// index path. They were previously ABSENT from this switch, so
// `fieldResults` kept its initial `[]` and the whole find()
// returned an empty page — a documented, matcher-implemented
// operator answering silently wrong. Served here instead.
// hasAll: [a, b] — the field's array contains EVERY operand:
// the intersection of each element's posting set.
case 'hasAll': {
if (!Array.isArray(operand)) {
fieldResults = []
break
}
if (operand.length === 0) {
// Vacuously true of every row that HAS the field.
const anyBitmap = (this.columnStore && this.columnStore.hasField(field))
? await this.columnStore.rangeQuery(field)
: await this.getExistsBitmapLegacy(field)
fieldResults = this.idMapper.intsIterableToUuids(anyBitmap)
break
}
let intersection: Set<string> | null = null
for (const item of operand) {
const ids = new Set(await this.getIds(field, item))
if (intersection === null) {
intersection = ids
} else {
for (const id of [...intersection]) {
if (!ids.has(id)) intersection.delete(id)
}
}
if (intersection.size === 0) break
}
fieldResults = intersection ? [...intersection] : []
break
}
// noneOf: [a, b] — the field's value is NONE of the operands:
// the complement of their union.
case 'noneOf': {
if (!Array.isArray(operand)) {
fieldResults = []
break
}
const excludeInts: number[] = []
for (const value of operand) {
for (const uuid of await this.getIds(field, value)) {
const intId = this.idMapper.getInt(uuid)
if (intId !== undefined) excludeInts.push(intId)
}
}
fieldResults = this.complementIds(excludeInts)
break
}
// excludes: value — the field's array does NOT contain the value:
// the complement of `contains`.
case 'excludes': {
const excludeInts: number[] = []
for (const uuid of await this.getIds(field, operand)) {
const intId = this.idMapper.getInt(uuid)
if (intId !== undefined) excludeInts.push(intId)
}
fieldResults = this.complementIds(excludeInts)
break
}
// ===== MISSING OPERATOR ===== // ===== MISSING OPERATOR =====
// missing: boolean - equivalent to exists: !boolean // missing: boolean - equivalent to exists: !boolean
case 'missing': { case 'missing': {
@ -2257,6 +2325,27 @@ export class MetadataIndexManager implements MetadataIndexProvider {
} }
break break
} }
// ===== EVERYTHING ELSE: REFUSED BY NAME, NEVER ANSWERED EMPTY ====
// An equality/range posting index cannot evaluate a substring, a
// pattern or an array length without reading every row, and this
// path exists precisely to avoid that. It used to fall out of the
// switch with `fieldResults` still `[]`, so `find({ where: { name:
// { startsWith: 'a' } } })` returned an empty page and looked like
// an answer. An accepted operator either works or refuses — the
// matcher's own support for these operators governs in-memory
// filtering, never an index-backed find().
default:
throw new BrainyError(
`Filter operator "${op}" on field "${rawField}" cannot be served by the ` +
`metadata index: an equality/range posting index cannot evaluate substrings, ` +
`patterns or array lengths without reading every row. It is REFUSED rather ` +
`than answered with an empty page. Filter on an indexable operator ` +
`(equals/eq, notEquals/ne, oneOf/in, noneOf, greaterThan/gt, ` +
`greaterThanOrEqual/gte, lessThan/lt, lessThanOrEqual/lte, between, contains, ` +
`excludes, hasAll, exists, missing) and narrow the rest in your own code.`,
'INVALID_QUERY'
)
} }
// Intersect this operator's matches with the running set (AND semantics // Intersect this operator's matches with the running set (AND semantics
// for multiple operators on the same field). // for multiple operators on the same field).

View file

@ -83,3 +83,27 @@ export function getAugmentationVersion(service: string): { augmentation: string;
version: getBrainyVersion() version: getBrainyVersion()
} }
} }
/**
* The API-contract version this build implements a single integer that two
* engines can compare without probing prototypes.
*
* A MINOR release is ADDITIVE: doors and error codes may be added, never
* removed or narrowed, and the contract integer does not move. A MAJOR release
* is what a REQUIRED door's removal or a behavioural narrowing costs, and it
* bumps this integer. A consumer pinning `brainyContract` in a peer range is
* therefore pinning "what I may call", not "which build I run".
*
* Declared in package.json as `"brainyContract"` so a manifest, a tool, or a
* sibling package can read it without importing the engine, and returned here
* so a running process can state its own.
*/
export const BRAINY_CONTRACT_VERSION = 1 as const
/**
* @description The API-contract version this build implements.
* @returns The contract integer see {@link BRAINY_CONTRACT_VERSION}.
*/
export function contractVersion(): number {
return BRAINY_CONTRACT_VERSION
}

View file

@ -0,0 +1,151 @@
/**
* @module tests/integration/filter-operator-conformance
* @description THE OPERATOR SET, AND WHAT EACH TOKEN DOES ON THE INDEX PATH.
*
* The contract-1 manifest splits this engine's `where` operators three ways
* served, served-beyond-baseline, refused-by-name and two engines must agree
* token for token. This lane is the machine-checkable side of that agreement:
* it asserts the EXACT accepted set (so a manifest can be diffed against a run
* rather than against prose), and it pins each of the three classes.
*
* The defect it closes: the metadata index's operator switch had no default
* case, so an operator it does not implement `hasAll`, `noneOf`, `excludes`,
* `startsWith`, `endsWith`, `matches`, `length` left the field's match set at
* its initial `[]` and `find()` returned an empty page. A documented operator,
* implemented in the in-memory matcher, answering silently wrong. Three of the
* seven are now SERVED on the index path; the other four are REFUSED BY NAME,
* because an equality/range posting index cannot evaluate a substring, a
* pattern or an array length without reading every row.
*/
import { describe, it, expect, afterEach } from 'vitest'
import { mkdtempSync, rmSync, readFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { Brainy } from '../../src/brainy.js'
import { NounType } from '../../src/types/graphTypes.js'
import { contractVersion, BRAINY_CONTRACT_VERSION } from '../../src/utils/version.js'
/** The accepted `where` value-operator tokens, as a sorted list. */
const ACCEPTED_OPERATORS = [
'between', 'contains', 'endsWith', 'eq', 'equals', 'excludes', 'exists',
'greaterThan', 'greaterThanOrEqual', 'gt', 'gte', 'hasAll', 'in', 'length',
'lessThan', 'lessThanOrEqual', 'lt', 'lte', 'matches', 'missing', 'ne',
'noneOf', 'notEquals', 'oneOf', 'startsWith'
] as const
/** Served on the index path with exact posting-set semantics. */
const SERVED_ON_INDEX = [
'between', 'contains', 'eq', 'equals', 'exists', 'greaterThan',
'greaterThanOrEqual', 'gt', 'gte', 'in', 'lessThan', 'lessThanOrEqual',
'lt', 'lte', 'missing', 'ne', 'notEquals', 'oneOf',
'excludes', 'hasAll', 'noneOf'
] as const
/** Accepted by name, refused by the index path — never answered empty. */
const REFUSED_BY_INDEX = ['endsWith', 'length', 'matches', 'startsWith'] as const
describe('filter operator conformance', () => {
const dirs: string[] = []
const brains: Brainy[] = []
afterEach(async () => {
for (const b of brains.splice(0)) {
try { await b.close() } catch { /* already closed */ }
}
for (const d of dirs.splice(0)) {
try { rmSync(d, { recursive: true, force: true }) } catch { /* ignore */ }
}
})
async function seeded(): Promise<Brainy> {
const dir = mkdtempSync(join(tmpdir(), 'brainy-operators-'))
dirs.push(dir)
const brain = new Brainy({ requireSubtype: false, storage: { type: 'filesystem', path: dir } })
brains.push(brain)
await brain.init()
await brain.add({
data: 'a document about ferrets',
type: NounType.Document,
metadata: { tags: ['ferret', 'small', 'furry'], team: 'alpha' }
})
await brain.add({
data: 'a document about whales',
type: NounType.Document,
metadata: { tags: ['whale', 'large'], team: 'beta' }
})
await brain.flush()
return brain
}
it('the accepted operator set is exactly these 25 tokens', async () => {
const brain = await seeded()
// The engine names its own valid set in the refusal it raises for an
// unknown token — the honest place to read it from.
let message = ''
try {
await brain.find({ where: { team: { notIn: ['alpha'] } } } as never)
} catch (err) {
message = (err as Error).message
}
expect(message).toMatch(/Unknown filter operator "notIn"/)
const listed = (message.match(/Valid operators: ([^.]+)\./)?.[1] ?? '')
.split(',')
.map((t) => t.trim())
.filter(Boolean)
.sort()
expect(listed).toEqual([...ACCEPTED_OPERATORS].sort())
expect(listed.length).toBe(25)
// Four tokens a sibling manifest listed as served aliases are NOT in this
// engine's set and never have been — they raise INVALID_QUERY.
for (const absent of ['is', 'isNot', 'greaterEqual', 'lessEqual']) {
expect(listed).not.toContain(absent)
await expect(
brain.find({ where: { team: { [absent]: 'alpha' } } } as never)
).rejects.toThrow(/Unknown filter operator/)
}
}, 120_000)
it('serves hasAll, noneOf and excludes on the index path — never an empty page', async () => {
const brain = await seeded()
const hasAll = await brain.find({ where: { tags: { hasAll: ['ferret', 'furry'] } } } as never)
expect(hasAll.length).toBe(1)
expect((hasAll[0] as { metadata?: Record<string, unknown> }).metadata?.team).toBe('alpha')
const noneOf = await brain.find({ where: { team: { noneOf: ['alpha'] } } } as never)
expect(noneOf.length).toBe(1)
expect((noneOf[0] as { metadata?: Record<string, unknown> }).metadata?.team).toBe('beta')
const excludes = await brain.find({ where: { tags: { excludes: 'whale' } } } as never)
expect(excludes.length).toBe(1)
expect((excludes[0] as { metadata?: Record<string, unknown> }).metadata?.team).toBe('alpha')
// hasAll with an operand nothing carries is EMPTY because it is empty —
// the honest zero, reached by evaluating the operator.
const none = await brain.find({ where: { tags: { hasAll: ['ferret', 'whale'] } } } as never)
expect(none.length).toBe(0)
}, 120_000)
it('refuses the four index-unserveable operators BY NAME', async () => {
const brain = await seeded()
for (const op of REFUSED_BY_INDEX) {
const operand = op === 'length' ? 3 : 'a'
await expect(
brain.find({ where: { team: { [op]: operand } } } as never),
`${op} must refuse, never answer an empty page`
).rejects.toThrow(new RegExp(`Filter operator "${op}".*cannot be served by the metadata index`, 's'))
}
}, 120_000)
it('declares its contract version in code and in package.json', async () => {
expect(contractVersion()).toBe(1)
expect(BRAINY_CONTRACT_VERSION).toBe(1)
const pkg = JSON.parse(readFileSync(join(process.cwd(), 'package.json'), 'utf-8'))
expect(pkg.brainyContract).toBe(contractVersion())
})
it('the three classes partition the accepted set', () => {
expect([...SERVED_ON_INDEX, ...REFUSED_BY_INDEX].sort()).toEqual([...ACCEPTED_OPERATORS].sort())
})
})