2025-09-12 14:37:39 -07:00
|
|
|
import { describe, it, expect, beforeEach } from 'vitest'
|
|
|
|
|
import {
|
|
|
|
|
validateFindParams,
|
|
|
|
|
validateAddParams,
|
|
|
|
|
validateUpdateParams,
|
|
|
|
|
validateRelateParams,
|
|
|
|
|
getValidationConfig,
|
|
|
|
|
recordQueryPerformance
|
|
|
|
|
} from '../../../src/utils/paramValidation.js'
|
|
|
|
|
import { NounType, VerbType } from '../../../src/types/graphTypes.js'
|
|
|
|
|
import { FindParams, AddParams, UpdateParams, RelateParams } from '../../../src/types/brainy.types.js'
|
|
|
|
|
|
|
|
|
|
describe('Zero-Config Parameter Validation', () => {
|
|
|
|
|
|
|
|
|
|
describe('validateFindParams', () => {
|
|
|
|
|
|
|
|
|
|
it('should accept valid parameters', () => {
|
|
|
|
|
expect(() => validateFindParams({
|
|
|
|
|
query: 'test query',
|
|
|
|
|
limit: 10,
|
|
|
|
|
offset: 0
|
|
|
|
|
})).not.toThrow()
|
|
|
|
|
|
|
|
|
|
expect(() => validateFindParams({
|
|
|
|
|
type: NounType.Document,
|
|
|
|
|
where: { status: 'active' }
|
|
|
|
|
})).not.toThrow()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should reject negative limit', () => {
|
|
|
|
|
expect(() => validateFindParams({
|
|
|
|
|
limit: -1
|
|
|
|
|
})).toThrow('limit must be non-negative')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should reject negative offset', () => {
|
|
|
|
|
expect(() => validateFindParams({
|
|
|
|
|
offset: -1
|
|
|
|
|
})).toThrow('offset must be non-negative')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should reject threshold outside 0-1 range', () => {
|
|
|
|
|
expect(() => validateFindParams({
|
|
|
|
|
near: { id: 'test', threshold: -0.1 }
|
|
|
|
|
})).toThrow('threshold must be between 0 and 1')
|
|
|
|
|
|
|
|
|
|
expect(() => validateFindParams({
|
|
|
|
|
near: { id: 'test', threshold: 1.1 }
|
|
|
|
|
})).toThrow('threshold must be between 0 and 1')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should reject both query and vector', () => {
|
|
|
|
|
expect(() => validateFindParams({
|
|
|
|
|
query: 'test',
|
|
|
|
|
vector: new Array(384).fill(0)
|
|
|
|
|
})).toThrow('cannot specify both query and vector')
|
|
|
|
|
})
|
|
|
|
|
|
feat(namespace): NO SPECIAL NAMES + storage fidelity — the ruled completion of the field-addressing law
The write side of the law, ruled 2026-08-03: data is either in main space
where developers can use anything, or it is in system.*.
- The reserved-name write door DIES: add/update/relate/updateRelation
metadata bags accept EVERY name (confidence, type, id, data, level,
content, ...) as ordinary user fields — indexed, filterable, sortable,
aggregatable, identical to any other field. The remap/enforce/warn
machinery, the reservedFieldPolicy config (now a typed init refusal),
and the compile-time metadata key bans are all removed. The one write
refusal left: keys spelled 'system.*' (namespace forgery), now enforced
on all four write doors.
- STORED RECORDS GO NESTED (v2): engine fields top-level, the user bag
nested verbatim under 'metadata', sealed by a format stamp — by-name
storage discrimination is unsound once colliders are admitted. Legacy
flat records stay readable forever through the shape-aware splitters
(sound for them: the old door refused colliders). Time travel rides the
same split (generation store snapshots whole records).
- Name-based index exclusions DIE: user frame indexes every name; the
excludeFields/indexedFields knobs and their silent-[] holes are gone;
bulk-payload protection is value-shape only, uniform across names.
- Consumer-sweep findings fixed in the same wave: per-type counts read
the frozen 'system.type' column (addToIndex sort, affinity tracking,
cold-count rehydration, VFS type bitmaps — legacy 'noun' fallback for
pre-rebuild reads); resolveHiddenIds addresses 'system.visibility'
(bare 'visibility' was a silent no-op under the law — VFS/system
entities leaked into default reads).
- Fidelity fallout fixed in the owning layers: readEntityFieldAddress
reads the bag first (colliders were absent-shadowed by its own guard)
and never serves system addresses from the bag; blob history refs read
the bag shape-aware; migration transforms now receive ONE normalized
view (engine fields + nested bag) regardless of stored era, and stray
flat-habit keys refuse with the fix in the message.
- THE REOPEN-COLLIDER CONFORMANCE CASE (required before any RC counts as
gates-green): all ten collider names + plumbing names written as user
fields, verified verbatim + queryable across live reads, flush+reopen,
a forced epoch rebuild, and asOf time travel; relation mirror; forgery
refusals; legacy flat-record compat. 8/8 green.
Gates: unit 1901/1901 (exit 0) · integration 758 (exit 0) · conformance
27/27 (exit 0) · consumer test sweep migrated (10 files).
2026-08-03 16:59:13 -07:00
|
|
|
it('should refuse cursor outright — even paired with offset — as an unimplemented option', () => {
|
|
|
|
|
// cursor is now a typed, unconditional refusal (UnsupportedFindOptionError):
|
|
|
|
|
// it used to be accepted-and-ignored, only conflicting when offset was also
|
|
|
|
|
// given. Accepted-and-ignored died as a class — cursor refuses on its own,
|
|
|
|
|
// so pairing it with offset refuses too, but with the SAME message.
|
2025-09-12 14:37:39 -07:00
|
|
|
expect(() => validateFindParams({
|
|
|
|
|
cursor: 'abc123',
|
|
|
|
|
offset: 10
|
feat(namespace): NO SPECIAL NAMES + storage fidelity — the ruled completion of the field-addressing law
The write side of the law, ruled 2026-08-03: data is either in main space
where developers can use anything, or it is in system.*.
- The reserved-name write door DIES: add/update/relate/updateRelation
metadata bags accept EVERY name (confidence, type, id, data, level,
content, ...) as ordinary user fields — indexed, filterable, sortable,
aggregatable, identical to any other field. The remap/enforce/warn
machinery, the reservedFieldPolicy config (now a typed init refusal),
and the compile-time metadata key bans are all removed. The one write
refusal left: keys spelled 'system.*' (namespace forgery), now enforced
on all four write doors.
- STORED RECORDS GO NESTED (v2): engine fields top-level, the user bag
nested verbatim under 'metadata', sealed by a format stamp — by-name
storage discrimination is unsound once colliders are admitted. Legacy
flat records stay readable forever through the shape-aware splitters
(sound for them: the old door refused colliders). Time travel rides the
same split (generation store snapshots whole records).
- Name-based index exclusions DIE: user frame indexes every name; the
excludeFields/indexedFields knobs and their silent-[] holes are gone;
bulk-payload protection is value-shape only, uniform across names.
- Consumer-sweep findings fixed in the same wave: per-type counts read
the frozen 'system.type' column (addToIndex sort, affinity tracking,
cold-count rehydration, VFS type bitmaps — legacy 'noun' fallback for
pre-rebuild reads); resolveHiddenIds addresses 'system.visibility'
(bare 'visibility' was a silent no-op under the law — VFS/system
entities leaked into default reads).
- Fidelity fallout fixed in the owning layers: readEntityFieldAddress
reads the bag first (colliders were absent-shadowed by its own guard)
and never serves system addresses from the bag; blob history refs read
the bag shape-aware; migration transforms now receive ONE normalized
view (engine fields + nested bag) regardless of stored era, and stray
flat-habit keys refuse with the fix in the message.
- THE REOPEN-COLLIDER CONFORMANCE CASE (required before any RC counts as
gates-green): all ten collider names + plumbing names written as user
fields, verified verbatim + queryable across live reads, flush+reopen,
a forced epoch rebuild, and asOf time travel; relation mirror; forgery
refusals; legacy flat-record compat. 8/8 green.
Gates: unit 1901/1901 (exit 0) · integration 758 (exit 0) · conformance
27/27 (exit 0) · consumer test sweep migrated (10 files).
2026-08-03 16:59:13 -07:00
|
|
|
})).toThrow("find() option 'cursor' is not implemented")
|
2025-09-12 14:37:39 -07:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should validate vector dimensions', () => {
|
|
|
|
|
expect(() => validateFindParams({
|
|
|
|
|
vector: new Array(100).fill(0) // Wrong dimensions
|
|
|
|
|
})).toThrow('vector must have exactly 384 dimensions')
|
|
|
|
|
|
|
|
|
|
expect(() => validateFindParams({
|
|
|
|
|
vector: new Array(384).fill(0) // Correct dimensions
|
|
|
|
|
})).not.toThrow()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should validate NounType enum', () => {
|
|
|
|
|
expect(() => validateFindParams({
|
|
|
|
|
type: 'InvalidType' as any
|
|
|
|
|
})).toThrow('invalid NounType: InvalidType')
|
|
|
|
|
|
|
|
|
|
expect(() => validateFindParams({
|
|
|
|
|
type: NounType.Document
|
|
|
|
|
})).not.toThrow()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should validate array of NounTypes', () => {
|
|
|
|
|
expect(() => validateFindParams({
|
|
|
|
|
type: [NounType.Document, NounType.Person]
|
|
|
|
|
})).not.toThrow()
|
|
|
|
|
|
|
|
|
|
expect(() => validateFindParams({
|
|
|
|
|
type: [NounType.Document, 'InvalidType' as any]
|
|
|
|
|
})).toThrow('invalid NounType: InvalidType')
|
|
|
|
|
})
|
|
|
|
|
|
fix: recalibrate find({ limit }) cap + two-tier enforcement + caller location
Brainy 7.30.0 introduced a memory-derived synchronous cap on `find({ limit })`
to prevent OOM. The cap was sound in intent but ~4x too conservative in
calibration: assumed 100 KB per result while typical entity footprint is 7-10 KB
(384-dim float32 vector ≈ 1.5 KB + standard fields + metadata). On a 900 MB
free-memory box the cap derived to 9000 — breaking common safety-cap patterns
like `find({ type, where, limit: 10_000 })` that typically return 10-500
entities. Surfaced as a runtime regression with cascading 500s degrading
production dashboards.
Three concurrent fixes:
A. RECALIBRATE THE FORMULA
- src/utils/paramValidation.ts:175,196,212 — the three memory-derived priorities
(reservedQueryMemory / containerMemory / freeMemory) all divided by
100 * 1024 * 1024 (100 KB per result, ~10-15x over conservative). Replaced
with a new MAX_LIMIT_KB_PER_RESULT = 25 constant that matches observed
entity size.
- Result: 4 GB container cap goes 10_000 → 40_000; 2 GB cap goes 5_000 →
20_000; 900 MB free-memory cap goes 9_000 → ~36_000. 100k hard ceiling
unchanged. `maxQueryLimit` / `reservedQueryMemory` constructor overrides
unchanged in behavior.
B. TWO-TIER ENFORCEMENT (warn-then-throw)
- Below cap (limit <= maxLimit): silent pass, unchanged.
- Soft tier (maxLimit < limit <= 2 * maxLimit): NEW — one-time warning per
call site (dedup keyed on caller stack frame + limit value), query
proceeds. Pre-7.30.2 code that relied on the cap silently allowing typical
safety-cap limits keeps working; the warning teaches the recipe so consumers
can fix it intentionally.
- Hard tier (limit > 2 * maxLimit): throw with the same teaching message
format. Real OOM territory; the cap stops being a recommendation and becomes
a guardrail.
- The 2x soft margin absorbs typical safety-cap patterns (limit: 10_000
against a 9 K-cap box) without disabling OOM protection. Real OOM territory
on a JS in-memory brain is hundreds of thousands of results, not 10x the
safety cap.
C. IMPROVED ERROR / WARNING MESSAGE
- Same shape as the 7.30.1 enforcement-error messages: state the problem,
name the three escape valves (maxQueryLimit / reservedQueryMemory /
pagination), include caller location, link to docs.
- Extracted findCallerLocation() helper from brainy.ts to a new
src/utils/callerLocation.ts so both the subtype enforcement (7.30.1) and
the limit enforcement (7.30.2) share one implementation without circular
imports.
DOCS
- New docs/guides/find-limits.md (public: true) — full reference: why the cap
exists, the four memory sources the auto-config considers, the three escape
valves with when-to-use-which guidance, and an explicit "pagination is the
future-proof pattern" callout (8.0 may tighten the cap further; pagination
keeps working unchanged).
- docs/api/README.md find() entry gets a one-paragraph `limit` tip + pointer
to the new guide.
- RELEASES.md v7.30.2 entry.
TESTS
- New tests/integration/find-limits.test.ts (9 tests): below-cap silent pass;
soft-tier warns once per call site (dedup verified by exercising same vs.
different source lines via wrapper closures); soft-tier message format
(names all three escape valves + docs link); soft-tier message includes
caller location; hard-tier throws; hard-tier message format same as
soft-tier; consumer maxQueryLimit override raises the cap and shifts both
tiers accordingly; pre-7.30.2 regression scenario explicitly covered.
- tests/unit/utils/memoryLimits.test.ts — 4 tests updated for the recalibrated
cap values (hardcoded expected numbers bumped 4x to match new 25 KB/result
assumption).
- tests/unit/utils/paramValidation.test.ts — auto-limit test extended to cover
the three-tier semantics (below-cap pass / soft-tier silent / hard-tier
throw).
- Existing suites unchanged: subtype-and-facets 26/26, verb-subtype-and-
enforcement 30/30, strict-mode-self-test 13/13. Unit 1468/1468.
CORTEX COMPATIBILITY
- Zero Cortex changes required. Every change is JS-side: formula recalibration
runs in ValidationConfig.constructor(), two-tier enforcement runs in
validateFindParams(), both fire before any storage / index / Cortex call.
- The new guide notes that Brainy 8.0's Datomic-style Db.find() may tighten
per-call limits to keep snapshot semantics cheap; pagination remains the
pattern that's guaranteed to keep working.
REPO-WIDE CLEANUP
Brainy is the only Soulcraft project that is open source. This commit also
scrubs closed-source product names and product-specific class/field references
from every tracked file in the repo (src/, docs/, tests/, RELEASES.md,
CHANGELOG.md). Consumer-reported bugs, regression scenarios, and release
notes now refer to "a consumer", "a downstream application", "a production
deployment", or "an internal report" — never to the named product. Two
product-named test files renamed to neutral diagnostic names. CLAUDE.md gains
a project-level guard rule documenting the policy and an example list of the
identifiers that may not appear in tracked code.
Verification
- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit
- All four integration subtype + verb + strict + find-limits suites: 78/78
- npm run build: clean
- Closed-source product reference audit: clean
2026-06-08 12:34:05 -07:00
|
|
|
it('should auto-limit based on system memory (two-tier enforcement)', () => {
|
2025-09-12 14:37:39 -07:00
|
|
|
const config = getValidationConfig()
|
fix: recalibrate find({ limit }) cap + two-tier enforcement + caller location
Brainy 7.30.0 introduced a memory-derived synchronous cap on `find({ limit })`
to prevent OOM. The cap was sound in intent but ~4x too conservative in
calibration: assumed 100 KB per result while typical entity footprint is 7-10 KB
(384-dim float32 vector ≈ 1.5 KB + standard fields + metadata). On a 900 MB
free-memory box the cap derived to 9000 — breaking common safety-cap patterns
like `find({ type, where, limit: 10_000 })` that typically return 10-500
entities. Surfaced as a runtime regression with cascading 500s degrading
production dashboards.
Three concurrent fixes:
A. RECALIBRATE THE FORMULA
- src/utils/paramValidation.ts:175,196,212 — the three memory-derived priorities
(reservedQueryMemory / containerMemory / freeMemory) all divided by
100 * 1024 * 1024 (100 KB per result, ~10-15x over conservative). Replaced
with a new MAX_LIMIT_KB_PER_RESULT = 25 constant that matches observed
entity size.
- Result: 4 GB container cap goes 10_000 → 40_000; 2 GB cap goes 5_000 →
20_000; 900 MB free-memory cap goes 9_000 → ~36_000. 100k hard ceiling
unchanged. `maxQueryLimit` / `reservedQueryMemory` constructor overrides
unchanged in behavior.
B. TWO-TIER ENFORCEMENT (warn-then-throw)
- Below cap (limit <= maxLimit): silent pass, unchanged.
- Soft tier (maxLimit < limit <= 2 * maxLimit): NEW — one-time warning per
call site (dedup keyed on caller stack frame + limit value), query
proceeds. Pre-7.30.2 code that relied on the cap silently allowing typical
safety-cap limits keeps working; the warning teaches the recipe so consumers
can fix it intentionally.
- Hard tier (limit > 2 * maxLimit): throw with the same teaching message
format. Real OOM territory; the cap stops being a recommendation and becomes
a guardrail.
- The 2x soft margin absorbs typical safety-cap patterns (limit: 10_000
against a 9 K-cap box) without disabling OOM protection. Real OOM territory
on a JS in-memory brain is hundreds of thousands of results, not 10x the
safety cap.
C. IMPROVED ERROR / WARNING MESSAGE
- Same shape as the 7.30.1 enforcement-error messages: state the problem,
name the three escape valves (maxQueryLimit / reservedQueryMemory /
pagination), include caller location, link to docs.
- Extracted findCallerLocation() helper from brainy.ts to a new
src/utils/callerLocation.ts so both the subtype enforcement (7.30.1) and
the limit enforcement (7.30.2) share one implementation without circular
imports.
DOCS
- New docs/guides/find-limits.md (public: true) — full reference: why the cap
exists, the four memory sources the auto-config considers, the three escape
valves with when-to-use-which guidance, and an explicit "pagination is the
future-proof pattern" callout (8.0 may tighten the cap further; pagination
keeps working unchanged).
- docs/api/README.md find() entry gets a one-paragraph `limit` tip + pointer
to the new guide.
- RELEASES.md v7.30.2 entry.
TESTS
- New tests/integration/find-limits.test.ts (9 tests): below-cap silent pass;
soft-tier warns once per call site (dedup verified by exercising same vs.
different source lines via wrapper closures); soft-tier message format
(names all three escape valves + docs link); soft-tier message includes
caller location; hard-tier throws; hard-tier message format same as
soft-tier; consumer maxQueryLimit override raises the cap and shifts both
tiers accordingly; pre-7.30.2 regression scenario explicitly covered.
- tests/unit/utils/memoryLimits.test.ts — 4 tests updated for the recalibrated
cap values (hardcoded expected numbers bumped 4x to match new 25 KB/result
assumption).
- tests/unit/utils/paramValidation.test.ts — auto-limit test extended to cover
the three-tier semantics (below-cap pass / soft-tier silent / hard-tier
throw).
- Existing suites unchanged: subtype-and-facets 26/26, verb-subtype-and-
enforcement 30/30, strict-mode-self-test 13/13. Unit 1468/1468.
CORTEX COMPATIBILITY
- Zero Cortex changes required. Every change is JS-side: formula recalibration
runs in ValidationConfig.constructor(), two-tier enforcement runs in
validateFindParams(), both fire before any storage / index / Cortex call.
- The new guide notes that Brainy 8.0's Datomic-style Db.find() may tighten
per-call limits to keep snapshot semantics cheap; pagination remains the
pattern that's guaranteed to keep working.
REPO-WIDE CLEANUP
Brainy is the only Soulcraft project that is open source. This commit also
scrubs closed-source product names and product-specific class/field references
from every tracked file in the repo (src/, docs/, tests/, RELEASES.md,
CHANGELOG.md). Consumer-reported bugs, regression scenarios, and release
notes now refer to "a consumer", "a downstream application", "a production
deployment", or "an internal report" — never to the named product. Two
product-named test files renamed to neutral diagnostic names. CLAUDE.md gains
a project-level guard rule documenting the policy and an example list of the
identifiers that may not appear in tracked code.
Verification
- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit
- All four integration subtype + verb + strict + find-limits suites: 78/78
- npm run build: clean
- Closed-source product reference audit: clean
2026-06-08 12:34:05 -07:00
|
|
|
|
|
|
|
|
// 7.30.2+ design: the cap fires in two tiers.
|
|
|
|
|
// - Below cap (`limit <= maxLimit`): silent pass, no signal.
|
|
|
|
|
// - Soft tier (`maxLimit < limit <= 2 * maxLimit`): one-time warning
|
|
|
|
|
// per call site, query proceeds. No throw.
|
|
|
|
|
// - Hard tier (`limit > 2 * maxLimit`): real OOM danger zone, throw.
|
|
|
|
|
|
|
|
|
|
// Below cap → pass
|
|
|
|
|
expect(() => validateFindParams({ limit: config.maxLimit })).not.toThrow()
|
|
|
|
|
|
|
|
|
|
// Soft tier → no throw (just a warn we don't assert here — proper coverage
|
|
|
|
|
// lives in the find-limits integration suite which can intercept the log).
|
|
|
|
|
expect(() => validateFindParams({ limit: config.maxLimit + 1 })).not.toThrow()
|
|
|
|
|
expect(() => validateFindParams({ limit: config.maxLimit * 2 })).not.toThrow()
|
|
|
|
|
|
|
|
|
|
// Hard tier → throw with the new message format
|
2025-09-12 14:37:39 -07:00
|
|
|
expect(() => validateFindParams({
|
fix: recalibrate find({ limit }) cap + two-tier enforcement + caller location
Brainy 7.30.0 introduced a memory-derived synchronous cap on `find({ limit })`
to prevent OOM. The cap was sound in intent but ~4x too conservative in
calibration: assumed 100 KB per result while typical entity footprint is 7-10 KB
(384-dim float32 vector ≈ 1.5 KB + standard fields + metadata). On a 900 MB
free-memory box the cap derived to 9000 — breaking common safety-cap patterns
like `find({ type, where, limit: 10_000 })` that typically return 10-500
entities. Surfaced as a runtime regression with cascading 500s degrading
production dashboards.
Three concurrent fixes:
A. RECALIBRATE THE FORMULA
- src/utils/paramValidation.ts:175,196,212 — the three memory-derived priorities
(reservedQueryMemory / containerMemory / freeMemory) all divided by
100 * 1024 * 1024 (100 KB per result, ~10-15x over conservative). Replaced
with a new MAX_LIMIT_KB_PER_RESULT = 25 constant that matches observed
entity size.
- Result: 4 GB container cap goes 10_000 → 40_000; 2 GB cap goes 5_000 →
20_000; 900 MB free-memory cap goes 9_000 → ~36_000. 100k hard ceiling
unchanged. `maxQueryLimit` / `reservedQueryMemory` constructor overrides
unchanged in behavior.
B. TWO-TIER ENFORCEMENT (warn-then-throw)
- Below cap (limit <= maxLimit): silent pass, unchanged.
- Soft tier (maxLimit < limit <= 2 * maxLimit): NEW — one-time warning per
call site (dedup keyed on caller stack frame + limit value), query
proceeds. Pre-7.30.2 code that relied on the cap silently allowing typical
safety-cap limits keeps working; the warning teaches the recipe so consumers
can fix it intentionally.
- Hard tier (limit > 2 * maxLimit): throw with the same teaching message
format. Real OOM territory; the cap stops being a recommendation and becomes
a guardrail.
- The 2x soft margin absorbs typical safety-cap patterns (limit: 10_000
against a 9 K-cap box) without disabling OOM protection. Real OOM territory
on a JS in-memory brain is hundreds of thousands of results, not 10x the
safety cap.
C. IMPROVED ERROR / WARNING MESSAGE
- Same shape as the 7.30.1 enforcement-error messages: state the problem,
name the three escape valves (maxQueryLimit / reservedQueryMemory /
pagination), include caller location, link to docs.
- Extracted findCallerLocation() helper from brainy.ts to a new
src/utils/callerLocation.ts so both the subtype enforcement (7.30.1) and
the limit enforcement (7.30.2) share one implementation without circular
imports.
DOCS
- New docs/guides/find-limits.md (public: true) — full reference: why the cap
exists, the four memory sources the auto-config considers, the three escape
valves with when-to-use-which guidance, and an explicit "pagination is the
future-proof pattern" callout (8.0 may tighten the cap further; pagination
keeps working unchanged).
- docs/api/README.md find() entry gets a one-paragraph `limit` tip + pointer
to the new guide.
- RELEASES.md v7.30.2 entry.
TESTS
- New tests/integration/find-limits.test.ts (9 tests): below-cap silent pass;
soft-tier warns once per call site (dedup verified by exercising same vs.
different source lines via wrapper closures); soft-tier message format
(names all three escape valves + docs link); soft-tier message includes
caller location; hard-tier throws; hard-tier message format same as
soft-tier; consumer maxQueryLimit override raises the cap and shifts both
tiers accordingly; pre-7.30.2 regression scenario explicitly covered.
- tests/unit/utils/memoryLimits.test.ts — 4 tests updated for the recalibrated
cap values (hardcoded expected numbers bumped 4x to match new 25 KB/result
assumption).
- tests/unit/utils/paramValidation.test.ts — auto-limit test extended to cover
the three-tier semantics (below-cap pass / soft-tier silent / hard-tier
throw).
- Existing suites unchanged: subtype-and-facets 26/26, verb-subtype-and-
enforcement 30/30, strict-mode-self-test 13/13. Unit 1468/1468.
CORTEX COMPATIBILITY
- Zero Cortex changes required. Every change is JS-side: formula recalibration
runs in ValidationConfig.constructor(), two-tier enforcement runs in
validateFindParams(), both fire before any storage / index / Cortex call.
- The new guide notes that Brainy 8.0's Datomic-style Db.find() may tighten
per-call limits to keep snapshot semantics cheap; pagination remains the
pattern that's guaranteed to keep working.
REPO-WIDE CLEANUP
Brainy is the only Soulcraft project that is open source. This commit also
scrubs closed-source product names and product-specific class/field references
from every tracked file in the repo (src/, docs/, tests/, RELEASES.md,
CHANGELOG.md). Consumer-reported bugs, regression scenarios, and release
notes now refer to "a consumer", "a downstream application", "a production
deployment", or "an internal report" — never to the named product. Two
product-named test files renamed to neutral diagnostic names. CLAUDE.md gains
a project-level guard rule documenting the policy and an example list of the
identifiers that may not appear in tracked code.
Verification
- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit
- All four integration subtype + verb + strict + find-limits suites: 78/78
- npm run build: clean
- Closed-source product reference audit: clean
2026-06-08 12:34:05 -07:00
|
|
|
limit: config.maxLimit * 2 + 1
|
|
|
|
|
})).toThrow(/exceeds the auto-configured query limit/)
|
2025-09-12 14:37:39 -07:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should auto-limit query length', () => {
|
|
|
|
|
const config = getValidationConfig()
|
|
|
|
|
const longQuery = 'a'.repeat(config.maxQueryLength + 1)
|
|
|
|
|
|
|
|
|
|
expect(() => validateFindParams({
|
|
|
|
|
query: longQuery
|
|
|
|
|
})).toThrow(`query exceeds auto-configured maximum length of ${config.maxQueryLength}`)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('validateAddParams', () => {
|
|
|
|
|
|
|
|
|
|
it('should accept valid add parameters', () => {
|
|
|
|
|
expect(() => validateAddParams({
|
|
|
|
|
data: 'test content',
|
|
|
|
|
type: NounType.Document
|
|
|
|
|
})).not.toThrow()
|
|
|
|
|
|
|
|
|
|
expect(() => validateAddParams({
|
|
|
|
|
vector: new Array(384).fill(0),
|
|
|
|
|
type: NounType.Person
|
|
|
|
|
})).not.toThrow()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should require either data or vector', () => {
|
|
|
|
|
expect(() => validateAddParams({
|
|
|
|
|
type: NounType.Document
|
2025-09-22 15:45:35 -07:00
|
|
|
} as AddParams)).toThrow('Invalid add() parameters: Missing required field \'data\'')
|
2025-09-12 14:37:39 -07:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should validate NounType', () => {
|
|
|
|
|
expect(() => validateAddParams({
|
|
|
|
|
data: 'test',
|
|
|
|
|
type: 'InvalidType' as any
|
2025-09-22 15:45:35 -07:00
|
|
|
})).toThrow('Invalid NounType: \'InvalidType\'')
|
2025-09-12 14:37:39 -07:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should validate vector dimensions', () => {
|
|
|
|
|
expect(() => validateAddParams({
|
|
|
|
|
vector: new Array(100).fill(0),
|
|
|
|
|
type: NounType.Document
|
|
|
|
|
})).toThrow('vector must have exactly 384 dimensions')
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('validateUpdateParams', () => {
|
|
|
|
|
|
|
|
|
|
it('should accept valid update parameters', () => {
|
|
|
|
|
expect(() => validateUpdateParams({
|
|
|
|
|
id: 'test-id',
|
|
|
|
|
data: 'new content'
|
|
|
|
|
})).not.toThrow()
|
|
|
|
|
|
|
|
|
|
expect(() => validateUpdateParams({
|
|
|
|
|
id: 'test-id',
|
|
|
|
|
metadata: { status: 'updated' }
|
|
|
|
|
})).not.toThrow()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should require an ID', () => {
|
|
|
|
|
expect(() => validateUpdateParams({
|
|
|
|
|
data: 'new content'
|
|
|
|
|
} as UpdateParams)).toThrow('id is required for update')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should require at least one field to update', () => {
|
|
|
|
|
expect(() => validateUpdateParams({
|
|
|
|
|
id: 'test-id'
|
|
|
|
|
})).toThrow('must specify at least one field to update')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should validate NounType if changing', () => {
|
|
|
|
|
expect(() => validateUpdateParams({
|
|
|
|
|
id: 'test-id',
|
|
|
|
|
type: 'InvalidType' as any
|
|
|
|
|
})).toThrow('invalid NounType: InvalidType')
|
|
|
|
|
|
|
|
|
|
expect(() => validateUpdateParams({
|
|
|
|
|
id: 'test-id',
|
|
|
|
|
type: NounType.Event
|
|
|
|
|
})).not.toThrow()
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('validateRelateParams', () => {
|
|
|
|
|
|
|
|
|
|
it('should accept valid relate parameters', () => {
|
|
|
|
|
expect(() => validateRelateParams({
|
|
|
|
|
from: 'entity1',
|
|
|
|
|
to: 'entity2',
|
|
|
|
|
type: VerbType.RelatedTo
|
|
|
|
|
})).not.toThrow()
|
|
|
|
|
|
|
|
|
|
expect(() => validateRelateParams({
|
|
|
|
|
from: 'entity1',
|
|
|
|
|
to: 'entity2',
|
|
|
|
|
type: VerbType.Creates,
|
|
|
|
|
weight: 0.8
|
|
|
|
|
})).not.toThrow()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should require from and to', () => {
|
|
|
|
|
expect(() => validateRelateParams({
|
|
|
|
|
to: 'entity2',
|
|
|
|
|
type: VerbType.RelatedTo
|
|
|
|
|
} as RelateParams)).toThrow('from entity ID is required')
|
|
|
|
|
|
|
|
|
|
expect(() => validateRelateParams({
|
|
|
|
|
from: 'entity1',
|
|
|
|
|
type: VerbType.RelatedTo
|
|
|
|
|
} as RelateParams)).toThrow('to entity ID is required')
|
|
|
|
|
})
|
|
|
|
|
|
2025-10-09 16:33:08 -07:00
|
|
|
// Self-referential relationships are now allowed (valid in graph systems)
|
|
|
|
|
// Previous test "should reject self-referential relationships" removed
|
|
|
|
|
|
2025-09-12 14:37:39 -07:00
|
|
|
it('should validate VerbType', () => {
|
|
|
|
|
expect(() => validateRelateParams({
|
|
|
|
|
from: 'entity1',
|
|
|
|
|
to: 'entity2',
|
|
|
|
|
type: 'InvalidVerb' as any
|
|
|
|
|
})).toThrow('invalid VerbType: InvalidVerb')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should validate weight range', () => {
|
|
|
|
|
expect(() => validateRelateParams({
|
|
|
|
|
from: 'entity1',
|
|
|
|
|
to: 'entity2',
|
|
|
|
|
type: VerbType.RelatedTo,
|
|
|
|
|
weight: -0.1
|
|
|
|
|
})).toThrow('weight must be between 0 and 1')
|
|
|
|
|
|
|
|
|
|
expect(() => validateRelateParams({
|
|
|
|
|
from: 'entity1',
|
|
|
|
|
to: 'entity2',
|
|
|
|
|
type: VerbType.RelatedTo,
|
|
|
|
|
weight: 1.1
|
|
|
|
|
})).toThrow('weight must be between 0 and 1')
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('Auto-configuration', () => {
|
|
|
|
|
|
|
|
|
|
it('should provide configuration based on system resources', () => {
|
|
|
|
|
const config = getValidationConfig()
|
|
|
|
|
|
|
|
|
|
expect(config.maxLimit).toBeGreaterThan(0)
|
|
|
|
|
expect(config.maxLimit).toBeLessThanOrEqual(100000)
|
|
|
|
|
expect(config.maxQueryLength).toBeGreaterThan(0)
|
|
|
|
|
expect(config.maxVectorDimensions).toBe(384)
|
|
|
|
|
expect(config.systemMemory).toBeGreaterThan(0)
|
|
|
|
|
expect(config.availableMemory).toBeGreaterThan(0)
|
|
|
|
|
})
|
|
|
|
|
|
2026-07-17 09:20:09 -07:00
|
|
|
it('never mutates the cap from query timing (telemetry only)', () => {
|
|
|
|
|
const initialLimit = getValidationConfig().maxLimit
|
|
|
|
|
|
|
|
|
|
// Fast queries with large results: no silent growth.
|
2025-09-12 14:37:39 -07:00
|
|
|
for (let i = 0; i < 10; i++) {
|
|
|
|
|
recordQueryPerformance(50, initialLimit * 0.9)
|
|
|
|
|
}
|
2026-07-17 09:20:09 -07:00
|
|
|
expect(getValidationConfig().maxLimit).toBe(initialLimit)
|
|
|
|
|
|
|
|
|
|
// A burst of catastrophically slow queries must not strangle the cap.
|
|
|
|
|
// The removed "learning" ratchet shrank it 20% per recorded query down
|
|
|
|
|
// to a floor of 1000 — below the documented MIN_AUTO_QUERY_LIMIT — and
|
|
|
|
|
// the error message blamed "available free memory" (a production
|
|
|
|
|
// incident: every find({ limit: 5000 }) failed on an idle 23GB-free box).
|
|
|
|
|
for (let i = 0; i < 50; i++) {
|
|
|
|
|
recordQueryPerformance(90_000, 100)
|
2025-09-12 14:37:39 -07:00
|
|
|
}
|
2026-07-17 09:20:09 -07:00
|
|
|
expect(getValidationConfig().maxLimit).toBe(initialLimit)
|
2025-09-12 14:37:39 -07:00
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
})
|