diff --git a/packages/json-cas-fs/src/store.test.ts b/packages/json-cas-fs/src/store.test.ts index 05b6176..7f157f7 100644 --- a/packages/json-cas-fs/src/store.test.ts +++ b/packages/json-cas-fs/src/store.test.ts @@ -65,7 +65,7 @@ describe("createFsStore – init and bootstrap", () => { const h2 = await bootstrap(store); expect(h1).toEqual(h2); - expect(store.listByType(h1["@schema"] ?? "")).toHaveLength(6); + expect(store.listByType(h1["@schema"] ?? "")).toHaveLength(24); }); }); diff --git a/packages/json-cas/src/bootstrap.test.ts b/packages/json-cas/src/bootstrap.test.ts index 9713fcf..7f328ce 100644 --- a/packages/json-cas/src/bootstrap.test.ts +++ b/packages/json-cas/src/bootstrap.test.ts @@ -1,18 +1,40 @@ import { describe, expect, test } from "bun:test"; import { bootstrap } from "./bootstrap.js"; +import type { JSONSchema } from "./schema.js"; import { getSchema } from "./schema.js"; import { createMemoryStore } from "./store.js"; +const OUTPUT_ALIASES = [ + "@output/put", + "@output/get", + "@output/has", + "@output/hash", + "@output/verify", + "@output/refs", + "@output/walk", + "@output/list", + "@output/var-set", + "@output/var-get", + "@output/var-delete", + "@output/var-tag", + "@output/var-list", + "@output/template-set", + "@output/template-get", + "@output/template-list", + "@output/template-delete", + "@output/gc", +] as const; + // ────────────────────────────────────────────────────────────────────────────── // Built-in Schema Registration Tests // ────────────────────────────────────────────────────────────────────────────── describe("bootstrap - Built-in Schemas", () => { - test("should return map of built-in schema aliases to hashes", async () => { + test("should return map of 24 built-in schema aliases to hashes", async () => { const store = createMemoryStore(); const builtinSchemas = await bootstrap(store); - // Should return object with 6 aliases + // Should return object with 6 primitive + 18 output aliases = 24 expect(builtinSchemas).toHaveProperty("@schema"); expect(builtinSchemas).toHaveProperty("@string"); expect(builtinSchemas).toHaveProperty("@number"); @@ -20,6 +42,12 @@ describe("bootstrap - Built-in Schemas", () => { expect(builtinSchemas).toHaveProperty("@array"); expect(builtinSchemas).toHaveProperty("@bool"); + for (const alias of OUTPUT_ALIASES) { + expect(builtinSchemas).toHaveProperty(alias); + } + + expect(Object.keys(builtinSchemas)).toHaveLength(24); + // All values should be valid hashes for (const [_alias, hash] of Object.entries(builtinSchemas)) { expect(typeof hash).toBe("string"); @@ -127,3 +155,164 @@ describe("bootstrap - Built-in Schemas", () => { } }); }); + +// ────────────────────────────────────────────────────────────────────────────── +// @output/* Schema Registration Tests +// ────────────────────────────────────────────────────────────────────────────── + +describe("bootstrap - @output/* Schemas", () => { + test("each @output/* schema has a title", async () => { + const store = createMemoryStore(); + const aliases = await bootstrap(store); + + for (const alias of OUTPUT_ALIASES) { + const hash = aliases[alias]; + if (!hash) throw new Error(`${alias} not found`); + + const schema = getSchema(store, hash) as JSONSchema; + expect(schema).not.toBeNull(); + expect(typeof schema.title).toBe("string"); + expect((schema.title as string).startsWith("ucas ")).toBe(true); + } + }); + + test("@output/put schema describes a cas_ref string", async () => { + const store = createMemoryStore(); + const aliases = await bootstrap(store); + const hash = aliases["@output/put"]; + if (!hash) throw new Error("@output/put not found"); + + const schema = getSchema(store, hash); + expect(schema).toEqual({ + type: "string", + format: "cas_ref", + title: "ucas put result", + }); + }); + + test("@output/get schema describes object with type, payload, timestamp", async () => { + const store = createMemoryStore(); + const aliases = await bootstrap(store); + const hash = aliases["@output/get"]; + if (!hash) throw new Error("@output/get not found"); + + const schema = getSchema(store, hash) as JSONSchema; + expect(schema.type).toBe("object"); + expect(schema.title).toBe("ucas get result"); + + const props = schema.properties as Record; + expect(props.type).toEqual({ type: "string", format: "cas_ref" }); + expect(props.payload).toEqual({}); + expect(props.timestamp).toEqual({ type: "number" }); + }); + + test("@output/has schema describes a boolean", async () => { + const store = createMemoryStore(); + const aliases = await bootstrap(store); + const hash = aliases["@output/has"]; + if (!hash) throw new Error("@output/has not found"); + + expect(getSchema(store, hash)).toEqual({ + type: "boolean", + title: "ucas has result", + }); + }); + + test("@output/verify schema describes enum of ok|corrupted|invalid", async () => { + const store = createMemoryStore(); + const aliases = await bootstrap(store); + const hash = aliases["@output/verify"]; + if (!hash) throw new Error("@output/verify not found"); + + const schema = getSchema(store, hash); + expect(schema).toEqual({ + type: "string", + enum: ["ok", "corrupted", "invalid"], + title: "ucas verify result", + }); + }); + + test("@output/refs schema describes array of cas_ref strings", async () => { + const store = createMemoryStore(); + const aliases = await bootstrap(store); + const hash = aliases["@output/refs"]; + if (!hash) throw new Error("@output/refs not found"); + + expect(getSchema(store, hash)).toEqual({ + type: "array", + items: { type: "string", format: "cas_ref" }, + title: "ucas refs result", + }); + }); + + test("@output/gc schema describes object with gc stats fields", async () => { + const store = createMemoryStore(); + const aliases = await bootstrap(store); + const hash = aliases["@output/gc"]; + if (!hash) throw new Error("@output/gc not found"); + + const schema = getSchema(store, hash) as JSONSchema; + expect(schema.type).toBe("object"); + expect(schema.title).toBe("ucas gc result"); + + const props = schema.properties as Record; + expect(props.total).toEqual({ type: "number" }); + expect(props.reachable).toEqual({ type: "number" }); + expect(props.collected).toEqual({ type: "number" }); + expect(props.scanned).toEqual({ type: "number" }); + }); + + test("@output/var-set schema describes a Variable object", async () => { + const store = createMemoryStore(); + const aliases = await bootstrap(store); + const hash = aliases["@output/var-set"]; + if (!hash) throw new Error("@output/var-set not found"); + + const schema = getSchema(store, hash) as JSONSchema; + expect(schema.type).toBe("object"); + expect(schema.title).toBe("ucas var set result"); + + const props = schema.properties as Record; + expect(props.name).toEqual({ type: "string" }); + expect(props.schema).toEqual({ type: "string", format: "cas_ref" }); + expect(props.value).toEqual({ type: "string", format: "cas_ref" }); + }); + + test("@output/var-list schema describes array of Variable objects", async () => { + const store = createMemoryStore(); + const aliases = await bootstrap(store); + const hash = aliases["@output/var-list"]; + if (!hash) throw new Error("@output/var-list not found"); + + const schema = getSchema(store, hash) as JSONSchema; + expect(schema.type).toBe("array"); + expect(schema.title).toBe("ucas var list result"); + + const items = schema.items as JSONSchema; + expect(items.type).toBe("object"); + const props = items.properties as Record; + expect(props.name).toEqual({ type: "string" }); + }); + + test("@output/template-delete schema describes object with deleted boolean", async () => { + const store = createMemoryStore(); + const aliases = await bootstrap(store); + const hash = aliases["@output/template-delete"]; + if (!hash) throw new Error("@output/template-delete not found"); + + expect(getSchema(store, hash)).toEqual({ + type: "object", + properties: { deleted: { type: "boolean" } }, + title: "ucas template delete result", + }); + }); + + test("all @output/* schemas are distinct hashes", async () => { + const store = createMemoryStore(); + const aliases = await bootstrap(store); + + const outputHashes = OUTPUT_ALIASES.map((alias) => aliases[alias]); + const uniqueHashes = new Set(outputHashes); + expect(uniqueHashes.size).toBe(OUTPUT_ALIASES.length); + }); +}); diff --git a/packages/json-cas/src/bootstrap.ts b/packages/json-cas/src/bootstrap.ts index dc205ec..8fbe5da 100644 --- a/packages/json-cas/src/bootstrap.ts +++ b/packages/json-cas/src/bootstrap.ts @@ -63,9 +63,168 @@ const BOOTSTRAP_PAYLOAD = { }, } as const; +const VARIABLE_PROPERTIES = { + name: { type: "string" }, + schema: { type: "string", format: "cas_ref" }, + value: { type: "string", format: "cas_ref" }, + created: { type: "number" }, + updated: { type: "number" }, + tags: { type: "object" }, + labels: { type: "array", items: { type: "string" } }, +} as const; + +const OUTPUT_SCHEMAS: ReadonlyArray< + readonly [alias: string, schema: Record] +> = [ + [ + "@output/put", + { type: "string", format: "cas_ref", title: "ucas put result" }, + ], + [ + "@output/get", + { + type: "object", + properties: { + type: { type: "string", format: "cas_ref" }, + payload: {}, + timestamp: { type: "number" }, + }, + title: "ucas get result", + }, + ], + ["@output/has", { type: "boolean", title: "ucas has result" }], + [ + "@output/hash", + { type: "string", format: "cas_ref", title: "ucas hash result" }, + ], + [ + "@output/verify", + { + type: "string", + enum: ["ok", "corrupted", "invalid"], + title: "ucas verify result", + }, + ], + [ + "@output/refs", + { + type: "array", + items: { type: "string", format: "cas_ref" }, + title: "ucas refs result", + }, + ], + [ + "@output/walk", + { + type: "array", + items: { type: "string" }, + title: "ucas walk result", + }, + ], + [ + "@output/list", + { + type: "array", + items: { type: "string", format: "cas_ref" }, + title: "ucas list result", + }, + ], + [ + "@output/var-set", + { + type: "object", + properties: { ...VARIABLE_PROPERTIES }, + title: "ucas var set result", + }, + ], + [ + "@output/var-get", + { + type: "object", + properties: { ...VARIABLE_PROPERTIES }, + title: "ucas var get result", + }, + ], + [ + "@output/var-delete", + { + type: "object", + properties: { ...VARIABLE_PROPERTIES }, + title: "ucas var delete result", + }, + ], + [ + "@output/var-tag", + { + type: "object", + properties: { ...VARIABLE_PROPERTIES }, + title: "ucas var tag result", + }, + ], + [ + "@output/var-list", + { + type: "array", + items: { type: "object", properties: { ...VARIABLE_PROPERTIES } }, + title: "ucas var list result", + }, + ], + [ + "@output/template-set", + { + type: "object", + properties: { + schemaHash: { type: "string", format: "cas_ref" }, + contentHash: { type: "string", format: "cas_ref" }, + }, + title: "ucas template set result", + }, + ], + [ + "@output/template-get", + { type: "string", title: "ucas template get result" }, + ], + [ + "@output/template-list", + { + type: "array", + items: { + type: "object", + properties: { + schemaHash: { type: "string", format: "cas_ref" }, + contentHash: { type: "string", format: "cas_ref" }, + }, + }, + title: "ucas template list result", + }, + ], + [ + "@output/template-delete", + { + type: "object", + properties: { deleted: { type: "boolean" } }, + title: "ucas template delete result", + }, + ], + [ + "@output/gc", + { + type: "object", + properties: { + total: { type: "number" }, + reachable: { type: "number" }, + collected: { type: "number" }, + scanned: { type: "number" }, + }, + title: "ucas gc result", + }, + ], +]; + /** * Write the meta-schema seed node into the store and register built-in schemas. - * The returned object contains aliases for the meta-schema and 5 primitive schemas. + * The returned object contains aliases for the meta-schema, 5 primitive schemas, + * and 18 @output/* schemas (24 total). * Idempotent: calling bootstrap multiple times returns the same hashes. */ export async function bootstrap(store: Store): Promise> { @@ -83,8 +242,8 @@ export async function bootstrap(store: Store): Promise> { const arrayHash = await store.put(metaHash, { type: "array" }); const boolHash = await store.put(metaHash, { type: "boolean" }); - // 3. Return map of aliases to hashes - return { + // 3. Register @output/* schemas + const aliases: Record = { "@schema": metaHash, "@string": stringHash, "@number": numberHash, @@ -92,4 +251,10 @@ export async function bootstrap(store: Store): Promise> { "@array": arrayHash, "@bool": boolHash, }; + + for (const [alias, schema] of OUTPUT_SCHEMAS) { + aliases[alias] = await store.put(metaHash, schema); + } + + return aliases; } diff --git a/packages/json-cas/src/index.test.ts b/packages/json-cas/src/index.test.ts index 45e42eb..573c800 100644 --- a/packages/json-cas/src/index.test.ts +++ b/packages/json-cas/src/index.test.ts @@ -264,7 +264,7 @@ describe("bootstrap", () => { ); }); - test("returns a map with 6 built-in schema aliases", async () => { + test("returns a map with 24 built-in schema aliases", async () => { const store = createMemoryStore(); const builtinSchemas = await bootstrap(store); @@ -280,6 +280,8 @@ describe("bootstrap", () => { expect(hash).toHaveLength(13); expect(hash).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); } + + expect(Object.keys(builtinSchemas)).toHaveLength(24); }); test("meta-schema node is stored and retrievable", async () => { @@ -316,7 +318,7 @@ describe("bootstrap", () => { const h2 = await bootstrap(store); expect(h1).toEqual(h2); - // All 6 built-in schemas should be typed by the meta-schema - expect(store.listByType(h1["@schema"] ?? "")).toHaveLength(6); + // All 24 built-in schemas should be typed by the meta-schema + expect(store.listByType(h1["@schema"] ?? "")).toHaveLength(24); }); }); diff --git a/packages/json-cas/src/index.ts b/packages/json-cas/src/index.ts index 8b69e8d..40f87b6 100644 --- a/packages/json-cas/src/index.ts +++ b/packages/json-cas/src/index.ts @@ -5,6 +5,7 @@ export { cborEncode } from "./cbor.js"; export { type GcStats, gc } from "./gc.js"; export { computeHash, computeSelfHash } from "./hash.js"; export { renderWithTemplate } from "./liquid-render.js"; +export { registerOutputTemplates } from "./output-templates.js"; export { type RenderOptions, render, @@ -34,3 +35,4 @@ export { VariableStore, } from "./variable-store.js"; export { verify } from "./verify.js"; +export { wrapEnvelope } from "./wrap-envelope.js"; diff --git a/packages/json-cas/src/output-templates.test.ts b/packages/json-cas/src/output-templates.test.ts new file mode 100644 index 0000000..c1dbd48 --- /dev/null +++ b/packages/json-cas/src/output-templates.test.ts @@ -0,0 +1,117 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { bootstrap } from "./bootstrap.js"; +import { registerOutputTemplates } from "./output-templates.js"; +import { createMemoryStore } from "./store.js"; +import type { Store } from "./types.js"; +import type { VariableStore } from "./variable-store.js"; +import { createVariableStore } from "./variable-store.js"; + +const OUTPUT_ALIASES = [ + "@output/put", + "@output/get", + "@output/has", + "@output/hash", + "@output/verify", + "@output/refs", + "@output/walk", + "@output/list", + "@output/var-set", + "@output/var-get", + "@output/var-delete", + "@output/var-tag", + "@output/var-list", + "@output/template-set", + "@output/template-get", + "@output/template-list", + "@output/template-delete", + "@output/gc", +] as const; + +describe("registerOutputTemplates", () => { + let store: Store; + let varStore: VariableStore; + let tempDir: string; + + afterEach(async () => { + varStore.close(); + await rm(tempDir, { recursive: true }); + }); + + test("registers a template for every @output/* schema", async () => { + tempDir = await mkdtemp(join(tmpdir(), "json-cas-tmpl-")); + store = createMemoryStore(); + await bootstrap(store); + varStore = createVariableStore(join(tempDir, "vars.db"), store); + + const registered = await registerOutputTemplates(store, varStore); + + expect(Object.keys(registered)).toHaveLength(18); + + for (const alias of OUTPUT_ALIASES) { + expect(registered).toHaveProperty(alias); + } + }); + + test("each template is retrievable via @ucas/template/text/", async () => { + tempDir = await mkdtemp(join(tmpdir(), "json-cas-tmpl-")); + store = createMemoryStore(); + const aliases = await bootstrap(store); + varStore = createVariableStore(join(tempDir, "vars.db"), store); + + await registerOutputTemplates(store, varStore); + + const stringHash = aliases["@string"]; + if (!stringHash) throw new Error("@string not found"); + + for (const alias of OUTPUT_ALIASES) { + const schemaHash = aliases[alias]; + if (!schemaHash) throw new Error(`${alias} not found`); + + const varName = `@ucas/template/text/${schemaHash}`; + const variable = varStore.get(varName, stringHash); + if (variable === null) throw new Error(`Variable ${varName} not found`); + + const templateNode = store.get(variable.value); + if (templateNode === null) + throw new Error(`Template node ${variable.value} not found`); + expect(typeof templateNode.payload).toBe("string"); + } + }); + + test("is idempotent — safe to call multiple times", async () => { + tempDir = await mkdtemp(join(tmpdir(), "json-cas-tmpl-")); + store = createMemoryStore(); + await bootstrap(store); + varStore = createVariableStore(join(tempDir, "vars.db"), store); + + const first = await registerOutputTemplates(store, varStore); + const second = await registerOutputTemplates(store, varStore); + + expect(first).toEqual(second); + }); + + test("@output/put template contains payload reference", async () => { + tempDir = await mkdtemp(join(tmpdir(), "json-cas-tmpl-")); + store = createMemoryStore(); + const aliases = await bootstrap(store); + varStore = createVariableStore(join(tempDir, "vars.db"), store); + + await registerOutputTemplates(store, varStore); + + const putHash = aliases["@output/put"]; + if (!putHash) throw new Error("@output/put not found"); + const stringHash = aliases["@string"]; + if (!stringHash) throw new Error("@string not found"); + + const variable = varStore.get(`@ucas/template/text/${putHash}`, stringHash); + if (variable === null) + throw new Error("@output/put template variable not found"); + + const templateNode = store.get(variable.value); + if (templateNode === null) throw new Error("Template node not found"); + expect(templateNode.payload).toBe("{{ payload }}"); + }); +}); diff --git a/packages/json-cas/src/output-templates.ts b/packages/json-cas/src/output-templates.ts new file mode 100644 index 0000000..e0f53e1 --- /dev/null +++ b/packages/json-cas/src/output-templates.ts @@ -0,0 +1,87 @@ +import { bootstrap } from "./bootstrap.js"; +import type { Hash, Store } from "./types.js"; +import type { VariableStore } from "./variable-store.js"; + +const DEFAULT_TEMPLATES: ReadonlyArray< + readonly [alias: string, template: string] +> = [ + ["@output/put", "{{ payload }}"], + [ + "@output/get", + "type: {{ payload.type }}\ntimestamp: {{ payload.timestamp }}", + ], + ["@output/has", "{{ payload }}"], + ["@output/hash", "{{ payload }}"], + ["@output/verify", "{{ payload }}"], + ["@output/refs", "{% for ref in payload %}{{ ref }}\n{% endfor %}"], + ["@output/walk", "{% for item in payload %}{{ item }}\n{% endfor %}"], + ["@output/list", "{% for item in payload %}{{ item }}\n{% endfor %}"], + [ + "@output/var-set", + "name: {{ payload.name }}\nschema: {{ payload.schema }}\nvalue: {{ payload.value }}", + ], + [ + "@output/var-get", + "name: {{ payload.name }}\nschema: {{ payload.schema }}\nvalue: {{ payload.value }}", + ], + [ + "@output/var-delete", + "name: {{ payload.name }}\nschema: {{ payload.schema }}\nvalue: {{ payload.value }}", + ], + [ + "@output/var-tag", + "name: {{ payload.name }}\nschema: {{ payload.schema }}\nvalue: {{ payload.value }}", + ], + [ + "@output/var-list", + "{% for v in payload %}name: {{ v.name }}\nschema: {{ v.schema }}\nvalue: {{ v.value }}\n{% endfor %}", + ], + [ + "@output/template-set", + "schemaHash: {{ payload.schemaHash }}\ncontentHash: {{ payload.contentHash }}", + ], + ["@output/template-get", "{{ payload }}"], + [ + "@output/template-list", + "{% for t in payload %}schemaHash: {{ t.schemaHash }}\ncontentHash: {{ t.contentHash }}\n{% endfor %}", + ], + ["@output/template-delete", "deleted: {{ payload.deleted }}"], + [ + "@output/gc", + "total: {{ payload.total }}\nreachable: {{ payload.reachable }}\ncollected: {{ payload.collected }}\nscanned: {{ payload.scanned }}", + ], +]; + +/** + * Register default LiquidJS templates for all @output/* schemas. + * Each template is stored as a @string CAS node and bound to + * the variable `@ucas/template/text/`. + * + * Idempotent: safe to call multiple times. + */ +export async function registerOutputTemplates( + store: Store, + varStore: VariableStore, +): Promise> { + const aliases = await bootstrap(store); + const stringHash = aliases["@string"]; + if (stringHash === undefined) { + throw new Error("@string schema not found in bootstrap result"); + } + + const registered: Record = {}; + + for (const [alias, template] of DEFAULT_TEMPLATES) { + const schemaHash = aliases[alias]; + if (schemaHash === undefined) { + throw new Error(`Schema alias not found: ${alias}`); + } + + const contentHash = await store.put(stringHash, template); + const varName = `@ucas/template/text/${schemaHash}`; + varStore.set(varName, contentHash); + registered[alias] = contentHash; + } + + return registered; +} diff --git a/packages/json-cas/src/wrap-envelope.test.ts b/packages/json-cas/src/wrap-envelope.test.ts new file mode 100644 index 0000000..3062979 --- /dev/null +++ b/packages/json-cas/src/wrap-envelope.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, test } from "bun:test"; +import { bootstrap } from "./bootstrap.js"; +import { createMemoryStore } from "./store.js"; +import { wrapEnvelope } from "./wrap-envelope.js"; + +describe("wrapEnvelope", () => { + test("resolves @output/put alias and returns envelope", async () => { + const store = createMemoryStore(); + const aliases = await bootstrap(store); + + const envelope = await wrapEnvelope(store, "@output/put", "AAAAAAAAAAAAA"); + + expect(envelope.type).toBe(aliases["@output/put"]); + expect(envelope.value).toBe("AAAAAAAAAAAAA"); + }); + + test("resolves @output/has alias with boolean value", async () => { + const store = createMemoryStore(); + const aliases = await bootstrap(store); + + const envelope = await wrapEnvelope(store, "@output/has", true); + + expect(envelope.type).toBe(aliases["@output/has"]); + expect(envelope.value).toBe(true); + }); + + test("resolves @output/gc alias with object value", async () => { + const store = createMemoryStore(); + const aliases = await bootstrap(store); + + const gcStats = { total: 100, reachable: 80, collected: 20, scanned: 5 }; + const envelope = await wrapEnvelope(store, "@output/gc", gcStats); + + expect(envelope.type).toBe(aliases["@output/gc"]); + expect(envelope.value).toEqual(gcStats); + }); + + test("resolves primitive alias @string", async () => { + const store = createMemoryStore(); + const aliases = await bootstrap(store); + + const envelope = await wrapEnvelope(store, "@string", "hello"); + + expect(envelope.type).toBe(aliases["@string"]); + expect(envelope.value).toBe("hello"); + }); + + test("throws for unknown alias", async () => { + const store = createMemoryStore(); + await bootstrap(store); + + await expect( + wrapEnvelope(store, "@output/nonexistent", "value"), + ).rejects.toThrow("Unknown schema alias: @output/nonexistent"); + }); + + test("is idempotent — same alias returns same type hash", async () => { + const store = createMemoryStore(); + + const first = await wrapEnvelope(store, "@output/verify", "ok"); + const second = await wrapEnvelope(store, "@output/verify", "corrupted"); + + expect(first.type).toBe(second.type); + expect(first.value).toBe("ok"); + expect(second.value).toBe("corrupted"); + }); + + test("preserves complex object values without mutation", async () => { + const store = createMemoryStore(); + await bootstrap(store); + + const original = { + name: "test", + schema: "AAAAAAAAAAAAA", + value: "BBBBBBBBBBBBB", + created: 1000, + updated: 2000, + tags: { env: "prod" }, + labels: ["stable"], + }; + const envelope = await wrapEnvelope(store, "@output/var-set", original); + + expect(envelope.value).toEqual(original); + }); +}); diff --git a/packages/json-cas/src/wrap-envelope.ts b/packages/json-cas/src/wrap-envelope.ts new file mode 100644 index 0000000..b2cedc4 --- /dev/null +++ b/packages/json-cas/src/wrap-envelope.ts @@ -0,0 +1,19 @@ +import { bootstrap } from "./bootstrap.js"; +import type { Hash, Store } from "./types.js"; + +/** + * Resolve a schema alias (e.g. "@output/put") to its hash via bootstrap, + * then return a typed envelope ready for store.put() or direct rendering. + */ +export async function wrapEnvelope( + store: Store, + schemaAlias: string, + value: unknown, +): Promise<{ type: Hash; value: unknown }> { + const aliases = await bootstrap(store); + const typeHash = aliases[schemaAlias]; + if (typeHash === undefined) { + throw new Error(`Unknown schema alias: ${schemaAlias}`); + } + return { type: typeHash, value }; +}