@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.
- package/LICENSE +21 -0
- package/README.md +598 -0
- package/dist/canonical.d.ts +32 -0
- package/dist/canonical.d.ts.map +1 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +3419 -0
- package/dist/contract.d.ts +33 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/graph.d.ts +67 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1988 -0
- package/dist/libsql.d.ts +57 -0
- package/dist/libsql.d.ts.map +1 -0
- package/dist/libsql.js +2662 -0
- package/dist/memory.d.ts +366 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +3650 -0
- package/dist/ontology.d.ts +242 -0
- package/dist/ontology.d.ts.map +1 -0
- package/dist/operation.d.ts +24 -0
- package/dist/operation.d.ts.map +1 -0
- package/dist/projection-public.d.ts +59 -0
- package/dist/projection-public.d.ts.map +1 -0
- package/dist/projection-public.js +1682 -0
- package/dist/projection-suss.d.ts +17 -0
- package/dist/projection-suss.d.ts.map +1 -0
- package/dist/projection-suss.js +1721 -0
- package/dist/projection.d.ts +315 -0
- package/dist/projection.d.ts.map +1 -0
- package/dist/schema.d.ts +45 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/sdk.d.ts +51 -0
- package/dist/sdk.d.ts.map +1 -0
- package/dist/sdk.js +3072 -0
- package/dist/search.d.ts +34 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/semantic.d.ts +83 -0
- package/dist/semantic.d.ts.map +1 -0
- package/dist/semantic.js +706 -0
- package/dist/sqlite/driver.d.ts +6 -0
- package/dist/sqlite/driver.d.ts.map +1 -0
- package/dist/sqlite/index.d.ts +5 -0
- package/dist/sqlite/index.d.ts.map +1 -0
- package/dist/sqlite/index.js +2840 -0
- package/dist/sqlite/migrations.d.ts +10 -0
- package/dist/sqlite/migrations.d.ts.map +1 -0
- package/dist/sqlite/port.d.ts +40 -0
- package/dist/sqlite/port.d.ts.map +1 -0
- package/dist/sqlite/runtime.d.ts +23 -0
- package/dist/sqlite/runtime.d.ts.map +1 -0
- package/dist/sqlite/store.d.ts +85 -0
- package/dist/sqlite/store.d.ts.map +1 -0
- package/dist/store.d.ts +236 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +996 -0
- package/dist/sync.d.ts +57 -0
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +1328 -0
- package/package.json +121 -0
- package/skills/oh/SKILL.md +206 -0
- package/skills/oh/agents/openai.yaml +4 -0
- package/spec/README.md +74 -0
- package/spec/manifest.json +41 -0
- package/spec/v1/canonical-json.md +59 -0
- package/spec/v1/contract.json +28 -0
- package/spec/v1/contract.schema.json +58 -0
- package/spec/v1/embedding-profile.json +11 -0
- package/spec/v1/embedding.md +56 -0
- package/spec/v1/graph.md +87 -0
- package/spec/v1/memory.md +193 -0
- package/spec/v1/migration.md +92 -0
- package/spec/v1/ontology.json +55 -0
- package/spec/v1/ontology.md +80 -0
- package/spec/v1/operation.schema.json +138 -0
- package/spec/v1/projection-identity.schema.json +58 -0
- package/spec/v1/projection-query.schema.json +60 -0
- package/spec/v1/projection-result.schema.json +452 -0
- package/spec/v1/projection-rule-pack.schema.json +182 -0
- package/spec/v1/projection.md +165 -0
- package/spec/v1/record.schema.json +95 -0
- package/spec/v1/schema-evolution.md +51 -0
- package/spec/v1/schema-revision.schema.json +178 -0
- package/spec/v1/storage.md +88 -0
- package/spec/v1/store.md +131 -0
- package/spec/v1/sync-bundle.schema.json +51 -0
- package/spec/v1/sync.md +67 -0
- package/src/canonical.test.ts +46 -0
- package/src/canonical.ts +203 -0
- package/src/cli.test.ts +103 -0
- package/src/cli.ts +308 -0
- package/src/contract.ts +87 -0
- package/src/contracts.test.ts +147 -0
- package/src/graph.ts +248 -0
- package/src/index.ts +8 -0
- package/src/libsql.test.ts +657 -0
- package/src/libsql.ts +1687 -0
- package/src/memory.test.ts +783 -0
- package/src/memory.ts +1684 -0
- package/src/ontology.ts +573 -0
- package/src/operation.ts +80 -0
- package/src/projection-public.ts +53 -0
- package/src/projection-suss.ts +129 -0
- package/src/projection.test.ts +418 -0
- package/src/projection.ts +1457 -0
- package/src/schema.ts +156 -0
- package/src/sdk.ts +96 -0
- package/src/search.ts +66 -0
- package/src/semantic.test.ts +480 -0
- package/src/semantic.ts +333 -0
- package/src/sqlite/driver.ts +47 -0
- package/src/sqlite/index.ts +4 -0
- package/src/sqlite/migrations.test.ts +44 -0
- package/src/sqlite/migrations.ts +178 -0
- package/src/sqlite/port.test.ts +127 -0
- package/src/sqlite/port.ts +120 -0
- package/src/sqlite/runtime.test.ts +68 -0
- package/src/sqlite/runtime.ts +53 -0
- package/src/sqlite/store.test.ts +295 -0
- package/src/sqlite/store.ts +988 -0
- package/src/store.test.ts +121 -0
- package/src/store.ts +701 -0
- package/src/sync.test.ts +117 -0
- package/src/sync.ts +227 -0
package/src/ontology.ts
ADDED
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
import {
|
|
2
|
+
boundedText,
|
|
3
|
+
canonicalJson,
|
|
4
|
+
canonicalSha256,
|
|
5
|
+
hasExactKeys,
|
|
6
|
+
isPlainRecord,
|
|
7
|
+
orderedUnique,
|
|
8
|
+
parseCanonicalInstantV1,
|
|
9
|
+
parseSha256Hex,
|
|
10
|
+
safeCode,
|
|
11
|
+
sha256Hex,
|
|
12
|
+
sortUnique,
|
|
13
|
+
type Sha256Hex,
|
|
14
|
+
} from "./canonical";
|
|
15
|
+
|
|
16
|
+
export const OH_ONTOLOGY_VERSION_V1 = "1.0.0" as const;
|
|
17
|
+
export const OH_CONTRACT_ID_V1 = "oh.ontology.v1" as const;
|
|
18
|
+
|
|
19
|
+
export const OH_KNOWLEDGE_LIMITS_V1 = Object.freeze({
|
|
20
|
+
dimensions: 64,
|
|
21
|
+
listValues: 256,
|
|
22
|
+
qualifiers: 128,
|
|
23
|
+
statementBytes: 256 * 1024,
|
|
24
|
+
textBytes: 64 * 1024,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export const OH_KNOWLEDGE_KERNEL_CONCEPTS_V1 = [
|
|
28
|
+
{ code: "entity", description: "A stable identity anchor for something that can be referred to.", label: "Entity" },
|
|
29
|
+
{ code: "statement", description: "An immutable proposition with a subject, predicate, object, and qualifiers.", label: "Statement" },
|
|
30
|
+
{ code: "assertion", description: "An attributable stance toward a statement.", label: "Assertion" },
|
|
31
|
+
{ code: "evidence", description: "A typed account of how an observation bears on an assertion.", label: "Evidence" },
|
|
32
|
+
{ code: "context", description: "The scenario and dimensions in which knowledge applies.", label: "Context" },
|
|
33
|
+
{ code: "inquiry", description: "A question and its durable investigation trail.", label: "Inquiry" },
|
|
34
|
+
{ code: "projection", description: "A reproducible view derived from exact knowledge.", label: "Projection" },
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
37
|
+
export type KnowledgeOntologyIssueCode =
|
|
38
|
+
| "dependency-missing"
|
|
39
|
+
| "digest-mismatch"
|
|
40
|
+
| "invalid-input"
|
|
41
|
+
| "limit-exceeded"
|
|
42
|
+
| "noncanonical-input";
|
|
43
|
+
|
|
44
|
+
export type KnowledgeOntologyResult<T> =
|
|
45
|
+
| Readonly<{ ok: true; value: T }>
|
|
46
|
+
| Readonly<{ error: Readonly<{ code: KnowledgeOntologyIssueCode; field: string }>; ok: false }>;
|
|
47
|
+
|
|
48
|
+
function success<T>(value: T): KnowledgeOntologyResult<T> { return { ok: true, value }; }
|
|
49
|
+
function failure<T>(field: string, code: KnowledgeOntologyIssueCode = "invalid-input"): KnowledgeOntologyResult<T> {
|
|
50
|
+
return { error: { code, field }, ok: false };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
declare const entityIdBrand: unique symbol;
|
|
54
|
+
declare const assertionIdBrand: unique symbol;
|
|
55
|
+
declare const evidenceIdBrand: unique symbol;
|
|
56
|
+
declare const inquiryIdBrand: unique symbol;
|
|
57
|
+
export type KnowledgeEntityId = string & { readonly [entityIdBrand]: "KnowledgeEntityId" };
|
|
58
|
+
export type KnowledgeAssertionId = string & { readonly [assertionIdBrand]: "KnowledgeAssertionId" };
|
|
59
|
+
export type KnowledgeEvidenceId = string & { readonly [evidenceIdBrand]: "KnowledgeEvidenceId" };
|
|
60
|
+
export type KnowledgeInquiryId = string & { readonly [inquiryIdBrand]: "KnowledgeInquiryId" };
|
|
61
|
+
|
|
62
|
+
function parseOpaqueId<T extends string>(value: unknown, prefix: string): T | null {
|
|
63
|
+
return typeof value === "string" && new RegExp(`^${prefix}[a-z0-9]{24}$`, "u").test(value)
|
|
64
|
+
? value as T : null;
|
|
65
|
+
}
|
|
66
|
+
export function parseKnowledgeEntityId(value: unknown): KnowledgeEntityId | null {
|
|
67
|
+
return parseOpaqueId<KnowledgeEntityId>(value, "kent_");
|
|
68
|
+
}
|
|
69
|
+
export function parseKnowledgeAssertionId(value: unknown): KnowledgeAssertionId | null {
|
|
70
|
+
return parseOpaqueId<KnowledgeAssertionId>(value, "kast_");
|
|
71
|
+
}
|
|
72
|
+
export function parseKnowledgeEvidenceId(value: unknown): KnowledgeEvidenceId | null {
|
|
73
|
+
return parseOpaqueId<KnowledgeEvidenceId>(value, "kevd_");
|
|
74
|
+
}
|
|
75
|
+
export function parseKnowledgeInquiryId(value: unknown): KnowledgeInquiryId | null {
|
|
76
|
+
return parseOpaqueId<KnowledgeInquiryId>(value, "kinq_");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const OH_KNOWLEDGE_ENTITY_STATES_V1 = ["active", "quarantined", "redirected", "tombstoned"] as const;
|
|
80
|
+
export type KnowledgeEntityStateV1 = (typeof OH_KNOWLEDGE_ENTITY_STATES_V1)[number];
|
|
81
|
+
export type KnowledgeEntityV1 = Readonly<{
|
|
82
|
+
entityId: KnowledgeEntityId;
|
|
83
|
+
identityOperationId: string;
|
|
84
|
+
identityRevision: number;
|
|
85
|
+
redirectEntityId: KnowledgeEntityId | null;
|
|
86
|
+
state: KnowledgeEntityStateV1;
|
|
87
|
+
v: 1;
|
|
88
|
+
}>;
|
|
89
|
+
|
|
90
|
+
export function parseKnowledgeEntityV1(value: unknown): KnowledgeOntologyResult<KnowledgeEntityV1> {
|
|
91
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, ["entityId", "identityOperationId", "identityRevision",
|
|
92
|
+
"redirectEntityId", "state", "v"]) || value.v !== 1) return failure("entity");
|
|
93
|
+
const entityId = parseKnowledgeEntityId(value.entityId);
|
|
94
|
+
const identityOperationId = safeCode(value.identityOperationId);
|
|
95
|
+
const identityRevision = Number.isSafeInteger(value.identityRevision) && (value.identityRevision as number) > 0
|
|
96
|
+
? value.identityRevision as number : null;
|
|
97
|
+
const redirectEntityId = value.redirectEntityId === null ? null : parseKnowledgeEntityId(value.redirectEntityId);
|
|
98
|
+
const state = OH_KNOWLEDGE_ENTITY_STATES_V1.find((candidate) => candidate === value.state);
|
|
99
|
+
return entityId !== null && identityOperationId !== null && identityRevision !== null
|
|
100
|
+
&& (value.redirectEntityId === null || redirectEntityId !== null) && state !== undefined
|
|
101
|
+
&& ((state === "redirected") === (redirectEntityId !== null)) && redirectEntityId !== entityId
|
|
102
|
+
? success({ entityId, identityOperationId, identityRevision, redirectEntityId, state, v: 1 })
|
|
103
|
+
: failure("entity");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export type KnowledgeSchemaRefV1 = Readonly<{
|
|
107
|
+
code: string;
|
|
108
|
+
namespace: string;
|
|
109
|
+
revision: number;
|
|
110
|
+
schemaSha256: Sha256Hex;
|
|
111
|
+
v: 1;
|
|
112
|
+
}>;
|
|
113
|
+
|
|
114
|
+
export function parseKnowledgeSchemaRefV1(value: unknown): KnowledgeOntologyResult<KnowledgeSchemaRefV1> {
|
|
115
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, ["code", "namespace", "revision", "schemaSha256", "v"])
|
|
116
|
+
|| value.v !== 1) return failure("schemaRef");
|
|
117
|
+
const code = safeCode(value.code);
|
|
118
|
+
const namespace = safeCode(value.namespace);
|
|
119
|
+
const revision = Number.isSafeInteger(value.revision) && (value.revision as number) > 0
|
|
120
|
+
? value.revision as number : null;
|
|
121
|
+
const schemaSha256 = parseSha256Hex(value.schemaSha256);
|
|
122
|
+
return code !== null && namespace !== null && revision !== null && schemaSha256 !== null
|
|
123
|
+
? success({ code, namespace, revision, schemaSha256, v: 1 }) : failure("schemaRef");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export type KnowledgeValueV1 =
|
|
127
|
+
| Readonly<{ entityId: KnowledgeEntityId; kind: "entity"; v: 1 }>
|
|
128
|
+
| Readonly<{ kind: "text"; language: string; text: string; v: 1 }>
|
|
129
|
+
| Readonly<{ kind: "string"; v: 1; value: string }>
|
|
130
|
+
| Readonly<{ kind: "boolean"; v: 1; value: boolean }>
|
|
131
|
+
| Readonly<{ kind: "integer" | "decimal"; v: 1; value: string }>
|
|
132
|
+
| Readonly<{ kind: "uri"; uri: string; v: 1 }>
|
|
133
|
+
| Readonly<{ kind: "list" | "set"; v: 1; values: readonly KnowledgeValueV1[] }>
|
|
134
|
+
| Readonly<{
|
|
135
|
+
canonicalizerSha256: Sha256Hex;
|
|
136
|
+
canonicalValue: string;
|
|
137
|
+
kind: "extension";
|
|
138
|
+
mediaType: string;
|
|
139
|
+
schema: KnowledgeSchemaRefV1;
|
|
140
|
+
v: 1;
|
|
141
|
+
valueSha256: Sha256Hex;
|
|
142
|
+
}>;
|
|
143
|
+
|
|
144
|
+
const INTEGER = /^(?:0|-[1-9][0-9]*|[1-9][0-9]*)$/u;
|
|
145
|
+
const DECIMAL = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]*[1-9])?$/u;
|
|
146
|
+
|
|
147
|
+
function parseKnowledgeValueInternal(value: unknown, depth: number): KnowledgeValueV1 | null {
|
|
148
|
+
if (!isPlainRecord(value) || value.v !== 1 || depth > 8) return null;
|
|
149
|
+
switch (value.kind) {
|
|
150
|
+
case "entity": {
|
|
151
|
+
if (!hasExactKeys(value, ["entityId", "kind", "v"])) return null;
|
|
152
|
+
const entityId = parseKnowledgeEntityId(value.entityId);
|
|
153
|
+
return entityId === null ? null : { entityId, kind: "entity", v: 1 };
|
|
154
|
+
}
|
|
155
|
+
case "text": {
|
|
156
|
+
if (!hasExactKeys(value, ["kind", "language", "text", "v"])) return null;
|
|
157
|
+
const language = typeof value.language === "string"
|
|
158
|
+
&& /^(?:und|[a-z]{2,3}(?:-[a-z0-9]{2,8})*)$/u.test(value.language)
|
|
159
|
+
? value.language : null;
|
|
160
|
+
const text = boundedText(value.text);
|
|
161
|
+
return language !== null && text !== null ? { kind: "text", language, text, v: 1 } : null;
|
|
162
|
+
}
|
|
163
|
+
case "string": {
|
|
164
|
+
const parsed = boundedText(value.value);
|
|
165
|
+
return hasExactKeys(value, ["kind", "v", "value"]) && parsed !== null
|
|
166
|
+
? { kind: "string", v: 1, value: parsed } : null;
|
|
167
|
+
}
|
|
168
|
+
case "boolean":
|
|
169
|
+
return hasExactKeys(value, ["kind", "v", "value"]) && typeof value.value === "boolean"
|
|
170
|
+
? { kind: "boolean", v: 1, value: value.value } : null;
|
|
171
|
+
case "integer":
|
|
172
|
+
case "decimal": {
|
|
173
|
+
const valid = typeof value.value === "string" && value.value.length <= 1024
|
|
174
|
+
&& (value.kind === "integer" ? INTEGER.test(value.value) : DECIMAL.test(value.value) && value.value !== "-0");
|
|
175
|
+
return hasExactKeys(value, ["kind", "v", "value"]) && valid
|
|
176
|
+
? { kind: value.kind, v: 1, value: value.value as string } : null;
|
|
177
|
+
}
|
|
178
|
+
case "uri": {
|
|
179
|
+
if (!hasExactKeys(value, ["kind", "uri", "v"]) || typeof value.uri !== "string" || value.uri.length > 4096) return null;
|
|
180
|
+
try {
|
|
181
|
+
const url = new URL(value.uri);
|
|
182
|
+
return url.href === value.uri && url.username === "" && url.password === ""
|
|
183
|
+
&& !["data:", "file:", "javascript:"].includes(url.protocol)
|
|
184
|
+
? { kind: "uri", uri: value.uri, v: 1 } : null;
|
|
185
|
+
} catch { return null; }
|
|
186
|
+
}
|
|
187
|
+
case "list":
|
|
188
|
+
case "set": {
|
|
189
|
+
if (!hasExactKeys(value, ["kind", "v", "values"]) || !Array.isArray(value.values)
|
|
190
|
+
|| value.values.length > OH_KNOWLEDGE_LIMITS_V1.listValues) return null;
|
|
191
|
+
const values: KnowledgeValueV1[] = [];
|
|
192
|
+
for (const item of value.values) {
|
|
193
|
+
const parsed = parseKnowledgeValueInternal(item, depth + 1);
|
|
194
|
+
if (parsed === null) return null;
|
|
195
|
+
values.push(parsed);
|
|
196
|
+
}
|
|
197
|
+
if (value.kind === "set" && !orderedUnique(values, canonicalJson)) return null;
|
|
198
|
+
return { kind: value.kind, v: 1, values };
|
|
199
|
+
}
|
|
200
|
+
case "extension": {
|
|
201
|
+
if (!hasExactKeys(value, ["canonicalizerSha256", "canonicalValue", "kind", "mediaType", "schema", "v", "valueSha256"])) return null;
|
|
202
|
+
const canonicalizerSha256 = parseSha256Hex(value.canonicalizerSha256);
|
|
203
|
+
const canonicalValue = boundedText(value.canonicalValue, 64 * 1024);
|
|
204
|
+
const mediaType = typeof value.mediaType === "string" && /^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/u.test(value.mediaType)
|
|
205
|
+
? value.mediaType : null;
|
|
206
|
+
const schema = parseKnowledgeSchemaRefV1(value.schema);
|
|
207
|
+
const valueSha256 = parseSha256Hex(value.valueSha256);
|
|
208
|
+
return canonicalizerSha256 !== null && canonicalValue !== null && mediaType !== null && schema.ok && valueSha256 !== null
|
|
209
|
+
? { canonicalizerSha256, canonicalValue, kind: "extension", mediaType, schema: schema.value, v: 1, valueSha256 }
|
|
210
|
+
: null;
|
|
211
|
+
}
|
|
212
|
+
default: return null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function parseKnowledgeValueV1(value: unknown): KnowledgeOntologyResult<KnowledgeValueV1> {
|
|
217
|
+
const parsed = parseKnowledgeValueInternal(value, 0);
|
|
218
|
+
return parsed === null ? failure("value") : success(parsed);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function verifyKnowledgeValueV1(value: KnowledgeValueV1): KnowledgeOntologyResult<KnowledgeValueV1> {
|
|
222
|
+
const parsed = parseKnowledgeValueV1(value);
|
|
223
|
+
if (!parsed.ok) return parsed;
|
|
224
|
+
if (parsed.value.kind === "extension" && sha256Hex(parsed.value.canonicalValue) !== parsed.value.valueSha256) {
|
|
225
|
+
return failure("valueSha256", "digest-mismatch");
|
|
226
|
+
}
|
|
227
|
+
if (parsed.value.kind === "list" || parsed.value.kind === "set") {
|
|
228
|
+
for (const child of parsed.value.values) {
|
|
229
|
+
const verified = verifyKnowledgeValueV1(child);
|
|
230
|
+
if (!verified.ok) return verified;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return success(parsed.value);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export type KnowledgeDimensionV1 = Readonly<{ predicate: KnowledgeSchemaRefV1; v: 1; value: KnowledgeValueV1 }>;
|
|
237
|
+
|
|
238
|
+
function parseDimension(value: unknown): KnowledgeDimensionV1 | null {
|
|
239
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, ["predicate", "v", "value"]) || value.v !== 1) return null;
|
|
240
|
+
const predicate = parseKnowledgeSchemaRefV1(value.predicate);
|
|
241
|
+
const parsedValue = parseKnowledgeValueV1(value.value);
|
|
242
|
+
return predicate.ok && parsedValue.ok ? { predicate: predicate.value, v: 1, value: parsedValue.value } : null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export type KnowledgeContextV1 = Readonly<{
|
|
246
|
+
contextSha256: Sha256Hex;
|
|
247
|
+
dimensions: readonly KnowledgeDimensionV1[];
|
|
248
|
+
scenario: "actual" | "counterfactual" | "hypothetical" | "planned";
|
|
249
|
+
v: 1;
|
|
250
|
+
}>;
|
|
251
|
+
export type KnowledgeContextInputV1 = Omit<KnowledgeContextV1, "contextSha256">;
|
|
252
|
+
|
|
253
|
+
export function createKnowledgeContextV1(input: KnowledgeContextInputV1): KnowledgeOntologyResult<KnowledgeContextV1> {
|
|
254
|
+
if (!isPlainRecord(input) || input.v !== 1 || !Array.isArray(input.dimensions)
|
|
255
|
+
|| input.dimensions.length > OH_KNOWLEDGE_LIMITS_V1.dimensions
|
|
256
|
+
|| !["actual", "counterfactual", "hypothetical", "planned"].includes(input.scenario)) return failure("context");
|
|
257
|
+
const dimensions: KnowledgeDimensionV1[] = [];
|
|
258
|
+
for (const item of input.dimensions) {
|
|
259
|
+
const parsed = parseDimension(item);
|
|
260
|
+
if (parsed === null) return failure("dimensions");
|
|
261
|
+
const verified = verifyKnowledgeValueV1(parsed.value);
|
|
262
|
+
if (!verified.ok) return verified;
|
|
263
|
+
dimensions.push(parsed);
|
|
264
|
+
}
|
|
265
|
+
let canonicalDimensions: readonly KnowledgeDimensionV1[];
|
|
266
|
+
try { canonicalDimensions = sortUnique(dimensions, canonicalJson); } catch { return failure("dimensions", "noncanonical-input"); }
|
|
267
|
+
const payload = { dimensions: canonicalDimensions, scenario: input.scenario, v: 1 as const };
|
|
268
|
+
return success({ ...payload, contextSha256: canonicalSha256(payload) });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function parseKnowledgeContextV1(value: unknown): KnowledgeOntologyResult<KnowledgeContextV1> {
|
|
272
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, ["contextSha256", "dimensions", "scenario", "v"])) return failure("context");
|
|
273
|
+
const digest = parseSha256Hex(value.contextSha256);
|
|
274
|
+
if (digest === null) return failure("contextSha256");
|
|
275
|
+
const created = createKnowledgeContextV1({ dimensions: value.dimensions as KnowledgeDimensionV1[], scenario: value.scenario as KnowledgeContextInputV1["scenario"], v: value.v as 1 });
|
|
276
|
+
return created.ok && created.value.contextSha256 === digest && canonicalJson(created.value.dimensions) === canonicalJson(value.dimensions)
|
|
277
|
+
? success({ ...created.value, contextSha256: digest }) : failure("contextSha256", "digest-mismatch");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export type KnowledgeStatementV1 = Readonly<{
|
|
281
|
+
object: KnowledgeValueV1;
|
|
282
|
+
predicate: KnowledgeSchemaRefV1;
|
|
283
|
+
qualifiers: readonly KnowledgeDimensionV1[];
|
|
284
|
+
statementSha256: Sha256Hex;
|
|
285
|
+
subject: KnowledgeEntityId;
|
|
286
|
+
v: 1;
|
|
287
|
+
}>;
|
|
288
|
+
export type KnowledgeStatementInputV1 = Omit<KnowledgeStatementV1, "statementSha256">;
|
|
289
|
+
|
|
290
|
+
export function createKnowledgeStatementV1(input: KnowledgeStatementInputV1): KnowledgeOntologyResult<KnowledgeStatementV1> {
|
|
291
|
+
const object = parseKnowledgeValueV1(input.object);
|
|
292
|
+
const predicate = parseKnowledgeSchemaRefV1(input.predicate);
|
|
293
|
+
const subject = parseKnowledgeEntityId(input.subject);
|
|
294
|
+
if (input.v !== 1 || !object.ok || !predicate.ok || subject === null || !Array.isArray(input.qualifiers)
|
|
295
|
+
|| input.qualifiers.length > OH_KNOWLEDGE_LIMITS_V1.qualifiers) return failure("statement");
|
|
296
|
+
const verifiedObject = verifyKnowledgeValueV1(object.value);
|
|
297
|
+
if (!verifiedObject.ok) return verifiedObject;
|
|
298
|
+
const qualifiers: KnowledgeDimensionV1[] = [];
|
|
299
|
+
for (const item of input.qualifiers) {
|
|
300
|
+
const parsed = parseDimension(item);
|
|
301
|
+
if (parsed === null) return failure("qualifiers");
|
|
302
|
+
const verified = verifyKnowledgeValueV1(parsed.value);
|
|
303
|
+
if (!verified.ok) return verified;
|
|
304
|
+
qualifiers.push(parsed);
|
|
305
|
+
}
|
|
306
|
+
let canonicalQualifiers: readonly KnowledgeDimensionV1[];
|
|
307
|
+
try { canonicalQualifiers = sortUnique(qualifiers, canonicalJson); } catch { return failure("qualifiers", "noncanonical-input"); }
|
|
308
|
+
const payload = { object: object.value, predicate: predicate.value, qualifiers: canonicalQualifiers, subject, v: 1 as const };
|
|
309
|
+
if (Buffer.byteLength(canonicalJson(payload), "utf8") > OH_KNOWLEDGE_LIMITS_V1.statementBytes) return failure("statement", "limit-exceeded");
|
|
310
|
+
return success({ ...payload, statementSha256: canonicalSha256(payload) });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function parseKnowledgeStatementV1(value: unknown): KnowledgeOntologyResult<KnowledgeStatementV1> {
|
|
314
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, ["object", "predicate", "qualifiers", "statementSha256", "subject", "v"])) return failure("statement");
|
|
315
|
+
const digest = parseSha256Hex(value.statementSha256);
|
|
316
|
+
const created = createKnowledgeStatementV1(value as unknown as KnowledgeStatementInputV1);
|
|
317
|
+
return digest !== null && created.ok && created.value.statementSha256 === digest
|
|
318
|
+
&& canonicalJson(created.value.qualifiers) === canonicalJson(value.qualifiers)
|
|
319
|
+
? success({ ...created.value, statementSha256: digest }) : failure("statementSha256", "digest-mismatch");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export type KnowledgeAgentRefV1 =
|
|
323
|
+
| Readonly<{ entityId: KnowledgeEntityId; kind: "entity"; v: 1 }>
|
|
324
|
+
| Readonly<{ kind: "model"; model: KnowledgeSchemaRefV1; receiptSha256: Sha256Hex; v: 1 }>
|
|
325
|
+
| Readonly<{ authority: KnowledgeSchemaRefV1; kind: "system"; receiptSha256: Sha256Hex; v: 1 }>;
|
|
326
|
+
|
|
327
|
+
function parseKnowledgeAgentRefV1(value: unknown): KnowledgeAgentRefV1 | null {
|
|
328
|
+
if (!isPlainRecord(value) || value.v !== 1) return null;
|
|
329
|
+
if (value.kind === "entity" && hasExactKeys(value, ["entityId", "kind", "v"])) {
|
|
330
|
+
const entityId = parseKnowledgeEntityId(value.entityId);
|
|
331
|
+
return entityId === null ? null : { entityId, kind: "entity", v: 1 };
|
|
332
|
+
}
|
|
333
|
+
if (value.kind === "model" && hasExactKeys(value, ["kind", "model", "receiptSha256", "v"])) {
|
|
334
|
+
const model = parseKnowledgeSchemaRefV1(value.model);
|
|
335
|
+
const receiptSha256 = parseSha256Hex(value.receiptSha256);
|
|
336
|
+
return model.ok && receiptSha256 !== null ? { kind: "model", model: model.value, receiptSha256, v: 1 } : null;
|
|
337
|
+
}
|
|
338
|
+
if (value.kind === "system" && hasExactKeys(value, ["authority", "kind", "receiptSha256", "v"])) {
|
|
339
|
+
const authority = parseKnowledgeSchemaRefV1(value.authority);
|
|
340
|
+
const receiptSha256 = parseSha256Hex(value.receiptSha256);
|
|
341
|
+
return authority.ok && receiptSha256 !== null
|
|
342
|
+
? { authority: authority.value, kind: "system", receiptSha256, v: 1 } : null;
|
|
343
|
+
}
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function parseDigestArray(value: unknown, maximum = 2048): readonly Sha256Hex[] | null {
|
|
348
|
+
if (!Array.isArray(value) || value.length > maximum) return null;
|
|
349
|
+
const digests = value.map(parseSha256Hex);
|
|
350
|
+
return digests.every((digest) => digest !== null) && orderedUnique(digests as Sha256Hex[], String)
|
|
351
|
+
? digests as Sha256Hex[] : null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export const OH_KNOWLEDGE_ACTIVITY_KINDS_V1 = [
|
|
355
|
+
"extraction", "human-entry", "human-review", "import", "model-proposal",
|
|
356
|
+
"normalization", "publication", "resolution", "transformation",
|
|
357
|
+
] as const;
|
|
358
|
+
export type KnowledgeActivityKindV1 = (typeof OH_KNOWLEDGE_ACTIVITY_KINDS_V1)[number];
|
|
359
|
+
export type KnowledgeActivityV1 = Readonly<{
|
|
360
|
+
activitySha256: Sha256Hex;
|
|
361
|
+
actor: KnowledgeAgentRefV1;
|
|
362
|
+
inputSha256s: readonly Sha256Hex[];
|
|
363
|
+
kind: KnowledgeActivityKindV1;
|
|
364
|
+
occurredAt: string;
|
|
365
|
+
outputSha256s: readonly Sha256Hex[];
|
|
366
|
+
policySha256: Sha256Hex;
|
|
367
|
+
tool: KnowledgeSchemaRefV1 | null;
|
|
368
|
+
v: 1;
|
|
369
|
+
}>;
|
|
370
|
+
export type KnowledgeActivityInputV1 = Omit<KnowledgeActivityV1, "activitySha256">;
|
|
371
|
+
|
|
372
|
+
function parseActivityInput(value: unknown): KnowledgeActivityInputV1 | null {
|
|
373
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, ["actor", "inputSha256s", "kind", "occurredAt",
|
|
374
|
+
"outputSha256s", "policySha256", "tool", "v"]) || value.v !== 1) return null;
|
|
375
|
+
const actor = parseKnowledgeAgentRefV1(value.actor);
|
|
376
|
+
const inputSha256s = parseDigestArray(value.inputSha256s);
|
|
377
|
+
const kind = OH_KNOWLEDGE_ACTIVITY_KINDS_V1.find((candidate) => candidate === value.kind);
|
|
378
|
+
const occurredAt = parseCanonicalInstantV1(value.occurredAt);
|
|
379
|
+
const outputSha256s = parseDigestArray(value.outputSha256s);
|
|
380
|
+
const policySha256 = parseSha256Hex(value.policySha256);
|
|
381
|
+
const tool = value.tool === null ? null : parseKnowledgeSchemaRefV1(value.tool);
|
|
382
|
+
const parsedTool = tool === null ? null : tool.ok ? tool.value : null;
|
|
383
|
+
return actor !== null && inputSha256s !== null && kind !== undefined && occurredAt !== null
|
|
384
|
+
&& outputSha256s !== null && policySha256 !== null && (value.tool === null || parsedTool !== null)
|
|
385
|
+
? { actor, inputSha256s, kind, occurredAt, outputSha256s, policySha256, tool: parsedTool, v: 1 }
|
|
386
|
+
: null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function createKnowledgeActivityV1(input: KnowledgeActivityInputV1): KnowledgeOntologyResult<KnowledgeActivityV1> {
|
|
390
|
+
const parsed = parseActivityInput(input);
|
|
391
|
+
return parsed === null ? failure("activity") : success({ ...parsed, activitySha256: canonicalSha256(parsed) });
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export function parseKnowledgeActivityV1(value: unknown): KnowledgeOntologyResult<KnowledgeActivityV1> {
|
|
395
|
+
if (!isPlainRecord(value) || !Object.hasOwn(value, "activitySha256")) return failure("activity");
|
|
396
|
+
const activitySha256 = parseSha256Hex(value.activitySha256);
|
|
397
|
+
const { activitySha256: _digest, ...input } = value;
|
|
398
|
+
const parsed = parseActivityInput(input);
|
|
399
|
+
return activitySha256 !== null && parsed !== null && canonicalSha256(parsed) === activitySha256
|
|
400
|
+
? success({ ...parsed, activitySha256 }) : failure("activitySha256", "digest-mismatch");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export const OH_KNOWLEDGE_ASSERTION_STANCES_V1 = ["questions", "refutes", "reports", "supports", "undetermined"] as const;
|
|
404
|
+
export type KnowledgeAssertionStanceV1 = (typeof OH_KNOWLEDGE_ASSERTION_STANCES_V1)[number];
|
|
405
|
+
export const OH_KNOWLEDGE_ASSERTION_STATES_V1 = [
|
|
406
|
+
"accepted-for-purpose", "disputed", "proposed", "reviewed", "superseded", "withdrawn",
|
|
407
|
+
] as const;
|
|
408
|
+
export type KnowledgeAssertionStateV1 = (typeof OH_KNOWLEDGE_ASSERTION_STATES_V1)[number];
|
|
409
|
+
export type KnowledgeAssertionV1 = Readonly<{
|
|
410
|
+
acceptedPurposes: readonly string[];
|
|
411
|
+
assertionId: KnowledgeAssertionId;
|
|
412
|
+
assertionSha256: Sha256Hex;
|
|
413
|
+
assertor: KnowledgeAgentRefV1;
|
|
414
|
+
confidence: KnowledgeSchemaRefV1 | null;
|
|
415
|
+
contextSha256: Sha256Hex | null;
|
|
416
|
+
provenanceActivitySha256: Sha256Hex;
|
|
417
|
+
reviewActivitySha256: Sha256Hex | null;
|
|
418
|
+
stance: KnowledgeAssertionStanceV1;
|
|
419
|
+
state: KnowledgeAssertionStateV1;
|
|
420
|
+
statementSha256: Sha256Hex;
|
|
421
|
+
v: 1;
|
|
422
|
+
}>;
|
|
423
|
+
export type KnowledgeAssertionInputV1 = Omit<KnowledgeAssertionV1, "assertionSha256">;
|
|
424
|
+
|
|
425
|
+
function parseStringCodes(value: unknown, maximum: number): readonly string[] | null {
|
|
426
|
+
if (!Array.isArray(value) || value.length > maximum) return null;
|
|
427
|
+
const codes = value.map((item) => safeCode(item));
|
|
428
|
+
return codes.every((code) => code !== null) && orderedUnique(codes as string[], String)
|
|
429
|
+
? codes as string[] : null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function parseAssertionInput(value: unknown): KnowledgeAssertionInputV1 | null {
|
|
433
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, ["acceptedPurposes", "assertionId", "assertor",
|
|
434
|
+
"confidence", "contextSha256", "provenanceActivitySha256", "reviewActivitySha256", "stance",
|
|
435
|
+
"state", "statementSha256", "v"]) || value.v !== 1) return null;
|
|
436
|
+
const acceptedPurposes = parseStringCodes(value.acceptedPurposes, 32);
|
|
437
|
+
const assertionId = parseKnowledgeAssertionId(value.assertionId);
|
|
438
|
+
const assertor = parseKnowledgeAgentRefV1(value.assertor);
|
|
439
|
+
const confidence = value.confidence === null ? null : parseKnowledgeSchemaRefV1(value.confidence);
|
|
440
|
+
const parsedConfidence = confidence === null ? null : confidence.ok ? confidence.value : null;
|
|
441
|
+
const contextSha256 = value.contextSha256 === null ? null : parseSha256Hex(value.contextSha256);
|
|
442
|
+
const provenanceActivitySha256 = parseSha256Hex(value.provenanceActivitySha256);
|
|
443
|
+
const reviewActivitySha256 = value.reviewActivitySha256 === null ? null : parseSha256Hex(value.reviewActivitySha256);
|
|
444
|
+
const stance = OH_KNOWLEDGE_ASSERTION_STANCES_V1.find((candidate) => candidate === value.stance);
|
|
445
|
+
const state = OH_KNOWLEDGE_ASSERTION_STATES_V1.find((candidate) => candidate === value.state);
|
|
446
|
+
const statementSha256 = parseSha256Hex(value.statementSha256);
|
|
447
|
+
if (acceptedPurposes === null || assertionId === null || assertor === null
|
|
448
|
+
|| (value.confidence !== null && parsedConfidence === null)
|
|
449
|
+
|| (value.contextSha256 !== null && contextSha256 === null)
|
|
450
|
+
|| provenanceActivitySha256 === null
|
|
451
|
+
|| (value.reviewActivitySha256 !== null && reviewActivitySha256 === null)
|
|
452
|
+
|| stance === undefined || state === undefined || statementSha256 === null) return null;
|
|
453
|
+
if (assertor.kind === "model" && (state !== "proposed" || acceptedPurposes.length !== 0 || reviewActivitySha256 !== null)) return null;
|
|
454
|
+
if ((state === "accepted-for-purpose") !== (acceptedPurposes.length > 0)
|
|
455
|
+
|| (state !== "proposed" && reviewActivitySha256 === null)) return null;
|
|
456
|
+
return { acceptedPurposes, assertionId, assertor, confidence: parsedConfidence, contextSha256,
|
|
457
|
+
provenanceActivitySha256, reviewActivitySha256, stance, state, statementSha256, v: 1 };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function createKnowledgeAssertionV1(input: KnowledgeAssertionInputV1): KnowledgeOntologyResult<KnowledgeAssertionV1> {
|
|
461
|
+
const parsed = parseAssertionInput(input);
|
|
462
|
+
return parsed === null ? failure("assertion") : success({ ...parsed, assertionSha256: canonicalSha256(parsed) });
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export function parseKnowledgeAssertionV1(value: unknown): KnowledgeOntologyResult<KnowledgeAssertionV1> {
|
|
466
|
+
if (!isPlainRecord(value) || !Object.hasOwn(value, "assertionSha256")) return failure("assertion");
|
|
467
|
+
const assertionSha256 = parseSha256Hex(value.assertionSha256);
|
|
468
|
+
const { assertionSha256: _digest, ...input } = value;
|
|
469
|
+
const parsed = parseAssertionInput(input);
|
|
470
|
+
return assertionSha256 !== null && parsed !== null && canonicalSha256(parsed) === assertionSha256
|
|
471
|
+
? success({ ...parsed, assertionSha256 }) : failure("assertionSha256", "digest-mismatch");
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export const OH_KNOWLEDGE_EVIDENCE_BEARINGS_V1 = [
|
|
475
|
+
"background", "contradicts", "corroborates", "direct-observation", "method",
|
|
476
|
+
"quotation", "registry-record", "supports",
|
|
477
|
+
] as const;
|
|
478
|
+
export type KnowledgeEvidenceBearingV1 = (typeof OH_KNOWLEDGE_EVIDENCE_BEARINGS_V1)[number];
|
|
479
|
+
export type KnowledgeEvidenceLinkV1 = Readonly<{
|
|
480
|
+
assertionSha256: Sha256Hex;
|
|
481
|
+
bearing: KnowledgeEvidenceBearingV1;
|
|
482
|
+
disclosure: "private" | "public" | "shared";
|
|
483
|
+
evidenceId: KnowledgeEvidenceId;
|
|
484
|
+
evidenceSha256: Sha256Hex;
|
|
485
|
+
observationSha256: Sha256Hex | null;
|
|
486
|
+
provenanceActivitySha256: Sha256Hex;
|
|
487
|
+
selector: string | null;
|
|
488
|
+
sourceEntityId: KnowledgeEntityId | null;
|
|
489
|
+
v: 1;
|
|
490
|
+
}>;
|
|
491
|
+
export type KnowledgeEvidenceLinkInputV1 = Omit<KnowledgeEvidenceLinkV1, "evidenceSha256">;
|
|
492
|
+
|
|
493
|
+
function parseEvidenceInput(value: unknown): KnowledgeEvidenceLinkInputV1 | null {
|
|
494
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, ["assertionSha256", "bearing", "disclosure",
|
|
495
|
+
"evidenceId", "observationSha256", "provenanceActivitySha256", "selector", "sourceEntityId", "v"])
|
|
496
|
+
|| value.v !== 1) return null;
|
|
497
|
+
const assertionSha256 = parseSha256Hex(value.assertionSha256);
|
|
498
|
+
const bearing = OH_KNOWLEDGE_EVIDENCE_BEARINGS_V1.find((candidate) => candidate === value.bearing);
|
|
499
|
+
const evidenceId = parseKnowledgeEvidenceId(value.evidenceId);
|
|
500
|
+
const observationSha256 = value.observationSha256 === null ? null : parseSha256Hex(value.observationSha256);
|
|
501
|
+
const provenanceActivitySha256 = parseSha256Hex(value.provenanceActivitySha256);
|
|
502
|
+
const selector = value.selector === null ? null : boundedText(value.selector, 8192);
|
|
503
|
+
const sourceEntityId = value.sourceEntityId === null ? null : parseKnowledgeEntityId(value.sourceEntityId);
|
|
504
|
+
return assertionSha256 !== null && bearing !== undefined
|
|
505
|
+
&& (value.disclosure === "private" || value.disclosure === "public" || value.disclosure === "shared")
|
|
506
|
+
&& evidenceId !== null && (value.observationSha256 === null || observationSha256 !== null)
|
|
507
|
+
&& provenanceActivitySha256 !== null && (value.selector === null || selector !== null)
|
|
508
|
+
&& (value.sourceEntityId === null || sourceEntityId !== null)
|
|
509
|
+
&& (observationSha256 !== null || sourceEntityId !== null)
|
|
510
|
+
? { assertionSha256, bearing, disclosure: value.disclosure, evidenceId, observationSha256,
|
|
511
|
+
provenanceActivitySha256, selector, sourceEntityId, v: 1 }
|
|
512
|
+
: null;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export function createKnowledgeEvidenceLinkV1(input: KnowledgeEvidenceLinkInputV1): KnowledgeOntologyResult<KnowledgeEvidenceLinkV1> {
|
|
516
|
+
const parsed = parseEvidenceInput(input);
|
|
517
|
+
return parsed === null ? failure("evidence") : success({ ...parsed, evidenceSha256: canonicalSha256(parsed) });
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export function parseKnowledgeEvidenceLinkV1(value: unknown): KnowledgeOntologyResult<KnowledgeEvidenceLinkV1> {
|
|
521
|
+
if (!isPlainRecord(value) || !Object.hasOwn(value, "evidenceSha256")) return failure("evidence");
|
|
522
|
+
const evidenceSha256 = parseSha256Hex(value.evidenceSha256);
|
|
523
|
+
const { evidenceSha256: _digest, ...input } = value;
|
|
524
|
+
const parsed = parseEvidenceInput(input);
|
|
525
|
+
return evidenceSha256 !== null && parsed !== null && canonicalSha256(parsed) === evidenceSha256
|
|
526
|
+
? success({ ...parsed, evidenceSha256 }) : failure("evidenceSha256", "digest-mismatch");
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
export type KnowledgeInquiryV1 = Readonly<{
|
|
530
|
+
answerForm: string;
|
|
531
|
+
authorEntityId: KnowledgeEntityId;
|
|
532
|
+
contextSha256: Sha256Hex | null;
|
|
533
|
+
createdAt: string;
|
|
534
|
+
inquiryId: KnowledgeInquiryId;
|
|
535
|
+
inquirySha256: Sha256Hex;
|
|
536
|
+
language: string;
|
|
537
|
+
parentInquiryIds: readonly KnowledgeInquiryId[];
|
|
538
|
+
privacy: "private" | "public" | "shared";
|
|
539
|
+
question: string;
|
|
540
|
+
status: "abandoned" | "open" | "paused" | "resolved";
|
|
541
|
+
v: 1;
|
|
542
|
+
}>;
|
|
543
|
+
export type KnowledgeInquiryInputV1 = Omit<KnowledgeInquiryV1, "inquirySha256">;
|
|
544
|
+
|
|
545
|
+
export function createKnowledgeInquiryV1(input: KnowledgeInquiryInputV1): KnowledgeOntologyResult<KnowledgeInquiryV1> {
|
|
546
|
+
const answerForm = safeCode(input.answerForm);
|
|
547
|
+
const authorEntityId = parseKnowledgeEntityId(input.authorEntityId);
|
|
548
|
+
const contextSha256 = input.contextSha256 === null ? null : parseSha256Hex(input.contextSha256);
|
|
549
|
+
const createdAt = parseCanonicalInstantV1(input.createdAt);
|
|
550
|
+
const inquiryId = parseKnowledgeInquiryId(input.inquiryId);
|
|
551
|
+
const language = typeof input.language === "string" && /^(?:und|[a-z]{2,3}(?:-[a-z0-9]{2,8})*)$/u.test(input.language)
|
|
552
|
+
? input.language : null;
|
|
553
|
+
const parents = Array.isArray(input.parentInquiryIds) ? input.parentInquiryIds.map(parseKnowledgeInquiryId) : null;
|
|
554
|
+
const question = boundedText(input.question, 16_384);
|
|
555
|
+
if (input.v !== 1 || answerForm === null || authorEntityId === null
|
|
556
|
+
|| (input.contextSha256 !== null && contextSha256 === null) || createdAt === null || inquiryId === null
|
|
557
|
+
|| language === null || parents === null || parents.some((item) => item === null)
|
|
558
|
+
|| !orderedUnique(parents as KnowledgeInquiryId[], String)
|
|
559
|
+
|| !["private", "public", "shared"].includes(input.privacy) || question === null
|
|
560
|
+
|| !["abandoned", "open", "paused", "resolved"].includes(input.status)) return failure("inquiry");
|
|
561
|
+
const payload = { answerForm, authorEntityId, contextSha256, createdAt, inquiryId, language,
|
|
562
|
+
parentInquiryIds: parents as KnowledgeInquiryId[], privacy: input.privacy, question, status: input.status, v: 1 as const };
|
|
563
|
+
return success({ ...payload, inquirySha256: canonicalSha256(payload) });
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
export function parseKnowledgeInquiryV1(value: unknown): KnowledgeOntologyResult<KnowledgeInquiryV1> {
|
|
567
|
+
if (!isPlainRecord(value) || !Object.hasOwn(value, "inquirySha256")) return failure("inquiry");
|
|
568
|
+
const digest = parseSha256Hex(value.inquirySha256);
|
|
569
|
+
const { inquirySha256: _digest, ...input } = value;
|
|
570
|
+
const created = createKnowledgeInquiryV1(input as unknown as KnowledgeInquiryInputV1);
|
|
571
|
+
return digest !== null && created.ok && created.value.inquirySha256 === digest
|
|
572
|
+
? success({ ...created.value, inquirySha256: digest }) : failure("inquirySha256", "digest-mismatch");
|
|
573
|
+
}
|
package/src/operation.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canonicalJson,
|
|
3
|
+
canonicalSha256,
|
|
4
|
+
hasExactKeys,
|
|
5
|
+
isPlainRecord,
|
|
6
|
+
parseCanonicalInstantV1,
|
|
7
|
+
parseSha256Hex,
|
|
8
|
+
safeCode,
|
|
9
|
+
type Sha256Hex,
|
|
10
|
+
} from "./canonical";
|
|
11
|
+
import { OH_CONTRACT_ID_V1 } from "./ontology";
|
|
12
|
+
import { canonicalKnowledgeGraphChangesV1, OH_GRAPH_LIMITS_V1, type KnowledgeGraphChangeV1 } from "./graph";
|
|
13
|
+
|
|
14
|
+
export { graphRevisionSha256V1 } from "./graph";
|
|
15
|
+
|
|
16
|
+
export const OH_OPERATION_MAX_BYTES_V1 = 64 * 1024 * 1024;
|
|
17
|
+
|
|
18
|
+
export type OhOperationPayloadV1 = Readonly<{
|
|
19
|
+
actorId: string;
|
|
20
|
+
changes: readonly KnowledgeGraphChangeV1[];
|
|
21
|
+
contractId: typeof OH_CONTRACT_ID_V1;
|
|
22
|
+
graphRevisionSha256: Sha256Hex;
|
|
23
|
+
instant: string;
|
|
24
|
+
operationId: string;
|
|
25
|
+
parentOperationSha256: Sha256Hex | null;
|
|
26
|
+
recordsSha256: Sha256Hex;
|
|
27
|
+
sequence: number;
|
|
28
|
+
spaceId: string;
|
|
29
|
+
v: 1;
|
|
30
|
+
}>;
|
|
31
|
+
|
|
32
|
+
export type OhOperationV1 = OhOperationPayloadV1 & Readonly<{ operationSha256: Sha256Hex }>;
|
|
33
|
+
|
|
34
|
+
function parsePayload(value: unknown): OhOperationPayloadV1 | null {
|
|
35
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, ["actorId", "changes", "contractId",
|
|
36
|
+
"graphRevisionSha256", "instant", "operationId", "parentOperationSha256", "recordsSha256",
|
|
37
|
+
"sequence", "spaceId", "v"]) || value.v !== 1 || value.contractId !== OH_CONTRACT_ID_V1
|
|
38
|
+
|| !Array.isArray(value.changes)) return null;
|
|
39
|
+
const actorId = safeCode(value.actorId);
|
|
40
|
+
const operationId = safeCode(value.operationId);
|
|
41
|
+
const spaceId = safeCode(value.spaceId);
|
|
42
|
+
const graphRevisionSha256 = parseSha256Hex(value.graphRevisionSha256);
|
|
43
|
+
const parentOperationSha256 = value.parentOperationSha256 === null
|
|
44
|
+
? null : parseSha256Hex(value.parentOperationSha256);
|
|
45
|
+
const recordsSha256 = parseSha256Hex(value.recordsSha256);
|
|
46
|
+
const instant = parseCanonicalInstantV1(value.instant);
|
|
47
|
+
const sequence = Number.isSafeInteger(value.sequence) && (value.sequence as number) > 0
|
|
48
|
+
? value.sequence as number : null;
|
|
49
|
+
let changes: readonly KnowledgeGraphChangeV1[];
|
|
50
|
+
try { changes = canonicalKnowledgeGraphChangesV1(value.changes as KnowledgeGraphChangeV1[]); } catch { return null; }
|
|
51
|
+
if (changes.length === 0 || changes.length > OH_GRAPH_LIMITS_V1.changesPerOperation) return null;
|
|
52
|
+
return actorId !== null && operationId !== null && spaceId !== null
|
|
53
|
+
&& graphRevisionSha256 !== null && recordsSha256 !== null && instant !== null && sequence !== null
|
|
54
|
+
&& (value.parentOperationSha256 === null || parentOperationSha256 !== null)
|
|
55
|
+
&& ((sequence === 1) === (parentOperationSha256 === null))
|
|
56
|
+
? { actorId, changes, contractId: OH_CONTRACT_ID_V1, graphRevisionSha256, instant,
|
|
57
|
+
operationId, parentOperationSha256, recordsSha256, sequence, spaceId, v: 1 }
|
|
58
|
+
: null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function createOhOperationV1(input: OhOperationPayloadV1): OhOperationV1 {
|
|
62
|
+
const payload = parsePayload(input);
|
|
63
|
+
if (payload === null) throw new TypeError("Invalid Oh operation payload.");
|
|
64
|
+
const operation = { ...payload, operationSha256: canonicalSha256(payload) };
|
|
65
|
+
if (Buffer.byteLength(canonicalJson(operation), "utf8") > OH_OPERATION_MAX_BYTES_V1) {
|
|
66
|
+
throw new RangeError("Oh operation exceeds its canonical byte limit.");
|
|
67
|
+
}
|
|
68
|
+
return operation;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function parseOhOperationV1(value: unknown): OhOperationV1 | null {
|
|
72
|
+
if (!isPlainRecord(value) || !Object.hasOwn(value, "operationSha256")) return null;
|
|
73
|
+
const operationSha256 = parseSha256Hex(value.operationSha256);
|
|
74
|
+
const { operationSha256: _digest, ...input } = value;
|
|
75
|
+
const payload = parsePayload(input);
|
|
76
|
+
return operationSha256 !== null && payload !== null
|
|
77
|
+
&& Buffer.byteLength(canonicalJson({ ...payload, operationSha256 }), "utf8") <= OH_OPERATION_MAX_BYTES_V1
|
|
78
|
+
&& canonicalSha256(payload) === operationSha256
|
|
79
|
+
? { ...payload, operationSha256 } : null;
|
|
80
|
+
}
|