Prepares the repo for its new home at soulcraftlabs/open-brainy ahead of the Forgejo transfer: package name, publish registry, release script, and every install/import reference across docs, src, tests, examples, and integrations now point at @soulcraftlabs/brainy on The Source. The npmjs storefront leg and byte-identity pair verification are stripped from the release script — The Source is now the only publish target. README gains an Open Brainy explainer and a registry note for consumers. @soulcraft/brainy 10.4.2 was the last release under the old name.
4.5 KiB
| title | slug | public | category | template | order | description | next | |
|---|---|---|---|---|---|---|---|---|
| Migrating to 9.0 — your fields and system fields | guides/namespace-migration | true | guides | guide | 1 | The simple story of the 9.0 field-addressing change and the mechanical checklist for updating your call sites — every miss fails loudly with the fix in the error. |
|
Migrating to 9.0 — your fields and system fields
The one-sentence version: your data's field names are now completely
yours, the engine's own fields all live behind one system. prefix, and
nothing in between can silently go wrong anymore.
What changed, simply
1. Any field name just works. Before 9.0 the engine quietly owned
certain names. A field called level could be shadowed by the engine's
internal index layer of the same name (sorts silently returned insertion
order); names like confidence or subtype were rejected inside
metadata; names like content or id were silently never indexed, so
filtering on them returned nothing. All of that is gone. Any name —
level, confidence, type, id, content, anything — is stored
exactly as written and works with every feature: filtering, sorting,
grouping, aggregation, search, and time-travel reads.
2. The engine's fields moved behind system.. The engine still keeps
its own per-record bookkeeping — creation time, type, confidence, and so
on. Those are reached one way only now: spelled out, e.g.
system.createdAt, system.type. They are just as queryable and sortable
as before. orderBy: 'createdAt' means your field named createdAt;
orderBy: 'system.createdAt' means the engine's timestamp. No guessing,
no priority rules.
3. Storage keeps the two physically separate. New records store your
metadata in its own nested compartment, so a user field named
confidence and the engine's confidence live side by side, both intact,
through restarts, index rebuilds, and asOf() history. Old records stay
readable forever; nothing rewrites your data.
4. Mistakes are loud. An ambiguous or unknown field name is a typed
error naming the fix. Unimplemented options refuse instead of being
ignored. The only forbidden name in your metadata is one literally
starting with system..
The mechanical checklist
Every missed site fails loudly with the correction in the error message — nothing silently changes meaning. Sweep these patterns:
| Before (8.x) | After (9.0) |
|---|---|
orderBy: 'createdAt' (meaning the engine timestamp) |
orderBy: 'system.createdAt' |
where: { subtype: 'invoice' } (the engine subtype) |
where: { 'system.subtype': 'invoice' } |
where: { confidence: { greaterThan: 0.8 } } (the engine scalar) |
where: { 'system.confidence': { greaterThan: 0.8 } } |
groupBy: ['noun'] or groupBy: ['type'] |
groupBy: ['system.type'] |
where: { visibility: 'internal' } / { service: … } (engine values) |
'system.visibility' / 'system.service' |
metadata: { confidence: 0.9 } expecting a throw or a lift to the engine scalar |
it is YOUR field now — set the engine scalar via the confidence param |
new Brainy({ reservedFieldPolicy: … }) |
remove the option (it throws with this note) |
find({ cursor }) / includeRelations / writeOnly |
refuse with UnsupportedFindOptionError — they were silently ignored before |
If a bare name in a query was genuinely your field all along (orderBy: 'score', where: { status: 'active' }), change nothing — bare names
mean your fields, always.
What happens at first open
Each existing database rebuilds its derived indexes once, automatically,
at the first open on 9.0 (index epoch 3 — the index keys split the two
namespaces). One-time cost, observable via getIndexStatus(); no manual
step, and your stored data is not modified.
For tooling and raw-record readers
If you read raw stored records (fact-log scanners, export tooling), use the exported shape-aware splitters — they handle both record eras:
import { splitNounMetadataRecord } from '@soulcraftlabs/brainy'
const { reserved, custom } = splitNounMetadataRecord(rawRecord)
// reserved = engine fields · custom = the user's bag, ANY names
Feature detection (never version-sniff):
import * as brainy from '@soulcraftlabs/brainy'
const lawActive = 'FIELD_ADDRESSING_CAPABILITY' in brainy // 'field-addressing/v1'
Where to go next
- Field addressing — the full contract: the ten system scalars, the relation mirror, refusal semantics, and the cross-engine ordering guarantees.