@x12i/xmemory-equal
v1.7.0
Published
Canonical Thing identity and equality layer for XMemory (MongoDB via @xronoces/xmemory-store data tier)
Downloads
146
Readme
xmemory-equal
Canonical Thing identity and 100% equality layer for XMemory. Stores things (literal text or keys), links them as equivalent, and resolves to a canonical representative. Persists to MongoDB via nx-mongo only (this package does not create Mongo clients or read MONGO_URI).
Install
npm install xmemory-equal nx-mongoUsage
- Create and initialize an nx-mongo helper (your app owns the connection and DB name).
Same Mongo env as @xronoces/xmemory-store: use resolveXmemoryStoreEnv().thingsDb (or tier.env.thingsDb) for your databaseName so role C — corpus / things matches the store’s resolution. Set corpus DB with the store’s keys — e.g. THINGS_DB or MONGO_XMEMORY_META_DB / METADATA_DB (see @xronoces/xmemory-store ENV_KEYS). Never use MONGO_XMEMORY_DB for equal or things data: in xmemory-store that variable resolves opDb (scoped views), not thingsDb.
Single physical database for everything: still set THINGS_DB (or MONGO_XMEMORY_META_DB) to that database name — do not repurpose MONGO_XMEMORY_DB.
import { SimpleMongoHelper } from "nx-mongo";
import { resolveXmemoryStoreEnv } from "@xronoces/xmemory-store";
const mongoUri = process.env.MONGO_URI!;
const env = resolveXmemoryStoreEnv({ mongoUri });
const helper = new SimpleMongoHelper(mongoUri);
await helper.initialize({ databaseName: env.thingsDb });- Create the equal client and ensure indexes:
import { createEqualClient } from "xmemory-equal";
const equal = createEqualClient({
nxMongo: helper,
namespace: "myapp", // optional; default from XMEMORY_NAMESPACE or "default"
defaultThingType: "generic", // optional; default from XMEMORY_DEFAULT_THING_TYPE or "generic"
normalization: "basic", // optional; default "basic"
canonicalStrategy: "preferKey" // optional; default "preferKey"
});
await equal.ensureIndexes();- Ensure things, link them, and query:
const ns = "myapp";
// Thing identity = (namespace, thingType, kind, refNorm). Same string can be different things by type.
const aliceKey = await equal.ensureThing({ namespace: ns, thingType: "person_name", kind: "key", ref: "user:alice" });
const aliceLit = await equal.ensureThing({ namespace: ns, thingType: "person_name", kind: "literal", ref: "Alice" });
// Link = same real-world thing (equivalence; transitive)
await equal.link(aliceKey, aliceLit, { reason: "same person" });
// Query
const equivs = await equal.getEquivalents(aliceKey); // all things in the same group
const same = await equal.areEqual(aliceKey, aliceLit); // true
const canon = await equal.resolve(aliceKey); // canonical representative (by strategy)ThingType: Use thingType to distinguish what the “something” is (e.g. person_name, company_name, product_name, doc_title, question, prompt, generic). The same ref in two different types are different things unless you explicitly link them. You can omit thingType and it will be filled from defaultThingType config or XMEMORY_DEFAULT_THING_TYPE (default generic).
externalIdentifier: Optional string on ThingRef to store a pointer to an external system (e.g. CRM id, external DB key). Stored in xmemory_things and returned on ThingRecord. If you set one that another thing in the same namespace already has, the client throws ExternalIdentifierConflictError.
metadata / data: Things can carry optional metadata (properties about the thing) and data (payload/snapshot). Pass them on ensureThing(thing, { metadata, data }) or patch later with updateThing(thingOrId, { set: { metadata, data } }) or merge: { metadata, data } for deep-merge. Each update increments version. Reserved metadata keys metadata.db and metadata.collection are the platform-standard scope (see Scope below). Use opts.scope on ensure or setThingScope(thingOrId, { db, collection }) to set them.
Soft delete: deleteThing(thingOrId, { deletedBy?, deleteReason? }) sets deletedAt (and optional deletedBy/deleteReason). By default, all read methods exclude deleted things; use { includeDeleted: true } to include them. purgeThing(thingOrId) hard-deletes the thing and its group membership (and optionally audit links).
API (summary)
| Method | Description |
|--------|-------------|
| ensureThing(thing, opts?) | Upsert thing; optional opts.metadata, opts.data, opts.scope (writes metadata.db, metadata.collection). Returns record with version, metadata, data. |
| updateThing(thingOrId, patch) | Patch with set (replace), merge (deep-merge), unset, unsetPaths. Reserved keys metadata.db/metadata.collection can be set/merged via metadata. Increments version. |
| setThingScope(thingOrId, { db, collection }) | Set reserved scope on a thing (merge into metadata). Returns updated ThingRecord. |
| deleteThing(thingOrId, opts?) | Soft delete (sets deletedAt). Returns { deleted: boolean }. |
| purgeThing(thingOrId) | Hard delete thing and group membership. |
| getThing(thing, opts?) | Read-only by identity. Returns null if not found or deleted (unless includeDeleted). |
| getThingById(id, opts?) | Read by id. |
| getThingByExternalIdentifier(ns, extId, opts?) | Read by namespace + externalIdentifier. |
| listThings(query) | List with thingTypes, kinds, db, collection (scope filters), externalIdentifierPrefix, updatedAfter/updatedBefore, includeDeleted (default false), limit/offset. |
| link(a, b, meta?) | Ensure both things, create/merge equivalence group; idempotent. |
| getEquivalents(thing, opts?) | All things in the same group (default excludes deleted). |
| areEqual(a, b) | true if same thing id or same group. |
| resolve(thing, opts?) | Canonical representative (default excludes deleted; recomputes if canonical is deleted). |
| setCanonical(thing) | When canonicalStrategy === "manualPinned", set the group’s canonical. |
| getEqualityLinks(thingOrId) | Audit edges where this thing is an endpoint. |
| getGroupLinks(groupId) | Audit edges within the group. |
| ensureIndexes() | Create required indexes on xmemory_* collections. |
| normalize(value) | Apply configured normalization. |
| inferenceFromFieldInferable(field) | Map aifunctions-js FieldInferable → ThingInference for ensureThing (see Data tier note on aifunctions-js). |
Config and env
Options can be set in code or via environment (env is used as default when the option is omitted):
| Option | Env | Default | Description |
|--------|-----|---------|-------------|
| namespace | XMEMORY_NAMESPACE | "default" | Logical partition / tenant. |
| defaultThingType | XMEMORY_DEFAULT_THING_TYPE | "generic" | Default thingType when omitted on ThingRef (e.g. person_name, doc_title, generic). |
| normalization | XMEMORY_NORMALIZATION | "basic" | none | basic | lowercase | unicode_nfkc. |
| canonicalStrategy | XMEMORY_CANONICAL_STRATEGY | "preferKey" | disabled | preferKey | preferLiteral | firstSeen | lexicographic | manualPinned. |
| collectionPrefix | XMEMORY_COLLECTION_PREFIX | "xmemory" | Collection names are {prefix}_things, {prefix}_groups, etc. |
| auditLinks | — | true | Write audit rows to xmemory_links on link. |
Data tier (nxMongo / xmemory-store)
This package does not read MONGO_URI or any DB name. You pass a data-tier provider that implements NxMongoProvider: getDb(): Db and withTransaction(callback): Promise<T>.
With nx-mongo: Create and initialize a
SimpleMongoHelper, then pass it asnxMongo(as in the Usage example above).With @xronoces/xmemory-store:
createXmemoryDataTier()/resolveXmemoryStoreEnv()define tier Mongo layout and env. The tier’sNxMongoClientis for store tier operations only; it is not the same contract as equal’sNxMongoProvider(raw driverDb+withTransaction). For xmemory-equal, openmongoUri+thingsDbwith nx-mongo (or anyNxMongoProvider) — same values astier.env(ENV_KEYS/THINGS_DB/MONGO_XMEMORY_META_DB; notMONGO_XMEMORY_DB, which isopDb).
import { createXmemoryDataTier } from "@xronoces/xmemory-store";
import { SimpleMongoHelper } from "nx-mongo";
const tier = createXmemoryDataTier(/* optional env overrides */);
await tier.init();
const equalHelper = new SimpleMongoHelper(tier.env.mongoUri);
await equalHelper.initialize({ databaseName: tier.env.thingsDb });
const equal = createEqualClient({ nxMongo: equalHelper, namespace: "myapp" });
await equal.ensureIndexes();Dependencies (package.json): @xronoces/xmemory-store and aifunctions-js are pinned for version alignment with the wider stack. Core equal code does not call an LLM; it only persists ThingInference on things. To map aifunctions-js FieldInferable into ThingInference, use inferenceFromFieldInferable().
Any object that implements getDb() and withTransaction() is accepted.
Scope (metadata.db, metadata.collection)
Things can be scoped to a logical db and collection via reserved metadata keys metadata.db and metadata.collection. Other layers in the XMemory stack may read these fields; we do not scope using data.
- Set on create:
ensureThing(thing, { scope: { db: "mydb", collection: "items" } }) - Set or change later:
setThingScope(thingOrId, { db, collection })orupdateThing(thingOrId, { merge: { metadata: { db, collection } } }) - Retrieve by scope:
listThings({ thingTypes: ["doc_title"], db: "mydb", collection: "items" })
Multi-DB limitation: This package writes to a single MongoDB database (the one configured in your nx-mongo helper). The metadata.db / metadata.collection values are stored as strings and used only for filtering and application-level grouping. They do not change which MongoDB database or collection is used for persistence. To use multiple MongoDB databases you must create separate nx-mongo helpers (and equal clients) per database.
Identity rule
Thing identity is (namespace, thingType, kind, refNorm). So the same string can exist as different things:
"Apple"asthingType: "product_name"and"Apple"asthingType: "company_name"are two distinct things unless you explicitlylink()them.
Collections (in the DB selected by nx-mongo)
xmemory_things— thing rows: namespace, thingType, kind, refRaw, refNorm, optional externalIdentifier, metadata (optional reservedmetadata.db,metadata.collection), data, deletedAt, deletedBy, deleteReason, version. Unique on(namespace, thingType, kind, refNorm); sparse index on(namespace, externalIdentifier); indexes on(namespace, thingType, updatedAt)and(namespace, thingType, metadata.db, metadata.collection, updatedAt).xmemory_groups— equivalence groups (optional canonical, size, mergedIntoGroupId).xmemory_thing_group— thing ↔ group membership.xmemory_links— audit edges for links (optional).
Downstream consumers of the xmemory-records-mapper (or any code reading from these collections): see docs/downstream.md for identity vs relations, ref/scope changes, Thing shape, and equality mode.
Shared XMemory Mongo env (with @xronoces/xmemory-store)
Source of truth: @xronoces/xmemory-store — resolveXmemoryStoreEnv(), ENV_KEYS, and docs/spec.md (§2 Mongo binding). This package does not fork that contract. Host wiring beyond “open thingsDb for equal” belongs in xmemory-store (and other upstream) docs, not here.
Mirror of the store role table (see store README “What this package does”):
| Role | Concern | Default DB (store) | Env keys (ENV_KEYS in xmemory-store) |
|------|---------|--------------------|----------------------------------------|
| B | Scoping maps | xmemory-meta | MAPS_DB: METADATA_DB, MONGO_XMEMORY_META_DB, MONGO_XMEMORY_METADATA_DB |
| C | Things corpus (equal + store tier) | xmemory-meta | THINGS_DB: THINGS_DB, MONGO_XMEMORY_META_DB; if unset → same as mapsDb |
| D | Scoped views / op | xmemory_op | OP_DB: MONGO_XMEMORY_DB, VIEWS_DB, MONGO_XMEMORY_OPERATIONAL_DB |
Equal’s nx-mongo databaseName must be thingsDb, never opDb. MONGO_XMEMORY_DB is role D only in the store.
Release checklist for store changes: docs/check-with-xmemory-store.md.
Testing
npm test runs: tsc, integration typecheck (test/store-alignment.types.ts), Tier-1 Node tests (env resolution + inferenceFromFieldInferable), then compiled real-DB checks via test/run-dist.mjs (requires Mongo).
Migration: If you previously used MONGO_XMEMORY_DB as the test database for equal, that variable is operational DB in xmemory-store. Set corpus DB the store way: THINGS_DB and/or MONGO_XMEMORY_META_DB (see ENV_KEYS.THINGS_DB and mapsDb / thingsDb fallback in store).
Real DB env (same as production: resolveXmemoryStoreEnv({ mongoUri }).thingsDb — no duplicate parsing here)
| Variable | Role |
|----------|------|
| MONGO_URI | Required for DB tests (ENV_KEYS.MONGO_URI in store) |
| THINGS_DB, MONGO_XMEMORY_META_DB, … | Corpus thingsDb — exactly as xmemory-store resolves (see table above; METADATA_DB affects mapsDb, hence thingsDb when THINGS_DB unset) |
| XMEMORY_TEST_DATABASE | Test-only: force Mongo database name (overrides thingsDb; CI isolation) |
| MONGO_EQUAL_TEST_DB, XMEMORY_EQUAL_TEST_DATABASE | Deprecated aliases for XMEMORY_TEST_DATABASE |
Optional
XMEMORY_TEST_STORE_TIER_COEXISTENCE=1— after main suite,test/test.tsrunscreateXmemoryDataTier().init(), a secondSimpleMongoHelperontier.env.thingsDb, minimalensureThing, thentier.close(). Deprecated:RUN_STORE_TIER_COEXISTENCE,XMEMORY_EQUAL_TEST_STORE_TIER_COEXISTENCE.XMEMORY_TEST_LIVE_LLM=1andOPENROUTER_API_KEY—npm run test:live(optional). Deprecated:RUN_LIVE_LLM,XMEMORY_EQUAL_TEST_LIVE_LLM.
npm test
npm run test:ts
npm run test:tier1
npm run typecheck:integration
npm run test:liveSee .env.example for a template.
License
ISC
