@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,53 @@
1
+ import * as Projection from "./projection";
2
+
3
+ export const OH_PROJECTION_FORMAT_VERSION_V1 = Projection.OH_PROJECTION_FORMAT_VERSION_V1;
4
+ export const OH_PROJECTION_INTERNAL_ENGINE_V1 = Projection.OH_PROJECTION_INTERNAL_ENGINE_V1;
5
+ export const OH_PROJECTION_LIMITS_V1 = Projection.OH_PROJECTION_LIMITS_V1;
6
+ export const OH_PROJECTION_RECORD_FACT_EXTRACTOR_V1 = Projection.OH_PROJECTION_RECORD_FACT_EXTRACTOR_V1;
7
+ export const OH_PROJECTION_SEMANTICS_V1 = Projection.OH_PROJECTION_SEMANTICS_V1;
8
+ export const createOhProjectionDatasetV1 = Projection.createOhProjectionDatasetV1;
9
+ export const createOhProjectionFactV1 = Projection.createOhProjectionFactV1;
10
+ export const createOhProjectionIdentityV1 = Projection.createOhProjectionIdentityV1;
11
+ export const createOhProjectionLiteralV1 = Projection.createOhProjectionLiteralV1;
12
+ export const createOhProjectionQueryV1 = Projection.createOhProjectionQueryV1;
13
+ export const createOhProjectionRecordFactsV1 = Projection.createOhProjectionRecordFactsV1;
14
+ export const createOhProjectionRulePackV1 = Projection.createOhProjectionRulePackV1;
15
+ export const createOhProjectionRuleV1 = Projection.createOhProjectionRuleV1;
16
+ export const createOhProjectionSnapshotV1 = Projection.createOhProjectionSnapshotV1;
17
+ export const evaluateOhProjectionV1 = Projection.evaluateOhProjectionV1;
18
+ export const invalidationForOhProjectionV1 = Projection.invalidationForOhProjectionV1;
19
+ export const isOhProjectionRecordKindV1 = Projection.isOhProjectionRecordKindV1;
20
+ export const ohProjectionConstantV1 = Projection.ohProjectionConstantV1;
21
+ export const ohProjectionVariableV1 = Projection.ohProjectionVariableV1;
22
+ export const parseOhProjectionDatasetV1 = Projection.parseOhProjectionDatasetV1;
23
+ export const parseOhProjectionFactV1 = Projection.parseOhProjectionFactV1;
24
+ export const parseOhProjectionIdentityV1 = Projection.parseOhProjectionIdentityV1;
25
+ export const parseOhProjectionLiteralV1 = Projection.parseOhProjectionLiteralV1;
26
+ export const parseOhProjectionProofV1 = Projection.parseOhProjectionProofV1;
27
+ export const parseOhProjectionQueryV1 = Projection.parseOhProjectionQueryV1;
28
+ export const parseOhProjectionResultV1 = Projection.parseOhProjectionResultV1;
29
+ export const parseOhProjectionRulePackV1 = Projection.parseOhProjectionRulePackV1;
30
+ export const parseOhProjectionRuleV1 = Projection.parseOhProjectionRuleV1;
31
+ export const parseOhProjectionSnapshotV1 = Projection.parseOhProjectionSnapshotV1;
32
+ export const parseOhProjectionTermV1 = Projection.parseOhProjectionTermV1;
33
+
34
+ export type {
35
+ OhProjectionAtomV1,
36
+ OhProjectionDatasetV1,
37
+ OhProjectionEvaluationOptionsV1,
38
+ OhProjectionFactSourceV1,
39
+ OhProjectionFactV1,
40
+ OhProjectionIdentityV1,
41
+ OhProjectionInvalidationReasonV1,
42
+ OhProjectionInvalidationV1,
43
+ OhProjectionLiteralV1,
44
+ OhProjectionProofV1,
45
+ OhProjectionQueryV1,
46
+ OhProjectionRecordFactOptionsV1,
47
+ OhProjectionResultRowV1,
48
+ OhProjectionResultV1,
49
+ OhProjectionRulePackV1,
50
+ OhProjectionRuleV1,
51
+ OhProjectionSnapshotV1,
52
+ OhProjectionTermV1,
53
+ } from "./projection";
@@ -0,0 +1,129 @@
1
+ import {
2
+ Database,
3
+ constant,
4
+ evaluate,
5
+ lit,
6
+ rule,
7
+ variable,
8
+ type Rule,
9
+ type Term,
10
+ } from "@suss/datalog";
11
+
12
+ import { canonicalJson, parseCanonicalJson } from "./canonical";
13
+ import {
14
+ evaluateOhProjectionWithMaterializerV1,
15
+ type OhProjectionAtomV1,
16
+ type OhProjectionDatasetV1,
17
+ type OhProjectionEvaluationOptionsV1,
18
+ type OhProjectionLiteralV1,
19
+ type OhProjectionQueryV1,
20
+ type OhProjectionResultV1,
21
+ type OhProjectionRulePackV1,
22
+ type OhProjectionSnapshotV1,
23
+ type OhProjectionTermV1,
24
+ } from "./projection";
25
+
26
+ export const OH_PROJECTION_SUSS_VERSION_V1 = "0.20.0" as const;
27
+ export const OH_PROJECTION_SUSS_ENGINE_V1 = "suss.datalog.v0-20-0.equivalence" as const;
28
+
29
+ function encodeAtom(value: OhProjectionAtomV1): string {
30
+ return canonicalJson(value);
31
+ }
32
+
33
+ function decodeAtom(value: string | number): OhProjectionAtomV1 {
34
+ if (typeof value !== "string") throw new TypeError("Suss returned a non-encoded projection atom.");
35
+ const decoded = parseCanonicalJson(value, 16 * 1024);
36
+ if (decoded !== null && typeof decoded === "object") {
37
+ throw new TypeError("Suss returned a non-atomic projection value.");
38
+ }
39
+ return decoded;
40
+ }
41
+
42
+ function sussTerm(term: OhProjectionTermV1): Term {
43
+ return term.kind === "constant" ? constant(encodeAtom(term.value)) : variable(term.name);
44
+ }
45
+
46
+ function sussLiteral(literal: OhProjectionLiteralV1) {
47
+ return lit(literal.relation, ...literal.terms.map(sussTerm));
48
+ }
49
+
50
+ function sussRule(input: OhProjectionRulePackV1["rules"][number]): Rule {
51
+ return rule(input.head.relation, input.head.terms.map(sussTerm),
52
+ input.body.map(sussLiteral), input.ruleId);
53
+ }
54
+
55
+ function projectionDomainSize(dataset: OhProjectionDatasetV1, rulePack: OhProjectionRulePackV1,
56
+ query: OhProjectionQueryV1): number {
57
+ const atoms = new Set<string>();
58
+ for (const fact of dataset.facts) for (const value of fact.tuple) atoms.add(encodeAtom(value));
59
+ const addTerms = (literal: OhProjectionLiteralV1): void => {
60
+ for (const term of literal.terms) if (term.kind === "constant") atoms.add(encodeAtom(term.value));
61
+ };
62
+ for (const item of rulePack.rules) {
63
+ addTerms(item.head);
64
+ for (const literal of item.body) addTerms(literal);
65
+ }
66
+ for (const literal of query.where) addTerms(literal);
67
+ return atoms.size;
68
+ }
69
+
70
+ function assertConservativeOutputBound(input: Readonly<{
71
+ dataset: OhProjectionDatasetV1;
72
+ maximumDerivedTuples: number;
73
+ query: OhProjectionQueryV1;
74
+ rulePack: OhProjectionRulePackV1;
75
+ }>): void {
76
+ const domainSize = BigInt(projectionDomainSize(input.dataset, input.rulePack, input.query));
77
+ const heads = new Map<string, number>();
78
+ for (const item of input.rulePack.rules) heads.set(item.head.relation, item.head.terms.length);
79
+ const baseCounts = new Map<string, number>();
80
+ for (const fact of input.dataset.facts) {
81
+ if (heads.has(fact.relation)) baseCounts.set(fact.relation, (baseCounts.get(fact.relation) ?? 0) + 1);
82
+ }
83
+ let possible = 0n;
84
+ const maximum = BigInt(input.maximumDerivedTuples);
85
+ for (const [relation, arity] of heads) {
86
+ const relationSpace = domainSize ** BigInt(arity);
87
+ const existing = BigInt(baseCounts.get(relation) ?? 0);
88
+ possible += relationSpace > existing ? relationSpace - existing : 0n;
89
+ if (possible > maximum) {
90
+ throw new RangeError("The Suss equivalence adapter cannot prove the requested derived-tuple bound before evaluation; use the bounded Oh evaluator.");
91
+ }
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Evaluates the positive rule pack with Suss 0.20.0, then requires its complete
97
+ * relation sets to equal Oh's bounded reference semantics before returning the
98
+ * canonical derived-only result. The conservative admission check keeps Suss,
99
+ * whose public evaluator has no execution-budget hook, inside Oh's tuple bound.
100
+ */
101
+ export function evaluateOhProjectionWithSussV1(input: Readonly<{
102
+ dataset: OhProjectionDatasetV1;
103
+ options?: OhProjectionEvaluationOptionsV1;
104
+ query: OhProjectionQueryV1;
105
+ rulePack: OhProjectionRulePackV1;
106
+ snapshot: OhProjectionSnapshotV1;
107
+ }>): OhProjectionResultV1 {
108
+ return evaluateOhProjectionWithMaterializerV1({
109
+ dataset: input.dataset,
110
+ engine: OH_PROJECTION_SUSS_ENGINE_V1,
111
+ ...(input.options === undefined ? {} : { options: input.options }),
112
+ query: input.query,
113
+ rulePack: input.rulePack,
114
+ snapshot: input.snapshot,
115
+ materialize: (program) => {
116
+ assertConservativeOutputBound(program);
117
+ const database = new Database();
118
+ for (const fact of program.dataset.facts) {
119
+ database.add(fact.relation, fact.tuple.map(encodeAtom));
120
+ }
121
+ evaluate(database, program.rulePack.rules.map(sussRule));
122
+ const relationFacts = new Map<string, readonly OhProjectionAtomV1[][]>();
123
+ for (const relation of database.relationNames()) {
124
+ relationFacts.set(relation, database.facts(relation).map((values) => values.map(decodeAtom)));
125
+ }
126
+ return { relationFacts };
127
+ },
128
+ });
129
+ }
@@ -0,0 +1,418 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { canonicalJson, canonicalSha256, type Sha256Hex } from "./canonical";
4
+ import { createKnowledgeGraphRecordV1, knowledgeGraphRecordRefV1,
5
+ type KnowledgeGraphRecordV1 } from "./graph";
6
+ import {
7
+ createOhProjectionDatasetV1,
8
+ createOhProjectionFactV1,
9
+ createOhProjectionIdentityV1,
10
+ createOhProjectionLiteralV1,
11
+ createOhProjectionQueryV1,
12
+ createOhProjectionRecordFactsV1,
13
+ createOhProjectionRulePackV1,
14
+ createOhProjectionRuleV1,
15
+ createOhProjectionSnapshotV1,
16
+ evaluateOhProjectionV1,
17
+ invalidationForOhProjectionV1,
18
+ OH_PROJECTION_LIMITS_V1,
19
+ OH_PROJECTION_RECORD_FACT_EXTRACTOR_V1,
20
+ ohProjectionConstantV1,
21
+ ohProjectionVariableV1,
22
+ parseOhProjectionIdentityV1,
23
+ parseOhProjectionProofV1,
24
+ parseOhProjectionQueryV1,
25
+ parseOhProjectionResultV1,
26
+ parseOhProjectionSnapshotV1,
27
+ type OhProjectionAtomV1,
28
+ type OhProjectionDatasetV1,
29
+ type OhProjectionProofV1,
30
+ type OhProjectionQueryV1,
31
+ type OhProjectionResultV1,
32
+ type OhProjectionRulePackV1,
33
+ type OhProjectionSnapshotV1,
34
+ } from "./projection";
35
+ import { evaluateOhProjectionWithSussV1, OH_PROJECTION_SUSS_ENGINE_V1,
36
+ OH_PROJECTION_SUSS_VERSION_V1 } from "./projection-suss";
37
+
38
+ const extractorSha256 = "e".repeat(64) as Sha256Hex;
39
+
40
+ const v = ohProjectionVariableV1;
41
+ const c = ohProjectionConstantV1;
42
+ const literal = (relation: string, ...terms: ReturnType<typeof v>[]) =>
43
+ createOhProjectionLiteralV1({ relation, terms });
44
+
45
+ function edgeRecord(from: string, to: string): KnowledgeGraphRecordV1 {
46
+ return createKnowledgeGraphRecordV1({ dependencies: [], key: `view:edge-${from}-${to}`,
47
+ kind: "view", v: 1, value: { from, to } });
48
+ }
49
+
50
+ function snapshot(records: readonly KnowledgeGraphRecordV1[], sequence = 1): OhProjectionSnapshotV1 {
51
+ const refs = [...records].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0)
52
+ .map(knowledgeGraphRecordRefV1);
53
+ return createOhProjectionSnapshotV1({
54
+ head: {
55
+ generation: sequence,
56
+ graphRevisionSha256: `${sequence.toString(16).padStart(64, "0")}` as Sha256Hex,
57
+ operationSha256: `${(sequence + 100).toString(16).padStart(64, "0")}` as Sha256Hex,
58
+ recordsSha256: canonicalSha256(refs),
59
+ sequence,
60
+ },
61
+ records,
62
+ spaceId: "session.test",
63
+ });
64
+ }
65
+
66
+ function dataset(records: readonly KnowledgeGraphRecordV1[], exactSnapshot: OhProjectionSnapshotV1,
67
+ reverse = false): OhProjectionDatasetV1 {
68
+ const ordered = reverse ? [...records].reverse() : records;
69
+ const facts = ordered.map((record) => createOhProjectionFactV1({ relation: "edge",
70
+ sources: [{ key: record.key, recordSha256: record.recordSha256, v: 1 }],
71
+ tuple: [(record.value as { from: string }).from, (record.value as { to: string }).to] }));
72
+ return createOhProjectionDatasetV1({ extractorSha256, factPackId: "test.edges",
73
+ factPackRevision: 1, facts, snapshot: exactSnapshot });
74
+ }
75
+
76
+ function reachabilityRules(reverse = false): OhProjectionRulePackV1 {
77
+ const x = v("x");
78
+ const y = v("y");
79
+ const z = v("z");
80
+ const direct = createOhProjectionRuleV1({ body: [literal("edge", x, y)],
81
+ head: literal("path", x, y), ruleId: "path.direct" });
82
+ const transitive = createOhProjectionRuleV1({ body: [literal("path", x, y), literal("edge", y, z)],
83
+ head: literal("path", x, z), ruleId: "path.transitive" });
84
+ return createOhProjectionRulePackV1({ rulePackId: "test.reachability", rulePackRevision: 1,
85
+ rules: reverse ? [transitive, direct] : [direct, transitive] });
86
+ }
87
+
88
+ function allPathsQuery(limit = 1_000): OhProjectionQueryV1 {
89
+ return createOhProjectionQueryV1({ find: ["x", "z"], limit, queryId: "all.paths",
90
+ where: [literal("path", v("x"), v("z"))] });
91
+ }
92
+
93
+ function countProofNodes(proof: OhProjectionProofV1): number {
94
+ return proof.kind === "derived"
95
+ ? 1 + proof.premises.reduce((sum, premise) => sum + countProofNodes(premise), 0)
96
+ : 1;
97
+ }
98
+
99
+ function expectedClosure(edges: readonly (readonly [string, string])[]): readonly OhProjectionAtomV1[][] {
100
+ const nodes = [...new Set(edges.flat())].sort();
101
+ const reachable = new Set(edges.map((edge) => canonicalJson(edge)));
102
+ for (const through of nodes) {
103
+ for (const from of nodes) {
104
+ for (const to of nodes) {
105
+ if (reachable.has(canonicalJson([from, through])) && reachable.has(canonicalJson([through, to]))) {
106
+ reachable.add(canonicalJson([from, to]));
107
+ }
108
+ }
109
+ }
110
+ }
111
+ return [...reachable].sort().map((value) => JSON.parse(value) as OhProjectionAtomV1[]);
112
+ }
113
+
114
+ function resignProjectionResult(value: Readonly<Record<string, unknown>>): Readonly<Record<string, unknown>> {
115
+ const { resultSha256: _resultSha256, ...payload } = value;
116
+ return { ...payload, resultSha256: canonicalSha256(payload) };
117
+ }
118
+
119
+ function resultWithEvaluation(result: OhProjectionResultV1,
120
+ evaluation: OhProjectionResultV1["evaluation"]): Readonly<Record<string, unknown>> {
121
+ const { projectionSha256: _projectionSha256, ...identityPayload } = {
122
+ ...result.identity,
123
+ evaluationSha256: canonicalSha256(evaluation),
124
+ };
125
+ const identity = { ...identityPayload, projectionSha256: canonicalSha256(identityPayload) };
126
+ return resignProjectionResult({ ...result, evaluation, identity });
127
+ }
128
+
129
+ describe("projection identity and validation", () => {
130
+ test("binds facts to an exact graph head without changing V1 graph bytes", () => {
131
+ const records = [edgeRecord("a", "b"), edgeRecord("b", "c")];
132
+ const exact = snapshot(records);
133
+ expect(parseOhProjectionSnapshotV1(exact)).toEqual(exact);
134
+ expect(exact.recordsSha256).toBe(canonicalSha256(records
135
+ .map(knowledgeGraphRecordRefV1).sort((left, right) => left.key.localeCompare(right.key))));
136
+
137
+ const facts = dataset(records, exact);
138
+ expect(facts.snapshotSha256).toBe(exact.snapshotSha256);
139
+ expect(facts.facts).toHaveLength(2);
140
+ expect(() => createOhProjectionDatasetV1({ extractorSha256, factPackId: "test.edges",
141
+ factPackRevision: 1, facts: [createOhProjectionFactV1({ relation: "edge",
142
+ sources: [{ key: records[0]!.key, recordSha256: records[1]!.recordSha256, v: 1 }],
143
+ tuple: ["a", "b"] })], snapshot: exact })).toThrow("exact input snapshot");
144
+
145
+ const forged = { ...facts, factsSha256: "f".repeat(64) as Sha256Hex };
146
+ expect(() => evaluateOhProjectionV1({ dataset: forged, query: allPathsQuery(),
147
+ rulePack: reachabilityRules(), snapshot: exact })).toThrow("Invalid projection snapshot");
148
+ });
149
+
150
+ test("canonicalizes declaration order and rejects non-positive AST shapes", () => {
151
+ const forward = reachabilityRules();
152
+ const reverse = reachabilityRules(true);
153
+ expect(reverse).toEqual(forward);
154
+ expect(parseOhProjectionQueryV1({ ...allPathsQuery(), where: [{
155
+ ...allPathsQuery().where[0], negated: true,
156
+ }] })).toBeNull();
157
+ expect(() => createOhProjectionRuleV1({ body: [literal("edge", v("x"), v("y"))],
158
+ head: literal("path", v("x"), v("unbound")), ruleId: "unsafe" })).toThrow("must be bound");
159
+ });
160
+
161
+ test("keeps every JSON primitive atom unambiguous", () => {
162
+ const record = edgeRecord("a", "b");
163
+ const exact = snapshot([record]);
164
+ const fact = createOhProjectionFactV1({ relation: "primitive",
165
+ sources: [{ key: record.key, recordSha256: record.recordSha256, v: 1 }],
166
+ tuple: [null, false, 0, "null"] });
167
+ const primitives = createOhProjectionDatasetV1({ extractorSha256, factPackId: "test.primitives",
168
+ factPackRevision: 1, facts: [fact], snapshot: exact });
169
+ const query = createOhProjectionQueryV1({ find: ["n", "b", "z", "s"], queryId: "all.primitives",
170
+ where: [createOhProjectionLiteralV1({ relation: "primitive",
171
+ terms: [v("n"), v("b"), v("z"), v("s")] })] });
172
+ const passthrough = createOhProjectionRulePackV1({ rulePackId: "test.primitives",
173
+ rulePackRevision: 1, rules: [createOhProjectionRuleV1({ body: query.where,
174
+ head: createOhProjectionLiteralV1({ relation: "copy",
175
+ terms: [v("n"), v("b"), v("z"), v("s")] }), ruleId: "copy.primitives" })] });
176
+ const input = { dataset: primitives, query, rulePack: passthrough, snapshot: exact };
177
+ const internal = evaluateOhProjectionV1(input);
178
+ expect(internal.rows[0]?.values).toEqual([null, false, 0, "null"]);
179
+ expect(evaluateOhProjectionWithSussV1(input).rows).toEqual(internal.rows);
180
+ });
181
+
182
+ test("coalesces duplicate tuples while retaining every exact source", () => {
183
+ const records = [edgeRecord("a", "b"), createKnowledgeGraphRecordV1({ dependencies: [],
184
+ key: "view:edge-a-b-second", kind: "view", v: 1, value: { from: "a", to: "b" } })];
185
+ const exact = snapshot(records);
186
+ const facts = records.map((record) => createOhProjectionFactV1({ relation: "edge",
187
+ sources: [{ key: record.key, recordSha256: record.recordSha256, v: 1 }], tuple: ["a", "b"] }));
188
+ const merged = createOhProjectionDatasetV1({ extractorSha256, factPackId: "test.edges",
189
+ factPackRevision: 1, facts, snapshot: exact });
190
+ expect(merged.facts).toHaveLength(1);
191
+ expect(merged.facts[0]?.sources).toHaveLength(2);
192
+ });
193
+
194
+ test("marks every identity change as an explicit full rebuild", () => {
195
+ const records = [edgeRecord("a", "b")];
196
+ const firstSnapshot = snapshot(records, 1);
197
+ const secondSnapshot = snapshot(records, 2);
198
+ const firstDataset = dataset(records, firstSnapshot);
199
+ const secondDataset = dataset(records, secondSnapshot);
200
+ const rules = reachabilityRules();
201
+ const query = allPathsQuery();
202
+ const first = createOhProjectionIdentityV1({ dataset: firstDataset, query, rulePack: rules,
203
+ snapshot: firstSnapshot });
204
+ expect(parseOhProjectionIdentityV1(first)).toEqual(first);
205
+ expect(parseOhProjectionIdentityV1({ ...first, querySha256: "f".repeat(64) })).toBeNull();
206
+ expect(invalidationForOhProjectionV1(first, first)).toEqual({ kind: "reusable", v: 1 });
207
+ const second = createOhProjectionIdentityV1({ dataset: secondDataset, query, rulePack: rules,
208
+ snapshot: secondSnapshot });
209
+ expect(invalidationForOhProjectionV1(first, second)).toEqual({ kind: "full-rebuild",
210
+ reasons: ["snapshot-changed", "dataset-changed"], v: 1 });
211
+ const changedQuery = createOhProjectionQueryV1({ ...query, limit: 1 });
212
+ const third = createOhProjectionIdentityV1({ dataset: firstDataset, query: changedQuery,
213
+ rulePack: rules, snapshot: firstSnapshot });
214
+ expect(invalidationForOhProjectionV1(first, third)).toEqual({ kind: "full-rebuild",
215
+ reasons: ["query-changed"], v: 1 });
216
+ const fourth = createOhProjectionIdentityV1({ dataset: firstDataset,
217
+ options: { maximumProofNodes: 1 }, query, rulePack: rules, snapshot: firstSnapshot });
218
+ expect(invalidationForOhProjectionV1(first, fourth)).toEqual({ kind: "full-rebuild",
219
+ reasons: ["evaluation-changed"], v: 1 });
220
+ });
221
+ });
222
+
223
+ describe("cached projection ingress", () => {
224
+ test("round-trips complete result and proof envelopes", () => {
225
+ const records = [edgeRecord("a", "b"), edgeRecord("b", "c")];
226
+ const exact = snapshot(records);
227
+ const result = evaluateOhProjectionV1({ dataset: dataset(records, exact),
228
+ query: allPathsQuery(), rulePack: reachabilityRules(), snapshot: exact });
229
+ expect(parseOhProjectionResultV1(result)).toEqual(result);
230
+ expect(parseOhProjectionResultV1(result, result.identity.projectionSha256)).toEqual(result);
231
+ expect(parseOhProjectionResultV1(result, "f".repeat(64) as Sha256Hex)).toBeNull();
232
+ const proof = result.rows[0]?.proofs[0] as OhProjectionProofV1;
233
+ expect(parseOhProjectionProofV1(proof)).toEqual(proof);
234
+ });
235
+
236
+ test("rejects cache tampering even when an attacker recomputes the outer digest", () => {
237
+ const records = [edgeRecord("a", "b"), edgeRecord("b", "c")];
238
+ const exact = snapshot(records);
239
+ const result = evaluateOhProjectionV1({ dataset: dataset(records, exact),
240
+ query: allPathsQuery(), rulePack: reachabilityRules(), snapshot: exact });
241
+ expect(parseOhProjectionResultV1({ ...result, resultSha256: "f".repeat(64) })).toBeNull();
242
+ const first = result.rows[0] as OhProjectionResultV1["rows"][number];
243
+ const rows = [{ ...first, supportCount: first.supportCount + 1 }, ...result.rows.slice(1)];
244
+ expect(parseOhProjectionResultV1(resignProjectionResult({ ...result, rows }))).toBeNull();
245
+ expect(parseOhProjectionResultV1(resignProjectionResult({ ...result,
246
+ engine: "oh.attacker.engine.v1" }))).toBeNull();
247
+ });
248
+
249
+ test("applies declared per-row proof limits and public aggregate ceilings on ingress", () => {
250
+ const records = [edgeRecord("a", "b"), edgeRecord("b", "c")];
251
+ const exact = snapshot(records);
252
+ const query = createOhProjectionQueryV1({ find: ["z"], queryId: "paths.from-a",
253
+ where: [createOhProjectionLiteralV1({ relation: "path", terms: [c("a"), v("z")] })] });
254
+ const result = evaluateOhProjectionV1({ dataset: dataset(records, exact), query,
255
+ rulePack: reachabilityRules(), snapshot: exact });
256
+ const smallerEvaluation = { ...result.evaluation, maximumProofNodes: 1 };
257
+ expect(parseOhProjectionResultV1(resultWithEvaluation(result, smallerEvaluation))).toBeNull();
258
+ expect(parseOhProjectionResultV1(resignProjectionResult({ ...result,
259
+ stats: { ...result.stats, proofNodes: OH_PROJECTION_LIMITS_V1.totalProofNodes + 1 } }))).toBeNull();
260
+ expect(parseOhProjectionResultV1(resignProjectionResult({ ...result,
261
+ evaluation: { ...result.evaluation, maximumWorkUnits: OH_PROJECTION_LIMITS_V1.workUnits + 1 } })))
262
+ .toBeNull();
263
+ });
264
+
265
+ test("rejects malformed and over-depth standalone proof trees", () => {
266
+ const digest = "a".repeat(64) as Sha256Hex;
267
+ const malformed = { kind: "fact", relation: "edge",
268
+ sources: [{ key: "view:one", recordSha256: digest, v: 1 }], tuple: ["a", "b"],
269
+ unexpected: true, v: 1 };
270
+ expect(parseOhProjectionProofV1(malformed)).toBeNull();
271
+ expect(parseOhProjectionProofV1({ kind: "fact", relation: "edge",
272
+ sources: [{ key: "view:one", recordSha256: digest, v: 1 },
273
+ { key: "view:one", recordSha256: "b".repeat(64), v: 1 }],
274
+ tuple: ["a", "b"], v: 1 })).toBeNull();
275
+
276
+ let proof: unknown = { kind: "fact", relation: "edge",
277
+ sources: [{ key: "view:one", recordSha256: digest, v: 1 }], tuple: ["a", "b"], v: 1 };
278
+ for (let depth = 0; depth <= OH_PROJECTION_LIMITS_V1.proofDepth; depth += 1) {
279
+ proof = { kind: "derived", premises: [proof], premisesTruncated: false,
280
+ relation: "path", ruleId: "path.deep", ruleSha256: digest, tuple: ["a", "b"], v: 1 };
281
+ }
282
+ expect(parseOhProjectionProofV1(proof)).toBeNull();
283
+ });
284
+ });
285
+
286
+ describe("positive recursive evaluation", () => {
287
+ test("computes a deterministic fixpoint and bounded proof trees", () => {
288
+ const records = [edgeRecord("a", "b"), edgeRecord("b", "c"), edgeRecord("c", "d")];
289
+ const exact = snapshot(records);
290
+ const result = evaluateOhProjectionV1({ dataset: dataset(records, exact, true),
291
+ options: { maximumProofNodes: 7 }, query: createOhProjectionQueryV1({ find: ["z"],
292
+ queryId: "paths.from-a", where: [createOhProjectionLiteralV1({ relation: "path",
293
+ terms: [c("a"), v("z")] })] }), rulePack: reachabilityRules(true), snapshot: exact });
294
+ expect(result.authority).toBe("derived");
295
+ expect(result.cache.strategy).toBe("full-rebuild");
296
+ expect(result.rows.map((row) => row.values)).toEqual([["b"], ["c"], ["d"]]);
297
+ expect(result.stats).toMatchObject({ baseFacts: 3, derivedFacts: 6, rounds: 3,
298
+ truncated: false });
299
+ expect(result.rows.every((row) => row.proofs.reduce((sum, proof) => sum + countProofNodes(proof), 0) <= 7))
300
+ .toBe(true);
301
+ expect(canonicalSha256((({ resultSha256: _digest, ...payload }) => payload)(result)))
302
+ .toBe(result.resultSha256);
303
+ });
304
+
305
+ test("fails closed at evaluation and arity bounds", () => {
306
+ const records = [edgeRecord("a", "b"), edgeRecord("b", "c")];
307
+ const exact = snapshot(records);
308
+ expect(() => evaluateOhProjectionV1({ dataset: dataset(records, exact),
309
+ options: { maximumDerivedTuples: 1 }, query: allPathsQuery(),
310
+ rulePack: reachabilityRules(), snapshot: exact })).toThrow("derived tuple bound");
311
+ const badQuery = createOhProjectionQueryV1({ find: ["x"], queryId: "bad.arity",
312
+ where: [literal("path", v("x"))] });
313
+ expect(() => evaluateOhProjectionV1({ dataset: dataset(records, exact), query: badQuery,
314
+ rulePack: reachabilityRules(), snapshot: exact })).toThrow("conflicting arities");
315
+ expect(() => evaluateOhProjectionV1({ dataset: dataset(records, exact),
316
+ options: { maximumWorkUnits: 1 }, query: allPathsQuery(),
317
+ rulePack: reachabilityRules(), snapshot: exact })).toThrow("work-unit bound");
318
+ });
319
+
320
+ test("marks proof-prefix exhaustion at both the row and result level", () => {
321
+ const records = [edgeRecord("a", "b"), edgeRecord("b", "c")];
322
+ const exact = snapshot(records);
323
+ const result = evaluateOhProjectionV1({ dataset: dataset(records, exact),
324
+ options: { maximumProofNodes: 1 }, query: createOhProjectionQueryV1({ find: ["x", "z"],
325
+ queryId: "two.edges", where: [literal("edge", v("x"), v("y")),
326
+ literal("edge", v("y"), v("z"))] }), rulePack: reachabilityRules(), snapshot: exact });
327
+ expect(result.rows).toHaveLength(1);
328
+ expect(result.rows[0]).toMatchObject({ proofsTruncated: true, values: ["a", "c"] });
329
+ expect(result.rows[0]?.proofs).toHaveLength(1);
330
+ expect(result.stats.proofsTruncated).toBe(true);
331
+ expect(parseOhProjectionResultV1(result)).toEqual(result);
332
+ });
333
+
334
+ test("bounds proofs across rows and reports collapsed alternative support", () => {
335
+ const records = [edgeRecord("a", "b"), edgeRecord("a", "c"), edgeRecord("d", "e")];
336
+ const exact = snapshot(records);
337
+ const result = evaluateOhProjectionV1({ dataset: dataset(records, exact),
338
+ options: { maximumTotalProofNodes: 1 }, query: createOhProjectionQueryV1({ find: ["x"],
339
+ queryId: "edge.sources", where: [literal("edge", v("x"), v("y"))] }),
340
+ rulePack: reachabilityRules(), snapshot: exact });
341
+ expect(result.rows).toHaveLength(2);
342
+ expect(result.rows[0]).toMatchObject({ proofsTruncated: false, supportCount: 2, values: ["a"] });
343
+ expect(result.rows[1]).toMatchObject({ proofs: [], proofsTruncated: true,
344
+ supportCount: 1, values: ["d"] });
345
+ expect(result.stats).toMatchObject({ proofNodes: 1, proofsTruncated: true });
346
+ expect(parseOhProjectionResultV1(result)).toEqual(result);
347
+ });
348
+
349
+ test("matches graph reachability across generated input orders", () => {
350
+ for (let seed = 1; seed <= 40; seed += 1) {
351
+ const edges: [string, string][] = [];
352
+ for (let from = 0; from < 7; from += 1) {
353
+ for (let to = from + 1; to < 7; to += 1) {
354
+ if (((seed * 31 + from * 17 + to * 13) % 5) < 2) edges.push([`n${from}`, `n${to}`]);
355
+ }
356
+ }
357
+ if (edges.length === 0) edges.push(["n0", "n1"]);
358
+ const records = edges.map(([from, to]) => edgeRecord(from, to));
359
+ const exact = snapshot(records);
360
+ const result = evaluateOhProjectionV1({ dataset: dataset(records, exact, seed % 2 === 0),
361
+ query: allPathsQuery(), rulePack: reachabilityRules(seed % 3 === 0), snapshot: exact });
362
+ expect(result.rows.map((row) => row.values)).toEqual([...expectedClosure(edges)]);
363
+ }
364
+ });
365
+
366
+ test("exposes stable structural record and dependency facts", () => {
367
+ const parent = createKnowledgeGraphRecordV1({ dependencies: [], key: "entity:parent",
368
+ kind: "entity", v: 1, value: { name: "parent" } });
369
+ const child = createKnowledgeGraphRecordV1({ dependencies: [parent.key], key: "statement:child",
370
+ kind: "statement", v: 1, value: { name: "child" } });
371
+ expect(OH_PROJECTION_RECORD_FACT_EXTRACTOR_V1).toMatchObject({ factPackId: "oh.record-facts",
372
+ factPackRevision: 1 });
373
+ expect(createOhProjectionRecordFactsV1([child, parent]).map((fact) => [fact.relation, fact.tuple]))
374
+ .toEqual([
375
+ ["oh.dependency", ["statement:child", "entity:parent"]],
376
+ ["oh.record", ["entity:parent", "entity", parent.recordSha256]],
377
+ ["oh.record", ["statement:child", "statement", child.recordSha256]],
378
+ ]);
379
+ });
380
+ });
381
+
382
+ describe("optional Suss equivalence adapter", () => {
383
+ test("pins the evaluated package and agrees on complete relation sets", () => {
384
+ expect(OH_PROJECTION_SUSS_VERSION_V1).toBe("0.20.0");
385
+ const records = [edgeRecord("a", "b"), edgeRecord("b", "c"), edgeRecord("a", "d")];
386
+ const exact = snapshot(records);
387
+ const input = { dataset: dataset(records, exact), query: allPathsQuery(),
388
+ rulePack: reachabilityRules(), snapshot: exact };
389
+ const internal = evaluateOhProjectionV1(input);
390
+ const external = evaluateOhProjectionWithSussV1(input);
391
+ expect(external.engine).toBe(OH_PROJECTION_SUSS_ENGINE_V1);
392
+ expect(external.identity).toMatchObject({ datasetSha256: internal.identity.datasetSha256,
393
+ evaluationSha256: internal.identity.evaluationSha256,
394
+ querySha256: internal.identity.querySha256, rulePackSha256: internal.identity.rulePackSha256,
395
+ snapshotSha256: internal.identity.snapshotSha256 });
396
+ expect(invalidationForOhProjectionV1(internal.identity, external.identity)).toEqual({
397
+ kind: "full-rebuild", reasons: ["engine-changed"], v: 1 });
398
+ expect(external.rows).toEqual(internal.rows);
399
+ expect(external.stats).toEqual(internal.stats);
400
+ expect(parseOhProjectionResultV1(external)).toEqual(external);
401
+ });
402
+
403
+ test("rejects programs Suss cannot prove within the requested bound", () => {
404
+ const single = [edgeRecord("a", "b")];
405
+ const singleSnapshot = snapshot(single);
406
+ const direct = createOhProjectionRulePackV1({ rulePackId: "test.direct", rulePackRevision: 1,
407
+ rules: [createOhProjectionRuleV1({ body: [literal("edge", v("x"), v("y"))],
408
+ head: literal("path", v("x"), v("y")), ruleId: "path.direct" })] });
409
+ expect(() => evaluateOhProjectionWithSussV1({ dataset: dataset(single, singleSnapshot),
410
+ options: { maximumDerivedTuples: 1 }, query: allPathsQuery(),
411
+ rulePack: direct, snapshot: singleSnapshot })).toThrow("cannot prove");
412
+ const records = [edgeRecord("a", "b"), edgeRecord("b", "c")];
413
+ const exact = snapshot(records);
414
+ expect(() => evaluateOhProjectionWithSussV1({ dataset: dataset(records, exact),
415
+ options: { maximumRounds: 1 }, query: allPathsQuery(),
416
+ rulePack: reachabilityRules(), snapshot: exact })).toThrow("evaluation round bound");
417
+ });
418
+ });