@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,639 @@
|
|
|
1
|
+
import { access, readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { isActiveAt } from './canonical-graph.js';
|
|
4
|
+
import { assertFoundationCatalog, findFoundationRecord } from './foundation-catalog.js';
|
|
5
|
+
import { GRAPH_CORRECTION_EDGE_FIELDS, GRAPH_CORRECTION_ENTITY_FIELDS, GRAPH_CORRECTION_NEW_EDGE_RELATIONS, GRAPH_CORRECTION_PROJECT_FIELDS, GRAPH_CORRECTIONS_FILE, graphRecordDigest, readGraphCorrectionHistory } from './graph-corrections.js';
|
|
6
|
+
import { ONTOLOGY_VERSION, portableOntology } from './ontology.js';
|
|
7
|
+
import { canonicalRelationRegistry, getCanonicalRelation } from './relation-registry.js';
|
|
8
|
+
import { loadPersonalOs } from './ssot.js';
|
|
9
|
+
import { bindPortableGraphOwner, validatePortableGraph } from './portable-graph.js';
|
|
10
|
+
/**
|
|
11
|
+
* Read views of the canonical local Graph for the local Web screens
|
|
12
|
+
* "プロジェクトと関係者" and "情報と関係".
|
|
13
|
+
*
|
|
14
|
+
* Every function takes an explicit data directory chosen by the host, never by
|
|
15
|
+
* the browser. A Graph v1 file is reported as `migration_required` and a
|
|
16
|
+
* missing data set as `not_initialized`; neither is presented as zero items,
|
|
17
|
+
* and a read failure is thrown instead of being returned as an empty list.
|
|
18
|
+
*
|
|
19
|
+
* `openOwnerPrivateGraph` gives the same read views over a portable bundle a
|
|
20
|
+
* host keeps for its owner (the owner-private area), without a data directory.
|
|
21
|
+
*/
|
|
22
|
+
export const GRAPH_WEB_VERSION = 'graph-web.v1';
|
|
23
|
+
export const GRAPH_WEB_START_COMMAND = 'brainbase onboard:start';
|
|
24
|
+
export const GRAPH_WEB_SEARCH_MAX_LIMIT = 100;
|
|
25
|
+
const CANONICAL_FILES = ['graph.json', 'relationships.json', 'personal-kg.jsonl', 'decisions.jsonl'];
|
|
26
|
+
const ENTITY_KINDS = ['person', 'org', 'project', 'decision'];
|
|
27
|
+
/** The kinds of record a Graph v2 can hold; any other kind is absent from it. */
|
|
28
|
+
export const GRAPH_WEB_ENTITY_KINDS = ENTITY_KINDS;
|
|
29
|
+
const PARTICIPATION = new Set(['participates_in', 'accountable_for']);
|
|
30
|
+
const EXTRACTED_CANDIDATE_FILE = /^extracted-[^/\\]+\.json$/u;
|
|
31
|
+
const ONBOARDING_LEDGER_FILE = 'runs/connected-onboarding.json';
|
|
32
|
+
const ONBOARDING_LEDGER_SCHEMA = 'connected_onboarding.v1';
|
|
33
|
+
export class GraphWebError extends Error {
|
|
34
|
+
kind;
|
|
35
|
+
code;
|
|
36
|
+
name = 'GraphWebError';
|
|
37
|
+
constructor(kind, code, message) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.kind = kind;
|
|
40
|
+
this.code = code;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function readGraphWebStatus(dataDir, options = {}) {
|
|
44
|
+
const loaded = await loadForRead(dataDir);
|
|
45
|
+
if ('status' in loaded)
|
|
46
|
+
return loaded;
|
|
47
|
+
const asOf = resolveAsOf(options);
|
|
48
|
+
const { graph } = loaded;
|
|
49
|
+
const entities = Object.fromEntries(ENTITY_KINDS.map((kind) => [kind, graph.entities.filter((entity) => entity.type === kind).length]));
|
|
50
|
+
const history = await readGraphCorrectionHistory(dataDir);
|
|
51
|
+
return {
|
|
52
|
+
status: 'ok',
|
|
53
|
+
version: GRAPH_WEB_VERSION,
|
|
54
|
+
source: loaded.source,
|
|
55
|
+
asOf,
|
|
56
|
+
ontology: { ...graph.ontology, currentVersion: ONTOLOGY_VERSION },
|
|
57
|
+
counts: {
|
|
58
|
+
entities: { ...entities, total: graph.entities.length },
|
|
59
|
+
edges: { total: graph.edges.length, active: graph.edges.filter((edge) => isActiveAt(edge, asOf)).length },
|
|
60
|
+
relationships: loaded.os.relationships.relationships.length,
|
|
61
|
+
corrections: history.records.length
|
|
62
|
+
},
|
|
63
|
+
issues: history.issues
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export async function listGraphProjects(dataDir, options = {}) {
|
|
67
|
+
const loaded = await loadForRead(dataDir);
|
|
68
|
+
if ('status' in loaded)
|
|
69
|
+
return loaded;
|
|
70
|
+
return projectListOf(loaded.graph, loaded.source, resolveAsOf(options));
|
|
71
|
+
}
|
|
72
|
+
function projectListOf(graph, source, asOf) {
|
|
73
|
+
const entities = entityIndex(graph);
|
|
74
|
+
const projects = graph.entities.filter((entity) => entity.type === 'project').map((project) => {
|
|
75
|
+
const participation = graph.edges.filter((edge) => edge.toId === project.id && PARTICIPATION.has(edge.relation)
|
|
76
|
+
&& isActiveAt(edge, asOf) && isEntityActive(entities.get(edge.fromId), asOf));
|
|
77
|
+
const byPerson = new Map();
|
|
78
|
+
for (const edge of participation) {
|
|
79
|
+
const person = byPerson.get(edge.fromId) ?? { id: edge.fromId, name: entities.get(edge.fromId)?.name ?? edge.fromId, accountable: false };
|
|
80
|
+
if (edge.relation === 'accountable_for')
|
|
81
|
+
person.accountable = true;
|
|
82
|
+
byPerson.set(edge.fromId, person);
|
|
83
|
+
}
|
|
84
|
+
const people = [...byPerson.values()].sort((left, right) => Number(right.accountable) - Number(left.accountable)
|
|
85
|
+
|| left.name.localeCompare(right.name, 'ja')
|
|
86
|
+
|| left.id.localeCompare(right.id, 'en'));
|
|
87
|
+
return {
|
|
88
|
+
...projectView(project, asOf),
|
|
89
|
+
participantCount: people.length,
|
|
90
|
+
accountableCount: people.filter((person) => person.accountable).length,
|
|
91
|
+
people
|
|
92
|
+
};
|
|
93
|
+
}).sort(byActiveThenName);
|
|
94
|
+
return { status: 'ok', source, asOf, projects, absenceConfirmed: projects.length === 0 };
|
|
95
|
+
}
|
|
96
|
+
export async function readGraphProject(dataDir, projectId, options = {}) {
|
|
97
|
+
const loaded = await loadForRead(dataDir);
|
|
98
|
+
if ('status' in loaded)
|
|
99
|
+
return loaded;
|
|
100
|
+
const asOf = resolveAsOf(options);
|
|
101
|
+
if (!loaded.graph.entities.some((entity) => entity.id === projectId && entity.type === 'project')) {
|
|
102
|
+
throw new GraphWebError('not_found', 'project_not_found', `No project ${projectId}`);
|
|
103
|
+
}
|
|
104
|
+
return projectDetailOf(loaded.graph, loaded.source, projectId, asOf, await loadReferences(dataDir));
|
|
105
|
+
}
|
|
106
|
+
function projectDetailOf(graph, source, projectId, asOf, references) {
|
|
107
|
+
const project = graph.entities.find((entity) => entity.id === projectId && entity.type === 'project');
|
|
108
|
+
if (!project)
|
|
109
|
+
throw new GraphWebError('not_found', 'project_not_found', `No project ${projectId}`);
|
|
110
|
+
const entities = entityIndex(graph);
|
|
111
|
+
const edges = incidentEdges(graph, project.id, asOf, entities, references);
|
|
112
|
+
const participants = edges.filter((edge) => edge.direction === 'incoming' && PARTICIPATION.has(edge.relation)).sort(byParticipation);
|
|
113
|
+
const relations = edges.filter((edge) => !participants.includes(edge));
|
|
114
|
+
const touched = new Set([project.id, ...edges.map((edge) => edge.id)]);
|
|
115
|
+
return {
|
|
116
|
+
status: 'ok',
|
|
117
|
+
source,
|
|
118
|
+
asOf,
|
|
119
|
+
project: {
|
|
120
|
+
...projectView(project, asOf),
|
|
121
|
+
decisionPrinciples: stringArray(project.metadata?.decisionPrinciples),
|
|
122
|
+
metadata: project.metadata ?? {}
|
|
123
|
+
},
|
|
124
|
+
participants,
|
|
125
|
+
relations,
|
|
126
|
+
history: historyFor(references.history, touched),
|
|
127
|
+
issues: references.issues
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
export async function searchGraphEntities(dataDir, input = {}) {
|
|
131
|
+
const query = parseSearchInput(input);
|
|
132
|
+
const loaded = await loadForRead(dataDir);
|
|
133
|
+
if ('status' in loaded)
|
|
134
|
+
return loaded;
|
|
135
|
+
return entitySearchOf(loaded.graph, loaded.source, query);
|
|
136
|
+
}
|
|
137
|
+
function parseSearchInput(input) {
|
|
138
|
+
const q = typeof input.q === 'string' ? input.q.trim() : '';
|
|
139
|
+
if (q.length > 200)
|
|
140
|
+
throw new GraphWebError('invalid', 'invalid_query', 'q must be at most 200 characters');
|
|
141
|
+
const type = parseEntityType(input.type);
|
|
142
|
+
const asOf = input.asOf === undefined || input.asOf === '' ? null : parseAsOf(input.asOf);
|
|
143
|
+
const limit = input.limit ?? 50;
|
|
144
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > GRAPH_WEB_SEARCH_MAX_LIMIT) {
|
|
145
|
+
throw new GraphWebError('invalid', 'invalid_query', `limit must be an integer from 1 to ${GRAPH_WEB_SEARCH_MAX_LIMIT}`);
|
|
146
|
+
}
|
|
147
|
+
return { q, type, asOf, limit, now: input.now };
|
|
148
|
+
}
|
|
149
|
+
function entitySearchOf(graph, source, { q, type, asOf, limit, now }) {
|
|
150
|
+
const needle = normalize(q);
|
|
151
|
+
const scored = graph.entities.flatMap((entity) => {
|
|
152
|
+
if (type && entity.type !== type)
|
|
153
|
+
return [];
|
|
154
|
+
if (asOf && !isActiveAt(entity, asOf))
|
|
155
|
+
return [];
|
|
156
|
+
const rank = matchRank(entity, needle);
|
|
157
|
+
return rank === undefined ? [] : [{ entity, rank }];
|
|
158
|
+
}).sort((left, right) => left.rank - right.rank
|
|
159
|
+
|| left.entity.type.localeCompare(right.entity.type, 'en')
|
|
160
|
+
|| left.entity.name.localeCompare(right.entity.name, 'ja')
|
|
161
|
+
|| left.entity.id.localeCompare(right.entity.id, 'en'));
|
|
162
|
+
const viewAsOf = asOf ?? (now ?? new Date()).toISOString();
|
|
163
|
+
const results = scored.slice(0, limit).map(({ entity }) => entityView(entity, viewAsOf));
|
|
164
|
+
return {
|
|
165
|
+
status: 'ok',
|
|
166
|
+
source,
|
|
167
|
+
query: { q, type, asOf },
|
|
168
|
+
results,
|
|
169
|
+
total: scored.length,
|
|
170
|
+
truncated: scored.length > results.length,
|
|
171
|
+
absenceConfirmed: scored.length === 0,
|
|
172
|
+
graphEmpty: graph.entities.length === 0
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
export async function readGraphEntity(dataDir, entityId, options = {}) {
|
|
176
|
+
const loaded = await loadForRead(dataDir);
|
|
177
|
+
if ('status' in loaded)
|
|
178
|
+
return loaded;
|
|
179
|
+
const asOf = resolveAsOf(options);
|
|
180
|
+
uniqueEntity(loaded.graph, entityId, 'graph.json');
|
|
181
|
+
return entityDetailOf(loaded.graph, loaded.source, entityId, asOf, await loadReferences(dataDir));
|
|
182
|
+
}
|
|
183
|
+
function uniqueEntity(graph, entityId, where) {
|
|
184
|
+
const matches = graph.entities.filter((candidate) => candidate.id === entityId);
|
|
185
|
+
if (matches.length === 0)
|
|
186
|
+
throw new GraphWebError('not_found', 'entity_not_found', `No entity ${entityId}`);
|
|
187
|
+
if (matches.length > 1)
|
|
188
|
+
throw new GraphWebError('unavailable', 'entity_id_ambiguous', `Entity id ${entityId} is duplicated in ${where}`);
|
|
189
|
+
return matches[0];
|
|
190
|
+
}
|
|
191
|
+
function entityDetailOf(graph, source, entityId, asOf, references) {
|
|
192
|
+
const entity = uniqueEntity(graph, entityId, 'the Graph');
|
|
193
|
+
const edges = incidentEdges(graph, entity.id, asOf, entityIndex(graph), references);
|
|
194
|
+
const touched = new Set([entity.id, ...edges.map((edge) => edge.id)]);
|
|
195
|
+
return {
|
|
196
|
+
status: 'ok',
|
|
197
|
+
source,
|
|
198
|
+
asOf,
|
|
199
|
+
entity: { ...entityView(entity, asOf), metadata: entity.metadata ?? {} },
|
|
200
|
+
outgoing: edges.filter((edge) => edge.direction === 'outgoing'),
|
|
201
|
+
incoming: edges.filter((edge) => edge.direction === 'incoming'),
|
|
202
|
+
history: historyFor(references.history, touched),
|
|
203
|
+
issues: references.issues
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
export async function readGraphOntology(dataDir, options = {}) {
|
|
207
|
+
const loaded = await loadForRead(dataDir);
|
|
208
|
+
if ('status' in loaded)
|
|
209
|
+
return loaded;
|
|
210
|
+
const asOf = resolveAsOf(options);
|
|
211
|
+
const { graph } = loaded;
|
|
212
|
+
const concepts = new Map(portableOntology.domains.types.concepts.map((concept) => [concept.id, concept.meaning]));
|
|
213
|
+
return {
|
|
214
|
+
status: 'ok',
|
|
215
|
+
source: loaded.source,
|
|
216
|
+
asOf,
|
|
217
|
+
ontology: { ...graph.ontology, currentVersion: ONTOLOGY_VERSION, upToDate: graph.ontology.version === ONTOLOGY_VERSION },
|
|
218
|
+
entityTypes: ENTITY_KINDS.map((kind) => ({
|
|
219
|
+
id: kind,
|
|
220
|
+
meaning: concepts.get(kind) ?? '',
|
|
221
|
+
count: graph.entities.filter((entity) => entity.type === kind).length
|
|
222
|
+
})),
|
|
223
|
+
relations: Object.values(canonicalRelationRegistry).map((definition) => {
|
|
224
|
+
const edges = graph.edges.filter((edge) => edge.relation === definition.id);
|
|
225
|
+
return {
|
|
226
|
+
id: definition.id,
|
|
227
|
+
from: definition.from,
|
|
228
|
+
to: definition.to,
|
|
229
|
+
meaning: definition.meaning,
|
|
230
|
+
count: edges.length,
|
|
231
|
+
activeCount: edges.filter((edge) => isActiveAt(edge, asOf)).length
|
|
232
|
+
};
|
|
233
|
+
}),
|
|
234
|
+
corrections: {
|
|
235
|
+
entityFields: GRAPH_CORRECTION_ENTITY_FIELDS,
|
|
236
|
+
projectFields: GRAPH_CORRECTION_PROJECT_FIELDS,
|
|
237
|
+
edgeFields: GRAPH_CORRECTION_EDGE_FIELDS,
|
|
238
|
+
newEdgeRelations: GRAPH_CORRECTION_NEW_EDGE_RELATIONS
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
// Owner-private snapshots
|
|
244
|
+
/** The source of every read view of an owner-private snapshot. */
|
|
245
|
+
export const GRAPH_WEB_OWNER_PRIVATE_SOURCE = Object.freeze({ dataDir: null, graphFormat: 2, authority: 'owner_private' });
|
|
246
|
+
const NO_REFERENCES = Object.freeze({
|
|
247
|
+
extracted: new Map(),
|
|
248
|
+
onboarding: new Map(),
|
|
249
|
+
history: [],
|
|
250
|
+
corrections: new Map(),
|
|
251
|
+
issues: []
|
|
252
|
+
});
|
|
253
|
+
/**
|
|
254
|
+
* Opens a portable bundle a host keeps for its owner, read as that owner
|
|
255
|
+
* (`bindPortableGraphOwner`: `self` becomes the owner's person ID and name).
|
|
256
|
+
*
|
|
257
|
+
* The bundle is checked with `validatePortableGraph` first
|
|
258
|
+
* (`portable_graph_invalid` otherwise). Its foundation is checked on its own
|
|
259
|
+
* (the Graph check ignores it): a foundation that does not pass fails only the
|
|
260
|
+
* objectives view. Nothing is written anywhere, and relation sources
|
|
261
|
+
* (candidate files, the onboarding ledger, corrections) stayed on the owner's
|
|
262
|
+
* machine, so a relation's source ID is shown unresolved.
|
|
263
|
+
*/
|
|
264
|
+
export function openOwnerPrivateGraph(bundle, owner) {
|
|
265
|
+
try {
|
|
266
|
+
validatePortableGraph(bundle);
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
throw new GraphWebError('unavailable', 'portable_graph_invalid', errorMessage(error));
|
|
270
|
+
}
|
|
271
|
+
let foundationError = null;
|
|
272
|
+
try {
|
|
273
|
+
assertFoundationCatalog(bundle.graph.foundation);
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
foundationError = errorMessage(error);
|
|
277
|
+
}
|
|
278
|
+
let bound;
|
|
279
|
+
try {
|
|
280
|
+
bound = bindPortableGraphOwner(bundle, owner);
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
throw new GraphWebError('unavailable', 'portable_graph_owner_conflict', errorMessage(error));
|
|
284
|
+
}
|
|
285
|
+
const graph = bound.graph;
|
|
286
|
+
const source = GRAPH_WEB_OWNER_PRIVATE_SOURCE;
|
|
287
|
+
const decisions = new Map(bound.decisions.map((record) => [record.id, record]));
|
|
288
|
+
return Object.freeze({
|
|
289
|
+
source,
|
|
290
|
+
listProjects: (options = {}) => projectListOf(graph, source, resolveAsOf(options)),
|
|
291
|
+
readProject: (projectId, options = {}) => projectDetailOf(graph, source, projectId, resolveAsOf(options), NO_REFERENCES),
|
|
292
|
+
search: (input = {}) => entitySearchOf(graph, source, parseSearchInput(input)),
|
|
293
|
+
readEntity: (entityId, options = {}) => {
|
|
294
|
+
const detail = entityDetailOf(graph, source, entityId, resolveAsOf(options), NO_REFERENCES);
|
|
295
|
+
if (detail.entity.type !== 'decision')
|
|
296
|
+
return detail;
|
|
297
|
+
const record = decisions.get(entityId);
|
|
298
|
+
return { ...detail, decisionRecord: record ? decisionRecordView(record) : null };
|
|
299
|
+
},
|
|
300
|
+
listObjectives: () => {
|
|
301
|
+
if (foundationError !== null)
|
|
302
|
+
throw new GraphWebError('unavailable', 'foundation_invalid', foundationError);
|
|
303
|
+
return objectiveListOf(graph, source);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
function decisionRecordView(record) {
|
|
308
|
+
return {
|
|
309
|
+
title: record.title,
|
|
310
|
+
decision: record.decision,
|
|
311
|
+
rationale: record.rationale ?? null,
|
|
312
|
+
topic: record.topic ?? null,
|
|
313
|
+
effectiveAt: record.effectiveAt ?? null,
|
|
314
|
+
supersedes: record.supersedes ?? []
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function objectiveListOf(graph, source) {
|
|
318
|
+
const catalog = graph.foundation;
|
|
319
|
+
if (!catalog)
|
|
320
|
+
return { status: 'ok', source, objectives: [], foundation: 'none', absenceConfirmed: true };
|
|
321
|
+
const entities = entityIndex(graph);
|
|
322
|
+
const person = (id) => ({ id, name: entities.get(id)?.name ?? null });
|
|
323
|
+
const objectives = Object.values(catalog.latest)
|
|
324
|
+
.filter((pointer) => pointer.type === 'objective')
|
|
325
|
+
.flatMap((pointer) => {
|
|
326
|
+
const definition = findFoundationRecord(catalog, pointer)?.definition;
|
|
327
|
+
if (!definition)
|
|
328
|
+
return [];
|
|
329
|
+
return [{
|
|
330
|
+
id: definition.id,
|
|
331
|
+
revision: definition.revision,
|
|
332
|
+
meaning: definition.meaning,
|
|
333
|
+
desiredState: definition.desiredState,
|
|
334
|
+
adoptionState: definition.adoptionState,
|
|
335
|
+
evaluationPeriod: { from: definition.evaluationPeriod.from, until: definition.evaluationPeriod.until },
|
|
336
|
+
criteria: definition.criteria.map((criterion) => {
|
|
337
|
+
const variable = findFoundationRecord(catalog, criterion.variableRef)?.definition;
|
|
338
|
+
return {
|
|
339
|
+
variable: { id: criterion.variableRef.id, revision: criterion.variableRef.revision, meaning: variable?.meaning ?? null, unit: variable?.unit ?? null },
|
|
340
|
+
operator: criterion.operator,
|
|
341
|
+
target: criterion.target ?? null
|
|
342
|
+
};
|
|
343
|
+
}),
|
|
344
|
+
accountable: definition.accountableId ? person(definition.accountableId) : null,
|
|
345
|
+
beneficiaries: definition.beneficiaryIds.map(person),
|
|
346
|
+
visibility: definition.acl.visibility
|
|
347
|
+
}];
|
|
348
|
+
})
|
|
349
|
+
.sort((left, right) => left.meaning.localeCompare(right.meaning, 'ja') || left.id.localeCompare(right.id, 'en'));
|
|
350
|
+
return { status: 'ok', source, objectives, foundation: 'ok', absenceConfirmed: objectives.length === 0 };
|
|
351
|
+
}
|
|
352
|
+
async function loadForRead(dataDir) {
|
|
353
|
+
if (typeof dataDir !== 'string' || dataDir.trim() === '')
|
|
354
|
+
throw new TypeError('dataDir is required');
|
|
355
|
+
const present = await Promise.all(CANONICAL_FILES.map((file) => exists(join(dataDir, file))));
|
|
356
|
+
if (!present.some(Boolean)) {
|
|
357
|
+
return {
|
|
358
|
+
status: 'not_initialized',
|
|
359
|
+
source: { dataDir, graphFormat: null, authority: 'local_graph' },
|
|
360
|
+
command: GRAPH_WEB_START_COMMAND,
|
|
361
|
+
message: 'No canonical Brainbase data exists in this data directory yet',
|
|
362
|
+
absenceConfirmed: true
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
let os;
|
|
366
|
+
try {
|
|
367
|
+
os = await loadPersonalOs(dataDir);
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
throw new GraphWebError('unavailable', 'graph_unavailable', error instanceof Error ? error.message : String(error));
|
|
371
|
+
}
|
|
372
|
+
if (os.graph.version !== 2) {
|
|
373
|
+
const dir = shellArg(dataDir);
|
|
374
|
+
return {
|
|
375
|
+
status: 'migration_required',
|
|
376
|
+
source: { dataDir, graphFormat: 1, authority: 'local_graph' },
|
|
377
|
+
legacyEntityCount: os.graph.entities.length,
|
|
378
|
+
command: `brainbase ontology:migrate --dir ${dir}`,
|
|
379
|
+
writeCommand: `brainbase ontology:migrate --dir ${dir} --write --expected-input-digest <preview expectedInputDigest>`,
|
|
380
|
+
message: 'graph.json is Graph v1. Migrate it to Graph v2 with the CLI; this host never migrates automatically',
|
|
381
|
+
absenceConfirmed: false
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
return { os, graph: os.graph, source: { dataDir, graphFormat: 2, authority: 'local_graph' } };
|
|
385
|
+
}
|
|
386
|
+
async function loadReferences(dataDir) {
|
|
387
|
+
const issues = [];
|
|
388
|
+
const extracted = new Map();
|
|
389
|
+
let candidateFiles = [];
|
|
390
|
+
try {
|
|
391
|
+
candidateFiles = (await readdir(join(dataDir, 'candidates'))).filter((file) => EXTRACTED_CANDIDATE_FILE.test(file)).sort();
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
if (errorCode(error) !== 'ENOENT')
|
|
395
|
+
issues.push({ file: 'candidates', reason: errorMessage(error) });
|
|
396
|
+
}
|
|
397
|
+
for (const name of candidateFiles) {
|
|
398
|
+
const file = `candidates/${name}`;
|
|
399
|
+
try {
|
|
400
|
+
const parsed = JSON.parse(await readFile(join(dataDir, file), 'utf8'));
|
|
401
|
+
const candidates = isRecord(parsed) && Array.isArray(parsed.candidates) ? parsed.candidates : undefined;
|
|
402
|
+
if (!candidates) {
|
|
403
|
+
issues.push({ file, reason: 'candidate file has no candidates array' });
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
for (const candidate of candidates) {
|
|
407
|
+
if (!isRecord(candidate) || typeof candidate.id !== 'string' || extracted.has(candidate.id))
|
|
408
|
+
continue;
|
|
409
|
+
extracted.set(candidate.id, { file, kind: stringOrNull(candidate.kind), label: payloadLabel(candidate.payload) });
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
issues.push({ file, reason: errorMessage(error) });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
const onboarding = new Map();
|
|
417
|
+
try {
|
|
418
|
+
const parsed = JSON.parse(await readFile(join(dataDir, ONBOARDING_LEDGER_FILE), 'utf8'));
|
|
419
|
+
if (!isRecord(parsed) || parsed.schemaVersion !== ONBOARDING_LEDGER_SCHEMA || !Array.isArray(parsed.runs)) {
|
|
420
|
+
issues.push({ file: ONBOARDING_LEDGER_FILE, reason: 'unsupported connected onboarding ledger schema' });
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
for (const run of parsed.runs) {
|
|
424
|
+
if (!isRecord(run) || typeof run.id !== 'string' || !Array.isArray(run.candidates))
|
|
425
|
+
continue;
|
|
426
|
+
for (const candidate of run.candidates) {
|
|
427
|
+
if (!isRecord(candidate) || typeof candidate.id !== 'string')
|
|
428
|
+
continue;
|
|
429
|
+
onboarding.set(candidate.id, {
|
|
430
|
+
runId: run.id,
|
|
431
|
+
kind: stringOrNull(candidate.kind),
|
|
432
|
+
reviewStatus: stringOrNull(candidate.reviewStatus),
|
|
433
|
+
label: payloadLabel(candidate.payload)
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
catch (error) {
|
|
440
|
+
if (errorCode(error) !== 'ENOENT')
|
|
441
|
+
issues.push({ file: ONBOARDING_LEDGER_FILE, reason: errorMessage(error) });
|
|
442
|
+
}
|
|
443
|
+
const history = await readGraphCorrectionHistory(dataDir);
|
|
444
|
+
issues.push(...history.issues);
|
|
445
|
+
return {
|
|
446
|
+
extracted,
|
|
447
|
+
onboarding,
|
|
448
|
+
history: history.records,
|
|
449
|
+
corrections: new Map(history.records.map((record) => [record.id, record])),
|
|
450
|
+
issues
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
function resolveReference(edge, references) {
|
|
454
|
+
const sourceId = edge.provenance?.sourceId;
|
|
455
|
+
if (!sourceId)
|
|
456
|
+
return { status: 'none' };
|
|
457
|
+
const correction = references.corrections.get(sourceId);
|
|
458
|
+
if (correction) {
|
|
459
|
+
return { status: 'resolved', kind: 'graph_correction', file: GRAPH_CORRECTIONS_FILE, correctionId: correction.id, at: correction.at, reason: correction.reason };
|
|
460
|
+
}
|
|
461
|
+
const run = references.onboarding.get(sourceId);
|
|
462
|
+
if (run) {
|
|
463
|
+
return {
|
|
464
|
+
status: 'resolved',
|
|
465
|
+
kind: 'onboarding_run_candidate',
|
|
466
|
+
file: ONBOARDING_LEDGER_FILE,
|
|
467
|
+
runId: run.runId,
|
|
468
|
+
candidateId: sourceId,
|
|
469
|
+
candidateKind: run.kind,
|
|
470
|
+
reviewStatus: run.reviewStatus,
|
|
471
|
+
label: run.label
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
const extracted = references.extracted.get(sourceId);
|
|
475
|
+
if (extracted) {
|
|
476
|
+
return { status: 'resolved', kind: 'extracted_candidate', file: extracted.file, candidateId: sourceId, candidateKind: extracted.kind, label: extracted.label };
|
|
477
|
+
}
|
|
478
|
+
return { status: 'unresolved', sourceId };
|
|
479
|
+
}
|
|
480
|
+
function incidentEdges(graph, entityId, asOf, entities, references) {
|
|
481
|
+
return graph.edges.flatMap((edge) => {
|
|
482
|
+
const views = [];
|
|
483
|
+
if (edge.fromId === entityId)
|
|
484
|
+
views.push(edgeView(edge, 'outgoing', entities.get(edge.toId), asOf, references));
|
|
485
|
+
if (edge.toId === entityId)
|
|
486
|
+
views.push(edgeView(edge, 'incoming', entities.get(edge.fromId), asOf, references));
|
|
487
|
+
return views;
|
|
488
|
+
}).sort((left, right) => Number(right.active) - Number(left.active)
|
|
489
|
+
|| left.relation.localeCompare(right.relation, 'en')
|
|
490
|
+
|| left.counterpart.name.localeCompare(right.counterpart.name, 'ja')
|
|
491
|
+
|| left.id.localeCompare(right.id, 'en'));
|
|
492
|
+
}
|
|
493
|
+
function edgeView(edge, direction, counterpart, asOf, references) {
|
|
494
|
+
if (!counterpart)
|
|
495
|
+
throw new GraphWebError('unavailable', 'graph_unavailable', `Relation ${edge.id} points to a missing entity`);
|
|
496
|
+
return {
|
|
497
|
+
id: edge.id,
|
|
498
|
+
relation: edge.relation,
|
|
499
|
+
meaning: getCanonicalRelation(edge.relation).meaning,
|
|
500
|
+
direction,
|
|
501
|
+
fromId: edge.fromId,
|
|
502
|
+
toId: edge.toId,
|
|
503
|
+
role: edge.role ?? null,
|
|
504
|
+
context: edge.context ?? null,
|
|
505
|
+
validFrom: edge.validFrom ?? null,
|
|
506
|
+
validTo: edge.validTo ?? null,
|
|
507
|
+
active: isActiveAt(edge, asOf),
|
|
508
|
+
provenance: {
|
|
509
|
+
sourceKind: edge.provenance?.sourceKind ?? null,
|
|
510
|
+
sourceId: edge.provenance?.sourceId ?? null,
|
|
511
|
+
evidenceHash: edge.provenance?.evidenceHash ?? null,
|
|
512
|
+
reference: resolveReference(edge, references)
|
|
513
|
+
},
|
|
514
|
+
counterpart: {
|
|
515
|
+
id: counterpart.id,
|
|
516
|
+
type: counterpart.type,
|
|
517
|
+
name: counterpart.name,
|
|
518
|
+
active: isActiveAt(counterpart, asOf),
|
|
519
|
+
digest: graphRecordDigest(counterpart)
|
|
520
|
+
},
|
|
521
|
+
digest: graphRecordDigest(edge)
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
function entityView(entity, asOf) {
|
|
525
|
+
return {
|
|
526
|
+
id: entity.id,
|
|
527
|
+
type: entity.type,
|
|
528
|
+
name: entity.name,
|
|
529
|
+
aliases: entity.aliases ?? [],
|
|
530
|
+
summary: entity.summary ?? null,
|
|
531
|
+
tags: entity.tags ?? [],
|
|
532
|
+
validFrom: entity.validFrom ?? null,
|
|
533
|
+
validTo: entity.validTo ?? null,
|
|
534
|
+
active: isActiveAt(entity, asOf),
|
|
535
|
+
digest: graphRecordDigest(entity),
|
|
536
|
+
...(entity.type === 'project' ? { goal: metadataString(entity, 'goal'), status: metadataString(entity, 'status') } : {})
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
function projectView(project, asOf) {
|
|
540
|
+
return { ...entityView(project, asOf), type: 'project', goal: metadataString(project, 'goal'), status: metadataString(project, 'status') };
|
|
541
|
+
}
|
|
542
|
+
function historyFor(records, ids) {
|
|
543
|
+
return records.filter((record) => ids.has(record.target.id)).reverse();
|
|
544
|
+
}
|
|
545
|
+
function matchRank(entity, needle) {
|
|
546
|
+
if (!needle)
|
|
547
|
+
return 3;
|
|
548
|
+
if (normalize(entity.id) === needle)
|
|
549
|
+
return 0;
|
|
550
|
+
let best;
|
|
551
|
+
for (const value of [entity.name, ...(entity.aliases ?? [])]) {
|
|
552
|
+
const candidate = normalize(value);
|
|
553
|
+
const rank = candidate === needle ? 0 : candidate.startsWith(needle) ? 1 : candidate.includes(needle) ? 2 : undefined;
|
|
554
|
+
if (rank !== undefined && (best === undefined || rank < best))
|
|
555
|
+
best = rank;
|
|
556
|
+
}
|
|
557
|
+
return best;
|
|
558
|
+
}
|
|
559
|
+
function parseEntityType(value) {
|
|
560
|
+
if (value === undefined || value === '')
|
|
561
|
+
return null;
|
|
562
|
+
if (!ENTITY_KINDS.includes(value)) {
|
|
563
|
+
throw new GraphWebError('invalid', 'invalid_query', `type must be one of ${ENTITY_KINDS.join(', ')}`);
|
|
564
|
+
}
|
|
565
|
+
return value;
|
|
566
|
+
}
|
|
567
|
+
function resolveAsOf(options) {
|
|
568
|
+
if (options.asOf !== undefined && options.asOf !== '')
|
|
569
|
+
return parseAsOf(options.asOf);
|
|
570
|
+
return (options.now ?? new Date()).toISOString();
|
|
571
|
+
}
|
|
572
|
+
function parseAsOf(value) {
|
|
573
|
+
try {
|
|
574
|
+
isActiveAt({}, value);
|
|
575
|
+
}
|
|
576
|
+
catch {
|
|
577
|
+
throw new GraphWebError('invalid', 'invalid_query', 'as_of must be an RFC 3339 date-time such as 2026-09-30T00:00:00Z');
|
|
578
|
+
}
|
|
579
|
+
return value;
|
|
580
|
+
}
|
|
581
|
+
function entityIndex(graph) {
|
|
582
|
+
return new Map(graph.entities.map((entity) => [entity.id, entity]));
|
|
583
|
+
}
|
|
584
|
+
function isEntityActive(entity, asOf) {
|
|
585
|
+
return entity !== undefined && isActiveAt(entity, asOf);
|
|
586
|
+
}
|
|
587
|
+
function byActiveThenName(left, right) {
|
|
588
|
+
return Number(right.active) - Number(left.active) || left.name.localeCompare(right.name, 'ja') || left.id.localeCompare(right.id, 'en');
|
|
589
|
+
}
|
|
590
|
+
function byParticipation(left, right) {
|
|
591
|
+
const order = (edge) => edge.relation === 'accountable_for' ? 0 : 1;
|
|
592
|
+
return Number(right.active) - Number(left.active)
|
|
593
|
+
|| order(left) - order(right)
|
|
594
|
+
|| left.counterpart.name.localeCompare(right.counterpart.name, 'ja')
|
|
595
|
+
|| left.id.localeCompare(right.id, 'en');
|
|
596
|
+
}
|
|
597
|
+
function metadataString(entity, key) {
|
|
598
|
+
const value = entity.metadata?.[key];
|
|
599
|
+
return typeof value === 'string' ? value : null;
|
|
600
|
+
}
|
|
601
|
+
function stringArray(value) {
|
|
602
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
|
|
603
|
+
}
|
|
604
|
+
function payloadLabel(payload) {
|
|
605
|
+
if (!isRecord(payload))
|
|
606
|
+
return null;
|
|
607
|
+
for (const key of ['name', 'person', 'title', 'text']) {
|
|
608
|
+
if (typeof payload[key] === 'string' && payload[key])
|
|
609
|
+
return payload[key];
|
|
610
|
+
}
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
function normalize(value) {
|
|
614
|
+
return value.normalize('NFKC').replace(/\s+/gu, '').toLowerCase();
|
|
615
|
+
}
|
|
616
|
+
function stringOrNull(value) {
|
|
617
|
+
return typeof value === 'string' ? value : null;
|
|
618
|
+
}
|
|
619
|
+
function isRecord(value) {
|
|
620
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
621
|
+
}
|
|
622
|
+
async function exists(path) {
|
|
623
|
+
try {
|
|
624
|
+
await access(path);
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
function errorCode(error) {
|
|
632
|
+
return typeof error === 'object' && error !== null && 'code' in error ? String(error.code) : undefined;
|
|
633
|
+
}
|
|
634
|
+
function errorMessage(error) {
|
|
635
|
+
return error instanceof Error ? error.message : String(error);
|
|
636
|
+
}
|
|
637
|
+
function shellArg(value) {
|
|
638
|
+
return /^[A-Za-z0-9_./:@%+=,-]+$/u.test(value) ? value : JSON.stringify(value);
|
|
639
|
+
}
|