feat: bootstrap writes to varStore #21
+66
-28
@@ -156,31 +156,37 @@ async function readStdinJson(): Promise<unknown> {
|
|||||||
/**
|
/**
|
||||||
* Open the filesystem-backed CAS store.
|
* Open the filesystem-backed CAS store.
|
||||||
* Automatically creates directory and bootstraps if needed.
|
* Automatically creates directory and bootstraps if needed.
|
||||||
|
* If a varStore is provided, builtin schema aliases are written to it during bootstrap.
|
||||||
*/
|
*/
|
||||||
async function openStore(): Promise<Store> {
|
async function openStore(varStore?: VariableStore): Promise<Store> {
|
||||||
const fullPath = resolve(storePath);
|
const fullPath = resolve(storePath);
|
||||||
return await openFsStore(fullPath);
|
return await openFsStore(fullPath, varStore);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openVarStore(): Promise<VariableStore> {
|
async function openVarStore(): Promise<VariableStore> {
|
||||||
const store = await openStore();
|
const store = await openStore();
|
||||||
return createVariableStore(resolve(varDbPath), store);
|
const varStore = createVariableStore(resolve(varDbPath), store);
|
||||||
|
// Populate varStore with builtin schema aliases (idempotent).
|
||||||
|
await bootstrap(store, varStore);
|
||||||
|
return varStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a type-hash, handling @ aliases
|
* Resolve a type-hash, handling @ aliases via varStore lookup.
|
||||||
* If the input starts with @, resolve it via bootstrap
|
|
||||||
* Otherwise, return the hash as-is
|
|
||||||
*/
|
*/
|
||||||
async function resolveTypeHash(typeHashOrAlias: string): Promise<Hash> {
|
async function resolveTypeHash(typeHashOrAlias: string): Promise<Hash> {
|
||||||
if (typeHashOrAlias.startsWith("@")) {
|
if (typeHashOrAlias.startsWith("@")) {
|
||||||
const store = await openStore();
|
const varStore = await openVarStore();
|
||||||
const builtinSchemas = await bootstrap(store);
|
try {
|
||||||
const resolvedHash = builtinSchemas[typeHashOrAlias];
|
const variants = varStore.list({ exactName: typeHashOrAlias });
|
||||||
if (!resolvedHash) {
|
const first = variants[0];
|
||||||
die(`Schema not found: ${typeHashOrAlias}`);
|
if (!first) {
|
||||||
|
die(`Schema not found: ${typeHashOrAlias}`);
|
||||||
|
}
|
||||||
|
return first.value;
|
||||||
|
} finally {
|
||||||
|
varStore.close();
|
||||||
}
|
}
|
||||||
return resolvedHash;
|
|
||||||
}
|
}
|
||||||
return typeHashOrAlias;
|
return typeHashOrAlias;
|
||||||
}
|
}
|
||||||
@@ -235,8 +241,7 @@ async function cmdPut(args: string[]): Promise<void> {
|
|||||||
|
|
||||||
// Schema nodes: use putSchema() which validates via isValidSchema() (recursive)
|
// Schema nodes: use putSchema() which validates via isValidSchema() (recursive)
|
||||||
// instead of ajv against meta-schema (which can't express recursive constraints)
|
// instead of ajv against meta-schema (which can't express recursive constraints)
|
||||||
const builtinSchemas = await bootstrap(store);
|
const metaHash = await resolveTypeHash("@ocas/schema");
|
||||||
const metaHash = builtinSchemas["@ocas/schema"];
|
|
||||||
if (typeHash === metaHash) {
|
if (typeHash === metaHash) {
|
||||||
try {
|
try {
|
||||||
const hash = await putSchema(store, payload as Record<string, unknown>);
|
const hash = await putSchema(store, payload as Record<string, unknown>);
|
||||||
@@ -283,7 +288,10 @@ async function cmdHas(args: string[]): Promise<void> {
|
|||||||
const hash = args[0];
|
const hash = args[0];
|
||||||
if (!hash) die("Usage: ocas has <hash>");
|
if (!hash) die("Usage: ocas has <hash>");
|
||||||
const store = await openStore();
|
const store = await openStore();
|
||||||
await out(await wrapEnvelope(store, "@ocas/output/has", store.has(hash)), store);
|
await out(
|
||||||
|
await wrapEnvelope(store, "@ocas/output/has", store.has(hash)),
|
||||||
|
store,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cmdVerify(args: string[]): Promise<void> {
|
async function cmdVerify(args: string[]): Promise<void> {
|
||||||
@@ -345,7 +353,10 @@ async function cmdWalk(args: string[]): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
printNode(hash, "", true);
|
printNode(hash, "", true);
|
||||||
await out(await wrapEnvelope(store, "@ocas/output/walk", lines.join("\n")), store);
|
await out(
|
||||||
|
await wrapEnvelope(store, "@ocas/output/walk", lines.join("\n")),
|
||||||
|
store,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const hashes: Hash[] = [];
|
const hashes: Hash[] = [];
|
||||||
walk(store, hash, (h) => {
|
walk(store, hash, (h) => {
|
||||||
@@ -523,7 +534,10 @@ async function cmdVarSet(args: string[]): Promise<void> {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const variable = varStore.set(name, value, options);
|
const variable = varStore.set(name, value, options);
|
||||||
await out(await wrapEnvelope(store, "@ocas/output/var-set", variable), store);
|
await out(
|
||||||
|
await wrapEnvelope(store, "@ocas/output/var-set", variable),
|
||||||
|
store,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (
|
if (
|
||||||
e instanceof InvalidVariableNameError ||
|
e instanceof InvalidVariableNameError ||
|
||||||
@@ -554,7 +568,10 @@ async function cmdVarGet(args: string[]): Promise<void> {
|
|||||||
if (variable === null) {
|
if (variable === null) {
|
||||||
die(`Error: Variable not found: name=${name}, schema=${schema}`);
|
die(`Error: Variable not found: name=${name}, schema=${schema}`);
|
||||||
}
|
}
|
||||||
await out(await wrapEnvelope(store, "@ocas/output/var-get", variable), store);
|
await out(
|
||||||
|
await wrapEnvelope(store, "@ocas/output/var-get", variable),
|
||||||
|
store,
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
varStore.close();
|
varStore.close();
|
||||||
}
|
}
|
||||||
@@ -579,11 +596,17 @@ async function cmdVarDelete(args: string[]): Promise<void> {
|
|||||||
if (schema !== undefined) {
|
if (schema !== undefined) {
|
||||||
// Precise deletion: remove specific (name, schema) variant
|
// Precise deletion: remove specific (name, schema) variant
|
||||||
const variable = varStore.remove(name, schema);
|
const variable = varStore.remove(name, schema);
|
||||||
await out(await wrapEnvelope(store, "@ocas/output/var-delete", variable), store);
|
await out(
|
||||||
|
await wrapEnvelope(store, "@ocas/output/var-delete", variable),
|
||||||
|
store,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// Batch deletion: remove all variants for this name
|
// Batch deletion: remove all variants for this name
|
||||||
const variables = varStore.remove(name);
|
const variables = varStore.remove(name);
|
||||||
await out(await wrapEnvelope(store, "@ocas/output/var-delete", variables), store);
|
await out(
|
||||||
|
await wrapEnvelope(store, "@ocas/output/var-delete", variables),
|
||||||
|
store,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof VariableNotFoundError) {
|
if (e instanceof VariableNotFoundError) {
|
||||||
@@ -620,7 +643,10 @@ async function cmdVarTag(args: string[]): Promise<void> {
|
|||||||
delete: deleteNames.length > 0 ? deleteNames : undefined,
|
delete: deleteNames.length > 0 ? deleteNames : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
await out(await wrapEnvelope(store, "@ocas/output/var-tag", variable), store);
|
await out(
|
||||||
|
await wrapEnvelope(store, "@ocas/output/var-tag", variable),
|
||||||
|
store,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (
|
if (
|
||||||
e instanceof VariableNotFoundError ||
|
e instanceof VariableNotFoundError ||
|
||||||
@@ -663,7 +689,10 @@ async function cmdVarList(args: string[]): Promise<void> {
|
|||||||
tags: Object.keys(tags).length > 0 ? tags : undefined,
|
tags: Object.keys(tags).length > 0 ? tags : undefined,
|
||||||
labels: labels.length > 0 ? labels : undefined,
|
labels: labels.length > 0 ? labels : undefined,
|
||||||
});
|
});
|
||||||
await out(await wrapEnvelope(store, "@ocas/output/var-list", variables), store);
|
await out(
|
||||||
|
await wrapEnvelope(store, "@ocas/output/var-list", variables),
|
||||||
|
store,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof InvalidVariableNameError) {
|
if (e instanceof InvalidVariableNameError) {
|
||||||
die(`Error: ${e.message}`);
|
die(`Error: ${e.message}`);
|
||||||
@@ -733,7 +762,8 @@ async function cmdTemplateSet(args: string[]): Promise<void> {
|
|||||||
schemaHash,
|
schemaHash,
|
||||||
contentHash,
|
contentHash,
|
||||||
}),
|
}),
|
||||||
store);
|
store,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof CasNodeNotFoundError) {
|
if (e instanceof CasNodeNotFoundError) {
|
||||||
die(`Error: ${e.message}`);
|
die(`Error: ${e.message}`);
|
||||||
@@ -775,7 +805,8 @@ async function cmdTemplateGet(args: string[]): Promise<void> {
|
|||||||
"@ocas/output/template-get",
|
"@ocas/output/template-get",
|
||||||
node.payload as string,
|
node.payload as string,
|
||||||
),
|
),
|
||||||
store);
|
store,
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
varStore.close();
|
varStore.close();
|
||||||
}
|
}
|
||||||
@@ -797,7 +828,10 @@ async function cmdTemplateList(_args: string[]): Promise<void> {
|
|||||||
contentHash: v.value,
|
contentHash: v.value,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await out(await wrapEnvelope(store, "@ocas/output/template-list", templates), store);
|
await out(
|
||||||
|
await wrapEnvelope(store, "@ocas/output/template-list", templates),
|
||||||
|
store,
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
varStore.close();
|
varStore.close();
|
||||||
}
|
}
|
||||||
@@ -822,7 +856,8 @@ async function cmdTemplateDelete(args: string[]): Promise<void> {
|
|||||||
await wrapEnvelope(store, "@ocas/output/template-delete", {
|
await wrapEnvelope(store, "@ocas/output/template-delete", {
|
||||||
deleted: true,
|
deleted: true,
|
||||||
}),
|
}),
|
||||||
store);
|
store,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof VariableNotFoundError) {
|
if (e instanceof VariableNotFoundError) {
|
||||||
die(`Error: Template not found for schema: ${schemaHash}`);
|
die(`Error: Template not found for schema: ${schemaHash}`);
|
||||||
@@ -864,7 +899,10 @@ async function cmdListMeta(_args: string[]): Promise<void> {
|
|||||||
async function cmdListSchema(_args: string[]): Promise<void> {
|
async function cmdListSchema(_args: string[]): Promise<void> {
|
||||||
const store = await openStore();
|
const store = await openStore();
|
||||||
const hashes = store.listSchemas();
|
const hashes = store.listSchemas();
|
||||||
await out(await wrapEnvelope(store, "@ocas/output/list-schema", hashes), store);
|
await out(
|
||||||
|
await wrapEnvelope(store, "@ocas/output/list-schema", hashes),
|
||||||
|
store,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function printUsage(): void {
|
function printUsage(): void {
|
||||||
@@ -1018,4 +1056,4 @@ switch (cmd) {
|
|||||||
|
|
||||||
default:
|
default:
|
||||||
die(`Unknown command: ${cmd}`);
|
die(`Unknown command: ${cmd}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,209 @@ exports[`Phase 3: Variable System 3.3 var list shows all variables 1`] = `
|
|||||||
{
|
{
|
||||||
"type": "AF0XACGXHPMC1",
|
"type": "AF0XACGXHPMC1",
|
||||||
"value": [
|
"value": [
|
||||||
|
{
|
||||||
|
"labels": [],
|
||||||
|
"name": "@ocas/schema",
|
||||||
|
"schema": "CTS5P6RD8HMCS",
|
||||||
|
"tags": {},
|
||||||
|
"value": "CTS5P6RD8HMCS",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": [],
|
||||||
|
"name": "@ocas/string",
|
||||||
|
"schema": "CTS5P6RD8HMCS",
|
||||||
|
"tags": {},
|
||||||
|
"value": "7VQ43ZSJTEWA7",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": [],
|
||||||
|
"name": "@ocas/number",
|
||||||
|
"schema": "CTS5P6RD8HMCS",
|
||||||
|
"tags": {},
|
||||||
|
"value": "BEAZQGKVXMZT8",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": [],
|
||||||
|
"name": "@ocas/integer",
|
||||||
|
"schema": "CTS5P6RD8HMCS",
|
||||||
|
"tags": {},
|
||||||
|
"value": "B26JM4PBHPAFK",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": [],
|
||||||
|
"name": "@ocas/boolean",
|
||||||
|
"schema": "CTS5P6RD8HMCS",
|
||||||
|
"tags": {},
|
||||||
|
"value": "1AVHCXEJVDCPP",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": [],
|
||||||
|
"name": "@ocas/bool",
|
||||||
|
"schema": "CTS5P6RD8HMCS",
|
||||||
|
"tags": {},
|
||||||
|
"value": "1AVHCXEJVDCPP",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": [],
|
||||||
|
"name": "@ocas/object",
|
||||||
|
"schema": "CTS5P6RD8HMCS",
|
||||||
|
"tags": {},
|
||||||
|
"value": "944RT37WX1PQ5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": [],
|
||||||
|
"name": "@ocas/array",
|
||||||
|
"schema": "CTS5P6RD8HMCS",
|
||||||
|
"tags": {},
|
||||||
|
"value": "D45CW047XS17Y",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": [],
|
||||||
|
"name": "@ocas/null",
|
||||||
|
"schema": "CTS5P6RD8HMCS",
|
||||||
|
"tags": {},
|
||||||
|
"value": "8E33KAS0HMAZ7",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": [],
|
||||||
|
"name": "@ocas/output/put",
|
||||||
|
"schema": "CTS5P6RD8HMCS",
|
||||||
|
"tags": {},
|
||||||
|
"value": "4ZHWK21APCFZ5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": [],
|
||||||
|
"name": "@ocas/output/get",
|
||||||
|
"schema": "CTS5P6RD8HMCS",
|
||||||
|
"tags": {},
|
||||||
|
"value": "FB4K0SXG68ZFS",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": [],
|
||||||
|
"name": "@ocas/output/has",
|
||||||
|
"schema": "CTS5P6RD8HMCS",
|
||||||
|
"tags": {},
|
||||||
|
"value": "FHXQQZMVHW924",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": [],
|
||||||
|
"name": "@ocas/output/hash",
|
||||||
|
"schema": "CTS5P6RD8HMCS",
|
||||||
|
"tags": {},
|
||||||
|
"value": "1B24CBF95Q5G6",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": [],
|
||||||
|
"name": "@ocas/output/verify",
|
||||||
|
"schema": "CTS5P6RD8HMCS",
|
||||||
|
"tags": {},
|
||||||
|
"value": "52HEFB52BD0GF",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": [],
|
||||||
|
"name": "@ocas/output/refs",
|
||||||
|
"schema": "CTS5P6RD8HMCS",
|
||||||
|
"tags": {},
|
||||||
|
"value": "2TKP4RGBJ4V43",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": [],
|
||||||
|
"name": "@ocas/output/walk",
|
||||||
|
"schema": "CTS5P6RD8HMCS",
|
||||||
|
"tags": {},
|
||||||
|
"value": "4HG6MD3XG5H5C",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": [],
|
||||||
|
"name": "@ocas/output/list",
|
||||||
|
"schema": "CTS5P6RD8HMCS",
|
||||||
|
"tags": {},
|
||||||
|
"value": "CTCEXSNPWMAQQ",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": [],
|
||||||
|
"name": "@ocas/output/list-meta",
|
||||||
|
"schema": "CTS5P6RD8HMCS",
|
||||||
|
"tags": {},
|
||||||
|
"value": "0V41JBWK72HS3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": [],
|
||||||
|
"name": "@ocas/output/list-schema",
|
||||||
|
"schema": "CTS5P6RD8HMCS",
|
||||||
|
"tags": {},
|
||||||
|
"value": "AW24Q8BKXQYTE",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": [],
|
||||||
|
"name": "@ocas/output/var-set",
|
||||||
|
"schema": "CTS5P6RD8HMCS",
|
||||||
|
"tags": {},
|
||||||
|
"value": "0Q5EMYK4SYSS9",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": [],
|
||||||
|
"name": "@ocas/output/var-get",
|
||||||
|
"schema": "CTS5P6RD8HMCS",
|
||||||
|
"tags": {},
|
||||||
|
"value": "7C75FQT98KKQD",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": [],
|
||||||
|
"name": "@ocas/output/var-delete",
|
||||||
|
"schema": "CTS5P6RD8HMCS",
|
||||||
|
"tags": {},
|
||||||
|
"value": "C3MYPR5RGQFZT",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": [],
|
||||||
|
"name": "@ocas/output/var-tag",
|
||||||
|
"schema": "CTS5P6RD8HMCS",
|
||||||
|
"tags": {},
|
||||||
|
"value": "9103EYRMM949A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": [],
|
||||||
|
"name": "@ocas/output/var-list",
|
||||||
|
"schema": "CTS5P6RD8HMCS",
|
||||||
|
"tags": {},
|
||||||
|
"value": "AF0XACGXHPMC1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": [],
|
||||||
|
"name": "@ocas/output/template-set",
|
||||||
|
"schema": "CTS5P6RD8HMCS",
|
||||||
|
"tags": {},
|
||||||
|
"value": "BJDHPAE4Q8TXM",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": [],
|
||||||
|
"name": "@ocas/output/template-get",
|
||||||
|
"schema": "CTS5P6RD8HMCS",
|
||||||
|
"tags": {},
|
||||||
|
"value": "0B0HBHZGYHR84",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": [],
|
||||||
|
"name": "@ocas/output/template-list",
|
||||||
|
"schema": "CTS5P6RD8HMCS",
|
||||||
|
"tags": {},
|
||||||
|
"value": "8917JQTD1R5JF",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": [],
|
||||||
|
"name": "@ocas/output/template-delete",
|
||||||
|
"schema": "CTS5P6RD8HMCS",
|
||||||
|
"tags": {},
|
||||||
|
"value": "BY7BGZJND3N7R",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": [],
|
||||||
|
"name": "@ocas/output/gc",
|
||||||
|
"schema": "CTS5P6RD8HMCS",
|
||||||
|
"tags": {},
|
||||||
|
"value": "7KHZTY010988K",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"labels": [],
|
"labels": [],
|
||||||
"name": "myapp/config",
|
"name": "myapp/config",
|
||||||
|
|||||||
@@ -32,23 +32,26 @@ const OUTPUT_ALIASES = [
|
|||||||
// ──────────────────────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
describe("bootstrap - Built-in Schemas", () => {
|
describe("bootstrap - Built-in Schemas", () => {
|
||||||
test("should return map of 26 built-in schema aliases to hashes", async () => {
|
test("should return map of 29 built-in schema aliases to hashes", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const builtinSchemas = await bootstrap(store);
|
const builtinSchemas = await bootstrap(store);
|
||||||
|
|
||||||
// Should return object with 6 primitive + 20 output aliases = 26
|
// Should return object with 9 primitive + 20 output aliases = 29
|
||||||
expect(builtinSchemas).toHaveProperty("@ocas/schema");
|
expect(builtinSchemas).toHaveProperty("@ocas/schema");
|
||||||
expect(builtinSchemas).toHaveProperty("@ocas/string");
|
expect(builtinSchemas).toHaveProperty("@ocas/string");
|
||||||
expect(builtinSchemas).toHaveProperty("@ocas/number");
|
expect(builtinSchemas).toHaveProperty("@ocas/number");
|
||||||
|
expect(builtinSchemas).toHaveProperty("@ocas/integer");
|
||||||
|
expect(builtinSchemas).toHaveProperty("@ocas/boolean");
|
||||||
|
expect(builtinSchemas).toHaveProperty("@ocas/bool");
|
||||||
expect(builtinSchemas).toHaveProperty("@ocas/object");
|
expect(builtinSchemas).toHaveProperty("@ocas/object");
|
||||||
expect(builtinSchemas).toHaveProperty("@ocas/array");
|
expect(builtinSchemas).toHaveProperty("@ocas/array");
|
||||||
expect(builtinSchemas).toHaveProperty("@ocas/bool");
|
expect(builtinSchemas).toHaveProperty("@ocas/null");
|
||||||
|
|
||||||
for (const alias of OUTPUT_ALIASES) {
|
for (const alias of OUTPUT_ALIASES) {
|
||||||
expect(builtinSchemas).toHaveProperty(alias);
|
expect(builtinSchemas).toHaveProperty(alias);
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(Object.keys(builtinSchemas)).toHaveLength(26);
|
expect(Object.keys(builtinSchemas)).toHaveLength(29);
|
||||||
|
|
||||||
// All values should be valid hashes
|
// All values should be valid hashes
|
||||||
for (const [_alias, hash] of Object.entries(builtinSchemas)) {
|
for (const [_alias, hash] of Object.entries(builtinSchemas)) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
isBootstrapCapableStore,
|
isBootstrapCapableStore,
|
||||||
} from "./bootstrap-capable.js";
|
} from "./bootstrap-capable.js";
|
||||||
import type { Hash, Store } from "./types.js";
|
import type { Hash, Store } from "./types.js";
|
||||||
|
import type { VariableStore } from "./variable-store.js";
|
||||||
|
|
||||||
const JSON_SCHEMA_TYPES = [
|
const JSON_SCHEMA_TYPES = [
|
||||||
"string",
|
"string",
|
||||||
@@ -282,11 +283,18 @@ const OUTPUT_SCHEMAS: ReadonlyArray<
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Write the meta-schema seed node into the store and register built-in schemas.
|
* Write the meta-schema seed node into the store and register built-in schemas.
|
||||||
* The returned object contains aliases for the meta-schema, 5 primitive schemas,
|
* The returned object contains aliases for the meta-schema, primitive schemas,
|
||||||
* and 18 @ocas/output/* schemas (24 total).
|
* and @ocas/output/* schemas.
|
||||||
* Idempotent: calling bootstrap multiple times returns the same hashes.
|
* Idempotent: calling bootstrap multiple times returns the same hashes.
|
||||||
|
*
|
||||||
|
* If a varStore is provided, all aliases are also written to it via
|
||||||
|
* varStore.set(name, hash). This bypasses @ocas/ namespace protection
|
||||||
|
* (protection is enforced only at the CLI layer).
|
||||||
*/
|
*/
|
||||||
export async function bootstrap(store: Store): Promise<Record<string, Hash>> {
|
export async function bootstrap(
|
||||||
|
store: Store,
|
||||||
|
varStore?: VariableStore,
|
||||||
|
): Promise<Record<string, Hash>> {
|
||||||
if (!isBootstrapCapableStore(store)) {
|
if (!isBootstrapCapableStore(store)) {
|
||||||
throw new Error("Store does not support bootstrap");
|
throw new Error("Store does not support bootstrap");
|
||||||
}
|
}
|
||||||
@@ -297,23 +305,37 @@ export async function bootstrap(store: Store): Promise<Record<string, Hash>> {
|
|||||||
// 2. Register built-in primitive schemas directly (without putSchema to avoid recursion)
|
// 2. Register built-in primitive schemas directly (without putSchema to avoid recursion)
|
||||||
const stringHash = await store.put(metaHash, { type: "string" });
|
const stringHash = await store.put(metaHash, { type: "string" });
|
||||||
const numberHash = await store.put(metaHash, { type: "number" });
|
const numberHash = await store.put(metaHash, { type: "number" });
|
||||||
|
const integerHash = await store.put(metaHash, { type: "integer" });
|
||||||
|
const boolHash = await store.put(metaHash, { type: "boolean" });
|
||||||
const objectHash = await store.put(metaHash, { type: "object" });
|
const objectHash = await store.put(metaHash, { type: "object" });
|
||||||
const arrayHash = await store.put(metaHash, { type: "array" });
|
const arrayHash = await store.put(metaHash, { type: "array" });
|
||||||
const boolHash = await store.put(metaHash, { type: "boolean" });
|
const nullHash = await store.put(metaHash, { type: "null" });
|
||||||
|
|
||||||
// 3. Register @ocas/output/* schemas
|
// 3. Register @ocas/output/* schemas
|
||||||
const aliases: Record<string, Hash> = {
|
const aliases: Record<string, Hash> = {
|
||||||
"@ocas/schema": metaHash,
|
"@ocas/schema": metaHash,
|
||||||
"@ocas/string": stringHash,
|
"@ocas/string": stringHash,
|
||||||
"@ocas/number": numberHash,
|
"@ocas/number": numberHash,
|
||||||
|
"@ocas/integer": integerHash,
|
||||||
|
"@ocas/boolean": boolHash,
|
||||||
|
"@ocas/bool": boolHash,
|
||||||
"@ocas/object": objectHash,
|
"@ocas/object": objectHash,
|
||||||
"@ocas/array": arrayHash,
|
"@ocas/array": arrayHash,
|
||||||
"@ocas/bool": boolHash,
|
"@ocas/null": nullHash,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const [alias, schema] of OUTPUT_SCHEMAS) {
|
for (const [alias, schema] of OUTPUT_SCHEMAS) {
|
||||||
aliases[alias] = await store.put(metaHash, schema);
|
aliases[alias] = await store.put(metaHash, schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. Write all aliases to varStore (when provided).
|
||||||
|
// Idempotent: VariableStore.set is an upsert. Bypasses @ocas/ namespace
|
||||||
|
// protection — protection is only enforced on the CLI `var set` command.
|
||||||
|
if (varStore !== undefined) {
|
||||||
|
for (const [name, hash] of Object.entries(aliases)) {
|
||||||
|
varStore.set(name, hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return aliases;
|
return aliases;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -271,9 +271,12 @@ describe("bootstrap", () => {
|
|||||||
expect(builtinSchemas).toHaveProperty("@ocas/schema");
|
expect(builtinSchemas).toHaveProperty("@ocas/schema");
|
||||||
expect(builtinSchemas).toHaveProperty("@ocas/string");
|
expect(builtinSchemas).toHaveProperty("@ocas/string");
|
||||||
expect(builtinSchemas).toHaveProperty("@ocas/number");
|
expect(builtinSchemas).toHaveProperty("@ocas/number");
|
||||||
|
expect(builtinSchemas).toHaveProperty("@ocas/integer");
|
||||||
|
expect(builtinSchemas).toHaveProperty("@ocas/boolean");
|
||||||
|
expect(builtinSchemas).toHaveProperty("@ocas/bool");
|
||||||
expect(builtinSchemas).toHaveProperty("@ocas/object");
|
expect(builtinSchemas).toHaveProperty("@ocas/object");
|
||||||
expect(builtinSchemas).toHaveProperty("@ocas/array");
|
expect(builtinSchemas).toHaveProperty("@ocas/array");
|
||||||
expect(builtinSchemas).toHaveProperty("@ocas/bool");
|
expect(builtinSchemas).toHaveProperty("@ocas/null");
|
||||||
|
|
||||||
// All values should be valid hashes
|
// All values should be valid hashes
|
||||||
for (const hash of Object.values(builtinSchemas)) {
|
for (const hash of Object.values(builtinSchemas)) {
|
||||||
@@ -281,7 +284,7 @@ describe("bootstrap", () => {
|
|||||||
expect(hash).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
expect(hash).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(Object.keys(builtinSchemas)).toHaveLength(26);
|
expect(Object.keys(builtinSchemas)).toHaveLength(29);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("meta-schema node is stored and retrievable", async () => {
|
test("meta-schema node is stored and retrievable", async () => {
|
||||||
@@ -318,7 +321,7 @@ describe("bootstrap", () => {
|
|||||||
const h2 = await bootstrap(store);
|
const h2 = await bootstrap(store);
|
||||||
|
|
||||||
expect(h1).toEqual(h2);
|
expect(h1).toEqual(h2);
|
||||||
// All 26 built-in schemas should be typed by the meta-schema
|
// All built-in schemas typed by the meta-schema (1 self + 7 unique primitives + 20 outputs)
|
||||||
expect(store.listByType(h1["@ocas/schema"] ?? "")).toHaveLength(26);
|
expect(store.listByType(h1["@ocas/schema"] ?? "")).toHaveLength(28);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ describe("createFsStore – init and bootstrap", () => {
|
|||||||
const h2 = await bootstrap(store);
|
const h2 = await bootstrap(store);
|
||||||
|
|
||||||
expect(h1).toEqual(h2);
|
expect(h1).toEqual(h2);
|
||||||
expect(store.listByType(h1["@ocas/schema"] ?? "")).toHaveLength(26);
|
expect(store.listByType(h1["@ocas/schema"] ?? "")).toHaveLength(28);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,12 @@ import {
|
|||||||
writeFileSync,
|
writeFileSync,
|
||||||
} from "node:fs";
|
} from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import type { BootstrapCapableStore, CasNode, Hash } from "@ocas/core";
|
import type {
|
||||||
|
BootstrapCapableStore,
|
||||||
|
CasNode,
|
||||||
|
Hash,
|
||||||
|
VariableStore,
|
||||||
|
} from "@ocas/core";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BOOTSTRAP_STORE,
|
BOOTSTRAP_STORE,
|
||||||
@@ -310,10 +315,15 @@ export function createFsStore(dir: string): BootstrapCapableStore {
|
|||||||
* 4. Runs bootstrap (which is idempotent)
|
* 4. Runs bootstrap (which is idempotent)
|
||||||
*
|
*
|
||||||
* @param dir - The directory path for the store
|
* @param dir - The directory path for the store
|
||||||
|
* @param varStore - Optional variable store; when provided, builtin schema
|
||||||
|
* aliases are written to it during bootstrap
|
||||||
* @returns A Promise resolving to the BootstrapCapableStore
|
* @returns A Promise resolving to the BootstrapCapableStore
|
||||||
* @throws Error if the path exists but is not a directory
|
* @throws Error if the path exists but is not a directory
|
||||||
*/
|
*/
|
||||||
export async function openStore(dir: string): Promise<BootstrapCapableStore> {
|
export async function openStore(
|
||||||
|
dir: string,
|
||||||
|
varStore?: VariableStore,
|
||||||
|
): Promise<BootstrapCapableStore> {
|
||||||
// Create directory if it doesn't exist
|
// Create directory if it doesn't exist
|
||||||
try {
|
try {
|
||||||
mkdirSync(dir, { recursive: true });
|
mkdirSync(dir, { recursive: true });
|
||||||
@@ -323,7 +333,7 @@ export async function openStore(dir: string): Promise<BootstrapCapableStore> {
|
|||||||
if (nodeError.code === "EACCES") {
|
if (nodeError.code === "EACCES") {
|
||||||
throw new Error(`Permission denied: cannot access store at ${dir}`);
|
throw new Error(`Permission denied: cannot access store at ${dir}`);
|
||||||
}
|
}
|
||||||
if (nodeError.code === "ENOTDIR") {
|
if (nodeError.code === "ENOTDIR" || nodeError.code === "EEXIST") {
|
||||||
throw new Error(`Path exists but is not a directory: ${dir}`);
|
throw new Error(`Path exists but is not a directory: ${dir}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -350,7 +360,7 @@ export async function openStore(dir: string): Promise<BootstrapCapableStore> {
|
|||||||
const store = createFsStore(dir);
|
const store = createFsStore(dir);
|
||||||
|
|
||||||
// Bootstrap (idempotent)
|
// Bootstrap (idempotent)
|
||||||
await bootstrap(store);
|
await bootstrap(store, varStore);
|
||||||
|
|
||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user