feat: wrap put/get/has/hash/verify/list with {type,value} envelope (Phase 2)

Fixes #70
This commit is contained in:
2026-05-31 15:29:49 +00:00
parent 11bb6b3e32
commit d328fbe6e4
4 changed files with 106 additions and 94 deletions
@@ -2,15 +2,23 @@
exports[`Phase 1: CAS Core 1.6 get returns node JSON (snapshot) 1`] = ` exports[`Phase 1: CAS Core 1.6 get returns node JSON (snapshot) 1`] = `
{ {
"payload": { "type": "ASE7K6A0HG8W9",
"age": 30, "value": {
"name": "Alice", "payload": {
"age": 30,
"name": "Alice",
},
"type": "7XX5H51CVD9H0",
}, },
"type": "7XX5H51CVD9H0",
} }
`; `;
exports[`Phase 1: CAS Core 1.9 verify returns ok for valid node 1`] = `"ok"`; exports[`Phase 1: CAS Core 1.9 verify returns ok for valid node 1`] = `
{
"type": "8E2M8H30BHXS8",
"value": "ok",
}
`;
exports[`Phase 1: CAS Core 1.10 refs lists direct references (snapshot) 1`] = `""`; exports[`Phase 1: CAS Core 1.10 refs lists direct references (snapshot) 1`] = `""`;
@@ -216,16 +224,16 @@ exports[`Phase 7: Edge Cases 7.5 no subcommand shows help text 1`] = `
"Usage: json-cas [--store <path>] [--json] <command> [args] "Usage: json-cas [--store <path>] [--json] <command> [args]
Commands: Commands:
put <type-hash> <file.json> Store node, print hash put <type-hash> <file.json> Store node, print { type, value } envelope (value=hash)
get <hash> Print node as JSON get <hash> Print node as { type, value } envelope
has <hash> Print true/false has <hash> Print { type, value } envelope (value=boolean)
verify <hash> Verify integrity + schema, print ok/corrupted/invalid verify <hash> Verify integrity + schema → { type, value } (value=ok/corrupted/invalid)
refs <hash> List direct cas_ref edges refs <hash> List direct cas_ref edges
walk <hash> [--format tree] Recursive traversal walk <hash> [--format tree] Recursive traversal
hash <type-hash> <file.json> Compute hash without storing (dry run) hash <type-hash> <file.json> Compute hash without storing → { type, value } envelope
render <hash> [options] Render node as YAML with resolution decay render <hash> [options] Render node as YAML with resolution decay
render --pipe/-p [options] Render { type, value } from stdin render --pipe/-p [options] Render { type, value } from stdin
list --type <hash-or-alias> List all hashes for a given type list --type <hash-or-alias> List hashes for a type → { type, value } envelope (value=string[])
var set <name> <hash> [--tag <tag>...] Create/update a variable var set <name> <hash> [--tag <tag>...] Create/update a variable
var get <name> --schema <hash> Get a variable by name + schema var get <name> --schema <hash> Get a variable by name + schema
var delete <name> [--schema <hash>] Delete variable(s) var delete <name> [--schema <hash>] Delete variable(s)
+47 -47
View File
@@ -15,6 +15,11 @@ import { createFsStore, openStore as openFsStore } from "@uncaged/json-cas-fs";
const pkgPath = resolve(import.meta.dir, "../package.json"); const pkgPath = resolve(import.meta.dir, "../package.json");
const entrypoint = resolve(import.meta.dir, "index.ts"); const entrypoint = resolve(import.meta.dir, "index.ts");
/** Extract the `value` field from a { type, value } envelope JSON string. */
function envValue(json: string): unknown {
return (JSON.parse(json.trim()) as { value: unknown }).value;
}
/** /**
* Register a schema directly via the library (CLI schema put was removed). * Register a schema directly via the library (CLI schema put was removed).
* Returns the type hash. * Returns the type hash.
@@ -168,8 +173,8 @@ describe("@ Alias Resolution - put", () => {
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(stderr).toBe(""); expect(stderr).toBe("");
// Should output a valid hash (13 chars) // Should output an envelope whose value is a valid hash (13 chars)
expect(stdout).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
}); });
test("ucas put @number <file> should resolve alias", async () => { test("ucas put @number <file> should resolve alias", async () => {
@@ -185,7 +190,7 @@ describe("@ Alias Resolution - put", () => {
); );
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(stdout).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
}); });
test("ucas put @object <file> should resolve alias", async () => { test("ucas put @object <file> should resolve alias", async () => {
@@ -201,7 +206,7 @@ describe("@ Alias Resolution - put", () => {
); );
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(stdout).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
}); });
test("ucas put @invalid <file> should fail", async () => { test("ucas put @invalid <file> should fail", async () => {
@@ -236,7 +241,7 @@ describe("@ Alias Resolution - hash", () => {
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(stderr).toBe(""); expect(stderr).toBe("");
expect(stdout).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
}); });
}); });
@@ -313,10 +318,10 @@ describe("Issue #50: Schema Validation in put", () => {
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(stderr).toBe(""); expect(stderr).toBe("");
expect(stdout.trim()).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
// Verify node was stored // Verify node was stored
const hash = stdout.trim(); const hash = envValue(stdout) as string;
const { exitCode: hasExitCode } = await runCli(["has", hash], tmpStore); const { exitCode: hasExitCode } = await runCli(["has", hash], tmpStore);
expect(hasExitCode).toBe(0); expect(hasExitCode).toBe(0);
} finally { } finally {
@@ -355,7 +360,7 @@ describe("Issue #50: Schema Validation in put", () => {
); );
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(stdout.trim()).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
} finally { } finally {
rmSync(tmpStore, { recursive: true, force: true }); rmSync(tmpStore, { recursive: true, force: true });
} }
@@ -403,7 +408,7 @@ describe("Issue #50: Schema Validation in put", () => {
); );
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(stdout.trim()).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
} finally { } finally {
rmSync(tmpStore, { recursive: true, force: true }); rmSync(tmpStore, { recursive: true, force: true });
} }
@@ -423,7 +428,7 @@ describe("Issue #50: Schema Validation in put", () => {
); );
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(stdout.trim()).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
} finally { } finally {
rmSync(tmpStore, { recursive: true, force: true }); rmSync(tmpStore, { recursive: true, force: true });
} }
@@ -469,7 +474,7 @@ describe("Issue #50: Schema Validation in put", () => {
["has", "0000000000000"], ["has", "0000000000000"],
tmpStore, tmpStore,
); );
expect(hasOutput.trim()).toBe("false"); expect(envValue(hasOutput)).toBe(false);
} finally { } finally {
rmSync(tmpStore, { recursive: true, force: true }); rmSync(tmpStore, { recursive: true, force: true });
} }
@@ -692,7 +697,7 @@ describe("Issue #50: Schema Validation in put", () => {
); );
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(stdout.trim()).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
} finally { } finally {
rmSync(tmpStore, { recursive: true, force: true }); rmSync(tmpStore, { recursive: true, force: true });
} }
@@ -850,33 +855,30 @@ describe("Suite 6: CLI Integration with Templates", () => {
// Create node // Create node
const nodeFile = join(tmpStore, "node.json"); const nodeFile = join(tmpStore, "node.json");
writeFileSync(nodeFile, JSON.stringify({ name: "Alice" })); writeFileSync(nodeFile, JSON.stringify({ name: "Alice" }));
const { stdout: nodeHash } = await runCli( const { stdout: nodeOut } = await runCli(
["put", schemaHash.trim(), nodeFile], ["put", schemaHash.trim(), nodeFile],
tmpStore, tmpStore,
); );
const nodeHash = envValue(nodeOut) as string;
// Create template file (JSON-encoded string) // Create template file (JSON-encoded string)
const templateFile = join(tmpStore, "template.json"); const templateFile = join(tmpStore, "template.json");
writeFileSync(templateFile, JSON.stringify("Hello {{ payload.name }}!")); writeFileSync(templateFile, JSON.stringify("Hello {{ payload.name }}!"));
const { stdout: tmplHash } = await runCli( const { stdout: tmplOut } = await runCli(
["put", "@string", templateFile], ["put", "@string", templateFile],
tmpStore, tmpStore,
); );
const tmplHash = envValue(tmplOut) as string;
// Register template // Register template
await runCli( await runCli(
[ ["var", "set", `@ucas/template/text/${schemaHash.trim()}`, tmplHash],
"var",
"set",
`@ucas/template/text/${schemaHash.trim()}`,
tmplHash.trim(),
],
tmpStore, tmpStore,
); );
// Render with template // Render with template
const { stdout: output, exitCode } = await runCli( const { stdout: output, exitCode } = await runCli(
["render", nodeHash.trim()], ["render", nodeHash],
tmpStore, tmpStore,
); );
@@ -911,21 +913,23 @@ describe("Suite 6: CLI Integration with Templates", () => {
// Create child node // Create child node
const childFile = join(tmpStore, "child.json"); const childFile = join(tmpStore, "child.json");
writeFileSync(childFile, JSON.stringify({ value: "child", child: null })); writeFileSync(childFile, JSON.stringify({ value: "child", child: null }));
const { stdout: childHash } = await runCli( const { stdout: childOut } = await runCli(
["put", schemaHash.trim(), childFile], ["put", schemaHash.trim(), childFile],
tmpStore, tmpStore,
); );
const childHash = envValue(childOut) as string;
// Create parent node // Create parent node
const parentFile = join(tmpStore, "parent.json"); const parentFile = join(tmpStore, "parent.json");
writeFileSync( writeFileSync(
parentFile, parentFile,
JSON.stringify({ value: "parent", child: childHash.trim() }), JSON.stringify({ value: "parent", child: childHash }),
); );
const { stdout: parentHash } = await runCli( const { stdout: parentOut } = await runCli(
["put", schemaHash.trim(), parentFile], ["put", schemaHash.trim(), parentFile],
tmpStore, tmpStore,
); );
const parentHash = envValue(parentOut) as string;
// Create template showing resolution (JSON-encoded string) // Create template showing resolution (JSON-encoded string)
const templateFile = join(tmpStore, "template.json"); const templateFile = join(tmpStore, "template.json");
@@ -933,25 +937,21 @@ describe("Suite 6: CLI Integration with Templates", () => {
templateFile, templateFile,
JSON.stringify("{{ payload.value }}(res={{ resolution }})"), JSON.stringify("{{ payload.value }}(res={{ resolution }})"),
); );
const { stdout: tmplHash } = await runCli( const { stdout: tmplOut } = await runCli(
["put", "@string", templateFile], ["put", "@string", templateFile],
tmpStore, tmpStore,
); );
const tmplHash = envValue(tmplOut) as string;
// Register template // Register template
await runCli( await runCli(
[ ["var", "set", `@ucas/template/text/${schemaHash.trim()}`, tmplHash],
"var",
"set",
`@ucas/template/text/${schemaHash.trim()}`,
tmplHash.trim(),
],
tmpStore, tmpStore,
); );
// Render with custom decay // Render with custom decay
const { stdout: output, exitCode } = await runCli( const { stdout: output, exitCode } = await runCli(
["render", parentHash.trim(), "--decay", "0.7"], ["render", parentHash, "--decay", "0.7"],
tmpStore, tmpStore,
); );
@@ -979,10 +979,11 @@ describe("Suite 6: CLI Integration with Templates", () => {
const nodeFile = join(tmpStore, "node.json"); const nodeFile = join(tmpStore, "node.json");
writeFileSync(nodeFile, JSON.stringify({ name: "Bob" })); writeFileSync(nodeFile, JSON.stringify({ name: "Bob" }));
const { stdout: nodeHash } = await runCli( const { stdout: nodeOut } = await runCli(
["put", schemaHash.trim(), nodeFile], ["put", schemaHash.trim(), nodeFile],
tmpStore, tmpStore,
); );
const nodeHash = envValue(nodeOut) as string;
// Create template (JSON-encoded string) // Create template (JSON-encoded string)
const templateFile = join(tmpStore, "template.json"); const templateFile = join(tmpStore, "template.json");
@@ -990,25 +991,21 @@ describe("Suite 6: CLI Integration with Templates", () => {
templateFile, templateFile,
JSON.stringify("Greetings {{ payload.name }}!"), JSON.stringify("Greetings {{ payload.name }}!"),
); );
const { stdout: tmplHash } = await runCli( const { stdout: tmplOut } = await runCli(
["put", "@string", templateFile], ["put", "@string", templateFile],
tmpStore, tmpStore,
); );
const tmplHash = envValue(tmplOut) as string;
await runCli( await runCli(
[ ["var", "set", `@ucas/template/text/${schemaHash.trim()}`, tmplHash],
"var",
"set",
`@ucas/template/text/${schemaHash.trim()}`,
tmplHash.trim(),
],
tmpStore, tmpStore,
); );
const { stdout: output, exitCode } = await runCli( const { stdout: output, exitCode } = await runCli(
[ [
"render", "render",
nodeHash.trim(), nodeHash,
"--resolution", "--resolution",
"0.8", "0.8",
"--decay", "--decay",
@@ -1043,14 +1040,15 @@ describe("Suite 6: CLI Integration with Templates", () => {
const nodeFile = join(tmpStore, "node.json"); const nodeFile = join(tmpStore, "node.json");
writeFileSync(nodeFile, JSON.stringify({ name: "Charlie" })); writeFileSync(nodeFile, JSON.stringify({ name: "Charlie" }));
const { stdout: nodeHash } = await runCli( const { stdout: nodeOut } = await runCli(
["put", schemaHash.trim(), nodeFile], ["put", schemaHash.trim(), nodeFile],
tmpStore, tmpStore,
); );
const nodeHash = envValue(nodeOut) as string;
// No template registered - should fall back to YAML // No template registered - should fall back to YAML
const { stdout: output, exitCode } = await runCli( const { stdout: output, exitCode } = await runCli(
["render", nodeHash.trim()], ["render", nodeHash],
tmpStore, tmpStore,
); );
@@ -1079,13 +1077,14 @@ describe("Suite 6: CLI Integration with Templates", () => {
const nodeFile = join(tmpStore, "node.json"); const nodeFile = join(tmpStore, "node.json");
writeFileSync(nodeFile, JSON.stringify({ name: "Test" })); writeFileSync(nodeFile, JSON.stringify({ name: "Test" }));
const { stdout: nodeHash } = await runCli( const { stdout: nodeOut } = await runCli(
["put", schemaHash.trim(), nodeFile], ["put", schemaHash.trim(), nodeFile],
tmpStore, tmpStore,
); );
const nodeHash = envValue(nodeOut) as string;
const { exitCode, stderr } = await runCli( const { exitCode, stderr } = await runCli(
["render", nodeHash.trim(), "--decay", "1.5"], ["render", nodeHash, "--decay", "1.5"],
tmpStore, tmpStore,
); );
@@ -1126,14 +1125,15 @@ describe("Suite 6: CLI Integration with Templates", () => {
// Create and store a simple string node // Create and store a simple string node
const nodeFile = join(tmpStore, "test.json"); const nodeFile = join(tmpStore, "test.json");
writeFileSync(nodeFile, JSON.stringify("hello world")); writeFileSync(nodeFile, JSON.stringify("hello world"));
const { stdout: nodeHash } = await runCli( const { stdout: nodeOut } = await runCli(
["put", stringType, nodeFile], ["put", stringType, nodeFile],
tmpStore, tmpStore,
); );
const nodeHash = envValue(nodeOut) as string;
// Render the valid hash // Render the valid hash
const { exitCode, stdout, stderr } = await runCli( const { exitCode, stdout, stderr } = await runCli(
["render", nodeHash.trim()], ["render", nodeHash],
tmpStore, tmpStore,
); );
+22 -20
View File
@@ -54,6 +54,11 @@ function stripVolatile(json: string): unknown {
return strip(JSON.parse(json)); return strip(JSON.parse(json));
} }
/** Extract the `value` field from a { type, value } envelope JSON string. */
function envValue(json: string): unknown {
return (JSON.parse(json) as { value: unknown }).value;
}
// ---- Phase 1: CAS Core ---- // ---- Phase 1: CAS Core ----
describe("Phase 1: CAS Core", () => { describe("Phase 1: CAS Core", () => {
@@ -88,8 +93,8 @@ describe("Phase 1: CAS Core", () => {
writeFileSync(nodeFile, JSON.stringify({ name: "Alice", age: 30 })); writeFileSync(nodeFile, JSON.stringify({ name: "Alice", age: 30 }));
const { stdout, exitCode } = await runCli(["put", typeHash, nodeFile]); const { stdout, exitCode } = await runCli(["put", typeHash, nodeFile]);
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(stdout).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
nodeHash = stdout; nodeHash = envValue(stdout) as string;
}); });
test("1.6 get returns node JSON (snapshot)", async () => { test("1.6 get returns node JSON (snapshot)", async () => {
@@ -101,19 +106,19 @@ describe("Phase 1: CAS Core", () => {
test("1.7 has returns true for existing node", async () => { test("1.7 has returns true for existing node", async () => {
const { stdout, exitCode } = await runCli(["has", nodeHash]); const { stdout, exitCode } = await runCli(["has", nodeHash]);
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(stdout).toBe("true"); expect(envValue(stdout)).toBe(true);
}); });
test("1.8 has returns false for non-existing hash", async () => { test("1.8 has returns false for non-existing hash", async () => {
const { stdout, exitCode } = await runCli(["has", "AAAAAAAAAAAAA"]); const { stdout, exitCode } = await runCli(["has", "AAAAAAAAAAAAA"]);
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(stdout).toBe("false"); expect(envValue(stdout)).toBe(false);
}); });
test("1.9 verify returns ok for valid node", async () => { test("1.9 verify returns ok for valid node", async () => {
const { stdout, exitCode } = await runCli(["verify", nodeHash]); const { stdout, exitCode } = await runCli(["verify", nodeHash]);
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(stdout).toMatchSnapshot(); expect(stripVolatile(stdout)).toMatchSnapshot();
}); });
test("1.10 refs lists direct references (snapshot)", async () => { test("1.10 refs lists direct references (snapshot)", async () => {
@@ -132,13 +137,13 @@ describe("Phase 1: CAS Core", () => {
const nodeFile = join(tmpStore, "test-node.json"); const nodeFile = join(tmpStore, "test-node.json");
const { stdout, exitCode } = await runCli(["hash", typeHash, nodeFile]); const { stdout, exitCode } = await runCli(["hash", typeHash, nodeFile]);
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(stdout).toBe(nodeHash); expect(envValue(stdout)).toBe(nodeHash);
}); });
test("1.13 list --type returns nodes of that type", async () => { test("1.13 list --type returns nodes of that type", async () => {
const { stdout, exitCode } = await runCli(["list", "--type", typeHash]); const { stdout, exitCode } = await runCli(["list", "--type", typeHash]);
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(stdout).toContain(nodeHash); expect(envValue(stdout)).toContain(nodeHash);
}); });
}); });
@@ -163,7 +168,7 @@ describe("Phase 2: Schema Validation", () => {
test("2.2 verify on valid node returns ok (hash + schema)", async () => { test("2.2 verify on valid node returns ok (hash + schema)", async () => {
const { stdout, exitCode } = await runCli(["verify", nodeHash]); const { stdout, exitCode } = await runCli(["verify", nodeHash]);
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(stdout).toBe("ok"); expect(envValue(stdout)).toBe("ok");
}); });
test("2.3 put against non-existent schema hash fails", async () => { test("2.3 put against non-existent schema hash fails", async () => {
@@ -222,12 +227,13 @@ describe("Phase 3: Variable System", () => {
test("3.5 var set upsert updates existing variable", async () => { test("3.5 var set upsert updates existing variable", async () => {
const node2File = join(tmpStore, "node2.json"); const node2File = join(tmpStore, "node2.json");
writeFileSync(node2File, JSON.stringify({ name: "Bob", age: 25 })); writeFileSync(node2File, JSON.stringify({ name: "Bob", age: 25 }));
const { stdout: node2Hash } = await runCli(["put", typeHash, node2File]); const { stdout: node2Out } = await runCli(["put", typeHash, node2File]);
const node2Hash = envValue(node2Out) as string;
const { exitCode, stdout } = await runCli([ const { exitCode, stdout } = await runCli([
"var", "var",
"set", "set",
"myapp/config", "myapp/config",
node2Hash.trim(), node2Hash,
]); ]);
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(stripVolatile(stdout)).toMatchSnapshot(); expect(stripVolatile(stdout)).toMatchSnapshot();
@@ -405,7 +411,7 @@ describe("Phase 6: GC", () => {
const gcNodeFile = join(tmpStore, "gc-node.json"); const gcNodeFile = join(tmpStore, "gc-node.json");
writeFileSync(gcNodeFile, JSON.stringify({ name: "GcAlice", age: 30 })); writeFileSync(gcNodeFile, JSON.stringify({ name: "GcAlice", age: 30 }));
const { stdout } = await runCli(["put", typeHash, gcNodeFile]); const { stdout } = await runCli(["put", typeHash, gcNodeFile]);
gcNodeHash = stdout.trim(); gcNodeHash = envValue(stdout) as string;
// Set a var referencing this node so it survives GC during Phase 6 // Set a var referencing this node so it survives GC during Phase 6
await runCli(["var", "set", "gc-test/ref", gcNodeHash]); await runCli(["var", "set", "gc-test/ref", gcNodeHash]);
}); });
@@ -428,25 +434,21 @@ describe("Phase 6: GC", () => {
const { exitCode } = await runCli(["gc"]); const { exitCode } = await runCli(["gc"]);
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const { stdout } = await runCli(["has", gcNodeHash]); const { stdout } = await runCli(["has", gcNodeHash]);
expect(stdout).toBe("true"); expect(envValue(stdout)).toBe(true);
}); });
test("6.3 gc reclaims orphan node", async () => { test("6.3 gc reclaims orphan node", async () => {
const orphanFile = join(tmpStore, "orphan.json"); const orphanFile = join(tmpStore, "orphan.json");
writeFileSync(orphanFile, JSON.stringify({ name: "Orphan", age: 99 })); writeFileSync(orphanFile, JSON.stringify({ name: "Orphan", age: 99 }));
const { stdout: orphanHashRaw } = await runCli([ const { stdout: orphanOut } = await runCli(["put", typeHash, orphanFile]);
"put", const orphanHash = envValue(orphanOut) as string;
typeHash,
orphanFile,
]);
const orphanHash = orphanHashRaw.trim();
const { stdout: beforeGc } = await runCli(["has", orphanHash]); const { stdout: beforeGc } = await runCli(["has", orphanHash]);
expect(beforeGc).toBe("true"); expect(envValue(beforeGc)).toBe(true);
await runCli(["gc"]); await runCli(["gc"]);
const { stdout: afterGc } = await runCli(["has", orphanHash]); const { stdout: afterGc } = await runCli(["has", orphanHash]);
expect(afterGc).toBe("false"); expect(envValue(afterGc)).toBe(false);
}); });
}); });
+18 -16
View File
@@ -22,6 +22,7 @@ import {
validate, validate,
verify, verify,
walk, walk,
wrapEnvelope,
} from "@uncaged/json-cas"; } from "@uncaged/json-cas";
import { openStore as openFsStore } from "@uncaged/json-cas-fs"; import { openStore as openFsStore } from "@uncaged/json-cas-fs";
@@ -253,7 +254,7 @@ async function cmdPut(args: string[]): Promise<void> {
} }
const hash = await store.put(typeHash, payload); const hash = await store.put(typeHash, payload);
console.log(hash); out(await wrapEnvelope(store, "@output/put", hash));
} }
async function cmdGet(args: string[]): Promise<void> { async function cmdGet(args: string[]): Promise<void> {
@@ -262,14 +263,14 @@ async function cmdGet(args: string[]): Promise<void> {
const store = await openStore(); const store = await openStore();
const node = store.get(hash); const node = store.get(hash);
if (node === null) die(`Node not found: ${hash}`); if (node === null) die(`Node not found: ${hash}`);
out(node); out(await wrapEnvelope(store, "@output/get", node));
} }
async function cmdHas(args: string[]): Promise<void> { async function cmdHas(args: string[]): Promise<void> {
const hash = args[0]; const hash = args[0];
if (!hash) die("Usage: json-cas has <hash>"); if (!hash) die("Usage: json-cas has <hash>");
const store = await openStore(); const store = await openStore();
console.log(String(store.has(hash))); out(await wrapEnvelope(store, "@output/has", store.has(hash)));
} }
async function cmdVerify(args: string[]): Promise<void> { async function cmdVerify(args: string[]): Promise<void> {
@@ -279,12 +280,13 @@ async function cmdVerify(args: string[]): Promise<void> {
const node = store.get(hash); const node = store.get(hash);
if (node === null) die(`Node not found: ${hash}`); if (node === null) die(`Node not found: ${hash}`);
const ok = await verify(hash, node); const ok = await verify(hash, node);
let status: string;
if (!ok) { if (!ok) {
console.log("corrupted"); status = "corrupted";
} else { } else {
const valid = validate(store, node); status = validate(store, node) ? "ok" : "invalid";
console.log(valid ? "ok" : "invalid");
} }
out(await wrapEnvelope(store, "@output/verify", status));
} }
async function cmdRefs(args: string[]): Promise<void> { async function cmdRefs(args: string[]): Promise<void> {
@@ -346,7 +348,8 @@ async function cmdHash(args: string[]): Promise<void> {
const typeHash = await resolveTypeHash(typeHashOrAlias); const typeHash = await resolveTypeHash(typeHashOrAlias);
const payload = readJsonFile(file); const payload = readJsonFile(file);
const hash = await computeHash(typeHash, payload); const hash = await computeHash(typeHash, payload);
console.log(hash); const store = await openStore();
out(await wrapEnvelope(store, "@output/hash", hash));
} }
async function cmdRender(args: string[]): Promise<void> { async function cmdRender(args: string[]): Promise<void> {
@@ -826,9 +829,8 @@ async function cmdList(_args: string[]): Promise<void> {
die("Usage: json-cas list --type <hash-or-alias>"); die("Usage: json-cas list --type <hash-or-alias>");
const typeHash = await resolveTypeHash(typeFlag); const typeHash = await resolveTypeHash(typeFlag);
const store = await openStore(); const store = await openStore();
for (const hash of store.listByType(typeHash)) { const hashes = Array.from(store.listByType(typeHash));
console.log(hash); out(await wrapEnvelope(store, "@output/list", hashes));
}
} }
function printUsage(): void { function printUsage(): void {
@@ -836,16 +838,16 @@ function printUsage(): void {
Usage: json-cas [--store <path>] [--json] <command> [args] Usage: json-cas [--store <path>] [--json] <command> [args]
Commands: Commands:
put <type-hash> <file.json> Store node, print hash put <type-hash> <file.json> Store node, print { type, value } envelope (value=hash)
get <hash> Print node as JSON get <hash> Print node as { type, value } envelope
has <hash> Print true/false has <hash> Print { type, value } envelope (value=boolean)
verify <hash> Verify integrity + schema, print ok/corrupted/invalid verify <hash> Verify integrity + schema → { type, value } (value=ok/corrupted/invalid)
refs <hash> List direct cas_ref edges refs <hash> List direct cas_ref edges
walk <hash> [--format tree] Recursive traversal walk <hash> [--format tree] Recursive traversal
hash <type-hash> <file.json> Compute hash without storing (dry run) hash <type-hash> <file.json> Compute hash without storing → { type, value } envelope
render <hash> [options] Render node as YAML with resolution decay render <hash> [options] Render node as YAML with resolution decay
render --pipe/-p [options] Render { type, value } from stdin render --pipe/-p [options] Render { type, value } from stdin
list --type <hash-or-alias> List all hashes for a given type list --type <hash-or-alias> List hashes for a type → { type, value } envelope (value=string[])
var set <name> <hash> [--tag <tag>...] Create/update a variable var set <name> <hash> [--tag <tag>...] Create/update a variable
var get <name> --schema <hash> Get a variable by name + schema var get <name> --schema <hash> Get a variable by name + schema
var delete <name> [--schema <hash>] Delete variable(s) var delete <name> [--schema <hash>] Delete variable(s)