@hraness/wordcell 0.20.0 → 0.21.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.
Files changed (81) hide show
  1. package/README.md +29 -15
  2. package/dist/agent-context.js +1 -0
  3. package/dist/agent-guide-audit.js +1 -0
  4. package/dist/attachments.js +1 -0
  5. package/dist/authoring.js +1 -0
  6. package/dist/authority-4sp9d552.js +663 -0
  7. package/dist/benchmark.js +1 -0
  8. package/dist/capture.js +1 -0
  9. package/dist/cli.js +3781 -32
  10. package/dist/clip/acquire.js +1 -0
  11. package/dist/clip/args.js +1 -0
  12. package/dist/clip/bounded-byte-buffer.js +1 -0
  13. package/dist/clip/browser-profiles.js +1 -0
  14. package/dist/clip/bundle-reader.js +1 -0
  15. package/dist/clip/cli.js +1 -0
  16. package/dist/clip/cookies.js +1 -0
  17. package/dist/clip/doctor.js +1 -0
  18. package/dist/clip/jobs.js +1 -0
  19. package/dist/clip/network-proxy.js +1 -0
  20. package/dist/clip/network.js +1 -0
  21. package/dist/clip/persist.js +1 -0
  22. package/dist/clip/refresh.js +1 -0
  23. package/dist/clip/terminal.js +1 -0
  24. package/dist/evaluation-builder.js +13 -5
  25. package/dist/evaluation-kb.js +11 -4
  26. package/dist/evaluation.js +1 -0
  27. package/dist/git.js +1 -0
  28. package/dist/graph-authority.js +29 -0
  29. package/dist/graph-percolation.js +16 -0
  30. package/dist/graph.js +1 -0
  31. package/dist/index-0k2x4nn9.js +393 -0
  32. package/dist/{index-n05s3wsb.js → index-115b07ap.js} +156 -541
  33. package/dist/index-11621h23.js +160 -0
  34. package/dist/{index-tkpxp48t.js → index-51bap0mq.js} +2 -2
  35. package/dist/{index-8a08v6x8.js → index-59ssmqqm.js} +1 -1
  36. package/dist/index-bcfn9xah.js +472 -0
  37. package/dist/index-bgfzwt4h.js +51 -0
  38. package/dist/{index-bs0yf4pp.js → index-pbr6rv47.js} +2 -2
  39. package/dist/{index-m7et4rz4.js → index-vdw5yjfa.js} +61 -9
  40. package/dist/index-z1w83f81.js +4 -0
  41. package/dist/index.js +61 -34
  42. package/dist/navigation.js +1 -0
  43. package/dist/pdf.js +1 -0
  44. package/dist/percolate.js +1 -0
  45. package/dist/portfolio.js +11 -4
  46. package/dist/query.js +1 -0
  47. package/dist/repository-memory.js +1 -0
  48. package/dist/sdk.js +10 -3
  49. package/dist/search-rules.js +1 -0
  50. package/dist/search.js +1 -0
  51. package/dist/semantic.js +3 -1
  52. package/dist/source-inbox.js +1 -0
  53. package/dist/untrusted-content.js +1 -0
  54. package/dist/url-intelligence.js +1 -0
  55. package/dist/workflow.js +1 -0
  56. package/dist/workflows/decision-context.js +11 -4
  57. package/dist/workflows/explain-change.js +1 -0
  58. package/dist/workflows/index.js +11 -4
  59. package/dist/workflows/plan-radar.js +1 -0
  60. package/package.json +24 -6
  61. package/skills/wordcell/SKILL.md +2 -2
  62. package/skills/wordcell/references/percolate.md +4 -0
  63. package/skills/wordcell/references/query.md +6 -0
  64. package/src/cli-program.ts +45 -2
  65. package/src/graph-authority-model.ts +116 -0
  66. package/src/graph-authority.ts +219 -0
  67. package/src/graph-cache-filesystem.ts +4 -0
  68. package/src/graph-cli.ts +96 -0
  69. package/src/graph-facts.ts +206 -0
  70. package/src/graph-percolation.ts +39 -0
  71. package/src/graph-query.ts +122 -0
  72. package/src/index.ts +2 -0
  73. package/src/oh/authority.ts +193 -0
  74. package/src/oh/programs.ts +99 -0
  75. package/src/oh/schema.ts +40 -0
  76. package/src/oh/snapshot.ts +114 -0
  77. package/src/oh/validation.ts +61 -0
  78. package/src/sdk.ts +51 -6
  79. package/dist/index-sy0zety7.js +0 -3630
  80. package/dist/kb-alias.js +0 -55
  81. package/src/kb-alias.ts +0 -14
@@ -0,0 +1,193 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { isProxy } from "node:util/types";
3
+ import { canonicalJson, canonicalSha256, type KnowledgeGraphChangeV1, type KnowledgeGraphRecordV1 } from "@hraness/oh";
4
+ import {
5
+ createOhProjectionDatasetV1, createOhProjectionFactV1, createOhProjectionSnapshotV1,
6
+ evaluateOhProjectionV1, parseOhProjectionResultV1, type OhProjectionProofV1,
7
+ } from "@hraness/oh/projection";
8
+ import { createOhSqliteStoreAuthorityV1 } from "@hraness/oh/sqlite";
9
+ import { createOhStoreProfileV1, OH_WORKING_STORE_PROFILE_V1, type OhStoreV1 } from "@hraness/oh/store";
10
+ import {
11
+ GRAPH_LIMITS, GraphAuthorityError, type GraphAuthority, type GraphProof,
12
+ type GraphQueryLimits, type GraphQueryRequest, type GraphQueryResult, type GraphSnapshot,
13
+ } from "../graph-authority-model";
14
+ import { compileGraphProgram, normalizeGraphQueryRequest } from "./programs";
15
+ import { GRAPH_EXTRACTOR_SHA256, GRAPH_MANIFEST_KEY, manifestGraphRecord, parseGraphSnapshot, sourceGraphRecord } from "./snapshot";
16
+ import { deepFreeze, detachedData, exactKeys } from "./validation";
17
+ import { validateOhCacheSchema } from "./schema";
18
+
19
+ export type OhGraphAdapterOptions = Readonly<{
20
+ databasePath?: string;
21
+ databaseBytes?: Uint8Array;
22
+ writable?: boolean;
23
+ }>;
24
+ const MAXIMUM_CACHE_OPERATIONS = 1_024;
25
+
26
+ function cacheError(error: unknown): GraphAuthorityError {
27
+ return error instanceof GraphAuthorityError ? error : new GraphAuthorityError("corrupt-cache", "The graph cache failed Oh integrity verification; rebuild it from Markdown.");
28
+ }
29
+
30
+ /** Paths identify coordinator-owned staging files. Read-only verification accepts bytes only. */
31
+ export async function openOhGraphAdapter(input: GraphSnapshot, options: OhGraphAdapterOptions = {}): Promise<GraphAuthority> {
32
+ const snapshot = parseGraphSnapshot(input);
33
+ if (options === null || typeof options !== "object" || isProxy(options)
34
+ || (Object.getPrototypeOf(options) !== Object.prototype && Object.getPrototypeOf(options) !== null)
35
+ || Reflect.ownKeys(options).some(key => typeof key !== "string" || !Object.getOwnPropertyDescriptor(options, key)?.enumerable
36
+ || !("value" in Object.getOwnPropertyDescriptor(options, key)!))) throw new GraphAuthorityError("invalid-input", "Graph adapter options must be plain data fields.");
37
+ exactKeys(options as Record<string, unknown>, [], ["databasePath", "databaseBytes", "writable"]);
38
+ if ((options.databasePath !== undefined && options.databaseBytes !== undefined)
39
+ || (options.databasePath !== undefined && (typeof options.databasePath !== "string" || options.databasePath.length === 0 || options.writable !== true))
40
+ || (options.databaseBytes !== undefined && (!(options.databaseBytes instanceof Uint8Array) || options.databaseBytes.byteLength < 1 || options.databaseBytes.byteLength > GRAPH_LIMITS.sourceBytes || options.writable === true))
41
+ || (options.writable !== undefined && typeof options.writable !== "boolean")) throw new GraphAuthorityError("invalid-input", "Use writable staging paths or read-only cache bytes, never both.");
42
+ const readonly = options.databaseBytes !== undefined;
43
+ let records: KnowledgeGraphRecordV1[], expected: KnowledgeGraphRecordV1[];
44
+ try {
45
+ records = snapshot.records.map(sourceGraphRecord);
46
+ expected = [...records, manifestGraphRecord(snapshot)].sort((a, b) => a.key < b.key ? -1 : 1);
47
+ } catch (error) {
48
+ if (error instanceof RangeError) throw new GraphAuthorityError("budget", "A graph source record exceeds the Oh record byte limit.");
49
+ throw new GraphAuthorityError("invalid-input", "Graph source records are not valid Oh records.");
50
+ }
51
+ const profile = createOhStoreProfileV1({
52
+ applicationProfileSha256: canonicalSha256({ vaultIdentity: snapshot.vaultIdentity, extractorSha256: GRAPH_EXTRACTOR_SHA256 }),
53
+ capabilities: OH_WORKING_STORE_PROFILE_V1.capabilities,
54
+ profileId: "wordcell.graph.working.v1", profileKind: "working", v: 1,
55
+ });
56
+ const spaceId = "wordcell.graph";
57
+ let database: Database | undefined;
58
+ let store: OhStoreV1 | undefined;
59
+ let closed = false;
60
+ try {
61
+ database = readonly
62
+ ? Database.deserialize(new Uint8Array(options.databaseBytes!), { strict: true })
63
+ : new Database(options.databasePath ?? ":memory:", { create: true, strict: true });
64
+ database.exec("PRAGMA trusted_schema = OFF");
65
+ const hasSchema = validateOhCacheSchema(database);
66
+ if (hasSchema) {
67
+ const spaces = database.query<{ space_id: string }, []>("SELECT space_id FROM oh_spaces LIMIT 2").all();
68
+ const bindings = database.query<{ binding_json: string }, []>("SELECT binding_json FROM oh_space_bindings LIMIT 2").all();
69
+ if (spaces.length !== 1 || spaces[0]?.space_id !== spaceId || bindings.length !== 1) throw new GraphAuthorityError("corrupt-cache", "Graph cache does not contain one bound Wordcell space.");
70
+ const binding = JSON.parse(bindings[0]!.binding_json) as { profile?: { profileSha256?: unknown } };
71
+ if (binding.profile?.profileSha256 !== profile.profileSha256) throw new GraphAuthorityError("stale", "Graph cache belongs to a different vault or extractor profile.");
72
+ const count = database.query<{ count: number }, []>(`SELECT count(*) AS count FROM (SELECT 1 FROM oh_operations LIMIT ${MAXIMUM_CACHE_OPERATIONS + 1})`).get()?.count;
73
+ if (!Number.isSafeInteger(count) || count! > MAXIMUM_CACHE_OPERATIONS) throw new GraphAuthorityError("budget", "Graph cache history exceeds its replay bound; run graph rebuild --fresh.");
74
+ } else if (readonly) throw new GraphAuthorityError("corrupt-cache", "Graph cache has no Oh authority schema.");
75
+ database.exec("PRAGMA foreign_keys = ON; PRAGMA journal_mode = DELETE; PRAGMA synchronous = FULL; PRAGMA busy_timeout = 5000");
76
+ store = createOhSqliteStoreAuthorityV1({ database, profile, realmId: `wordcell:${snapshot.vaultIdentity}`, spaceId }).store;
77
+ await store.verify();
78
+ const current = await store.snapshot({ maximumRecords: GRAPH_LIMITS.notes + 1 });
79
+ if (canonicalJson(current.records) !== canonicalJson(expected)) {
80
+ if (readonly) throw new GraphAuthorityError("stale", "Graph cache does not match the current Markdown snapshot.");
81
+ if (current.head.sequence >= MAXIMUM_CACHE_OPERATIONS) throw new GraphAuthorityError("budget", "Graph cache history is full; run graph rebuild --fresh.");
82
+ if (current.records.some(record => record.key !== GRAPH_MANIFEST_KEY && !/^edition:note\/[0-9a-f]{64}$/u.test(record.key))) throw new GraphAuthorityError("corrupt-cache", "Graph cache contains foreign records.");
83
+ const prior = new Map(current.records.map(record => [record.key, record]));
84
+ const next = new Map(expected.map(record => [record.key, record]));
85
+ const changes: KnowledgeGraphChangeV1[] = [];
86
+ for (const record of expected) if (prior.get(record.key)?.recordSha256 !== record.recordSha256) changes.push({ kind: "put", record, v: 1 });
87
+ for (const record of current.records) if (!next.has(record.key)) changes.push({ kind: "tombstone", key: record.key, priorSha256: record.recordSha256, v: 1 });
88
+ await store.commit({ actorId: "wordcell.graph", changes, expectedHead: current.head,
89
+ // Disposable projections use a fixed logical genesis instant, never wall-clock evidence.
90
+ // Persisted authorities retain real timestamps and their distinct operation history.
91
+ ...(options.databasePath === undefined ? { instant: "1970-01-01T00:00:00.000Z" } : {}),
92
+ operationId: `graph:${snapshot.revision}:${current.head.sequence}`, maximumOperationBytes: GRAPH_LIMITS.sourceBytes });
93
+ }
94
+ const selectedStore = store;
95
+ const materialized = await selectedStore.snapshot({ maximumRecords: GRAPH_LIMITS.notes + 1 });
96
+ if (canonicalJson(materialized.records) !== canonicalJson(expected)) throw new GraphAuthorityError("corrupt-cache", "Graph reconciliation did not retain exact source records.");
97
+ const projectionSnapshot = createOhProjectionSnapshotV1({ ...materialized, spaceId });
98
+ const recordMap = new Map(records.map((record, index) => [record.key, { record, source: snapshot.records[index]! }]));
99
+ const assertOpen = () => { if (closed) throw new GraphAuthorityError("closed", "The graph authority is closed."); };
100
+ const assertCurrent = async () => {
101
+ assertOpen();
102
+ if (canonicalJson(await selectedStore.head()) !== canonicalJson(materialized.head)) throw new GraphAuthorityError("stale", "Graph authority head changed after opening.");
103
+ };
104
+ function mapProof(proof: OhProjectionProofV1, budget: { remaining: number }): GraphProof {
105
+ const reserve = (value: unknown) => {
106
+ budget.remaining -= Buffer.byteLength(canonicalJson(value)) + 16;
107
+ if (budget.remaining < 0) throw new GraphAuthorityError("budget", "Graph proof source labels exceed the outward result byte limit.");
108
+ };
109
+ if (proof.kind === "truncated") {
110
+ const result = { kind: proof.kind, relation: proof.relation, tuple: proof.tuple, reason: proof.reason };
111
+ reserve(result); return result;
112
+ }
113
+ if (proof.kind === "derived") {
114
+ const result = { kind: proof.kind, relation: proof.relation, tuple: proof.tuple,
115
+ ruleId: proof.ruleId, ruleSha256: proof.ruleSha256, premisesTruncated: proof.premisesTruncated };
116
+ reserve(result);
117
+ return { ...result, premises: proof.premises.map(premise => mapProof(premise, budget)) };
118
+ }
119
+ const result = { kind: "fact" as const, relation: proof.relation, tuple: proof.tuple, sources: proof.sources.map(reference => {
120
+ const bound = recordMap.get(reference.key);
121
+ if (bound === undefined || bound.record.recordSha256 !== reference.recordSha256) throw new GraphAuthorityError("corrupt-cache", "Projection proof contains an unbound source record.");
122
+ return { key: reference.key, recordSha256: reference.recordSha256, noteId: bound.source.id,
123
+ path: bound.source.path, contentSha256: bound.source.contentSha256 };
124
+ }) };
125
+ reserve(result); return result;
126
+ }
127
+ async function query(value: GraphQueryRequest): Promise<GraphQueryResult> {
128
+ assertOpen();
129
+ const request = normalizeGraphQueryRequest(value);
130
+ await assertCurrent();
131
+ const limits = request.limits as Required<GraphQueryLimits>;
132
+ const program = compileGraphProgram(request);
133
+ const facts = snapshot.records.flatMap((source, index) => {
134
+ const record = records[index]!;
135
+ const sources = [{ key: record.key, recordSha256: record.recordSha256, v: 1 as const }];
136
+ const items = source.facts.map(fact => createOhProjectionFactV1({ ...fact, sources }));
137
+ if (request.program !== "scope-route" && source.id === request.note) items.push(createOhProjectionFactV1({ relation: "wordcell.selected", tuple: [source.id], sources }));
138
+ if ((request.program === "shared-tags" || request.program === "shared-concepts") && source.id !== request.note) items.push(createOhProjectionFactV1({ relation: "wordcell.peer", tuple: [source.id], sources }));
139
+ if (request.program === "shared-concepts" && !source.facts.some(fact => fact.relation === "wordcell.concept")) items.push(createOhProjectionFactV1({ relation: "wordcell.ordinary", tuple: [source.id], sources }));
140
+ return items;
141
+ });
142
+ try {
143
+ const dataset = createOhProjectionDatasetV1({ extractorSha256: GRAPH_EXTRACTOR_SHA256, factPackId: "wordcell.graph-facts", factPackRevision: 1, facts, snapshot: projectionSnapshot });
144
+ const result = evaluateOhProjectionV1({ ...program, dataset, snapshot: projectionSnapshot, options: {
145
+ maximumDerivedTuples: limits.derivedTuples, maximumRounds: limits.rounds, maximumWorkUnits: limits.workUnits,
146
+ maximumProofDepth: limits.proofDepth, maximumProofNodes: limits.proofNodes, maximumTotalProofNodes: limits.totalProofNodes, maximumResultBytes: limits.resultBytes,
147
+ } });
148
+ if (parseOhProjectionResultV1(result, result.identity.projectionSha256) === null) throw new GraphAuthorityError("corrupt-cache", "Oh returned an invalid bounded projection result.");
149
+ await assertCurrent();
150
+ const proofBudget = { remaining: limits.resultBytes };
151
+ const output: GraphQueryResult = { schemaVersion: 1, authority: "derived", vaultIdentity: snapshot.vaultIdentity,
152
+ revision: snapshot.revision, projectionSha256: result.identity.projectionSha256,
153
+ request, columns: program.columns, rows: result.rows.map(row => ({ values: row.values,
154
+ proofs: row.proofs.map(proof => mapProof(proof, proofBudget)), proofsTruncated: row.proofsTruncated, supportCount: row.supportCount })),
155
+ limits, truncated: result.stats.truncated, proofsTruncated: result.stats.proofsTruncated,
156
+ truncationReasons: result.stats.truncationReasons, stats: { baseFacts: result.stats.baseFacts,
157
+ derivedFacts: result.stats.derivedFacts, workUnits: result.stats.workUnits, rounds: result.stats.rounds,
158
+ proofNodes: result.stats.proofNodes, queryMatches: result.stats.queryMatches } };
159
+ if (Buffer.byteLength(canonicalJson(output)) > limits.resultBytes) throw new GraphAuthorityError("budget", "Graph result with source labels exceeds the outward result byte limit.");
160
+ return deepFreeze(output);
161
+ } catch (error) {
162
+ if (error instanceof GraphAuthorityError) throw error;
163
+ if (error instanceof RangeError) throw new GraphAuthorityError("budget", error.message);
164
+ throw error;
165
+ }
166
+ }
167
+ return Object.freeze({
168
+ query,
169
+ verifyResult: async (value: unknown) => {
170
+ assertOpen();
171
+ try {
172
+ const result = detachedData(value, GRAPH_LIMITS.resultBytes * 4) as GraphQueryResult;
173
+ if (result === null || typeof result !== "object" || Array.isArray(result)
174
+ || result.vaultIdentity !== snapshot.vaultIdentity || result.revision !== snapshot.revision) return false;
175
+ const verified = await query(result.request);
176
+ return canonicalJson(result) === canonicalJson(verified);
177
+ } catch { return false; }
178
+ },
179
+ verify: async () => {
180
+ await assertCurrent();
181
+ await selectedStore.verify();
182
+ const actual = await selectedStore.snapshot({ maximumRecords: GRAPH_LIMITS.notes + 1 });
183
+ if (canonicalJson(actual.records) !== canonicalJson(expected)) throw new GraphAuthorityError("corrupt-cache", "Graph cache differs from its exact snapshot.");
184
+ return deepFreeze({ schemaVersion: 1 as const, status: "verified" as const, vaultIdentity: snapshot.vaultIdentity,
185
+ revision: snapshot.revision, records: snapshot.records.length, facts: snapshot.factCount });
186
+ },
187
+ close: async () => { if (!closed) { closed = true; await selectedStore.close(); } },
188
+ });
189
+ } catch (error) {
190
+ try { if (store !== undefined) await store.close(); else database?.close(); } catch { /* preserve original failure */ }
191
+ throw cacheError(error);
192
+ }
193
+ }
@@ -0,0 +1,99 @@
1
+ import {
2
+ createOhProjectionLiteralV1 as literal,
3
+ createOhProjectionQueryV1,
4
+ createOhProjectionRulePackV1,
5
+ createOhProjectionRuleV1,
6
+ ohProjectionConstantV1 as constant,
7
+ ohProjectionVariableV1 as variable,
8
+ type OhProjectionLiteralV1,
9
+ type OhProjectionRuleV1,
10
+ type OhProjectionTermV1,
11
+ } from "@hraness/oh/projection";
12
+ import { type GraphQueryLimits, type GraphQueryRequest } from "../graph-authority-model";
13
+ import { validateGraphQueryRequest } from "../graph-query";
14
+ import { detachedData } from "./validation";
15
+
16
+ export const DEFAULT_GRAPH_QUERY_LIMITS: Required<GraphQueryLimits> = Object.freeze({
17
+ rows: 100, workUnits: 1_000_000, derivedTuples: 50_000, rounds: 32,
18
+ proofDepth: 32, proofNodes: 256, totalProofNodes: 8_192, resultBytes: 1_048_576,
19
+ });
20
+
21
+ /** Validate before scanning a vault or opening any storage. Returned requests include all defaults. */
22
+ export function normalizeGraphQueryRequest(input: unknown): GraphQueryRequest {
23
+ const request = validateGraphQueryRequest(detachedData(input, 64 * 1024));
24
+ const depth = request.program === "reachability" || request.program === "relation-closure"
25
+ ? { depth: request.depth ?? 3 } : {};
26
+ return Object.freeze({ ...request, ...depth,
27
+ limits: Object.freeze({ ...DEFAULT_GRAPH_QUERY_LIMITS, ...request.limits }) });
28
+ }
29
+
30
+ const atom = (relation: string, ...terms: OhProjectionTermV1[]) => literal({ relation, terms });
31
+
32
+ export function compileGraphProgram(request: GraphQueryRequest) {
33
+ const rules: OhProjectionRuleV1[] = [];
34
+ const add = (ruleId: string, head: OhProjectionLiteralV1, ...body: OhProjectionLiteralV1[]) => {
35
+ rules.push(createOhProjectionRuleV1({ ruleId: `wordcell.${ruleId}`, head, body }));
36
+ };
37
+ const a = variable("source"), b = variable("target"), line = variable("line"), predicate = variable("predicate");
38
+ let columns: string[];
39
+ let where: OhProjectionLiteralV1[];
40
+ switch (request.program) {
41
+ case "backlinks": {
42
+ add("backlink.link", atom("wordcell.answer", a, b, line, constant("link"), constant("")), atom("wordcell.link", a, b, line));
43
+ add("backlink.relation", atom("wordcell.answer", a, b, line, constant("relation"), predicate), atom("wordcell.relation", a, b, predicate, line));
44
+ columns = ["source", "target", "line", "kind", "predicate"];
45
+ where = [atom("wordcell.answer", a, b, line, variable("kind"), predicate), atom("wordcell.selected", b)];
46
+ break;
47
+ }
48
+ case "reachability":
49
+ case "relation-closure": {
50
+ const relation = request.program === "relation-closure";
51
+ const edge = "wordcell.edge";
52
+ if (relation) add("edge.relation", atom(edge, a, b), atom("wordcell.relation", a, b, constant(request.predicate), line));
53
+ else {
54
+ add("edge.link", atom(edge, a, b), atom("wordcell.link", a, b, line));
55
+ add("edge.relation", atom(edge, a, b), atom("wordcell.relation", a, b, predicate, line));
56
+ }
57
+ const source = constant(request.note);
58
+ // Positive walks of exact lengths 1..depth. Cycles may return the starting note,
59
+ // and a destination reached at different lengths produces one row per length.
60
+ add("walk.1", atom("wordcell.walk.1", b), atom(edge, source, b));
61
+ for (let depth = 1; depth <= (request.depth ?? 3); depth += 1) {
62
+ if (depth > 1) add(`walk.${depth}`, atom(`wordcell.walk.${depth}`, b), atom(`wordcell.walk.${depth - 1}`, a), atom(edge, a, b));
63
+ add(`walk.answer.${depth}`, atom("wordcell.answer", source, b, constant(depth)), atom(`wordcell.walk.${depth}`, b));
64
+ }
65
+ columns = ["source", "target", "depth"];
66
+ where = [atom("wordcell.answer", a, b, variable("depth"))];
67
+ break;
68
+ }
69
+ case "scope-route": {
70
+ add("scope", atom("wordcell.answer", a, constant(request.scope)), atom("wordcell.scope", a, constant(request.scope)));
71
+ columns = ["note", "scope"];
72
+ where = [atom("wordcell.answer", variable("note"), variable("scope"))];
73
+ break;
74
+ }
75
+ case "shared-tags": {
76
+ const note = variable("note"), other = variable("other"), tag = variable("tag");
77
+ add("shared.tags", atom("wordcell.answer", note, other, tag), atom("wordcell.selected", note), atom("wordcell.tag", note, tag), atom("wordcell.tag", other, tag), atom("wordcell.peer", other));
78
+ columns = ["note", "other", "tag"];
79
+ where = [atom("wordcell.answer", note, other, tag)];
80
+ break;
81
+ }
82
+ case "shared-concepts": {
83
+ const note = variable("note"), other = variable("other"), concept = variable("concept");
84
+ add("concept.link", atom("wordcell.concept-edge", a, b), atom("wordcell.link", a, b, line), atom("wordcell.concept", b));
85
+ add("concept.relation", atom("wordcell.concept-edge", a, b), atom("wordcell.relation", a, b, predicate, line), atom("wordcell.concept", b));
86
+ add("concept.reverse-link", atom("wordcell.concept-edge", a, b), atom("wordcell.link", b, a, line), atom("wordcell.concept", b));
87
+ add("concept.reverse-relation", atom("wordcell.concept-edge", a, b), atom("wordcell.relation", b, a, predicate, line), atom("wordcell.concept", b));
88
+ add("shared.concepts", atom("wordcell.answer", note, other, concept), atom("wordcell.selected", note), atom("wordcell.ordinary", note), atom("wordcell.concept-edge", note, concept), atom("wordcell.concept-edge", other, concept), atom("wordcell.peer", other), atom("wordcell.ordinary", other));
89
+ columns = ["note", "other", "concept"];
90
+ where = [atom("wordcell.answer", note, other, concept)];
91
+ break;
92
+ }
93
+ }
94
+ return {
95
+ columns,
96
+ query: createOhProjectionQueryV1({ queryId: `wordcell.${request.program}`, find: columns, where, limit: request.limits?.rows ?? DEFAULT_GRAPH_QUERY_LIMITS.rows }),
97
+ rulePack: createOhProjectionRulePackV1({ rulePackId: `wordcell.${request.program}`, rulePackRevision: 1, rules }),
98
+ };
99
+ }
@@ -0,0 +1,40 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { canonicalJson } from "@hraness/oh";
3
+ import { applyOhSqliteMigrations } from "@hraness/oh/sqlite";
4
+ import { GraphAuthorityError } from "../graph-authority-model";
5
+
6
+ type SchemaEntry = Readonly<{ type: string; name: string; tbl_name: string; sql: string | null;
7
+ nameLength: number; tableNameLength: number; sqlLength: number | null }>;
8
+ const MAXIMUM_SCHEMA_ENTRIES = 256;
9
+ const MAXIMUM_SCHEMA_SQL_LENGTH = 16_384;
10
+ let expectedSchema: string | undefined;
11
+
12
+ function schemaEntries(database: Database): SchemaEntry[] {
13
+ // Only SQLite's own catalog is read before its named objects are authenticated.
14
+ // Bound both the row count and text copied out of a potentially hostile cache.
15
+ const entries = database.query<SchemaEntry, []>(`SELECT type,
16
+ substr(name, 1, 257) AS name, length(name) AS nameLength,
17
+ substr(tbl_name, 1, 257) AS tbl_name, length(tbl_name) AS tableNameLength,
18
+ substr(sql, 1, ${MAXIMUM_SCHEMA_SQL_LENGTH + 1}) AS sql, length(sql) AS sqlLength
19
+ FROM main.sqlite_schema LIMIT ${MAXIMUM_SCHEMA_ENTRIES + 1}`).all();
20
+ if (entries.length > MAXIMUM_SCHEMA_ENTRIES || entries.some(entry => entry.nameLength > 256
21
+ || entry.tableNameLength > 256 || (entry.sqlLength ?? 0) > MAXIMUM_SCHEMA_SQL_LENGTH)) {
22
+ throw new GraphAuthorityError("corrupt-cache", "Graph cache schema exceeds its metadata bounds.");
23
+ }
24
+ return entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : a.type < b.type ? -1 : 1);
25
+ }
26
+
27
+ /** Reject altered tables, virtual tables, indexes, foreign views and triggers before any data query or migration. */
28
+ export function validateOhCacheSchema(database: Database): boolean {
29
+ const entries = schemaEntries(database);
30
+ if (entries.length === 0) return false;
31
+ if (expectedSchema === undefined) {
32
+ const trusted = new Database(":memory:", { strict: true });
33
+ try {
34
+ applyOhSqliteMigrations(trusted);
35
+ expectedSchema = canonicalJson(schemaEntries(trusted));
36
+ } finally { trusted.close(); }
37
+ }
38
+ if (canonicalJson(entries) !== expectedSchema) throw new GraphAuthorityError("corrupt-cache", "Graph cache schema differs from the pinned Oh schema; rebuild it from Markdown.");
39
+ return true;
40
+ }
@@ -0,0 +1,114 @@
1
+ import { createHash } from "node:crypto";
2
+ import { canonicalJson, canonicalSha256, createKnowledgeGraphRecordV1, type JsonValue } from "@hraness/oh";
3
+ import { GRAPH_LIMITS, GraphAuthorityError, type GraphSnapshot, type GraphSourceRecord } from "../graph-authority-model";
4
+ import { isCanonicalNoteId, isCanonicalRelationPredicate } from "../graph";
5
+ import { validateRepositoryScopeSelection } from "../repository-memory";
6
+ import { detachedData, exactKeys, deepFreeze } from "./validation";
7
+
8
+ const digest = (value: unknown): value is string => typeof value === "string" && /^[0-9a-f]{64}$/u.test(value);
9
+ const fail = (message: string): never => { throw new GraphAuthorityError("invalid-input", message); };
10
+ const nonempty = (value: unknown): value is string => typeof value === "string" && value.length > 0 && Buffer.byteLength(value) <= GRAPH_LIMITS.atomBytes;
11
+ const path = (value: unknown): value is string => nonempty(value) && !value.startsWith("/") && !value.includes("\\") && !value.split("/").some(part => part === "" || part === "." || part === "..");
12
+
13
+ export const GRAPH_EXTRACTOR_PROFILE = Object.freeze({
14
+ schemaVersion: 1,
15
+ id: "wordcell.graph-facts.v1",
16
+ relations: { note: ["id", "path", "title", "type"], link: ["source", "target", "line"],
17
+ relation: ["source", "target", "predicate", "line"], "external-relation": ["source", "uri", "predicate", "line"],
18
+ tag: ["id", "tag"], scope: ["id", "scope"], concept: ["id"] },
19
+ selectors: "selected is the requested note; peers are all other notes; ordinary excludes type concept; scope is an exact constant",
20
+ semantics: "positive walks of lengths 1..depth; shared concepts connect ordinary notes to concepts through explicit links or typed relations in either direction",
21
+ });
22
+ export const GRAPH_EXTRACTOR_SHA256 = canonicalSha256(GRAPH_EXTRACTOR_PROFILE);
23
+ export const GRAPH_MANIFEST_KEY = "view:wordcell/manifest";
24
+
25
+ export function parseGraphSnapshot(input: unknown): GraphSnapshot {
26
+ const snapshot = detachedData(input, GRAPH_LIMITS.sourceBytes) as GraphSnapshot;
27
+ if (snapshot === null || typeof snapshot !== "object" || Array.isArray(snapshot)) fail("Invalid graph snapshot.");
28
+ exactKeys(snapshot as unknown as Record<string, unknown>, ["schemaVersion", "vaultIdentity", "revision", "catalogNoteId", "sourceDigests", "records", "factCount"]);
29
+ if (snapshot.schemaVersion !== 1 || !digest(snapshot.vaultIdentity) || !digest(snapshot.revision) || !nonempty(snapshot.catalogNoteId)
30
+ || !Array.isArray(snapshot.records) || snapshot.records.length > GRAPH_LIMITS.notes || !Array.isArray(snapshot.sourceDigests)
31
+ || snapshot.sourceDigests.length > GRAPH_LIMITS.notes + 1 || !Number.isSafeInteger(snapshot.factCount)
32
+ || snapshot.factCount < 0 || snapshot.factCount > GRAPH_LIMITS.facts) fail("Invalid graph snapshot identity or bounds.");
33
+ const sourceDigests = new Map<string, string>();
34
+ let lastPath = "";
35
+ for (const source of snapshot.sourceDigests) {
36
+ if (source === null || typeof source !== "object" || Array.isArray(source)) fail("Invalid graph source digest.");
37
+ exactKeys(source as unknown as Record<string, unknown>, ["path", "contentSha256"]);
38
+ if (!path(source.path) || source.path <= lastPath || !digest(source.contentSha256)) fail("Graph source digests must be sorted unique paths and hashes.");
39
+ sourceDigests.set(source.path, source.contentSha256); lastPath = source.path;
40
+ }
41
+ const ids = new Set<string>(), paths = new Set<string>();
42
+ let lastKey = "", count = 0;
43
+ for (const record of snapshot.records) {
44
+ if (record === null || typeof record !== "object" || Array.isArray(record)) fail("Invalid graph source record.");
45
+ exactKeys(record as unknown as Record<string, unknown>, ["key", "id", "path", "documentId", "contentSha256", "facts"]);
46
+ if (!nonempty(record.id) || !isCanonicalNoteId(record.id) || record.path !== `${record.id}.md` || !path(record.path) || !digest(record.contentSha256)
47
+ || (record.documentId !== null && !nonempty(record.documentId)) || !Array.isArray(record.facts)
48
+ || ids.has(record.id) || paths.has(record.path) || record.id === snapshot.catalogNoteId) fail("Invalid or duplicated graph source identity.");
49
+ const identity = record.documentId === null ? `path:${record.id}` : `document:${record.documentId}`;
50
+ const expectedKey = `edition:note/${createHash("sha256").update(identity).digest("hex")}`;
51
+ if (record.key !== expectedKey || record.key <= lastKey || sourceDigests.get(record.path) !== record.contentSha256) fail("Graph source key, order or digest differs.");
52
+ lastKey = record.key; ids.add(record.id); paths.add(record.path);
53
+ let lastFact = "", noteFacts = 0;
54
+ for (const fact of record.facts) {
55
+ if (fact === null || typeof fact !== "object" || Array.isArray(fact)) fail("Invalid graph fact.");
56
+ exactKeys(fact as unknown as Record<string, unknown>, ["relation", "tuple"]);
57
+ if (!Array.isArray(fact.tuple) || fact.tuple[0] !== record.id) fail("Graph facts must belong to their source note.");
58
+ const key = canonicalJson([fact.relation, fact.tuple]);
59
+ if (key <= lastFact) fail("Graph facts must be sorted and unique.");
60
+ lastFact = key;
61
+ for (const value of fact.tuple) if (Buffer.byteLength(canonicalJson(value)) > GRAPH_LIMITS.atomBytes) fail("Graph atom exceeds its byte bound.");
62
+ const tuple = fact.tuple;
63
+ switch (fact.relation) {
64
+ case "wordcell.note":
65
+ if (tuple.length !== 4 || tuple[1] !== record.path || typeof tuple[2] !== "string" || typeof tuple[3] !== "string") fail("Invalid note fact.");
66
+ noteFacts += 1; break;
67
+ case "wordcell.link":
68
+ if (tuple.length !== 3 || !nonempty(tuple[1]) || !Number.isSafeInteger(tuple[2]) || (tuple[2] as number) < 0) fail("Invalid link fact.");
69
+ break;
70
+ case "wordcell.relation":
71
+ case "wordcell.external-relation":
72
+ if (tuple.length !== 4 || !nonempty(tuple[1]) || !nonempty(tuple[2]) || !Number.isSafeInteger(tuple[3]) || (tuple[3] as number) < 0) fail("Invalid relation fact.");
73
+ if (!isCanonicalRelationPredicate(tuple[2] as string)) fail("Invalid relation predicate.");
74
+ if (fact.relation === "wordcell.external-relation" && !(tuple[1] as string).startsWith("kb://")) fail("Invalid external relation target.");
75
+ break;
76
+ case "wordcell.tag":
77
+ case "wordcell.scope":
78
+ if (tuple.length !== 2 || !nonempty(tuple[1])) fail("Invalid tag or scope fact.");
79
+ if (fact.relation === "wordcell.scope") {
80
+ try { validateRepositoryScopeSelection([tuple[1] as string]); } catch { fail("Invalid repository scope fact."); }
81
+ }
82
+ break;
83
+ case "wordcell.concept":
84
+ if (tuple.length !== 1) fail("Invalid concept fact.");
85
+ break;
86
+ default: fail("Unknown graph fact relation.");
87
+ }
88
+ count += 1;
89
+ if (count > GRAPH_LIMITS.facts) fail("Graph fact count exceeds its limit.");
90
+ }
91
+ if (noteFacts !== 1) fail("Every graph record must contain exactly one note fact.");
92
+ const type = record.facts.find(fact => fact.relation === "wordcell.note")!.tuple[3];
93
+ if (record.facts.some(fact => fact.relation === "wordcell.concept") !== ((type as string).normalize("NFC").toLocaleLowerCase("en-US") === "concept")) fail("Concept facts must match authored note type.");
94
+ }
95
+ for (const record of snapshot.records) for (const fact of record.facts) {
96
+ if ((fact.relation === "wordcell.link" || fact.relation === "wordcell.relation") && !ids.has(fact.tuple[1] as string)) fail("Internal graph fact target is absent.");
97
+ }
98
+ if ([...sourceDigests.keys()].some(source => !paths.has(source) && source !== `${snapshot.catalogNoteId}.md`)) fail("Graph source inventory contains an unrepresented non-catalog note.");
99
+ if (count !== snapshot.factCount || canonicalSha256({ schemaVersion: 1, vaultIdentity: snapshot.vaultIdentity,
100
+ catalogNoteId: snapshot.catalogNoteId, sourceDigests: snapshot.sourceDigests, records: snapshot.records }) !== snapshot.revision) fail("Graph snapshot revision or fact count differs.");
101
+ return deepFreeze(snapshot);
102
+ }
103
+
104
+ export function sourceGraphRecord(record: GraphSourceRecord) {
105
+ return createKnowledgeGraphRecordV1({ key: record.key, kind: "edition", dependencies: [], v: 1,
106
+ value: JSON.parse(canonicalJson(record)) as JsonValue });
107
+ }
108
+
109
+ export function manifestGraphRecord(snapshot: GraphSnapshot) {
110
+ return createKnowledgeGraphRecordV1({ key: GRAPH_MANIFEST_KEY, kind: "view", dependencies: [], v: 1,
111
+ value: JSON.parse(canonicalJson({ schemaVersion: 1, vaultIdentity: snapshot.vaultIdentity, revision: snapshot.revision,
112
+ catalogNoteId: snapshot.catalogNoteId, sourceDigests: snapshot.sourceDigests,
113
+ records: snapshot.records.length, facts: snapshot.factCount, extractorSha256: GRAPH_EXTRACTOR_SHA256 })) as JsonValue });
114
+ }
@@ -0,0 +1,61 @@
1
+ import { isProxy } from "node:util/types";
2
+ import { canonicalJson } from "@hraness/oh";
3
+ import { GraphAuthorityError } from "../graph-authority-model";
4
+
5
+ /** Detach foreign data without invoking getters, prototypes, proxies or toJSON. */
6
+ export function detachedData(input: unknown, maximumBytes: number): unknown {
7
+ const seen = new Set<object>();
8
+ let nodes = 0, bytes = 0;
9
+ function visit(value: unknown, depth: number): unknown {
10
+ nodes += 1;
11
+ if (nodes > 1_000_000 || depth > 192) throw new GraphAuthorityError("budget", "Graph input exceeds structural bounds.");
12
+ if (value === null || typeof value === "boolean") { bytes += 5; return value; }
13
+ if (typeof value === "string") {
14
+ bytes += Buffer.byteLength(value) + 2;
15
+ if (bytes > maximumBytes) throw new GraphAuthorityError("budget", "Graph input exceeds its byte bound.");
16
+ return value;
17
+ }
18
+ if (typeof value === "number" && Number.isFinite(value)) { bytes += 32; return value; }
19
+ if (typeof value !== "object" || value === null || isProxy(value) || seen.has(value)) throw new GraphAuthorityError("invalid-input", "Graph input must be acyclic plain data.");
20
+ seen.add(value);
21
+ const array = Array.isArray(value);
22
+ if (!array && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) throw new GraphAuthorityError("invalid-input", "Graph input must contain plain objects.");
23
+ const keys = Reflect.ownKeys(value);
24
+ if (keys.length > 200_000 || keys.some(key => typeof key !== "string")) throw new GraphAuthorityError("budget", "Graph input has too many fields.");
25
+ const output: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
26
+ if (array && keys.length !== value.length + 1) throw new GraphAuthorityError("invalid-input", "Graph arrays must be dense.");
27
+ for (const key of keys as string[]) {
28
+ if (array && key === "length") continue;
29
+ if (array && (!/^(0|[1-9][0-9]*)$/u.test(key) || Number(key) >= value.length)) throw new GraphAuthorityError("invalid-input", "Graph array has extra fields.");
30
+ const descriptor = Object.getOwnPropertyDescriptor(value, key)!;
31
+ if (!descriptor.enumerable || !("value" in descriptor)) throw new GraphAuthorityError("invalid-input", "Graph input accessors are forbidden.");
32
+ bytes += Buffer.byteLength(key) + 4;
33
+ if (bytes > maximumBytes) throw new GraphAuthorityError("budget", "Graph input exceeds its byte bound.");
34
+ output[key] = visit(descriptor.value, depth + 1);
35
+ }
36
+ seen.delete(value);
37
+ return array ? Array.from({ length: (value as unknown[]).length }, (_, index) => output[String(index)]) : output;
38
+ }
39
+ const value = visit(input, 0);
40
+ try {
41
+ const encoded = canonicalJson(value);
42
+ if (Buffer.byteLength(encoded) > maximumBytes) throw new GraphAuthorityError("budget", "Graph input exceeds its canonical byte bound.");
43
+ return JSON.parse(encoded) as unknown;
44
+ } catch (error) {
45
+ if (error instanceof GraphAuthorityError) throw error;
46
+ throw new GraphAuthorityError("invalid-input", "Graph input is not canonical JSON data.");
47
+ }
48
+ }
49
+
50
+ export function exactKeys(value: Record<string, unknown>, required: readonly string[], optional: readonly string[] = []): void {
51
+ const keys = Object.keys(value);
52
+ if (required.some(key => !keys.includes(key)) || keys.some(key => !required.includes(key) && !optional.includes(key))) throw new GraphAuthorityError("invalid-input", "Graph input has missing or unexpected fields.");
53
+ }
54
+
55
+ export function deepFreeze<T>(value: T): T {
56
+ if (value !== null && typeof value === "object") {
57
+ for (const item of Object.values(value)) deepFreeze(item);
58
+ Object.freeze(value);
59
+ }
60
+ return value;
61
+ }
package/src/sdk.ts CHANGED
@@ -2,6 +2,11 @@ import { resolve } from "node:path";
2
2
 
3
3
  import type { MetadataObject, Note } from "./graph.js";
4
4
  import { lookupNote } from "./graph.js";
5
+ import { openGraphAuthority } from "./graph-authority.js";
6
+ import type { GraphAuthority, GraphQueryRequest, GraphQueryResult } from "./graph-authority-model.js";
7
+ import { validateGraphPercolationOptions, validateGraphQueryRequest } from "./graph-query.js";
8
+ import { percolateWithGraph, type GraphPercolationResult } from "./graph-percolation.js";
9
+ import type { PercolateOptions } from "./percolate.js";
5
10
  import {
6
11
  GitHistoryError,
7
12
  gitHistoryForNotes,
@@ -229,6 +234,9 @@ export type OpenKnowledgeBaseOptions = {
229
234
  };
230
235
 
231
236
  export type KnowledgeBaseDependencies = {
237
+ /** Read-only, in-memory graph authority over this session's single vault scan. */
238
+ readonly openGraphAuthority?: typeof openGraphAuthority;
239
+ readonly percolateWithGraph?: typeof percolateWithGraph;
232
240
  readonly scanVault?: typeof scanVault;
233
241
  readonly semantic?: SemanticDependencies;
234
242
  readonly openSemanticSearchSession?: typeof openSemanticSearchSession;
@@ -250,6 +258,12 @@ export type KnowledgeBaseSession = {
250
258
  note: string,
251
259
  options?: Omit<NavigateLinksOptions, "direction">,
252
260
  ) => LinkNeighborhood;
261
+ /** Bounded named graph programs, evaluated against this session's original snapshot. */
262
+ readonly graphQuery: (request: GraphQueryRequest) => Promise<GraphQueryResult>;
263
+ /** Verify evidence against the same snapshot and named program; never accepts a foreign revision. */
264
+ readonly graphVerifyResult: (value: unknown) => Promise<boolean>;
265
+ /** Source-scoped advisory suggestions with separate positive graph proofs. */
266
+ readonly percolateWithProofs: (options: PercolateOptions & Readonly<{ note: string }>) => Promise<GraphPercolationResult>;
253
267
  readonly search: (options: KnowledgeBaseSearchOptions) => Promise<KnowledgeBaseSearchResult>;
254
268
  readonly history: (
255
269
  noteIds: readonly string[],
@@ -524,10 +538,23 @@ export async function openKnowledgeBase(
524
538
  ? undefined
525
539
  : Promise.resolve(injectedSemantic);
526
540
  let gitPromise: Promise<GitHistoryIndexResult> | undefined;
541
+ let graphAuthorityPromise: Promise<GraphAuthority> | undefined;
542
+ let graphOperationTail: Promise<void> = Promise.resolve();
527
543
 
528
544
  const assertOpen = (): void => {
529
545
  if (closeRequested) throw new Error("Knowledge-base session is closed.");
530
546
  };
547
+ const withGraphOperation = <T>(operation: () => Promise<T>): Promise<T> => {
548
+ assertOpen();
549
+ const pending = graphOperationTail.then(operation);
550
+ graphOperationTail = pending.then(() => undefined, () => undefined);
551
+ return pending;
552
+ };
553
+ const withGraphAuthority = <T>(operation: (authority: GraphAuthority) => Promise<T>): Promise<T> =>
554
+ withGraphOperation(async () => {
555
+ graphAuthorityPromise ??= (dependencies.openGraphAuthority ?? openGraphAuthority)(snapshot);
556
+ return operation(await graphAuthorityPromise);
557
+ });
531
558
  const semantic = (): Promise<SemanticSearchSession> => {
532
559
  assertOpen();
533
560
  semanticPromise ??= (dependencies.openSemanticSearchSession
@@ -980,6 +1007,17 @@ export async function openKnowledgeBase(
980
1007
  ...linkOptions,
981
1008
  direction: "in",
982
1009
  }),
1010
+ graphQuery: async (request) => {
1011
+ assertOpen();
1012
+ const checked = validateGraphQueryRequest(request);
1013
+ return withGraphAuthority((authority) => authority.query(checked));
1014
+ },
1015
+ graphVerifyResult: async (value) => withGraphAuthority((authority) => authority.verifyResult(value)),
1016
+ percolateWithProofs: async (percolateOptions) => {
1017
+ assertOpen();
1018
+ const checked = validateGraphPercolationOptions(percolateOptions);
1019
+ return withGraphOperation(() => (dependencies.percolateWithGraph ?? percolateWithGraph)(snapshot, checked));
1020
+ },
983
1021
  search,
984
1022
  history: async (noteIds, historyOptions = {}) => {
985
1023
  assertOpen();
@@ -1000,12 +1038,19 @@ export async function openKnowledgeBase(
1000
1038
  close: () => {
1001
1039
  if (closePromise !== undefined) return closePromise;
1002
1040
  closeRequested = true;
1003
- closePromise = semanticPromise === undefined
1004
- ? Promise.resolve()
1005
- : semanticPromise.then(
1006
- (session) => session.close(),
1007
- () => undefined,
1008
- );
1041
+ closePromise = (async () => {
1042
+ const semanticClose = semanticPromise === undefined
1043
+ ? Promise.resolve()
1044
+ : semanticPromise.then((session) => session.close(), () => undefined);
1045
+ const graphClose = graphOperationTail.then(async () => {
1046
+ await graphAuthorityPromise?.then((authority) => authority.close(), () => undefined);
1047
+ });
1048
+ const results = await Promise.allSettled([semanticClose, graphClose]);
1049
+ const errors = results.filter((result): result is PromiseRejectedResult => result.status === "rejected")
1050
+ .map((result) => result.reason as unknown);
1051
+ if (errors.length === 1) throw errors[0];
1052
+ if (errors.length > 1) throw new AggregateError(errors, "Knowledge-base stores could not close.");
1053
+ })();
1009
1054
  return closePromise;
1010
1055
  },
1011
1056
  };