@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,1457 @@
1
+ import {
2
+ canonicalJson,
3
+ canonicalSha256,
4
+ hasExactKeys,
5
+ isPlainRecord,
6
+ orderedUnique,
7
+ parseSha256Hex,
8
+ safeCode,
9
+ sha256Hex,
10
+ sortUnique,
11
+ utf8ByteLength,
12
+ type JsonPrimitive,
13
+ type Sha256Hex,
14
+ } from "./canonical";
15
+ import { OH_CONTRACT_MANIFEST_V1 } from "./contract";
16
+ import {
17
+ knowledgeGraphRecordRefV1,
18
+ OH_GRAPH_LIMITS_V1,
19
+ OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1,
20
+ parseKnowledgeGraphRecordV1,
21
+ type KnowledgeGraphRecordKindV1,
22
+ type KnowledgeGraphRecordRefV1,
23
+ type KnowledgeGraphRecordV1,
24
+ } from "./graph";
25
+
26
+ export const OH_PROJECTION_FORMAT_VERSION_V1 = 1 as const;
27
+ export const OH_PROJECTION_SEMANTICS_V1 = "oh.projection.positive-datalog.v1" as const;
28
+ export const OH_PROJECTION_INTERNAL_ENGINE_V1 = "oh.naive.positive.v1" as const;
29
+
30
+ export const OH_PROJECTION_LIMITS_V1 = Object.freeze({
31
+ arity: 32,
32
+ atomBytes: 16 * 1024,
33
+ derivedTuples: 262_144,
34
+ facts: 262_144,
35
+ literalsPerRule: 64,
36
+ proofDepth: 128,
37
+ proofNodes: 4_096,
38
+ queryLiterals: 64,
39
+ queryMatches: 262_144,
40
+ queryResults: 65_536,
41
+ relations: 4_096,
42
+ resultBytes: 16 * 1024 * 1024,
43
+ rounds: 1_024,
44
+ rules: 1_024,
45
+ sourcesPerFact: 64,
46
+ totalProofNodes: 65_536,
47
+ variables: 256,
48
+ workUnits: 16_777_216,
49
+ });
50
+
51
+ export type OhProjectionAtomV1 = JsonPrimitive;
52
+
53
+ export type OhProjectionSnapshotV1 = Readonly<{
54
+ contractSha256: Sha256Hex;
55
+ generation: number;
56
+ graphRevisionSha256: Sha256Hex | null;
57
+ operationSha256: Sha256Hex | null;
58
+ recordRefs: readonly KnowledgeGraphRecordRefV1[];
59
+ recordsSha256: Sha256Hex;
60
+ sequence: number;
61
+ snapshotSha256: Sha256Hex;
62
+ spaceId: string;
63
+ v: 1;
64
+ }>;
65
+
66
+ export type OhProjectionFactSourceV1 = Readonly<{
67
+ key: string;
68
+ recordSha256: Sha256Hex;
69
+ v: 1;
70
+ }>;
71
+
72
+ export type OhProjectionFactV1 = Readonly<{
73
+ factSha256: Sha256Hex;
74
+ relation: string;
75
+ sources: readonly OhProjectionFactSourceV1[];
76
+ tuple: readonly OhProjectionAtomV1[];
77
+ v: 1;
78
+ }>;
79
+
80
+ export type OhProjectionDatasetV1 = Readonly<{
81
+ datasetSha256: Sha256Hex;
82
+ extractorSha256: Sha256Hex;
83
+ factPackId: string;
84
+ factPackRevision: number;
85
+ factPackSha256: Sha256Hex;
86
+ facts: readonly OhProjectionFactV1[];
87
+ factsSha256: Sha256Hex;
88
+ snapshotSha256: Sha256Hex;
89
+ v: 1;
90
+ }>;
91
+
92
+ const recordFactExtractorPayloadV1 = {
93
+ factPackId: "oh.record-facts",
94
+ factPackRevision: 1,
95
+ relations: ["oh.dependency", "oh.record"],
96
+ semantics: OH_PROJECTION_SEMANTICS_V1,
97
+ v: 1,
98
+ } as const;
99
+
100
+ export const OH_PROJECTION_RECORD_FACT_EXTRACTOR_V1 = Object.freeze({
101
+ ...recordFactExtractorPayloadV1,
102
+ extractorSha256: canonicalSha256(recordFactExtractorPayloadV1),
103
+ });
104
+
105
+ export type OhProjectionTermV1 =
106
+ | Readonly<{ kind: "constant"; v: 1; value: OhProjectionAtomV1 }>
107
+ | Readonly<{ kind: "variable"; name: string; v: 1 }>;
108
+
109
+ export type OhProjectionLiteralV1 = Readonly<{
110
+ relation: string;
111
+ terms: readonly OhProjectionTermV1[];
112
+ v: 1;
113
+ }>;
114
+
115
+ export type OhProjectionRuleV1 = Readonly<{
116
+ body: readonly OhProjectionLiteralV1[];
117
+ head: OhProjectionLiteralV1;
118
+ ruleId: string;
119
+ ruleSha256: Sha256Hex;
120
+ v: 1;
121
+ }>;
122
+
123
+ export type OhProjectionRulePackV1 = Readonly<{
124
+ rulePackId: string;
125
+ rulePackRevision: number;
126
+ rulePackSha256: Sha256Hex;
127
+ rules: readonly OhProjectionRuleV1[];
128
+ rulesSha256: Sha256Hex;
129
+ semantics: typeof OH_PROJECTION_SEMANTICS_V1;
130
+ v: 1;
131
+ }>;
132
+
133
+ export type OhProjectionQueryV1 = Readonly<{
134
+ find: readonly string[];
135
+ limit: number;
136
+ queryId: string;
137
+ querySha256: Sha256Hex;
138
+ where: readonly OhProjectionLiteralV1[];
139
+ v: 1;
140
+ }>;
141
+
142
+ export type OhProjectionIdentityV1 = Readonly<{
143
+ contractSha256: Sha256Hex;
144
+ datasetSha256: Sha256Hex;
145
+ engineSha256: Sha256Hex;
146
+ evaluationSha256: Sha256Hex;
147
+ projectionSha256: Sha256Hex;
148
+ querySha256: Sha256Hex;
149
+ rulePackSha256: Sha256Hex;
150
+ semantics: typeof OH_PROJECTION_SEMANTICS_V1;
151
+ snapshotSha256: Sha256Hex;
152
+ v: 1;
153
+ }>;
154
+
155
+ export type OhProjectionProofV1 =
156
+ | Readonly<{
157
+ kind: "fact";
158
+ relation: string;
159
+ sources: readonly OhProjectionFactSourceV1[];
160
+ tuple: readonly OhProjectionAtomV1[];
161
+ v: 1;
162
+ }>
163
+ | Readonly<{
164
+ kind: "derived";
165
+ premises: readonly OhProjectionProofV1[];
166
+ premisesTruncated: boolean;
167
+ relation: string;
168
+ ruleId: string;
169
+ ruleSha256: Sha256Hex;
170
+ tuple: readonly OhProjectionAtomV1[];
171
+ v: 1;
172
+ }>
173
+ | Readonly<{
174
+ kind: "truncated";
175
+ reason: "cycle" | "depth" | "nodes";
176
+ relation: string;
177
+ tuple: readonly OhProjectionAtomV1[];
178
+ v: 1;
179
+ }>;
180
+
181
+ export type OhProjectionResultRowV1 = Readonly<{
182
+ proofs: readonly OhProjectionProofV1[];
183
+ proofsTruncated: boolean;
184
+ supportCount: number;
185
+ values: readonly OhProjectionAtomV1[];
186
+ v: 1;
187
+ }>;
188
+
189
+ export type OhProjectionResultV1 = Readonly<{
190
+ authority: "derived";
191
+ cache: Readonly<{ strategy: "full-rebuild"; v: 1 }>;
192
+ engine: string;
193
+ evaluation: Readonly<{
194
+ maximumDerivedTuples: number;
195
+ maximumProofDepth: number;
196
+ maximumProofNodes: number;
197
+ maximumResultBytes: number;
198
+ maximumRounds: number;
199
+ maximumTotalProofNodes: number;
200
+ maximumWorkUnits: number;
201
+ v: 1;
202
+ }>;
203
+ identity: OhProjectionIdentityV1;
204
+ resultSha256: Sha256Hex;
205
+ rows: readonly OhProjectionResultRowV1[];
206
+ stats: Readonly<{
207
+ baseFacts: number;
208
+ derivedFacts: number;
209
+ proofNodes: number;
210
+ queryMatches: number;
211
+ relations: number;
212
+ rounds: number;
213
+ proofsTruncated: boolean;
214
+ truncated: boolean;
215
+ truncationReasons: readonly ("query-limit" | "result-bytes")[];
216
+ workUnits: number;
217
+ v: 1;
218
+ }>;
219
+ v: 1;
220
+ }>;
221
+
222
+ export type OhProjectionEvaluationOptionsV1 = Readonly<{
223
+ maximumDerivedTuples?: number;
224
+ maximumProofDepth?: number;
225
+ maximumProofNodes?: number;
226
+ maximumResultBytes?: number;
227
+ maximumRounds?: number;
228
+ maximumTotalProofNodes?: number;
229
+ maximumWorkUnits?: number;
230
+ }>;
231
+
232
+ export type OhProjectionInvalidationReasonV1 =
233
+ | "dataset-changed"
234
+ | "engine-changed"
235
+ | "evaluation-changed"
236
+ | "query-changed"
237
+ | "rule-pack-changed"
238
+ | "snapshot-changed";
239
+
240
+ export type OhProjectionInvalidationV1 =
241
+ | Readonly<{ kind: "reusable"; v: 1 }>
242
+ | Readonly<{
243
+ kind: "full-rebuild";
244
+ reasons: readonly OhProjectionInvalidationReasonV1[];
245
+ v: 1;
246
+ }>;
247
+
248
+ type ProjectionHeadInputV1 = Readonly<{
249
+ generation: number;
250
+ graphRevisionSha256: Sha256Hex | null;
251
+ operationSha256: Sha256Hex | null;
252
+ recordsSha256: Sha256Hex;
253
+ sequence: number;
254
+ }>;
255
+
256
+ function nonnegativeInteger(value: unknown): number | null {
257
+ return Number.isSafeInteger(value) && (value as number) >= 0 ? value as number : null;
258
+ }
259
+
260
+ function positiveInteger(value: unknown, maximum = Number.MAX_SAFE_INTEGER): number | null {
261
+ return Number.isSafeInteger(value) && (value as number) >= 1 && (value as number) <= maximum
262
+ ? value as number : null;
263
+ }
264
+
265
+ function projectionName(value: unknown, maximumLength = 128): string | null {
266
+ return safeCode(value, maximumLength);
267
+ }
268
+
269
+ function compareCanonical(left: unknown, right: unknown): number {
270
+ const leftKey = canonicalJson(left);
271
+ const rightKey = canonicalJson(right);
272
+ return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
273
+ }
274
+
275
+ function compareProjectionFacts(left: OhProjectionFactV1, right: OhProjectionFactV1): number {
276
+ return compareCanonical([left.relation, left.tuple], [right.relation, right.tuple]);
277
+ }
278
+
279
+ const INVALID_PROJECTION_ATOM = Symbol("invalid-projection-atom");
280
+
281
+ function atom(value: unknown): OhProjectionAtomV1 | typeof INVALID_PROJECTION_ATOM {
282
+ if (value !== null && typeof value !== "boolean" && typeof value !== "number" && typeof value !== "string") {
283
+ return INVALID_PROJECTION_ATOM;
284
+ }
285
+ try {
286
+ const encoded = canonicalJson(value);
287
+ return utf8ByteLength(encoded) <= OH_PROJECTION_LIMITS_V1.atomBytes
288
+ ? value as OhProjectionAtomV1 : INVALID_PROJECTION_ATOM;
289
+ } catch {
290
+ return INVALID_PROJECTION_ATOM;
291
+ }
292
+ }
293
+
294
+ function tuple(value: unknown): readonly OhProjectionAtomV1[] | null {
295
+ if (!Array.isArray(value) || value.length < 1 || value.length > OH_PROJECTION_LIMITS_V1.arity) return null;
296
+ const parsed = value.map(atom);
297
+ return parsed.some((item) => item === INVALID_PROJECTION_ATOM)
298
+ ? null : parsed as readonly OhProjectionAtomV1[];
299
+ }
300
+
301
+ function parseRecordRef(value: unknown): KnowledgeGraphRecordRefV1 | null {
302
+ if (!isPlainRecord(value) || !hasExactKeys(value, ["dependencies", "key", "kind", "sha256", "v"])
303
+ || value.v !== 1 || !Array.isArray(value.dependencies)) return null;
304
+ const key = safeCode(value.key, 512);
305
+ const kind = OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.find((candidate) => candidate === value.kind);
306
+ const sha256 = parseSha256Hex(value.sha256);
307
+ const dependencies = value.dependencies.map((dependency) => safeCode(dependency, 512));
308
+ if (key === null || kind === undefined || sha256 === null
309
+ || dependencies.length > OH_GRAPH_LIMITS_V1.dependenciesPerRecord
310
+ || dependencies.some((dependency) => dependency === null)
311
+ || !orderedUnique(dependencies as readonly string[], String)
312
+ || dependencies.includes(key)) return null;
313
+ return { dependencies: dependencies as readonly string[], key, kind, sha256, v: 1 };
314
+ }
315
+
316
+ export function createOhProjectionSnapshotV1(input: Readonly<{
317
+ head: ProjectionHeadInputV1;
318
+ records: readonly KnowledgeGraphRecordV1[];
319
+ spaceId: string;
320
+ }>): OhProjectionSnapshotV1 {
321
+ const spaceId = projectionName(input.spaceId);
322
+ const generation = nonnegativeInteger(input.head.generation);
323
+ const sequence = nonnegativeInteger(input.head.sequence);
324
+ const operationSha256 = input.head.operationSha256 === null ? null : parseSha256Hex(input.head.operationSha256);
325
+ const graphRevisionSha256 = input.head.graphRevisionSha256 === null
326
+ ? null : parseSha256Hex(input.head.graphRevisionSha256);
327
+ const declaredRecordsSha256 = parseSha256Hex(input.head.recordsSha256);
328
+ if (spaceId === null || generation === null || sequence === null || generation !== sequence
329
+ || (input.head.operationSha256 !== null && operationSha256 === null)
330
+ || (input.head.graphRevisionSha256 !== null && graphRevisionSha256 === null)
331
+ || ((sequence === 0) !== (operationSha256 === null))
332
+ || ((sequence === 0) !== (graphRevisionSha256 === null))
333
+ || declaredRecordsSha256 === null || input.records.length > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) {
334
+ throw new TypeError("Invalid projection snapshot head.");
335
+ }
336
+ const records = input.records.map(parseKnowledgeGraphRecordV1);
337
+ if (records.some((record) => record === null)) throw new TypeError("Invalid record in projection snapshot.");
338
+ const recordRefs = sortUnique(
339
+ (records as readonly KnowledgeGraphRecordV1[]).map(knowledgeGraphRecordRefV1),
340
+ (reference) => reference.key,
341
+ );
342
+ const recordsSha256 = canonicalSha256(recordRefs);
343
+ if (recordsSha256 !== declaredRecordsSha256) {
344
+ throw new TypeError("Projection snapshot records do not reproduce the declared head.");
345
+ }
346
+ const keys = new Set(recordRefs.map((reference) => reference.key));
347
+ if (recordRefs.some((reference) => reference.dependencies.some((dependency) => !keys.has(dependency)))) {
348
+ throw new TypeError("Projection snapshot has a missing record dependency.");
349
+ }
350
+ const payload = {
351
+ contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256,
352
+ generation,
353
+ graphRevisionSha256,
354
+ operationSha256,
355
+ recordRefs,
356
+ recordsSha256,
357
+ sequence,
358
+ spaceId,
359
+ v: 1 as const,
360
+ };
361
+ return { ...payload, snapshotSha256: canonicalSha256(payload) };
362
+ }
363
+
364
+ export function parseOhProjectionSnapshotV1(value: unknown): OhProjectionSnapshotV1 | null {
365
+ if (!isPlainRecord(value) || !hasExactKeys(value, ["contractSha256", "generation",
366
+ "graphRevisionSha256", "operationSha256", "recordRefs", "recordsSha256", "sequence",
367
+ "snapshotSha256", "spaceId", "v"]) || value.v !== 1 || !Array.isArray(value.recordRefs)
368
+ || value.recordRefs.length > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) return null;
369
+ const contractSha256 = parseSha256Hex(value.contractSha256);
370
+ const generation = nonnegativeInteger(value.generation);
371
+ const sequence = nonnegativeInteger(value.sequence);
372
+ const graphRevisionSha256 = value.graphRevisionSha256 === null
373
+ ? null : parseSha256Hex(value.graphRevisionSha256);
374
+ const operationSha256 = value.operationSha256 === null ? null : parseSha256Hex(value.operationSha256);
375
+ const recordsSha256 = parseSha256Hex(value.recordsSha256);
376
+ const snapshotSha256 = parseSha256Hex(value.snapshotSha256);
377
+ const spaceId = projectionName(value.spaceId);
378
+ const recordRefs = value.recordRefs.map(parseRecordRef);
379
+ if (contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256 || generation === null || sequence === null
380
+ || generation !== sequence || spaceId === null || recordsSha256 === null || snapshotSha256 === null
381
+ || (value.graphRevisionSha256 !== null && graphRevisionSha256 === null)
382
+ || (value.operationSha256 !== null && operationSha256 === null)
383
+ || ((sequence === 0) !== (operationSha256 === null))
384
+ || ((sequence === 0) !== (graphRevisionSha256 === null))
385
+ || recordRefs.some((reference) => reference === null)) return null;
386
+ const refs = recordRefs as readonly KnowledgeGraphRecordRefV1[];
387
+ if (!orderedUnique(refs, (reference) => reference.key) || canonicalSha256(refs) !== recordsSha256) return null;
388
+ const keys = new Set(refs.map((reference) => reference.key));
389
+ if (refs.some((reference) => reference.dependencies.some((dependency) => !keys.has(dependency)))) return null;
390
+ const payload = { contractSha256, generation, graphRevisionSha256, operationSha256,
391
+ recordRefs: refs, recordsSha256, sequence, spaceId, v: 1 as const };
392
+ return canonicalSha256(payload) === snapshotSha256 ? { ...payload, snapshotSha256 } : null;
393
+ }
394
+
395
+ export function createOhProjectionFactV1(input: Readonly<{
396
+ relation: string;
397
+ sources: readonly OhProjectionFactSourceV1[];
398
+ tuple: readonly OhProjectionAtomV1[];
399
+ }>): OhProjectionFactV1 {
400
+ const relation = projectionName(input.relation);
401
+ const parsedTuple = tuple(input.tuple);
402
+ if (relation === null || parsedTuple === null || input.sources.length < 1
403
+ || input.sources.length > OH_PROJECTION_LIMITS_V1.sourcesPerFact) {
404
+ throw new TypeError("Invalid projection fact.");
405
+ }
406
+ const sources = input.sources.map((source) => {
407
+ if (!isPlainRecord(source) || !hasExactKeys(source, ["key", "recordSha256", "v"]) || source.v !== 1) {
408
+ throw new TypeError("Invalid projection fact source.");
409
+ }
410
+ const key = safeCode(source.key, 512);
411
+ const recordSha256 = parseSha256Hex(source.recordSha256);
412
+ if (key === null || recordSha256 === null) throw new TypeError("Invalid projection fact source.");
413
+ return { key, recordSha256, v: 1 as const };
414
+ }).sort(compareCanonical);
415
+ if (!orderedUnique(sources, (source) => source.key)) {
416
+ throw new TypeError("Projection fact sources must have unique record keys.");
417
+ }
418
+ const payload = { relation, sources, tuple: parsedTuple, v: 1 as const };
419
+ return { ...payload, factSha256: canonicalSha256(payload) };
420
+ }
421
+
422
+ export function parseOhProjectionFactV1(value: unknown): OhProjectionFactV1 | null {
423
+ if (!isPlainRecord(value) || !hasExactKeys(value, ["factSha256", "relation", "sources", "tuple", "v"])
424
+ || value.v !== 1 || !Array.isArray(value.sources) || !Array.isArray(value.tuple)) return null;
425
+ const factSha256 = parseSha256Hex(value.factSha256);
426
+ try {
427
+ const fact = createOhProjectionFactV1({ relation: value.relation as string,
428
+ sources: value.sources as OhProjectionFactSourceV1[], tuple: value.tuple as OhProjectionAtomV1[] });
429
+ return factSha256 !== null && fact.factSha256 === factSha256 ? fact : null;
430
+ } catch {
431
+ return null;
432
+ }
433
+ }
434
+
435
+ function mergeProjectionFacts(facts: readonly OhProjectionFactV1[]): readonly OhProjectionFactV1[] {
436
+ const grouped = new Map<string, { relation: string; sources: Map<string, OhProjectionFactSourceV1>;
437
+ tuple: readonly OhProjectionAtomV1[] }>();
438
+ for (const fact of facts) {
439
+ const identity = canonicalJson([fact.relation, fact.tuple]);
440
+ let group = grouped.get(identity);
441
+ if (group === undefined) {
442
+ group = { relation: fact.relation, sources: new Map(), tuple: fact.tuple };
443
+ grouped.set(identity, group);
444
+ }
445
+ for (const source of fact.sources) {
446
+ const existing = group.sources.get(source.key);
447
+ if (existing !== undefined && existing.recordSha256 !== source.recordSha256) {
448
+ throw new TypeError("One fact source key is bound to multiple record digests.");
449
+ }
450
+ group.sources.set(source.key, source);
451
+ }
452
+ }
453
+ return [...grouped.values()].map((group) => createOhProjectionFactV1({ relation: group.relation,
454
+ sources: [...group.sources.values()], tuple: group.tuple })).sort(compareProjectionFacts);
455
+ }
456
+
457
+ export function createOhProjectionDatasetV1(input: Readonly<{
458
+ extractorSha256: Sha256Hex;
459
+ factPackId: string;
460
+ factPackRevision: number;
461
+ facts: readonly OhProjectionFactV1[];
462
+ snapshot: OhProjectionSnapshotV1;
463
+ }>): OhProjectionDatasetV1 {
464
+ const snapshot = parseOhProjectionSnapshotV1(input.snapshot);
465
+ const extractorSha256 = parseSha256Hex(input.extractorSha256);
466
+ const factPackId = projectionName(input.factPackId);
467
+ const factPackRevision = positiveInteger(input.factPackRevision);
468
+ if (snapshot === null || extractorSha256 === null || factPackId === null || factPackRevision === null
469
+ || input.facts.length > OH_PROJECTION_LIMITS_V1.facts) throw new TypeError("Invalid projection dataset.");
470
+ const parsedFacts = input.facts.map(parseOhProjectionFactV1);
471
+ if (parsedFacts.some((fact) => fact === null)) throw new TypeError("Invalid fact in projection dataset.");
472
+ const facts = mergeProjectionFacts(parsedFacts as readonly OhProjectionFactV1[]);
473
+ if (facts.length > OH_PROJECTION_LIMITS_V1.facts) throw new RangeError("Projection dataset has too many facts.");
474
+ const refs = new Map(snapshot.recordRefs.map((reference) => [reference.key, reference.sha256]));
475
+ for (const fact of facts) {
476
+ for (const source of fact.sources) {
477
+ if (refs.get(source.key) !== source.recordSha256) {
478
+ throw new TypeError("Projection fact source is not present at the exact input snapshot.");
479
+ }
480
+ }
481
+ }
482
+ const factPackPayload = { extractorSha256, factPackId, factPackRevision,
483
+ semantics: OH_PROJECTION_SEMANTICS_V1, v: 1 as const };
484
+ const factPackSha256 = canonicalSha256(factPackPayload);
485
+ const factsSha256 = canonicalSha256(facts);
486
+ const payload = { extractorSha256, factPackId, factPackRevision, factPackSha256, facts,
487
+ factsSha256, snapshotSha256: snapshot.snapshotSha256, v: 1 as const };
488
+ return { ...payload, datasetSha256: canonicalSha256(payload) };
489
+ }
490
+
491
+ export function parseOhProjectionDatasetV1(value: unknown,
492
+ snapshot: OhProjectionSnapshotV1): OhProjectionDatasetV1 | null {
493
+ if (!isPlainRecord(value) || !hasExactKeys(value, ["datasetSha256", "extractorSha256", "factPackId",
494
+ "factPackRevision", "factPackSha256", "facts", "factsSha256", "snapshotSha256", "v"])
495
+ || value.v !== 1 || !Array.isArray(value.facts)) return null;
496
+ const datasetSha256 = parseSha256Hex(value.datasetSha256);
497
+ const declaredFactPackSha256 = parseSha256Hex(value.factPackSha256);
498
+ const declaredFactsSha256 = parseSha256Hex(value.factsSha256);
499
+ try {
500
+ const dataset = createOhProjectionDatasetV1({ extractorSha256: value.extractorSha256 as Sha256Hex,
501
+ factPackId: value.factPackId as string, factPackRevision: value.factPackRevision as number,
502
+ facts: value.facts as OhProjectionFactV1[], snapshot });
503
+ return datasetSha256 !== null && declaredFactPackSha256 === dataset.factPackSha256
504
+ && declaredFactsSha256 === dataset.factsSha256 && value.snapshotSha256 === dataset.snapshotSha256
505
+ && dataset.datasetSha256 === datasetSha256
506
+ ? dataset : null;
507
+ } catch {
508
+ return null;
509
+ }
510
+ }
511
+
512
+ export function ohProjectionVariableV1(name: string): OhProjectionTermV1 {
513
+ const parsed = projectionName(name);
514
+ if (parsed === null) throw new TypeError("Invalid projection variable name.");
515
+ return { kind: "variable", name: parsed, v: 1 };
516
+ }
517
+
518
+ export function ohProjectionConstantV1(value: OhProjectionAtomV1): OhProjectionTermV1 {
519
+ const parsed = atom(value);
520
+ if (parsed === INVALID_PROJECTION_ATOM) throw new TypeError("Invalid projection constant.");
521
+ return { kind: "constant", v: 1, value: parsed };
522
+ }
523
+
524
+ export function createOhProjectionLiteralV1(input: Readonly<{
525
+ relation: string;
526
+ terms: readonly OhProjectionTermV1[];
527
+ }>): OhProjectionLiteralV1 {
528
+ const relation = projectionName(input.relation);
529
+ if (relation === null || input.terms.length < 1 || input.terms.length > OH_PROJECTION_LIMITS_V1.arity) {
530
+ throw new TypeError("Invalid projection literal.");
531
+ }
532
+ const terms = input.terms.map((term) => parseOhProjectionTermV1(term));
533
+ if (terms.some((term) => term === null)) throw new TypeError("Invalid term in projection literal.");
534
+ return { relation, terms: terms as readonly OhProjectionTermV1[], v: 1 };
535
+ }
536
+
537
+ export function parseOhProjectionTermV1(value: unknown): OhProjectionTermV1 | null {
538
+ if (!isPlainRecord(value) || value.v !== 1) return null;
539
+ if (value.kind === "variable" && hasExactKeys(value, ["kind", "name", "v"])) {
540
+ const name = projectionName(value.name);
541
+ return name === null ? null : { kind: "variable", name, v: 1 };
542
+ }
543
+ if (value.kind === "constant" && hasExactKeys(value, ["kind", "v", "value"])) {
544
+ const parsed = atom(value.value);
545
+ return parsed === INVALID_PROJECTION_ATOM ? null : { kind: "constant", v: 1, value: parsed };
546
+ }
547
+ return null;
548
+ }
549
+
550
+ export function parseOhProjectionLiteralV1(value: unknown): OhProjectionLiteralV1 | null {
551
+ if (!isPlainRecord(value) || !hasExactKeys(value, ["relation", "terms", "v"])
552
+ || value.v !== 1 || !Array.isArray(value.terms)) return null;
553
+ try {
554
+ return createOhProjectionLiteralV1({ relation: value.relation as string,
555
+ terms: value.terms as OhProjectionTermV1[] });
556
+ } catch {
557
+ return null;
558
+ }
559
+ }
560
+
561
+ function literalVariables(literal: OhProjectionLiteralV1): readonly string[] {
562
+ return literal.terms.flatMap((term) => term.kind === "variable" ? [term.name] : []);
563
+ }
564
+
565
+ export function createOhProjectionRuleV1(input: Readonly<{
566
+ body: readonly OhProjectionLiteralV1[];
567
+ head: OhProjectionLiteralV1;
568
+ ruleId: string;
569
+ }>): OhProjectionRuleV1 {
570
+ const ruleId = projectionName(input.ruleId);
571
+ const head = parseOhProjectionLiteralV1(input.head);
572
+ if (ruleId === null || head === null || input.body.length < 1
573
+ || input.body.length > OH_PROJECTION_LIMITS_V1.literalsPerRule) throw new TypeError("Invalid projection rule.");
574
+ const body = input.body.map(parseOhProjectionLiteralV1);
575
+ if (body.some((literal) => literal === null)) throw new TypeError("Invalid body literal in projection rule.");
576
+ const bound = new Set((body as readonly OhProjectionLiteralV1[]).flatMap(literalVariables));
577
+ if (literalVariables(head).some((variable) => !bound.has(variable))
578
+ || bound.size > OH_PROJECTION_LIMITS_V1.variables) {
579
+ throw new TypeError("Every projection rule head variable must be bound in its body.");
580
+ }
581
+ const payload = { body: body as readonly OhProjectionLiteralV1[], head, ruleId, v: 1 as const };
582
+ return { ...payload, ruleSha256: canonicalSha256(payload) };
583
+ }
584
+
585
+ export function parseOhProjectionRuleV1(value: unknown): OhProjectionRuleV1 | null {
586
+ if (!isPlainRecord(value) || !hasExactKeys(value, ["body", "head", "ruleId", "ruleSha256", "v"])
587
+ || value.v !== 1 || !Array.isArray(value.body)) return null;
588
+ const ruleSha256 = parseSha256Hex(value.ruleSha256);
589
+ try {
590
+ const rule = createOhProjectionRuleV1({ body: value.body as OhProjectionLiteralV1[],
591
+ head: value.head as OhProjectionLiteralV1, ruleId: value.ruleId as string });
592
+ return ruleSha256 !== null && rule.ruleSha256 === ruleSha256 ? rule : null;
593
+ } catch {
594
+ return null;
595
+ }
596
+ }
597
+
598
+ export function createOhProjectionRulePackV1(input: Readonly<{
599
+ rulePackId: string;
600
+ rulePackRevision: number;
601
+ rules: readonly OhProjectionRuleV1[];
602
+ }>): OhProjectionRulePackV1 {
603
+ const rulePackId = projectionName(input.rulePackId);
604
+ const rulePackRevision = positiveInteger(input.rulePackRevision);
605
+ if (rulePackId === null || rulePackRevision === null || input.rules.length < 1
606
+ || input.rules.length > OH_PROJECTION_LIMITS_V1.rules) throw new TypeError("Invalid projection rule pack.");
607
+ const parsedRules = input.rules.map(parseOhProjectionRuleV1);
608
+ if (parsedRules.some((rule) => rule === null)) throw new TypeError("Invalid rule in projection rule pack.");
609
+ const rules = sortUnique(parsedRules as readonly OhProjectionRuleV1[], (rule) => rule.ruleId);
610
+ const rulesSha256 = canonicalSha256(rules);
611
+ const payload = { rulePackId, rulePackRevision, rules, rulesSha256,
612
+ semantics: OH_PROJECTION_SEMANTICS_V1, v: 1 as const };
613
+ return { ...payload, rulePackSha256: canonicalSha256(payload) };
614
+ }
615
+
616
+ export function parseOhProjectionRulePackV1(value: unknown): OhProjectionRulePackV1 | null {
617
+ if (!isPlainRecord(value) || !hasExactKeys(value, ["rulePackId", "rulePackRevision",
618
+ "rulePackSha256", "rules", "rulesSha256", "semantics", "v"]) || value.v !== 1
619
+ || value.semantics !== OH_PROJECTION_SEMANTICS_V1 || !Array.isArray(value.rules)) return null;
620
+ const rulePackSha256 = parseSha256Hex(value.rulePackSha256);
621
+ const rulesSha256 = parseSha256Hex(value.rulesSha256);
622
+ try {
623
+ const pack = createOhProjectionRulePackV1({ rulePackId: value.rulePackId as string,
624
+ rulePackRevision: value.rulePackRevision as number, rules: value.rules as OhProjectionRuleV1[] });
625
+ return rulePackSha256 === pack.rulePackSha256 && rulesSha256 === pack.rulesSha256 ? pack : null;
626
+ } catch {
627
+ return null;
628
+ }
629
+ }
630
+
631
+ export function createOhProjectionQueryV1(input: Readonly<{
632
+ find: readonly string[];
633
+ limit?: number;
634
+ queryId: string;
635
+ where: readonly OhProjectionLiteralV1[];
636
+ }>): OhProjectionQueryV1 {
637
+ const queryId = projectionName(input.queryId);
638
+ const limit = positiveInteger(input.limit ?? 1_000, OH_PROJECTION_LIMITS_V1.queryResults);
639
+ if (queryId === null || limit === null || input.find.length < 1
640
+ || input.find.length > OH_PROJECTION_LIMITS_V1.arity || input.where.length < 1
641
+ || input.where.length > OH_PROJECTION_LIMITS_V1.queryLiterals) throw new TypeError("Invalid projection query.");
642
+ const find = input.find.map((name) => projectionName(name));
643
+ const where = input.where.map(parseOhProjectionLiteralV1);
644
+ if (find.some((name) => name === null) || !orderedUnique([...find as string[]].sort(), String)
645
+ || where.some((literal) => literal === null)) throw new TypeError("Invalid projection query variables or literals.");
646
+ const bound = new Set((where as readonly OhProjectionLiteralV1[]).flatMap(literalVariables));
647
+ if ((find as readonly string[]).some((name) => !bound.has(name)) || bound.size > OH_PROJECTION_LIMITS_V1.variables) {
648
+ throw new TypeError("Every projected query variable must be bound in the query body.");
649
+ }
650
+ const payload = { find: find as readonly string[], limit, queryId,
651
+ where: where as readonly OhProjectionLiteralV1[], v: 1 as const };
652
+ return { ...payload, querySha256: canonicalSha256(payload) };
653
+ }
654
+
655
+ export function parseOhProjectionQueryV1(value: unknown): OhProjectionQueryV1 | null {
656
+ if (!isPlainRecord(value) || !hasExactKeys(value, ["find", "limit", "queryId", "querySha256", "where", "v"])
657
+ || value.v !== 1 || !Array.isArray(value.find) || !Array.isArray(value.where)) return null;
658
+ const querySha256 = parseSha256Hex(value.querySha256);
659
+ try {
660
+ const query = createOhProjectionQueryV1({ find: value.find as string[], limit: value.limit as number,
661
+ queryId: value.queryId as string, where: value.where as OhProjectionLiteralV1[] });
662
+ return querySha256 !== null && query.querySha256 === querySha256 ? query : null;
663
+ } catch {
664
+ return null;
665
+ }
666
+ }
667
+
668
+ export function createOhProjectionIdentityV1(input: Readonly<{
669
+ dataset: OhProjectionDatasetV1;
670
+ engine?: string;
671
+ options?: OhProjectionEvaluationOptionsV1;
672
+ query: OhProjectionQueryV1;
673
+ rulePack: OhProjectionRulePackV1;
674
+ snapshot: OhProjectionSnapshotV1;
675
+ }>): OhProjectionIdentityV1 {
676
+ const snapshot = parseOhProjectionSnapshotV1(input.snapshot);
677
+ const dataset = snapshot === null ? null : parseOhProjectionDatasetV1(input.dataset, snapshot);
678
+ const query = parseOhProjectionQueryV1(input.query);
679
+ const rulePack = parseOhProjectionRulePackV1(input.rulePack);
680
+ if (snapshot === null || dataset === null || query === null || rulePack === null) {
681
+ throw new TypeError("Invalid projection identity input.");
682
+ }
683
+ const engine = safeCode(input.engine ?? OH_PROJECTION_INTERNAL_ENGINE_V1, 256);
684
+ if (engine === null) throw new TypeError("Invalid projection engine identity.");
685
+ const evaluation = { ...resolveEvaluationOptions(input.options ?? {}), v: 1 as const };
686
+ const payload = {
687
+ contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256,
688
+ datasetSha256: dataset.datasetSha256,
689
+ engineSha256: canonicalSha256({ engine, v: 1 }),
690
+ evaluationSha256: canonicalSha256(evaluation),
691
+ querySha256: query.querySha256,
692
+ rulePackSha256: rulePack.rulePackSha256,
693
+ semantics: OH_PROJECTION_SEMANTICS_V1,
694
+ snapshotSha256: snapshot.snapshotSha256,
695
+ v: 1 as const,
696
+ };
697
+ return { ...payload, projectionSha256: canonicalSha256(payload) };
698
+ }
699
+
700
+ export function parseOhProjectionIdentityV1(value: unknown): OhProjectionIdentityV1 | null {
701
+ if (!isPlainRecord(value) || !hasExactKeys(value, ["contractSha256", "datasetSha256",
702
+ "engineSha256", "evaluationSha256", "projectionSha256", "querySha256", "rulePackSha256",
703
+ "semantics", "snapshotSha256", "v"])
704
+ || value.v !== 1 || value.semantics !== OH_PROJECTION_SEMANTICS_V1) return null;
705
+ const contractSha256 = parseSha256Hex(value.contractSha256);
706
+ const datasetSha256 = parseSha256Hex(value.datasetSha256);
707
+ const engineSha256 = parseSha256Hex(value.engineSha256);
708
+ const evaluationSha256 = parseSha256Hex(value.evaluationSha256);
709
+ const projectionSha256 = parseSha256Hex(value.projectionSha256);
710
+ const querySha256 = parseSha256Hex(value.querySha256);
711
+ const rulePackSha256 = parseSha256Hex(value.rulePackSha256);
712
+ const snapshotSha256 = parseSha256Hex(value.snapshotSha256);
713
+ if (contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256 || datasetSha256 === null
714
+ || engineSha256 === null || evaluationSha256 === null || projectionSha256 === null
715
+ || querySha256 === null || rulePackSha256 === null
716
+ || snapshotSha256 === null) return null;
717
+ const payload = { contractSha256, datasetSha256, engineSha256, evaluationSha256,
718
+ querySha256, rulePackSha256,
719
+ semantics: OH_PROJECTION_SEMANTICS_V1, snapshotSha256, v: 1 as const };
720
+ return canonicalSha256(payload) === projectionSha256 ? { ...payload, projectionSha256 } : null;
721
+ }
722
+
723
+ export function invalidationForOhProjectionV1(previous: OhProjectionIdentityV1,
724
+ next: OhProjectionIdentityV1): OhProjectionInvalidationV1 {
725
+ const parsedPrevious = parseOhProjectionIdentityV1(previous);
726
+ const parsedNext = parseOhProjectionIdentityV1(next);
727
+ if (parsedPrevious === null || parsedNext === null) throw new TypeError("Invalid projection identity.");
728
+ if (parsedPrevious.projectionSha256 === parsedNext.projectionSha256) return { kind: "reusable", v: 1 };
729
+ const reasons: OhProjectionInvalidationReasonV1[] = [];
730
+ if (parsedPrevious.snapshotSha256 !== parsedNext.snapshotSha256) reasons.push("snapshot-changed");
731
+ if (parsedPrevious.datasetSha256 !== parsedNext.datasetSha256) reasons.push("dataset-changed");
732
+ if (parsedPrevious.engineSha256 !== parsedNext.engineSha256) reasons.push("engine-changed");
733
+ if (parsedPrevious.evaluationSha256 !== parsedNext.evaluationSha256) reasons.push("evaluation-changed");
734
+ if (parsedPrevious.rulePackSha256 !== parsedNext.rulePackSha256) reasons.push("rule-pack-changed");
735
+ if (parsedPrevious.querySha256 !== parsedNext.querySha256) reasons.push("query-changed");
736
+ return { kind: "full-rebuild", reasons, v: 1 };
737
+ }
738
+
739
+ type TupleReference = Readonly<{ relation: string; tuple: readonly OhProjectionAtomV1[] }>;
740
+ type FactWitness = Readonly<{ kind: "fact"; sources: readonly OhProjectionFactSourceV1[] }>;
741
+ type DerivedWitness = Readonly<{
742
+ kind: "derived";
743
+ premises: readonly TupleReference[];
744
+ rule: OhProjectionRuleV1;
745
+ }>;
746
+ type TupleState = Readonly<{
747
+ tuple: readonly OhProjectionAtomV1[];
748
+ witness: FactWitness | DerivedWitness;
749
+ }>;
750
+ type RelationState = Map<string, TupleState>;
751
+ type MaterializedProjection = Readonly<{
752
+ baseFacts: number;
753
+ derivedFacts: number;
754
+ relations: Map<string, RelationState>;
755
+ rounds: number;
756
+ }>;
757
+
758
+ function tupleKey(value: readonly OhProjectionAtomV1[]): string {
759
+ return canonicalJson(value);
760
+ }
761
+
762
+ function referenceKey(reference: TupleReference): string {
763
+ return canonicalJson([reference.relation, reference.tuple]);
764
+ }
765
+
766
+ function relationTuples(relations: Map<string, RelationState>, relation: string): readonly TupleState[] {
767
+ return [...(relations.get(relation)?.values() ?? [])]
768
+ .sort((left, right) => compareCanonical(left.tuple, right.tuple));
769
+ }
770
+
771
+ function setArity(arities: Map<string, number>, relation: string, arity: number): void {
772
+ const existing = arities.get(relation);
773
+ if (existing !== undefined && existing !== arity) {
774
+ throw new TypeError(`Projection relation ${relation} is used with conflicting arities.`);
775
+ }
776
+ arities.set(relation, arity);
777
+ if (arities.size > OH_PROJECTION_LIMITS_V1.relations) throw new RangeError("Projection uses too many relations.");
778
+ }
779
+
780
+ function validateProgramArities(dataset: OhProjectionDatasetV1, rulePack: OhProjectionRulePackV1,
781
+ query: OhProjectionQueryV1): void {
782
+ const arities = new Map<string, number>();
783
+ for (const fact of dataset.facts) setArity(arities, fact.relation, fact.tuple.length);
784
+ for (const rule of rulePack.rules) {
785
+ setArity(arities, rule.head.relation, rule.head.terms.length);
786
+ for (const literal of rule.body) setArity(arities, literal.relation, literal.terms.length);
787
+ }
788
+ for (const literal of query.where) setArity(arities, literal.relation, literal.terms.length);
789
+ }
790
+
791
+ type Binding = Map<string, OhProjectionAtomV1>;
792
+
793
+ function sameAtom(left: OhProjectionAtomV1, right: OhProjectionAtomV1): boolean {
794
+ return left === right;
795
+ }
796
+
797
+ function unifyLiteral(literal: OhProjectionLiteralV1, state: TupleState, binding: Binding): Binding | null {
798
+ const next = new Map(binding);
799
+ for (let index = 0; index < literal.terms.length; index += 1) {
800
+ const term = literal.terms[index] as OhProjectionTermV1;
801
+ const value = state.tuple[index] as OhProjectionAtomV1;
802
+ if (term.kind === "constant") {
803
+ if (!sameAtom(term.value, value)) return null;
804
+ continue;
805
+ }
806
+ if (next.has(term.name)) {
807
+ if (!sameAtom(next.get(term.name) as OhProjectionAtomV1, value)) return null;
808
+ } else next.set(term.name, value);
809
+ }
810
+ return next;
811
+ }
812
+
813
+ type BodyMatch = Readonly<{ binding: Binding; premises: readonly TupleReference[] }>;
814
+
815
+ type ProjectionWorkBudget = { maximum: number; units: number };
816
+
817
+ function consumeWorkUnit(budget: ProjectionWorkBudget): void {
818
+ if (budget.units >= budget.maximum) throw new RangeError("Projection exceeds its work-unit bound.");
819
+ budget.units += 1;
820
+ }
821
+
822
+ function matchBody(relations: Map<string, RelationState>, body: readonly OhProjectionLiteralV1[],
823
+ maximumMatches: number, work: ProjectionWorkBudget): readonly BodyMatch[] {
824
+ let matches: readonly BodyMatch[] = [{ binding: new Map(), premises: [] }];
825
+ for (const literal of body) {
826
+ const next: BodyMatch[] = [];
827
+ const candidates = relationTuples(relations, literal.relation);
828
+ for (const match of matches) {
829
+ for (const candidate of candidates) {
830
+ consumeWorkUnit(work);
831
+ const binding = unifyLiteral(literal, candidate, match.binding);
832
+ if (binding === null) continue;
833
+ next.push({ binding, premises: [...match.premises, { relation: literal.relation,
834
+ tuple: candidate.tuple }] });
835
+ if (next.length > maximumMatches) throw new RangeError("Projection join exceeds its match bound.");
836
+ }
837
+ }
838
+ matches = next;
839
+ if (matches.length === 0) break;
840
+ }
841
+ return matches;
842
+ }
843
+
844
+ function instantiateHead(head: OhProjectionLiteralV1, binding: Binding): readonly OhProjectionAtomV1[] {
845
+ return head.terms.map((term) => term.kind === "constant"
846
+ ? term.value : binding.get(term.name) as OhProjectionAtomV1);
847
+ }
848
+
849
+ function canonicalWitness(witness: FactWitness | DerivedWitness): string {
850
+ if (witness.kind === "fact") return canonicalJson(witness);
851
+ return canonicalJson({ kind: witness.kind, premises: witness.premises, ruleSha256: witness.rule.ruleSha256 });
852
+ }
853
+
854
+ function materializeNaive(input: Readonly<{
855
+ dataset: OhProjectionDatasetV1;
856
+ maximumDerivedTuples: number;
857
+ maximumRounds: number;
858
+ rulePack: OhProjectionRulePackV1;
859
+ work: ProjectionWorkBudget;
860
+ }>): MaterializedProjection {
861
+ const relations = new Map<string, RelationState>();
862
+ for (const fact of input.dataset.facts) {
863
+ let relation = relations.get(fact.relation);
864
+ if (relation === undefined) {
865
+ relation = new Map();
866
+ relations.set(fact.relation, relation);
867
+ }
868
+ relation.set(tupleKey(fact.tuple), { tuple: fact.tuple, witness: { kind: "fact", sources: fact.sources } });
869
+ }
870
+ let derivedFacts = 0;
871
+ let rounds = 0;
872
+ while (true) {
873
+ const candidates = new Map<string, Readonly<{ relation: string; state: TupleState }>>();
874
+ for (const rule of input.rulePack.rules) {
875
+ for (const match of matchBody(relations, rule.body, OH_PROJECTION_LIMITS_V1.queryMatches, input.work)) {
876
+ const derivedTuple = instantiateHead(rule.head, match.binding);
877
+ const relation = relations.get(rule.head.relation);
878
+ const key = tupleKey(derivedTuple);
879
+ if (relation?.has(key) === true) continue;
880
+ const state: TupleState = { tuple: derivedTuple,
881
+ witness: { kind: "derived", premises: match.premises, rule } };
882
+ const identity = referenceKey({ relation: rule.head.relation, tuple: derivedTuple });
883
+ const existing = candidates.get(identity);
884
+ if (existing === undefined || canonicalWitness(state.witness) < canonicalWitness(existing.state.witness)) {
885
+ candidates.set(identity, { relation: rule.head.relation, state });
886
+ }
887
+ }
888
+ }
889
+ if (candidates.size === 0) break;
890
+ if (rounds >= input.maximumRounds) throw new RangeError("Projection exceeds its evaluation round bound.");
891
+ if (derivedFacts + candidates.size > input.maximumDerivedTuples) {
892
+ throw new RangeError("Projection exceeds its derived tuple bound.");
893
+ }
894
+ const ordered = [...candidates.values()].sort((left, right) => compareCanonical(
895
+ [left.relation, left.state.tuple], [right.relation, right.state.tuple]));
896
+ for (const candidate of ordered) {
897
+ let relation = relations.get(candidate.relation);
898
+ if (relation === undefined) {
899
+ relation = new Map();
900
+ relations.set(candidate.relation, relation);
901
+ }
902
+ relation.set(tupleKey(candidate.state.tuple), candidate.state);
903
+ }
904
+ derivedFacts += candidates.size;
905
+ rounds += 1;
906
+ }
907
+ return { baseFacts: input.dataset.facts.length, derivedFacts, relations, rounds };
908
+ }
909
+
910
+ function boundedOption(value: number | undefined, fallback: number, maximum: number, label: string): number {
911
+ const parsed = positiveInteger(value ?? fallback, maximum);
912
+ if (parsed === null) throw new RangeError(`${label} must be an integer from 1 through ${maximum}.`);
913
+ return parsed;
914
+ }
915
+
916
+ type ResolvedEvaluationOptions = Readonly<{
917
+ maximumDerivedTuples: number;
918
+ maximumProofDepth: number;
919
+ maximumProofNodes: number;
920
+ maximumResultBytes: number;
921
+ maximumRounds: number;
922
+ maximumTotalProofNodes: number;
923
+ maximumWorkUnits: number;
924
+ }>;
925
+
926
+ function resolveEvaluationOptions(options: OhProjectionEvaluationOptionsV1): ResolvedEvaluationOptions {
927
+ const resolved = {
928
+ maximumDerivedTuples: boundedOption(options.maximumDerivedTuples, OH_PROJECTION_LIMITS_V1.derivedTuples,
929
+ OH_PROJECTION_LIMITS_V1.derivedTuples, "maximumDerivedTuples"),
930
+ maximumProofDepth: boundedOption(options.maximumProofDepth, 32,
931
+ OH_PROJECTION_LIMITS_V1.proofDepth, "maximumProofDepth"),
932
+ maximumProofNodes: boundedOption(options.maximumProofNodes, 1_024,
933
+ OH_PROJECTION_LIMITS_V1.proofNodes, "maximumProofNodes"),
934
+ maximumResultBytes: boundedOption(options.maximumResultBytes, OH_PROJECTION_LIMITS_V1.resultBytes,
935
+ OH_PROJECTION_LIMITS_V1.resultBytes, "maximumResultBytes"),
936
+ maximumRounds: boundedOption(options.maximumRounds, OH_PROJECTION_LIMITS_V1.rounds,
937
+ OH_PROJECTION_LIMITS_V1.rounds, "maximumRounds"),
938
+ maximumTotalProofNodes: boundedOption(options.maximumTotalProofNodes,
939
+ OH_PROJECTION_LIMITS_V1.totalProofNodes, OH_PROJECTION_LIMITS_V1.totalProofNodes,
940
+ "maximumTotalProofNodes"),
941
+ maximumWorkUnits: boundedOption(options.maximumWorkUnits, OH_PROJECTION_LIMITS_V1.workUnits,
942
+ OH_PROJECTION_LIMITS_V1.workUnits, "maximumWorkUnits"),
943
+ };
944
+ if (resolved.maximumResultBytes < 64 * 1024) {
945
+ throw new RangeError("maximumResultBytes must be at least 65536.");
946
+ }
947
+ return resolved;
948
+ }
949
+
950
+ type ProjectionResultBudget = { bytes: number; maximumBytes: number; nodes: number };
951
+ type ProjectionRowProofBudget = { nodes: number; result: ProjectionResultBudget };
952
+
953
+ function reserveResultBytes(budget: ProjectionResultBudget, value: unknown): boolean {
954
+ const bytes = utf8ByteLength(canonicalJson(value));
955
+ if (budget.bytes + bytes > budget.maximumBytes) return false;
956
+ budget.bytes += bytes;
957
+ return true;
958
+ }
959
+
960
+ function reserveProofNode(budget: ProjectionRowProofBudget, options: ResolvedEvaluationOptions,
961
+ envelope: OhProjectionProofV1): boolean {
962
+ if (budget.nodes >= options.maximumProofNodes
963
+ || budget.result.nodes >= options.maximumTotalProofNodes
964
+ || !reserveResultBytes(budget.result, envelope)) return false;
965
+ budget.nodes += 1;
966
+ budget.result.nodes += 1;
967
+ return true;
968
+ }
969
+
970
+ function proofForReference(relations: Map<string, RelationState>, reference: TupleReference,
971
+ budget: ProjectionRowProofBudget, options: ResolvedEvaluationOptions, depth: number,
972
+ visiting: Set<string>): OhProjectionProofV1 | null {
973
+ if (depth >= options.maximumProofDepth) {
974
+ const proof = { kind: "truncated" as const, reason: "depth" as const,
975
+ relation: reference.relation, tuple: reference.tuple, v: 1 as const };
976
+ return reserveProofNode(budget, options, proof) ? proof : null;
977
+ }
978
+ const identity = referenceKey(reference);
979
+ if (visiting.has(identity)) {
980
+ const proof = { kind: "truncated" as const, reason: "cycle" as const,
981
+ relation: reference.relation, tuple: reference.tuple, v: 1 as const };
982
+ return reserveProofNode(budget, options, proof) ? proof : null;
983
+ }
984
+ const state = relations.get(reference.relation)?.get(tupleKey(reference.tuple));
985
+ if (state === undefined) throw new Error("Projection proof references a tuple outside the materialized result.");
986
+ if (state.witness.kind === "fact") {
987
+ const proof = { kind: "fact" as const, relation: reference.relation, sources: state.witness.sources,
988
+ tuple: reference.tuple, v: 1 as const };
989
+ return reserveProofNode(budget, options, proof) ? proof : null;
990
+ }
991
+ const envelope = { kind: "derived" as const, premises: [], premisesTruncated: false,
992
+ relation: reference.relation, ruleId: state.witness.rule.ruleId,
993
+ ruleSha256: state.witness.rule.ruleSha256, tuple: reference.tuple, v: 1 as const };
994
+ if (!reserveProofNode(budget, options, envelope)) return null;
995
+ visiting.add(identity);
996
+ try {
997
+ const premises: OhProjectionProofV1[] = [];
998
+ let premisesTruncated = false;
999
+ for (const premise of state.witness.premises) {
1000
+ const proof = proofForReference(relations, premise, budget, options, depth + 1, visiting);
1001
+ if (proof === null) { premisesTruncated = true; break; }
1002
+ premises.push(proof);
1003
+ }
1004
+ return { kind: "derived", premises, premisesTruncated,
1005
+ relation: reference.relation, ruleId: state.witness.rule.ruleId,
1006
+ ruleSha256: state.witness.rule.ruleSha256, tuple: reference.tuple, v: 1 };
1007
+ } finally {
1008
+ visiting.delete(identity);
1009
+ }
1010
+ }
1011
+
1012
+ function proofIsTruncated(proof: OhProjectionProofV1): boolean {
1013
+ return proof.kind === "truncated" || (proof.kind === "derived"
1014
+ && (proof.premisesTruncated || proof.premises.some(proofIsTruncated)));
1015
+ }
1016
+
1017
+ type ProjectionParseBudget = {
1018
+ bytes: number;
1019
+ maximumBytes: number;
1020
+ maximumDepth: number;
1021
+ maximumNodes: number;
1022
+ nodes: number;
1023
+ };
1024
+
1025
+ function reserveProjectionParseBytes(budget: ProjectionParseBudget, value: unknown): boolean {
1026
+ const bytes = utf8ByteLength(canonicalJson(value));
1027
+ if (budget.bytes + bytes > budget.maximumBytes) return false;
1028
+ budget.bytes += bytes;
1029
+ return true;
1030
+ }
1031
+
1032
+ function parseProjectionFactSource(value: unknown): OhProjectionFactSourceV1 | null {
1033
+ if (!isPlainRecord(value) || !hasExactKeys(value, ["key", "recordSha256", "v"]) || value.v !== 1) {
1034
+ return null;
1035
+ }
1036
+ const key = safeCode(value.key, 512);
1037
+ const recordSha256 = parseSha256Hex(value.recordSha256);
1038
+ return key === null || recordSha256 === null ? null : { key, recordSha256, v: 1 };
1039
+ }
1040
+
1041
+ function parseProjectionProofWithBudget(value: unknown, budget: ProjectionParseBudget,
1042
+ depth: number): OhProjectionProofV1 | null {
1043
+ if (depth > budget.maximumDepth || budget.nodes >= budget.maximumNodes
1044
+ || !isPlainRecord(value) || value.v !== 1) return null;
1045
+ const relation = projectionName(value.relation);
1046
+ const parsedTuple = tuple(value.tuple);
1047
+ if (relation === null || parsedTuple === null) return null;
1048
+
1049
+ if (value.kind === "fact") {
1050
+ if (!hasExactKeys(value, ["kind", "relation", "sources", "tuple", "v"])
1051
+ || !Array.isArray(value.sources) || value.sources.length < 1
1052
+ || value.sources.length > OH_PROJECTION_LIMITS_V1.sourcesPerFact) return null;
1053
+ const sources = value.sources.map(parseProjectionFactSource);
1054
+ if (sources.some((source) => source === null)) return null;
1055
+ const parsedSources = sources as readonly OhProjectionFactSourceV1[];
1056
+ if (!orderedUnique(parsedSources, (source) => source.key)) return null;
1057
+ const proof = { kind: "fact" as const, relation, sources: parsedSources,
1058
+ tuple: parsedTuple, v: 1 as const };
1059
+ if (!reserveProjectionParseBytes(budget, proof)) return null;
1060
+ budget.nodes += 1;
1061
+ return proof;
1062
+ }
1063
+
1064
+ if (value.kind === "truncated") {
1065
+ if (!hasExactKeys(value, ["kind", "reason", "relation", "tuple", "v"])
1066
+ || (value.reason !== "cycle" && value.reason !== "depth" && value.reason !== "nodes")) return null;
1067
+ const reason = value.reason as "cycle" | "depth" | "nodes";
1068
+ const proof = { kind: "truncated" as const, reason,
1069
+ relation, tuple: parsedTuple, v: 1 as const };
1070
+ if (!reserveProjectionParseBytes(budget, proof)) return null;
1071
+ budget.nodes += 1;
1072
+ return proof;
1073
+ }
1074
+
1075
+ if (value.kind !== "derived" || !hasExactKeys(value, ["kind", "premises", "premisesTruncated",
1076
+ "relation", "ruleId", "ruleSha256", "tuple", "v"]) || !Array.isArray(value.premises)
1077
+ || value.premises.length > OH_PROJECTION_LIMITS_V1.literalsPerRule
1078
+ || typeof value.premisesTruncated !== "boolean") return null;
1079
+ const ruleId = projectionName(value.ruleId);
1080
+ const ruleSha256 = parseSha256Hex(value.ruleSha256);
1081
+ if (ruleId === null || ruleSha256 === null
1082
+ || (!value.premisesTruncated && value.premises.length === 0)
1083
+ || (value.premisesTruncated && value.premises.length === OH_PROJECTION_LIMITS_V1.literalsPerRule)) {
1084
+ return null;
1085
+ }
1086
+ const skeleton = { kind: "derived" as const, premises: [],
1087
+ premisesTruncated: value.premisesTruncated, relation, ruleId, ruleSha256,
1088
+ tuple: parsedTuple, v: 1 as const };
1089
+ if (!reserveProjectionParseBytes(budget, skeleton)) return null;
1090
+ budget.nodes += 1;
1091
+ const premises: OhProjectionProofV1[] = [];
1092
+ for (const premise of value.premises) {
1093
+ const parsed = parseProjectionProofWithBudget(premise, budget, depth + 1);
1094
+ if (parsed === null) return null;
1095
+ premises.push(parsed);
1096
+ }
1097
+ return { ...skeleton, premises };
1098
+ }
1099
+
1100
+ /**
1101
+ * Parses one untrusted proof tree under the public hard depth, node, and byte
1102
+ * ceilings. Cached result envelopes should normally be parsed as a whole with
1103
+ * `parseOhProjectionResultV1`, which also applies their smaller declared limits.
1104
+ */
1105
+ export function parseOhProjectionProofV1(value: unknown): OhProjectionProofV1 | null {
1106
+ try {
1107
+ const budget: ProjectionParseBudget = { bytes: 0,
1108
+ maximumBytes: OH_PROJECTION_LIMITS_V1.resultBytes,
1109
+ maximumDepth: OH_PROJECTION_LIMITS_V1.proofDepth,
1110
+ maximumNodes: OH_PROJECTION_LIMITS_V1.proofNodes, nodes: 0 };
1111
+ const proof = parseProjectionProofWithBudget(value, budget, 0);
1112
+ return proof !== null && utf8ByteLength(canonicalJson(proof)) <= budget.maximumBytes ? proof : null;
1113
+ } catch {
1114
+ return null;
1115
+ }
1116
+ }
1117
+
1118
+ type OhProjectionResolvedEvaluationV1 = ResolvedEvaluationOptions & Readonly<{ v: 1 }>;
1119
+
1120
+ function parseProjectionEvaluation(value: unknown): OhProjectionResolvedEvaluationV1 | null {
1121
+ if (!isPlainRecord(value) || !hasExactKeys(value, ["maximumDerivedTuples", "maximumProofDepth",
1122
+ "maximumProofNodes", "maximumResultBytes", "maximumRounds", "maximumTotalProofNodes",
1123
+ "maximumWorkUnits", "v"]) || value.v !== 1) return null;
1124
+ const maximumDerivedTuples = positiveInteger(value.maximumDerivedTuples,
1125
+ OH_PROJECTION_LIMITS_V1.derivedTuples);
1126
+ const maximumProofDepth = positiveInteger(value.maximumProofDepth, OH_PROJECTION_LIMITS_V1.proofDepth);
1127
+ const maximumProofNodes = positiveInteger(value.maximumProofNodes, OH_PROJECTION_LIMITS_V1.proofNodes);
1128
+ const maximumResultBytes = positiveInteger(value.maximumResultBytes, OH_PROJECTION_LIMITS_V1.resultBytes);
1129
+ const maximumRounds = positiveInteger(value.maximumRounds, OH_PROJECTION_LIMITS_V1.rounds);
1130
+ const maximumTotalProofNodes = positiveInteger(value.maximumTotalProofNodes,
1131
+ OH_PROJECTION_LIMITS_V1.totalProofNodes);
1132
+ const maximumWorkUnits = positiveInteger(value.maximumWorkUnits, OH_PROJECTION_LIMITS_V1.workUnits);
1133
+ if (maximumDerivedTuples === null || maximumProofDepth === null || maximumProofNodes === null
1134
+ || maximumResultBytes === null || maximumResultBytes < 64 * 1024 || maximumRounds === null
1135
+ || maximumTotalProofNodes === null || maximumWorkUnits === null) return null;
1136
+ return { maximumDerivedTuples, maximumProofDepth, maximumProofNodes, maximumResultBytes,
1137
+ maximumRounds, maximumTotalProofNodes, maximumWorkUnits, v: 1 };
1138
+ }
1139
+
1140
+ function parseProjectionResultRow(value: unknown, evaluation: OhProjectionResolvedEvaluationV1,
1141
+ resultBudget: ProjectionParseBudget): Readonly<{ nodes: number; row: OhProjectionResultRowV1 }> | null {
1142
+ if (!isPlainRecord(value) || !hasExactKeys(value,
1143
+ ["proofs", "proofsTruncated", "supportCount", "values", "v"]) || value.v !== 1
1144
+ || !Array.isArray(value.proofs) || value.proofs.length > OH_PROJECTION_LIMITS_V1.queryLiterals
1145
+ || typeof value.proofsTruncated !== "boolean") return null;
1146
+ const values = tuple(value.values);
1147
+ const supportCount = positiveInteger(value.supportCount, OH_PROJECTION_LIMITS_V1.queryMatches);
1148
+ if (values === null || supportCount === null || (!value.proofsTruncated && value.proofs.length === 0)) return null;
1149
+ if (!reserveProjectionParseBytes(resultBudget, { proofs: [], proofsTruncated: value.proofsTruncated,
1150
+ supportCount, values, v: 1 })) return null;
1151
+ const before = resultBudget.nodes;
1152
+ resultBudget.maximumNodes = Math.min(resultBudget.maximumNodes, before + evaluation.maximumProofNodes);
1153
+ const proofs: OhProjectionProofV1[] = [];
1154
+ for (const proof of value.proofs) {
1155
+ const parsed = parseProjectionProofWithBudget(proof, resultBudget, 0);
1156
+ if (parsed === null) return null;
1157
+ proofs.push(parsed);
1158
+ }
1159
+ resultBudget.maximumNodes = evaluation.maximumTotalProofNodes;
1160
+ const containsTruncation = proofs.some(proofIsTruncated);
1161
+ if ((!value.proofsTruncated && containsTruncation)
1162
+ || (value.proofsTruncated && proofs.length === OH_PROJECTION_LIMITS_V1.queryLiterals
1163
+ && !containsTruncation)) return null;
1164
+ return { nodes: resultBudget.nodes - before,
1165
+ row: { proofs, proofsTruncated: value.proofsTruncated, supportCount, values, v: 1 } };
1166
+ }
1167
+
1168
+ /**
1169
+ * Parses a cached projection result as untrusted data. It verifies exact keys,
1170
+ * all aggregate and declared bounds, proof truncation markers, canonical row
1171
+ * order, engine/evaluation identity links, and `resultSha256`. Pass the
1172
+ * projection digest requested from a cache to reject identity substitution.
1173
+ */
1174
+ export function parseOhProjectionResultV1(value: unknown,
1175
+ expectedProjectionSha256?: Sha256Hex): OhProjectionResultV1 | null {
1176
+ try {
1177
+ if (!isPlainRecord(value) || !hasExactKeys(value, ["authority", "cache", "engine", "evaluation",
1178
+ "identity", "resultSha256", "rows", "stats", "v"]) || value.v !== 1
1179
+ || value.authority !== "derived" || !isPlainRecord(value.cache)
1180
+ || !hasExactKeys(value.cache, ["strategy", "v"]) || value.cache.strategy !== "full-rebuild"
1181
+ || value.cache.v !== 1 || !Array.isArray(value.rows)
1182
+ || value.rows.length > OH_PROJECTION_LIMITS_V1.queryResults || !isPlainRecord(value.stats)
1183
+ || !hasExactKeys(value.stats, ["baseFacts", "derivedFacts", "proofNodes", "proofsTruncated",
1184
+ "queryMatches", "relations", "rounds", "truncated", "truncationReasons", "v", "workUnits"])
1185
+ || value.stats.v !== 1 || !Array.isArray(value.stats.truncationReasons)
1186
+ || typeof value.stats.proofsTruncated !== "boolean" || typeof value.stats.truncated !== "boolean") return null;
1187
+ const engine = safeCode(value.engine, 256);
1188
+ const evaluation = parseProjectionEvaluation(value.evaluation);
1189
+ const identity = parseOhProjectionIdentityV1(value.identity);
1190
+ const resultSha256 = parseSha256Hex(value.resultSha256);
1191
+ const expected = expectedProjectionSha256 === undefined ? undefined
1192
+ : parseSha256Hex(expectedProjectionSha256);
1193
+ if (engine === null || evaluation === null || identity === null || resultSha256 === null
1194
+ || (expectedProjectionSha256 !== undefined && expected === null)
1195
+ || (expected !== undefined && identity.projectionSha256 !== expected)
1196
+ || identity.engineSha256 !== canonicalSha256({ engine, v: 1 })
1197
+ || identity.evaluationSha256 !== canonicalSha256(evaluation)) return null;
1198
+
1199
+ const baseFacts = nonnegativeInteger(value.stats.baseFacts);
1200
+ const derivedFacts = nonnegativeInteger(value.stats.derivedFacts);
1201
+ const proofNodes = nonnegativeInteger(value.stats.proofNodes);
1202
+ const queryMatches = nonnegativeInteger(value.stats.queryMatches);
1203
+ const relations = nonnegativeInteger(value.stats.relations);
1204
+ const rounds = nonnegativeInteger(value.stats.rounds);
1205
+ const workUnits = nonnegativeInteger(value.stats.workUnits);
1206
+ if (baseFacts === null || baseFacts > OH_PROJECTION_LIMITS_V1.facts
1207
+ || derivedFacts === null || derivedFacts > evaluation.maximumDerivedTuples
1208
+ || proofNodes === null || proofNodes > evaluation.maximumTotalProofNodes
1209
+ || queryMatches === null || queryMatches > OH_PROJECTION_LIMITS_V1.queryMatches
1210
+ || relations === null || relations > OH_PROJECTION_LIMITS_V1.relations
1211
+ || rounds === null || rounds > evaluation.maximumRounds
1212
+ || workUnits === null || workUnits > evaluation.maximumWorkUnits
1213
+ || relations > baseFacts + derivedFacts || rounds > derivedFacts
1214
+ || ((rounds === 0) !== (derivedFacts === 0)) || queryMatches > workUnits) return null;
1215
+ const truncationReasons = value.stats.truncationReasons;
1216
+ if (truncationReasons.length > 2
1217
+ || !orderedUnique(truncationReasons, (reason) => reason === "query-limit" ? "0" : reason === "result-bytes" ? "1" : "x")
1218
+ || truncationReasons.some((reason) => reason !== "query-limit" && reason !== "result-bytes")
1219
+ || value.stats.truncated !== (truncationReasons.length > 0)) return null;
1220
+
1221
+ const budget: ProjectionParseBudget = { bytes: 0, maximumBytes: evaluation.maximumResultBytes,
1222
+ maximumDepth: evaluation.maximumProofDepth, maximumNodes: evaluation.maximumTotalProofNodes, nodes: 0 };
1223
+ const rows: OhProjectionResultRowV1[] = [];
1224
+ let supportCount = 0;
1225
+ for (const row of value.rows) {
1226
+ const parsed = parseProjectionResultRow(row, evaluation, budget);
1227
+ if (parsed === null) return null;
1228
+ rows.push(parsed.row);
1229
+ supportCount += parsed.row.supportCount;
1230
+ if (supportCount > queryMatches) return null;
1231
+ }
1232
+ if (!orderedUnique(rows, (row) => canonicalJson(row.values)) || budget.nodes !== proofNodes
1233
+ || value.stats.proofsTruncated !== rows.some((row) => row.proofsTruncated)
1234
+ || (value.stats.truncated ? supportCount >= queryMatches : supportCount !== queryMatches)) return null;
1235
+
1236
+ const reasons = truncationReasons as readonly ("query-limit" | "result-bytes")[];
1237
+ const payload = {
1238
+ authority: "derived" as const,
1239
+ cache: { strategy: "full-rebuild" as const, v: 1 as const },
1240
+ engine,
1241
+ evaluation,
1242
+ identity,
1243
+ rows,
1244
+ stats: { baseFacts, derivedFacts, proofNodes, proofsTruncated: value.stats.proofsTruncated,
1245
+ queryMatches, relations, rounds, truncated: value.stats.truncated,
1246
+ truncationReasons: reasons, v: 1 as const, workUnits },
1247
+ v: 1 as const,
1248
+ };
1249
+ const serialized = canonicalJson(payload);
1250
+ return utf8ByteLength(serialized) <= evaluation.maximumResultBytes
1251
+ && sha256Hex(serialized) === resultSha256
1252
+ ? { ...payload, resultSha256 } : null;
1253
+ } catch {
1254
+ return null;
1255
+ }
1256
+ }
1257
+
1258
+ function buildProjectionResult(input: Readonly<{
1259
+ dataset: OhProjectionDatasetV1;
1260
+ engine: string;
1261
+ materialized: MaterializedProjection;
1262
+ options: ResolvedEvaluationOptions;
1263
+ query: OhProjectionQueryV1;
1264
+ rulePack: OhProjectionRulePackV1;
1265
+ snapshot: OhProjectionSnapshotV1;
1266
+ work: ProjectionWorkBudget;
1267
+ }>): OhProjectionResultV1 {
1268
+ const matches = matchBody(input.materialized.relations, input.query.where,
1269
+ OH_PROJECTION_LIMITS_V1.queryMatches, input.work);
1270
+ const byValues = new Map<string, { match: BodyMatch; supportCount: number }>();
1271
+ for (const match of matches) {
1272
+ const values = input.query.find.map((name) => match.binding.get(name) as OhProjectionAtomV1);
1273
+ const key = tupleKey(values);
1274
+ const existing = byValues.get(key);
1275
+ if (existing === undefined) byValues.set(key, { match, supportCount: 1 });
1276
+ else byValues.set(key, { match: compareCanonical(match.premises, existing.match.premises) < 0
1277
+ ? match : existing.match, supportCount: existing.supportCount + 1 });
1278
+ }
1279
+ const ordered = [...byValues.entries()].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
1280
+ const resultBudget: ProjectionResultBudget = { bytes: 0,
1281
+ maximumBytes: input.options.maximumResultBytes - 64 * 1024, nodes: 0 };
1282
+ const rows: OhProjectionResultRowV1[] = [];
1283
+ let resultBytesTruncated = false;
1284
+ for (const [key, support] of ordered.slice(0, input.query.limit)) {
1285
+ const values = JSON.parse(key) as OhProjectionAtomV1[];
1286
+ if (!reserveResultBytes(resultBudget, { proofs: [], proofsTruncated: false,
1287
+ supportCount: support.supportCount, values, v: 1 })) {
1288
+ resultBytesTruncated = true;
1289
+ break;
1290
+ }
1291
+ const budget: ProjectionRowProofBudget = { nodes: 0, result: resultBudget };
1292
+ const proofs: OhProjectionProofV1[] = [];
1293
+ for (const premise of support.match.premises) {
1294
+ const proof = proofForReference(input.materialized.relations, premise, budget, input.options, 0, new Set());
1295
+ if (proof === null) break;
1296
+ proofs.push(proof);
1297
+ }
1298
+ const proofsTruncated = proofs.length !== support.match.premises.length || proofs.some(proofIsTruncated);
1299
+ rows.push({ proofs, proofsTruncated, supportCount: support.supportCount, values, v: 1 });
1300
+ }
1301
+ const queryLimitTruncated = ordered.length > input.query.limit;
1302
+ const truncationReasons = [
1303
+ ...(queryLimitTruncated ? ["query-limit" as const] : []),
1304
+ ...(resultBytesTruncated ? ["result-bytes" as const] : []),
1305
+ ];
1306
+ const truncated = truncationReasons.length > 0;
1307
+ const identity = createOhProjectionIdentityV1({ dataset: input.dataset, query: input.query,
1308
+ engine: input.engine, options: input.options, rulePack: input.rulePack, snapshot: input.snapshot });
1309
+ const payload = {
1310
+ authority: "derived" as const,
1311
+ cache: { strategy: "full-rebuild" as const, v: 1 as const },
1312
+ engine: input.engine,
1313
+ evaluation: { ...input.options, v: 1 as const },
1314
+ identity,
1315
+ rows,
1316
+ stats: { baseFacts: input.materialized.baseFacts, derivedFacts: input.materialized.derivedFacts,
1317
+ proofNodes: resultBudget.nodes, proofsTruncated: rows.some((row) => row.proofsTruncated),
1318
+ queryMatches: matches.length,
1319
+ relations: input.materialized.relations.size, rounds: input.materialized.rounds,
1320
+ truncated, truncationReasons, v: 1 as const, workUnits: input.work.units },
1321
+ v: 1 as const,
1322
+ };
1323
+ const serialized = canonicalJson(payload);
1324
+ if (utf8ByteLength(serialized) > input.options.maximumResultBytes) {
1325
+ throw new RangeError("Projection result exceeds its canonical byte bound.");
1326
+ }
1327
+ return { ...payload, resultSha256: sha256Hex(serialized) };
1328
+ }
1329
+
1330
+ export function evaluateOhProjectionV1(input: Readonly<{
1331
+ dataset: OhProjectionDatasetV1;
1332
+ options?: OhProjectionEvaluationOptionsV1;
1333
+ query: OhProjectionQueryV1;
1334
+ rulePack: OhProjectionRulePackV1;
1335
+ snapshot: OhProjectionSnapshotV1;
1336
+ }>): OhProjectionResultV1 {
1337
+ const snapshot = parseOhProjectionSnapshotV1(input.snapshot);
1338
+ const dataset = snapshot === null ? null : parseOhProjectionDatasetV1(input.dataset, snapshot);
1339
+ const rulePack = parseOhProjectionRulePackV1(input.rulePack);
1340
+ const query = parseOhProjectionQueryV1(input.query);
1341
+ if (snapshot === null || dataset === null || rulePack === null || query === null) {
1342
+ throw new TypeError("Invalid projection snapshot, dataset, rule pack, or query.");
1343
+ }
1344
+ const options = resolveEvaluationOptions(input.options ?? {});
1345
+ validateProgramArities(dataset, rulePack, query);
1346
+ const work = { maximum: options.maximumWorkUnits, units: 0 };
1347
+ const materialized = materializeNaive({ dataset,
1348
+ maximumDerivedTuples: options.maximumDerivedTuples, maximumRounds: options.maximumRounds, rulePack, work });
1349
+ return buildProjectionResult({ dataset, engine: OH_PROJECTION_INTERNAL_ENGINE_V1,
1350
+ materialized, options, query, rulePack, snapshot, work });
1351
+ }
1352
+
1353
+ /**
1354
+ * Internal adapter seam. It is exported for package-owned optional engines,
1355
+ * not as authority: callers receive the same derived-only result envelope.
1356
+ */
1357
+ export function evaluateOhProjectionWithMaterializerV1(input: Readonly<{
1358
+ dataset: OhProjectionDatasetV1;
1359
+ engine: string;
1360
+ materialize: (program: Readonly<{
1361
+ dataset: OhProjectionDatasetV1;
1362
+ maximumDerivedTuples: number;
1363
+ maximumRounds: number;
1364
+ query: OhProjectionQueryV1;
1365
+ rulePack: OhProjectionRulePackV1;
1366
+ }>) => Readonly<{ relationFacts: ReadonlyMap<string, readonly OhProjectionAtomV1[][]> }>;
1367
+ options?: OhProjectionEvaluationOptionsV1;
1368
+ query: OhProjectionQueryV1;
1369
+ rulePack: OhProjectionRulePackV1;
1370
+ snapshot: OhProjectionSnapshotV1;
1371
+ }>): OhProjectionResultV1 {
1372
+ const snapshot = parseOhProjectionSnapshotV1(input.snapshot);
1373
+ const dataset = snapshot === null ? null : parseOhProjectionDatasetV1(input.dataset, snapshot);
1374
+ const rulePack = parseOhProjectionRulePackV1(input.rulePack);
1375
+ const query = parseOhProjectionQueryV1(input.query);
1376
+ const engine = safeCode(input.engine, 256);
1377
+ if (snapshot === null || dataset === null || rulePack === null || query === null || engine === null) {
1378
+ throw new TypeError("Invalid projection adapter input.");
1379
+ }
1380
+ const options = resolveEvaluationOptions(input.options ?? {});
1381
+ validateProgramArities(dataset, rulePack, query);
1382
+ const work = { maximum: options.maximumWorkUnits, units: 0 };
1383
+ const witnessMaterialization = materializeNaive({ dataset,
1384
+ maximumDerivedTuples: options.maximumDerivedTuples, maximumRounds: options.maximumRounds, rulePack, work });
1385
+ const external = input.materialize({ dataset,
1386
+ maximumDerivedTuples: options.maximumDerivedTuples, maximumRounds: options.maximumRounds, query, rulePack });
1387
+ const externalCanonical = new Map<string, readonly string[]>();
1388
+ for (const [relationName, tuples] of external.relationFacts) {
1389
+ const relation = projectionName(relationName);
1390
+ if (relation === null || tuples.length > OH_PROJECTION_LIMITS_V1.facts + options.maximumDerivedTuples) {
1391
+ throw new TypeError("Projection adapter returned an invalid relation.");
1392
+ }
1393
+ const parsed = tuples.map(tuple);
1394
+ if (parsed.some((value) => value === null)) throw new TypeError("Projection adapter returned an invalid tuple.");
1395
+ const keys: string[] = [];
1396
+ for (const value of parsed) {
1397
+ if (value === null) throw new TypeError("Projection adapter returned an invalid tuple.");
1398
+ keys.push(tupleKey(value));
1399
+ }
1400
+ externalCanonical.set(relation, [...new Set(keys)].sort());
1401
+ }
1402
+ const expectedCanonical = new Map([...witnessMaterialization.relations.entries()].map(([relation, states]) =>
1403
+ [relation, [...states.values()].map((state) => tupleKey(state.tuple)).sort()] as const));
1404
+ const relationNames = [...new Set([...externalCanonical.keys(), ...expectedCanonical.keys()])].sort();
1405
+ for (const relation of relationNames) {
1406
+ if (canonicalJson(externalCanonical.get(relation) ?? []) !== canonicalJson(expectedCanonical.get(relation) ?? [])) {
1407
+ throw new Error(`Projection adapter disagrees with Oh semantics for relation ${relation}.`);
1408
+ }
1409
+ }
1410
+ return buildProjectionResult({ dataset, engine,
1411
+ materialized: witnessMaterialization, options, query, rulePack, snapshot, work });
1412
+ }
1413
+
1414
+ export type OhProjectionRecordFactOptionsV1 = Readonly<{
1415
+ includeDependencies?: boolean;
1416
+ includeRecords?: boolean;
1417
+ }>;
1418
+
1419
+ /** Builds the stable structural fact layer shared by every domain fact pack. */
1420
+ export function createOhProjectionRecordFactsV1(records: readonly KnowledgeGraphRecordV1[],
1421
+ options: OhProjectionRecordFactOptionsV1 = {}): readonly OhProjectionFactV1[] {
1422
+ if (records.length > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) throw new RangeError("Too many records for projection facts.");
1423
+ const parsedRecords = [...records]
1424
+ .sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0)
1425
+ .map((candidate) => {
1426
+ const record = parseKnowledgeGraphRecordV1(candidate);
1427
+ if (record === null) throw new TypeError("Invalid graph record for projection facts.");
1428
+ return record;
1429
+ });
1430
+ let projectedFactCount = 0;
1431
+ for (const record of parsedRecords) {
1432
+ if (options.includeRecords !== false) projectedFactCount += 1;
1433
+ if (options.includeDependencies !== false) projectedFactCount += record.dependencies.length;
1434
+ if (projectedFactCount > OH_PROJECTION_LIMITS_V1.facts) {
1435
+ throw new RangeError("Structural projection exceeds its fact bound.");
1436
+ }
1437
+ }
1438
+ const facts: OhProjectionFactV1[] = [];
1439
+ for (const record of parsedRecords) {
1440
+ const source = [{ key: record.key, recordSha256: record.recordSha256, v: 1 as const }];
1441
+ if (options.includeRecords !== false) {
1442
+ facts.push(createOhProjectionFactV1({ relation: "oh.record", sources: source,
1443
+ tuple: [record.key, record.kind, record.recordSha256] }));
1444
+ }
1445
+ if (options.includeDependencies !== false) {
1446
+ for (const dependency of record.dependencies) {
1447
+ facts.push(createOhProjectionFactV1({ relation: "oh.dependency", sources: source,
1448
+ tuple: [record.key, dependency] }));
1449
+ }
1450
+ }
1451
+ }
1452
+ return facts.sort(compareProjectionFacts);
1453
+ }
1454
+
1455
+ export function isOhProjectionRecordKindV1(value: unknown): value is KnowledgeGraphRecordKindV1 {
1456
+ return OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.some((kind) => kind === value);
1457
+ }