@hraness/oh 0.2.6 → 0.3.0
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.
- package/README.md +116 -12
- package/dist/canonical.d.ts.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +91 -19
- package/dist/cloudflare-embedding.d.ts +104 -0
- package/dist/cloudflare-embedding.d.ts.map +1 -0
- package/dist/graph.d.ts.map +1 -1
- package/dist/index.js +90 -18
- package/dist/libsql-semantic.d.ts +111 -0
- package/dist/libsql-semantic.d.ts.map +1 -0
- package/dist/libsql.js +105 -18
- package/dist/memory-page.d.ts +2 -0
- package/dist/memory-page.d.ts.map +1 -0
- package/dist/memory-page.js +725 -0
- package/dist/memory-pages.d.ts +76 -0
- package/dist/memory-pages.d.ts.map +1 -0
- package/dist/memory.d.ts +1 -0
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +478 -18
- package/dist/projection-public.js +105 -18
- package/dist/projection-suss.js +105 -18
- package/dist/sdk.js +90 -18
- package/dist/semantic-cloud.d.ts +3 -0
- package/dist/semantic-cloud.d.ts.map +1 -0
- package/dist/semantic-cloud.js +1843 -0
- package/dist/semantic.d.ts.map +1 -1
- package/dist/semantic.js +104 -21
- package/dist/sqlite/index.js +90 -18
- package/dist/store.js +105 -18
- package/dist/sync.js +90 -18
- package/package.json +10 -2
- package/skills/oh/SKILL.md +28 -2
- package/spec/README.md +11 -3
- package/spec/manifest.json +9 -1
- package/spec/v1/cloudflare-embedding-profile.json +13 -0
- package/spec/v1/cloudflare-embedding-renderer.json +8 -0
- package/spec/v1/memory-page.md +153 -0
- package/spec/v1/memory-page.schema.json +154 -0
- package/spec/v1/memory.md +18 -0
- package/spec/v1/migration.md +13 -0
- package/spec/v1/semantic-cloud.md +87 -0
- package/src/canonical.ts +28 -13
- package/src/cli.ts +1 -1
- package/src/cloudflare-embedding.test.ts +306 -0
- package/src/cloudflare-embedding.ts +385 -0
- package/src/contracts.test.ts +20 -0
- package/src/graph.ts +63 -6
- package/src/libsql-semantic.test.ts +478 -0
- package/src/libsql-semantic.ts +1117 -0
- package/src/memory-page.ts +1 -0
- package/src/memory-pages.test.ts +277 -0
- package/src/memory-pages.ts +440 -0
- package/src/memory.ts +2 -0
- package/src/semantic-cloud.ts +2 -0
- package/src/semantic.ts +14 -3
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
// src/canonical.ts
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
|
|
4
|
+
class OhValidationError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
path;
|
|
7
|
+
constructor(code, path, message) {
|
|
8
|
+
super(`${path}: ${message}`);
|
|
9
|
+
this.name = "OhValidationError";
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.path = path;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function isPlainRecord(value) {
|
|
15
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
16
|
+
return false;
|
|
17
|
+
const prototype = Object.getPrototypeOf(value);
|
|
18
|
+
return prototype === Object.prototype || prototype === null;
|
|
19
|
+
}
|
|
20
|
+
function hasExactKeys(value, keys) {
|
|
21
|
+
const actual = Object.keys(value);
|
|
22
|
+
return actual.length === keys.length && keys.every((key) => Object.hasOwn(value, key));
|
|
23
|
+
}
|
|
24
|
+
function assertUnicodeScalarString(value, path) {
|
|
25
|
+
for (let index = 0;index < value.length; index += 1) {
|
|
26
|
+
const code = value.charCodeAt(index);
|
|
27
|
+
if (code >= 55296 && code <= 56319) {
|
|
28
|
+
const next = value.charCodeAt(index + 1);
|
|
29
|
+
if (!(next >= 56320 && next <= 57343)) {
|
|
30
|
+
throw new OhValidationError("invalid-unicode", path, "contains an unpaired surrogate");
|
|
31
|
+
}
|
|
32
|
+
index += 1;
|
|
33
|
+
} else if (code >= 56320 && code <= 57343) {
|
|
34
|
+
throw new OhValidationError("invalid-unicode", path, "contains an unpaired surrogate");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function encodeCanonical(value, path, ancestors) {
|
|
39
|
+
if (value === null || typeof value === "boolean")
|
|
40
|
+
return JSON.stringify(value);
|
|
41
|
+
if (typeof value === "string") {
|
|
42
|
+
assertUnicodeScalarString(value, path);
|
|
43
|
+
return JSON.stringify(value);
|
|
44
|
+
}
|
|
45
|
+
if (typeof value === "number") {
|
|
46
|
+
if (!Number.isFinite(value)) {
|
|
47
|
+
throw new OhValidationError("non-json-number", path, "must be finite");
|
|
48
|
+
}
|
|
49
|
+
if (Object.is(value, -0)) {
|
|
50
|
+
throw new OhValidationError("noncanonical-number", path, "negative zero is not canonical");
|
|
51
|
+
}
|
|
52
|
+
return JSON.stringify(value);
|
|
53
|
+
}
|
|
54
|
+
if (typeof value !== "object" || value === null) {
|
|
55
|
+
throw new OhValidationError("non-json-value", path, `cannot encode ${typeof value}`);
|
|
56
|
+
}
|
|
57
|
+
if (ancestors.has(value)) {
|
|
58
|
+
throw new OhValidationError("cycle", path, "contains a cycle");
|
|
59
|
+
}
|
|
60
|
+
ancestors.add(value);
|
|
61
|
+
try {
|
|
62
|
+
if (Array.isArray(value)) {
|
|
63
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
|
|
64
|
+
const length = lengthDescriptor?.value;
|
|
65
|
+
if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0) {
|
|
66
|
+
throw new OhValidationError("non-json-property", path, "array has an invalid length descriptor");
|
|
67
|
+
}
|
|
68
|
+
const ownKeys2 = Reflect.ownKeys(value);
|
|
69
|
+
if (!ownKeys2.includes("length") || ownKeys2.some((key) => key !== "length" && (typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= length))) {
|
|
70
|
+
throw new OhValidationError("non-json-property", path, "array has non-index properties");
|
|
71
|
+
}
|
|
72
|
+
const elements = [];
|
|
73
|
+
for (let index = 0;index < length; index += 1) {
|
|
74
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
75
|
+
if (descriptor === undefined) {
|
|
76
|
+
throw new OhValidationError("sparse-array", `${path}[${index}]`, "must not contain holes");
|
|
77
|
+
}
|
|
78
|
+
if (!descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined) {
|
|
79
|
+
throw new OhValidationError("non-json-property", `${path}[${index}]`, "must be an enumerable data property");
|
|
80
|
+
}
|
|
81
|
+
elements.push(descriptor.value);
|
|
82
|
+
}
|
|
83
|
+
const encoded = elements.map((element, index) => encodeCanonical(element, `${path}[${index}]`, ancestors));
|
|
84
|
+
return `[${encoded.join(",")}]`;
|
|
85
|
+
}
|
|
86
|
+
if (!isPlainRecord(value)) {
|
|
87
|
+
throw new OhValidationError("non-plain-object", path, "must be a plain object");
|
|
88
|
+
}
|
|
89
|
+
const ownKeys = Reflect.ownKeys(value);
|
|
90
|
+
if (ownKeys.some((key) => typeof key !== "string")) {
|
|
91
|
+
throw new OhValidationError("non-json-property", path, "object has a symbol property");
|
|
92
|
+
}
|
|
93
|
+
const entries = [];
|
|
94
|
+
const keys = ownKeys;
|
|
95
|
+
for (const key of keys) {
|
|
96
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
97
|
+
if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined) {
|
|
98
|
+
throw new OhValidationError("non-json-property", `${path}.${key}`, "must be an enumerable data property");
|
|
99
|
+
}
|
|
100
|
+
entries.push([key, descriptor.value]);
|
|
101
|
+
}
|
|
102
|
+
entries.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
|
|
103
|
+
const encodedEntries = entries.map(([key, entryValue]) => {
|
|
104
|
+
assertUnicodeScalarString(key, `${path}.<key>`);
|
|
105
|
+
return `${JSON.stringify(key)}:${encodeCanonical(entryValue, `${path}.${key}`, ancestors)}`;
|
|
106
|
+
});
|
|
107
|
+
return `{${encodedEntries.join(",")}}`;
|
|
108
|
+
} finally {
|
|
109
|
+
ancestors.delete(value);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function canonicalJson(value) {
|
|
113
|
+
return encodeCanonical(value, "$", new Set);
|
|
114
|
+
}
|
|
115
|
+
function parseCanonicalJson(text, maximumBytes = 16 * 1024 * 1024) {
|
|
116
|
+
if (utf8ByteLength(text) > maximumBytes) {
|
|
117
|
+
throw new OhValidationError("limit-exceeded", "$", "canonical JSON exceeds its byte limit");
|
|
118
|
+
}
|
|
119
|
+
let value;
|
|
120
|
+
try {
|
|
121
|
+
value = JSON.parse(text);
|
|
122
|
+
} catch {
|
|
123
|
+
throw new OhValidationError("invalid-json", "$", "is not valid JSON");
|
|
124
|
+
}
|
|
125
|
+
if (canonicalJson(value) !== text) {
|
|
126
|
+
throw new OhValidationError("noncanonical-json", "$", "keys or values are not canonical");
|
|
127
|
+
}
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
function utf8ByteLength(value) {
|
|
131
|
+
return Buffer.byteLength(value, "utf8");
|
|
132
|
+
}
|
|
133
|
+
function sha256Hex(value) {
|
|
134
|
+
return createHash("sha256").update(value).digest("hex");
|
|
135
|
+
}
|
|
136
|
+
function canonicalSha256(value) {
|
|
137
|
+
return sha256Hex(canonicalJson(value));
|
|
138
|
+
}
|
|
139
|
+
function parseSha256Hex(value) {
|
|
140
|
+
return typeof value === "string" && /^[a-f0-9]{64}$/u.test(value) ? value : null;
|
|
141
|
+
}
|
|
142
|
+
function parseCanonicalInstantV1(value) {
|
|
143
|
+
if (typeof value !== "string" || !/^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d\.\d{3}Z$/u.test(value)) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
const timestamp = Date.parse(value);
|
|
147
|
+
return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value ? value : null;
|
|
148
|
+
}
|
|
149
|
+
function canonicalNow() {
|
|
150
|
+
return new Date().toISOString();
|
|
151
|
+
}
|
|
152
|
+
function safeCode(value, maximumLength = 128) {
|
|
153
|
+
return typeof value === "string" && value.length <= maximumLength && /^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$/u.test(value) ? value : null;
|
|
154
|
+
}
|
|
155
|
+
function boundedText(value, maximumBytes = 64 * 1024) {
|
|
156
|
+
if (typeof value !== "string" || value.length === 0 || value.normalize("NFC") !== value || utf8ByteLength(value) > maximumBytes)
|
|
157
|
+
return null;
|
|
158
|
+
try {
|
|
159
|
+
assertUnicodeScalarString(value, "$text");
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
for (const character of value) {
|
|
164
|
+
const code = character.codePointAt(0) ?? 0;
|
|
165
|
+
if (code <= 8 || code >= 11 && code <= 12 || code >= 14 && code <= 31 || code >= 127 && code <= 159)
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
return value;
|
|
169
|
+
}
|
|
170
|
+
function orderedUnique(values, key) {
|
|
171
|
+
return values.every((value, index) => index === 0 || key(values[index - 1]) < key(value));
|
|
172
|
+
}
|
|
173
|
+
function sortUnique(values, key) {
|
|
174
|
+
const sorted = [...values].sort((left, right) => {
|
|
175
|
+
const leftKey = key(left);
|
|
176
|
+
const rightKey = key(right);
|
|
177
|
+
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
|
|
178
|
+
});
|
|
179
|
+
if (!orderedUnique(sorted, key)) {
|
|
180
|
+
throw new OhValidationError("duplicate", "$", "contains duplicate canonical values");
|
|
181
|
+
}
|
|
182
|
+
return sorted;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/graph.ts
|
|
186
|
+
var OH_GRAPH_FORMAT_VERSION_V1 = 1;
|
|
187
|
+
var OH_GRAPH_LIMITS_V1 = Object.freeze({
|
|
188
|
+
changesPerOperation: 8192,
|
|
189
|
+
dependenciesPerRecord: 4096,
|
|
190
|
+
recordBytes: 1024 * 1024,
|
|
191
|
+
recordsPerSnapshot: 65536
|
|
192
|
+
});
|
|
193
|
+
var OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1 = [
|
|
194
|
+
"activity",
|
|
195
|
+
"assertion",
|
|
196
|
+
"context",
|
|
197
|
+
"dependency-manifest",
|
|
198
|
+
"edition",
|
|
199
|
+
"entity",
|
|
200
|
+
"evidence",
|
|
201
|
+
"identity-operation",
|
|
202
|
+
"inquiry",
|
|
203
|
+
"inquiry-event",
|
|
204
|
+
"review-decision",
|
|
205
|
+
"rights-decision",
|
|
206
|
+
"schema",
|
|
207
|
+
"shape",
|
|
208
|
+
"statement",
|
|
209
|
+
"type-membership",
|
|
210
|
+
"view",
|
|
211
|
+
"vocabulary"
|
|
212
|
+
];
|
|
213
|
+
var KNOWLEDGE_GRAPH_RECORD_KEYS_V1 = [
|
|
214
|
+
"dependencies",
|
|
215
|
+
"key",
|
|
216
|
+
"kind",
|
|
217
|
+
"recordSha256",
|
|
218
|
+
"v",
|
|
219
|
+
"value"
|
|
220
|
+
];
|
|
221
|
+
function exactKnowledgeGraphRecordEnvelopeV1(value) {
|
|
222
|
+
try {
|
|
223
|
+
if (!isPlainRecord(value))
|
|
224
|
+
return null;
|
|
225
|
+
const ownKeys = Reflect.ownKeys(value);
|
|
226
|
+
if (ownKeys.length !== KNOWLEDGE_GRAPH_RECORD_KEYS_V1.length || ownKeys.some((key) => typeof key !== "string") || KNOWLEDGE_GRAPH_RECORD_KEYS_V1.some((key) => !ownKeys.includes(key)))
|
|
227
|
+
return null;
|
|
228
|
+
const detached = {};
|
|
229
|
+
for (const key of KNOWLEDGE_GRAPH_RECORD_KEYS_V1) {
|
|
230
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
231
|
+
if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined)
|
|
232
|
+
return null;
|
|
233
|
+
detached[key] = descriptor.value;
|
|
234
|
+
}
|
|
235
|
+
return detached;
|
|
236
|
+
} catch {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function exactGraphDependenciesV1(value) {
|
|
241
|
+
try {
|
|
242
|
+
if (!Array.isArray(value))
|
|
243
|
+
return null;
|
|
244
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
|
|
245
|
+
const length = lengthDescriptor?.value;
|
|
246
|
+
if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0 || length > OH_GRAPH_LIMITS_V1.dependenciesPerRecord)
|
|
247
|
+
return null;
|
|
248
|
+
const ownKeys = Reflect.ownKeys(value);
|
|
249
|
+
if (ownKeys.length !== length + 1 || !ownKeys.includes("length") || ownKeys.some((key) => key !== "length" && (typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= length)))
|
|
250
|
+
return null;
|
|
251
|
+
const detached = [];
|
|
252
|
+
for (let index = 0;index < length; index += 1) {
|
|
253
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
254
|
+
if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined)
|
|
255
|
+
return null;
|
|
256
|
+
detached.push(descriptor.value);
|
|
257
|
+
}
|
|
258
|
+
return detached;
|
|
259
|
+
} catch {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function recordKey(value) {
|
|
264
|
+
return typeof value === "string" && value.length <= 512 && /^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$/u.test(value) ? value : null;
|
|
265
|
+
}
|
|
266
|
+
function createKnowledgeGraphRecordV1(input) {
|
|
267
|
+
if (!isPlainRecord(input) || !hasExactKeys(input, ["dependencies", "key", "kind", "v", "value"]) || input.v !== 1)
|
|
268
|
+
throw new TypeError("Invalid graph record input.");
|
|
269
|
+
const dependencyInput = exactGraphDependenciesV1(input.dependencies);
|
|
270
|
+
if (dependencyInput === null)
|
|
271
|
+
throw new TypeError("Invalid graph record dependencies.");
|
|
272
|
+
const key = recordKey(input.key);
|
|
273
|
+
const kind = OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.find((candidate) => candidate === input.kind);
|
|
274
|
+
if (key === null || kind === undefined)
|
|
275
|
+
throw new TypeError("Invalid graph record identity.");
|
|
276
|
+
const dependencies = dependencyInput.map(recordKey);
|
|
277
|
+
if (dependencies.some((dependency) => dependency === null) || !orderedUnique(dependencies, String) || dependencies.includes(key)) {
|
|
278
|
+
throw new TypeError("Graph dependencies must be ordered, unique, and non-reflexive.");
|
|
279
|
+
}
|
|
280
|
+
const valueJson = canonicalJson(input.value);
|
|
281
|
+
if (Buffer.byteLength(valueJson, "utf8") > OH_GRAPH_LIMITS_V1.recordBytes) {
|
|
282
|
+
throw new RangeError("Graph record value exceeds its canonical byte limit.");
|
|
283
|
+
}
|
|
284
|
+
const payload = { dependencies, key, kind, v: 1, value: input.value };
|
|
285
|
+
return { ...payload, recordSha256: canonicalSha256(payload) };
|
|
286
|
+
}
|
|
287
|
+
function parseKnowledgeGraphRecordV1(value) {
|
|
288
|
+
const envelope = exactKnowledgeGraphRecordEnvelopeV1(value);
|
|
289
|
+
if (envelope === null)
|
|
290
|
+
return null;
|
|
291
|
+
const recordSha256 = parseSha256Hex(envelope.recordSha256);
|
|
292
|
+
const input = {
|
|
293
|
+
dependencies: envelope.dependencies,
|
|
294
|
+
key: envelope.key,
|
|
295
|
+
kind: envelope.kind,
|
|
296
|
+
v: envelope.v,
|
|
297
|
+
value: envelope.value
|
|
298
|
+
};
|
|
299
|
+
try {
|
|
300
|
+
const created = createKnowledgeGraphRecordV1(input);
|
|
301
|
+
return recordSha256 !== null && created.recordSha256 === recordSha256 ? { ...created, recordSha256 } : null;
|
|
302
|
+
} catch {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function knowledgeGraphRecordRefV1(record) {
|
|
307
|
+
return {
|
|
308
|
+
dependencies: record.dependencies,
|
|
309
|
+
key: record.key,
|
|
310
|
+
kind: record.kind,
|
|
311
|
+
sha256: record.recordSha256,
|
|
312
|
+
v: 1
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function changeKey(change) {
|
|
316
|
+
return change.kind === "put" ? change.record.key : change.key;
|
|
317
|
+
}
|
|
318
|
+
function canonicalKnowledgeGraphChangesV1(changes) {
|
|
319
|
+
const normalized = [];
|
|
320
|
+
for (const change of changes) {
|
|
321
|
+
if (!isPlainRecord(change) || change.v !== 1)
|
|
322
|
+
throw new TypeError("Invalid graph change.");
|
|
323
|
+
if (change.kind === "put") {
|
|
324
|
+
const record = parseKnowledgeGraphRecordV1(change.record);
|
|
325
|
+
if (record === null)
|
|
326
|
+
throw new TypeError("Invalid graph record in change.");
|
|
327
|
+
normalized.push({ kind: "put", record, v: 1 });
|
|
328
|
+
} else if (change.kind === "tombstone") {
|
|
329
|
+
const key = recordKey(change.key);
|
|
330
|
+
const priorSha256 = parseSha256Hex(change.priorSha256);
|
|
331
|
+
if (key === null || priorSha256 === null)
|
|
332
|
+
throw new TypeError("Invalid graph tombstone.");
|
|
333
|
+
normalized.push({ key, kind: "tombstone", priorSha256, v: 1 });
|
|
334
|
+
} else
|
|
335
|
+
throw new TypeError("Unknown graph change kind.");
|
|
336
|
+
}
|
|
337
|
+
return sortUnique(normalized, changeKey);
|
|
338
|
+
}
|
|
339
|
+
function graphRevisionSha256V1(input) {
|
|
340
|
+
const changes = canonicalKnowledgeGraphChangesV1(input.changes);
|
|
341
|
+
const operationId = safeCode(input.operationId);
|
|
342
|
+
const parentGraphRevisionSha256 = input.parentGraphRevisionSha256 === null ? null : parseSha256Hex(input.parentGraphRevisionSha256);
|
|
343
|
+
const recordsSha256 = parseSha256Hex(input.recordsSha256);
|
|
344
|
+
const revision = Number.isSafeInteger(input.revision) && input.revision > 0 ? input.revision : null;
|
|
345
|
+
if (changes.length === 0 || changes.length > OH_GRAPH_LIMITS_V1.changesPerOperation || operationId === null || recordsSha256 === null || revision === null || input.parentGraphRevisionSha256 !== null && parentGraphRevisionSha256 === null || revision === 1 !== (parentGraphRevisionSha256 === null)) {
|
|
346
|
+
throw new TypeError("Invalid graph revision digest input.");
|
|
347
|
+
}
|
|
348
|
+
return canonicalSha256({ changes, operationId, parentGraphRevisionSha256, recordsSha256, revision, v: 1 });
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/memory-pages.ts
|
|
352
|
+
var OH_MEMORY_PAGE_FORMAT_V1 = "oh.memory-page.v1";
|
|
353
|
+
var OH_MEMORY_PAGE_MARKDOWN_EXTENSION_V1 = ".oh.md";
|
|
354
|
+
var OH_MEMORY_PAGE_LIMITS_V1 = Object.freeze({
|
|
355
|
+
bodyBytes: 512 * 1024,
|
|
356
|
+
fileBytes: 1024 * 1024,
|
|
357
|
+
frontmatterLines: 18 + OH_GRAPH_LIMITS_V1.dependenciesPerRecord + 5 * 128,
|
|
358
|
+
languageBytes: 255,
|
|
359
|
+
sourceTitleBytes: 1024,
|
|
360
|
+
sourceUrlBytes: 4096,
|
|
361
|
+
sources: 128,
|
|
362
|
+
summaryBytes: 8192,
|
|
363
|
+
titleBytes: 512,
|
|
364
|
+
valueBytes: 768 * 1024
|
|
365
|
+
});
|
|
366
|
+
function singleLineText(value, maximumBytes) {
|
|
367
|
+
const parsed = boundedText(value, maximumBytes);
|
|
368
|
+
return parsed !== null && !/[\r\n\u0085\u2028\u2029]/u.test(parsed) ? parsed : null;
|
|
369
|
+
}
|
|
370
|
+
function exactDataRecord(value, keys) {
|
|
371
|
+
try {
|
|
372
|
+
if (!isPlainRecord(value))
|
|
373
|
+
return null;
|
|
374
|
+
const ownKeys = Reflect.ownKeys(value);
|
|
375
|
+
if (ownKeys.length !== keys.length || ownKeys.some((key) => typeof key !== "string") || keys.some((key) => !ownKeys.includes(key)))
|
|
376
|
+
return null;
|
|
377
|
+
const detached = {};
|
|
378
|
+
for (const key of keys) {
|
|
379
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
380
|
+
if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined)
|
|
381
|
+
return null;
|
|
382
|
+
detached[key] = descriptor.value;
|
|
383
|
+
}
|
|
384
|
+
return detached;
|
|
385
|
+
} catch {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function exactDataArray(value, maximumLength) {
|
|
390
|
+
try {
|
|
391
|
+
if (!Array.isArray(value))
|
|
392
|
+
return null;
|
|
393
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
|
|
394
|
+
const length = lengthDescriptor?.value;
|
|
395
|
+
if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0 || length > maximumLength)
|
|
396
|
+
return null;
|
|
397
|
+
const ownKeys = Reflect.ownKeys(value);
|
|
398
|
+
if (ownKeys.length !== length + 1 || ownKeys.some((key) => typeof key !== "string") || !ownKeys.includes("length"))
|
|
399
|
+
return null;
|
|
400
|
+
const detached = [];
|
|
401
|
+
for (let index = 0;index < length; index += 1) {
|
|
402
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
403
|
+
if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined)
|
|
404
|
+
return null;
|
|
405
|
+
detached.push(descriptor.value);
|
|
406
|
+
}
|
|
407
|
+
return detached;
|
|
408
|
+
} catch {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function parseLanguage(value) {
|
|
413
|
+
if (value === null)
|
|
414
|
+
return null;
|
|
415
|
+
return typeof value === "string" && utf8ByteLength(value) <= OH_MEMORY_PAGE_LIMITS_V1.languageBytes && /^(?:und|[a-z]{2,3}(?:-[a-z0-9]{2,8})*)$/u.test(value) ? value : undefined;
|
|
416
|
+
}
|
|
417
|
+
function parseCanonicalSourceUrl(value) {
|
|
418
|
+
if (typeof value !== "string" || value.normalize("NFC") !== value || utf8ByteLength(value) > OH_MEMORY_PAGE_LIMITS_V1.sourceUrlBytes)
|
|
419
|
+
return null;
|
|
420
|
+
try {
|
|
421
|
+
const url = new URL(value);
|
|
422
|
+
if (url.protocol !== "https:" && url.protocol !== "http:" || url.username !== "" || url.password !== "" || url.href !== value)
|
|
423
|
+
return null;
|
|
424
|
+
for (let index = value.indexOf("%");index >= 0; index = value.indexOf("%", index + 3)) {
|
|
425
|
+
const encoded = value.slice(index + 1, index + 3);
|
|
426
|
+
if (!/^[0-9A-F]{2}$/u.test(encoded))
|
|
427
|
+
return null;
|
|
428
|
+
const decoded = String.fromCharCode(Number.parseInt(encoded, 16));
|
|
429
|
+
if (/^[A-Za-z0-9._~-]$/u.test(decoded))
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
return value;
|
|
433
|
+
} catch {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function parseSource(value) {
|
|
438
|
+
const source = exactDataRecord(value, ["contentSha256", "observedAt", "title", "url", "v"]);
|
|
439
|
+
if (source === null || source.v !== 1)
|
|
440
|
+
return null;
|
|
441
|
+
const contentSha256 = parseSha256Hex(source.contentSha256);
|
|
442
|
+
const observedAt = parseCanonicalInstantV1(source.observedAt);
|
|
443
|
+
const title = singleLineText(source.title, OH_MEMORY_PAGE_LIMITS_V1.sourceTitleBytes);
|
|
444
|
+
const url = parseCanonicalSourceUrl(source.url);
|
|
445
|
+
return contentSha256 !== null && observedAt !== null && title !== null && url !== null ? { contentSha256, observedAt, title, url, v: 1 } : null;
|
|
446
|
+
}
|
|
447
|
+
function parseProvenance(value) {
|
|
448
|
+
const provenance = exactDataRecord(value, ["actorId", "attestationSha256", "attestedAt", "kind", "v"]);
|
|
449
|
+
if (provenance === null || provenance.kind !== "host-attested" || provenance.v !== 1)
|
|
450
|
+
return null;
|
|
451
|
+
const actorId = safeCode(provenance.actorId);
|
|
452
|
+
const attestationSha256 = parseSha256Hex(provenance.attestationSha256);
|
|
453
|
+
const attestedAt = parseCanonicalInstantV1(provenance.attestedAt);
|
|
454
|
+
return actorId !== null && attestationSha256 !== null && attestedAt !== null ? { actorId, attestationSha256, attestedAt, kind: "host-attested", v: 1 } : null;
|
|
455
|
+
}
|
|
456
|
+
function parseOhMemoryPageValueV1(value) {
|
|
457
|
+
const page = exactDataRecord(value, [
|
|
458
|
+
"body",
|
|
459
|
+
"createdAt",
|
|
460
|
+
"format",
|
|
461
|
+
"language",
|
|
462
|
+
"provenance",
|
|
463
|
+
"sources",
|
|
464
|
+
"summary",
|
|
465
|
+
"title",
|
|
466
|
+
"updatedAt",
|
|
467
|
+
"v"
|
|
468
|
+
]);
|
|
469
|
+
if (page === null || page.format !== OH_MEMORY_PAGE_FORMAT_V1 || page.v !== 1)
|
|
470
|
+
return null;
|
|
471
|
+
const sourceValues = exactDataArray(page.sources, OH_MEMORY_PAGE_LIMITS_V1.sources);
|
|
472
|
+
if (sourceValues === null)
|
|
473
|
+
return null;
|
|
474
|
+
const body = boundedText(page.body, OH_MEMORY_PAGE_LIMITS_V1.bodyBytes);
|
|
475
|
+
const createdAt = parseCanonicalInstantV1(page.createdAt);
|
|
476
|
+
const language = parseLanguage(page.language);
|
|
477
|
+
const provenance = parseProvenance(page.provenance);
|
|
478
|
+
const sources = sourceValues.map(parseSource);
|
|
479
|
+
const summary = boundedText(page.summary, OH_MEMORY_PAGE_LIMITS_V1.summaryBytes);
|
|
480
|
+
const title = singleLineText(page.title, OH_MEMORY_PAGE_LIMITS_V1.titleBytes);
|
|
481
|
+
const updatedAt = parseCanonicalInstantV1(page.updatedAt);
|
|
482
|
+
if (body === null || createdAt === null || language === undefined || provenance === null || sources.some((source) => source === null) || summary === null || title === null || updatedAt === null) {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
const parsedSources = sources;
|
|
486
|
+
if (!orderedUnique(parsedSources, (source) => source.url) || Date.parse(createdAt) > Date.parse(updatedAt) || Date.parse(updatedAt) > Date.parse(provenance.attestedAt) || parsedSources.some((source) => Date.parse(source.observedAt) > Date.parse(updatedAt)))
|
|
487
|
+
return null;
|
|
488
|
+
const parsed = {
|
|
489
|
+
body,
|
|
490
|
+
createdAt,
|
|
491
|
+
format: OH_MEMORY_PAGE_FORMAT_V1,
|
|
492
|
+
language,
|
|
493
|
+
provenance,
|
|
494
|
+
sources: parsedSources,
|
|
495
|
+
summary,
|
|
496
|
+
title,
|
|
497
|
+
updatedAt,
|
|
498
|
+
v: 1
|
|
499
|
+
};
|
|
500
|
+
return utf8ByteLength(canonicalJson(parsed)) <= OH_MEMORY_PAGE_LIMITS_V1.valueBytes ? parsed : null;
|
|
501
|
+
}
|
|
502
|
+
function createOhMemoryPageValueV1(value) {
|
|
503
|
+
const parsed = parseOhMemoryPageValueV1(value);
|
|
504
|
+
if (parsed === null)
|
|
505
|
+
throw new TypeError("Invalid Oh memory page value.");
|
|
506
|
+
return parsed;
|
|
507
|
+
}
|
|
508
|
+
function createOhMemoryPageRecordV1(input) {
|
|
509
|
+
const parsedInput = exactDataRecord(input, ["dependencies", "key", "value"]);
|
|
510
|
+
if (parsedInput === null) {
|
|
511
|
+
throw new TypeError("Invalid Oh memory page record input.");
|
|
512
|
+
}
|
|
513
|
+
const value = createOhMemoryPageValueV1(parsedInput.value);
|
|
514
|
+
const record = createKnowledgeGraphRecordV1({
|
|
515
|
+
dependencies: parsedInput.dependencies,
|
|
516
|
+
key: parsedInput.key,
|
|
517
|
+
kind: "edition",
|
|
518
|
+
v: 1,
|
|
519
|
+
value
|
|
520
|
+
});
|
|
521
|
+
return { ...record, kind: "edition", value };
|
|
522
|
+
}
|
|
523
|
+
function parseOhMemoryPageRecordV1(value) {
|
|
524
|
+
const envelope = exactDataRecord(value, [
|
|
525
|
+
"dependencies",
|
|
526
|
+
"key",
|
|
527
|
+
"kind",
|
|
528
|
+
"recordSha256",
|
|
529
|
+
"v",
|
|
530
|
+
"value"
|
|
531
|
+
]);
|
|
532
|
+
if (envelope === null)
|
|
533
|
+
return null;
|
|
534
|
+
const record = parseKnowledgeGraphRecordV1(envelope);
|
|
535
|
+
if (record === null || record.kind !== "edition")
|
|
536
|
+
return null;
|
|
537
|
+
const page = parseOhMemoryPageValueV1(record.value);
|
|
538
|
+
return page === null ? null : { ...record, kind: "edition", value: page };
|
|
539
|
+
}
|
|
540
|
+
var OH_MEMORY_PAGE_RECORD_CODEC_V1 = Object.freeze({
|
|
541
|
+
kind: "edition",
|
|
542
|
+
parse(value) {
|
|
543
|
+
return parseOhMemoryPageValueV1(value);
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
function scalar(value) {
|
|
547
|
+
return JSON.stringify(value).replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029");
|
|
548
|
+
}
|
|
549
|
+
function dependencyPrefix(index) {
|
|
550
|
+
return `dependency-${index.toString().padStart(4, "0")}`;
|
|
551
|
+
}
|
|
552
|
+
function sourcePrefix(index) {
|
|
553
|
+
return `source-${index.toString().padStart(3, "0")}`;
|
|
554
|
+
}
|
|
555
|
+
function markdownEntries(record) {
|
|
556
|
+
const page = record.value;
|
|
557
|
+
const entries = [
|
|
558
|
+
["format", page.format],
|
|
559
|
+
["record-v", record.v],
|
|
560
|
+
["record-kind", record.kind],
|
|
561
|
+
["record-key", record.key],
|
|
562
|
+
["record-sha256", record.recordSha256],
|
|
563
|
+
["dependency-count", record.dependencies.length]
|
|
564
|
+
];
|
|
565
|
+
record.dependencies.forEach((dependency, index) => {
|
|
566
|
+
entries.push([`${dependencyPrefix(index)}-key`, dependency]);
|
|
567
|
+
});
|
|
568
|
+
entries.push(["page-v", page.v], ["title", page.title], ["summary", page.summary], ["language", page.language], ["created-at", page.createdAt], ["updated-at", page.updatedAt], ["provenance-kind", page.provenance.kind], ["provenance-v", page.provenance.v], ["provenance-actor-id", page.provenance.actorId], ["provenance-attested-at", page.provenance.attestedAt], ["provenance-attestation-sha256", page.provenance.attestationSha256], ["source-count", page.sources.length]);
|
|
569
|
+
page.sources.forEach((source, index) => {
|
|
570
|
+
const prefix = sourcePrefix(index);
|
|
571
|
+
entries.push([`${prefix}-v`, source.v], [`${prefix}-url`, source.url], [`${prefix}-title`, source.title], [`${prefix}-observed-at`, source.observedAt], [`${prefix}-content-sha256`, source.contentSha256]);
|
|
572
|
+
});
|
|
573
|
+
return entries;
|
|
574
|
+
}
|
|
575
|
+
function renderOhMemoryPageMarkdownV1(value) {
|
|
576
|
+
const record = parseOhMemoryPageRecordV1(value);
|
|
577
|
+
if (record === null)
|
|
578
|
+
throw new TypeError("Invalid Oh memory page record.");
|
|
579
|
+
const frontmatter = markdownEntries(record).map(([key, item]) => `${key}: ${scalar(item)}`).join(`
|
|
580
|
+
`);
|
|
581
|
+
const rendered = `---
|
|
582
|
+
${frontmatter}
|
|
583
|
+
---
|
|
584
|
+
${record.value.body}`;
|
|
585
|
+
if (utf8ByteLength(rendered) > OH_MEMORY_PAGE_LIMITS_V1.fileBytes) {
|
|
586
|
+
throw new RangeError("Oh memory page Markdown exceeds its byte limit.");
|
|
587
|
+
}
|
|
588
|
+
return rendered;
|
|
589
|
+
}
|
|
590
|
+
function parseFrontmatterLine(line) {
|
|
591
|
+
const separator = line.indexOf(": ");
|
|
592
|
+
if (separator < 1 || !/^[a-z][a-z0-9-]*$/u.test(line.slice(0, separator)))
|
|
593
|
+
return null;
|
|
594
|
+
const key = line.slice(0, separator);
|
|
595
|
+
const encoded = line.slice(separator + 2);
|
|
596
|
+
let value;
|
|
597
|
+
try {
|
|
598
|
+
value = JSON.parse(encoded);
|
|
599
|
+
} catch {
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
if (value !== null && typeof value !== "string" && typeof value !== "number" || typeof value === "number" && !Number.isFinite(value) || scalar(value) !== encoded)
|
|
603
|
+
return null;
|
|
604
|
+
return [key, value];
|
|
605
|
+
}
|
|
606
|
+
function parseOhMemoryPageMarkdownV1(text) {
|
|
607
|
+
if (typeof text !== "string" || utf8ByteLength(text) > OH_MEMORY_PAGE_LIMITS_V1.fileBytes || !text.startsWith(`---
|
|
608
|
+
`))
|
|
609
|
+
return null;
|
|
610
|
+
const closing = text.indexOf(`
|
|
611
|
+
---
|
|
612
|
+
`, 4);
|
|
613
|
+
if (closing < 0)
|
|
614
|
+
return null;
|
|
615
|
+
const frontmatter = text.slice(4, closing);
|
|
616
|
+
let frontmatterLines = 1;
|
|
617
|
+
for (let index = frontmatter.indexOf(`
|
|
618
|
+
`);index >= 0; index = frontmatter.indexOf(`
|
|
619
|
+
`, index + 1)) {
|
|
620
|
+
frontmatterLines += 1;
|
|
621
|
+
if (frontmatterLines > OH_MEMORY_PAGE_LIMITS_V1.frontmatterLines)
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
const lines = frontmatter.split(`
|
|
625
|
+
`);
|
|
626
|
+
const entries = lines.map(parseFrontmatterLine);
|
|
627
|
+
if (entries.some((entry) => entry === null) || entries.length < 18)
|
|
628
|
+
return null;
|
|
629
|
+
const parsedEntries = entries;
|
|
630
|
+
const dependencyCount = parsedEntries[5]?.[1];
|
|
631
|
+
if (!Number.isSafeInteger(dependencyCount) || dependencyCount < 0 || dependencyCount > OH_GRAPH_LIMITS_V1.dependenciesPerRecord)
|
|
632
|
+
return null;
|
|
633
|
+
const pageOffset = 6 + dependencyCount;
|
|
634
|
+
const sourceCount = parsedEntries[pageOffset + 11]?.[1];
|
|
635
|
+
if (!Number.isSafeInteger(sourceCount) || sourceCount < 0 || sourceCount > OH_MEMORY_PAGE_LIMITS_V1.sources)
|
|
636
|
+
return null;
|
|
637
|
+
const expectedKeys = [
|
|
638
|
+
"format",
|
|
639
|
+
"record-v",
|
|
640
|
+
"record-kind",
|
|
641
|
+
"record-key",
|
|
642
|
+
"record-sha256",
|
|
643
|
+
"dependency-count",
|
|
644
|
+
...Array.from({ length: dependencyCount }, (_, index) => `${dependencyPrefix(index)}-key`),
|
|
645
|
+
"page-v",
|
|
646
|
+
"title",
|
|
647
|
+
"summary",
|
|
648
|
+
"language",
|
|
649
|
+
"created-at",
|
|
650
|
+
"updated-at",
|
|
651
|
+
"provenance-kind",
|
|
652
|
+
"provenance-v",
|
|
653
|
+
"provenance-actor-id",
|
|
654
|
+
"provenance-attested-at",
|
|
655
|
+
"provenance-attestation-sha256",
|
|
656
|
+
"source-count",
|
|
657
|
+
...Array.from({ length: sourceCount }, (_, index) => {
|
|
658
|
+
const prefix = sourcePrefix(index);
|
|
659
|
+
return [
|
|
660
|
+
`${prefix}-v`,
|
|
661
|
+
`${prefix}-url`,
|
|
662
|
+
`${prefix}-title`,
|
|
663
|
+
`${prefix}-observed-at`,
|
|
664
|
+
`${prefix}-content-sha256`
|
|
665
|
+
];
|
|
666
|
+
}).flat()
|
|
667
|
+
];
|
|
668
|
+
if (parsedEntries.length !== expectedKeys.length || parsedEntries.some(([key], index) => key !== expectedKeys[index]))
|
|
669
|
+
return null;
|
|
670
|
+
const dependencies = Array.from({ length: dependencyCount }, (_, index) => parsedEntries[6 + index]?.[1]);
|
|
671
|
+
const sources = [];
|
|
672
|
+
for (let index = 0;index < sourceCount; index += 1) {
|
|
673
|
+
const offset = pageOffset + 12 + index * 5;
|
|
674
|
+
sources.push({
|
|
675
|
+
v: parsedEntries[offset]?.[1],
|
|
676
|
+
url: parsedEntries[offset + 1]?.[1],
|
|
677
|
+
title: parsedEntries[offset + 2]?.[1],
|
|
678
|
+
observedAt: parsedEntries[offset + 3]?.[1],
|
|
679
|
+
contentSha256: parsedEntries[offset + 4]?.[1]
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
const page = parseOhMemoryPageValueV1({
|
|
683
|
+
body: text.slice(closing + 5),
|
|
684
|
+
createdAt: parsedEntries[pageOffset + 4]?.[1],
|
|
685
|
+
format: parsedEntries[0]?.[1],
|
|
686
|
+
language: parsedEntries[pageOffset + 3]?.[1],
|
|
687
|
+
provenance: {
|
|
688
|
+
actorId: parsedEntries[pageOffset + 8]?.[1],
|
|
689
|
+
attestationSha256: parsedEntries[pageOffset + 10]?.[1],
|
|
690
|
+
attestedAt: parsedEntries[pageOffset + 9]?.[1],
|
|
691
|
+
kind: parsedEntries[pageOffset + 6]?.[1],
|
|
692
|
+
v: parsedEntries[pageOffset + 7]?.[1]
|
|
693
|
+
},
|
|
694
|
+
sources,
|
|
695
|
+
summary: parsedEntries[pageOffset + 2]?.[1],
|
|
696
|
+
title: parsedEntries[pageOffset + 1]?.[1],
|
|
697
|
+
updatedAt: parsedEntries[pageOffset + 5]?.[1],
|
|
698
|
+
v: parsedEntries[pageOffset]?.[1]
|
|
699
|
+
});
|
|
700
|
+
if (page === null || parsedEntries[1]?.[1] !== 1 || parsedEntries[2]?.[1] !== "edition" || typeof parsedEntries[3]?.[1] !== "string" || typeof parsedEntries[4]?.[1] !== "string" || dependencies.some((dependency) => typeof dependency !== "string"))
|
|
701
|
+
return null;
|
|
702
|
+
let record;
|
|
703
|
+
try {
|
|
704
|
+
record = createOhMemoryPageRecordV1({
|
|
705
|
+
dependencies,
|
|
706
|
+
key: parsedEntries[3][1],
|
|
707
|
+
value: page
|
|
708
|
+
});
|
|
709
|
+
} catch {
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
return record.recordSha256 === parsedEntries[4][1] && renderOhMemoryPageMarkdownV1(record) === text ? record : null;
|
|
713
|
+
}
|
|
714
|
+
export {
|
|
715
|
+
renderOhMemoryPageMarkdownV1,
|
|
716
|
+
parseOhMemoryPageValueV1,
|
|
717
|
+
parseOhMemoryPageRecordV1,
|
|
718
|
+
parseOhMemoryPageMarkdownV1,
|
|
719
|
+
createOhMemoryPageValueV1,
|
|
720
|
+
createOhMemoryPageRecordV1,
|
|
721
|
+
OH_MEMORY_PAGE_RECORD_CODEC_V1,
|
|
722
|
+
OH_MEMORY_PAGE_MARKDOWN_EXTENSION_V1,
|
|
723
|
+
OH_MEMORY_PAGE_LIMITS_V1,
|
|
724
|
+
OH_MEMORY_PAGE_FORMAT_V1
|
|
725
|
+
};
|