@hraness/oh 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +598 -0
  3. package/dist/canonical.d.ts +32 -0
  4. package/dist/canonical.d.ts.map +1 -0
  5. package/dist/cli.d.ts +4 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +3419 -0
  8. package/dist/contract.d.ts +33 -0
  9. package/dist/contract.d.ts.map +1 -0
  10. package/dist/graph.d.ts +67 -0
  11. package/dist/graph.d.ts.map +1 -0
  12. package/dist/index.d.ts +9 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +1988 -0
  15. package/dist/libsql.d.ts +57 -0
  16. package/dist/libsql.d.ts.map +1 -0
  17. package/dist/libsql.js +2662 -0
  18. package/dist/memory.d.ts +366 -0
  19. package/dist/memory.d.ts.map +1 -0
  20. package/dist/memory.js +3650 -0
  21. package/dist/ontology.d.ts +242 -0
  22. package/dist/ontology.d.ts.map +1 -0
  23. package/dist/operation.d.ts +24 -0
  24. package/dist/operation.d.ts.map +1 -0
  25. package/dist/projection-public.d.ts +59 -0
  26. package/dist/projection-public.d.ts.map +1 -0
  27. package/dist/projection-public.js +1682 -0
  28. package/dist/projection-suss.d.ts +17 -0
  29. package/dist/projection-suss.d.ts.map +1 -0
  30. package/dist/projection-suss.js +1721 -0
  31. package/dist/projection.d.ts +315 -0
  32. package/dist/projection.d.ts.map +1 -0
  33. package/dist/schema.d.ts +45 -0
  34. package/dist/schema.d.ts.map +1 -0
  35. package/dist/sdk.d.ts +51 -0
  36. package/dist/sdk.d.ts.map +1 -0
  37. package/dist/sdk.js +3072 -0
  38. package/dist/search.d.ts +34 -0
  39. package/dist/search.d.ts.map +1 -0
  40. package/dist/semantic.d.ts +83 -0
  41. package/dist/semantic.d.ts.map +1 -0
  42. package/dist/semantic.js +706 -0
  43. package/dist/sqlite/driver.d.ts +6 -0
  44. package/dist/sqlite/driver.d.ts.map +1 -0
  45. package/dist/sqlite/index.d.ts +5 -0
  46. package/dist/sqlite/index.d.ts.map +1 -0
  47. package/dist/sqlite/index.js +2840 -0
  48. package/dist/sqlite/migrations.d.ts +10 -0
  49. package/dist/sqlite/migrations.d.ts.map +1 -0
  50. package/dist/sqlite/port.d.ts +40 -0
  51. package/dist/sqlite/port.d.ts.map +1 -0
  52. package/dist/sqlite/runtime.d.ts +23 -0
  53. package/dist/sqlite/runtime.d.ts.map +1 -0
  54. package/dist/sqlite/store.d.ts +85 -0
  55. package/dist/sqlite/store.d.ts.map +1 -0
  56. package/dist/store.d.ts +236 -0
  57. package/dist/store.d.ts.map +1 -0
  58. package/dist/store.js +996 -0
  59. package/dist/sync.d.ts +57 -0
  60. package/dist/sync.d.ts.map +1 -0
  61. package/dist/sync.js +1328 -0
  62. package/package.json +121 -0
  63. package/skills/oh/SKILL.md +206 -0
  64. package/skills/oh/agents/openai.yaml +4 -0
  65. package/spec/README.md +74 -0
  66. package/spec/manifest.json +41 -0
  67. package/spec/v1/canonical-json.md +59 -0
  68. package/spec/v1/contract.json +28 -0
  69. package/spec/v1/contract.schema.json +58 -0
  70. package/spec/v1/embedding-profile.json +11 -0
  71. package/spec/v1/embedding.md +56 -0
  72. package/spec/v1/graph.md +87 -0
  73. package/spec/v1/memory.md +193 -0
  74. package/spec/v1/migration.md +92 -0
  75. package/spec/v1/ontology.json +55 -0
  76. package/spec/v1/ontology.md +80 -0
  77. package/spec/v1/operation.schema.json +138 -0
  78. package/spec/v1/projection-identity.schema.json +58 -0
  79. package/spec/v1/projection-query.schema.json +60 -0
  80. package/spec/v1/projection-result.schema.json +452 -0
  81. package/spec/v1/projection-rule-pack.schema.json +182 -0
  82. package/spec/v1/projection.md +165 -0
  83. package/spec/v1/record.schema.json +95 -0
  84. package/spec/v1/schema-evolution.md +51 -0
  85. package/spec/v1/schema-revision.schema.json +178 -0
  86. package/spec/v1/storage.md +88 -0
  87. package/spec/v1/store.md +131 -0
  88. package/spec/v1/sync-bundle.schema.json +51 -0
  89. package/spec/v1/sync.md +67 -0
  90. package/src/canonical.test.ts +46 -0
  91. package/src/canonical.ts +203 -0
  92. package/src/cli.test.ts +103 -0
  93. package/src/cli.ts +308 -0
  94. package/src/contract.ts +87 -0
  95. package/src/contracts.test.ts +147 -0
  96. package/src/graph.ts +248 -0
  97. package/src/index.ts +8 -0
  98. package/src/libsql.test.ts +657 -0
  99. package/src/libsql.ts +1687 -0
  100. package/src/memory.test.ts +783 -0
  101. package/src/memory.ts +1684 -0
  102. package/src/ontology.ts +573 -0
  103. package/src/operation.ts +80 -0
  104. package/src/projection-public.ts +53 -0
  105. package/src/projection-suss.ts +129 -0
  106. package/src/projection.test.ts +418 -0
  107. package/src/projection.ts +1457 -0
  108. package/src/schema.ts +156 -0
  109. package/src/sdk.ts +96 -0
  110. package/src/search.ts +66 -0
  111. package/src/semantic.test.ts +480 -0
  112. package/src/semantic.ts +333 -0
  113. package/src/sqlite/driver.ts +47 -0
  114. package/src/sqlite/index.ts +4 -0
  115. package/src/sqlite/migrations.test.ts +44 -0
  116. package/src/sqlite/migrations.ts +178 -0
  117. package/src/sqlite/port.test.ts +127 -0
  118. package/src/sqlite/port.ts +120 -0
  119. package/src/sqlite/runtime.test.ts +68 -0
  120. package/src/sqlite/runtime.ts +53 -0
  121. package/src/sqlite/store.test.ts +295 -0
  122. package/src/sqlite/store.ts +988 -0
  123. package/src/store.test.ts +121 -0
  124. package/src/store.ts +701 -0
  125. package/src/sync.test.ts +117 -0
  126. package/src/sync.ts +227 -0
@@ -0,0 +1,103 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+
7
+ const roots: string[] = [];
8
+ const CLI_PATH = join(import.meta.dir, "cli.ts");
9
+ const REPOSITORY_ROOT = join(import.meta.dir, "..");
10
+ afterEach(async () => { await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); });
11
+
12
+ async function run(arguments_: readonly string[], cwd = REPOSITORY_ROOT): Promise<{ code: number; stderr: string; stdout: string }> {
13
+ const process_ = Bun.spawn([process.execPath, CLI_PATH, ...arguments_], { cwd,
14
+ stderr: "pipe", stdout: "pipe" });
15
+ const [code, stdout, stderr] = await Promise.all([process_.exited,
16
+ new Response(process_.stdout).text(), new Response(process_.stderr).text()]);
17
+ return { code, stderr, stdout };
18
+ }
19
+
20
+ describe("oh CLI", () => {
21
+ test("initializes, writes, searches, and verifies one local database", async () => {
22
+ const root = await mkdtemp(join(tmpdir(), "oh-cli-test-"));
23
+ roots.push(root);
24
+ const database = join(root, "oh.sqlite");
25
+ expect(await run(["init", "--db", database])).toMatchObject({ code: 0, stderr: "" });
26
+ const put = await run(["put", "--db", database, "--kind", "entity", "--key", "entity:ada",
27
+ "--json", '{"name":"Ada Lovelace"}', "--operation", "op_cli"]);
28
+ expect(put.code).toBe(0);
29
+ expect(JSON.parse(put.stdout)).toMatchObject({ operationId: "op_cli", sequence: 1 });
30
+ const search = await run(["search", "Ada", "--db", database]);
31
+ expect(JSON.parse(search.stdout).results[0].record.key).toBe("entity:ada");
32
+ const verify = await run(["verify", "--db", database]);
33
+ expect(JSON.parse(verify.stdout)).toMatchObject({ operations: 1, records: 1, sqliteIntegrity: "ok" });
34
+ });
35
+
36
+ test("returns a distinct missing-record status", async () => {
37
+ const root = await mkdtemp(join(tmpdir(), "oh-cli-test-"));
38
+ roots.push(root);
39
+ const result = await run(["get", "entity:missing", "--db", join(root, "oh.sqlite")]);
40
+ expect(result.code).toBe(3);
41
+ expect(result.stdout).toBe("");
42
+ });
43
+
44
+ test("rejects malformed and command-invalid input before opening a database", async () => {
45
+ const root = await mkdtemp(join(tmpdir(), "oh-cli-invalid-test-"));
46
+ roots.push(root);
47
+ const database = join(root, "custom.sqlite");
48
+ const malformedJson = join(root, "malformed.json");
49
+ const malformedBundle = join(root, "malformed-bundle.json");
50
+ await writeFile(malformedJson, "not-json", "utf8");
51
+ await writeFile(malformedBundle, '{"v":1}', "utf8");
52
+ const invalidInvocations: readonly (readonly string[])[] = [
53
+ ["unknown"],
54
+ ["help", "extra"],
55
+ ["version", "--db", database],
56
+ ["init", "extra"],
57
+ ["init", "--dba", database],
58
+ ["init", "--db", ""],
59
+ ["init", "--db", database, "--spacee", "default"],
60
+ ["init", "--db", database, "--space", "bad space"],
61
+ ["get", "entity:ada", "--db", database, "--limit", "1"],
62
+ ["get", "entity:ada", "--db", database, "--db", database],
63
+ ["list", "--db", database, "--limit", "01"],
64
+ ["list", "--db", database, "--limit", "0"],
65
+ ["log", "--db", database, "--limit", "1001"],
66
+ ["search", "Ada", "--db", database, "--mode", "remote"],
67
+ ["search", "Ada", "--db", database, "--limit", "101"],
68
+ ["put", "--kind", "unknown", "--key", "entity:ada", "--json", "{}"],
69
+ ["put", "--kind", "entity", "--key", "bad key", "--json", "{}"],
70
+ ["put", "--kind", "entity", "--key", "entity:ada", "--json", "{}", "--file", malformedJson],
71
+ ["put", "--kind", "entity", "--key", "entity:ada", "--json", "not-json"],
72
+ ["put", "--kind", "entity", "--key", "entity:ada", "--file", malformedJson],
73
+ ["put", "--kind", "entity", "--key", "entity:ada", "--json", "{}", "--expected-generation", "+1"],
74
+ ["sync", "import", "--file", malformedBundle],
75
+ ["sync", "export", "--after", "-1"],
76
+ ["sync", "export", "--limit", "0"],
77
+ ];
78
+ for (const invocation of invalidInvocations) {
79
+ const result = await run(invocation, root);
80
+ expect(result.code, invocation.join(" ")).toBe(1);
81
+ expect(result.stderr, invocation.join(" ")).toStartWith("oh: ");
82
+ expect(result.stdout, invocation.join(" ")).toBe("");
83
+ expect(existsSync(join(root, ".oh")), invocation.join(" ")).toBe(false);
84
+ expect(existsSync(database), invocation.join(" ")).toBe(false);
85
+ }
86
+ });
87
+
88
+ test("serves the static contract without creating a default store", async () => {
89
+ const root = await mkdtemp(join(tmpdir(), "oh-cli-contract-test-"));
90
+ roots.push(root);
91
+ const database = join(root, "contract.sqlite");
92
+ const result = await run(["contract"], root);
93
+ expect(result.code).toBe(0);
94
+ expect(JSON.parse(result.stdout)).toMatchObject({
95
+ manifest: { contractId: "oh.ontology.v1" },
96
+ sqliteSchemaVersion: 2,
97
+ v: 1,
98
+ });
99
+ expect(existsSync(join(root, ".oh"))).toBe(false);
100
+ expect((await run(["contract", "--db", database, "--space", "contract.test"], root)).code).toBe(0);
101
+ expect(existsSync(database)).toBe(false);
102
+ });
103
+ });
package/src/cli.ts ADDED
@@ -0,0 +1,308 @@
1
+ #!/usr/bin/env bun
2
+ import { readFile } from "node:fs/promises";
3
+
4
+ import { canonicalJson, opaqueId, safeCode, type JsonValue } from "./canonical";
5
+ import { OH_CONTRACT_MANIFEST_V1 } from "./contract";
6
+ import { OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1, createKnowledgeGraphRecordV1,
7
+ type KnowledgeGraphRecordKindV1, type KnowledgeGraphRecordV1 } from "./graph";
8
+ import { Oh } from "./sdk";
9
+ import { OH_SQLITE_SCHEMA_VERSION } from "./sqlite/migrations";
10
+ import { createOhSyncBundleV1, parseOhSyncBundleV1 } from "./sync";
11
+
12
+ export const OH_PACKAGE_VERSION = "0.2.3" as const;
13
+
14
+ type ParsedArguments = { options: Map<string, string[]>; positionals: string[] };
15
+ type ValidatedInvocation = Readonly<{
16
+ databasePath: string;
17
+ putRecord: KnowledgeGraphRecordV1 | null;
18
+ spaceId: string;
19
+ syncBundle: NonNullable<ReturnType<typeof parseOhSyncBundleV1>> | null;
20
+ }>;
21
+
22
+ const KNOWN_OPTIONS = new Set([
23
+ "actor", "after", "db", "depends-on", "expected-generation", "file",
24
+ "json", "key", "kind", "limit", "mode", "operation", "space",
25
+ ]);
26
+ const GLOBAL_OPTIONS = ["db", "space"] as const;
27
+ const MUTATION_OPTIONS = ["actor", "expected-generation", "operation"] as const;
28
+
29
+ function parseArguments(arguments_: readonly string[]): ParsedArguments {
30
+ const options = new Map<string, string[]>();
31
+ const positionals: string[] = [];
32
+ for (let index = 0; index < arguments_.length; index += 1) {
33
+ const argument = arguments_[index] as string;
34
+ if (!argument.startsWith("--")) {
35
+ if (argument.startsWith("-")) throw new TypeError(`Unknown option: ${argument}`);
36
+ positionals.push(argument);
37
+ continue;
38
+ }
39
+ const equals = argument.indexOf("=");
40
+ const name = equals === -1 ? argument.slice(2) : argument.slice(2, equals);
41
+ const next = equals === -1 ? arguments_[index + 1] : argument.slice(equals + 1);
42
+ if (!KNOWN_OPTIONS.has(name)) throw new TypeError(`Unknown option: --${name}`);
43
+ if (next === undefined || next.length === 0 || (equals === -1 && next.startsWith("--"))) {
44
+ throw new TypeError(`Option --${name} needs a value.`);
45
+ }
46
+ if (equals === -1) index += 1;
47
+ const current = options.get(name) ?? [];
48
+ if (name !== "depends-on" && current.length !== 0) {
49
+ throw new TypeError(`Option --${name} may appear only once.`);
50
+ }
51
+ options.set(name, [...current, next]);
52
+ }
53
+ return { options, positionals };
54
+ }
55
+
56
+ function one(parsed: ParsedArguments, name: string, fallback?: string): string | undefined {
57
+ const values = parsed.options.get(name);
58
+ return values?.[0] ?? fallback;
59
+ }
60
+
61
+ function integer(value: string | undefined, name: string, minimum = 0,
62
+ maximum = Number.MAX_SAFE_INTEGER): number | undefined {
63
+ if (value === undefined) return undefined;
64
+ if (!/^(?:0|[1-9][0-9]*)$/u.test(value)) {
65
+ throw new TypeError(`--${name} must be a canonical integer from ${minimum} through ${maximum}.`);
66
+ }
67
+ const parsed = Number(value);
68
+ if (!Number.isSafeInteger(parsed) || parsed < minimum || parsed > maximum) {
69
+ throw new TypeError(`--${name} must be a canonical integer from ${minimum} through ${maximum}.`);
70
+ }
71
+ return parsed;
72
+ }
73
+
74
+ function assertAllowedOptions(parsed: ParsedArguments, allowed: readonly string[]): void {
75
+ const admitted = new Set(allowed);
76
+ for (const name of parsed.options.keys()) {
77
+ if (!admitted.has(name)) throw new TypeError(`Option --${name} is not valid for this command.`);
78
+ }
79
+ }
80
+
81
+ function assertPositionals(parsed: ParsedArguments, minimum: number, maximum = minimum): void {
82
+ if (parsed.positionals.length < minimum || parsed.positionals.length > maximum) {
83
+ throw new TypeError(`This command needs ${minimum === maximum ? String(minimum) : `${minimum} through ${maximum}`} positional argument${maximum === 1 ? "" : "s"}.`);
84
+ }
85
+ }
86
+
87
+ function parsedSafeCode(value: string | undefined, label: string, maximum = 128): string {
88
+ const parsed = safeCode(value, maximum);
89
+ if (parsed === null) throw new TypeError(`${label} is invalid.`);
90
+ return parsed;
91
+ }
92
+
93
+ function validateCommon(parsed: ParsedArguments): Pick<ValidatedInvocation, "databasePath" | "spaceId"> {
94
+ const databasePath = one(parsed, "db", ".oh/oh.sqlite") as string;
95
+ if (databasePath.length === 0 || databasePath.length > 4096 || databasePath.includes("\0")) {
96
+ throw new TypeError("--db is invalid.");
97
+ }
98
+ return { databasePath, spaceId: parsedSafeCode(one(parsed, "space", "default"), "--space") };
99
+ }
100
+
101
+ function validateMutation(parsed: ParsedArguments): void {
102
+ if (parsed.options.has("actor")) parsedSafeCode(one(parsed, "actor"), "--actor");
103
+ if (parsed.options.has("operation")) parsedSafeCode(one(parsed, "operation"), "--operation");
104
+ integer(one(parsed, "expected-generation"), "expected-generation");
105
+ }
106
+
107
+ async function validateInvocation(command: string, parsed: ParsedArguments): Promise<ValidatedInvocation> {
108
+ let putRecord: KnowledgeGraphRecordV1 | null = null;
109
+ let syncBundle: ValidatedInvocation["syncBundle"] = null;
110
+ if (command === "contract") {
111
+ assertAllowedOptions(parsed, GLOBAL_OPTIONS);
112
+ assertPositionals(parsed, 0);
113
+ return { ...validateCommon(parsed), putRecord, syncBundle };
114
+ }
115
+ if (command === "init" || command === "verify") {
116
+ assertAllowedOptions(parsed, GLOBAL_OPTIONS);
117
+ assertPositionals(parsed, 0);
118
+ } else if (command === "get") {
119
+ assertAllowedOptions(parsed, GLOBAL_OPTIONS);
120
+ assertPositionals(parsed, 1);
121
+ parsedSafeCode(parsed.positionals[0], "Record key", 512);
122
+ } else if (command === "list") {
123
+ assertAllowedOptions(parsed, [...GLOBAL_OPTIONS, "kind", "limit"]);
124
+ assertPositionals(parsed, 0);
125
+ const kind = one(parsed, "kind") as KnowledgeGraphRecordKindV1 | undefined;
126
+ if (kind !== undefined && !OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.includes(kind)) {
127
+ throw new TypeError("Unknown record kind.");
128
+ }
129
+ integer(one(parsed, "limit"), "limit", 1, 1000);
130
+ } else if (command === "log") {
131
+ assertAllowedOptions(parsed, [...GLOBAL_OPTIONS, "limit"]);
132
+ assertPositionals(parsed, 0);
133
+ integer(one(parsed, "limit"), "limit", 1, 1000);
134
+ } else if (command === "search") {
135
+ assertAllowedOptions(parsed, [...GLOBAL_OPTIONS, "limit", "mode"]);
136
+ assertPositionals(parsed, 1, 1024);
137
+ const query = parsed.positionals.join(" ");
138
+ const mode = one(parsed, "mode", "keyword");
139
+ if (query.trim().length === 0 || query.length > 4096
140
+ || (mode !== "keyword" && mode !== "semantic" && mode !== "hybrid")) {
141
+ throw new TypeError("search needs a bounded query and a valid mode.");
142
+ }
143
+ integer(one(parsed, "limit"), "limit", 1, 100);
144
+ } else if (command === "put") {
145
+ assertAllowedOptions(parsed, [...GLOBAL_OPTIONS, ...MUTATION_OPTIONS,
146
+ "depends-on", "file", "json", "key", "kind"]);
147
+ assertPositionals(parsed, 0);
148
+ validateMutation(parsed);
149
+ const key = parsedSafeCode(one(parsed, "key"), "--key", 512);
150
+ const kind = one(parsed, "kind") as KnowledgeGraphRecordKindV1 | undefined;
151
+ const inline = one(parsed, "json");
152
+ const file = one(parsed, "file");
153
+ if (kind === undefined || !OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.includes(kind)
154
+ || (inline === undefined) === (file === undefined)) {
155
+ throw new TypeError("put needs --key, a valid --kind, and exactly one of --json or --file.");
156
+ }
157
+ const dependencies = (parsed.options.get("depends-on") ?? [])
158
+ .map((dependency) => parsedSafeCode(dependency, "--depends-on", 512)).sort();
159
+ const value = JSON.parse(inline ?? await readFile(file as string, "utf8")) as JsonValue;
160
+ putRecord = createKnowledgeGraphRecordV1({ dependencies, key, kind, v: 1, value });
161
+ } else if (command === "tombstone") {
162
+ assertAllowedOptions(parsed, [...GLOBAL_OPTIONS, ...MUTATION_OPTIONS]);
163
+ assertPositionals(parsed, 1);
164
+ parsedSafeCode(parsed.positionals[0], "Record key", 512);
165
+ validateMutation(parsed);
166
+ } else if (command === "sync") {
167
+ assertPositionals(parsed, 1);
168
+ const action = parsed.positionals[0];
169
+ if (action === "export") {
170
+ assertAllowedOptions(parsed, [...GLOBAL_OPTIONS, "after", "limit"]);
171
+ integer(one(parsed, "after"), "after");
172
+ integer(one(parsed, "limit"), "limit", 1, 1000);
173
+ } else if (action === "import") {
174
+ assertAllowedOptions(parsed, [...GLOBAL_OPTIONS, "file"]);
175
+ const file = one(parsed, "file");
176
+ if (file === undefined) throw new TypeError("sync import needs --file.");
177
+ syncBundle = parseOhSyncBundleV1(JSON.parse(await readFile(file, "utf8")));
178
+ if (syncBundle === null) throw new TypeError("Invalid sync bundle.");
179
+ } else {
180
+ throw new TypeError("sync needs export or import.");
181
+ }
182
+ } else {
183
+ throw new TypeError(`Unknown command: ${command}`);
184
+ }
185
+ const common = validateCommon(parsed);
186
+ if (syncBundle !== null && syncBundle.spaceId !== common.spaceId) {
187
+ throw new TypeError("Invalid sync bundle.");
188
+ }
189
+ return { ...common, putRecord, syncBundle };
190
+ }
191
+
192
+ function print(value: unknown): void { process.stdout.write(`${canonicalJson(value)}\n`); }
193
+
194
+ const HELP = `oh ${OH_PACKAGE_VERSION}
195
+
196
+ Usage:
197
+ oh init [--db PATH] [--space ID]
198
+ oh put --kind KIND --key KEY (--json JSON | --file PATH) [--depends-on KEY]
199
+ oh get KEY
200
+ oh list [--kind KIND] [--limit N]
201
+ oh log [--limit N]
202
+ oh search QUERY [--mode keyword|semantic|hybrid] [--limit N]
203
+ oh tombstone KEY
204
+ oh verify
205
+ oh sync export [--after N] [--limit N]
206
+ oh sync import --file PATH
207
+ oh contract
208
+ oh version
209
+
210
+ Global options: --db PATH (default .oh/oh.sqlite), --space ID (default default)
211
+ Mutation options: --actor ID, --operation ID, --expected-generation N
212
+ `;
213
+
214
+ export async function runOhCli(arguments_: readonly string[]): Promise<number> {
215
+ const command = arguments_[0];
216
+ if (command === undefined || command === "help" || command === "--help") {
217
+ if (arguments_.length > 1) throw new TypeError("help does not accept arguments or options.");
218
+ process.stdout.write(HELP);
219
+ return 0;
220
+ }
221
+ if (command === "version" || command === "--version") {
222
+ if (arguments_.length !== 1) throw new TypeError("version does not accept arguments or options.");
223
+ process.stdout.write(`${OH_PACKAGE_VERSION}\n`);
224
+ return 0;
225
+ }
226
+ const parsed = parseArguments(arguments_.slice(1));
227
+ const validated = await validateInvocation(command, parsed);
228
+ if (command === "contract") {
229
+ print({ manifest: OH_CONTRACT_MANIFEST_V1, sqliteSchemaVersion: OH_SQLITE_SCHEMA_VERSION, v: 1 });
230
+ return 0;
231
+ }
232
+ const oh = Oh.open({ databasePath: validated.databasePath, spaceId: validated.spaceId });
233
+ try {
234
+ if (command === "init") { print({ head: oh.head(), spaceId: oh.store.spaceId, v: 1 }); return 0; }
235
+ if (command === "put") {
236
+ const record = validated.putRecord;
237
+ if (record === null) throw new TypeError("Invalid prepared put command.");
238
+ const head = oh.head();
239
+ const expectedGeneration = integer(one(parsed, "expected-generation"), "expected-generation");
240
+ const operation = oh.store.commit({ actorId: one(parsed, "actor", "agent.local") as string,
241
+ changes: [{ kind: "put", record, v: 1 }], expectedHead: { generation: expectedGeneration ?? head.generation,
242
+ operationSha256: head.operationSha256 }, operationId: one(parsed, "operation") ?? opaqueId("op_") });
243
+ print(operation); return 0;
244
+ }
245
+ if (command === "tombstone") {
246
+ const key = parsed.positionals[0];
247
+ if (key === undefined || parsed.positionals.length !== 1) throw new TypeError("tombstone needs one record key.");
248
+ const head = oh.head();
249
+ const expectedGeneration = integer(one(parsed, "expected-generation"), "expected-generation");
250
+ const record = oh.get(key);
251
+ if (record === null) return 3;
252
+ const operation = oh.store.commit({ actorId: one(parsed, "actor", "agent.local") as string,
253
+ changes: [{ key, kind: "tombstone", priorSha256: record.recordSha256, v: 1 }],
254
+ expectedHead: { generation: expectedGeneration ?? head.generation, operationSha256: head.operationSha256 },
255
+ operationId: one(parsed, "operation") ?? opaqueId("op_") });
256
+ print(operation); return 0;
257
+ }
258
+ if (command === "get") {
259
+ const key = parsed.positionals[0];
260
+ if (key === undefined || parsed.positionals.length !== 1) throw new TypeError("get needs one record key.");
261
+ const record = oh.get(key);
262
+ if (record === null) return 3;
263
+ print(record); return 0;
264
+ }
265
+ if (command === "list") {
266
+ const kind = one(parsed, "kind") as KnowledgeGraphRecordKindV1 | undefined;
267
+ if (kind !== undefined && !OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.includes(kind)) throw new TypeError("Unknown record kind.");
268
+ const limit = integer(one(parsed, "limit"), "limit");
269
+ print({ records: oh.list({ ...(kind === undefined ? {} : { kind }), ...(limit === undefined ? {} : { limit }) }), v: 1 });
270
+ return 0;
271
+ }
272
+ if (command === "log") { print({ operations: oh.store.log(integer(one(parsed, "limit"), "limit")), v: 1 }); return 0; }
273
+ if (command === "search") {
274
+ const query = parsed.positionals.join(" ");
275
+ const mode = one(parsed, "mode", "keyword");
276
+ if (query.length === 0 || (mode !== "keyword" && mode !== "semantic" && mode !== "hybrid")) throw new TypeError("search needs a query and a valid mode.");
277
+ const limit = integer(one(parsed, "limit"), "limit");
278
+ print(await oh.search(query, { ...(limit === undefined ? {} : { limit }), mode })); return 0;
279
+ }
280
+ if (command === "verify") { print(oh.verify()); return 0; }
281
+ if (command === "sync") {
282
+ const action = parsed.positionals[0];
283
+ if (action === "export") {
284
+ const after = integer(one(parsed, "after"), "after") ?? 0;
285
+ const limit = integer(one(parsed, "limit"), "limit") ?? 1000;
286
+ print(createOhSyncBundleV1(oh.store.spaceId, oh.store.exportOperations(after, limit))); return 0;
287
+ }
288
+ if (action === "import") {
289
+ const bundle = validated.syncBundle;
290
+ if (bundle === null) throw new TypeError("Invalid prepared sync import command.");
291
+ let imported = 0;
292
+ for (const operation of bundle.operations) if (oh.store.importOperation(operation).imported) imported += 1;
293
+ print({ head: oh.head(), imported, v: 1 }); return 0;
294
+ }
295
+ throw new TypeError("sync needs export or import.");
296
+ }
297
+ throw new TypeError(`Unknown command: ${command}`);
298
+ } finally {
299
+ await oh.close();
300
+ }
301
+ }
302
+
303
+ if (import.meta.main) {
304
+ runOhCli(process.argv.slice(2)).then((code) => { process.exitCode = code; }).catch((error) => {
305
+ process.stderr.write(`oh: ${error instanceof Error ? error.message : String(error)}\n`);
306
+ process.exitCode = 1;
307
+ });
308
+ }
@@ -0,0 +1,87 @@
1
+ import { canonicalJson, canonicalSha256, type JsonValue, type Sha256Hex } from "./canonical";
2
+ import { OH_GRAPH_FORMAT_VERSION_V1, OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1,
3
+ type KnowledgeGraphRecordKindV1 } from "./graph";
4
+ import { OH_CONTRACT_ID_V1, OH_ONTOLOGY_VERSION_V1 } from "./ontology";
5
+ import { OH_SCHEMA_FORMAT_VERSION_V1 } from "./schema";
6
+
7
+ export type OhContractManifestV1 = Readonly<{
8
+ contractId: typeof OH_CONTRACT_ID_V1;
9
+ contractSha256: Sha256Hex;
10
+ graphFormatVersion: typeof OH_GRAPH_FORMAT_VERSION_V1;
11
+ ontologyVersion: typeof OH_ONTOLOGY_VERSION_V1;
12
+ recordKinds: readonly KnowledgeGraphRecordKindV1[];
13
+ schemaFormatVersion: typeof OH_SCHEMA_FORMAT_VERSION_V1;
14
+ v: 1;
15
+ }>;
16
+
17
+ const manifestPayload = Object.freeze({
18
+ contractId: OH_CONTRACT_ID_V1,
19
+ graphFormatVersion: OH_GRAPH_FORMAT_VERSION_V1,
20
+ ontologyVersion: OH_ONTOLOGY_VERSION_V1,
21
+ recordKinds: OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1,
22
+ schemaFormatVersion: OH_SCHEMA_FORMAT_VERSION_V1,
23
+ v: 1 as const,
24
+ });
25
+
26
+ export const OH_CONTRACT_MANIFEST_V1: OhContractManifestV1 = Object.freeze({
27
+ ...manifestPayload,
28
+ contractSha256: canonicalSha256(manifestPayload),
29
+ });
30
+
31
+ /** Accepts only the complete, byte-equivalent V1 contract manifest. */
32
+ export function parseOhContractManifestV1(value: unknown): OhContractManifestV1 | null {
33
+ try {
34
+ return canonicalJson(value) === canonicalJson(OH_CONTRACT_MANIFEST_V1)
35
+ ? OH_CONTRACT_MANIFEST_V1 : null;
36
+ } catch { return null; }
37
+ }
38
+
39
+ export type OhRecordCodec<T extends JsonValue = JsonValue> = Readonly<{
40
+ kind: KnowledgeGraphRecordKindV1;
41
+ parse(value: unknown): T | null;
42
+ }>;
43
+
44
+ /** Optional semantic validation layered over the immutable generic record envelope. */
45
+ export class OhRecordCodecRegistry {
46
+ readonly #codecs = new Map<KnowledgeGraphRecordKindV1, OhRecordCodec>();
47
+ #sealed = false;
48
+
49
+ register(codec: OhRecordCodec): this {
50
+ if (this.#sealed) throw new TypeError("The codec registry is sealed.");
51
+ if (this.#codecs.has(codec.kind)) throw new TypeError(`A codec is already registered for ${codec.kind}.`);
52
+ this.#codecs.set(codec.kind, Object.freeze({ kind: codec.kind, parse: codec.parse }));
53
+ return this;
54
+ }
55
+
56
+ parse(kind: KnowledgeGraphRecordKindV1, value: unknown): JsonValue | null {
57
+ const codec = this.#codecs.get(kind);
58
+ if (codec !== undefined) return codec.parse(value);
59
+ try { canonicalJson(value); return value as JsonValue; } catch { return null; }
60
+ }
61
+
62
+ has(kind: KnowledgeGraphRecordKindV1): boolean {
63
+ return this.#codecs.has(kind);
64
+ }
65
+
66
+ /** Parses only through an explicitly registered codec. */
67
+ parseRequired(kind: KnowledgeGraphRecordKindV1, value: unknown): JsonValue | null {
68
+ const codec = this.#codecs.get(kind);
69
+ if (codec === undefined) return null;
70
+ try {
71
+ const parsed = codec.parse(value);
72
+ if (parsed === null) return null;
73
+ canonicalJson(parsed);
74
+ return parsed;
75
+ } catch { return null; }
76
+ }
77
+
78
+ /** Prevents the validation policy from changing after an ingress is created. */
79
+ seal(): this {
80
+ this.#sealed = true;
81
+ return this;
82
+ }
83
+
84
+ get sealed(): boolean {
85
+ return this.#sealed;
86
+ }
87
+ }
@@ -0,0 +1,147 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { canonicalJson, type JsonObject, type Sha256Hex } from "./canonical";
4
+ import { OH_CONTRACT_MANIFEST_V1 } from "./contract";
5
+ import { createKnowledgeGraphRecordV1, createKnowledgeGraphRevisionV1,
6
+ graphRevisionSha256V1, OH_GRAPH_LIMITS_V1, parseKnowledgeGraphRecordV1,
7
+ parseKnowledgeGraphRevisionV1, reduceKnowledgeGraphRevisionsV1 } from "./graph";
8
+ import { createKnowledgeActivityV1, createKnowledgeAssertionV1, createKnowledgeContextV1,
9
+ createKnowledgeEvidenceLinkV1, createKnowledgeInquiryV1, createKnowledgeStatementV1,
10
+ parseKnowledgeAssertionV1, parseKnowledgeEntityV1, parseKnowledgeEvidenceLinkV1,
11
+ parseKnowledgeStatementV1, type KnowledgeAssertionId, type KnowledgeEntityId,
12
+ type KnowledgeEvidenceId, type KnowledgeInquiryId,
13
+ type KnowledgeSchemaRefV1 } from "./ontology";
14
+ import { createKnowledgeSchemaRevisionV1, createKnowledgeVocabularyRevisionV1,
15
+ knowledgeSchemaRefV1, parseKnowledgeSchemaRevisionV1, verifyKnowledgeSchemaEvolutionV1 } from "./schema";
16
+
17
+ const digest = "a".repeat(64) as Sha256Hex;
18
+ const entityId = `kent_${"1".repeat(24)}` as KnowledgeEntityId;
19
+ const authorId = `kent_${"2".repeat(24)}` as KnowledgeEntityId;
20
+ const schemaRef: KnowledgeSchemaRefV1 = { code: "name", namespace: "oh.core", revision: 1,
21
+ schemaSha256: digest, v: 1 };
22
+
23
+ describe("ontology contracts", () => {
24
+ test("creates content-addressed contexts, statements, and inquiries", () => {
25
+ const context = createKnowledgeContextV1({ dimensions: [], scenario: "actual", v: 1 });
26
+ expect(context.ok).toBe(true);
27
+ const statement = createKnowledgeStatementV1({ object: { kind: "text", language: "en", text: "Ada", v: 1 },
28
+ predicate: schemaRef, qualifiers: [], subject: entityId, v: 1 });
29
+ expect(statement.ok).toBe(true);
30
+ if (!statement.ok) return;
31
+ expect(parseKnowledgeStatementV1(statement.value)).toEqual({ ok: true, value: statement.value });
32
+ expect(parseKnowledgeStatementV1({ ...statement.value, subject: authorId })).toMatchObject({ ok: false });
33
+ const inquiry = createKnowledgeInquiryV1({ answerForm: "narrative", authorEntityId: authorId,
34
+ contextSha256: context.ok ? context.value.contextSha256 : null,
35
+ createdAt: "2026-08-27T12:00:00.000Z", inquiryId: `kinq_${"3".repeat(24)}` as KnowledgeInquiryId,
36
+ language: "en", parentInquiryIds: [], privacy: "public", question: "What changed?",
37
+ status: "open", v: 1 });
38
+ expect(inquiry.ok).toBe(true);
39
+ });
40
+
41
+ test("requires canonical set ordering", () => {
42
+ const statement = createKnowledgeStatementV1({ object: { kind: "set", v: 1, values: [
43
+ { kind: "string", v: 1, value: "z" }, { kind: "string", v: 1, value: "a" },
44
+ ] }, predicate: schemaRef, qualifiers: [], subject: entityId, v: 1 });
45
+ expect(statement).toMatchObject({ ok: false });
46
+ expect(createKnowledgeStatementV1({ object: { canonicalizerSha256: digest,
47
+ canonicalValue: "canonical extension bytes", kind: "extension", mediaType: "application/json",
48
+ schema: schemaRef, v: 1, valueSha256: digest }, predicate: schemaRef, qualifiers: [],
49
+ subject: entityId, v: 1 })).toMatchObject({ ok: false, error: { code: "digest-mismatch" } });
50
+ });
51
+
52
+ test("preserves entity, activity, assertion, and evidence authority laws", () => {
53
+ expect(parseKnowledgeEntityV1({ entityId, identityOperationId: "identity.create",
54
+ identityRevision: 1, redirectEntityId: null, state: "active", v: 1 })).toMatchObject({ ok: true });
55
+ expect(parseKnowledgeEntityV1({ entityId, identityOperationId: "identity.redirect",
56
+ identityRevision: 2, redirectEntityId: null, state: "redirected", v: 1 })).toMatchObject({ ok: false });
57
+ const activity = createKnowledgeActivityV1({ actor: { entityId: authorId, kind: "entity", v: 1 },
58
+ inputSha256s: [], kind: "human-entry", occurredAt: "2026-08-27T12:00:00.000Z",
59
+ outputSha256s: [], policySha256: digest, tool: null, v: 1 });
60
+ expect(activity.ok).toBe(true);
61
+ const assertion = createKnowledgeAssertionV1({ acceptedPurposes: [],
62
+ assertionId: `kast_${"4".repeat(24)}` as KnowledgeAssertionId,
63
+ assertor: { kind: "model", model: schemaRef, receiptSha256: digest, v: 1 }, confidence: null,
64
+ contextSha256: null, provenanceActivitySha256: activity.ok ? activity.value.activitySha256 : digest,
65
+ reviewActivitySha256: null, stance: "reports", state: "proposed", statementSha256: digest, v: 1 });
66
+ expect(assertion.ok).toBe(true);
67
+ if (!assertion.ok) return;
68
+ expect(parseKnowledgeAssertionV1(assertion.value)).toEqual({ ok: true, value: assertion.value });
69
+ const { assertionSha256: _assertionSha256, ...assertionInput } = assertion.value;
70
+ expect(createKnowledgeAssertionV1({ ...assertionInput, acceptedPurposes: ["publication"],
71
+ state: "accepted-for-purpose" })).toMatchObject({ ok: false });
72
+ const evidence = createKnowledgeEvidenceLinkV1({ assertionSha256: assertion.value.assertionSha256,
73
+ bearing: "supports", disclosure: "public", evidenceId: `kevd_${"5".repeat(24)}` as KnowledgeEvidenceId,
74
+ observationSha256: digest, provenanceActivitySha256: activity.ok ? activity.value.activitySha256 : digest,
75
+ selector: "paragraph:1", sourceEntityId: null, v: 1 });
76
+ expect(evidence.ok).toBe(true);
77
+ if (evidence.ok) {
78
+ expect(parseKnowledgeEvidenceLinkV1({ ...evidence.value, bearing: "contradicts" })).toMatchObject({ ok: false });
79
+ }
80
+ });
81
+ });
82
+
83
+ describe("schema evolution", () => {
84
+ const localized = [{ language: "en", text: "Name", v: 1 as const }];
85
+ const revision = (revision: number, body: JsonObject, prior: Sha256Hex | null, compatibility: "additive" | "breaking") =>
86
+ createKnowledgeSchemaRevisionV1({ body, code: "person", compatibility, description: localized,
87
+ kind: "concept", labels: localized, namespace: "oh.core", previousSchemaSha256: prior, revision, v: 1 });
88
+
89
+ test("chains revisions and rejects a false additive claim", () => {
90
+ const first = revision(1, { required: ["name"] }, null, "additive");
91
+ const second = revision(2, { optional: ["url"], required: ["name"] }, first.schemaSha256, "additive");
92
+ expect(parseKnowledgeSchemaRevisionV1(first)).toEqual(first);
93
+ expect(verifyKnowledgeSchemaEvolutionV1(first, second)).toEqual({ ok: true });
94
+ const falseAdditive = revision(2, { required: [] }, first.schemaSha256, "additive");
95
+ expect(verifyKnowledgeSchemaEvolutionV1(first, falseAdditive)).toEqual({ ok: false, reason: "false-additive-claim" });
96
+ });
97
+
98
+ test("builds ordered vocabulary manifests", () => {
99
+ const first = revision(1, { type: "object" }, null, "additive");
100
+ const ref = knowledgeSchemaRefV1(first);
101
+ const vocabulary = createKnowledgeVocabularyRevisionV1({ namespace: "oh.core", revision: 1,
102
+ schemaRefs: [ref], v: 1 });
103
+ expect(vocabulary.schemaRefs).toEqual([ref]);
104
+ });
105
+ });
106
+
107
+ describe("graph contracts", () => {
108
+ test("addresses records and deterministically reduces revisions", () => {
109
+ const entity = createKnowledgeGraphRecordV1({ dependencies: [], key: "entity:ada", kind: "entity",
110
+ v: 1, value: { entityId } });
111
+ const statement = createKnowledgeGraphRecordV1({ dependencies: [entity.key], key: "statement:ada-name",
112
+ kind: "statement", v: 1, value: { text: "Ada" } });
113
+ expect(parseKnowledgeGraphRecordV1(entity)).toEqual(entity);
114
+ const first = createKnowledgeGraphRevisionV1({ changes: [
115
+ { kind: "put", record: statement, v: 1 }, { kind: "put", record: entity, v: 1 },
116
+ ], operationId: "op_first", parent: null });
117
+ expect(parseKnowledgeGraphRevisionV1(first)).toEqual(first);
118
+ expect(first.graphRevisionSha256).toBe(graphRevisionSha256V1({ changes: first.changes,
119
+ operationId: first.operationId, parentGraphRevisionSha256: first.parentGraphRevisionSha256,
120
+ recordsSha256: first.recordsSha256, revision: first.revision }));
121
+ expect(first.recordRefs.find((ref) => ref.key === statement.key)?.dependencies).toEqual([entity.key]);
122
+ expect(() => createKnowledgeGraphRevisionV1({ changes: [{ key: entity.key, kind: "tombstone",
123
+ priorSha256: entity.recordSha256, v: 1 }], operationId: "op_bad_delete", parent: first }))
124
+ .toThrow("Missing graph dependency");
125
+ const updatedStatement = createKnowledgeGraphRecordV1({ dependencies: [entity.key], key: statement.key,
126
+ kind: "statement", v: 1, value: { text: "Ada Lovelace" } });
127
+ const second = createKnowledgeGraphRevisionV1({ changes: [{ kind: "put", record: updatedStatement, v: 1 }],
128
+ operationId: "op_second", parent: first });
129
+ expect(reduceKnowledgeGraphRevisionsV1([second, first])?.graphRevisionSha256).toBe(second.graphRevisionSha256);
130
+ expect(reduceKnowledgeGraphRevisionsV1([{ ...second, recordsSha256: digest }])).toBeNull();
131
+ });
132
+
133
+ test("rejects snapshots beyond the V1 record ceiling before parsing their contents", () => {
134
+ expect(parseKnowledgeGraphRevisionV1({
135
+ changes: [], graphRevisionSha256: digest, operationId: "op_oversized",
136
+ parentGraphRevisionSha256: null,
137
+ recordRefs: Array.from({ length: OH_GRAPH_LIMITS_V1.recordsPerSnapshot + 1 }),
138
+ recordsSha256: digest, revision: 1, v: 1,
139
+ })).toBeNull();
140
+ });
141
+
142
+ test("manifest pins every wire-level contract input", () => {
143
+ expect(OH_CONTRACT_MANIFEST_V1.contractId).toBe("oh.ontology.v1");
144
+ expect(OH_CONTRACT_MANIFEST_V1.recordKinds).toContain("statement");
145
+ expect(canonicalJson(OH_CONTRACT_MANIFEST_V1)).toContain(OH_CONTRACT_MANIFEST_V1.contractSha256);
146
+ });
147
+ });