Compare commits

...

6 Commits

Author SHA1 Message Date
xiaoju 7e23d911a4 feat: implement render engine with resolution decay (#39)
Implement Phase 3: render core engine with resolution-based decay and
default YAML rendering.

Core Features:
- Resolution decay model: child nodes receive resolution = parent × decay
- Epsilon threshold: nodes with resolution ≤ epsilon render as cas:<hash>
- Default YAML output format with 2-space indentation
- Cycle detection via visited set
- Floating-point tolerance for epsilon comparisons

Implementation:
- packages/json-cas/src/render.ts: Core render function
- packages/json-cas/src/render.test.ts: 38 comprehensive tests
- packages/cli-json-cas: ucas render command with --resolution, --decay, --epsilon flags
- CLI integration tests for render command

Tests: All 276 tests pass (38 new render tests, 3 CLI tests)
Build: Clean compilation with tsc
Lint: Passes biome check

Fixes #39

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-31 04:50:55 +00:00
xiaoju 301b05c212 Merge pull request 'feat: add built-in schema aliases with @ prefix support' (#42) from fix/37-builtin-schema-aliases into main 2026-05-31 04:45:20 +00:00
xiaoju 22fce0ac66 feat: add built-in schema aliases with @ prefix support
Implements Phase 1 of issue #37:
- Extended variable name validation to allow @ prefix (system-reserved)
- Registered 6 built-in schemas with @ aliases during bootstrap
  - @schema → meta-schema (self-referential)
  - @string → { type: "string" }
  - @number → { type: "number" }
  - @object → { type: "object" }
  - @array → { type: "array" }
  - @bool → { type: "boolean" }
- Bootstrap now returns Record<string, Hash> instead of Hash
- Added CLI @ alias resolution for all commands accepting type-hash
  - ucas schema get @string
  - ucas put @string <file>
  - ucas hash @string <file>
- Added comprehensive test coverage for all features
  - Variable name validation with @ prefix
  - Built-in schema registration
  - CLI alias resolution
  - Integration tests

Fixes #37

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-31 04:18:46 +00:00
xiaoju fddbb1549e feat: RFC-31 Phase 3 — CLI var 子命令重写
- var set <name> <hash> (upsert, replaces create+update)
- var get <name> --schema <hash> (schema required)
- var delete <name> [--schema <hash>] (optional schema)
- var list [prefix] (replaces --scope)
- var tag <name> --schema <hash> ...
- 41 CLI tests, all passing

Fixes #34
Ref #31
2026-05-30 15:44:19 +00:00
xiaoju 109aaab9b8 feat: RFC-31 Phase 3 — rewrite CLI var subcommands for composite key model
Migrate CLI var subcommands from ULID ID model to (name, schema) composite key model.

- Replace var create/update with unified var set (upsert semantics)
- Update var get to require --schema parameter for precise query
- Enhance var delete with batch (no --schema) and precise (with --schema) modes
- Refactor var list to use positional prefix parameter
- Update var tag to target composite keys
- Add comprehensive test suite (41 tests, 100% coverage)
- Update Variable schema: remove id/scope, add name field

Fixes #34

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-30 14:29:33 +00:00
xiaoju 906a6dfd1c feat: RFC-31 Phase 1+2 — Variable model refactor with (name, schema) composite key
- Replace ULID id + scope with qualified name + schema composite PK
- Add set() upsert, remove() with optional schema, validateName()
- get(name, schema) with fixed return type (no polymorphic)
- Tags/labels adapted to composite foreign keys
- GC compatible

Fixes #32
Ref #31
2026-05-30 13:36:51 +00:00
16 changed files with 3072 additions and 146 deletions
+1
View File
@@ -2,3 +2,4 @@ node_modules/
dist/
*.d.ts.map
*.tsbuildinfo
.worktrees/
+274 -3
View File
@@ -1,7 +1,27 @@
import { describe, expect, test } from "bun:test";
import { resolve } from "node:path";
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
const pkgPath = resolve(import.meta.dir, "../package.json");
const entrypoint = resolve(import.meta.dir, "index.ts");
async function runCli(
args: string[],
storePath?: string,
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
const finalArgs = storePath
? ["bun", entrypoint, "--store", storePath, ...args]
: ["bun", entrypoint, ...args];
const proc = Bun.spawn(finalArgs, {
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await proc.exited;
const stdout = await new Response(proc.stdout).text();
const stderr = await new Response(proc.stderr).text();
return { stdout, stderr, exitCode };
}
describe("ucas command alias", () => {
test("T1: ucas bin entry exists in package.json", async () => {
@@ -15,7 +35,6 @@ describe("ucas command alias", () => {
});
test("T3: ucas command is executable and shows help", async () => {
const entrypoint = resolve(import.meta.dir, "index.ts");
const proc = Bun.spawn(["bun", entrypoint, "--help"], {
stdout: "pipe",
stderr: "pipe",
@@ -31,3 +50,255 @@ describe("ucas command alias", () => {
expect(pkg.bin.ucas).toBe(pkg.bin["json-cas"]);
});
});
// ---- @ Alias Resolution Tests ----
let testDir: string;
let storePath: string;
let cliPath: string;
beforeEach(() => {
// Create unique temp directory for each test
testDir = join(
tmpdir(),
`json-cas-cli-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
);
storePath = join(testDir, "store");
cliPath = join(import.meta.dir, "index.ts");
mkdirSync(testDir, { recursive: true });
mkdirSync(storePath, { recursive: true });
});
afterEach(() => {
// Clean up test directory
try {
rmSync(testDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
});
/**
* Run CLI command and return stdout, stderr, and exit code
*/
async function runCliAlias(...args: string[]): Promise<{
stdout: string;
stderr: string;
exitCode: number;
}> {
const proc = Bun.spawn(
["bun", "run", cliPath, "--store", storePath, ...args],
{
stdout: "pipe",
stderr: "pipe",
},
);
const [stdout, stderr] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
]);
await proc.exited;
return {
stdout: stdout.trim(),
stderr: stderr.trim(),
exitCode: proc.exitCode ?? 0,
};
}
describe("@ Alias Resolution - schema get", () => {
test("ucas schema get @string should work", async () => {
await runCliAlias("init"); // Initialize store
const { stdout, stderr, exitCode } = await runCliAlias(
"schema",
"get",
"@string",
);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
const schema = JSON.parse(stdout);
expect(schema).toEqual({ type: "string" });
});
test("ucas schema get @number should work", async () => {
await runCliAlias("init");
const { stdout, exitCode } = await runCliAlias("schema", "get", "@number");
expect(exitCode).toBe(0);
const schema = JSON.parse(stdout);
expect(schema).toEqual({ type: "number" });
});
test("ucas schema get @object should work", async () => {
await runCliAlias("init");
const { stdout, exitCode } = await runCliAlias("schema", "get", "@object");
expect(exitCode).toBe(0);
const schema = JSON.parse(stdout);
expect(schema).toEqual({ type: "object" });
});
test("ucas schema get @array should work", async () => {
await runCliAlias("init");
const { stdout, exitCode } = await runCliAlias("schema", "get", "@array");
expect(exitCode).toBe(0);
const schema = JSON.parse(stdout);
expect(schema).toEqual({ type: "array" });
});
test("ucas schema get @bool should work", async () => {
await runCliAlias("init");
const { stdout, exitCode } = await runCliAlias("schema", "get", "@bool");
expect(exitCode).toBe(0);
const schema = JSON.parse(stdout);
expect(schema).toEqual({ type: "boolean" });
});
test("ucas schema get @schema should work", async () => {
await runCliAlias("init");
const { stdout, exitCode } = await runCliAlias("schema", "get", "@schema");
expect(exitCode).toBe(0);
const schema = JSON.parse(stdout);
expect(schema).toHaveProperty("type", "object");
expect(schema).toHaveProperty(
"description",
"json-cas JSON Schema meta-schema",
);
});
test("ucas schema get @invalid should fail gracefully", async () => {
await runCliAlias("init");
const { stderr, exitCode } = await runCliAlias("schema", "get", "@invalid");
expect(exitCode).not.toBe(0);
expect(stderr).toContain("Schema not found");
});
});
describe("@ Alias Resolution - put", () => {
test("ucas put @string <file> should resolve alias", async () => {
await runCliAlias("init");
const payloadFile = join(testDir, "payload.json");
writeFileSync(payloadFile, JSON.stringify("hello world"));
const { stdout, stderr, exitCode } = await runCliAlias(
"put",
"@string",
payloadFile,
);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
// Should output a valid hash (13 chars)
expect(stdout).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
});
test("ucas put @number <file> should resolve alias", async () => {
await runCliAlias("init");
const payloadFile = join(testDir, "payload.json");
writeFileSync(payloadFile, "42");
const { stdout, exitCode } = await runCliAlias("put", "@number", payloadFile);
expect(exitCode).toBe(0);
expect(stdout).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
});
test("ucas put @object <file> should resolve alias", async () => {
await runCliAlias("init");
const payloadFile = join(testDir, "payload.json");
writeFileSync(payloadFile, JSON.stringify({ foo: "bar" }));
const { stdout, exitCode } = await runCliAlias("put", "@object", payloadFile);
expect(exitCode).toBe(0);
expect(stdout).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
});
test("ucas put @invalid <file> should fail", async () => {
await runCliAlias("init");
const payloadFile = join(testDir, "payload.json");
writeFileSync(payloadFile, "{}");
const { stderr, exitCode } = await runCliAlias("put", "@invalid", payloadFile);
expect(exitCode).not.toBe(0);
expect(stderr.length).toBeGreaterThan(0);
});
});
describe("@ Alias Resolution - hash", () => {
test("ucas hash @string <file> should compute hash without storing", async () => {
await runCliAlias("init");
const payloadFile = join(testDir, "payload.json");
writeFileSync(payloadFile, JSON.stringify("test"));
const { stdout, stderr, exitCode } = await runCliAlias(
"hash",
"@string",
payloadFile,
);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
expect(stdout).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
});
});
describe("ucas render command", () => {
test("R1: render requires hash argument", async () => {
const { exitCode, stderr } = await runCli(["render"]);
expect(exitCode).not.toBe(0);
expect(stderr).toContain("Usage");
});
test("R2: render with missing hash shows error", async () => {
const tmpStore = mkdtempSync(join(tmpdir(), "json-cas-test-"));
try {
await runCli(["init"], tmpStore);
const { exitCode, stdout } = await runCli(
["render", "ZZZZZZZZZZZZZ"],
tmpStore,
);
// Missing hash renders as cas: reference
expect(exitCode).toBe(0);
expect(stdout).toContain("cas:ZZZZZZZZZZZZZ");
} finally {
rmSync(tmpStore, { recursive: true, force: true });
}
});
test("R3: render with invalid numeric flag fails", async () => {
const tmpStore = mkdtempSync(join(tmpdir(), "json-cas-test-"));
try {
await runCli(["init"], tmpStore);
const { exitCode, stderr } = await runCli(
["render", "AAAAAAAAAAAAA", "--resolution", "invalid"],
tmpStore,
);
expect(exitCode).not.toBe(0);
expect(stderr).toContain("valid number");
} finally {
rmSync(tmpStore, { recursive: true, force: true });
}
});
});
+175 -94
View File
@@ -11,11 +11,11 @@ import {
createVariableStore,
gc,
getSchema,
InvalidScopeError,
InvalidTagFormatError,
InvalidVariableNameError,
putSchema,
refs,
SchemaMismatchError,
render,
TagLabelConflictError,
VariableNotFoundError,
validate,
@@ -32,10 +32,12 @@ type Flags = Record<string, string | boolean | string[]>;
const VALUE_FLAGS = new Set([
"store",
"format",
"scope",
"value",
"var-db",
"tag",
"schema",
"resolution",
"decay",
"epsilon",
]);
function parseArgs(argv: string[]): { flags: Flags; positional: string[] } {
@@ -117,6 +119,24 @@ function openVarStore(): VariableStore {
return createVariableStore(resolve(varDbPath), store);
}
/**
* Resolve a type-hash, handling @ aliases
* If the input starts with @, resolve it via bootstrap
* Otherwise, return the hash as-is
*/
async function resolveTypeHash(typeHashOrAlias: string): Promise<Hash> {
if (typeHashOrAlias.startsWith("@")) {
const store = openStore();
const builtinSchemas = await bootstrap(store);
const resolvedHash = builtinSchemas[typeHashOrAlias];
if (!resolvedHash) {
die(`Schema not found: ${typeHashOrAlias}`);
}
return resolvedHash;
}
return typeHashOrAlias;
}
/**
* Get the Variable schema's CAS hash
* This is the type hash used in JSON envelopes
@@ -124,25 +144,23 @@ function openVarStore(): VariableStore {
async function getVariableSchemaHash(): Promise<Hash> {
const store = openStore();
// Define the Variable JSON Schema (simple version for envelope)
// Define the Variable JSON Schema (updated for new model with composite key)
const variableSchema: JSONSchema = {
title: "Variable",
type: "object",
properties: {
id: { type: "string" },
scope: { type: "string" },
value: { type: "string" },
name: { type: "string" },
schema: { type: "string" },
value: { type: "string" },
created: { type: "number" },
updated: { type: "number" },
tags: { type: "object" },
labels: { type: "array", items: { type: "string" } },
},
required: [
"id",
"scope",
"value",
"name",
"schema",
"value",
"created",
"updated",
"tags",
@@ -206,14 +224,16 @@ async function cmdInit(): Promise<void> {
const dir = resolve(storePath);
mkdirSync(dir, { recursive: true });
const store = createFsStore(dir);
const hash = await bootstrap(store);
console.log(hash);
const builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@schema"];
console.log(metaHash);
}
async function cmdBootstrap(): Promise<void> {
const store = openStore();
const hash = await bootstrap(store);
console.log(hash);
const builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@schema"];
console.log(metaHash);
}
async function cmdSchemaPut(args: string[]): Promise<void> {
@@ -226,17 +246,20 @@ async function cmdSchemaPut(args: string[]): Promise<void> {
}
async function cmdSchemaGet(args: string[]): Promise<void> {
const hash = args[0];
if (!hash) die("Usage: json-cas schema get <type-hash>");
const hashOrAlias = args[0];
if (!hashOrAlias) die("Usage: json-cas schema get <type-hash>");
const hash = await resolveTypeHash(hashOrAlias);
const store = openStore();
const schema = getSchema(store, hash);
if (schema === null) die(`Schema not found: ${hash}`);
if (schema === null) die(`Schema not found: ${hashOrAlias}`);
out(schema);
}
async function cmdSchemaList(): Promise<void> {
const store = openStore();
const metaHash = await bootstrap(store);
const builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@schema"];
if (!metaHash) throw new Error("Meta-schema not found");
for (const hash of store.listByType(metaHash)) {
if (hash === metaHash) continue;
const node = store.get(hash);
@@ -262,9 +285,11 @@ async function cmdSchemaValidate(args: string[]): Promise<void> {
}
async function cmdPut(args: string[]): Promise<void> {
const typeHash = args[0];
const typeHashOrAlias = args[0];
const file = args[1];
if (!typeHash || !file) die("Usage: json-cas put <type-hash> <file.json>");
if (!typeHashOrAlias || !file)
die("Usage: json-cas put <type-hash> <file.json>");
const typeHash = await resolveTypeHash(typeHashOrAlias);
const payload = readJsonFile(file);
const store = openStore();
const hash = await store.put(typeHash, payload);
@@ -349,14 +374,63 @@ async function cmdWalk(args: string[]): Promise<void> {
}
async function cmdHash(args: string[]): Promise<void> {
const typeHash = args[0];
const typeHashOrAlias = args[0];
const file = args[1];
if (!typeHash || !file) die("Usage: json-cas hash <type-hash> <file.json>");
if (!typeHashOrAlias || !file)
die("Usage: json-cas hash <type-hash> <file.json>");
const typeHash = await resolveTypeHash(typeHashOrAlias);
const payload = readJsonFile(file);
const hash = await computeHash(typeHash, payload);
console.log(hash);
}
async function cmdRender(args: string[]): Promise<void> {
const hash = args[0];
if (!hash) {
die(
"Usage: ucas render <hash> [--resolution <n>] [--decay <n>] [--epsilon <n>]",
);
}
const store = openStore();
// Parse numeric options
const resolution =
typeof flags.resolution === "string"
? Number.parseFloat(flags.resolution)
: undefined;
const decay =
typeof flags.decay === "string"
? Number.parseFloat(flags.decay)
: undefined;
const epsilon =
typeof flags.epsilon === "string"
? Number.parseFloat(flags.epsilon)
: undefined;
// Validate numeric values
if (resolution !== undefined && Number.isNaN(resolution)) {
die("--resolution must be a valid number");
}
if (decay !== undefined && Number.isNaN(decay)) {
die("--decay must be a valid number");
}
if (epsilon !== undefined && Number.isNaN(epsilon)) {
die("--epsilon must be a valid number");
}
try {
const output = render(store, hash, { resolution, decay, epsilon });
// Output to stdout without JSON wrapping (raw YAML)
process.stdout.write(output);
} catch (error) {
if (error instanceof Error) {
die(error.message);
}
die(String(error));
}
}
async function cmdCat(args: string[]): Promise<void> {
const hash = args[0];
if (!hash) die("Usage: json-cas cat <hash>");
@@ -370,13 +444,14 @@ async function cmdCat(args: string[]): Promise<void> {
}
}
async function cmdVarCreate(_args: string[]): Promise<void> {
const scope = flags.scope as string | undefined;
const value = flags.value as string | undefined;
async function cmdVarSet(args: string[]): Promise<void> {
const name = args[0];
const value = args[1];
const tagFlags = flags.tag;
if (!scope) die("Usage: json-cas var create --scope <scope> --value <hash>");
if (!value) die("Usage: json-cas var create --scope <scope> --value <hash>");
if (!name || !value) {
die("Usage: json-cas var set <name> <hash> [--tag <tag>...]");
}
const varStore = openVarStore();
@@ -391,18 +466,25 @@ async function cmdVarCreate(_args: string[]): Promise<void> {
// Check for conflicts in initial tags/labels
if (deleteNames.length > 0) {
die("Error: Cannot use deletion syntax (:name) in var create");
die("Error: Cannot use deletion syntax (:name) in var set");
}
const variable = varStore.create(scope, value, {
tags: Object.keys(tags).length > 0 ? tags : undefined,
labels: labels.length > 0 ? labels : undefined,
});
// If --tag flags are provided at all, always pass options to replace tags/labels
// If no --tag flags, pass undefined to preserve existing tags/labels
const options =
tagArgs.length > 0
? {
tags: Object.keys(tags).length > 0 ? tags : {},
labels: labels.length > 0 ? labels : [],
}
: undefined;
const variable = varStore.set(name, value, options);
const envelope = await wrapVariableEnvelope(variable);
out(envelope);
} catch (e) {
if (
e instanceof InvalidScopeError ||
e instanceof InvalidVariableNameError ||
e instanceof CasNodeNotFoundError ||
e instanceof TagLabelConflictError
) {
@@ -415,15 +497,19 @@ async function cmdVarCreate(_args: string[]): Promise<void> {
}
async function cmdVarGet(args: string[]): Promise<void> {
const id = args[0];
if (!id) die("Usage: json-cas var get <id>");
const name = args[0];
const schema = flags.schema as string | undefined;
if (!name || !schema) {
die("Usage: json-cas var get <name> --schema <hash>");
}
const varStore = openVarStore();
try {
const variable = varStore.get(id);
const variable = varStore.get(name, schema);
if (variable === null) {
die(`Error: Variable not found: ${id}`);
die(`Error: Variable not found: name=${name}, schema=${schema}`);
}
const envelope = await wrapVariableEnvelope(variable);
out(envelope);
@@ -432,44 +518,28 @@ async function cmdVarGet(args: string[]): Promise<void> {
}
}
async function cmdVarUpdate(args: string[]): Promise<void> {
const id = args[0];
const value = args[1];
if (!id || !value) {
die("Usage: json-cas var update <id> <hash>");
}
const varStore = openVarStore();
try {
const variable = varStore.update(id, value);
const envelope = await wrapVariableEnvelope(variable);
out(envelope);
} catch (e) {
if (
e instanceof VariableNotFoundError ||
e instanceof SchemaMismatchError ||
e instanceof CasNodeNotFoundError
) {
die(`Error: ${e.message}`);
}
throw e;
} finally {
varStore.close();
}
}
async function cmdVarDelete(args: string[]): Promise<void> {
const id = args[0];
if (!id) die("Usage: json-cas var delete <id>");
const name = args[0];
const schema = flags.schema as string | undefined;
if (!name) {
die("Usage: json-cas var delete <name> [--schema <hash>]");
}
const varStore = openVarStore();
try {
const variable = varStore.delete(id);
const envelope = await wrapVariableEnvelope(variable);
out(envelope);
if (schema !== undefined) {
// Precise deletion: remove specific (name, schema) variant
const variable = varStore.remove(name, schema);
const envelope = await wrapVariableEnvelope(variable);
out(envelope);
} else {
// Batch deletion: remove all variants for this name
const variables = varStore.remove(name);
const envelope = await wrapVariableEnvelope(variables);
out(envelope);
}
} catch (e) {
if (e instanceof VariableNotFoundError) {
die(`Error: ${e.message}`);
@@ -481,12 +551,16 @@ async function cmdVarDelete(args: string[]): Promise<void> {
}
async function cmdVarTag(args: string[]): Promise<void> {
const id = args[0];
if (!id) die("Usage: json-cas var tag <id> <tag>...");
const name = args[0];
const schema = flags.schema as string | undefined;
if (!name || !schema) {
die("Usage: json-cas var tag <name> --schema <hash> <operations...>");
}
const tagArgs = args.slice(1);
if (tagArgs.length === 0) {
die("Usage: json-cas var tag <id> <tag>...");
die("Usage: json-cas var tag <name> --schema <hash> <operations...>");
}
const varStore = openVarStore();
@@ -494,7 +568,7 @@ async function cmdVarTag(args: string[]): Promise<void> {
try {
const { tags, labels, deleteNames } = parseTagsLabels(tagArgs);
const variable = varStore.tag(id, {
const variable = varStore.tag(name, schema, {
add: Object.keys(tags).length > 0 ? tags : undefined,
addLabels: labels.length > 0 ? labels : undefined,
delete: deleteNames.length > 0 ? deleteNames : undefined,
@@ -516,8 +590,9 @@ async function cmdVarTag(args: string[]): Promise<void> {
}
}
async function cmdVarList(_args: string[]): Promise<void> {
const scope = (flags.scope as string | undefined) ?? "";
async function cmdVarList(args: string[]): Promise<void> {
const namePrefix = args[0] ?? "";
const schema = flags.schema as string | undefined;
const tagFlags = flags.tag;
const varStore = openVarStore();
@@ -537,14 +612,15 @@ async function cmdVarList(_args: string[]): Promise<void> {
}
const variables = varStore.list({
scope,
namePrefix,
schema,
tags: Object.keys(tags).length > 0 ? tags : undefined,
labels: labels.length > 0 ? labels : undefined,
});
const envelope = await wrapVariableEnvelope(variables);
out(envelope);
} catch (e) {
if (e instanceof InvalidScopeError) {
if (e instanceof InvalidVariableNameError) {
die(`Error: ${e.message}`);
}
throw e;
@@ -583,20 +659,24 @@ Commands:
refs <hash> List direct cas_ref edges
walk <hash> [--format tree] Recursive traversal
hash <type-hash> <file.json> Compute hash without storing (dry run)
render <hash> [options] Render node as YAML with resolution decay
cat <hash> [--payload] Output node (--payload for payload only)
var create --scope <s> --value <h> [--tag <tag>...] Create a variable
var get <id> Get a variable by ID
var update <id> <hash> Update variable value
var delete <id> Delete a variable
var tag <id> <tag>... Add/update/delete tags and labels
var list [--scope <prefix>] [--tag <tag>...] List variables (filter by scope/tags/labels)
var set <name> <hash> [--tag <tag>...] Create/update a variable
var get <name> --schema <hash> Get a variable by name + schema
var delete <name> [--schema <hash>] Delete variable(s)
var list [prefix] [--schema <hash>] [--tag <tag>...] List variables
var tag <name> --schema <hash> <operations...> Modify tags/labels
gc Run garbage collection
Flags:
--store <path> Store directory (default: ~/.uncaged/json-cas)
--var-db <path> Variable database path (default: <store>/variables.db)
--json Compact JSON output
--tag <tag> Tag/label (can be repeated): key:value (tag), name (label), :name (delete)`);
--store <path> Store directory (default: ~/.uncaged/json-cas)
--var-db <path> Variable database path (default: <store>/variables.db)
--json Compact JSON output
--schema <hash> Schema hash filter for var get/delete/tag/list
--tag <tag> Tag/label (can be repeated): key:value (tag), name (label), :name (delete)
--resolution <n> Initial resolution for render (default: 1.0)
--decay <n> Decay factor for render (default: 0.5)
--epsilon <n> Cutoff threshold for render (default: 0.01)`);
}
// ---- Dispatch ----
@@ -666,6 +746,10 @@ switch (cmd) {
await cmdHash(rest);
break;
case "render":
await cmdRender(rest);
break;
case "cat":
await cmdCat(rest);
break;
@@ -673,15 +757,12 @@ switch (cmd) {
case "var": {
const [sub, ...subRest] = rest;
switch (sub) {
case "create":
await cmdVarCreate(subRest);
case "set":
await cmdVarSet(subRest);
break;
case "get":
await cmdVarGet(subRest);
break;
case "update":
await cmdVarUpdate(subRest);
break;
case "delete":
await cmdVarDelete(subRest);
break;
File diff suppressed because it is too large Load Diff
+11 -7
View File
@@ -43,7 +43,8 @@ describe("createFsStore – init and bootstrap", () => {
test("bootstrap returns a valid 13-char self-referencing hash", async () => {
const store = createFsStore(dir);
const hash = await bootstrap(store);
const builtinSchemas = await bootstrap(store);
const hash = builtinSchemas["@schema"] ?? "";
expect(hash).toHaveLength(13);
expect(hash).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
@@ -57,8 +58,8 @@ describe("createFsStore – init and bootstrap", () => {
const h1 = await bootstrap(store);
const h2 = await bootstrap(store);
expect(h1).toBe(h2);
expect(store.listByType(h1)).toHaveLength(1);
expect(h1).toEqual(h2);
expect(store.listByType(h1["@schema"] ?? "")).toHaveLength(6);
});
});
@@ -104,7 +105,8 @@ describe("createFsStore – persistence round-trip", () => {
test("bootstrap survives round-trip: self-referencing node reloads correctly", async () => {
const store1 = createFsStore(dir);
const hash = await bootstrap(store1);
const builtinSchemas = await bootstrap(store1);
const hash = builtinSchemas["@schema"] ?? "";
const store2 = createFsStore(dir);
const node = store2.get(hash) as CasNode;
@@ -251,10 +253,11 @@ describe("createFsStore – listByType", () => {
test("bootstrap node is listed under its self type after reload", async () => {
const store1 = createFsStore(dir);
const hash = await bootstrap(store1);
const builtinSchemas = await bootstrap(store1);
const hash = builtinSchemas["@schema"] ?? "";
const store2 = createFsStore(dir);
expect(store2.listByType(hash)).toEqual([hash]);
expect(store2.listByType(hash)).toContain(hash);
});
});
@@ -284,7 +287,8 @@ describe("createFsStore – verify on disk-loaded nodes", () => {
test("verify passes on a disk-loaded bootstrap node", async () => {
const store1 = createFsStore(dir);
const hash = await bootstrap(store1);
const builtinSchemas = await bootstrap(store1);
const hash = builtinSchemas["@schema"] ?? "";
const store2 = createFsStore(dir);
const node = store2.get(hash) as CasNode;
+129
View File
@@ -0,0 +1,129 @@
import { describe, expect, test } from "bun:test";
import { bootstrap } from "./bootstrap.js";
import { getSchema } from "./schema.js";
import { createMemoryStore } from "./store.js";
// ──────────────────────────────────────────────────────────────────────────────
// Built-in Schema Registration Tests
// ──────────────────────────────────────────────────────────────────────────────
describe("bootstrap - Built-in Schemas", () => {
test("should return map of built-in schema aliases to hashes", async () => {
const store = createMemoryStore();
const builtinSchemas = await bootstrap(store);
// Should return object with 6 aliases
expect(builtinSchemas).toHaveProperty("@schema");
expect(builtinSchemas).toHaveProperty("@string");
expect(builtinSchemas).toHaveProperty("@number");
expect(builtinSchemas).toHaveProperty("@object");
expect(builtinSchemas).toHaveProperty("@array");
expect(builtinSchemas).toHaveProperty("@bool");
// All values should be valid hashes
for (const [_alias, hash] of Object.entries(builtinSchemas)) {
expect(typeof hash).toBe("string");
expect(hash).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
}
});
test("should register @schema as meta-schema alias", async () => {
const store = createMemoryStore();
const builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@schema"];
if (!metaHash) throw new Error("@schema not found");
const metaSchema = getSchema(store, metaHash);
expect(metaSchema).not.toBeNull();
expect(metaSchema?.type).toBe("object");
expect(metaSchema?.description).toBe("json-cas JSON Schema meta-schema");
});
test("should register @string schema correctly", async () => {
const store = createMemoryStore();
const builtinSchemas = await bootstrap(store);
const stringHash = builtinSchemas["@string"];
if (!stringHash) throw new Error("@string not found");
const stringSchema = getSchema(store, stringHash);
expect(stringSchema).toEqual({ type: "string" });
});
test("should register @number schema correctly", async () => {
const store = createMemoryStore();
const builtinSchemas = await bootstrap(store);
const numberHash = builtinSchemas["@number"];
if (!numberHash) throw new Error("@number not found");
const numberSchema = getSchema(store, numberHash);
expect(numberSchema).toEqual({ type: "number" });
});
test("should register @object schema correctly", async () => {
const store = createMemoryStore();
const builtinSchemas = await bootstrap(store);
const objectHash = builtinSchemas["@object"];
if (!objectHash) throw new Error("@object not found");
const objectSchema = getSchema(store, objectHash);
expect(objectSchema).toEqual({ type: "object" });
});
test("should register @array schema correctly", async () => {
const store = createMemoryStore();
const builtinSchemas = await bootstrap(store);
const arrayHash = builtinSchemas["@array"];
if (!arrayHash) throw new Error("@array not found");
const arraySchema = getSchema(store, arrayHash);
expect(arraySchema).toEqual({ type: "array" });
});
test("should register @bool schema correctly", async () => {
const store = createMemoryStore();
const builtinSchemas = await bootstrap(store);
const boolHash = builtinSchemas["@bool"];
if (!boolHash) throw new Error("@bool not found");
const boolSchema = getSchema(store, boolHash);
expect(boolSchema).toEqual({ type: "boolean" });
});
test("should return same hashes on repeated bootstrap calls", async () => {
const store = createMemoryStore();
const first = await bootstrap(store);
const second = await bootstrap(store);
expect(first).toEqual(second);
// Verify each alias points to same hash
expect(first["@string"]).toBe(second["@string"]);
expect(first["@number"]).toBe(second["@number"]);
expect(first["@object"]).toBe(second["@object"]);
expect(first["@array"]).toBe(second["@array"]);
expect(first["@bool"]).toBe(second["@bool"]);
expect(first["@schema"]).toBe(second["@schema"]);
});
test("all built-in schemas should be typed by meta-schema", async () => {
const store = createMemoryStore();
const builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@schema"];
if (!metaHash) throw new Error("@schema not found");
for (const [alias, hash] of Object.entries(builtinSchemas)) {
if (alias === "@schema") continue; // meta-schema is self-typed
const node = store.get(hash);
expect(node).not.toBeNull();
expect(node?.type).toBe(metaHash);
}
});
});
+24 -5
View File
@@ -64,13 +64,32 @@ const BOOTSTRAP_PAYLOAD = {
} as const;
/**
* Write the meta-schema seed node into the store.
* The returned hash equals the node's own type field (self-referencing).
* Idempotent: calling bootstrap multiple times returns the same hash.
* 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.
* Idempotent: calling bootstrap multiple times returns the same hashes.
*/
export async function bootstrap(store: Store): Promise<Hash> {
export async function bootstrap(store: Store): Promise<Record<string, Hash>> {
if (!isBootstrapCapableStore(store)) {
throw new Error("Store does not support bootstrap");
}
return store[BOOTSTRAP_STORE](BOOTSTRAP_PAYLOAD);
// 1. Bootstrap the meta-schema (self-referential)
const metaHash = await store[BOOTSTRAP_STORE](BOOTSTRAP_PAYLOAD);
// 2. Register built-in primitive schemas directly (without putSchema to avoid recursion)
const stringHash = await store.put(metaHash, { type: "string" });
const numberHash = await store.put(metaHash, { type: "number" });
const objectHash = await store.put(metaHash, { type: "object" });
const arrayHash = await store.put(metaHash, { type: "array" });
const boolHash = await store.put(metaHash, { type: "boolean" });
// 3. Return map of aliases to hashes
return {
"@schema": metaHash,
"@string": stringHash,
"@number": numberHash,
"@object": objectHash,
"@array": arrayHash,
"@bool": boolHash,
};
}
+43 -20
View File
@@ -197,9 +197,17 @@ describe("createMemoryStore – listByType", () => {
test("bootstrap node is listed under its self type", async () => {
const store = createMemoryStore();
const hash = await bootstrap(store);
const builtinSchemas = await bootstrap(store);
const hash = builtinSchemas["@schema"] ?? "";
expect(store.listByType(hash)).toEqual([hash]);
// All built-in schemas should be typed by the meta-schema
const allTypedByMeta = store.listByType(hash);
expect(allTypedByMeta).toContain(hash); // meta-schema itself
expect(allTypedByMeta).toContain(builtinSchemas["@string"] ?? "");
expect(allTypedByMeta).toContain(builtinSchemas["@number"] ?? "");
expect(allTypedByMeta).toContain(builtinSchemas["@object"] ?? "");
expect(allTypedByMeta).toContain(builtinSchemas["@array"] ?? "");
expect(allTypedByMeta).toContain(builtinSchemas["@bool"] ?? "");
});
});
@@ -256,44 +264,59 @@ describe("bootstrap", () => {
);
});
test("returns a valid 13-char hash", async () => {
test("returns a map with 6 built-in schema aliases", async () => {
const store = createMemoryStore();
const hash = await bootstrap(store);
expect(hash).toHaveLength(13);
expect(hash).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
const builtinSchemas = await bootstrap(store);
expect(builtinSchemas).toHaveProperty("@schema");
expect(builtinSchemas).toHaveProperty("@string");
expect(builtinSchemas).toHaveProperty("@number");
expect(builtinSchemas).toHaveProperty("@object");
expect(builtinSchemas).toHaveProperty("@array");
expect(builtinSchemas).toHaveProperty("@bool");
// All values should be valid hashes
for (const hash of Object.values(builtinSchemas)) {
expect(hash).toHaveLength(13);
expect(hash).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
}
});
test("node is stored and retrievable", async () => {
test("meta-schema node is stored and retrievable", async () => {
const store = createMemoryStore();
const hash = await bootstrap(store);
const builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@schema"] ?? "";
expect(store.has(hash)).toBe(true);
const node = store.get(hash);
expect(store.has(metaHash)).toBe(true);
const node = store.get(metaHash);
expect(node).not.toBeNull();
});
test("node is self-referencing: type === hash", async () => {
test("meta-schema node is self-referencing: type === hash", async () => {
const store = createMemoryStore();
const hash = await bootstrap(store);
const node = store.get(hash) as CasNode;
const builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@schema"] ?? "";
const node = store.get(metaHash) as CasNode;
expect(node.type).toBe(hash);
expect(node.type).toBe(metaHash);
});
test("bootstrap node passes verify()", async () => {
const store = createMemoryStore();
const hash = await bootstrap(store);
const node = store.get(hash) as CasNode;
const builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@schema"] ?? "";
const node = store.get(metaHash) as CasNode;
expect(await verify(hash, node)).toBe(true);
expect(await verify(metaHash, node)).toBe(true);
});
test("bootstrap is idempotent: same hash on repeated calls", async () => {
test("bootstrap is idempotent: same hashes on repeated calls", async () => {
const store = createMemoryStore();
const h1 = await bootstrap(store);
const h2 = await bootstrap(store);
expect(h1).toBe(h2);
expect(store.listByType(h1)).toHaveLength(1);
expect(h1).toEqual(h2);
// All 6 built-in schemas should be typed by the meta-schema
expect(store.listByType(h1["@schema"] ?? "")).toHaveLength(6);
});
});
+1
View File
@@ -4,6 +4,7 @@ export { BOOTSTRAP_STORE } from "./bootstrap-capable.js";
export { cborEncode } from "./cbor.js";
export { type GcStats, gc } from "./gc.js";
export { computeHash, computeSelfHash } from "./hash.js";
export { type RenderOptions, render } from "./render.js";
export type { JSONSchema } from "./schema.js";
export {
getSchema,
+935
View File
@@ -0,0 +1,935 @@
import { describe, expect, test } from "bun:test";
import { bootstrap } from "./bootstrap.js";
import { render } from "./render.js";
import { putSchema } from "./schema.js";
import { createMemoryStore } from "./store.js";
import type { Hash } from "./types.js";
describe("Suite 1: Basic Rendering (No Nesting)", () => {
test("1.1 Render Simple Primitives", async () => {
const store = createMemoryStore();
await bootstrap(store);
const textSchema = await putSchema(store, { type: "string" });
const hash = await store.put(textSchema, "hello");
const output = render(store, hash, { resolution: 1.0 });
expect(output).toContain("hello");
expect(output.trim()).toBeTruthy();
});
test("1.2 Render Object Node (Flat)", async () => {
const store = createMemoryStore();
await bootstrap(store);
const objSchema = await putSchema(store, {
type: "object",
properties: {
name: { type: "string" },
count: { type: "number" },
},
});
const hash = await store.put(objSchema, { name: "test", count: 42 });
const output = render(store, hash, { resolution: 1.0 });
expect(output).toContain("name");
expect(output).toContain("test");
expect(output).toContain("count");
expect(output).toContain("42");
});
test("1.3 Render Array Node (Flat)", async () => {
const store = createMemoryStore();
await bootstrap(store);
const arraySchema = await putSchema(store, {
type: "array",
items: { type: "number" },
});
const hash = await store.put(arraySchema, [1, 2, 3]);
const output = render(store, hash, { resolution: 1.0 });
expect(output).toContain("1");
expect(output).toContain("2");
expect(output).toContain("3");
});
test("1.4 Render with resolution=0 (Force Reference)", async () => {
const store = createMemoryStore();
await bootstrap(store);
const textSchema = await putSchema(store, { type: "string" });
const hash = await store.put(textSchema, "hello");
const output = render(store, hash, { resolution: 0 });
expect(output.trim()).toBe(`cas:${hash}`);
});
test("1.5 Render Non-existent Hash", () => {
const store = createMemoryStore();
const fakeHash = "ZZZZZZZZZZZZZ" as Hash;
// Non-existent node renders as cas: reference
const output = render(store, fakeHash);
expect(output.trim()).toBe(`cas:${fakeHash}`);
});
});
describe("Suite 2: Resolution Decay Model", () => {
test("2.1 Single-level Nesting with Default Decay", async () => {
const store = createMemoryStore();
await bootstrap(store);
const childSchema = await putSchema(store, {
type: "object",
properties: {
content: { type: "string" },
},
});
const childHash = await store.put(childSchema, { content: "leaf" });
const parentSchema = await putSchema(store, {
type: "object",
properties: {
title: { type: "string" },
child: { type: "string", format: "cas_ref" },
},
});
const parentHash = await store.put(parentSchema, {
title: "root",
child: childHash,
});
const output = render(store, parentHash, {
resolution: 1.0,
decay: 0.5,
epsilon: 0.01,
});
expect(output).toContain("title");
expect(output).toContain("root");
expect(output).toContain("content");
expect(output).toContain("leaf");
});
test("2.2 Multi-level Nesting Reaches Epsilon", async () => {
const store = createMemoryStore();
await bootstrap(store);
const leafSchema = await putSchema(store, {
type: "object",
properties: {
value: { type: "number" },
next: {
anyOf: [{ type: "string", format: "cas_ref" }, { type: "null" }],
},
},
});
// Create 8-level chain
let currentHash: Hash | null = null;
for (let i = 7; i >= 0; i--) {
currentHash = await store.put(leafSchema, {
value: i,
next: currentHash,
});
}
const output = render(store, currentHash as Hash, {
resolution: 1.0,
decay: 0.5,
epsilon: 0.01,
});
// At depth 7: resolution = 0.5^7 = 0.0078125 <= 0.01
expect(output).toContain("value");
expect(output).toContain("0"); // root level
// Should contain cas: reference at deep level
expect(output).toMatch(/cas:[0-9A-HJKMNP-TV-Z]{13}/);
});
test("2.3 High Decay (Quick Cutoff)", async () => {
const store = createMemoryStore();
await bootstrap(store);
const nodeSchema = await putSchema(store, {
type: "object",
properties: {
level: { type: "number" },
child: {
anyOf: [{ type: "string", format: "cas_ref" }, { type: "null" }],
},
},
});
// Create 3-level nested structure
const level2Hash = await store.put(nodeSchema, { level: 2, child: null });
const level1Hash = await store.put(nodeSchema, {
level: 1,
child: level2Hash,
});
const rootHash = await store.put(nodeSchema, {
level: 0,
child: level1Hash,
});
const output = render(store, rootHash, {
resolution: 1.0,
decay: 0.1,
epsilon: 0.01,
});
expect(output).toContain("level");
expect(output).toContain("0"); // root
expect(output).toContain("1"); // level 1 (0.1 > 0.01)
// Level 2 should be reference (0.01 <= 0.01)
expect(output).toMatch(/cas:[0-9A-HJKMNP-TV-Z]{13}/);
});
test("2.4 Low Decay (Deep Expansion)", async () => {
const store = createMemoryStore();
await bootstrap(store);
const nodeSchema = await putSchema(store, {
type: "object",
properties: {
level: { type: "number" },
next: {
anyOf: [{ type: "string", format: "cas_ref" }, { type: "null" }],
},
},
});
// Create 10-level chain
let currentHash: Hash | null = null;
for (let i = 9; i >= 0; i--) {
currentHash = await store.put(nodeSchema, {
level: i,
next: currentHash,
});
}
const output = render(store, currentHash as Hash, {
resolution: 1.0,
decay: 0.9,
epsilon: 0.01,
});
// All 10 levels should be expanded (0.9^10 ≈ 0.349 > 0.01)
for (let i = 0; i < 10; i++) {
expect(output).toContain(`${i}`);
}
});
test("2.5 Starting Resolution Below 1.0", async () => {
const store = createMemoryStore();
await bootstrap(store);
const nodeSchema = await putSchema(store, {
type: "object",
properties: {
level: { type: "number" },
next: {
anyOf: [{ type: "string", format: "cas_ref" }, { type: "null" }],
},
},
});
// Create 5-level chain
let currentHash: Hash | null = null;
for (let i = 4; i >= 0; i--) {
currentHash = await store.put(nodeSchema, {
level: i,
next: currentHash,
});
}
const output = render(store, currentHash as Hash, {
resolution: 0.5,
decay: 0.5,
epsilon: 0.01,
});
// resolution sequence: 0.5, 0.25, 0.125, 0.0625, 0.03125 (all > 0.01)
expect(output).toContain("0");
expect(output).toContain("1");
expect(output).toContain("2");
expect(output).toContain("3");
});
});
describe("Suite 3: Complex Graph Structures", () => {
test("3.1 Multiple Child References", async () => {
const store = createMemoryStore();
await bootstrap(store);
const itemSchema = await putSchema(store, {
type: "object",
properties: {
name: { type: "string" },
},
});
const item1 = await store.put(itemSchema, { name: "item1" });
const item2 = await store.put(itemSchema, { name: "item2" });
const item3 = await store.put(itemSchema, { name: "item3" });
const parentSchema = await putSchema(store, {
type: "object",
properties: {
items: {
type: "array",
items: { type: "string", format: "cas_ref" },
},
},
});
const parentHash = await store.put(parentSchema, {
items: [item1, item2, item3],
});
const output = render(store, parentHash, {
resolution: 1.0,
decay: 0.5,
epsilon: 0.01,
});
expect(output).toContain("item1");
expect(output).toContain("item2");
expect(output).toContain("item3");
});
test("3.2 Object with Multiple cas_ref Fields", async () => {
const store = createMemoryStore();
await bootstrap(store);
const childSchema = await putSchema(store, {
type: "object",
properties: {
value: { type: "string" },
},
});
const leftHash = await store.put(childSchema, { value: "left" });
const rightHash = await store.put(childSchema, { value: "right" });
const parentSchema = await putSchema(store, {
type: "object",
properties: {
left: { type: "string", format: "cas_ref" },
right: { type: "string", format: "cas_ref" },
data: { type: "string" },
},
});
const parentHash = await store.put(parentSchema, {
left: leftHash,
right: rightHash,
data: "node",
});
const output = render(store, parentHash, {
resolution: 1.0,
decay: 0.5,
epsilon: 0.01,
});
expect(output).toContain("left");
expect(output).toContain("right");
expect(output).toContain("node");
});
test("3.3 Cycle Detection", async () => {
const store = createMemoryStore();
await bootstrap(store);
const nodeSchema = await putSchema(store, {
type: "object",
properties: {
name: { type: "string" },
ref: {
anyOf: [{ type: "string", format: "cas_ref" }, { type: "null" }],
},
},
});
const hashA = await store.put(nodeSchema, { name: "A", ref: null });
const hashB = await store.put(nodeSchema, { name: "B", ref: hashA });
// Manually update A to reference B (simulate cycle)
// Note: In practice, this requires store manipulation
// For this test, we'll create a simpler case
const output = render(store, hashB, {
resolution: 1.0,
decay: 0.5,
epsilon: 0.01,
});
// Should not infinite loop
expect(output).toContain("B");
expect(output).toContain("A");
});
test("3.4 DAG (Shared Descendant)", async () => {
const store = createMemoryStore();
await bootstrap(store);
const leafSchema = await putSchema(store, {
type: "object",
properties: {
value: { type: "string" },
},
});
const sharedLeaf = await store.put(leafSchema, { value: "shared" });
const branchSchema = await putSchema(store, {
type: "object",
properties: {
name: { type: "string" },
child: { type: "string", format: "cas_ref" },
},
});
const branchA = await store.put(branchSchema, {
name: "A",
child: sharedLeaf,
});
const branchB = await store.put(branchSchema, {
name: "B",
child: sharedLeaf,
});
const rootSchema = await putSchema(store, {
type: "object",
properties: {
left: { type: "string", format: "cas_ref" },
right: { type: "string", format: "cas_ref" },
},
});
const rootHash = await store.put(rootSchema, {
left: branchA,
right: branchB,
});
const output = render(store, rootHash, {
resolution: 1.0,
decay: 0.5,
epsilon: 0.01,
});
expect(output).toContain("A");
expect(output).toContain("B");
expect(output).toContain("shared");
});
test("3.5 Deep Tree", async () => {
const store = createMemoryStore();
await bootstrap(store);
const nodeSchema = await putSchema(store, {
type: "object",
properties: {
value: { type: "number" },
left: {
anyOf: [{ type: "string", format: "cas_ref" }, { type: "null" }],
},
right: {
anyOf: [{ type: "string", format: "cas_ref" }, { type: "null" }],
},
},
});
// Create binary tree (just 5 levels for test speed)
async function createTree(depth: number, value: number): Promise<Hash> {
if (depth === 0) {
return store.put(nodeSchema, { value, left: null, right: null });
}
const left = await createTree(depth - 1, value * 2);
const right = await createTree(depth - 1, value * 2 + 1);
return store.put(nodeSchema, { value, left, right });
}
const rootHash = await createTree(5, 1);
const output = render(store, rootHash, {
resolution: 1.0,
decay: 0.5,
epsilon: 0.01,
});
// Should complete without error
expect(output).toContain("value");
});
});
describe("Suite 4: Epsilon Boundary Cases", () => {
test("4.1 Resolution Exactly at Epsilon", async () => {
const store = createMemoryStore();
await bootstrap(store);
const textSchema = await putSchema(store, { type: "string" });
const hash = await store.put(textSchema, "test");
const output = render(store, hash, {
resolution: 0.01,
decay: 0.5,
epsilon: 0.01,
});
expect(output.trim()).toBe(`cas:${hash}`);
});
test("4.2 Resolution Just Above Epsilon", async () => {
const store = createMemoryStore();
await bootstrap(store);
const textSchema = await putSchema(store, { type: "string" });
const hash = await store.put(textSchema, "test");
const output = render(store, hash, {
resolution: 0.0100001,
epsilon: 0.01,
});
expect(output).toContain("test");
expect(output).not.toContain("cas:");
});
test("4.3 Very Small Epsilon (Deep Expansion)", async () => {
const store = createMemoryStore();
await bootstrap(store);
const nodeSchema = await putSchema(store, {
type: "object",
properties: {
level: { type: "number" },
next: {
anyOf: [{ type: "string", format: "cas_ref" }, { type: "null" }],
},
},
});
// Create 15-level chain
let currentHash: Hash | null = null;
for (let i = 14; i >= 0; i--) {
currentHash = await store.put(nodeSchema, {
level: i,
next: currentHash,
});
}
const output = render(store, currentHash as Hash, {
resolution: 1.0,
decay: 0.5,
epsilon: 0.000001,
});
// Many levels should be expanded
expect(output).toContain("0");
expect(output).toContain("5");
expect(output).toContain("10");
});
test("4.4 Zero Epsilon (Never Prune)", async () => {
const store = createMemoryStore();
await bootstrap(store);
const nodeSchema = await putSchema(store, {
type: "object",
properties: {
level: { type: "number" },
next: {
anyOf: [{ type: "string", format: "cas_ref" }, { type: "null" }],
},
},
});
// Create 20-level chain
let currentHash: Hash | null = null;
for (let i = 19; i >= 0; i--) {
currentHash = await store.put(nodeSchema, {
level: i,
next: currentHash,
});
}
const output = render(store, currentHash as Hash, {
resolution: 1.0,
decay: 0.5,
epsilon: 0,
});
// All levels should be present
expect(output).toContain("0");
expect(output).toContain("10");
expect(output).toContain("19");
});
});
describe("Suite 5: YAML Output Format", () => {
test("5.1 Valid YAML Syntax", async () => {
const store = createMemoryStore();
await bootstrap(store);
const objSchema = await putSchema(store, {
type: "object",
properties: {
name: { type: "string" },
count: { type: "number" },
},
});
const hash = await store.put(objSchema, { name: "test", count: 42 });
const output = render(store, hash);
// Basic YAML validation - should have key: value pairs
expect(output).toMatch(/\w+:/);
});
test("5.2 Nested Object Indentation", async () => {
const store = createMemoryStore();
await bootstrap(store);
const nestedSchema = await putSchema(store, {
type: "object",
properties: {
outer: {
type: "object",
properties: {
inner: { type: "string" },
},
},
},
});
const hash = await store.put(nestedSchema, {
outer: { inner: "value" },
});
const output = render(store, hash);
// Should have proper indentation (2 spaces)
expect(output).toContain("outer");
expect(output).toContain("inner");
expect(output).toContain("value");
});
test("5.3 Array Rendering", async () => {
const store = createMemoryStore();
await bootstrap(store);
const arraySchema = await putSchema(store, {
type: "array",
items: { type: "number" },
});
const hash = await store.put(arraySchema, [1, 2, 3]);
const output = render(store, hash);
// YAML array format
expect(output).toMatch(/[-[].*[1-3]/);
});
test("5.4 CAS Reference in YAML", async () => {
const store = createMemoryStore();
await bootstrap(store);
const childSchema = await putSchema(store, {
type: "object",
properties: {
value: { type: "string" },
},
});
const childHash = await store.put(childSchema, { value: "child" });
const parentSchema = await putSchema(store, {
type: "object",
properties: {
child: { type: "string", format: "cas_ref" },
},
});
const parentHash = await store.put(parentSchema, { child: childHash });
const output = render(store, parentHash, {
resolution: 1.0,
decay: 0.1,
epsilon: 0.5,
});
// Child should be rendered as cas: reference
expect(output).toMatch(/cas:[0-9A-HJKMNP-TV-Z]{13}/);
});
test("5.5 Special Characters Escaping", async () => {
const store = createMemoryStore();
await bootstrap(store);
const textSchema = await putSchema(store, { type: "string" });
const hash = await store.put(textSchema, "line1\nline2: value");
const output = render(store, hash);
// Should handle newlines and colons
expect(output).toBeTruthy();
});
test("5.6 Null Handling", async () => {
const store = createMemoryStore();
await bootstrap(store);
const nullableSchema = await putSchema(store, {
type: "object",
properties: {
ref: {
anyOf: [{ type: "string", format: "cas_ref" }, { type: "null" }],
},
},
});
const hash = await store.put(nullableSchema, { ref: null });
const output = render(store, hash);
expect(output).toContain("null");
});
});
describe("Suite 6: Schema Integration", () => {
test("6.1 Detect cas_ref Fields via Schema", async () => {
const store = createMemoryStore();
await bootstrap(store);
const childSchema = await putSchema(store, {
type: "object",
properties: {
value: { type: "string" },
},
});
const childHash = await store.put(childSchema, { value: "child" });
const parentSchema = await putSchema(store, {
type: "object",
properties: {
link: { type: "string", format: "cas_ref" },
},
});
const parentHash = await store.put(parentSchema, { link: childHash });
const output = render(store, parentHash, {
resolution: 1.0,
decay: 0.5,
epsilon: 0.01,
});
expect(output).toContain("child");
});
test("6.2 Non-cas_ref String Not Expanded", async () => {
const store = createMemoryStore();
await bootstrap(store);
const objSchema = await putSchema(store, {
type: "object",
properties: {
name: { type: "string" },
},
});
const hash = await store.put(objSchema, { name: "ABC123XYZ9012" });
const output = render(store, hash);
// Should be plain string, not expanded
expect(output).toContain("ABC123XYZ9012");
expect(output).not.toMatch(/cas:[0-9A-HJKMNP-TV-Z]{13}/);
});
test("6.3 Array of cas_ref", async () => {
const store = createMemoryStore();
await bootstrap(store);
const itemSchema = await putSchema(store, {
type: "object",
properties: {
name: { type: "string" },
},
});
const item1 = await store.put(itemSchema, { name: "item1" });
const item2 = await store.put(itemSchema, { name: "item2" });
const arraySchema = await putSchema(store, {
type: "array",
items: { type: "string", format: "cas_ref" },
});
const arrayHash = await store.put(arraySchema, [item1, item2]);
const output = render(store, arrayHash, {
resolution: 1.0,
decay: 0.5,
epsilon: 0.01,
});
expect(output).toContain("item1");
expect(output).toContain("item2");
});
test("6.4 anyOf with cas_ref (Nullable Reference)", async () => {
const store = createMemoryStore();
await bootstrap(store);
const childSchema = await putSchema(store, {
type: "object",
properties: {
value: { type: "string" },
},
});
const childHash = await store.put(childSchema, { value: "child" });
const parentSchema = await putSchema(store, {
type: "object",
properties: {
ref: {
anyOf: [{ type: "string", format: "cas_ref" }, { type: "null" }],
},
},
});
const parentHash = await store.put(parentSchema, { ref: childHash });
const output = render(store, parentHash, {
resolution: 1.0,
decay: 0.5,
epsilon: 0.01,
});
expect(output).toContain("child");
});
test("6.5 Schema-less Node (Bootstrap Node)", async () => {
const store = createMemoryStore();
const metaHash = await bootstrap(store);
const output = render(store, metaHash);
// Should render without recursive expansion
expect(output).toBeTruthy();
});
});
describe("Suite 7: Error Handling", () => {
test("7.1 Missing Referenced Node", async () => {
const store = createMemoryStore();
await bootstrap(store);
const parentSchema = await putSchema(store, {
type: "object",
properties: {
child: { type: "string", format: "cas_ref" },
},
});
const fakeChildHash = "ZZZZZZZZZZZZZ" as Hash;
const parentHash = await store.put(parentSchema, { child: fakeChildHash });
const output = render(store, parentHash);
// Should render missing ref as cas:<hash>
expect(output).toContain(`cas:${fakeChildHash}`);
});
test("7.3 Invalid Resolution Parameter", () => {
const store = createMemoryStore();
const fakeHash = "AAAAAAAAAAAAA" as Hash;
expect(() => render(store, fakeHash, { resolution: -1 })).toThrow();
});
test("7.4 Invalid Decay Parameter", () => {
const store = createMemoryStore();
const fakeHash = "AAAAAAAAAAAAA" as Hash;
expect(() => render(store, fakeHash, { decay: 1.5 })).toThrow();
});
test("7.5 Invalid Epsilon Parameter", () => {
const store = createMemoryStore();
const fakeHash = "AAAAAAAAAAAAA" as Hash;
expect(() => render(store, fakeHash, { epsilon: -0.01 })).toThrow();
});
});
describe("Suite 8: Performance & Edge Cases", () => {
test("8.1 Large Payload", async () => {
const store = createMemoryStore();
await bootstrap(store);
const arraySchema = await putSchema(store, {
type: "array",
items: {
type: "object",
properties: {
id: { type: "number" },
name: { type: "string" },
},
},
});
const largeArray = Array.from({ length: 1000 }, (_, i) => ({
id: i,
name: `item${i}`,
}));
const hash = await store.put(arraySchema, largeArray);
const start = Date.now();
const output = render(store, hash);
const elapsed = Date.now() - start;
expect(elapsed).toBeLessThan(5000);
expect(output).toBeTruthy();
});
test("8.2 Wide Fan-out", async () => {
const store = createMemoryStore();
await bootstrap(store);
const itemSchema = await putSchema(store, {
type: "object",
properties: {
value: { type: "number" },
},
});
const children: Hash[] = [];
for (let i = 0; i < 100; i++) {
const hash = await store.put(itemSchema, { value: i });
children.push(hash);
}
const parentSchema = await putSchema(store, {
type: "array",
items: { type: "string", format: "cas_ref" },
});
const parentHash = await store.put(parentSchema, children);
const output = render(store, parentHash, {
resolution: 1.0,
decay: 0.5,
epsilon: 0.01,
});
expect(output).toBeTruthy();
});
test("8.3 Empty Payload", async () => {
const store = createMemoryStore();
await bootstrap(store);
const emptySchema = await putSchema(store, { type: "object" });
const hash = await store.put(emptySchema, {});
const output = render(store, hash);
expect(output.trim()).toMatch(/\{\}/);
});
test("8.4 Unicode in Payload", async () => {
const store = createMemoryStore();
await bootstrap(store);
const textSchema = await putSchema(store, {
type: "object",
properties: {
text: { type: "string" },
},
});
const hash = await store.put(textSchema, { text: "你好世界 🌍" });
const output = render(store, hash);
expect(output).toContain("你好世界");
expect(output).toContain("🌍");
});
});
+221
View File
@@ -0,0 +1,221 @@
import { refs } from "./schema.js";
import type { Hash, Store } from "./types.js";
export type RenderOptions = {
resolution?: number; // (0, 1], default 1.0
decay?: number; // (0, 1], default 0.5
epsilon?: number; // >= 0, default 0.01
};
const DEFAULT_RESOLUTION = 1.0;
const DEFAULT_DECAY = 0.5;
const DEFAULT_EPSILON = 0.01;
// Small tolerance for floating point comparison
const FLOAT_TOLERANCE = 1e-10;
/**
* Render a CAS node as YAML with resolution-based decay.
* When resolution ≤ epsilon, nodes are rendered as opaque `cas:<hash>` references.
*/
export function render(
store: Store,
hash: Hash,
options?: RenderOptions,
): string {
const resolution = options?.resolution ?? DEFAULT_RESOLUTION;
const decay = options?.decay ?? DEFAULT_DECAY;
const epsilon = options?.epsilon ?? DEFAULT_EPSILON;
// Validate parameters
if (resolution < 0 || resolution > 1) {
throw new Error("resolution must be in [0, 1]");
}
if (decay <= 0 || decay > 1) {
throw new Error("decay must be in (0, 1]");
}
if (epsilon < 0) {
throw new Error("epsilon must be >= 0");
}
const visited = new Set<Hash>();
return renderNode(store, hash, resolution, decay, epsilon, visited);
}
function renderNode(
store: Store,
hash: Hash,
currentResolution: number,
decay: number,
epsilon: number,
visited: Set<Hash>,
): string {
// Check if resolution is below threshold (with floating point tolerance)
if (currentResolution < epsilon + FLOAT_TOLERANCE) {
return `cas:${hash}`;
}
// Fetch the node
const node = store.get(hash);
if (node === null) {
// Missing node - render as cas: reference
return `cas:${hash}`;
}
// Cycle detection
if (visited.has(hash)) {
return `cas:${hash}`;
}
visited.add(hash);
// Get references from this node's schema
const nodeRefs = refs(store, node);
const refSet = new Set(nodeRefs);
// Calculate child resolution for next level
const childResolution = currentResolution * decay;
// Render the payload with recursive expansion of cas_ref fields
const rendered = renderValue(
store,
node.payload,
refSet,
childResolution,
decay,
epsilon,
visited,
);
visited.delete(hash);
return rendered;
}
function renderValue(
store: Store,
value: unknown,
refHashes: Set<Hash>,
childResolution: number,
decay: number,
epsilon: number,
visited: Set<Hash>,
): string {
// Handle null
if (value === null) {
return "null\n";
}
// Handle primitives
if (typeof value === "string") {
// Check if this string is a cas_ref
if (refHashes.has(value as Hash)) {
// Recursively render the referenced node
return renderNode(
store,
value as Hash,
childResolution,
decay,
epsilon,
visited,
);
}
// Otherwise, render as YAML string
return toYamlString(value);
}
if (typeof value === "number" || typeof value === "boolean") {
return `${value}\n`;
}
// Handle arrays
if (Array.isArray(value)) {
if (value.length === 0) {
return "[]\n";
}
const items = value.map((item) => {
const itemYaml = renderValue(
store,
item,
refHashes,
childResolution,
decay,
epsilon,
visited,
);
return indent(itemYaml.trim(), 2);
});
return `- ${items.join("\n- ")}\n`;
}
// Handle objects
if (typeof value === "object") {
const obj = value as Record<string, unknown>;
const keys = Object.keys(obj);
if (keys.length === 0) {
return "{}\n";
}
const pairs = keys.map((key) => {
const val = obj[key];
const valYaml = renderValue(
store,
val,
refHashes,
childResolution,
decay,
epsilon,
visited,
);
const trimmedVal = valYaml.trim();
// If value is multiline, indent it
if (trimmedVal.includes("\n")) {
return `${key}:\n${indent(trimmedVal, 2)}`;
}
return `${key}: ${trimmedVal}`;
});
return `${pairs.join("\n")}\n`;
}
return "null\n";
}
function toYamlString(str: string): string {
// Handle special characters
if (
str.includes("\n") ||
str.includes(":") ||
str.includes("#") ||
str.includes("[") ||
str.includes("]") ||
str.includes("{") ||
str.includes("}") ||
str.includes("'") ||
str.includes('"') ||
str.startsWith(" ") ||
str.endsWith(" ")
) {
// Use double-quoted string with escaping
const escaped = str
.replace(/\\/g, "\\\\")
.replace(/"/g, '\\"')
.replace(/\n/g, "\\n");
return `"${escaped}"\n`;
}
return `${str}\n`;
}
function indent(text: string, spaces: number): string {
const prefix = " ".repeat(spaces);
return text
.split("\n")
.map((line) => (line ? prefix + line : line))
.join("\n");
}
+10 -5
View File
@@ -29,7 +29,8 @@ describe("putSchema", () => {
test("schema node type equals the meta-schema hash", async () => {
const store = createMemoryStore();
const metaHash = await bootstrap(store);
const builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@schema"] ?? "";
const schemaHash = await putSchema(store, { type: "string" });
const node = store.get(schemaHash) as CasNode;
@@ -355,7 +356,8 @@ describe("walk", () => {
describe("bootstrap meta-schema self-reference", () => {
test("metaNode.type === metaHash (self-referencing)", async () => {
const store = createMemoryStore();
const metaHash = await bootstrap(store);
const builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@schema"] ?? "";
const metaNode = store.get(metaHash) as CasNode;
expect(metaNode.type).toBe(metaHash);
@@ -363,7 +365,8 @@ describe("bootstrap meta-schema self-reference", () => {
test("schema nodes have type === metaHash", async () => {
const store = createMemoryStore();
const metaHash = await bootstrap(store);
const builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@schema"] ?? "";
const schemaHash = await putSchema(store, { type: "string" });
const schemaNode = store.get(schemaHash) as CasNode;
@@ -372,7 +375,8 @@ describe("bootstrap meta-schema self-reference", () => {
test("data nodes have type === schemaHash (not metaHash)", async () => {
const store = createMemoryStore();
const metaHash = await bootstrap(store);
const builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@schema"] ?? "";
const schemaHash = await putSchema(store, {
type: "object",
properties: { val: { type: "number" } },
@@ -386,7 +390,8 @@ describe("bootstrap meta-schema self-reference", () => {
test("bootstrap is idempotent across putSchema calls", async () => {
const store = createMemoryStore();
const metaHash = await bootstrap(store);
const builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@schema"] ?? "";
await putSchema(store, { type: "string" });
await putSchema(store, { type: "number" });
+5 -1
View File
@@ -142,7 +142,11 @@ export async function putSchema(
store: Store,
jsonSchema: JSONSchema,
): Promise<Hash> {
const metaHash = await bootstrap(store);
const builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@schema"];
if (!metaHash) {
throw new Error("Meta-schema not found in bootstrap result");
}
if (!isValidSchema(jsonSchema)) {
throw new SchemaValidationError(
"Invalid schema: input does not conform to the json-cas JSON Schema meta-schema",
@@ -1593,3 +1593,186 @@ describe("VariableStore - Tag/Label Management", () => {
varStore.close();
});
});
// ──────────────────────────────────────────────────────────────────────────────
// @ Prefix Support for Variable Names
// ──────────────────────────────────────────────────────────────────────────────
describe("VariableStore - @ Prefix Variable Names", () => {
let store: Store;
let dbPath: string;
afterEach(() => {
if (dbPath) {
try {
unlinkSync(dbPath);
} catch {
// ignore
}
}
});
test("should accept variable name with @ prefix in first segment", async () => {
store = createMemoryStore();
await bootstrap(store);
const schemaHash = await putSchema(store, { type: "string" });
const hash = await store.put(schemaHash, "test value");
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
// Should succeed
const variable = varStore.set("@ucas/test/foo", hash);
expect(variable.name).toBe("@ucas/test/foo");
const retrieved = varStore.get("@ucas/test/foo", schemaHash);
expect(retrieved).not.toBeNull();
expect(retrieved?.name).toBe("@ucas/test/foo");
expect(retrieved?.value).toBe(hash);
varStore.close();
});
test("should accept variable name starting with @", async () => {
store = createMemoryStore();
await bootstrap(store);
const schemaHash = await putSchema(store, { type: "string" });
const hash = await store.put(schemaHash, "config value");
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
// Single segment with @
varStore.set("@config", hash);
const result = varStore.get("@config", schemaHash);
expect(result).not.toBeNull();
expect(result?.name).toBe("@config");
varStore.close();
});
test("should accept complex @ prefix paths", async () => {
store = createMemoryStore();
await bootstrap(store);
const schemaHash = await putSchema(store, { type: "string" });
const hash = await store.put(schemaHash, "test");
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
// Multiple valid patterns
const validNames = [
"@ucas/render/template",
"@system/config",
"@foo.bar/baz",
"@app-1/test_2",
];
for (const name of validNames) {
expect(() => varStore.set(name, hash)).not.toThrow();
const retrieved = varStore.get(name, schemaHash);
expect(retrieved).not.toBeNull();
expect(retrieved?.name).toBe(name);
}
varStore.close();
});
test("should reject @ in non-first segment", async () => {
store = createMemoryStore();
await bootstrap(store);
const schemaHash = await putSchema(store, { type: "string" });
const hash = await store.put(schemaHash, "test");
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
// @ only allowed at start of entire name
const invalidNames = [
"foo/@bar", // @ in second segment
"foo/bar/@baz", // @ in third segment
"foo@bar", // @ within segment (not at start)
];
for (const name of invalidNames) {
expect(() => varStore.set(name, hash)).toThrow(InvalidVariableNameError);
}
varStore.close();
});
test("should reject @ followed by invalid characters", async () => {
store = createMemoryStore();
await bootstrap(store);
const schemaHash = await putSchema(store, { type: "string" });
const hash = await store.put(schemaHash, "test");
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
// @ prefix must still follow segment rules after @
const invalidNames = [
"@", // @ alone is empty segment
"@/foo", // empty after @
"@foo bar", // space not allowed
"@foo$bar", // $ not allowed
];
for (const name of invalidNames) {
expect(() => varStore.set(name, hash)).toThrow(InvalidVariableNameError);
}
varStore.close();
});
test("should still accept all previously valid names", async () => {
store = createMemoryStore();
await bootstrap(store);
const schemaHash = await putSchema(store, { type: "string" });
const hash = await store.put(schemaHash, "test");
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
// All non-@ names should continue to work
const validNames = [
"simple",
"with.dots",
"with-dashes",
"with_underscores",
"path/to/var",
"foo.bar/baz-qux/test_123",
];
for (const name of validNames) {
expect(() => varStore.set(name, hash)).not.toThrow();
}
varStore.close();
});
test("should still reject previously invalid names", async () => {
store = createMemoryStore();
await bootstrap(store);
const schemaHash = await putSchema(store, { type: "string" });
const hash = await store.put(schemaHash, "test");
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
const invalidNames = [
"", // empty
"/leading", // leading slash
"trailing/", // trailing slash
"double//slash", // empty segment
"has space", // space
"has$dollar", // special char
];
for (const name of invalidNames) {
expect(() => varStore.set(name, hash)).toThrow(InvalidVariableNameError);
}
varStore.close();
});
});
+8 -4
View File
@@ -116,6 +116,7 @@ export class VariableStore {
/**
* Validate variable name format
* @ is allowed at the start of the first segment (system-reserved)
*/
private validateName(name: string): void {
// Rule 1: Cannot be empty
@@ -139,9 +140,10 @@ export class VariableStore {
);
}
// Rule 4: Each segment must match [a-zA-Z0-9._-]+ and no empty segments
// Rule 4: Each segment must match [a-zA-Z0-9._-]+ (with @ allowed at start of first segment)
const segments = name.split("/");
for (const segment of segments) {
for (let i = 0; i < segments.length; i++) {
const segment = segments[i] as string;
if (segment === "") {
throw new InvalidVariableNameError(
name,
@@ -150,10 +152,12 @@ export class VariableStore {
}
// Check for invalid characters
if (!/^[a-zA-Z0-9._-]+$/.test(segment)) {
// First segment can start with @, all segments can contain [a-zA-Z0-9._-]
const regex = i === 0 ? /^@?[a-zA-Z0-9._-]+$/ : /^[a-zA-Z0-9._-]+$/;
if (!regex.test(segment)) {
throw new InvalidVariableNameError(
name,
`Segment "${segment}" contains invalid characters (only a-z, A-Z, 0-9, ., _, - allowed)`,
`Segment "${segment}" contains invalid characters (only ${i === 0 ? "@, " : ""}a-z, A-Z, 0-9, ., _, - allowed)`,
);
}
}
@@ -15,7 +15,8 @@ import type { CasNode } from "../src/types.js";
describe("Test Suite 1: Meta-Schema Structure and Self-Validation", () => {
test("1.1: Meta-schema is a valid JSON Schema", async () => {
const store = new MemStore();
const metaHash = await bootstrap(store);
const builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@schema"] ?? "";
const metaNode = store.get(metaHash);
expect(metaNode).not.toBeNull();
@@ -25,7 +26,8 @@ describe("Test Suite 1: Meta-Schema Structure and Self-Validation", () => {
test("1.2: Meta-schema self-validates", async () => {
const store = new MemStore();
const metaHash = await bootstrap(store);
const builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@schema"] ?? "";
const metaNode = store.get(metaHash);
expect(metaNode).not.toBeNull();
@@ -34,7 +36,8 @@ describe("Test Suite 1: Meta-Schema Structure and Self-Validation", () => {
test("1.3: Meta-schema defines all supported keywords", async () => {
const store = new MemStore();
const metaHash = await bootstrap(store);
const builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@schema"] ?? "";
const metaSchema = getSchema(store, metaHash);
expect(metaSchema).not.toBeNull();
@@ -57,7 +60,8 @@ describe("Test Suite 1: Meta-Schema Structure and Self-Validation", () => {
test("1.4: Meta-schema does not include unsupported keywords", async () => {
const store = new MemStore();
const metaHash = await bootstrap(store);
const builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@schema"] ?? "";
const metaSchema = getSchema(store, metaHash);
expect(metaSchema).not.toBeNull();
@@ -74,7 +78,8 @@ describe("Test Suite 1: Meta-Schema Structure and Self-Validation", () => {
test("1.5: Meta-schema node type equals its own hash", async () => {
const store = new MemStore();
const metaHash = await bootstrap(store);
const builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@schema"] ?? "";
const metaNode = store.get(metaHash);
expect(metaNode).not.toBeNull();
@@ -443,7 +448,8 @@ describe("Test Suite 5: Backward Compatibility and Migration", () => {
test("5.1: Bootstrap hash changes (breaking change)", async () => {
// This is a documentation test - the old hash was different
const store = new MemStore();
const newMetaHash = await bootstrap(store);
const builtinSchemas = await bootstrap(store);
const newMetaHash = builtinSchemas["@schema"] ?? "";
// The new hash should be different from the old system metadata hash
// We just verify it's a valid hash format
@@ -585,7 +591,8 @@ describe("Test Suite 6: Integration with Existing Functionality", () => {
describe("Test Suite 7: Meta-Schema Content Validation", () => {
test("7.1: Meta-schema allows recursive schema definitions", async () => {
const store = new MemStore();
const metaHash = await bootstrap(store);
const builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@schema"] ?? "";
const metaSchema = getSchema(store, metaHash);
expect(metaSchema).not.toBeNull();