From 56e32538293a46caef364401281a479dbdbc9de6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Tue, 2 Jun 2026 06:30:01 +0000 Subject: [PATCH 1/2] feat(core): MemoryStore returns OcasStore (cas + var + tag) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrite createMemoryStore() to return an OcasStore with three sub-stores: cas, var, tag. The cas sub-store keeps the existing Map-based logic, but its put is now synchronous; the in-memory VarStore and TagStore are new Map-based implementations of the types defined in #39. The legacy Store.put signature is widened to Hash | Promise so that the new sync cas can still be passed to helpers (bootstrap, gc, render, schema, …) that have not yet been migrated to the unified surface. Tests in packages/core were mechanically updated from store.* to store.cas.* and new test suites for VarStore (var-store.test.ts) and TagStore (tag-store.test.ts) cover the spec from issue #40. Closes #40 Co-Authored-By: Claude Opus 4.6 --- packages/core/src/bootstrap-capable.ts | 2 +- packages/core/src/bootstrap.test.ts | 44 +- packages/core/src/gc.test.ts | 10 +- packages/core/src/hash.ts | 32 ++ packages/core/src/index.test.ts | 40 +- packages/core/src/liquid-render.test.ts | 2 +- packages/core/src/list-pagination.test.ts | 32 +- packages/core/src/mem-store.ts | 12 +- packages/core/src/output-templates.test.ts | 8 +- packages/core/src/render.test.ts | 90 +-- packages/core/src/schema.test.ts | 96 ++-- packages/core/src/schema.ts | 2 +- packages/core/src/store.test.ts | 162 ++++-- packages/core/src/store.ts | 511 +++++++++++++++++- packages/core/src/tag-store.test.ts | 107 ++++ packages/core/src/types.ts | 2 +- packages/core/src/var-store.test.ts | 226 ++++++++ .../core/src/variable-list-pagination.test.ts | 2 +- packages/core/src/variable-store.test.ts | 134 ++--- packages/core/src/wrap-envelope.test.ts | 14 +- 20 files changed, 1215 insertions(+), 313 deletions(-) create mode 100644 packages/core/src/tag-store.test.ts create mode 100644 packages/core/src/var-store.test.ts diff --git a/packages/core/src/bootstrap-capable.ts b/packages/core/src/bootstrap-capable.ts index feef49e..0b09e02 100644 --- a/packages/core/src/bootstrap-capable.ts +++ b/packages/core/src/bootstrap-capable.ts @@ -4,7 +4,7 @@ import type { Hash, Store } from "./types.js"; export const BOOTSTRAP_STORE = Symbol.for("@ocas/core/bootstrap-store"); export type BootstrapCapableStore = Store & { - [BOOTSTRAP_STORE](payload: unknown): Promise; + [BOOTSTRAP_STORE](payload: unknown): Hash | Promise; }; export function isBootstrapCapableStore( diff --git a/packages/core/src/bootstrap.test.ts b/packages/core/src/bootstrap.test.ts index 23f1d33..042ee28 100644 --- a/packages/core/src/bootstrap.test.ts +++ b/packages/core/src/bootstrap.test.ts @@ -34,7 +34,7 @@ const OUTPUT_ALIASES = [ describe("bootstrap - Built-in Schemas", () => { test("should return map of 30 built-in schema aliases to hashes", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); // Should return object with 9 primitive + 21 output aliases = 30 @@ -62,7 +62,7 @@ describe("bootstrap - Built-in Schemas", () => { }); test("should register @ocas/schema as meta-schema alias", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const metaHash = builtinSchemas["@ocas/schema"]; @@ -75,7 +75,7 @@ describe("bootstrap - Built-in Schemas", () => { }); test("should register @ocas/string schema correctly", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const stringHash = builtinSchemas["@ocas/string"]; @@ -86,7 +86,7 @@ describe("bootstrap - Built-in Schemas", () => { }); test("should register @ocas/number schema correctly", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const numberHash = builtinSchemas["@ocas/number"]; @@ -97,7 +97,7 @@ describe("bootstrap - Built-in Schemas", () => { }); test("should register @ocas/object schema correctly", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const objectHash = builtinSchemas["@ocas/object"]; @@ -108,7 +108,7 @@ describe("bootstrap - Built-in Schemas", () => { }); test("should register @ocas/array schema correctly", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const arrayHash = builtinSchemas["@ocas/array"]; @@ -119,7 +119,7 @@ describe("bootstrap - Built-in Schemas", () => { }); test("should register @ocas/bool schema correctly", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const boolHash = builtinSchemas["@ocas/bool"]; @@ -130,7 +130,7 @@ describe("bootstrap - Built-in Schemas", () => { }); test("should return same hashes on repeated bootstrap calls", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const first = await bootstrap(store); const second = await bootstrap(store); @@ -146,7 +146,7 @@ describe("bootstrap - Built-in Schemas", () => { }); test("all built-in schemas should be typed by meta-schema", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const metaHash = builtinSchemas["@ocas/schema"]; @@ -168,7 +168,7 @@ describe("bootstrap - Built-in Schemas", () => { describe("bootstrap - @ocas/output/* Schemas", () => { test("each @ocas/output/* schema has a title", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); for (const alias of OUTPUT_ALIASES) { @@ -183,7 +183,7 @@ describe("bootstrap - @ocas/output/* Schemas", () => { }); test("@ocas/output/put schema describes a ocas_ref string", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const hash = aliases["@ocas/output/put"]; if (!hash) throw new Error("@ocas/output/put not found"); @@ -197,7 +197,7 @@ describe("bootstrap - @ocas/output/* Schemas", () => { }); test("@ocas/output/get schema describes object with type, payload, timestamp", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const hash = aliases["@ocas/output/get"]; if (!hash) throw new Error("@ocas/output/get not found"); @@ -213,7 +213,7 @@ describe("bootstrap - @ocas/output/* Schemas", () => { }); test("@ocas/output/has schema describes a boolean", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const hash = aliases["@ocas/output/has"]; if (!hash) throw new Error("@ocas/output/has not found"); @@ -225,7 +225,7 @@ describe("bootstrap - @ocas/output/* Schemas", () => { }); test("@ocas/output/verify schema describes enum of ok|corrupted|invalid", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const hash = aliases["@ocas/output/verify"]; if (!hash) throw new Error("@ocas/output/verify not found"); @@ -239,7 +239,7 @@ describe("bootstrap - @ocas/output/* Schemas", () => { }); test("@ocas/output/refs schema describes array of ocas_ref strings", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const hash = aliases["@ocas/output/refs"]; if (!hash) throw new Error("@ocas/output/refs not found"); @@ -252,7 +252,7 @@ describe("bootstrap - @ocas/output/* Schemas", () => { }); test("@ocas/output/gc schema describes object with gc stats fields", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const hash = aliases["@ocas/output/gc"]; if (!hash) throw new Error("@ocas/output/gc not found"); @@ -269,7 +269,7 @@ describe("bootstrap - @ocas/output/* Schemas", () => { }); test("@ocas/output/var-set schema describes a Variable object", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const hash = aliases["@ocas/output/var-set"]; if (!hash) throw new Error("@ocas/output/var-set not found"); @@ -285,7 +285,7 @@ describe("bootstrap - @ocas/output/* Schemas", () => { }); test("@ocas/output/var-list schema describes array of Variable objects", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const hash = aliases["@ocas/output/var-list"]; if (!hash) throw new Error("@ocas/output/var-list not found"); @@ -301,7 +301,7 @@ describe("bootstrap - @ocas/output/* Schemas", () => { }); test("@ocas/output/template-delete schema describes object with deleted boolean", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const hash = aliases["@ocas/output/template-delete"]; if (!hash) throw new Error("@ocas/output/template-delete not found"); @@ -314,7 +314,7 @@ describe("bootstrap - @ocas/output/* Schemas", () => { }); test("all @ocas/output/* schemas are distinct hashes", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const outputHashes = OUTPUT_ALIASES.map((alias) => aliases[alias]); @@ -325,14 +325,14 @@ describe("bootstrap - @ocas/output/* Schemas", () => { describe("bootstrap - meta and schemas indexes (D1)", () => { test("listMeta contains the bootstrap meta-schema hash", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const metaHash = aliases["@ocas/schema"]; expect(store.listMeta().map((e) => e.hash)).toContain(metaHash as string); }); test("listSchemas contains meta-schema and all built-in schemas", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const schemas = store.listSchemas().map((e) => e.hash); diff --git a/packages/core/src/gc.test.ts b/packages/core/src/gc.test.ts index eb8d80f..7c95129 100644 --- a/packages/core/src/gc.test.ts +++ b/packages/core/src/gc.test.ts @@ -28,7 +28,7 @@ describe("GC - Variable Model Refactoring", () => { }); test("GC preserves variable-referenced nodes", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = { type: "object", properties: { name: { type: "string" } } }; const schemaHash = await putSchema(store, schema); @@ -52,7 +52,7 @@ describe("GC - Variable Model Refactoring", () => { }); test("GC preserves nodes from variables with same name, different schemas", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = { type: "object", properties: { x: { type: "number" } } }; const schemaB = { type: "object", properties: { y: { type: "string" } } }; @@ -80,7 +80,7 @@ describe("GC - Variable Model Refactoring", () => { }); test("GC removes nodes after variable deletion", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = { type: "object", properties: { name: { type: "string" } } }; const schemaHash = await putSchema(store, schema); @@ -102,7 +102,7 @@ describe("GC - Variable Model Refactoring", () => { }); test("GC is global across all variables", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = { type: "object", properties: { x: { type: "number" } } }; const schemaB = { type: "object", properties: { y: { type: "string" } } }; @@ -133,7 +133,7 @@ describe("GC - Variable Model Refactoring", () => { }); test("GC integration with refactored variable store", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = { type: "object", properties: { x: { type: "number" } } }; diff --git a/packages/core/src/hash.ts b/packages/core/src/hash.ts index 559d4ae..8e9c247 100644 --- a/packages/core/src/hash.ts +++ b/packages/core/src/hash.ts @@ -48,6 +48,38 @@ async function getInstance(): Promise { return _pending; } +/** + * Initialize the xxhash WASM instance. After this resolves, the synchronous + * hashing functions {@link computeHashSync} and {@link computeSelfHashSync} + * may be called. + */ +export async function initHasher(): Promise { + await getInstance(); +} + +/** + * Synchronous variant of {@link computeHash}. Must only be called after + * {@link initHasher} has resolved at least once; throws otherwise. + */ +export function computeHashSync(typeHash: Hash, payload: unknown): Hash { + if (_instance === null) { + throw new Error("Hasher not initialised — call initHasher() first"); + } + const input = concatBytes(asciiToBytes(typeHash), cborEncode(payload)); + return u64ToCrockford(_instance.h64Raw(input)); +} + +/** + * Synchronous variant of {@link computeSelfHash}. Must only be called after + * {@link initHasher} has resolved at least once; throws otherwise. + */ +export function computeSelfHashSync(payload: unknown): Hash { + if (_instance === null) { + throw new Error("Hasher not initialised — call initHasher() first"); + } + return u64ToCrockford(_instance.h64Raw(cborEncode(payload))); +} + /** * hash = XXH64(utf8(typeHash) ++ CBOR_deterministic(payload)) * Used for all normal nodes. diff --git a/packages/core/src/index.test.ts b/packages/core/src/index.test.ts index 319d9fd..df664bb 100644 --- a/packages/core/src/index.test.ts +++ b/packages/core/src/index.test.ts @@ -72,7 +72,7 @@ describe("computeHash", () => { // ────────────────────────────────────────────────────────────────────────────── describe("createMemoryStore – put and get", () => { test("put returns a hash and get retrieves the node", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const typeHash = await computeSelfHash({ name: "my-type" }); const hash = await store.put(typeHash, { greeting: "hello" }); @@ -86,12 +86,12 @@ describe("createMemoryStore – put and get", () => { }); test("get returns null for unknown hash", () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; expect(store.get("0000000000000")).toBeNull(); }); test("put is idempotent: same type+payload → same hash, no duplicate", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const typeHash = await computeSelfHash({ name: "my-type" }); const h1 = await store.put(typeHash, { n: 42 }); @@ -101,7 +101,7 @@ describe("createMemoryStore – put and get", () => { }); test("put does not create self-referencing nodes", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const payload = { name: "type-descriptor" }; const typeHash = await computeSelfHash(payload); const hash = await store.put(typeHash, payload); @@ -112,7 +112,7 @@ describe("createMemoryStore – put and get", () => { }); test("timestamp is preserved on second put (idempotency)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const typeHash = await computeSelfHash({ name: "my-type" }); const h1 = await store.put(typeHash, { v: 1 }); @@ -131,7 +131,7 @@ describe("createMemoryStore – put and get", () => { // ────────────────────────────────────────────────────────────────────────────── describe("createMemoryStore – has", () => { test("has returns false before put, true after", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const typeHash = await computeSelfHash({ name: "t" }); const hash = await computeHash(typeHash, { x: 1 }); @@ -141,7 +141,7 @@ describe("createMemoryStore – has", () => { }); test("listByType returns all stored hashes for a type", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const typeHash = await computeSelfHash({ name: "t" }); const h1 = await store.put(typeHash, { a: 1 }); @@ -156,7 +156,7 @@ describe("createMemoryStore – has", () => { }); test("listByType returns empty array on fresh store", () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; expect(store.listByType("0000000000000")).toEqual([]); }); }); @@ -166,12 +166,12 @@ describe("createMemoryStore – has", () => { // ────────────────────────────────────────────────────────────────────────────── describe("createMemoryStore – listByType", () => { test("returns empty array for unknown type", () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; expect(store.listByType("0000000000000")).toEqual([]); }); test("returns all hashes for the given type", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const typeHash = await computeSelfHash({ name: "t" }); const otherType = await computeSelfHash({ name: "other" }); @@ -186,7 +186,7 @@ describe("createMemoryStore – listByType", () => { }); test("idempotent put does not duplicate in listByType", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const typeHash = await computeSelfHash({ name: "t" }); const h1 = await store.put(typeHash, { n: 1 }); @@ -196,7 +196,7 @@ describe("createMemoryStore – listByType", () => { }); test("bootstrap node is listed under its self type", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const hash = builtinSchemas["@ocas/schema"] ?? ""; @@ -216,7 +216,7 @@ describe("createMemoryStore – listByType", () => { // ────────────────────────────────────────────────────────────────────────────── describe("verify", () => { test("returns true for a correctly stored node", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const typeHash = await computeSelfHash({ name: "my-type" }); const hash = await store.put(typeHash, { data: 123 }); const node = store.get(hash) as CasNode; @@ -225,7 +225,7 @@ describe("verify", () => { }); test("returns false when payload is tampered", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const typeHash = await computeSelfHash({ name: "my-type" }); const hash = await store.put(typeHash, { data: 123 }); @@ -238,7 +238,7 @@ describe("verify", () => { }); test("returns false when type is tampered", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const typeHash = await computeSelfHash({ name: "my-type" }); const hash = await store.put(typeHash, { data: 123 }); const node = store.get(hash) as CasNode; @@ -265,7 +265,7 @@ describe("bootstrap", () => { }); test("returns a map with 30 built-in schema aliases", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); expect(builtinSchemas).toHaveProperty("@ocas/schema"); @@ -288,7 +288,7 @@ describe("bootstrap", () => { }); test("meta-schema node is stored and retrievable", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const metaHash = builtinSchemas["@ocas/schema"] ?? ""; @@ -298,7 +298,7 @@ describe("bootstrap", () => { }); test("meta-schema node is self-referencing: type === hash", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const metaHash = builtinSchemas["@ocas/schema"] ?? ""; const node = store.get(metaHash) as CasNode; @@ -307,7 +307,7 @@ describe("bootstrap", () => { }); test("bootstrap node passes verify()", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const metaHash = builtinSchemas["@ocas/schema"] ?? ""; const node = store.get(metaHash) as CasNode; @@ -316,7 +316,7 @@ describe("bootstrap", () => { }); test("bootstrap is idempotent: same hashes on repeated calls", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const h1 = await bootstrap(store); const h2 = await bootstrap(store); diff --git a/packages/core/src/liquid-render.test.ts b/packages/core/src/liquid-render.test.ts index 8fa4dbd..0a2ba55 100644 --- a/packages/core/src/liquid-render.test.ts +++ b/packages/core/src/liquid-render.test.ts @@ -13,7 +13,7 @@ import { createVariableStore } from "./variable-store.js"; async function createTempVarStore() { const tempDir = await mkdtemp(join(tmpdir(), "ocas-test-")); const dbPath = join(tempDir, "vars.db"); - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const varStore = createVariableStore(dbPath, store); return { diff --git a/packages/core/src/list-pagination.test.ts b/packages/core/src/list-pagination.test.ts index 943fb60..6b52fee 100644 --- a/packages/core/src/list-pagination.test.ts +++ b/packages/core/src/list-pagination.test.ts @@ -22,7 +22,7 @@ async function putN( describe("listByType - pagination + sort + timestamps", () => { test("A1. returns objects with hash/created/updated", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const m = await store[BOOTSTRAP_STORE]({ type: "object" }); await putN(store, m, 3, 0); @@ -36,7 +36,7 @@ describe("listByType - pagination + sort + timestamps", () => { }); test("A2. default sort is created ASC", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const m = await store[BOOTSTRAP_STORE]({ type: "object" }); await putN(store, m, 4); @@ -49,7 +49,7 @@ describe("listByType - pagination + sort + timestamps", () => { }); test("A3. desc:true reverses order", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const m = await store[BOOTSTRAP_STORE]({ type: "object" }); await putN(store, m, 4); @@ -62,7 +62,7 @@ describe("listByType - pagination + sort + timestamps", () => { }); test("A4. sort: 'updated' is equivalent to 'created' for CAS nodes", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const m = await store[BOOTSTRAP_STORE]({ type: "object" }); await putN(store, m, 4); @@ -72,14 +72,14 @@ describe("listByType - pagination + sort + timestamps", () => { }); test("A5. limit truncates", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const m = await store[BOOTSTRAP_STORE]({ type: "object" }); await putN(store, m, 5, 0); expect(store.listByType(m, { limit: 2 })).toHaveLength(2); }); test("A6. offset skips", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const m = await store[BOOTSTRAP_STORE]({ type: "object" }); await putN(store, m, 5); @@ -90,21 +90,21 @@ describe("listByType - pagination + sort + timestamps", () => { }); test("A7. limit:0 returns empty array", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const m = await store[BOOTSTRAP_STORE]({ type: "object" }); await putN(store, m, 3, 0); expect(store.listByType(m, { limit: 0 })).toEqual([]); }); test("A8. offset past end returns empty array", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const m = await store[BOOTSTRAP_STORE]({ type: "object" }); await putN(store, m, 3, 0); expect(store.listByType(m, { offset: 100 })).toEqual([]); }); test("A9. core has no default limit (returns all)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const m = await store[BOOTSTRAP_STORE]({ type: "object" }); await putN(store, m, 150, 0); // No CLI-layer cap; with 150 nodes of type m (plus m itself which is @@ -113,7 +113,7 @@ describe("listByType - pagination + sort + timestamps", () => { }); test("A10. desc + offset + limit combined", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const m = await store[BOOTSTRAP_STORE]({ type: "object" }); await putN(store, m, 5, 15); const all = store.listByType(m); @@ -128,7 +128,7 @@ describe("listByType - pagination + sort + timestamps", () => { describe("listMeta / listSchemas - pagination", () => { test("B1. listMeta returns {hash,created,updated}", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const h = await store[BOOTSTRAP_STORE]({ type: "object" }); const list = store.listMeta(); expect(list).toHaveLength(1); @@ -139,7 +139,7 @@ describe("listMeta / listSchemas - pagination", () => { }); test("B2. listMeta has no default limit (returns all)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; for (let i = 0; i < 150; i++) { await store[BOOTSTRAP_STORE]({ type: "object", i }); } @@ -147,7 +147,7 @@ describe("listMeta / listSchemas - pagination", () => { }); test("B3. listMeta limit/offset/desc", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; for (let i = 0; i < 5; i++) { await store[BOOTSTRAP_STORE]({ type: "object", i }); await new Promise((r) => setTimeout(r, 2)); @@ -159,7 +159,7 @@ describe("listMeta / listSchemas - pagination", () => { }); test("B4. listSchemas returns objects, supports limit", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const m = await store[BOOTSTRAP_STORE]({ type: "object" }); await store.put(m, { type: "string" }); await store.put(m, { type: "number" }); @@ -175,7 +175,7 @@ describe("listMeta / listSchemas - pagination", () => { describe("Determinism / edge cases", () => { test("I1. same-ms timestamps yield deterministic ordering across calls", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const m = await store[BOOTSTRAP_STORE]({ type: "object" }); // No delay → likely same millisecond await putN(store, m, 5, 0); @@ -185,7 +185,7 @@ describe("Determinism / edge cases", () => { }); test("I2. empty store returns []", () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; expect(store.listByType("0000000000000")).toEqual([]); expect(store.listMeta()).toEqual([]); expect(store.listSchemas()).toEqual([]); diff --git a/packages/core/src/mem-store.ts b/packages/core/src/mem-store.ts index 51806c2..75ac1f3 100644 --- a/packages/core/src/mem-store.ts +++ b/packages/core/src/mem-store.ts @@ -3,15 +3,17 @@ import { BOOTSTRAP_STORE } from "./bootstrap-capable.js"; import { createMemoryStore } from "./store.js"; import type { CasNode, Hash, ListEntry, ListOptions } from "./types.js"; -/** In-memory store wrapper used by schema validation tests. */ +/** In-memory store wrapper used by schema validation tests. Wraps the + * `cas` sub-store of an `OcasStore` and exposes the legacy + * `BootstrapCapableStore` interface (async `put`, etc.). */ export class MemStore implements BootstrapCapableStore { - readonly #inner: BootstrapCapableStore; + readonly #inner: ReturnType["cas"]; constructor() { - this.#inner = createMemoryStore(); + this.#inner = createMemoryStore().cas; } - put(typeHash: Hash, payload: unknown): Promise { + async put(typeHash: Hash, payload: unknown): Promise { return this.#inner.put(typeHash, payload); } @@ -43,7 +45,7 @@ export class MemStore implements BootstrapCapableStore { this.#inner.delete(hash); } - [BOOTSTRAP_STORE](payload: unknown): Promise { + async [BOOTSTRAP_STORE](payload: unknown): Promise { return this.#inner[BOOTSTRAP_STORE](payload); } } diff --git a/packages/core/src/output-templates.test.ts b/packages/core/src/output-templates.test.ts index 2d5f009..469ef9d 100644 --- a/packages/core/src/output-templates.test.ts +++ b/packages/core/src/output-templates.test.ts @@ -43,7 +43,7 @@ describe("registerOutputTemplates", () => { test("registers a template for every @ocas/output/* schema", async () => { tempDir = await mkdtemp(join(tmpdir(), "ocas-tmpl-")); - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); varStore = createVariableStore(join(tempDir, "vars.db"), store); @@ -58,7 +58,7 @@ describe("registerOutputTemplates", () => { test("each template is retrievable via @ocas/template/text/", async () => { tempDir = await mkdtemp(join(tmpdir(), "ocas-tmpl-")); - store = createMemoryStore(); + store = createMemoryStore().cas; const aliases = await bootstrap(store); varStore = createVariableStore(join(tempDir, "vars.db"), store); @@ -84,7 +84,7 @@ describe("registerOutputTemplates", () => { test("is idempotent — safe to call multiple times", async () => { tempDir = await mkdtemp(join(tmpdir(), "ocas-tmpl-")); - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); varStore = createVariableStore(join(tempDir, "vars.db"), store); @@ -96,7 +96,7 @@ describe("registerOutputTemplates", () => { test("@ocas/output/put template contains payload reference", async () => { tempDir = await mkdtemp(join(tmpdir(), "ocas-tmpl-")); - store = createMemoryStore(); + store = createMemoryStore().cas; const aliases = await bootstrap(store); varStore = createVariableStore(join(tempDir, "vars.db"), store); diff --git a/packages/core/src/render.test.ts b/packages/core/src/render.test.ts index 8076d80..8e0210e 100644 --- a/packages/core/src/render.test.ts +++ b/packages/core/src/render.test.ts @@ -8,7 +8,7 @@ import { CasNodeNotFoundError } from "./variable-store.js"; describe("Suite 1: Basic Rendering (No Nesting)", () => { test("1.1 Render Simple Primitives", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const textSchema = await putSchema(store, { type: "string" }); const hash = await store.put(textSchema, "hello"); @@ -20,7 +20,7 @@ describe("Suite 1: Basic Rendering (No Nesting)", () => { }); test("1.2 Render Object Node (Flat)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const objSchema = await putSchema(store, { type: "object", @@ -40,7 +40,7 @@ describe("Suite 1: Basic Rendering (No Nesting)", () => { }); test("1.3 Render Array Node (Flat)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const arraySchema = await putSchema(store, { type: "array", @@ -56,7 +56,7 @@ describe("Suite 1: Basic Rendering (No Nesting)", () => { }); test("1.4 Render with resolution=0 (Force Reference)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const textSchema = await putSchema(store, { type: "string" }); const hash = await store.put(textSchema, "hello"); @@ -67,7 +67,7 @@ describe("Suite 1: Basic Rendering (No Nesting)", () => { }); test("1.5 Render Non-existent Hash Throws Error", () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const fakeHash = "ZZZZZZZZZZZZZ" as Hash; // Non-existent root node should throw @@ -79,7 +79,7 @@ describe("Suite 1: Basic Rendering (No Nesting)", () => { describe("Suite 2: Resolution Decay Model", () => { test("2.1 Single-level Nesting with Default Decay", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const childSchema = await putSchema(store, { @@ -115,7 +115,7 @@ describe("Suite 2: Resolution Decay Model", () => { }); test("2.2 Multi-level Nesting Reaches Epsilon", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const leafSchema = await putSchema(store, { @@ -151,7 +151,7 @@ describe("Suite 2: Resolution Decay Model", () => { }); test("2.3 High Decay (Quick Cutoff)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const nodeSchema = await putSchema(store, { @@ -189,7 +189,7 @@ describe("Suite 2: Resolution Decay Model", () => { }); test("2.4 Low Decay (Deep Expansion)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const nodeSchema = await putSchema(store, { @@ -224,7 +224,7 @@ describe("Suite 2: Resolution Decay Model", () => { }); test("2.5 Starting Resolution Below 1.0", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const nodeSchema = await putSchema(store, { @@ -262,7 +262,7 @@ describe("Suite 2: Resolution Decay Model", () => { describe("Suite 3: Complex Graph Structures", () => { test("3.1 Multiple Child References", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const itemSchema = await putSchema(store, { @@ -301,7 +301,7 @@ describe("Suite 3: Complex Graph Structures", () => { }); test("3.2 Object with Multiple ocas_ref Fields", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const childSchema = await putSchema(store, { @@ -340,7 +340,7 @@ describe("Suite 3: Complex Graph Structures", () => { }); test("3.3 Cycle Detection", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const nodeSchema = await putSchema(store, { @@ -372,7 +372,7 @@ describe("Suite 3: Complex Graph Structures", () => { }); test("3.4 DAG (Shared Descendant)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const leafSchema = await putSchema(store, { @@ -423,7 +423,7 @@ describe("Suite 3: Complex Graph Structures", () => { }); test("3.5 Deep Tree", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const nodeSchema = await putSchema(store, { @@ -464,7 +464,7 @@ describe("Suite 3: Complex Graph Structures", () => { describe("Suite 4: Epsilon Boundary Cases", () => { test("4.1 Resolution Exactly at Epsilon", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const textSchema = await putSchema(store, { type: "string" }); const hash = await store.put(textSchema, "test"); @@ -479,7 +479,7 @@ describe("Suite 4: Epsilon Boundary Cases", () => { }); test("4.2 Resolution Just Above Epsilon", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const textSchema = await putSchema(store, { type: "string" }); const hash = await store.put(textSchema, "test"); @@ -494,7 +494,7 @@ describe("Suite 4: Epsilon Boundary Cases", () => { }); test("4.3 Very Small Epsilon (Deep Expansion)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const nodeSchema = await putSchema(store, { @@ -529,7 +529,7 @@ describe("Suite 4: Epsilon Boundary Cases", () => { }); test("4.4 Zero Epsilon (Never Prune)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const nodeSchema = await putSchema(store, { @@ -566,7 +566,7 @@ describe("Suite 4: Epsilon Boundary Cases", () => { describe("Suite 5: YAML Output Format", () => { test("5.1 Valid YAML Syntax", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const objSchema = await putSchema(store, { type: "object", @@ -584,7 +584,7 @@ describe("Suite 5: YAML Output Format", () => { }); test("5.2 Nested Object Indentation", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const nestedSchema = await putSchema(store, { type: "object", @@ -610,7 +610,7 @@ describe("Suite 5: YAML Output Format", () => { }); test("5.3 Array Rendering", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const arraySchema = await putSchema(store, { type: "array", @@ -625,7 +625,7 @@ describe("Suite 5: YAML Output Format", () => { }); test("5.4 CAS Reference in YAML", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const childSchema = await putSchema(store, { @@ -655,7 +655,7 @@ describe("Suite 5: YAML Output Format", () => { }); test("5.5 Special Characters Escaping", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const textSchema = await putSchema(store, { type: "string" }); const hash = await store.put(textSchema, "line1\nline2: value"); @@ -667,7 +667,7 @@ describe("Suite 5: YAML Output Format", () => { }); test("5.6 Null Handling", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const nullableSchema = await putSchema(store, { type: "object", @@ -687,7 +687,7 @@ describe("Suite 5: YAML Output Format", () => { describe("Suite 6: Schema Integration", () => { test("6.1 Detect ocas_ref Fields via Schema", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const childSchema = await putSchema(store, { @@ -716,7 +716,7 @@ describe("Suite 6: Schema Integration", () => { }); test("6.2 Non-ocas_ref String Not Expanded", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const objSchema = await putSchema(store, { type: "object", @@ -734,7 +734,7 @@ describe("Suite 6: Schema Integration", () => { }); test("6.3 Array of ocas_ref", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const itemSchema = await putSchema(store, { @@ -763,7 +763,7 @@ describe("Suite 6: Schema Integration", () => { }); test("6.4 anyOf with ocas_ref (Nullable Reference)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const childSchema = await putSchema(store, { @@ -794,7 +794,7 @@ describe("Suite 6: Schema Integration", () => { }); test("6.5 Schema-less Node (Bootstrap Node)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const types = await bootstrap(store); const schemaHash = types["@ocas/schema"]; @@ -807,7 +807,7 @@ describe("Suite 6: Schema Integration", () => { describe("Suite 7: Error Handling", () => { test("7.1 Missing Referenced Node", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const parentSchema = await putSchema(store, { @@ -826,21 +826,21 @@ describe("Suite 7: Error Handling", () => { }); test("7.3 Invalid Resolution Parameter", () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const fakeHash = "AAAAAAAAAAAAA" as Hash; expect(() => render(store, fakeHash, { resolution: -1 })).toThrow(); }); test("7.4 Invalid Decay Parameter", () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const fakeHash = "AAAAAAAAAAAAA" as Hash; expect(() => render(store, fakeHash, { decay: 1.5 })).toThrow(); }); test("7.5 Invalid Epsilon Parameter", () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const fakeHash = "AAAAAAAAAAAAA" as Hash; expect(() => render(store, fakeHash, { epsilon: -0.01 })).toThrow(); @@ -849,7 +849,7 @@ describe("Suite 7: Error Handling", () => { describe("Suite 8: Performance & Edge Cases", () => { test("8.1 Large Payload", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const arraySchema = await putSchema(store, { type: "array", @@ -877,7 +877,7 @@ describe("Suite 8: Performance & Edge Cases", () => { }); test("8.2 Wide Fan-out", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const itemSchema = await putSchema(store, { @@ -909,7 +909,7 @@ describe("Suite 8: Performance & Edge Cases", () => { }); test("8.3 Empty Payload", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const emptySchema = await putSchema(store, { type: "object" }); const hash = await store.put(emptySchema, {}); @@ -920,7 +920,7 @@ describe("Suite 8: Performance & Edge Cases", () => { }); test("8.4 Unicode in Payload", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const textSchema = await putSchema(store, { type: "object", @@ -985,7 +985,7 @@ describe("Suite 9: renderDirect (in-memory rendering)", () => { }); test("9.5 Render with store expands ocas_ref fields", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); // Create a child node @@ -1051,7 +1051,7 @@ describe("Suite 9: renderDirect (in-memory rendering)", () => { }); test("9.10 store present but schema missing — renders without ref expansion", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const unknownType = "ZZZZZZZZZZZZ0" as Hash; const output = renderDirect(unknownType, { key: "val" }, store, null); @@ -1061,7 +1061,7 @@ describe("Suite 9: renderDirect (in-memory rendering)", () => { describe("Suite 10: Missing Root Hash Error Handling (Issue #53)", () => { test("10.1 renderAsync() throws CasNodeNotFoundError for missing root hash", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const fakeHash = "AAAAAAAAAAAAA" as Hash; @@ -1075,7 +1075,7 @@ describe("Suite 10: Missing Root Hash Error Handling (Issue #53)", () => { }); test("10.2 render() throws CasNodeNotFoundError for missing root hash", () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const fakeHash = "ZZZZZZZZZZZZZ" as Hash; expect(() => render(store, fakeHash)).toThrow(CasNodeNotFoundError); @@ -1084,7 +1084,7 @@ describe("Suite 10: Missing Root Hash Error Handling (Issue #53)", () => { }); test("10.3 renderDirect() does NOT throw for non-existent type hash", () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const fakeTypeHash = "0000000000000" as Hash; const output = renderDirect(fakeTypeHash, { key: "value" }, store, null); @@ -1092,7 +1092,7 @@ describe("Suite 10: Missing Root Hash Error Handling (Issue #53)", () => { }); test("10.4 Missing nested node renders as cas: reference (no error)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const parentSchema = await putSchema(store, { @@ -1116,7 +1116,7 @@ describe("Suite 10: Missing Root Hash Error Handling (Issue #53)", () => { }); test("10.5 Resolution below epsilon renders as cas: reference (no error)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const nodeSchema = await putSchema(store, { diff --git a/packages/core/src/schema.test.ts b/packages/core/src/schema.test.ts index 53134b9..87a8dc5 100644 --- a/packages/core/src/schema.test.ts +++ b/packages/core/src/schema.test.ts @@ -10,14 +10,14 @@ import type { CasNode } from "./types.js"; // ────────────────────────────────────────────────────────────────────────────── describe("putSchema", () => { test("returns a valid 13-char hash", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { type: "object", properties: {} }); expect(hash).toHaveLength(13); expect(hash).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); }); test("schema node is stored in the store", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schema = { type: "object", properties: { name: { type: "string" } } }; const hash = await putSchema(store, schema); @@ -28,7 +28,7 @@ describe("putSchema", () => { }); test("schema node type equals the meta-schema hash", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const metaHash = builtinSchemas["@ocas/schema"] ?? ""; const schemaHash = await putSchema(store, { type: "string" }); @@ -38,7 +38,7 @@ describe("putSchema", () => { }); test("putSchema is idempotent: same schema → same hash", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schema = { type: "number" }; const h1 = await putSchema(store, schema); const h2 = await putSchema(store, schema); @@ -47,7 +47,7 @@ describe("putSchema", () => { }); test("different schemas produce different hashes", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const h1 = await putSchema(store, { type: "string" }); const h2 = await putSchema(store, { type: "number" }); @@ -60,7 +60,7 @@ describe("putSchema", () => { // ────────────────────────────────────────────────────────────────────────────── describe("getSchema", () => { test("returns the original schema object", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schema = { type: "object", properties: { age: { type: "number" } } }; const hash = await putSchema(store, schema); @@ -68,12 +68,12 @@ describe("getSchema", () => { }); test("returns null for an unknown hash", () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; expect(getSchema(store, "0000000000000")).toBeNull(); }); test("roundtrip: put then get returns the same schema", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schema = { type: "object", required: ["id"], @@ -92,7 +92,7 @@ describe("getSchema", () => { // ────────────────────────────────────────────────────────────────────────────── describe("validate", () => { test("returns true when payload matches the schema", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schemaHash = await putSchema(store, { type: "object", properties: { name: { type: "string" }, age: { type: "number" } }, @@ -105,7 +105,7 @@ describe("validate", () => { }); test("returns false when payload violates the schema", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schemaHash = await putSchema(store, { type: "object", properties: { count: { type: "number" } }, @@ -118,7 +118,7 @@ describe("validate", () => { }); test("returns false when required field is missing", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schemaHash = await putSchema(store, { type: "object", required: ["title"], @@ -131,7 +131,7 @@ describe("validate", () => { }); test("returns false when schema cannot be found", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const fakeNode: CasNode = { type: "0000000000000", payload: { x: 1 }, @@ -147,7 +147,7 @@ describe("validate", () => { // ────────────────────────────────────────────────────────────────────────────── describe("refs", () => { test("returns empty array when schema has no ocas_ref fields", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schemaHash = await putSchema(store, { type: "object", properties: { title: { type: "string" } }, @@ -159,7 +159,7 @@ describe("refs", () => { }); test("returns the ocas_ref hash values from payload", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schemaHash = await putSchema(store, { type: "object", properties: { @@ -179,7 +179,7 @@ describe("refs", () => { }); test("collects multiple ocas_ref fields", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schemaHash = await putSchema(store, { type: "object", properties: { @@ -203,7 +203,7 @@ describe("refs", () => { }); test("skips null/undefined ocas_ref values", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schemaHash = await putSchema(store, { type: "object", properties: { @@ -219,7 +219,7 @@ describe("refs", () => { }); test("returns empty array when schema is not found", () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const orphanNode: CasNode = { type: "0000000000000", payload: { x: 1 }, @@ -235,7 +235,7 @@ describe("refs", () => { // ────────────────────────────────────────────────────────────────────────────── describe("walk", () => { test("visits a single node with no refs", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schemaHash = await putSchema(store, { type: "object", properties: { val: { type: "number" } }, @@ -249,7 +249,7 @@ describe("walk", () => { }); test("visits all reachable nodes in a chain A → B → C", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schemaHash = await putSchema(store, { type: "object", properties: { @@ -273,7 +273,7 @@ describe("walk", () => { }); test("handles cycles without infinite loop", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schemaHash = await putSchema(store, { type: "object", properties: { @@ -316,7 +316,7 @@ describe("walk", () => { }); test("skips missing hashes gracefully", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schemaHash = await putSchema(store, { type: "object", properties: { ref: { type: "string", format: "ocas_ref" } }, @@ -331,7 +331,7 @@ describe("walk", () => { }); test("visitor receives both hash and node", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schemaHash = await putSchema(store, { type: "object", properties: { x: { type: "number" } }, @@ -355,7 +355,7 @@ describe("walk", () => { // ────────────────────────────────────────────────────────────────────────────── describe("bootstrap meta-schema self-reference", () => { test("metaNode.type === metaHash (self-referencing)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const metaHash = builtinSchemas["@ocas/schema"] ?? ""; const metaNode = store.get(metaHash) as CasNode; @@ -364,7 +364,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("schema nodes have type === metaHash", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const metaHash = builtinSchemas["@ocas/schema"] ?? ""; const schemaHash = await putSchema(store, { type: "string" }); @@ -374,7 +374,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("data nodes have type === schemaHash (not metaHash)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const metaHash = builtinSchemas["@ocas/schema"] ?? ""; const schemaHash = await putSchema(store, { @@ -391,7 +391,7 @@ describe("bootstrap meta-schema self-reference", () => { // ── P1 leaf constraints ────────────────────────────────────────────────── test("accepts schema with numeric constraints (minimum/maximum)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { type: "number", minimum: 0, @@ -413,7 +413,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("accepts schema with string constraints (minLength/maxLength/pattern)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { type: "string", minLength: 1, @@ -430,7 +430,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("accepts schema with array constraints (minItems/maxItems/uniqueItems)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { type: "array", items: { type: "number" }, @@ -451,7 +451,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("rejects schema with wrong constraint types", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await expect( putSchema(store, { type: "number", minimum: "zero" } as never), ).rejects.toThrow(); @@ -464,7 +464,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("accepts schema with nested property constraints", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { type: "object", properties: { @@ -490,7 +490,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("bootstrap is idempotent across putSchema calls", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const metaHash = builtinSchemas["@ocas/schema"] ?? ""; @@ -505,7 +505,7 @@ describe("bootstrap meta-schema self-reference", () => { // ── P2 combinators, conditionals, and leaf constraints ────────────────── test("accepts schema with allOf", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { allOf: [ { type: "object", properties: { name: { type: "string" } } }, @@ -522,7 +522,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("accepts schema with if/then/else", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { type: "object", properties: { @@ -538,7 +538,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("accepts schema with patternProperties", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { type: "object", patternProperties: { @@ -552,7 +552,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("accepts schema with prefixItems (tuple)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { type: "array", prefixItems: [{ type: "string" }, { type: "number" }], @@ -561,7 +561,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("accepts schema with multipleOf", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { type: "number", multipleOf: 5, @@ -576,7 +576,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("accepts schema with minProperties/maxProperties", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { type: "object", minProperties: 1, @@ -592,7 +592,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("accepts schema with default value", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { type: "string", default: "hello", @@ -601,7 +601,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("rejects invalid P2 keyword types", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await expect( putSchema(store, { allOf: "not-array" } as never), ).rejects.toThrow(); @@ -614,7 +614,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("collectRefs traverses allOf sub-schemas", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const innerSchema = await putSchema(store, { type: "string" }); const schema = await putSchema(store, { allOf: [ @@ -633,7 +633,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("collectRefs traverses patternProperties", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const innerSchema = await putSchema(store, { type: "string" }); const schema = await putSchema(store, { type: "object", @@ -650,7 +650,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("collectRefs traverses prefixItems", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const innerSchema = await putSchema(store, { type: "string" }); const schema = await putSchema(store, { type: "array", @@ -667,7 +667,7 @@ describe("bootstrap meta-schema self-reference", () => { // ── P3 combinators, propertyNames, and metadata ────────────────────────── test("accepts schema with not", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { not: { type: "string" }, }); @@ -681,7 +681,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("accepts schema with contains", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { type: "array", contains: { type: "number", minimum: 10 }, @@ -696,7 +696,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("accepts schema with propertyNames", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { type: "object", propertyNames: { pattern: "^[a-z]+$" }, @@ -711,7 +711,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("accepts schema with metadata keywords", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { type: "string", examples: ["hello", "world"], @@ -723,7 +723,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("rejects invalid P3 keyword types", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await expect( putSchema(store, { not: "not-object" } as never), ).rejects.toThrow(); @@ -737,7 +737,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("collectRefs traverses contains", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const innerSchema = await putSchema(store, { type: "string" }); const schema = await putSchema(store, { type: "array", diff --git a/packages/core/src/schema.ts b/packages/core/src/schema.ts index cfd370b..251499e 100644 --- a/packages/core/src/schema.ts +++ b/packages/core/src/schema.ts @@ -262,7 +262,7 @@ export async function putSchema( "Invalid schema: input does not conform to the ocas JSON Schema meta-schema", ); } - return store.put(metaHash, jsonSchema); + return Promise.resolve(store.put(metaHash, jsonSchema)); } /** diff --git a/packages/core/src/store.test.ts b/packages/core/src/store.test.ts index f6a4b1e..1f9d79b 100644 --- a/packages/core/src/store.test.ts +++ b/packages/core/src/store.test.ts @@ -2,88 +2,154 @@ import { describe, expect, test } from "bun:test"; import { BOOTSTRAP_STORE } from "./bootstrap-capable.js"; import { createMemoryStore } from "./store.js"; -describe("createMemoryStore – meta and schema indexes", () => { +describe("A. createMemoryStore – shape", () => { + test("A1. returns an object with cas, var, tag sub-stores", () => { + const store = createMemoryStore(); + expect(typeof store.cas).toBe("object"); + expect(store.cas).not.toBeNull(); + expect(typeof store.var).toBe("object"); + expect(store.var).not.toBeNull(); + expect(typeof store.tag).toBe("object"); + expect(store.tag).not.toBeNull(); + }); + + test("A2. cas/var/tag are independent objects", () => { + const store = createMemoryStore(); + expect(store.cas).not.toBe(store.var as unknown as typeof store.cas); + expect(store.cas).not.toBe(store.tag as unknown as typeof store.cas); + expect(store.var).not.toBe(store.tag as unknown as typeof store.var); + }); +}); + +describe("B. cas sub-store – meta and schema indexes", () => { test("B1. listMeta and listSchemas are empty on a fresh store", () => { const store = createMemoryStore(); - expect(store.listMeta()).toEqual([]); - expect(store.listSchemas()).toEqual([]); + expect(store.cas.listMeta()).toEqual([]); + expect(store.cas.listSchemas()).toEqual([]); }); - test("B2. self-referencing put adds hash to metaSet and listSchemas", async () => { + test("B2. self-referencing put adds hash to metaSet and listSchemas", () => { const store = createMemoryStore(); - const hash = await store[BOOTSTRAP_STORE]({ type: "object" }); - - expect(store.listMeta().map((e) => e.hash)).toContain(hash); - expect(store.listSchemas().map((e) => e.hash)).toContain(hash); + const hash = store.cas[BOOTSTRAP_STORE]({ type: "object" }) as string; + expect(store.cas.listMeta().map((e) => e.hash)).toContain(hash); + expect(store.cas.listSchemas().map((e) => e.hash)).toContain(hash); }); - test("B3. regular put does not add hash to metaSet", async () => { + test("B3. regular put does not add hash to metaSet", () => { const store = createMemoryStore(); - const metaHash = await store[BOOTSTRAP_STORE]({ type: "object" }); - const schemaHash = await store.put(metaHash, { type: "string" }); - - expect(store.listMeta().map((e) => e.hash)).not.toContain(schemaHash); - expect(store.listMeta().map((e) => e.hash)).toContain(metaHash); + const metaHash = store.cas[BOOTSTRAP_STORE]({ type: "object" }) as string; + const schemaHash = store.cas.put(metaHash, { type: "string" }); + expect(store.cas.listMeta().map((e) => e.hash)).not.toContain(schemaHash); + expect(store.cas.listMeta().map((e) => e.hash)).toContain(metaHash); }); - test("B4. schema typed by meta-schema appears in listSchemas", async () => { + test("B4. schema typed by meta-schema appears in listSchemas", () => { const store = createMemoryStore(); - const m = await store[BOOTSTRAP_STORE]({ type: "object" }); - const s = await store.put(m, { type: "string" }); - - const schemas = store.listSchemas().map((e) => e.hash); + const m = store.cas[BOOTSTRAP_STORE]({ type: "object" }) as string; + const s = store.cas.put(m, { type: "string" }); + const schemas = store.cas.listSchemas().map((e) => e.hash); expect(schemas).toContain(m); expect(schemas).toContain(s); - - const meta = store.listMeta().map((e) => e.hash); + const meta = store.cas.listMeta().map((e) => e.hash); expect(meta).toContain(m); expect(meta).not.toContain(s); }); - test("B5. multiple meta-schemas (versioning)", async () => { + test("B5. multiple meta-schemas (versioning)", () => { const store = createMemoryStore(); - const m1 = await store[BOOTSTRAP_STORE]({ type: "object", title: "v1" }); - const m2 = await store[BOOTSTRAP_STORE]({ type: "object", title: "v2" }); - const s1 = await store.put(m1, { type: "string" }); - const s2 = await store.put(m2, { type: "number" }); - - const meta = store.listMeta().map((e) => e.hash); + const m1 = store.cas[BOOTSTRAP_STORE]({ + type: "object", + title: "v1", + }) as string; + const m2 = store.cas[BOOTSTRAP_STORE]({ + type: "object", + title: "v2", + }) as string; + const s1 = store.cas.put(m1, { type: "string" }); + const s2 = store.cas.put(m2, { type: "number" }); + const meta = store.cas.listMeta().map((e) => e.hash); expect(meta).toContain(m1); expect(meta).toContain(m2); expect(meta).toHaveLength(2); - - const schemas = store.listSchemas().map((e) => e.hash); + const schemas = store.cas.listSchemas().map((e) => e.hash); expect(schemas).toContain(m1); expect(schemas).toContain(m2); expect(schemas).toContain(s1); expect(schemas).toContain(s2); }); - test("B6. idempotent self-referencing put does not duplicate", async () => { + test("B6. idempotent self-referencing put does not duplicate", () => { const store = createMemoryStore(); const payload = { type: "object", title: "dup" }; - const h1 = await store[BOOTSTRAP_STORE](payload); - const h2 = await store[BOOTSTRAP_STORE](payload); + const h1 = store.cas[BOOTSTRAP_STORE](payload) as string; + const h2 = store.cas[BOOTSTRAP_STORE](payload) as string; expect(h1).toBe(h2); - - const meta = store.listMeta().map((e) => e.hash); - const occurrences = meta.filter((h) => h === h1).length; - expect(occurrences).toBe(1); + const meta = store.cas.listMeta().map((e) => e.hash); + expect(meta.filter((h) => h === h1)).toHaveLength(1); }); - test("B7. delete removes hash from metaSet and listSchemas", async () => { + test("B7. delete removes hash from metaSet and listSchemas", () => { const store = createMemoryStore(); - const m = await store[BOOTSTRAP_STORE]({ type: "object" }); - const s = await store.put(m, { type: "string" }); + const m = store.cas[BOOTSTRAP_STORE]({ type: "object" }) as string; + const s = store.cas.put(m, { type: "string" }); + expect(store.cas.listMeta().map((e) => e.hash)).toContain(m); + expect(store.cas.listSchemas().map((e) => e.hash)).toContain(s); + store.cas.delete(m); + expect(store.cas.listMeta().map((e) => e.hash)).not.toContain(m); + expect(store.cas.listSchemas().map((e) => e.hash)).not.toContain(s); + expect(store.cas.listSchemas().map((e) => e.hash)).not.toContain(m); + }); - expect(store.listMeta().map((e) => e.hash)).toContain(m); - expect(store.listSchemas().map((e) => e.hash)).toContain(s); + test("B8. cas.put returns Hash synchronously (not a Promise)", () => { + const store = createMemoryStore(); + const m = store.cas[BOOTSTRAP_STORE]({ type: "object" }) as string; + const result = store.cas.put(m, { type: "string" }); + expect(typeof result).toBe("string"); + expect(result).toHaveLength(13); + }); - store.delete(m); - - expect(store.listMeta().map((e) => e.hash)).not.toContain(m); - // schemas typed by deleted meta no longer surface - expect(store.listSchemas().map((e) => e.hash)).not.toContain(s); - expect(store.listSchemas().map((e) => e.hash)).not.toContain(m); + test("B9. has/get/delete reflect put state", () => { + const store = createMemoryStore(); + const m = store.cas[BOOTSTRAP_STORE]({ type: "object" }) as string; + const h = store.cas.put(m, { type: "string" }); + expect(store.cas.has(h)).toBe(true); + expect(store.cas.get(h)?.payload).toEqual({ type: "string" }); + expect(store.cas.has("0000000000000")).toBe(false); + expect(store.cas.get("0000000000000")).toBeNull(); + expect(store.cas.delete(h)).toBe(true); + expect(store.cas.delete(h)).toBe(false); + expect(store.cas.has(h)).toBe(false); + }); +}); + +describe("E. cross-store independence", () => { + test("E1. tagging a hash does not surface it in cas.listByType", () => { + const store = createMemoryStore(); + const fakeTarget = "0000000000000"; + store.tag.tag(fakeTarget, [{ op: "set", key: "env", value: "prod" }]); + expect(store.cas.listByType(fakeTarget)).toEqual([]); + }); + + test("E2. setting a variable does not mutate the CAS node", () => { + const store = createMemoryStore(); + const m = store.cas[BOOTSTRAP_STORE]({ type: "object" }) as string; + const s = store.cas.put(m, { type: "string" }); + const value = store.cas.put(s, "hello"); + const before = store.cas.get(value); + store.var.set("@app/x", value); + const after = store.cas.get(value); + expect(after).toEqual(before); + }); + + test("E3. cas.delete does not cascade-delete a variable referencing it", () => { + const store = createMemoryStore(); + const m = store.cas[BOOTSTRAP_STORE]({ type: "object" }) as string; + const s = store.cas.put(m, { type: "string" }); + const value = store.cas.put(s, "hello"); + store.var.set("@app/x", value); + store.cas.delete(value); + const got = store.var.get("@app/x", s); + expect(got).not.toBeNull(); + expect(got?.value).toBe(value); }); }); diff --git a/packages/core/src/store.ts b/packages/core/src/store.ts index f7ced1f..6a9bb66 100644 --- a/packages/core/src/store.ts +++ b/packages/core/src/store.ts @@ -2,11 +2,83 @@ import { BOOTSTRAP_STORE, type BootstrapCapableStore, } from "./bootstrap-capable.js"; -import { computeHash, computeSelfHash } from "./hash.js"; +import { computeHashSync, computeSelfHashSync, initHasher } from "./hash.js"; import { applyListOptions, casListEntry } from "./list-utils.js"; -import type { CasNode, Hash, ListEntry, ListOptions } from "./types.js"; +import type { + CasNode, + Hash, + HistoryEntry, + ListEntry, + ListOptions, + OcasStore, + Tag, + TagOp, + TagStore, + VarListOptions, + VarSetOptions, + VarStore, +} from "./types.js"; +import type { Variable } from "./variable.js"; +import { + CasNodeNotFoundError, + InvalidVariableNameError, + MAX_HISTORY, + SchemaMismatchError, + TagLabelConflictError, + VariableNotFoundError, +} from "./variable-store.js"; -export function createMemoryStore(): BootstrapCapableStore { +// Initialise the xxhash WASM instance once at module load. This allows the +// CAS sub-store's `put` method to be synchronous (per the new CasStore type). +await initHasher(); + +/** + * The cas sub-store of an in-memory `OcasStore` — also satisfies the legacy + * `BootstrapCapableStore` interface so that helpers that have not yet been + * refactored (e.g. bootstrap, gc, render) continue to work against + * `store.cas`. + */ +export type MemoryCasStore = BootstrapCapableStore & { + put(typeHash: Hash, payload: unknown): Hash; + delete(hash: Hash): boolean; +}; + +function validateName(name: string): void { + if (name === "") { + throw new InvalidVariableNameError(name, "Name cannot be empty"); + } + const match = name.match(/^@([a-zA-Z][a-zA-Z0-9]*)\/(.+)$/); + if (!match) { + throw new InvalidVariableNameError( + name, + "Name must follow @scope/name format (e.g. @myapp/config)", + ); + } + const rest = match[2] as string; + if (rest.endsWith("/")) { + throw new InvalidVariableNameError( + name, + "Name cannot end with trailing slash", + ); + } + const segments = rest.split("/"); + for (const segment of segments) { + if (segment === "") { + throw new InvalidVariableNameError( + name, + "Name contains empty segment (consecutive slashes //)", + ); + } + if (!/^[a-zA-Z0-9._-]+$/.test(segment)) { + throw new InvalidVariableNameError( + name, + `Segment "${segment}" contains invalid characters (only a-z, A-Z, 0-9, ., _, - allowed)`, + ); + } + } +} + +function createCasStore(): MemoryCasStore { const data = new Map(); const byType = new Map>(); const metaSet = new Set(); @@ -20,8 +92,8 @@ export function createMemoryStore(): BootstrapCapableStore { set.add(hash); } - async function putSelfReferencing(payload: unknown): Promise { - const hash = await computeSelfHash(payload); + function putSelfReferencing(payload: unknown): Hash { + const hash = computeSelfHashSync(payload); if (!data.has(hash)) { data.set(hash, { type: hash, payload, timestamp: Date.now() }); indexHash(hash, hash); @@ -39,15 +111,13 @@ export function createMemoryStore(): BootstrapCapableStore { return result; } - const store: BootstrapCapableStore = { - async put(typeHash: Hash, payload: unknown): Promise { - const hash = await computeHash(typeHash, payload); - + const store: MemoryCasStore = { + put(typeHash: Hash, payload: unknown): Hash { + const hash = computeHashSync(typeHash, payload); if (!data.has(hash)) { data.set(hash, { type: typeHash, payload, timestamp: Date.now() }); indexHash(typeHash, hash); } - return hash; }, @@ -85,20 +155,19 @@ export function createMemoryStore(): BootstrapCapableStore { return applyListOptions(entriesForHashes(result), options); }, - delete(hash: Hash): void { + delete(hash: Hash): boolean { const node = data.get(hash); - if (node) { - data.delete(hash); - // Remove from type index - const set = byType.get(node.type); - if (set) { - set.delete(hash); - if (set.size === 0) { - byType.delete(node.type); - } + if (!node) return false; + data.delete(hash); + const set = byType.get(node.type); + if (set) { + set.delete(hash); + if (set.size === 0) { + byType.delete(node.type); } - metaSet.delete(hash); } + metaSet.delete(hash); + return true; }, [BOOTSTRAP_STORE]: putSelfReferencing, @@ -106,3 +175,403 @@ export function createMemoryStore(): BootstrapCapableStore { return store; } + +type VarRecord = { + name: string; + schema: Hash; + value: Hash; + created: number; + updated: number; + tags: Record; + labels: string[]; + history: HistoryEntry[]; +}; + +function cloneVar(rec: VarRecord): Variable { + return { + name: rec.name, + schema: rec.schema, + value: rec.value, + created: rec.created, + updated: rec.updated, + tags: { ...rec.tags }, + labels: [...rec.labels], + }; +} + +function createMemoryVarStore(cas: MemoryCasStore): VarStore { + // composite key: `${name}\u0000${schema}` + const records = new Map(); + const byName = new Map>(); // name -> set of composite keys + + function key(name: string, schema: Hash): string { + return `${name}\u0000${schema}`; + } + + function addIndex(name: string, k: string): void { + let set = byName.get(name); + if (!set) { + set = new Set(); + byName.set(name, set); + } + set.add(k); + } + + function removeIndex(name: string, k: string): void { + const set = byName.get(name); + if (!set) return; + set.delete(k); + if (set.size === 0) byName.delete(name); + } + + function extractSchema(hash: Hash): Hash { + const node = cas.get(hash); + if (node === null) { + throw new CasNodeNotFoundError(hash); + } + return node.type; + } + + function checkConflict(tags: Record, labels: string[]): void { + for (const tk of Object.keys(tags)) { + if (labels.includes(tk)) { + throw new TagLabelConflictError(tk, "label", "tag"); + } + } + } + + function pushHistory(rec: VarRecord, value: Hash, now: number): boolean { + if (rec.history.length > 0 && rec.history[0]?.value === value) { + return false; + } + const existingIdx = rec.history.findIndex((e) => e.value === value); + if (existingIdx > 0) { + rec.history.splice(existingIdx, 1); + } + rec.history.unshift({ value, position: 0, setAt: now }); + if (rec.history.length > MAX_HISTORY) { + rec.history.length = MAX_HISTORY; + } + for (let i = 0; i < rec.history.length; i++) { + const entry = rec.history[i]; + if (entry !== undefined) entry.position = i; + } + return true; + } + + const varStore: VarStore = { + set(name: string, hash: Hash, options?: VarSetOptions): Variable { + validateName(name); + const schema = extractSchema(hash); + const k = key(name, schema); + const existing = records.get(k); + const now = Date.now(); + + if (existing) { + const tags = options?.tags ?? existing.tags; + const labels = options?.labels ?? existing.labels; + if (options !== undefined) checkConflict(tags, labels); + const changed = pushHistory(existing, hash, now); + if (changed) { + existing.value = hash; + existing.updated = now; + } + if (options !== undefined) { + existing.tags = { ...tags }; + existing.labels = [...labels]; + } + return cloneVar(existing); + } + + const tags = options?.tags ?? {}; + const labels = options?.labels ?? []; + checkConflict(tags, labels); + const rec: VarRecord = { + name, + schema, + value: hash, + created: now, + updated: now, + tags: { ...tags }, + labels: [...labels], + history: [{ value: hash, position: 0, setAt: now }], + }; + records.set(k, rec); + addIndex(name, k); + return cloneVar(rec); + }, + + get(name: string, schema?: Hash): Variable | null { + if (schema !== undefined) { + const rec = records.get(key(name, schema)); + return rec ? cloneVar(rec) : null; + } + // No schema: if exactly one variant, return it; otherwise null + const set = byName.get(name); + if (!set || set.size !== 1) return null; + const onlyKey = set.values().next().value; + if (onlyKey === undefined) return null; + const rec = records.get(onlyKey); + return rec ? cloneVar(rec) : null; + }, + + remove(name: string, schema?: Hash): Variable[] { + if (schema !== undefined) { + const k = key(name, schema); + const rec = records.get(k); + if (!rec) return []; + records.delete(k); + removeIndex(name, k); + return [cloneVar(rec)]; + } + const set = byName.get(name); + if (!set) return []; + const removed: Variable[] = []; + for (const k of [...set]) { + const rec = records.get(k); + if (rec) { + removed.push(cloneVar(rec)); + records.delete(k); + } + } + byName.delete(name); + return removed; + }, + + update(name: string, hash: Hash, options?: VarSetOptions): Variable { + validateName(name); + const newSchema = extractSchema(hash); + // Find existing record by name; require existing schema match new schema + const set = byName.get(name); + if (!set || set.size === 0) { + throw new VariableNotFoundError(name, newSchema); + } + // find a record matching newSchema + const k = key(name, newSchema); + const existing = records.get(k); + if (!existing) { + // Find any existing — schema mismatch + for (const ek of set) { + const erec = records.get(ek); + if (erec) { + throw new SchemaMismatchError(erec.schema, newSchema); + } + } + throw new VariableNotFoundError(name, newSchema); + } + const now = Date.now(); + const tags = options?.tags ?? existing.tags; + const labels = options?.labels ?? existing.labels; + if (options !== undefined) checkConflict(tags, labels); + const changed = pushHistory(existing, hash, now); + if (changed) { + existing.value = hash; + existing.updated = now; + } + if (options !== undefined) { + existing.tags = { ...tags }; + existing.labels = [...labels]; + } + return cloneVar(existing); + }, + + list(options?: VarListOptions): Variable[] { + if ( + options?.namePrefix !== undefined && + options?.exactName !== undefined + ) { + throw new Error( + "namePrefix and exactName are mutually exclusive - cannot specify both", + ); + } + const namePrefix = options?.namePrefix; + const exactName = options?.exactName; + const schema = options?.schema; + const filterTags = options?.tags ?? {}; + const filterLabels = options?.labels ?? []; + const sort = options?.sort ?? "created"; + const desc = options?.desc ?? false; + const limit = options?.limit; + const offset = options?.offset ?? 0; + + if (limit !== undefined && limit <= 0) return []; + + let results: VarRecord[] = []; + for (const rec of records.values()) { + if (exactName !== undefined && rec.name !== exactName) continue; + if (namePrefix !== undefined && !rec.name.startsWith(namePrefix)) + continue; + if (schema !== undefined && rec.schema !== schema) continue; + let ok = true; + for (const [tk, tv] of Object.entries(filterTags)) { + if (rec.tags[tk] !== tv) { + ok = false; + break; + } + } + if (!ok) continue; + for (const lb of filterLabels) { + if (!rec.labels.includes(lb)) { + ok = false; + break; + } + } + if (!ok) continue; + results.push(rec); + } + + results.sort((a, b) => { + const av = sort === "updated" ? a.updated : a.created; + const bv = sort === "updated" ? b.updated : b.created; + if (av !== bv) return desc ? bv - av : av - bv; + return a.name < b.name ? -1 : a.name > b.name ? 1 : 0; + }); + + if (offset > 0) results = results.slice(offset); + if (limit !== undefined) results = results.slice(0, limit); + return results.map(cloneVar); + }, + + history(name: string, schema?: Hash): HistoryEntry[] { + if (schema !== undefined) { + const rec = records.get(key(name, schema)); + return rec ? rec.history.map((e) => ({ ...e })) : []; + } + const set = byName.get(name); + if (!set || set.size !== 1) return []; + const onlyKey = set.values().next().value; + if (onlyKey === undefined) return []; + const rec = records.get(onlyKey); + return rec ? rec.history.map((e) => ({ ...e })) : []; + }, + + close(): void { + // no-op for in-memory store + }, + }; + + return varStore; +} + +function createMemoryTagStore(): TagStore { + // target -> key -> Tag + const byTarget = new Map>(); + // key -> set of targets + const byKey = new Map>(); + // per-target ordering (created) + const targetOrder = new Map(); + + function addKeyIndex(key: string, target: Hash): void { + let set = byKey.get(key); + if (!set) { + set = new Set(); + byKey.set(key, set); + } + set.add(target); + } + + function removeKeyIndex(key: string, target: Hash): void { + const set = byKey.get(key); + if (!set) return; + // only remove if this target no longer has that key in any tag + const tmap = byTarget.get(target); + if (tmap && tmap.has(key)) return; + set.delete(target); + if (set.size === 0) byKey.delete(key); + } + + return { + tag(target: Hash, operations: TagOp[]): Tag[] { + let tmap = byTarget.get(target); + if (!tmap) { + tmap = new Map(); + byTarget.set(target, tmap); + } + const now = Date.now(); + for (const op of operations) { + if (op.op === "set") { + const tag: Tag = { + key: op.key, + value: op.value ?? null, + target, + created: tmap.get(op.key)?.created ?? now, + }; + tmap.set(op.key, tag); + addKeyIndex(op.key, target); + } else { + tmap.delete(op.key); + removeKeyIndex(op.key, target); + } + } + if (!targetOrder.has(target)) { + targetOrder.set(target, now); + } + // return the current tags + return [...tmap.values()].sort((a, b) => + a.key < b.key ? -1 : a.key > b.key ? 1 : 0, + ); + }, + + untag(target: Hash, keys: string[]): void { + const tmap = byTarget.get(target); + if (!tmap) return; + for (const k of keys) { + tmap.delete(k); + removeKeyIndex(k, target); + } + if (tmap.size === 0) { + byTarget.delete(target); + targetOrder.delete(target); + } + }, + + tags(target: Hash): Tag[] { + const tmap = byTarget.get(target); + if (!tmap) return []; + return [...tmap.values()].sort((a, b) => + a.key < b.key ? -1 : a.key > b.key ? 1 : 0, + ); + }, + + listByTag(tag: string, options?: ListOptions): Hash[] { + // accept "key" or "key=value" form + let key = tag; + let value: string | null | undefined; + const eqIdx = tag.indexOf("="); + if (eqIdx >= 0) { + key = tag.slice(0, eqIdx); + value = tag.slice(eqIdx + 1); + } + const targets = byKey.get(key); + if (!targets) return []; + let entries: ListEntry[] = []; + for (const t of targets) { + const tmap = byTarget.get(t); + if (!tmap) continue; + const tagEntry = tmap.get(key); + if (!tagEntry) continue; + if (value !== undefined && tagEntry.value !== value) continue; + entries.push(casListEntry(t, tagEntry.created)); + } + entries = applyListOptions(entries, options); + return entries.map((e) => e.hash); + }, + }; +} + +/** + * Create an in-memory `OcasStore` with three sub-stores: `cas`, `var`, `tag`. + * + * The `cas` sub-store also satisfies the legacy `BootstrapCapableStore` + * contract — it carries a `[BOOTSTRAP_STORE]` callable and a `listAll()` + * helper — so existing helpers (`bootstrap`, `gc`, `render`, …) can be + * called with `store.cas` until they are migrated to the unified surface. + */ +export function createMemoryStore(): OcasStore & { + cas: MemoryCasStore; +} { + const cas = createCasStore(); + const varStore = createMemoryVarStore(cas); + const tagStore = createMemoryTagStore(); + return { cas, var: varStore, tag: tagStore }; +} diff --git a/packages/core/src/tag-store.test.ts b/packages/core/src/tag-store.test.ts new file mode 100644 index 0000000..4f982d4 --- /dev/null +++ b/packages/core/src/tag-store.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, test } from "bun:test"; +import { createMemoryStore } from "./store.js"; + +const T1 = "AAAAAAAAAAAAA"; +const T2 = "BBBBBBBBBBBBB"; + +describe("In-memory TagStore", () => { + test("D1. tag set with key/value round-trip", () => { + const { tag } = createMemoryStore(); + const result = tag.tag(T1, [{ op: "set", key: "env", value: "prod" }]); + expect(result).toHaveLength(1); + expect(result[0]?.key).toBe("env"); + expect(result[0]?.value).toBe("prod"); + expect(result[0]?.target).toBe(T1); + expect(typeof result[0]?.created).toBe("number"); + expect(tag.tags(T1)).toEqual(result); + }); + + test("D2. label tag (value omitted) records value: null", () => { + const { tag } = createMemoryStore(); + tag.tag(T1, [{ op: "set", key: "pinned" }]); + const tags = tag.tags(T1); + expect(tags).toHaveLength(1); + expect(tags[0]?.key).toBe("pinned"); + expect(tags[0]?.value).toBeNull(); + }); + + test("D3. multiple ops in one call, sorted by key", () => { + const { tag } = createMemoryStore(); + const result = tag.tag(T1, [ + { op: "set", key: "b", value: "2" }, + { op: "set", key: "a", value: "1" }, + ]); + expect(result.map((t) => t.key)).toEqual(["a", "b"]); + expect(tag.tags(T1).map((t) => t.key)).toEqual(["a", "b"]); + }); + + test("D4. update existing key overwrites value", () => { + const { tag } = createMemoryStore(); + tag.tag(T1, [{ op: "set", key: "env", value: "prod" }]); + tag.tag(T1, [{ op: "set", key: "env", value: "dev" }]); + const tags = tag.tags(T1); + expect(tags).toHaveLength(1); + expect(tags[0]?.value).toBe("dev"); + }); + + test("D5. delete via tag op removes the entry", () => { + const { tag } = createMemoryStore(); + tag.tag(T1, [{ op: "set", key: "env", value: "prod" }]); + tag.tag(T1, [{ op: "delete", key: "env" }]); + expect(tag.tags(T1)).toEqual([]); + }); + + test("D6. untag removes listed keys; missing keys silently skipped", () => { + const { tag } = createMemoryStore(); + tag.tag(T1, [ + { op: "set", key: "a", value: "1" }, + { op: "set", key: "b", value: "2" }, + ]); + tag.untag(T1, ["a", "missing"]); + expect(tag.tags(T1).map((t) => t.key)).toEqual(["b"]); + }); + + test("D7. listByTag returns all targets with the bare key", () => { + const { tag } = createMemoryStore(); + tag.tag(T1, [{ op: "set", key: "env", value: "prod" }]); + tag.tag(T2, [{ op: "set", key: "env", value: "dev" }]); + const listed = tag.listByTag("env").sort(); + expect(listed).toEqual([T1, T2].sort()); + }); + + test("D8. listByTag with key=value form filters by exact value", () => { + const { tag } = createMemoryStore(); + tag.tag(T1, [{ op: "set", key: "env", value: "prod" }]); + tag.tag(T2, [{ op: "set", key: "env", value: "dev" }]); + expect(tag.listByTag("env=prod")).toEqual([T1]); + }); + + test("D9. ListOptions on listByTag (limit, offset, desc)", () => { + const { tag } = createMemoryStore(); + const targets: string[] = []; + for (let i = 0; i < 5; i++) { + const t = `${"C".repeat(12)}${i}`; + targets.push(t); + tag.tag(t, [{ op: "set", key: "k", value: String(i) }]); + } + expect(tag.listByTag("k", { limit: 2 })).toHaveLength(2); + expect(tag.listByTag("k", { offset: 3 })).toHaveLength(2); + const desc = tag.listByTag("k", { desc: true }); + expect(desc[0]).toBe(targets[targets.length - 1] as string); + }); + + test("D10. different targets are independent", () => { + const { tag } = createMemoryStore(); + tag.tag(T1, [{ op: "set", key: "k", value: "1" }]); + expect(tag.tags(T2)).toEqual([]); + }); + + test("D11. each Tag returned has a created timestamp", () => { + const { tag } = createMemoryStore(); + const before = Date.now(); + const result = tag.tag(T1, [{ op: "set", key: "k", value: "v" }]); + const after = Date.now(); + expect(result[0]?.created).toBeGreaterThanOrEqual(before); + expect(result[0]?.created).toBeLessThanOrEqual(after); + }); +}); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 3450846..b344606 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -51,7 +51,7 @@ export type ListEntry = { * Self-referencing nodes are created only via bootstrap(). */ export type Store = { - put(typeHash: Hash, payload: unknown): Promise; + put(typeHash: Hash, payload: unknown): Hash | Promise; get(hash: Hash): CasNode | null; has(hash: Hash): boolean; listByType(typeHash: Hash, options?: ListOptions): ListEntry[]; diff --git a/packages/core/src/var-store.test.ts b/packages/core/src/var-store.test.ts new file mode 100644 index 0000000..30bcf93 --- /dev/null +++ b/packages/core/src/var-store.test.ts @@ -0,0 +1,226 @@ +import { describe, expect, test } from "bun:test"; +import { createMemoryStore } from "./store.js"; +import type { Hash } from "./types.js"; +import { + CasNodeNotFoundError, + InvalidVariableNameError, + MAX_HISTORY, + SchemaMismatchError, + TagLabelConflictError, + VariableNotFoundError, +} from "./variable-store.js"; + +function makeStoreWithSchema(): { + store: ReturnType; + schema: Hash; + meta: Hash; + put: (payload: unknown) => Hash; +} { + const store = createMemoryStore(); + const meta = store.cas[Symbol.for("@ocas/core/bootstrap-store")]({ + type: "object", + }) as Hash; + const schema = store.cas.put(meta, { type: "string" }); + return { + store, + schema, + meta, + put: (payload) => store.cas.put(schema, payload), + }; +} + +describe("In-memory VarStore", () => { + test("C1. set + get round-trip", () => { + const { store, schema, put } = makeStoreWithSchema(); + const h = put("hello"); + const v = store.var.set("@app/x", h); + expect(v.name).toBe("@app/x"); + expect(v.value).toBe(h); + expect(v.schema).toBe(schema); + expect(typeof v.created).toBe("number"); + expect(typeof v.updated).toBe("number"); + expect(v.tags).toEqual({}); + expect(v.labels).toEqual([]); + + const got = store.var.get("@app/x", schema); + expect(got).not.toBeNull(); + expect(got?.value).toBe(h); + expect(got?.schema).toBe(schema); + }); + + test("C2. name validation", () => { + const { store, put } = makeStoreWithSchema(); + const h = put("v"); + expect(() => store.var.set("x", h)).toThrow(InvalidVariableNameError); + expect(() => store.var.set("@/x", h)).toThrow(InvalidVariableNameError); + expect(() => store.var.set("@app/", h)).toThrow(InvalidVariableNameError); + expect(() => store.var.set("@app//x", h)).toThrow(InvalidVariableNameError); + expect(() => store.var.set("@app/x.y_z-1", h)).not.toThrow(); + }); + + test("C3. set throws CasNodeNotFoundError if hash not in cas", () => { + const { store } = makeStoreWithSchema(); + expect(() => store.var.set("@app/x", "ZZZZZZZZZZZZZ")).toThrow( + CasNodeNotFoundError, + ); + }); + + test("C4. idempotent same-value set", async () => { + const { store, schema, put } = makeStoreWithSchema(); + const h = put("v"); + const v1 = store.var.set("@app/x", h); + await new Promise((r) => setTimeout(r, 5)); + const v2 = store.var.set("@app/x", h); + expect(v2.updated).toBe(v1.updated); + expect(store.var.history("@app/x", schema)).toHaveLength(1); + }); + + test("C5. update via re-set with new value bumps updated and history", async () => { + const { store, schema, put } = makeStoreWithSchema(); + const h1 = put("v1"); + const h2 = put("v2"); + const v1 = store.var.set("@app/x", h1); + await new Promise((r) => setTimeout(r, 5)); + const v2 = store.var.set("@app/x", h2); + expect(v2.updated).toBeGreaterThan(v1.updated); + const hist = store.var.history("@app/x", schema); + expect(hist.map((e) => e.value)).toEqual([h2, h1]); + expect(hist[0]?.position).toBe(0); + expect(hist[1]?.position).toBe(1); + }); + + test("C6. history bound to MAX_HISTORY (10)", () => { + const { store, schema, put } = makeStoreWithSchema(); + const hashes: Hash[] = []; + for (let i = 0; i < 12; i++) { + const h = put(`v${i}`); + hashes.push(h); + store.var.set("@app/x", h); + } + const hist = store.var.history("@app/x", schema); + expect(hist).toHaveLength(MAX_HISTORY); + expect(hist[0]?.value).toBe(hashes[11] as string); + }); + + test("C7. history rotation when re-setting an older value", () => { + const { store, schema, put } = makeStoreWithSchema(); + const h1 = put("v1"); + const h2 = put("v2"); + const h3 = put("v3"); + store.var.set("@app/x", h1); + store.var.set("@app/x", h2); + store.var.set("@app/x", h3); + expect(store.var.history("@app/x", schema).map((e) => e.value)).toEqual([ + h3, + h2, + h1, + ]); + store.var.set("@app/x", h1); + expect(store.var.history("@app/x", schema).map((e) => e.value)).toEqual([ + h1, + h3, + h2, + ]); + }); + + test("C8. tags + labels round-trip", () => { + const { store, schema, put } = makeStoreWithSchema(); + const h = put("v"); + store.var.set("@app/x", h, { + tags: { env: "prod" }, + labels: ["pinned"], + }); + const got = store.var.get("@app/x", schema); + expect(got?.tags).toEqual({ env: "prod" }); + expect(got?.labels).toEqual(["pinned"]); + }); + + test("C9. tag/label conflict throws TagLabelConflictError", () => { + const { store, put } = makeStoreWithSchema(); + const h = put("v"); + expect(() => + store.var.set("@app/x", h, { + tags: { x: "y" }, + labels: ["x"], + }), + ).toThrow(TagLabelConflictError); + }); + + test("C10. update requires existing variable and matching schema", () => { + const { store, put } = makeStoreWithSchema(); + const h = put("v"); + expect(() => store.var.update("@app/x", h)).toThrow(VariableNotFoundError); + store.var.set("@app/x", h); + + // Different schema for new value + const meta = store.cas[Symbol.for("@ocas/core/bootstrap-store")]({ + type: "object", + }) as Hash; + const otherSchema = store.cas.put(meta, { type: "number" }); + const h2 = store.cas.put(otherSchema, 42); + expect(() => store.var.update("@app/x", h2)).toThrow(SchemaMismatchError); + }); + + test("C11. remove with and without schema", () => { + const { store, schema, put } = makeStoreWithSchema(); + const h = put("v"); + store.var.set("@app/x", h); + const removed = store.var.remove("@app/x", schema); + expect(removed).toHaveLength(1); + expect(removed[0]?.value).toBe(h); + expect(store.var.get("@app/x", schema)).toBeNull(); + + // remove(name) without schema returns array; empty if none + expect(store.var.remove("@app/none")).toEqual([]); + + store.var.set("@app/y", put("y1")); + const removedAll = store.var.remove("@app/y"); + expect(Array.isArray(removedAll)).toBe(true); + expect(removedAll).toHaveLength(1); + }); + + test("C12. list filters and ListOptions", () => { + const { store, schema, put } = makeStoreWithSchema(); + const h1 = put("v1"); + const h2 = put("v2"); + store.var.set("@app/a", h1, { tags: { env: "prod" } }); + store.var.set("@app/b", h2, { labels: ["pinned"] }); + + expect(() => + store.var.list({ namePrefix: "@app", exactName: "@app/a" }), + ).toThrow(); + + const byPrefix = store.var.list({ namePrefix: "@app/" }); + expect(byPrefix.map((v) => v.name).sort()).toEqual(["@app/a", "@app/b"]); + + const exact = store.var.list({ exactName: "@app/a" }); + expect(exact).toHaveLength(1); + + const byTag = store.var.list({ tags: { env: "prod" } }); + expect(byTag.map((v) => v.name)).toEqual(["@app/a"]); + + const byLabel = store.var.list({ labels: ["pinned"] }); + expect(byLabel.map((v) => v.name)).toEqual(["@app/b"]); + + const bySchema = store.var.list({ schema }); + expect(bySchema).toHaveLength(2); + + const limited = store.var.list({ limit: 1 }); + expect(limited).toHaveLength(1); + }); + + test("C13. close() is a no-op", () => { + const { store } = makeStoreWithSchema(); + expect(() => store.var.close()).not.toThrow(); + }); + + test("C14. cas.delete does NOT cascade-delete the variable", () => { + const { store, schema, put } = makeStoreWithSchema(); + const h = put("v"); + store.var.set("@app/x", h); + store.cas.delete(h); + const got = store.var.get("@app/x", schema); + expect(got).not.toBeNull(); + expect(got?.value).toBe(h); + }); +}); diff --git a/packages/core/src/variable-list-pagination.test.ts b/packages/core/src/variable-list-pagination.test.ts index 93775c7..70f282e 100644 --- a/packages/core/src/variable-list-pagination.test.ts +++ b/packages/core/src/variable-list-pagination.test.ts @@ -16,7 +16,7 @@ let stringHash: Hash; beforeEach(async () => { dbDir = mkdtempSync(join(tmpdir(), "ocas-var-pagination-")); dbPath = join(dbDir, "vars.db"); - casStore = createMemoryStore(); + casStore = createMemoryStore().cas; const aliases = await bootstrap(casStore); stringHash = aliases["@ocas/string"] as Hash; varStore = createVariableStore(dbPath, casStore); diff --git a/packages/core/src/variable-store.test.ts b/packages/core/src/variable-store.test.ts index 7444c4f..c354bd4 100644 --- a/packages/core/src/variable-store.test.ts +++ b/packages/core/src/variable-store.test.ts @@ -25,7 +25,7 @@ const tmpDbPath = () => describe("VariableStore - Database Schema", () => { test("Database schema has (name, schema) composite primary key", () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const dbPath = tmpDbPath(); const varStore = new VariableStore(dbPath, store); @@ -61,7 +61,7 @@ describe("VariableStore - Database Schema", () => { }); test("Database indexes reference name instead of id/scope", () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const dbPath = tmpDbPath(); const varStore = new VariableStore(dbPath, store); @@ -92,7 +92,7 @@ describe("VariableStore - Database Schema", () => { }); test("variable_tags table has composite foreign key", () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const dbPath = tmpDbPath(); const varStore = new VariableStore(dbPath, store); @@ -129,7 +129,7 @@ describe("VariableStore - set() Upsert Method", () => { test("set() creates new variable when (name, schema) doesn't exist", async () => { // Setup: store with schema and data node - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object", @@ -161,7 +161,7 @@ describe("VariableStore - set() Upsert Method", () => { }); test("set() updates value when (name, schema) already exists", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object", @@ -197,7 +197,7 @@ describe("VariableStore - set() Upsert Method", () => { }); test("set() creates variable with tags and labels", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object" }); const dataHash = await store.put(schemaHash, {}); @@ -217,7 +217,7 @@ describe("VariableStore - set() Upsert Method", () => { }); test("set() preserves tags/labels when updating without options", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object", @@ -247,7 +247,7 @@ describe("VariableStore - set() Upsert Method", () => { }); test("set() allows same name with different schemas", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = await putSchema(store, { type: "object", @@ -285,7 +285,7 @@ describe("VariableStore - set() Upsert Method", () => { }); test("set() validates variable name", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object" }); const dataHash = await store.put(schemaHash, {}); @@ -320,7 +320,7 @@ describe("VariableStore - set() Upsert Method", () => { test("set() extracts schema from value hash internally", async () => { // Given: Two different schemas - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = await putSchema(store, { type: "number" }); const schemaB = await putSchema(store, { type: "string" }); @@ -349,7 +349,7 @@ describe("VariableStore - set() Upsert Method", () => { test("set() upserts based on extracted schema", async () => { // Given: Existing variable with schemaA - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = await putSchema(store, { type: "number" }); const value1 = await store.put(schemaA, 42); @@ -371,7 +371,7 @@ describe("VariableStore - set() Upsert Method", () => { }); test("set() throws CasNodeNotFoundError for invalid hash", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; dbPath = tmpDbPath(); const varStore = new VariableStore(dbPath, store); @@ -388,7 +388,7 @@ describe("VariableStore - set() Upsert Method", () => { }); test("set() throws TagLabelConflictError when updating with tag key that matches new label", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = { type: "object", properties: { x: { type: "number" } } }; const schemaHash = await putSchema(store, schema); @@ -413,7 +413,7 @@ describe("VariableStore - set() Upsert Method", () => { }); test("set() throws TagLabelConflictError when updating with label that matches new tag key", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = { type: "object", properties: { x: { type: "number" } } }; const schemaHash = await putSchema(store, schema); @@ -438,7 +438,7 @@ describe("VariableStore - set() Upsert Method", () => { }); test("set() allows updating tags/labels when no conflicts", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = { type: "object", properties: { x: { type: "number" } } }; const schemaHash = await putSchema(store, schema); @@ -482,7 +482,7 @@ describe("VariableStore - get() with Optional Schema", () => { test("get(name, schema) returns Variable when exists", async () => { // Given: Variable with (name, schema) - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = await putSchema(store, { type: "number" }); const value = await store.put(schema, 42); @@ -505,7 +505,7 @@ describe("VariableStore - get() with Optional Schema", () => { }); test("get(name, schema) returns null when name doesn't exist", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = await putSchema(store, { type: "number" }); @@ -523,7 +523,7 @@ describe("VariableStore - get() with Optional Schema", () => { test("get(name, schema) returns null when schema doesn't match", async () => { // Given: Variable with schemaA - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = await putSchema(store, { type: "number" }); const schemaB = await putSchema(store, { type: "string" }); @@ -545,7 +545,7 @@ describe("VariableStore - get() with Optional Schema", () => { test("get(name, schema) returns correct variant when multiple schemas exist", async () => { // Given: Same name with two different schemas - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = await putSchema(store, { type: "number" }); const schemaB = await putSchema(store, { type: "string" }); @@ -575,7 +575,7 @@ describe("VariableStore - get() with Optional Schema", () => { }); test("get(name, schema) includes tags and labels", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = await putSchema(store, { type: "object" }); const value = await store.put(schema, {}); @@ -598,7 +598,7 @@ describe("VariableStore - get() with Optional Schema", () => { }); test("get(name, schema) returns exact match", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = await putSchema(store, { type: "object", @@ -635,7 +635,7 @@ describe("VariableStore - get() with Optional Schema", () => { }); test("get(name, schema) returns null when combination doesn't exist", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = await putSchema(store, { type: "object", @@ -674,7 +674,7 @@ describe("VariableStore - remove() with Optional Schema", () => { }); test("remove(name) deletes all schema variants", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = await putSchema(store, { type: "object", @@ -712,7 +712,7 @@ describe("VariableStore - remove() with Optional Schema", () => { }); test("remove(name) returns empty array when variable doesn't exist", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; dbPath = tmpDbPath(); const varStore = new VariableStore(dbPath, store); @@ -725,7 +725,7 @@ describe("VariableStore - remove() with Optional Schema", () => { }); test("remove(name, schema) deletes only specified variant", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = await putSchema(store, { type: "object", @@ -762,7 +762,7 @@ describe("VariableStore - remove() with Optional Schema", () => { }); test("remove(name, schema) throws VariableNotFoundError when not found", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object" }); @@ -777,7 +777,7 @@ describe("VariableStore - remove() with Optional Schema", () => { }); test("remove() cascades deletion to tags and labels", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object" }); const dataHash = await store.put(schemaHash, {}); @@ -817,7 +817,7 @@ describe("VariableStore - remove() with Optional Schema", () => { }); test("remove(name) returns array even with single variant", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object" }); const dataHash = await store.put(schemaHash, {}); @@ -852,7 +852,7 @@ describe("VariableStore - Name Validation", () => { }); test("validateName accepts valid variable names", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object" }); const dataHash = await store.put(schemaHash, {}); @@ -878,7 +878,7 @@ describe("VariableStore - Name Validation", () => { }); test("validateName rejects empty name", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object" }); const dataHash = await store.put(schemaHash, {}); @@ -893,7 +893,7 @@ describe("VariableStore - Name Validation", () => { }); test("validateName rejects invalid characters", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object" }); const dataHash = await store.put(schemaHash, {}); @@ -933,7 +933,7 @@ describe("VariableStore - Name Validation", () => { }); test("validateName rejects empty segments", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object" }); const dataHash = await store.put(schemaHash, {}); @@ -958,7 +958,7 @@ describe("VariableStore - Name Validation", () => { }); test("validateName rejects leading or trailing slashes", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object" }); const dataHash = await store.put(schemaHash, {}); @@ -1034,7 +1034,7 @@ describe("VariableStore - validateName() Error Messages", () => { }); test("validateName error message mentions 'empty' for empty string", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); schemaHash = await putSchema(store, { type: "object" }); dataHash = await store.put(schemaHash, {}); @@ -1053,7 +1053,7 @@ describe("VariableStore - validateName() Error Messages", () => { }); test("validateName error message identifies specific invalid segment", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); schemaHash = await putSchema(store, { type: "object" }); dataHash = await store.put(schemaHash, {}); @@ -1073,7 +1073,7 @@ describe("VariableStore - validateName() Error Messages", () => { }); test("validateName error message explains consecutive slashes", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); schemaHash = await putSchema(store, { type: "object" }); dataHash = await store.put(schemaHash, {}); @@ -1092,7 +1092,7 @@ describe("VariableStore - validateName() Error Messages", () => { }); test("validateName error message distinguishes leading vs trailing slash", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); schemaHash = await putSchema(store, { type: "object" }); dataHash = await store.put(schemaHash, {}); @@ -1126,7 +1126,7 @@ describe("VariableStore - validateName() Error Messages", () => { }); test("validateName accepts valid names with dots, underscores, hyphens", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); schemaHash = await putSchema(store, { type: "object" }); dataHash = await store.put(schemaHash, {}); @@ -1160,7 +1160,7 @@ describe("VariableStore - Integration Tests", () => { }); test("Complete workflow: set, get, remove with multiple schemas", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaConfig = await putSchema(store, { @@ -1231,7 +1231,7 @@ describe("VariableStore - Integration Tests", () => { }); test("Upsert workflow preserves and updates tags", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object", @@ -1280,7 +1280,7 @@ describe("VariableStore - Legacy Update Method", () => { }); test("update() is distinct from set() and fails when variable doesn't exist", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object" }); const dataHash = await store.put(schemaHash, {}); @@ -1305,7 +1305,7 @@ describe("VariableStore - Legacy Update Method", () => { }); test("update() throws SchemaMismatchError when schema changes", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = await putSchema(store, { type: "object" }); const schemaB = await putSchema(store, { type: "string" }); @@ -1338,7 +1338,7 @@ describe("VariableStore - List Operation", () => { }); test("list() returns all variables", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object" }); const data1 = await store.put(schemaHash, { a: 1 }); @@ -1362,7 +1362,7 @@ describe("VariableStore - List Operation", () => { }); test("list() with namePrefix filters results", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object" }); const data = await store.put(schemaHash, {}); @@ -1397,7 +1397,7 @@ describe("VariableStore - list() with exactName", () => { test("list({ exactName }) returns all schema variants for name", async () => { // Given: Same name with multiple schemas - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = await putSchema(store, { type: "number" }); const schemaB = await putSchema(store, { type: "string" }); @@ -1429,7 +1429,7 @@ describe("VariableStore - list() with exactName", () => { }); test("list({ exactName }) returns empty array when name doesn't exist", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); dbPath = tmpDbPath(); @@ -1443,7 +1443,7 @@ describe("VariableStore - list() with exactName", () => { test("list({ exactName, schema }) filters to specific variant", async () => { // Given: Same name with two schemas - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = await putSchema(store, { type: "number" }); const schemaB = await putSchema(store, { type: "string" }); @@ -1470,7 +1470,7 @@ describe("VariableStore - list() with exactName", () => { }); test("list({ exactName }) with tags filters variants", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = await putSchema(store, { type: "number" }); const schemaB = await putSchema(store, { type: "string" }); @@ -1497,7 +1497,7 @@ describe("VariableStore - list() with exactName", () => { }); test("exactName and namePrefix are mutually exclusive", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); dbPath = tmpDbPath(); @@ -1512,7 +1512,7 @@ describe("VariableStore - list() with exactName", () => { }); test("list({ namePrefix }) does match partial exact names", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = await putSchema(store, { type: "number" }); const value = await store.put(schema, 42); @@ -1542,7 +1542,7 @@ describe("VariableStore - list() with exactName", () => { // This test demonstrates that list({ exactName }) provides // the functionality previously available via get(name) → Variable[] - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = await putSchema(store, { type: "number" }); const schemaB = await putSchema(store, { type: "string" }); @@ -1579,7 +1579,7 @@ describe("VariableStore - Tag/Label Management", () => { }); test("tag() adds tags to existing variable", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object" }); const dataHash = await store.put(schemaHash, {}); @@ -1599,7 +1599,7 @@ describe("VariableStore - Tag/Label Management", () => { }); test("tag() throws error for conflicting tag/label names", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object" }); const dataHash = await store.put(schemaHash, {}); @@ -1638,7 +1638,7 @@ describe("VariableStore - @ Prefix Variable Names", () => { }); test("should accept variable name with @ prefix in first segment", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "string" }); const hash = await store.put(schemaHash, "test value"); @@ -1659,7 +1659,7 @@ describe("VariableStore - @ Prefix Variable Names", () => { }); test("should accept variable name starting with @", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "string" }); const hash = await store.put(schemaHash, "config value"); @@ -1677,7 +1677,7 @@ describe("VariableStore - @ Prefix Variable Names", () => { }); test("should accept complex @ prefix paths", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "string" }); const hash = await store.put(schemaHash, "test"); @@ -1704,7 +1704,7 @@ describe("VariableStore - @ Prefix Variable Names", () => { }); test("should reject @ in non-first segment", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "string" }); const hash = await store.put(schemaHash, "test"); @@ -1727,7 +1727,7 @@ describe("VariableStore - @ Prefix Variable Names", () => { }); test("should reject @ followed by invalid characters", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "string" }); const hash = await store.put(schemaHash, "test"); @@ -1751,7 +1751,7 @@ describe("VariableStore - @ Prefix Variable Names", () => { }); test("should still accept all previously valid names", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "string" }); const hash = await store.put(schemaHash, "test"); @@ -1777,7 +1777,7 @@ describe("VariableStore - @ Prefix Variable Names", () => { }); test("should still reject previously invalid names", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "string" }); const hash = await store.put(schemaHash, "test"); @@ -1819,7 +1819,7 @@ describe("VariableStore - History (LRU)", () => { }); test("history() initializes with single entry on create", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = await putSchema(store, { type: "number" }); const v1 = await store.put(schema, 1); @@ -1834,7 +1834,7 @@ describe("VariableStore - History (LRU)", () => { }); test("history() pushes new values to position 0", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = await putSchema(store, { type: "number" }); const v1 = await store.put(schema, 1); @@ -1852,7 +1852,7 @@ describe("VariableStore - History (LRU)", () => { }); test("set() with same value as current is idempotent (no history change)", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = await putSchema(store, { type: "number" }); const v1 = await store.put(schema, 1); @@ -1870,7 +1870,7 @@ describe("VariableStore - History (LRU)", () => { }); test("setting an existing-history value moves it to position 0 (no duplicates)", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = await putSchema(store, { type: "number" }); const v1 = await store.put(schema, 1); @@ -1890,7 +1890,7 @@ describe("VariableStore - History (LRU)", () => { }); test("history is bounded by MAX_HISTORY=10", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = await putSchema(store, { type: "number" }); const values: string[] = []; @@ -1911,7 +1911,7 @@ describe("VariableStore - History (LRU)", () => { }); test("rollback semantics: re-setting an old value moves it to position 0", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = await putSchema(store, { type: "number" }); const v1 = await store.put(schema, 1); @@ -1933,7 +1933,7 @@ describe("VariableStore - History (LRU)", () => { }); test("history is cascade-deleted with the variable", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = await putSchema(store, { type: "number" }); const v1 = await store.put(schema, 1); diff --git a/packages/core/src/wrap-envelope.test.ts b/packages/core/src/wrap-envelope.test.ts index 5f60348..400a0e1 100644 --- a/packages/core/src/wrap-envelope.test.ts +++ b/packages/core/src/wrap-envelope.test.ts @@ -5,7 +5,7 @@ import { wrapEnvelope } from "./wrap-envelope.js"; describe("wrapEnvelope", () => { test("resolves @ocas/output/put alias and returns envelope", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const envelope = await wrapEnvelope( @@ -19,7 +19,7 @@ describe("wrapEnvelope", () => { }); test("resolves @ocas/output/has alias with boolean value", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const envelope = await wrapEnvelope(store, "@ocas/output/has", true); @@ -29,7 +29,7 @@ describe("wrapEnvelope", () => { }); test("resolves @ocas/output/gc alias with object value", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const gcStats = { total: 100, reachable: 80, collected: 20, scanned: 5 }; @@ -40,7 +40,7 @@ describe("wrapEnvelope", () => { }); test("resolves primitive alias @ocas/string", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const envelope = await wrapEnvelope(store, "@ocas/string", "hello"); @@ -50,7 +50,7 @@ describe("wrapEnvelope", () => { }); test("throws for unknown alias", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); await expect( @@ -59,7 +59,7 @@ describe("wrapEnvelope", () => { }); test("is idempotent — same alias returns same type hash", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const first = await wrapEnvelope(store, "@ocas/output/verify", "ok"); const second = await wrapEnvelope( @@ -74,7 +74,7 @@ describe("wrapEnvelope", () => { }); test("preserves complex object values without mutation", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const original = { -- 2.43.0 From 0f1058093708ba2e93db6e9a2e9feb6f7b286f6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Tue, 2 Jun 2026 06:54:09 +0000 Subject: [PATCH 2/2] test: update snapshots for Phase 2 changes --- packages/cli/tests/__snapshots__/edge-cases.test.ts.snap | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/cli/tests/__snapshots__/edge-cases.test.ts.snap b/packages/cli/tests/__snapshots__/edge-cases.test.ts.snap index 373127d..b9f8746 100644 --- a/packages/cli/tests/__snapshots__/edge-cases.test.ts.snap +++ b/packages/cli/tests/__snapshots__/edge-cases.test.ts.snap @@ -49,7 +49,9 @@ Flags: --resolution Initial resolution for render (default: 1.0) --decay Decay factor for render (default: 0.5) --epsilon Cutoff threshold for render (default: 0.01) - --pipe, -p Read from stdin (put/hash: raw JSON payload; render: { type, value } envelope)" + --pipe, -p Read from stdin (put/hash: raw JSON payload; render: { type, value } envelope) + +Agent: If you have not installed the ocas skill for this version (0.1.2), run \`ocas prompt setup\` and follow the instructions." `; exports[`Phase 3: Variable System 3.1 var set creates variable 1`] = ` -- 2.43.0