Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d83173107a |
@@ -46,7 +46,6 @@ ocas var delete <name> [--schema <hash>]
|
||||
ocas var list [prefix] [--schema <hash>] [--tag ...]
|
||||
ocas var tag <name> --schema <hash> <operations...>
|
||||
ocas var history <name> [--schema <hash>]
|
||||
ocas var rollback <name> <position> [--schema <hash>]
|
||||
```
|
||||
|
||||
### [[Render System|Template & Render]]
|
||||
|
||||
@@ -62,11 +62,9 @@ Every variable tracks its last `MAX_HISTORY` (default 10) values with LRU rotati
|
||||
|
||||
- **set()** appends to history: if the new value already exists in history, it rotates to position 0; otherwise it's inserted at 0 and the oldest entry beyond MAX_HISTORY is evicted.
|
||||
- **Idempotent**: setting the same value as the current (position 0) is a no-op — important for [[Bootstrap]] which calls set() repeatedly.
|
||||
- **rollback(name, schema, position)** promotes a historical value back to current.
|
||||
|
||||
```bash
|
||||
ocas var history <name> [--schema <hash-or-name>] # show history, [0] = current
|
||||
ocas var rollback <name> <position> [--schema <hash-or-name>] # restore a previous value
|
||||
```
|
||||
|
||||
## Role in Garbage Collection
|
||||
|
||||
@@ -765,60 +765,6 @@ async function cmdVarHistory(args: string[]): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function cmdVarRollback(args: string[]): Promise<void> {
|
||||
const name = args[0];
|
||||
const positionStr = args[1];
|
||||
const schemaInput = flags.schema as string | undefined;
|
||||
|
||||
if (!name || positionStr === undefined) {
|
||||
die("Usage: ocas var rollback <name> <position> [--schema <hash-or-name>]");
|
||||
}
|
||||
|
||||
if (name.startsWith("@ocas/")) {
|
||||
die("The @ocas/ namespace is reserved and cannot be modified directly.");
|
||||
}
|
||||
|
||||
const position = Number.parseInt(positionStr, 10);
|
||||
if (!Number.isInteger(position) || position < 0) {
|
||||
die(
|
||||
`Error: Invalid position: ${positionStr} (must be a non-negative integer)`,
|
||||
);
|
||||
}
|
||||
|
||||
const { store, varStore } = await openStoreAndVarStore();
|
||||
|
||||
try {
|
||||
let schema: Hash;
|
||||
if (schemaInput !== undefined) {
|
||||
schema = resolveHash(schemaInput, varStore);
|
||||
} else {
|
||||
const variants = varStore.list({ exactName: name });
|
||||
if (variants.length === 0) {
|
||||
die(`Error: Variable not found: ${name}`);
|
||||
}
|
||||
if (variants.length > 1) {
|
||||
die(
|
||||
`Error: Multiple schema variants for "${name}"; use --schema to disambiguate`,
|
||||
);
|
||||
}
|
||||
schema = (variants[0] as { schema: string }).schema as Hash;
|
||||
}
|
||||
|
||||
const variable = varStore.rollback(name, schema, position);
|
||||
await out(
|
||||
await wrapEnvelope(store, "@ocas/output/var-rollback", variable),
|
||||
store,
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof VariableNotFoundError) {
|
||||
die(`Error: ${e.message}`);
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
varStore.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function cmdVarList(args: string[]): Promise<void> {
|
||||
const namePrefix = args[0] ?? "";
|
||||
const schemaInput = flags.schema as string | undefined;
|
||||
@@ -1098,7 +1044,6 @@ Commands:
|
||||
var list [prefix] [--schema <hash>] [--tag <tag>...] List variables (@ocas/output/var-list)
|
||||
var tag <name> --schema <hash> <operations...> Modify tags/labels (@ocas/output/var-tag)
|
||||
var history <name> [--schema <hash>] Show value history (LRU) (@ocas/output/var-history)
|
||||
var rollback <name> <position> [--schema <hash>] Rollback to historical value (@ocas/output/var-rollback)
|
||||
template set <schema-hash> <file> | --inline <text> Set template for schema (@ocas/output/template-set)
|
||||
template get <schema-hash> Get template content (value=string) (@ocas/output/template-get)
|
||||
template list List all templates (@ocas/output/template-list)
|
||||
@@ -1194,9 +1139,6 @@ switch (cmd) {
|
||||
case "history":
|
||||
await cmdVarHistory(subRest);
|
||||
break;
|
||||
case "rollback":
|
||||
await cmdVarRollback(subRest);
|
||||
break;
|
||||
default:
|
||||
die(`Unknown var subcommand: ${sub ?? "(none)"}`);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ Commands:
|
||||
var list [prefix] [--schema <hash>] [--tag <tag>...] List variables (@ocas/output/var-list)
|
||||
var tag <name> --schema <hash> <operations...> Modify tags/labels (@ocas/output/var-tag)
|
||||
var history <name> [--schema <hash>] Show value history (LRU) (@ocas/output/var-history)
|
||||
var rollback <name> <position> [--schema <hash>] Rollback to historical value (@ocas/output/var-rollback)
|
||||
template set <schema-hash> <file> | --inline <text> Set template for schema (@ocas/output/template-set)
|
||||
template get <schema-hash> Get template content (value=string) (@ocas/output/template-get)
|
||||
template list List all templates (@ocas/output/template-list)
|
||||
@@ -258,13 +257,6 @@ exports[`Phase 3: Variable System 3.3 var list shows all variables 1`] = `
|
||||
"tags": {},
|
||||
"value": "EVZJS80TRFKE1",
|
||||
},
|
||||
{
|
||||
"labels": [],
|
||||
"name": "@ocas/output/var-rollback",
|
||||
"schema": "CTS5P6RD8HMCS",
|
||||
"tags": {},
|
||||
"value": "3S1EZX570VBHF",
|
||||
},
|
||||
{
|
||||
"labels": [],
|
||||
"name": "@ocas/output/template-set",
|
||||
|
||||
@@ -131,46 +131,3 @@ describe("var history", () => {
|
||||
expect(r.stderr).toContain("Variable not found");
|
||||
});
|
||||
});
|
||||
|
||||
describe("var rollback", () => {
|
||||
test("rolls back to a previous value", async () => {
|
||||
const { schema, values } = await setupSchemaAndValues();
|
||||
const [v1, v2, v3] = values as [Hash, Hash, Hash, Hash];
|
||||
|
||||
await runCli("var", "set", "x", v1);
|
||||
await runCli("var", "set", "x", v2);
|
||||
await runCli("var", "set", "x", v3);
|
||||
|
||||
// History: [v3, v2, v1] — rollback to position 2 (v1)
|
||||
const r = await runCli("var", "rollback", "x", "2", "--schema", schema);
|
||||
expect(r.exitCode).toBe(0);
|
||||
const envelope = JSON.parse(r.stdout);
|
||||
expect(envelope.value.value).toBe(v1);
|
||||
|
||||
// After rollback, history is [v1, v3, v2]
|
||||
const h = await runCli("var", "history", "x", "--schema", schema);
|
||||
expect(JSON.parse(h.stdout).value.values).toEqual([v1, v3, v2]);
|
||||
});
|
||||
|
||||
test("rollback fails for invalid position", async () => {
|
||||
const { schema: _schema, values } = await setupSchemaAndValues();
|
||||
const v1 = values[0] as Hash;
|
||||
await runCli("var", "set", "x", v1);
|
||||
|
||||
const r = await runCli("var", "rollback", "x", "9");
|
||||
expect(r.exitCode).toBe(1);
|
||||
expect(r.stderr).toContain("not found");
|
||||
});
|
||||
|
||||
test("rollback fails for negative position", async () => {
|
||||
const r = await runCli("var", "rollback", "x", "-1");
|
||||
expect(r.exitCode).toBe(1);
|
||||
expect(r.stderr).toContain("Invalid position");
|
||||
});
|
||||
|
||||
test("rollback rejects @ocas/ namespace", async () => {
|
||||
const r = await runCli("var", "rollback", "@ocas/string", "1");
|
||||
expect(r.exitCode).toBe(1);
|
||||
expect(r.stderr).toContain("@ocas/");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,7 +21,6 @@ const OUTPUT_ALIASES = [
|
||||
"@ocas/output/var-tag",
|
||||
"@ocas/output/var-list",
|
||||
"@ocas/output/var-history",
|
||||
"@ocas/output/var-rollback",
|
||||
"@ocas/output/template-set",
|
||||
"@ocas/output/template-get",
|
||||
"@ocas/output/template-list",
|
||||
@@ -34,11 +33,11 @@ const OUTPUT_ALIASES = [
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("bootstrap - Built-in Schemas", () => {
|
||||
test("should return map of 31 built-in schema aliases to hashes", async () => {
|
||||
test("should return map of 30 built-in schema aliases to hashes", async () => {
|
||||
const store = createMemoryStore();
|
||||
const builtinSchemas = await bootstrap(store);
|
||||
|
||||
// Should return object with 9 primitive + 22 output aliases = 31
|
||||
// Should return object with 9 primitive + 21 output aliases = 30
|
||||
expect(builtinSchemas).toHaveProperty("@ocas/schema");
|
||||
expect(builtinSchemas).toHaveProperty("@ocas/string");
|
||||
expect(builtinSchemas).toHaveProperty("@ocas/number");
|
||||
@@ -53,7 +52,7 @@ describe("bootstrap - Built-in Schemas", () => {
|
||||
expect(builtinSchemas).toHaveProperty(alias);
|
||||
}
|
||||
|
||||
expect(Object.keys(builtinSchemas)).toHaveLength(31);
|
||||
expect(Object.keys(builtinSchemas)).toHaveLength(30);
|
||||
|
||||
// All values should be valid hashes
|
||||
for (const [_alias, hash] of Object.entries(builtinSchemas)) {
|
||||
|
||||
@@ -244,14 +244,6 @@ const OUTPUT_SCHEMAS: ReadonlyArray<
|
||||
title: "ocas var history result",
|
||||
},
|
||||
],
|
||||
[
|
||||
"@ocas/output/var-rollback",
|
||||
{
|
||||
type: "object",
|
||||
properties: { ...VARIABLE_PROPERTIES },
|
||||
title: "ocas var rollback result",
|
||||
},
|
||||
],
|
||||
[
|
||||
"@ocas/output/template-set",
|
||||
{
|
||||
|
||||
@@ -264,7 +264,7 @@ describe("bootstrap", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("returns a map with 31 built-in schema aliases", async () => {
|
||||
test("returns a map with 30 built-in schema aliases", async () => {
|
||||
const store = createMemoryStore();
|
||||
const builtinSchemas = await bootstrap(store);
|
||||
|
||||
@@ -284,7 +284,7 @@ describe("bootstrap", () => {
|
||||
expect(hash).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
||||
}
|
||||
|
||||
expect(Object.keys(builtinSchemas)).toHaveLength(31);
|
||||
expect(Object.keys(builtinSchemas)).toHaveLength(30);
|
||||
});
|
||||
|
||||
test("meta-schema node is stored and retrievable", async () => {
|
||||
@@ -321,7 +321,7 @@ describe("bootstrap", () => {
|
||||
const h2 = await bootstrap(store);
|
||||
|
||||
expect(h1).toEqual(h2);
|
||||
// All built-in schemas typed by the meta-schema (1 self + 7 unique primitives + 22 outputs)
|
||||
expect(store.listByType(h1["@ocas/schema"] ?? "")).toHaveLength(30);
|
||||
// All built-in schemas typed by the meta-schema (1 self + 7 unique primitives + 21 outputs)
|
||||
expect(store.listByType(h1["@ocas/schema"] ?? "")).toHaveLength(29);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,7 +24,6 @@ const OUTPUT_ALIASES = [
|
||||
"@ocas/output/var-tag",
|
||||
"@ocas/output/var-list",
|
||||
"@ocas/output/var-history",
|
||||
"@ocas/output/var-rollback",
|
||||
"@ocas/output/template-set",
|
||||
"@ocas/output/template-get",
|
||||
"@ocas/output/template-list",
|
||||
@@ -50,7 +49,7 @@ describe("registerOutputTemplates", () => {
|
||||
|
||||
const registered = await registerOutputTemplates(store, varStore);
|
||||
|
||||
expect(Object.keys(registered)).toHaveLength(20);
|
||||
expect(Object.keys(registered)).toHaveLength(19);
|
||||
|
||||
for (const alias of OUTPUT_ALIASES) {
|
||||
expect(registered).toHaveProperty(alias);
|
||||
|
||||
@@ -40,10 +40,6 @@ const DEFAULT_TEMPLATES: ReadonlyArray<
|
||||
"@ocas/output/var-history",
|
||||
"name: {{ payload.name }}\nschema: {{ payload.schema }}\n{% for v in payload.values %}{{ forloop.index0 }}: {{ v }}\n{% endfor %}",
|
||||
],
|
||||
[
|
||||
"@ocas/output/var-rollback",
|
||||
"name: {{ payload.name }}\nschema: {{ payload.schema }}\nvalue: {{ payload.value }}",
|
||||
],
|
||||
[
|
||||
"@ocas/output/template-set",
|
||||
"schemaHash: {{ payload.schemaHash }}\ncontentHash: {{ payload.contentHash }}",
|
||||
|
||||
@@ -1886,7 +1886,7 @@ describe("VariableStore - History (LRU)", () => {
|
||||
varStore.close();
|
||||
});
|
||||
|
||||
test("rollback() moves a historical value to position 0", async () => {
|
||||
test("rollback semantics: re-setting an old value moves it to position 0", async () => {
|
||||
store = createMemoryStore();
|
||||
await bootstrap(store);
|
||||
const schema = await putSchema(store, { type: "number" });
|
||||
@@ -1899,8 +1899,8 @@ describe("VariableStore - History (LRU)", () => {
|
||||
varStore.set("x", v1);
|
||||
varStore.set("x", v2);
|
||||
varStore.set("x", v3);
|
||||
// History: [v3, v2, v1]; rollback to position 2 (v1)
|
||||
const result = varStore.rollback("x", schema, 2);
|
||||
// History: [v3, v2, v1]; rolling back is just calling set() with v1
|
||||
const result = varStore.set("x", v1);
|
||||
|
||||
expect(result.value).toBe(v1);
|
||||
expect(varStore.history("x", schema)).toEqual([v1, v3, v2]);
|
||||
@@ -1908,51 +1908,6 @@ describe("VariableStore - History (LRU)", () => {
|
||||
varStore.close();
|
||||
});
|
||||
|
||||
test("rollback() with position 0 is a no-op", async () => {
|
||||
store = createMemoryStore();
|
||||
await bootstrap(store);
|
||||
const schema = await putSchema(store, { type: "number" });
|
||||
const v1 = await store.put(schema, 1);
|
||||
|
||||
dbPath = tmpDbPath();
|
||||
const varStore = new VariableStore(dbPath, store);
|
||||
varStore.set("x", v1);
|
||||
|
||||
const r = varStore.rollback("x", schema, 0);
|
||||
expect(r.value).toBe(v1);
|
||||
expect(varStore.history("x", schema)).toEqual([v1]);
|
||||
varStore.close();
|
||||
});
|
||||
|
||||
test("rollback() throws for non-existent position", async () => {
|
||||
store = createMemoryStore();
|
||||
await bootstrap(store);
|
||||
const schema = await putSchema(store, { type: "number" });
|
||||
const v1 = await store.put(schema, 1);
|
||||
|
||||
dbPath = tmpDbPath();
|
||||
const varStore = new VariableStore(dbPath, store);
|
||||
varStore.set("x", v1);
|
||||
|
||||
expect(() => varStore.rollback("x", schema, 5)).toThrow(
|
||||
VariableNotFoundError,
|
||||
);
|
||||
varStore.close();
|
||||
});
|
||||
|
||||
test("rollback() throws for non-existent variable", async () => {
|
||||
store = createMemoryStore();
|
||||
await bootstrap(store);
|
||||
const schema = await putSchema(store, { type: "number" });
|
||||
|
||||
dbPath = tmpDbPath();
|
||||
const varStore = new VariableStore(dbPath, store);
|
||||
expect(() => varStore.rollback("nope", schema, 0)).toThrow(
|
||||
VariableNotFoundError,
|
||||
);
|
||||
varStore.close();
|
||||
});
|
||||
|
||||
test("history is cascade-deleted with the variable", async () => {
|
||||
store = createMemoryStore();
|
||||
await bootstrap(store);
|
||||
|
||||
@@ -843,67 +843,6 @@ export class VariableStore {
|
||||
return rows.map((r) => r.value as Hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Roll back a variable to the value currently at the given history position.
|
||||
* The value at `position` is moved to position 0 (current); positions in
|
||||
* [0, position) are shifted +1.
|
||||
*
|
||||
* Throws VariableNotFoundError if the variable does not exist or there is
|
||||
* no history entry at the given position.
|
||||
*/
|
||||
rollback(name: string, schema: Hash, position: number): Variable {
|
||||
if (!Number.isInteger(position) || position < 0) {
|
||||
throw new Error(
|
||||
`Invalid history position: ${position} (must be a non-negative integer)`,
|
||||
);
|
||||
}
|
||||
|
||||
const existing = this.get(name, schema);
|
||||
if (existing === null) {
|
||||
throw new VariableNotFoundError(name, schema);
|
||||
}
|
||||
|
||||
if (position === 0) {
|
||||
// No-op rollback to current
|
||||
return existing;
|
||||
}
|
||||
|
||||
const targetRow = this.db
|
||||
.prepare(
|
||||
`SELECT value FROM variable_history WHERE variable_name = ? AND variable_schema = ? AND position = ?`,
|
||||
)
|
||||
.get(name, schema, position) as { value: string } | undefined | null;
|
||||
|
||||
if (!targetRow) {
|
||||
throw new VariableNotFoundError(name, schema);
|
||||
}
|
||||
|
||||
const targetValue = targetRow.value as Hash;
|
||||
const now = Date.now();
|
||||
|
||||
this.db.exec("BEGIN TRANSACTION");
|
||||
try {
|
||||
const changed = this.recordHistory(name, schema, targetValue, now);
|
||||
if (changed) {
|
||||
this.db
|
||||
.prepare(
|
||||
`UPDATE variables SET value = ?, updated = ? WHERE name = ? AND schema = ?`,
|
||||
)
|
||||
.run(targetValue, now, name, schema);
|
||||
}
|
||||
this.db.exec("COMMIT");
|
||||
} catch (e) {
|
||||
this.db.exec("ROLLBACK");
|
||||
throw e;
|
||||
}
|
||||
|
||||
const updated = this.get(name, schema);
|
||||
if (updated === null) {
|
||||
throw new VariableNotFoundError(name, schema);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database connection
|
||||
*/
|
||||
|
||||
@@ -67,7 +67,7 @@ describe("createFsStore – init and bootstrap", () => {
|
||||
const h2 = await bootstrap(store);
|
||||
|
||||
expect(h1).toEqual(h2);
|
||||
expect(store.listByType(h1["@ocas/schema"] ?? "")).toHaveLength(30);
|
||||
expect(store.listByType(h1["@ocas/schema"] ?? "")).toHaveLength(29);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user