@unson/brainbase-mcp 0.7.0 → 0.9.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 +1 -1
- package/contracts/foundation/graph-history.sql +634 -0
- package/contracts/judgment-dag/digest.json +2 -2
- package/contracts/judgment-value-proof/schema.json +43 -1
- package/dist/canonical-task-service.js +16 -5
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +107 -0
- package/dist/company-os-objectives.d.ts +11 -0
- package/dist/company-os-objectives.js +84 -47
- package/dist/foundation-authenticated-tools.d.ts +56 -0
- package/dist/foundation-authenticated-tools.js +347 -0
- package/dist/foundation-graph-http.d.ts +43 -0
- package/dist/foundation-graph-http.js +243 -0
- package/dist/foundation-graph-write.d.ts +90 -0
- package/dist/foundation-graph-write.js +447 -0
- package/dist/foundation-public-provider.d.ts +140 -0
- package/dist/foundation-public-provider.js +192 -0
- package/dist/graph-corrections.d.ts +157 -0
- package/dist/graph-corrections.js +620 -0
- package/dist/graph-foundation-reader.d.ts +60 -0
- package/dist/graph-foundation-reader.js +494 -0
- package/dist/graph-retrieval.d.ts +6 -1
- package/dist/graph-revision-reader.d.ts +80 -0
- package/dist/graph-revision-reader.js +246 -0
- package/dist/graph-web-http.d.ts +26 -0
- package/dist/graph-web-http.js +215 -0
- package/dist/graph-web.d.ts +364 -0
- package/dist/graph-web.js +639 -0
- package/dist/judgment-problem-snapshot.d.ts +30 -2
- package/dist/judgment-problem-snapshot.js +302 -6
- package/dist/judgment-value-proof-review.d.ts +166 -0
- package/dist/judgment-value-proof-review.js +437 -0
- package/dist/judgment-value-proof.d.ts +28 -1
- package/dist/judgment-value-proof.js +53 -8
- package/dist/knowledge-continuation.d.ts +127 -0
- package/dist/knowledge-continuation.js +446 -0
- package/dist/knowledge-lookup.d.ts +178 -0
- package/dist/knowledge-lookup.js +758 -0
- package/dist/local-web-fetch-bridge.d.ts +5 -0
- package/dist/local-web-fetch-bridge.js +37 -0
- package/dist/local-web-host.d.ts +109 -0
- package/dist/local-web-host.js +688 -0
- package/dist/local-web-security.d.ts +25 -0
- package/dist/local-web-security.js +95 -0
- package/dist/organization-graph.d.ts +15 -0
- package/dist/organization-graph.js +24 -4
- package/dist/personal-knowledge.d.ts +12 -0
- package/dist/personal-knowledge.js +29 -0
- package/dist/personal-memory-handover.d.ts +108 -0
- package/dist/personal-memory-handover.js +369 -0
- package/dist/philosophy-revision-reader.d.ts +85 -0
- package/dist/philosophy-revision-reader.js +241 -0
- package/dist/portable-graph.d.ts +24 -0
- package/dist/portable-graph.js +77 -0
- package/dist/server.d.ts +7 -2
- package/dist/server.js +9 -6
- package/dist/value-proof-review-http.d.ts +25 -0
- package/dist/value-proof-review-http.js +185 -0
- package/dist/world-model.d.ts +21 -3
- package/dist/world-model.js +94 -12
- package/package.json +94 -6
- package/ui/brainbase-tokens.css +56 -0
- package/ui/graph-own-share.js +367 -0
- package/ui/graph-projects-view.css +23 -0
- package/ui/graph-projects-view.js +730 -0
- package/ui/graph-registry-view.css +101 -0
- package/ui/graph-registry-view.js +804 -0
- package/ui/graph-view-shared.css +522 -0
- package/ui/graph-view-shared.js +1227 -0
- package/ui/judgment-view.css +119 -45
- package/ui/local-web-shell.css +396 -0
- package/ui/local-web-shell.js +378 -0
- package/ui/objective-editor-http-port.js +154 -0
- package/ui/objective-editor.css +180 -74
- package/ui/objective-editor.js +490 -43
- package/ui/outcome-knowledge.css +50 -14
- package/ui/outcome-knowledge.js +15 -1
- package/ui/value-proof-review.css +504 -0
- package/ui/value-proof-review.js +1104 -0
- package/ui/workspace-kit.css +485 -0
- package/ui/workspace-kit.js +229 -0
- package/ui/world-model-view.css +188 -0
- package/ui/world-model-view.js +486 -0
- package/ui/icons/mana/LICENSE.tabler.txt +0 -21
- package/ui/icons/mana/affiliate.svg +0 -20
- package/ui/icons/mana/brand-github.svg +0 -15
- package/ui/icons/mana/brand-google-drive.svg +0 -17
- package/ui/icons/mana/brand-slack.svg +0 -18
- package/ui/icons/mana/cpu.svg +0 -24
- package/ui/outcome-mana.css +0 -255
- package/ui/outcome-mana.js +0 -2325
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { access, readFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { canonicalEdgeId, isActiveAt, validateCanonicalGraph } from './canonical-graph.js';
|
|
5
|
+
import { stableHash } from './import-extract.js';
|
|
6
|
+
import { canonicalPortableJson } from './portable-graph.js';
|
|
7
|
+
import { getCanonicalRelation } from './relation-registry.js';
|
|
8
|
+
import { loadPersonalOs, mutatePersonalOsWithSidecar, readPersonalOsSidecar } from './ssot.js';
|
|
9
|
+
/**
|
|
10
|
+
* Owner corrections for the canonical local Graph v2.
|
|
11
|
+
*
|
|
12
|
+
* Graph entities and edges are stored without revisions, so every correction
|
|
13
|
+
* carries the digest of the record it was based on, a one-sentence reason and
|
|
14
|
+
* leaves an append-only history line. The history line, the Graph change and
|
|
15
|
+
* the relationships.json projection are committed in one SSOT transaction.
|
|
16
|
+
*/
|
|
17
|
+
export const GRAPH_CORRECTION_SCHEMA = 'brainbase-graph-correction.v1';
|
|
18
|
+
/** Relative to the data directory. Committed as a sidecar of the canonical SSOT transaction. */
|
|
19
|
+
export const GRAPH_CORRECTIONS_FILE = 'evidence/graph-corrections.jsonl';
|
|
20
|
+
export const GRAPH_CORRECTION_ENTITY_FIELDS = ['name', 'aliases', 'summary', 'validFrom', 'validTo'];
|
|
21
|
+
export const GRAPH_CORRECTION_PROJECT_FIELDS = ['goal', 'status'];
|
|
22
|
+
export const GRAPH_CORRECTION_EDGE_FIELDS = ['role', 'context', 'validTo'];
|
|
23
|
+
export const GRAPH_CORRECTION_NEW_EDGE_RELATIONS = ['participates_in', 'accountable_for', 'member_of'];
|
|
24
|
+
/** MCP tools that read the same graph.json on every call and therefore see a saved correction next time. */
|
|
25
|
+
export const GRAPH_CORRECTION_APPLIES_TO = ['search', 'get_context', 'resolve_entity'];
|
|
26
|
+
const MAX_REASON_LENGTH = 500;
|
|
27
|
+
const MAX_NAME_LENGTH = 200;
|
|
28
|
+
const MAX_TEXT_LENGTH = 2000;
|
|
29
|
+
const MAX_ALIASES = 50;
|
|
30
|
+
const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/u;
|
|
31
|
+
const PROJECTED_RELATIONS = new Set(['participates_in', 'accountable_for']);
|
|
32
|
+
export class GraphCorrectionError extends Error {
|
|
33
|
+
kind;
|
|
34
|
+
code;
|
|
35
|
+
current;
|
|
36
|
+
name = 'GraphCorrectionError';
|
|
37
|
+
constructor(kind, code, message, current) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.kind = kind;
|
|
40
|
+
this.code = code;
|
|
41
|
+
this.current = current;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** SHA-256 over canonical JSON (recursively sorted keys) of one stored record. */
|
|
45
|
+
export function graphRecordDigest(record) {
|
|
46
|
+
return `sha256:${createHash('sha256').update(canonicalPortableJson(record), 'utf8').digest('hex')}`;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Validates an untrusted correction request. Only the fields listed in the
|
|
50
|
+
* correction contract are accepted; ids, types, relation types, provenance and
|
|
51
|
+
* deletions are rejected with a specific code.
|
|
52
|
+
*/
|
|
53
|
+
export function parseGraphCorrectionInput(value) {
|
|
54
|
+
if (!isRecord(value))
|
|
55
|
+
invalid('correction_invalid', 'Correction must be a JSON object');
|
|
56
|
+
const kind = value.kind;
|
|
57
|
+
if (typeof kind === 'string' && /(^|_)(delete|remove)(_|$)/u.test(kind)) {
|
|
58
|
+
invalid('correction_delete_not_allowed', 'Graph records are never deleted; set validTo to end an entity or relation');
|
|
59
|
+
}
|
|
60
|
+
if (kind === 'update_entity') {
|
|
61
|
+
assertOnlyKeys(value, ['kind', 'entityId', 'expectedDigest', 'reason', 'changes'], '');
|
|
62
|
+
return {
|
|
63
|
+
kind,
|
|
64
|
+
entityId: requireId(value.entityId, 'entityId'),
|
|
65
|
+
expectedDigest: requireDigest(value.expectedDigest),
|
|
66
|
+
reason: requireReason(value.reason),
|
|
67
|
+
changes: parseEntityChanges(value.changes)
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (kind === 'update_edge') {
|
|
71
|
+
assertOnlyKeys(value, ['kind', 'edgeId', 'expectedDigest', 'reason', 'changes'], '');
|
|
72
|
+
return {
|
|
73
|
+
kind,
|
|
74
|
+
edgeId: requireId(value.edgeId, 'edgeId'),
|
|
75
|
+
expectedDigest: requireDigest(value.expectedDigest),
|
|
76
|
+
reason: requireReason(value.reason),
|
|
77
|
+
changes: parseEdgeChanges(value.changes)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (kind === 'create_edge') {
|
|
81
|
+
assertOnlyKeys(value, ['kind', 'reason', 'edge'], '');
|
|
82
|
+
return { kind, reason: requireReason(value.reason), edge: parseEdgeCreation(value.edge) };
|
|
83
|
+
}
|
|
84
|
+
invalid('correction_kind_unsupported', 'kind must be update_entity, update_edge or create_edge');
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Applies one owner correction to the canonical Graph v2 in `dataDir`.
|
|
88
|
+
*
|
|
89
|
+
* The Graph write, the relationships.json projection and the history line in
|
|
90
|
+
* `evidence/graph-corrections.jsonl` are committed together through
|
|
91
|
+
* `mutatePersonalOsWithSidecar`, so a crash never leaves a Graph change without
|
|
92
|
+
* its history line (or the reverse). After the commit the record is read back
|
|
93
|
+
* and its digest must equal the digest computed before saving.
|
|
94
|
+
*/
|
|
95
|
+
export async function applyGraphCorrection(dataDir, input, options = {}) {
|
|
96
|
+
if (typeof dataDir !== 'string' || dataDir.trim() === '')
|
|
97
|
+
throw new TypeError('dataDir is required');
|
|
98
|
+
const correction = parseGraphCorrectionInput(input);
|
|
99
|
+
const at = (options.now ?? new Date()).toISOString();
|
|
100
|
+
const present = await Promise.all(['graph.json', 'relationships.json', 'personal-kg.jsonl', 'decisions.jsonl']
|
|
101
|
+
.map((file) => fileExists(join(dataDir, file))));
|
|
102
|
+
if (!present.some(Boolean)) {
|
|
103
|
+
// Checked before taking the SSOT lock so a correction never creates the data directory.
|
|
104
|
+
throw new GraphCorrectionError('not_found', 'graph_not_initialized', 'No local Graph exists yet; start with brainbase onboard:start');
|
|
105
|
+
}
|
|
106
|
+
const outcome = await mutatePersonalOsWithSidecar(dataDir, GRAPH_CORRECTIONS_FILE, (current, existingHistory) => {
|
|
107
|
+
if (current.graph.version !== 2) {
|
|
108
|
+
throw new GraphCorrectionError('migration_required', 'migration_required', 'Graph v1 cannot be corrected; migrate graph.json to Graph v2 with brainbase ontology:migrate first');
|
|
109
|
+
}
|
|
110
|
+
const planned = planCorrection(current.graph, current.relationships.relationships, correction, at);
|
|
111
|
+
const history = appendJsonl(existingHistory, planned.record);
|
|
112
|
+
return {
|
|
113
|
+
next: { ...current, graph: planned.graph, relationships: { version: 1, relationships: planned.relationships } },
|
|
114
|
+
sidecarContent: history,
|
|
115
|
+
result: planned
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
const reloaded = await loadPersonalOs(dataDir);
|
|
119
|
+
if (reloaded.graph.version !== 2) {
|
|
120
|
+
throw new GraphCorrectionError('readback_mismatch', 'readback_mismatch', 'graph.json is no longer Graph v2 after the correction');
|
|
121
|
+
}
|
|
122
|
+
const stored = outcome.recordType === 'entity'
|
|
123
|
+
? reloaded.graph.entities.find((entity) => entity.id === outcome.record.target.id)
|
|
124
|
+
: reloaded.graph.edges.find((edge) => edge.id === outcome.record.target.id);
|
|
125
|
+
if (!stored || graphRecordDigest(stored) !== outcome.record.afterDigest) {
|
|
126
|
+
throw new GraphCorrectionError('readback_mismatch', 'readback_mismatch', 'The saved record does not match the corrected record');
|
|
127
|
+
}
|
|
128
|
+
const historyText = await readPersonalOsSidecar(dataDir, GRAPH_CORRECTIONS_FILE);
|
|
129
|
+
const recorded = (historyText ?? '').split('\n').some((line) => {
|
|
130
|
+
if (!line.trim())
|
|
131
|
+
return false;
|
|
132
|
+
try {
|
|
133
|
+
return JSON.parse(line).id === outcome.record.id;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
if (!recorded) {
|
|
140
|
+
throw new GraphCorrectionError('readback_mismatch', 'readback_mismatch', 'The correction history line was not found after saving');
|
|
141
|
+
}
|
|
142
|
+
const digest = graphRecordDigest(stored);
|
|
143
|
+
return {
|
|
144
|
+
status: 'saved',
|
|
145
|
+
correction: outcome.record,
|
|
146
|
+
recordType: outcome.recordType,
|
|
147
|
+
record: stored,
|
|
148
|
+
digest,
|
|
149
|
+
readback: { verified: true, digest, historyRecorded: true },
|
|
150
|
+
appliesTo: GRAPH_CORRECTION_APPLIES_TO
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Reads the correction history in append order. A missing file means no
|
|
155
|
+
* correction has been saved; an unreadable line is reported as an issue and
|
|
156
|
+
* never silently dropped.
|
|
157
|
+
*/
|
|
158
|
+
export async function readGraphCorrectionHistory(dataDir) {
|
|
159
|
+
let text;
|
|
160
|
+
try {
|
|
161
|
+
text = await readFile(join(dataDir, GRAPH_CORRECTIONS_FILE), 'utf8');
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
if (error.code === 'ENOENT')
|
|
165
|
+
return { records: [], issues: [] };
|
|
166
|
+
return { records: [], issues: [{ file: GRAPH_CORRECTIONS_FILE, reason: error instanceof Error ? error.message : String(error) }] };
|
|
167
|
+
}
|
|
168
|
+
const records = [];
|
|
169
|
+
const issues = [];
|
|
170
|
+
text.split('\n').forEach((line, index) => {
|
|
171
|
+
if (!line.trim())
|
|
172
|
+
return;
|
|
173
|
+
try {
|
|
174
|
+
const value = JSON.parse(line);
|
|
175
|
+
if (!isGraphCorrectionRecord(value))
|
|
176
|
+
throw new Error('not a graph correction record');
|
|
177
|
+
records.push(value);
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
issues.push({ file: GRAPH_CORRECTIONS_FILE, line: index + 1, reason: error instanceof Error ? error.message : String(error) });
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
return { records, issues };
|
|
184
|
+
}
|
|
185
|
+
function isGraphCorrectionRecord(value) {
|
|
186
|
+
if (!isRecord(value) || value.schema !== GRAPH_CORRECTION_SCHEMA)
|
|
187
|
+
return false;
|
|
188
|
+
const target = value.target;
|
|
189
|
+
return typeof value.id === 'string'
|
|
190
|
+
&& typeof value.at === 'string'
|
|
191
|
+
&& (value.kind === 'update_entity' || value.kind === 'update_edge' || value.kind === 'create_edge')
|
|
192
|
+
&& isRecord(target) && (target.recordType === 'entity' || target.recordType === 'edge') && typeof target.id === 'string'
|
|
193
|
+
&& typeof value.reason === 'string'
|
|
194
|
+
&& Array.isArray(value.changedFields) && value.changedFields.every((field) => typeof field === 'string')
|
|
195
|
+
&& isRecord(value.changes)
|
|
196
|
+
&& (value.beforeDigest === null || typeof value.beforeDigest === 'string')
|
|
197
|
+
&& typeof value.afterDigest === 'string'
|
|
198
|
+
&& Array.isArray(value.projections);
|
|
199
|
+
}
|
|
200
|
+
function planCorrection(graph, relationships, correction, at) {
|
|
201
|
+
if (correction.kind === 'update_entity')
|
|
202
|
+
return planEntityUpdate(graph, relationships, correction, at);
|
|
203
|
+
if (correction.kind === 'update_edge')
|
|
204
|
+
return planEdgeUpdate(graph, relationships, correction, at);
|
|
205
|
+
return planEdgeCreation(graph, relationships, correction, at);
|
|
206
|
+
}
|
|
207
|
+
function planEntityUpdate(graph, relationships, correction, at) {
|
|
208
|
+
const matches = graph.entities.filter((entity) => entity.id === correction.entityId);
|
|
209
|
+
if (matches.length === 0)
|
|
210
|
+
notFound('entity_not_found', `No entity ${correction.entityId}`);
|
|
211
|
+
if (matches.length > 1)
|
|
212
|
+
invalid('entity_id_ambiguous', `Entity id ${correction.entityId} is duplicated in graph.json; repair it with the CLI first`);
|
|
213
|
+
const before = matches[0];
|
|
214
|
+
assertExpectedDigest('entity', before, correction.expectedDigest);
|
|
215
|
+
const changes = correction.changes;
|
|
216
|
+
if ((changes.goal !== undefined || changes.status !== undefined) && before.type !== 'project') {
|
|
217
|
+
invalid('correction_field_not_allowed', 'goal and status can be corrected only on a project');
|
|
218
|
+
}
|
|
219
|
+
const after = structuredClone(before);
|
|
220
|
+
const diff = {};
|
|
221
|
+
if (changes.name !== undefined)
|
|
222
|
+
setField(after, 'name', changes.name, before, diff);
|
|
223
|
+
if (changes.aliases !== undefined)
|
|
224
|
+
setField(after, 'aliases', changes.aliases.length > 0 ? changes.aliases : null, before, diff);
|
|
225
|
+
if (changes.summary !== undefined)
|
|
226
|
+
setField(after, 'summary', changes.summary, before, diff);
|
|
227
|
+
if (changes.validFrom !== undefined)
|
|
228
|
+
setField(after, 'validFrom', changes.validFrom, before, diff);
|
|
229
|
+
if (changes.validTo !== undefined)
|
|
230
|
+
setField(after, 'validTo', changes.validTo, before, diff);
|
|
231
|
+
if (changes.goal !== undefined || changes.status !== undefined) {
|
|
232
|
+
const metadata = { ...(before.metadata ?? {}) };
|
|
233
|
+
for (const key of ['goal', 'status']) {
|
|
234
|
+
const next = changes[key];
|
|
235
|
+
if (next === undefined)
|
|
236
|
+
continue;
|
|
237
|
+
const previous = typeof metadata[key] === 'string' ? metadata[key] : null;
|
|
238
|
+
if (previous === next)
|
|
239
|
+
continue;
|
|
240
|
+
if (next === null)
|
|
241
|
+
delete metadata[key];
|
|
242
|
+
else
|
|
243
|
+
metadata[key] = next;
|
|
244
|
+
diff[`metadata.${key}`] = { before: previous, after: next };
|
|
245
|
+
}
|
|
246
|
+
if (Object.keys(metadata).length > 0)
|
|
247
|
+
after.metadata = metadata;
|
|
248
|
+
else
|
|
249
|
+
delete after.metadata;
|
|
250
|
+
}
|
|
251
|
+
const entities = graph.entities.map((entity) => entity.id === before.id ? after : entity);
|
|
252
|
+
const nextGraph = validatedGraph({ ...graph, entities });
|
|
253
|
+
const incident = nextGraph.edges.filter((edge) => PROJECTED_RELATIONS.has(edge.relation)
|
|
254
|
+
&& (edge.fromId === before.id || edge.toId === before.id)).map((edge) => edge.id);
|
|
255
|
+
const projected = syncProjections(nextGraph, relationships, incident, at, { createFor: undefined, previous: before });
|
|
256
|
+
return finalize('update_entity', 'entity', before, after, diff, correction.reason, at, nextGraph, projected);
|
|
257
|
+
}
|
|
258
|
+
function planEdgeUpdate(graph, relationships, correction, at) {
|
|
259
|
+
const before = graph.edges.find((edge) => edge.id === correction.edgeId);
|
|
260
|
+
if (!before)
|
|
261
|
+
notFound('edge_not_found', `No relation ${correction.edgeId}`);
|
|
262
|
+
assertExpectedDigest('edge', before, correction.expectedDigest);
|
|
263
|
+
const after = structuredClone(before);
|
|
264
|
+
const diff = {};
|
|
265
|
+
const changes = correction.changes;
|
|
266
|
+
if (changes.role !== undefined)
|
|
267
|
+
setField(after, 'role', changes.role, before, diff);
|
|
268
|
+
if (changes.context !== undefined)
|
|
269
|
+
setField(after, 'context', changes.context, before, diff);
|
|
270
|
+
if (changes.validTo !== undefined)
|
|
271
|
+
setField(after, 'validTo', changes.validTo, before, diff);
|
|
272
|
+
const nextGraph = validatedGraph({ ...graph, edges: graph.edges.map((edge) => edge.id === before.id ? after : edge) });
|
|
273
|
+
const projected = PROJECTED_RELATIONS.has(after.relation)
|
|
274
|
+
? syncProjections(nextGraph, relationships, [after.id], at, { createFor: after.id })
|
|
275
|
+
: { relationships, changes: [] };
|
|
276
|
+
return finalize('update_edge', 'edge', before, after, diff, correction.reason, at, nextGraph, projected);
|
|
277
|
+
}
|
|
278
|
+
function planEdgeCreation(graph, relationships, correction, at) {
|
|
279
|
+
const input = correction.edge;
|
|
280
|
+
const definition = getCanonicalRelation(input.relation);
|
|
281
|
+
const from = graph.entities.find((entity) => entity.id === input.fromId);
|
|
282
|
+
const to = graph.entities.find((entity) => entity.id === input.toId);
|
|
283
|
+
if (!from)
|
|
284
|
+
notFound('entity_not_found', `No entity ${input.fromId}`);
|
|
285
|
+
if (!to)
|
|
286
|
+
notFound('entity_not_found', `No entity ${input.toId}`);
|
|
287
|
+
if (from.type !== definition.from || to.type !== definition.to) {
|
|
288
|
+
invalid('correction_endpoint_type_invalid', `${input.relation} requires ${definition.from} -> ${definition.to}`);
|
|
289
|
+
}
|
|
290
|
+
const id = canonicalEdgeId(input);
|
|
291
|
+
const existing = graph.edges.find((edge) => edge.id === id);
|
|
292
|
+
if (existing) {
|
|
293
|
+
throw new GraphCorrectionError('conflict', 'edge_already_exists', 'This relation already exists; correct it (for example clear validTo) instead of adding it again', { recordType: 'edge', record: existing, digest: graphRecordDigest(existing) });
|
|
294
|
+
}
|
|
295
|
+
const correctionId = graphCorrectionId(at, 'create_edge', id, null, correction.reason);
|
|
296
|
+
const edge = compact({
|
|
297
|
+
id,
|
|
298
|
+
fromId: input.fromId,
|
|
299
|
+
relation: input.relation,
|
|
300
|
+
toId: input.toId,
|
|
301
|
+
role: input.role,
|
|
302
|
+
context: input.context,
|
|
303
|
+
validFrom: input.validFrom,
|
|
304
|
+
provenance: { sourceKind: 'user_approved', sourceId: correctionId }
|
|
305
|
+
});
|
|
306
|
+
const nextGraph = validatedGraph({ ...graph, edges: [...graph.edges, edge] });
|
|
307
|
+
const diff = {};
|
|
308
|
+
for (const [key, value] of Object.entries(edge)) {
|
|
309
|
+
if (key === 'id')
|
|
310
|
+
continue;
|
|
311
|
+
diff[key] = { before: null, after: value };
|
|
312
|
+
}
|
|
313
|
+
const projected = PROJECTED_RELATIONS.has(edge.relation)
|
|
314
|
+
? syncProjections(nextGraph, relationships, [edge.id], at, { createFor: edge.id })
|
|
315
|
+
: { relationships, changes: [] };
|
|
316
|
+
return finalize('create_edge', 'edge', null, edge, diff, correction.reason, at, nextGraph, projected, correctionId);
|
|
317
|
+
}
|
|
318
|
+
function finalize(kind, recordType, before, after, diff, reason, at, graph, projected, presetId) {
|
|
319
|
+
const beforeDigest = before ? graphRecordDigest(before) : null;
|
|
320
|
+
const afterDigest = graphRecordDigest(after);
|
|
321
|
+
if (Object.keys(diff).length === 0 || beforeDigest === afterDigest) {
|
|
322
|
+
invalid('correction_no_change', 'The correction does not change the record');
|
|
323
|
+
}
|
|
324
|
+
const record = {
|
|
325
|
+
schema: GRAPH_CORRECTION_SCHEMA,
|
|
326
|
+
id: presetId ?? graphCorrectionId(at, kind, after.id, beforeDigest, reason),
|
|
327
|
+
at,
|
|
328
|
+
kind,
|
|
329
|
+
target: { recordType, id: after.id },
|
|
330
|
+
reason,
|
|
331
|
+
changedFields: Object.keys(diff),
|
|
332
|
+
changes: diff,
|
|
333
|
+
beforeDigest,
|
|
334
|
+
afterDigest,
|
|
335
|
+
projections: projected.changes
|
|
336
|
+
};
|
|
337
|
+
return { graph, relationships: projected.relationships, record, recordType };
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Keeps relationships.json aligned with person -> project relations, using the
|
|
341
|
+
* record shape written by project registration: the person's name, the role,
|
|
342
|
+
* `"<project name>: <context>"`, and the `project`/`relationship` tags.
|
|
343
|
+
*
|
|
344
|
+
* A projection record is linked to an edge when its id is the edge's
|
|
345
|
+
* provenance sourceId (project registration, seed), the id import writes for
|
|
346
|
+
* the same person and context, or `relationship-<edge id>` (records created
|
|
347
|
+
* here). Records linked to more than one edge are left untouched. A record is
|
|
348
|
+
* removed when its relation or an endpoint is no longer active; the removed
|
|
349
|
+
* record is kept in the history line. Only the relation targeted by the
|
|
350
|
+
* correction may gain a new projection record.
|
|
351
|
+
*/
|
|
352
|
+
function syncProjections(graph, relationships, edgeIds, at, options) {
|
|
353
|
+
let next = [...relationships];
|
|
354
|
+
const changes = [];
|
|
355
|
+
const entities = new Map(graph.entities.map((entity) => [entity.id, entity]));
|
|
356
|
+
const projectedEdges = graph.edges.filter((edge) => PROJECTED_RELATIONS.has(edge.relation));
|
|
357
|
+
for (const edgeId of edgeIds) {
|
|
358
|
+
const edge = projectedEdges.find((candidate) => candidate.id === edgeId);
|
|
359
|
+
if (!edge)
|
|
360
|
+
continue;
|
|
361
|
+
const person = entities.get(edge.fromId);
|
|
362
|
+
const project = entities.get(edge.toId);
|
|
363
|
+
if (!person || !project)
|
|
364
|
+
continue;
|
|
365
|
+
const personVersions = options.previous?.id === person.id ? [person, options.previous] : [person];
|
|
366
|
+
const ids = linkedProjectionIds(edge, personVersions);
|
|
367
|
+
const linked = next.filter((record) => ids.has(record.id));
|
|
368
|
+
const shared = linked.filter((record) => projectedEdges.some((other) => other.id !== edge.id
|
|
369
|
+
&& linkedProjectionIds(other, [entities.get(other.fromId)]).has(record.id)));
|
|
370
|
+
for (const record of shared) {
|
|
371
|
+
changes.push({ edgeId, relationshipId: record.id, action: 'shared_unchanged', beforeDigest: graphRecordDigest(record), afterDigest: graphRecordDigest(record), before: null });
|
|
372
|
+
}
|
|
373
|
+
const exclusive = linked.filter((record) => !shared.includes(record));
|
|
374
|
+
const active = isActiveAt(edge, at) && isActiveAt(person, at) && isActiveAt(project, at);
|
|
375
|
+
if (!active) {
|
|
376
|
+
for (const record of exclusive) {
|
|
377
|
+
next = next.filter((candidate) => candidate.id !== record.id);
|
|
378
|
+
changes.push({ edgeId, relationshipId: record.id, action: 'removed', beforeDigest: graphRecordDigest(record), afterDigest: null, before: record });
|
|
379
|
+
}
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
const targets = exclusive.length > 0 || shared.length > 0 || options.createFor !== edge.id
|
|
383
|
+
? exclusive
|
|
384
|
+
: [undefined];
|
|
385
|
+
for (const existing of targets) {
|
|
386
|
+
const rendered = renderProjection(edge, person, project, existing, at);
|
|
387
|
+
if (existing && sameProjection(existing, rendered))
|
|
388
|
+
continue;
|
|
389
|
+
if (existing)
|
|
390
|
+
next = next.map((candidate) => candidate.id === existing.id ? rendered : candidate);
|
|
391
|
+
else
|
|
392
|
+
next.push(rendered);
|
|
393
|
+
changes.push({
|
|
394
|
+
edgeId,
|
|
395
|
+
relationshipId: rendered.id,
|
|
396
|
+
action: existing ? 'updated' : 'created',
|
|
397
|
+
beforeDigest: existing ? graphRecordDigest(existing) : null,
|
|
398
|
+
afterDigest: graphRecordDigest(rendered),
|
|
399
|
+
before: existing ?? null
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return { relationships: next, changes };
|
|
404
|
+
}
|
|
405
|
+
function linkedProjectionIds(edge, persons) {
|
|
406
|
+
const ids = new Set([projectionIdFor(edge)]);
|
|
407
|
+
if (edge.provenance?.sourceId)
|
|
408
|
+
ids.add(edge.provenance.sourceId);
|
|
409
|
+
for (const person of persons) {
|
|
410
|
+
// Import (`onboard:apply`, connected onboarding) writes this id for a
|
|
411
|
+
// relationship candidate with the same person name and context.
|
|
412
|
+
if (person && edge.context)
|
|
413
|
+
ids.add(`relationship-${stableHash(`${person.name}|${edge.context}`)}`);
|
|
414
|
+
}
|
|
415
|
+
return ids;
|
|
416
|
+
}
|
|
417
|
+
function projectionIdFor(edge) {
|
|
418
|
+
return `relationship-${edge.id}`;
|
|
419
|
+
}
|
|
420
|
+
function renderProjection(edge, person, project, existing, at) {
|
|
421
|
+
return compact({
|
|
422
|
+
id: existing?.id ?? projectionIdFor(edge),
|
|
423
|
+
person: person.name,
|
|
424
|
+
role: edge.role,
|
|
425
|
+
context: edge.context ? `${project.name}: ${edge.context}` : project.name,
|
|
426
|
+
tags: existing?.tags ?? ['project', 'relationship'],
|
|
427
|
+
updatedAt: at
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
function sameProjection(left, right) {
|
|
431
|
+
const { updatedAt: _left, ...leftRest } = left;
|
|
432
|
+
const { updatedAt: _right, ...rightRest } = right;
|
|
433
|
+
return canonicalPortableJson(leftRest) === canonicalPortableJson(rightRest);
|
|
434
|
+
}
|
|
435
|
+
function validatedGraph(graph) {
|
|
436
|
+
try {
|
|
437
|
+
validateCanonicalGraph(graph);
|
|
438
|
+
}
|
|
439
|
+
catch (error) {
|
|
440
|
+
invalid('correction_invalid', error instanceof Error ? error.message : String(error));
|
|
441
|
+
}
|
|
442
|
+
return graph;
|
|
443
|
+
}
|
|
444
|
+
function assertExpectedDigest(recordType, record, expected) {
|
|
445
|
+
const digest = graphRecordDigest(record);
|
|
446
|
+
if (digest !== expected) {
|
|
447
|
+
throw new GraphCorrectionError('conflict', 'digest_conflict', 'The record changed after it was read; review the current record and correct it again', { recordType, record, digest });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
function setField(target, key, value, before, diff) {
|
|
451
|
+
const previous = (before[key] ?? null);
|
|
452
|
+
const next = value ?? null;
|
|
453
|
+
if (canonicalPortableJson(previous) === canonicalPortableJson(next))
|
|
454
|
+
return;
|
|
455
|
+
if (next === null)
|
|
456
|
+
delete target[key];
|
|
457
|
+
else
|
|
458
|
+
target[key] = next;
|
|
459
|
+
diff[key] = { before: previous, after: next };
|
|
460
|
+
}
|
|
461
|
+
function graphCorrectionId(at, kind, targetId, beforeDigest, reason) {
|
|
462
|
+
return `graph-correction-${createHash('sha256').update(JSON.stringify([at, kind, targetId, beforeDigest, reason]), 'utf8').digest('hex').slice(0, 24)}`;
|
|
463
|
+
}
|
|
464
|
+
function appendJsonl(existing, record) {
|
|
465
|
+
const prefix = existing === undefined || existing === '' ? '' : existing.endsWith('\n') ? existing : `${existing}\n`;
|
|
466
|
+
return `${prefix}${JSON.stringify(record)}\n`;
|
|
467
|
+
}
|
|
468
|
+
function parseEntityChanges(value) {
|
|
469
|
+
if (!isRecord(value) || Object.keys(value).length === 0)
|
|
470
|
+
invalid('correction_changes_required', 'changes must be a non-empty object');
|
|
471
|
+
assertOnlyKeys(value, [...GRAPH_CORRECTION_ENTITY_FIELDS, ...GRAPH_CORRECTION_PROJECT_FIELDS], 'changes.');
|
|
472
|
+
const changes = {};
|
|
473
|
+
if (value.name !== undefined) {
|
|
474
|
+
const name = optionalText(value.name, 'changes.name', MAX_NAME_LENGTH);
|
|
475
|
+
if (name === null)
|
|
476
|
+
invalid('correction_invalid', 'changes.name must be a non-empty string');
|
|
477
|
+
changes.name = name;
|
|
478
|
+
}
|
|
479
|
+
if (value.aliases !== undefined)
|
|
480
|
+
changes.aliases = parseAliases(value.aliases);
|
|
481
|
+
if (value.summary !== undefined)
|
|
482
|
+
changes.summary = optionalText(value.summary, 'changes.summary', MAX_TEXT_LENGTH);
|
|
483
|
+
if (value.validFrom !== undefined)
|
|
484
|
+
changes.validFrom = optionalDateTime(value.validFrom, 'changes.validFrom');
|
|
485
|
+
if (value.validTo !== undefined)
|
|
486
|
+
changes.validTo = optionalDateTime(value.validTo, 'changes.validTo');
|
|
487
|
+
if (value.goal !== undefined)
|
|
488
|
+
changes.goal = optionalText(value.goal, 'changes.goal', MAX_TEXT_LENGTH);
|
|
489
|
+
if (value.status !== undefined)
|
|
490
|
+
changes.status = optionalText(value.status, 'changes.status', MAX_NAME_LENGTH);
|
|
491
|
+
return changes;
|
|
492
|
+
}
|
|
493
|
+
function parseEdgeChanges(value) {
|
|
494
|
+
if (!isRecord(value) || Object.keys(value).length === 0)
|
|
495
|
+
invalid('correction_changes_required', 'changes must be a non-empty object');
|
|
496
|
+
assertOnlyKeys(value, GRAPH_CORRECTION_EDGE_FIELDS, 'changes.');
|
|
497
|
+
const changes = {};
|
|
498
|
+
if (value.role !== undefined)
|
|
499
|
+
changes.role = optionalText(value.role, 'changes.role', MAX_NAME_LENGTH);
|
|
500
|
+
if (value.context !== undefined)
|
|
501
|
+
changes.context = optionalText(value.context, 'changes.context', MAX_TEXT_LENGTH);
|
|
502
|
+
if (value.validTo !== undefined)
|
|
503
|
+
changes.validTo = optionalDateTime(value.validTo, 'changes.validTo');
|
|
504
|
+
return changes;
|
|
505
|
+
}
|
|
506
|
+
function parseEdgeCreation(value) {
|
|
507
|
+
if (!isRecord(value))
|
|
508
|
+
invalid('correction_invalid', 'edge must be an object');
|
|
509
|
+
assertOnlyKeys(value, ['fromId', 'relation', 'toId', 'role', 'context', 'validFrom'], 'edge.');
|
|
510
|
+
const relation = value.relation;
|
|
511
|
+
if (typeof relation !== 'string' || !GRAPH_CORRECTION_NEW_EDGE_RELATIONS.includes(relation)) {
|
|
512
|
+
invalid('correction_relation_not_allowed', `edge.relation must be one of ${GRAPH_CORRECTION_NEW_EDGE_RELATIONS.join(', ')}`);
|
|
513
|
+
}
|
|
514
|
+
const role = value.role === undefined ? null : optionalText(value.role, 'edge.role', MAX_NAME_LENGTH);
|
|
515
|
+
const context = value.context === undefined ? null : optionalText(value.context, 'edge.context', MAX_TEXT_LENGTH);
|
|
516
|
+
const validFrom = value.validFrom === undefined ? null : optionalDateTime(value.validFrom, 'edge.validFrom');
|
|
517
|
+
return compact({
|
|
518
|
+
fromId: requireId(value.fromId, 'edge.fromId'),
|
|
519
|
+
relation: relation,
|
|
520
|
+
toId: requireId(value.toId, 'edge.toId'),
|
|
521
|
+
role: role ?? undefined,
|
|
522
|
+
context: context ?? undefined,
|
|
523
|
+
validFrom: validFrom ?? undefined
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
function assertOnlyKeys(value, allowed, prefix) {
|
|
527
|
+
for (const key of Object.keys(value)) {
|
|
528
|
+
if (allowed.includes(key))
|
|
529
|
+
continue;
|
|
530
|
+
if (key === 'id')
|
|
531
|
+
invalid('correction_id_change_not_allowed', `${prefix}id cannot be changed`);
|
|
532
|
+
if (key === 'type')
|
|
533
|
+
invalid('correction_type_change_not_allowed', `${prefix}type cannot be changed`);
|
|
534
|
+
if (key === 'relation') {
|
|
535
|
+
invalid('correction_relation_change_not_allowed', 'A relation type cannot be changed; end this relation with validTo and add a new one');
|
|
536
|
+
}
|
|
537
|
+
if (key === 'fromId' || key === 'toId') {
|
|
538
|
+
invalid('correction_endpoint_change_not_allowed', 'A relation endpoint cannot be changed; end this relation with validTo and add a new one');
|
|
539
|
+
}
|
|
540
|
+
invalid('correction_field_not_allowed', `${prefix}${key} cannot be corrected`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
function parseAliases(value) {
|
|
544
|
+
if (!Array.isArray(value) || value.length > MAX_ALIASES) {
|
|
545
|
+
invalid('correction_invalid', `changes.aliases must be an array of at most ${MAX_ALIASES} strings`);
|
|
546
|
+
}
|
|
547
|
+
const aliases = [];
|
|
548
|
+
for (const [index, alias] of value.entries()) {
|
|
549
|
+
const text = optionalText(alias, `changes.aliases[${index}]`, MAX_NAME_LENGTH);
|
|
550
|
+
if (text === null)
|
|
551
|
+
invalid('correction_invalid', `changes.aliases[${index}] must be a non-empty string`);
|
|
552
|
+
if (!aliases.includes(text))
|
|
553
|
+
aliases.push(text);
|
|
554
|
+
}
|
|
555
|
+
return aliases;
|
|
556
|
+
}
|
|
557
|
+
function optionalText(value, field, max) {
|
|
558
|
+
if (value === null)
|
|
559
|
+
return null;
|
|
560
|
+
if (typeof value !== 'string')
|
|
561
|
+
invalid('correction_invalid', `${field} must be a string or null`);
|
|
562
|
+
const text = value.trim();
|
|
563
|
+
if (text.length > max)
|
|
564
|
+
invalid('correction_invalid', `${field} must be at most ${max} characters`);
|
|
565
|
+
return text === '' ? null : text;
|
|
566
|
+
}
|
|
567
|
+
function optionalDateTime(value, field) {
|
|
568
|
+
if (value === null)
|
|
569
|
+
return null;
|
|
570
|
+
if (typeof value !== 'string')
|
|
571
|
+
invalid('correction_invalid', `${field} must be an RFC 3339 date-time or null`);
|
|
572
|
+
try {
|
|
573
|
+
isActiveAt({}, value);
|
|
574
|
+
}
|
|
575
|
+
catch {
|
|
576
|
+
invalid('correction_invalid', `${field} must be an RFC 3339 date-time such as 2026-09-30T00:00:00Z`);
|
|
577
|
+
}
|
|
578
|
+
return value;
|
|
579
|
+
}
|
|
580
|
+
function requireId(value, field) {
|
|
581
|
+
if (typeof value !== 'string' || value.trim() === '' || value.length > 500)
|
|
582
|
+
invalid('correction_invalid', `${field} must be a non-empty id`);
|
|
583
|
+
return value;
|
|
584
|
+
}
|
|
585
|
+
function requireDigest(value) {
|
|
586
|
+
if (typeof value !== 'string' || !DIGEST_PATTERN.test(value)) {
|
|
587
|
+
invalid('correction_digest_required', 'expectedDigest must be the sha256 digest of the record that was read');
|
|
588
|
+
}
|
|
589
|
+
return value;
|
|
590
|
+
}
|
|
591
|
+
function requireReason(value) {
|
|
592
|
+
if (typeof value !== 'string' || value.trim() === '')
|
|
593
|
+
invalid('correction_reason_required', 'A one-sentence reason is required');
|
|
594
|
+
const reason = value.trim();
|
|
595
|
+
if (/[\r\n]/u.test(reason) || reason.length > MAX_REASON_LENGTH) {
|
|
596
|
+
invalid('correction_reason_invalid', `The reason must be one sentence on one line and at most ${MAX_REASON_LENGTH} characters`);
|
|
597
|
+
}
|
|
598
|
+
return reason;
|
|
599
|
+
}
|
|
600
|
+
function compact(value) {
|
|
601
|
+
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
|
|
602
|
+
}
|
|
603
|
+
async function fileExists(path) {
|
|
604
|
+
try {
|
|
605
|
+
await access(path);
|
|
606
|
+
return true;
|
|
607
|
+
}
|
|
608
|
+
catch {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
function isRecord(value) {
|
|
613
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
614
|
+
}
|
|
615
|
+
function invalid(code, message) {
|
|
616
|
+
throw new GraphCorrectionError('invalid', code, message);
|
|
617
|
+
}
|
|
618
|
+
function notFound(code, message) {
|
|
619
|
+
throw new GraphCorrectionError('not_found', code, message);
|
|
620
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type FoundationRevisionStore, type FoundationStoreContext } from './foundation-store.js';
|
|
2
|
+
import type { PhilosophyRevisionReader } from './philosophy-revision-reader.js';
|
|
3
|
+
/** Read contract for history that is joined to the current Graph row. */
|
|
4
|
+
export declare const GRAPH_FOUNDATION_READ_CONTRACT_VERSION: "graph-foundation-history-read.v1";
|
|
5
|
+
/**
|
|
6
|
+
* The query deliberately joins the historical row to the current Graph row.
|
|
7
|
+
* The host supplies a transaction-bound query function; its RLS policies make
|
|
8
|
+
* an invisible current row disappear before the adapter sees the result.
|
|
9
|
+
*
|
|
10
|
+
* `storage_digest_valid` is calculated by PostgreSQL over `payload::text` so
|
|
11
|
+
* that an adapter never has to reproduce database JSON serialization rules.
|
|
12
|
+
* `current_visible` is selected explicitly to make fake/test adapters and
|
|
13
|
+
* future providers fail closed when they do not prove current-row visibility.
|
|
14
|
+
*/
|
|
15
|
+
export declare const GRAPH_FOUNDATION_READ_SQL = "\nSELECT\n history.entity_id,\n history.entity_type,\n history.revision,\n history.payload,\n history.project_id,\n history.role_min,\n history.sensitivity,\n history.lifecycle_status,\n history.storage_digest,\n history.captured_at,\n (\n history.storage_digest = 'sha256:' ||\n encode(sha256(convert_to(history.payload::text, 'UTF8')), 'hex')\n ) AS storage_digest_valid,\n current_entity.id AS current_entity_id,\n current_entity.entity_type AS current_entity_type,\n current_entity.payload AS current_payload,\n current_entity.project_id AS current_project_id,\n current_entity.role_min AS current_role_min,\n current_entity.sensitivity AS current_sensitivity,\n current_entity.lifecycle_status AS current_lifecycle_status,\n projects.code AS current_project_code,\n true AS current_visible\nFROM public.graph_foundation_revisions AS history\nJOIN public.graph_entities AS current_entity\n ON current_entity.id = history.entity_id\n AND current_entity.entity_type = history.entity_type\nJOIN public.projects AS projects\n ON projects.id = current_entity.project_id\nWHERE history.entity_id = $1\n AND history.entity_type = $2\n AND history.revision = $3\n";
|
|
16
|
+
/** Flat row shape returned by GRAPH_FOUNDATION_READ_SQL. */
|
|
17
|
+
export interface GraphFoundationHistoryRow {
|
|
18
|
+
readonly entity_id?: unknown;
|
|
19
|
+
readonly entity_type?: unknown;
|
|
20
|
+
readonly revision?: unknown;
|
|
21
|
+
readonly payload?: unknown;
|
|
22
|
+
readonly project_id?: unknown;
|
|
23
|
+
readonly role_min?: unknown;
|
|
24
|
+
readonly sensitivity?: unknown;
|
|
25
|
+
readonly lifecycle_status?: unknown;
|
|
26
|
+
readonly storage_digest?: unknown;
|
|
27
|
+
readonly storage_digest_valid?: unknown;
|
|
28
|
+
readonly captured_at?: unknown;
|
|
29
|
+
readonly current_entity_id?: unknown;
|
|
30
|
+
readonly current_entity_type?: unknown;
|
|
31
|
+
readonly current_payload?: unknown;
|
|
32
|
+
readonly current_project_id?: unknown;
|
|
33
|
+
readonly current_project_code?: unknown;
|
|
34
|
+
readonly current_role_min?: unknown;
|
|
35
|
+
readonly current_sensitivity?: unknown;
|
|
36
|
+
readonly current_lifecycle_status?: unknown;
|
|
37
|
+
readonly current_visible?: unknown;
|
|
38
|
+
}
|
|
39
|
+
export interface GraphFoundationQueryResult {
|
|
40
|
+
readonly rows: readonly GraphFoundationHistoryRow[];
|
|
41
|
+
}
|
|
42
|
+
/** A pg-compatible callback, normally `client.query.bind(client)`. */
|
|
43
|
+
export type GraphFoundationQuery = (text: string, values: readonly unknown[]) => GraphFoundationQueryResult | readonly GraphFoundationHistoryRow[] | Promise<GraphFoundationQueryResult | readonly GraphFoundationHistoryRow[]>;
|
|
44
|
+
export interface GraphFoundationReaderOptions {
|
|
45
|
+
/** Context resolved by the trusted host auth boundary. */
|
|
46
|
+
readonly context: FoundationStoreContext;
|
|
47
|
+
/** Query callback bound to the host's already-authenticated transaction. */
|
|
48
|
+
readonly query: GraphFoundationQuery;
|
|
49
|
+
}
|
|
50
|
+
export interface GraphFoundationReaders {
|
|
51
|
+
readonly store: Pick<FoundationRevisionStore, 'read'>;
|
|
52
|
+
readonly philosophyReader: PhilosophyRevisionReader;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Create both public read ports over one trusted, transaction-bound context.
|
|
56
|
+
* No principal or tenant is read from a request payload. The context is
|
|
57
|
+
* closed over by both readers and a caller-supplied context must carry the
|
|
58
|
+
* same principal before any query is issued.
|
|
59
|
+
*/
|
|
60
|
+
export declare function createGraphFoundationReaders(options: GraphFoundationReaderOptions): GraphFoundationReaders;
|