feat: wrap refs/walk/gc/var with envelope (Phase 3) #80

Merged
xiaoju merged 1 commits from fix/71-refs-walk-var-envelope into main 2026-05-31 15:41:08 +00:00
3 changed files with 51 additions and 88 deletions
@@ -20,15 +20,27 @@ exports[`Phase 1: CAS Core 1.9 verify returns ok for valid node 1`] = `
}
`;
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`] = `
"{
"type": "4N5REDA48XYJP",
"value": []
}"
`;
exports[`Phase 1: CAS Core 1.11 walk shows traversal tree (snapshot) 1`] = `"ERARPP19YJT05"`;
exports[`Phase 1: CAS Core 1.11 walk shows traversal tree (snapshot) 1`] = `
"{
"type": "7124NEATTWYYY",
"value": [
"ERARPP19YJT05"
]
}"
`;
exports[`Phase 2: Schema Validation 2.3 put against non-existent schema hash fails 1`] = `"Schema not found: AAAAAAAAAAAAA"`;
exports[`Phase 3: Variable System 3.1 var set creates variable 1`] = `
{
"type": "E1D32N3GT69Q8",
"type": "AYHQD2YA9G667",
"value": {
"labels": [],
"name": "myapp/config",
@@ -41,7 +53,7 @@ exports[`Phase 3: Variable System 3.1 var set creates variable 1`] = `
exports[`Phase 3: Variable System 3.2 var get returns variable 1`] = `
{
"type": "E1D32N3GT69Q8",
"type": "BVW2SAJ8606EZ",
"value": {
"labels": [],
"name": "myapp/config",
@@ -54,7 +66,7 @@ exports[`Phase 3: Variable System 3.2 var get returns variable 1`] = `
exports[`Phase 3: Variable System 3.3 var list shows all variables 1`] = `
{
"type": "E1D32N3GT69Q8",
"type": "3BY1S4RKNMR0P",
"value": [
{
"labels": [],
@@ -69,7 +81,7 @@ exports[`Phase 3: Variable System 3.3 var list shows all variables 1`] = `
exports[`Phase 3: Variable System 3.4 var list prefix filters by prefix 1`] = `
{
"type": "E1D32N3GT69Q8",
"type": "3BY1S4RKNMR0P",
"value": [
{
"labels": [],
@@ -84,7 +96,7 @@ exports[`Phase 3: Variable System 3.4 var list prefix filters by prefix 1`] = `
exports[`Phase 3: Variable System 3.5 var set upsert updates existing variable 1`] = `
{
"type": "E1D32N3GT69Q8",
"type": "AYHQD2YA9G667",
"value": {
"labels": [],
"name": "myapp/config",
@@ -97,7 +109,7 @@ exports[`Phase 3: Variable System 3.5 var set upsert updates existing variable 1
exports[`Phase 3: Variable System 3.6 var tag adds kv tag and label 1`] = `
{
"type": "E1D32N3GT69Q8",
"type": "BKMJ3DJHTS6VB",
"value": {
"labels": [
"important",
@@ -114,7 +126,7 @@ exports[`Phase 3: Variable System 3.6 var tag adds kv tag and label 1`] = `
exports[`Phase 3: Variable System 3.7 var list --tag env:prod filters by kv tag 1`] = `
{
"type": "E1D32N3GT69Q8",
"type": "3BY1S4RKNMR0P",
"value": [
{
"labels": [
@@ -133,7 +145,7 @@ exports[`Phase 3: Variable System 3.7 var list --tag env:prod filters by kv tag
exports[`Phase 3: Variable System 3.8 var list --tag important filters by label 1`] = `
{
"type": "E1D32N3GT69Q8",
"type": "3BY1S4RKNMR0P",
"value": [
{
"labels": [
@@ -152,7 +164,7 @@ exports[`Phase 3: Variable System 3.8 var list --tag important filters by label
exports[`Phase 3: Variable System 3.9 var tag remove deletes label 1`] = `
{
"type": "E1D32N3GT69Q8",
"type": "BKMJ3DJHTS6VB",
"value": {
"labels": [],
"name": "myapp/config",
@@ -167,7 +179,7 @@ exports[`Phase 3: Variable System 3.9 var tag remove deletes label 1`] = `
exports[`Phase 3: Variable System 3.10 var delete removes variable 1`] = `
{
"type": "E1D32N3GT69Q8",
"type": "ASWN8JEGAG7AP",
"value": [
{
"labels": [],
+1 -1
View File
@@ -420,7 +420,7 @@ describe("Phase 6: GC", () => {
const { exitCode, stdout } = await runCli(["gc"]);
expect(exitCode).toBe(0);
// Assert structural shape only — exact counts depend on phase history
const result = JSON.parse(stdout) as Record<string, unknown>;
const result = envValue(stdout) as Record<string, unknown>;
expect(typeof result.total).toBe("number");
expect(typeof result.reachable).toBe("number");
expect(typeof result.collected).toBe("number");
+26 -75
View File
@@ -3,7 +3,7 @@
import { existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join, resolve } from "node:path";
import type { Hash, JSONSchema, Store, VariableStore } from "@uncaged/json-cas";
import type { Hash, Store, VariableStore } from "@uncaged/json-cas";
import {
bootstrap,
CasNodeNotFoundError,
@@ -13,7 +13,6 @@ import {
getSchema,
InvalidTagFormatError,
InvalidVariableNameError,
putSchema,
refs,
renderAsync,
renderDirect,
@@ -145,55 +144,6 @@ async function resolveTypeHash(typeHashOrAlias: string): Promise<Hash> {
return typeHashOrAlias;
}
/**
* Get the Variable schema's CAS hash
* This is the type hash used in JSON envelopes
*/
async function getVariableSchemaHash(): Promise<Hash> {
const store = await openStore();
// Define the Variable JSON Schema (updated for new model with composite key)
const variableSchema: JSONSchema = {
title: "Variable",
type: "object",
properties: {
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: [
"name",
"schema",
"value",
"created",
"updated",
"tags",
"labels",
],
};
// Compute hash or retrieve from store
const hash = await putSchema(store, variableSchema);
return hash;
}
/**
* Wrap Variable output in JSON envelope
*/
async function wrapVariableEnvelope(
variable: unknown,
): Promise<{ type: Hash; value: unknown }> {
const typeHash = await getVariableSchemaHash();
return {
type: typeHash,
value: variable,
};
}
/**
* Parse tag/label arguments
* Returns: { tags: Record<string, string>, labels: string[], deleteNames: string[] }
@@ -296,9 +246,7 @@ async function cmdRefs(args: string[]): Promise<void> {
const node = store.get(hash);
if (node === null) die(`Node not found: ${hash}`);
const refHashes = refs(store, node);
for (const r of refHashes) {
console.log(r);
}
out(await wrapEnvelope(store, "@output/refs", refHashes));
}
async function cmdWalk(args: string[]): Promise<void> {
@@ -314,15 +262,16 @@ async function cmdWalk(args: string[]): Promise<void> {
});
const printed = new Set<Hash>();
const lines: string[] = [];
function printNode(h: Hash, prefix: string, isLast: boolean): void {
const connector = prefix === "" ? "" : isLast ? "└── " : "├── ";
if (printed.has(h)) {
console.log(`${prefix}${connector}${h} (seen)`);
lines.push(`${prefix}${connector}${h} (seen)`);
return;
}
printed.add(h);
console.log(`${prefix}${connector}${h}`);
lines.push(`${prefix}${connector}${h}`);
const kids = childMap.get(h) ?? [];
const childPrefix =
@@ -333,10 +282,13 @@ async function cmdWalk(args: string[]): Promise<void> {
}
printNode(hash, "", true);
out(await wrapEnvelope(store, "@output/walk", lines.join("\n")));
} else {
const hashes: Hash[] = [];
walk(store, hash, (h) => {
console.log(h);
hashes.push(h);
});
out(await wrapEnvelope(store, "@output/walk", hashes));
}
}
@@ -471,7 +423,8 @@ async function cmdVarSet(args: string[]): Promise<void> {
die("Usage: json-cas var set <name> <hash> [--tag <tag>...]");
}
const varStore = await openVarStore();
const store = await openStore();
const varStore = createVariableStore(resolve(varDbPath), store);
try {
// Parse tags/labels from --tag flags
@@ -498,8 +451,7 @@ async function cmdVarSet(args: string[]): Promise<void> {
: undefined;
const variable = varStore.set(name, value, options);
const envelope = await wrapVariableEnvelope(variable);
out(envelope);
out(await wrapEnvelope(store, "@output/var-set", variable));
} catch (e) {
if (
e instanceof InvalidVariableNameError ||
@@ -522,15 +474,15 @@ async function cmdVarGet(args: string[]): Promise<void> {
die("Usage: json-cas var get <name> --schema <hash>");
}
const varStore = await openVarStore();
const store = await openStore();
const varStore = createVariableStore(resolve(varDbPath), store);
try {
const variable = varStore.get(name, schema);
if (variable === null) {
die(`Error: Variable not found: name=${name}, schema=${schema}`);
}
const envelope = await wrapVariableEnvelope(variable);
out(envelope);
out(await wrapEnvelope(store, "@output/var-get", variable));
} finally {
varStore.close();
}
@@ -544,19 +496,18 @@ async function cmdVarDelete(args: string[]): Promise<void> {
die("Usage: json-cas var delete <name> [--schema <hash>]");
}
const varStore = await openVarStore();
const store = await openStore();
const varStore = createVariableStore(resolve(varDbPath), store);
try {
if (schema !== undefined) {
// Precise deletion: remove specific (name, schema) variant
const variable = varStore.remove(name, schema);
const envelope = await wrapVariableEnvelope(variable);
out(envelope);
out(await wrapEnvelope(store, "@output/var-delete", variable));
} else {
// Batch deletion: remove all variants for this name
const variables = varStore.remove(name);
const envelope = await wrapVariableEnvelope(variables);
out(envelope);
out(await wrapEnvelope(store, "@output/var-delete", variables));
}
} catch (e) {
if (e instanceof VariableNotFoundError) {
@@ -581,7 +532,8 @@ async function cmdVarTag(args: string[]): Promise<void> {
die("Usage: json-cas var tag <name> --schema <hash> <operations...>");
}
const varStore = await openVarStore();
const store = await openStore();
const varStore = createVariableStore(resolve(varDbPath), store);
try {
const { tags, labels, deleteNames } = parseTagsLabels(tagArgs);
@@ -592,8 +544,7 @@ async function cmdVarTag(args: string[]): Promise<void> {
delete: deleteNames.length > 0 ? deleteNames : undefined,
});
const envelope = await wrapVariableEnvelope(variable);
out(envelope);
out(await wrapEnvelope(store, "@output/var-tag", variable));
} catch (e) {
if (
e instanceof VariableNotFoundError ||
@@ -613,7 +564,8 @@ async function cmdVarList(args: string[]): Promise<void> {
const schema = flags.schema as string | undefined;
const tagFlags = flags.tag;
const varStore = await openVarStore();
const store = await openStore();
const varStore = createVariableStore(resolve(varDbPath), store);
try {
// Parse tags/labels from --tag flags
@@ -635,8 +587,7 @@ async function cmdVarList(args: string[]): Promise<void> {
tags: Object.keys(tags).length > 0 ? tags : undefined,
labels: labels.length > 0 ? labels : undefined,
});
const envelope = await wrapVariableEnvelope(variables);
out(envelope);
out(await wrapEnvelope(store, "@output/var-list", variables));
} catch (e) {
if (e instanceof InvalidVariableNameError) {
die(`Error: ${e.message}`);
@@ -817,7 +768,7 @@ async function cmdGc(_args: string[]): Promise<void> {
try {
const stats = gc(store, varStore);
out(stats);
out(await wrapEnvelope(store, "@output/gc", stats));
} finally {
varStore.close();
}