@unson/brainbase-mcp 0.15.1 → 0.15.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/canonical-task-service.d.ts +11 -7
- package/dist/canonical-task-service.js +49 -21
- package/dist/cli.js +39 -3
- package/dist/graph-web-http.d.ts +7 -0
- package/dist/graph-web-http.js +14 -0
- package/dist/graph-web.d.ts +36 -6
- package/dist/graph-web.js +45 -4
- package/dist/judgment-value-proof-review.d.ts +24 -0
- package/dist/judgment-value-proof-review.js +120 -9
- package/dist/local-web-host.d.ts +36 -0
- package/dist/local-web-host.js +26 -7
- package/dist/organization-graph-web.d.ts +80 -0
- package/dist/organization-graph-web.js +309 -0
- package/dist/value-proof-review-http.d.ts +5 -0
- package/dist/value-proof-review-http.js +19 -7
- package/dist/world-extension.d.ts +183 -0
- package/dist/world-extension.js +361 -0
- package/package.json +9 -3
- package/ui/graph-projects-view.js +11 -4
- package/ui/graph-registry-view.js +14 -4
- package/ui/graph-view-shared.css +2 -0
- package/ui/graph-view-shared.js +58 -3
- package/ui/local-web-shell.js +74 -12
- package/ui/value-proof-review.js +67 -7
- package/ui/world/world-placement.js +45 -0
- package/ui/world/world-vendor.LICENSE.txt +23 -0
- package/ui/world/world-vendor.js +4220 -0
- package/ui/world/world-view.css +234 -0
- package/ui/world/world-view.js +1092 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { buildJudgmentValueProofReviewHome, readJudgmentValueProofAnswers, readJudgmentValueProofFeedback, readJudgmentValueProofJournal, } from './judgment-value-proof-review.js';
|
|
4
|
+
import { publicHome } from './value-proof-review-http.js';
|
|
5
|
+
import { projectOrganizationGraph, projectVocabularyTerms } from './organization-graph-web.js';
|
|
6
|
+
import { readLocalGraphState } from './local-web-host.js';
|
|
7
|
+
import { loadPersonalOs } from './ssot.js';
|
|
8
|
+
import { requestUrl, writeJson } from './local-web-security.js';
|
|
9
|
+
/**
|
|
10
|
+
* World view (ledger P17, adopted 2026-10-04): the home's upper layer.
|
|
11
|
+
*
|
|
12
|
+
* The world is a read-only projection. Businesses come from the same Graph
|
|
13
|
+
* the other screens read (the organization Graph under C1, else the local
|
|
14
|
+
* Graph); judgments come from the existing value-proof home on the browser
|
|
15
|
+
* side. Nothing here writes, and a failure to read is reported as a state,
|
|
16
|
+
* never as zero businesses.
|
|
17
|
+
*/
|
|
18
|
+
export const WORLD_EXTENSION_ID = 'world';
|
|
19
|
+
export const WORLD_EXTENSION_VERSION = 'world-extension.v1';
|
|
20
|
+
const LIFECYCLE_PHASES = ['active', 'maintenance', 'finished', 'concept'];
|
|
21
|
+
const BUILDING_FORMS = ['tower', 'hall', 'dome', 'office'];
|
|
22
|
+
/** Lifecycle words common to any Graph; a host vocabulary may relabel or add to them. */
|
|
23
|
+
const DEFAULT_STATUSES = Object.freeze({
|
|
24
|
+
active: { label: '進行中', phase: 'active' },
|
|
25
|
+
maintenance: { label: '保守', phase: 'maintenance' },
|
|
26
|
+
completed: { label: '完了', phase: 'finished' },
|
|
27
|
+
closed: { label: '終了', phase: 'finished' },
|
|
28
|
+
archived: { label: '保管', phase: 'finished' },
|
|
29
|
+
concept: { label: '構想', phase: 'concept' },
|
|
30
|
+
});
|
|
31
|
+
const KIND_PALETTE = ['#1261ad', '#b07a1f', '#35684c', '#6b4fa0', '#a8433a', '#2f7d86', '#7a6a2b', '#5b6470'];
|
|
32
|
+
const UNCLASSIFIED_COLOR = '#69746d';
|
|
33
|
+
const HEX_COLOR = /^#[0-9a-f]{6}$/iu;
|
|
34
|
+
function isRecord(value) {
|
|
35
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
36
|
+
}
|
|
37
|
+
function text(value) {
|
|
38
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
39
|
+
}
|
|
40
|
+
function engagementOf(entity) {
|
|
41
|
+
return {
|
|
42
|
+
id: entity.id,
|
|
43
|
+
code: text(entity.metadata?.code),
|
|
44
|
+
name: entity.name,
|
|
45
|
+
summary: text(entity.summary) ?? text(entity.metadata?.goal),
|
|
46
|
+
status: text(entity.metadata?.status),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The projects of a Graph v2 → cities (top-level projects) and their districts
|
|
51
|
+
* (projects whose `metadata.parent_project_id` names another project). A
|
|
52
|
+
* project whose declared parent (`metadata.parent_project_code`) is not in the
|
|
53
|
+
* Graph is counted as unplaced. Projects past `validTo` are counted, not drawn.
|
|
54
|
+
* Nothing here knows any organization's words: kinds and statuses are the
|
|
55
|
+
* Graph's own values.
|
|
56
|
+
*/
|
|
57
|
+
export function projectWorldFromGraph(graph, now = new Date()) {
|
|
58
|
+
const projects = graph.entities.filter((entity) => entity.type === 'project');
|
|
59
|
+
const active = projects.filter((entity) => !entity.validTo || Date.parse(entity.validTo) > now.getTime());
|
|
60
|
+
const byId = new Map(active.map((entity) => [entity.id, entity]));
|
|
61
|
+
// A district hangs on its top-level ancestor, so nested projects stay in one city.
|
|
62
|
+
const topOf = (entity) => {
|
|
63
|
+
let current = entity;
|
|
64
|
+
const seen = new Set();
|
|
65
|
+
for (;;) {
|
|
66
|
+
const parentId = text(current.metadata?.parent_project_id);
|
|
67
|
+
if (!parentId || parentId === current.id)
|
|
68
|
+
return current;
|
|
69
|
+
const parent = byId.get(parentId);
|
|
70
|
+
if (!parent || seen.has(parentId))
|
|
71
|
+
return parent ? current : null;
|
|
72
|
+
seen.add(parentId);
|
|
73
|
+
current = parent;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const cities = new Map();
|
|
77
|
+
const unplaced = [];
|
|
78
|
+
const children = [];
|
|
79
|
+
for (const entity of active) {
|
|
80
|
+
const parentId = text(entity.metadata?.parent_project_id);
|
|
81
|
+
if (parentId && parentId !== entity.id)
|
|
82
|
+
children.push(entity);
|
|
83
|
+
else if (text(entity.metadata?.parent_project_code))
|
|
84
|
+
unplaced.push(engagementOf(entity));
|
|
85
|
+
else
|
|
86
|
+
cities.set(entity.id, { entity, engagements: [] });
|
|
87
|
+
}
|
|
88
|
+
for (const entity of children) {
|
|
89
|
+
const top = topOf(entity);
|
|
90
|
+
const city = top ? cities.get(top.id) : undefined;
|
|
91
|
+
if (city && top.id !== entity.id)
|
|
92
|
+
city.engagements.push(engagementOf(entity));
|
|
93
|
+
else
|
|
94
|
+
unplaced.push(engagementOf(entity));
|
|
95
|
+
}
|
|
96
|
+
const businesses = [...cities.values()].map(({ entity, engagements }) => ({
|
|
97
|
+
id: entity.id,
|
|
98
|
+
code: text(entity.metadata?.code) ?? entity.id,
|
|
99
|
+
name: entity.name,
|
|
100
|
+
kind: text(entity.metadata?.kind),
|
|
101
|
+
purpose: text(entity.metadata?.goal) ?? text(entity.summary),
|
|
102
|
+
status: text(entity.metadata?.status),
|
|
103
|
+
repositories: Array.isArray(entity.metadata?.repositories)
|
|
104
|
+
? entity.metadata.repositories.map(text).filter((name) => name !== null)
|
|
105
|
+
: [],
|
|
106
|
+
engagements: engagements.sort((a, b) => a.name.localeCompare(b.name, 'ja')),
|
|
107
|
+
}));
|
|
108
|
+
return {
|
|
109
|
+
businesses: businesses.sort((a, b) => a.name.localeCompare(b.name, 'ja')),
|
|
110
|
+
unplaced,
|
|
111
|
+
excluded: { inactive: projects.length - active.length },
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The vocabulary the world draws with: the host's kinds in their order, then
|
|
116
|
+
* kinds the Graph uses that the host did not name (under their own value),
|
|
117
|
+
* then unclassified projects; statuses are the common lifecycle words with the
|
|
118
|
+
* host's on top.
|
|
119
|
+
*/
|
|
120
|
+
export function resolveWorldVocabulary(config, businesses, terms = undefined) {
|
|
121
|
+
// The organization names its kinds in the Graph; the host's file only adds a look (form, colour).
|
|
122
|
+
const graphTerms = new Map((terms ?? []).filter((term) => term.field === 'project.kind').map((term) => [term.value, term]));
|
|
123
|
+
const named = (key, configLabel) => {
|
|
124
|
+
const term = graphTerms.get(key);
|
|
125
|
+
if (term)
|
|
126
|
+
return { label: term.label, label_source: 'graph', definition: term.definition };
|
|
127
|
+
const label = text(configLabel);
|
|
128
|
+
return label ? { label, label_source: 'config', definition: null } : { label: key, label_source: 'value', definition: null };
|
|
129
|
+
};
|
|
130
|
+
const configured = Object.entries(config?.kinds ?? {});
|
|
131
|
+
const used = new Set(businesses.map((business) => business.kind));
|
|
132
|
+
const kinds = configured.map(([key, entry], index) => ({
|
|
133
|
+
key,
|
|
134
|
+
...named(key, entry.label),
|
|
135
|
+
form: entry.form && BUILDING_FORMS.includes(entry.form) ? entry.form : 'office',
|
|
136
|
+
color: entry.color && HEX_COLOR.test(entry.color) ? entry.color : KIND_PALETTE[index % KIND_PALETTE.length],
|
|
137
|
+
configured: true,
|
|
138
|
+
}));
|
|
139
|
+
const configuredKeys = new Set(configured.map(([key]) => key));
|
|
140
|
+
[...used].filter((key) => key !== null && !configuredKeys.has(key)).sort().forEach((key) => {
|
|
141
|
+
kinds.push({ key, ...named(key, undefined), form: 'office', color: KIND_PALETTE[kinds.length % KIND_PALETTE.length], configured: false });
|
|
142
|
+
});
|
|
143
|
+
if (used.has(null))
|
|
144
|
+
kinds.push({ key: null, label: '分類なし', label_source: 'value', definition: null, form: 'office', color: UNCLASSIFIED_COLOR, configured: false });
|
|
145
|
+
const statuses = new Map(Object.entries(DEFAULT_STATUSES));
|
|
146
|
+
for (const [key, entry] of Object.entries(config?.statuses ?? {})) {
|
|
147
|
+
const base = statuses.get(key);
|
|
148
|
+
statuses.set(key, {
|
|
149
|
+
label: text(entry.label) ?? base?.label ?? key,
|
|
150
|
+
phase: entry.phase && LIFECYCLE_PHASES.includes(entry.phase) ? entry.phase : base?.phase ?? 'active',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return { kinds, statuses: [...statuses].map(([key, entry]) => ({ key, ...entry })), terms: terms === undefined ? 'none' : terms === null ? 'unavailable' : 'read' };
|
|
154
|
+
}
|
|
155
|
+
/** Reads and checks a vocabulary file; an invalid file is an error, never silently ignored. */
|
|
156
|
+
export async function readWorldVocabulary(path) {
|
|
157
|
+
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
158
|
+
if (!isRecord(parsed))
|
|
159
|
+
throw new Error('world vocabulary must be a JSON object');
|
|
160
|
+
const kinds = {};
|
|
161
|
+
for (const [key, entry] of Object.entries(isRecord(parsed.kinds) ? parsed.kinds : {})) {
|
|
162
|
+
if (!isRecord(entry))
|
|
163
|
+
throw new Error(`world vocabulary kind ${key} must be an object`);
|
|
164
|
+
if (entry.form !== undefined && !BUILDING_FORMS.includes(entry.form))
|
|
165
|
+
throw new Error(`world vocabulary kind ${key} has an unknown form`);
|
|
166
|
+
if (entry.color !== undefined && !(typeof entry.color === 'string' && HEX_COLOR.test(entry.color)))
|
|
167
|
+
throw new Error(`world vocabulary kind ${key} has an invalid color`);
|
|
168
|
+
kinds[key] = { ...(text(entry.label) ? { label: text(entry.label) } : {}), ...(entry.form ? { form: entry.form } : {}), ...(entry.color ? { color: entry.color } : {}) };
|
|
169
|
+
}
|
|
170
|
+
const statuses = {};
|
|
171
|
+
for (const [key, entry] of Object.entries(isRecord(parsed.statuses) ? parsed.statuses : {})) {
|
|
172
|
+
if (!isRecord(entry))
|
|
173
|
+
throw new Error(`world vocabulary status ${key} must be an object`);
|
|
174
|
+
if (entry.phase !== undefined && !LIFECYCLE_PHASES.includes(entry.phase))
|
|
175
|
+
throw new Error(`world vocabulary status ${key} has an unknown phase`);
|
|
176
|
+
statuses[key] = { ...(text(entry.label) ? { label: text(entry.label) } : {}), ...(entry.phase ? { phase: entry.phase } : {}) };
|
|
177
|
+
}
|
|
178
|
+
return { kinds, statuses };
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The businesses from the same Graph the other screens read: the organization
|
|
182
|
+
* Graph when the host reads it (C1), else the local Graph. A Graph that cannot
|
|
183
|
+
* be read is a state with its reason, never zero businesses.
|
|
184
|
+
*/
|
|
185
|
+
export async function readWorldBusinesses(context, options = {}) {
|
|
186
|
+
const now = (options.now ?? (() => new Date()))();
|
|
187
|
+
let graph;
|
|
188
|
+
let source;
|
|
189
|
+
let terms;
|
|
190
|
+
try {
|
|
191
|
+
if (context.organizationGraph) {
|
|
192
|
+
if (!context.organizationGraph.readGraphFile) {
|
|
193
|
+
return { status: 'unavailable', version: WORLD_EXTENSION_VERSION, reason: 'organization_graph_records_unavailable' };
|
|
194
|
+
}
|
|
195
|
+
graph = await context.organizationGraph.readGraphFile();
|
|
196
|
+
source = { authority: 'organization_graph', server: context.organizationGraph.server };
|
|
197
|
+
terms = context.organizationGraph.readVocabularyTerms ? await context.organizationGraph.readVocabularyTerms().catch(() => null) : null;
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
const state = await readLocalGraphState(context.dataDir);
|
|
201
|
+
if (state.status === 'not_initialized' || state.status === 'migration_required') {
|
|
202
|
+
return { status: state.status, version: WORLD_EXTENSION_VERSION, reason: state.status };
|
|
203
|
+
}
|
|
204
|
+
if (state.status !== 'ready')
|
|
205
|
+
return { status: 'unavailable', version: WORLD_EXTENSION_VERSION, reason: state.message ?? 'local_graph_unreadable' };
|
|
206
|
+
const os = await loadPersonalOs(context.dataDir);
|
|
207
|
+
if (os.graph.version !== 2)
|
|
208
|
+
return { status: 'migration_required', version: WORLD_EXTENSION_VERSION, reason: 'migration_required' };
|
|
209
|
+
graph = os.graph;
|
|
210
|
+
source = { authority: 'local', dataDir: context.dataDir };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
return { status: 'unavailable', version: WORLD_EXTENSION_VERSION, reason: error instanceof Error ? error.message : 'graph_unreadable' };
|
|
215
|
+
}
|
|
216
|
+
const projection = projectWorldFromGraph(graph, now);
|
|
217
|
+
return {
|
|
218
|
+
status: 'ok',
|
|
219
|
+
version: WORLD_EXTENSION_VERSION,
|
|
220
|
+
source,
|
|
221
|
+
as_of: now.toISOString(),
|
|
222
|
+
vocabulary: resolveWorldVocabulary(options.vocabulary, projection.businesses, terms),
|
|
223
|
+
...projection,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* The world of an organization from the project and glossary-term records its Graph API returns, for an
|
|
228
|
+
* organization web that hosts the same world screen (P16). Pure: the caller reads the records with the
|
|
229
|
+
* member's own grants, so the world shows only what that member may read.
|
|
230
|
+
*/
|
|
231
|
+
export function projectOrganizationWorld(records, options) {
|
|
232
|
+
const now = options.now ?? new Date();
|
|
233
|
+
const { graph } = projectOrganizationGraph({
|
|
234
|
+
entities: { person: [], org: [], project: records.projects, decision: [], raci_assignment: [] },
|
|
235
|
+
memberOf: [],
|
|
236
|
+
assignedTo: [],
|
|
237
|
+
});
|
|
238
|
+
const projection = projectWorldFromGraph(graph, now);
|
|
239
|
+
return {
|
|
240
|
+
status: 'ok',
|
|
241
|
+
version: WORLD_EXTENSION_VERSION,
|
|
242
|
+
source: { authority: 'organization_graph', server: options.server },
|
|
243
|
+
as_of: now.toISOString(),
|
|
244
|
+
vocabulary: resolveWorldVocabulary(options.vocabulary, projection.businesses, projectVocabularyTerms(records.glossaryTerms ?? [])),
|
|
245
|
+
...projection,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const VALUE_PROOF_SUFFIX = '.value-proof.json';
|
|
249
|
+
/**
|
|
250
|
+
* Reads, for every saved value proof in the judgment journal, the repository
|
|
251
|
+
* the Host recorded for that turn. The journal is listed through the host's
|
|
252
|
+
* shared listing cache (the same one as the 今日 list), so only folders that
|
|
253
|
+
* changed are listed again; turn inputs are never rewritten by the Host, so
|
|
254
|
+
* each is read once. A journal that cannot be read is `unavailable`, never
|
|
255
|
+
* an empty list.
|
|
256
|
+
*/
|
|
257
|
+
export async function readJudgmentPlaces(journalRoot, options = {}) {
|
|
258
|
+
const journal = await readJudgmentValueProofJournal({ root: journalRoot, ...(options.cache ? { cache: options.cache } : {}) });
|
|
259
|
+
if (journal.status !== 'available')
|
|
260
|
+
return { status: 'unavailable', places: [], reason: 'journal_unreadable' };
|
|
261
|
+
const workspaces = options.workspaces ?? new Map();
|
|
262
|
+
const places = await Promise.all(journal.entries.map(async (entry) => {
|
|
263
|
+
const turnInputFile = `${entry.file.slice(0, -VALUE_PROOF_SUFFIX.length)}.turn-input.json`;
|
|
264
|
+
if (!workspaces.has(turnInputFile)) {
|
|
265
|
+
const turnInput = await readJsonFile(turnInputFile);
|
|
266
|
+
workspaces.set(turnInputFile, isRecord(turnInput) ? text(turnInput.project_code) : null);
|
|
267
|
+
}
|
|
268
|
+
return { decision_attempt_id: entry.proof.decision_attempt_id, workspace: workspaces.get(turnInputFile) ?? null };
|
|
269
|
+
}));
|
|
270
|
+
return { status: 'available', places };
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* The judgments of one organization, for a world drawn from that organization's Graph. A judgment
|
|
274
|
+
* belongs to the organization only when the repository it was worked in is one of the organization's
|
|
275
|
+
* businesses (a registered repository or the business code, the same rule as the world's placement).
|
|
276
|
+
* Every other judgment (another company's work, or one whose repository was not recorded) is withheld:
|
|
277
|
+
* only their count leaves the host, never their questions or repositories.
|
|
278
|
+
*/
|
|
279
|
+
export async function readOrganizationJudgments(context, businesses, workspaces = new Map()) {
|
|
280
|
+
const journal = await readJudgmentValueProofJournal({ root: context.journalRoot, cache: context.journalCache });
|
|
281
|
+
if (journal.status !== 'available')
|
|
282
|
+
return { status: 'unavailable', scope: 'organization', withheld: 0, places: [], home: null, reason: 'journal_unreadable' };
|
|
283
|
+
const belongs = (workspace) => workspace !== null
|
|
284
|
+
&& businesses.some((business) => business.repositories.includes(workspace) || business.code === workspace);
|
|
285
|
+
const kept = [];
|
|
286
|
+
const places = [];
|
|
287
|
+
for (const entry of journal.entries) {
|
|
288
|
+
const workspace = await entryWorkspace(entry.file, workspaces);
|
|
289
|
+
if (!belongs(workspace))
|
|
290
|
+
continue;
|
|
291
|
+
kept.push(entry);
|
|
292
|
+
places.push({ decision_attempt_id: entry.proof.decision_attempt_id, workspace });
|
|
293
|
+
}
|
|
294
|
+
const [feedback, answers] = await Promise.all([
|
|
295
|
+
readJudgmentValueProofFeedback({ dataDir: context.dataDir }),
|
|
296
|
+
readJudgmentValueProofAnswers({ dataDir: context.dataDir }),
|
|
297
|
+
]);
|
|
298
|
+
// Rejected files are left out too: their names would say nothing about the organization they belong to.
|
|
299
|
+
const home = buildJudgmentValueProofReviewHome({ ...journal, entries: kept, rejected: [] }, feedback, { now: context.now(), answers });
|
|
300
|
+
return { status: 'available', scope: 'organization', withheld: journal.entries.length - kept.length, places, home: publicHome(home) };
|
|
301
|
+
}
|
|
302
|
+
async function entryWorkspace(valueProofFile, workspaces) {
|
|
303
|
+
const turnInputFile = `${valueProofFile.slice(0, -VALUE_PROOF_SUFFIX.length)}.turn-input.json`;
|
|
304
|
+
if (!workspaces.has(turnInputFile)) {
|
|
305
|
+
const turnInput = await readJsonFile(turnInputFile);
|
|
306
|
+
workspaces.set(turnInputFile, isRecord(turnInput) ? text(turnInput.project_code) : null);
|
|
307
|
+
}
|
|
308
|
+
return workspaces.get(turnInputFile) ?? null;
|
|
309
|
+
}
|
|
310
|
+
async function readJsonFile(path) {
|
|
311
|
+
try {
|
|
312
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function createWorldModule(context, read) {
|
|
319
|
+
const workspaces = new Map();
|
|
320
|
+
return {
|
|
321
|
+
id: WORLD_EXTENSION_ID,
|
|
322
|
+
uiFiles: [],
|
|
323
|
+
async handle(request, response) {
|
|
324
|
+
if (request.method !== 'GET')
|
|
325
|
+
return false;
|
|
326
|
+
const path = requestUrl(request).pathname;
|
|
327
|
+
if (path === `/api/extensions/${WORLD_EXTENSION_ID}/businesses`) {
|
|
328
|
+
response.setHeader('Cache-Control', 'no-store');
|
|
329
|
+
writeJson(response, 200, await read());
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
if (path === `/api/extensions/${WORLD_EXTENSION_ID}/organization-judgments`) {
|
|
333
|
+
response.setHeader('Cache-Control', 'no-store');
|
|
334
|
+
const world = await read();
|
|
335
|
+
if (world.status !== 'ok' || world.source.authority !== 'organization_graph') {
|
|
336
|
+
writeJson(response, 200, { version: WORLD_EXTENSION_VERSION, status: 'unavailable', scope: 'organization', withheld: 0, places: [], home: null, reason: 'not_reading_organization_graph' });
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
writeJson(response, 200, { version: WORLD_EXTENSION_VERSION, ...(await readOrganizationJudgments(context, world.businesses, workspaces)) });
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
if (path === `/api/extensions/${WORLD_EXTENSION_ID}/judgment-places`) {
|
|
343
|
+
response.setHeader('Cache-Control', 'no-store');
|
|
344
|
+
writeJson(response, 200, { version: WORLD_EXTENSION_VERSION, ...(await readJudgmentPlaces(context.journalRoot, { cache: context.journalCache, workspaces })) });
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
return false;
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
export function createWorldExtension(options = {}) {
|
|
352
|
+
return {
|
|
353
|
+
id: WORLD_EXTENSION_ID,
|
|
354
|
+
uiDir: fileURLToPath(new URL('../ui/world/', import.meta.url)),
|
|
355
|
+
uiFiles: ['world-view.js', 'world-view.css', 'world-vendor.js', 'world-placement.js'],
|
|
356
|
+
screenEntry: 'world-view.js',
|
|
357
|
+
// P17: the world sits above the home and opens first.
|
|
358
|
+
navPosition: 'first',
|
|
359
|
+
createModule: (context) => createWorldModule(context, () => readWorldBusinesses(context, { vocabulary: options.vocabulary, now: context.now })),
|
|
360
|
+
};
|
|
361
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unson/brainbase-mcp",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.3",
|
|
4
4
|
"description": "Local-first Brainbase judgment OS that lets supported AI agents reuse a user's approved goals, priorities, decisions, and judgment criteria.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -147,6 +147,10 @@
|
|
|
147
147
|
"types": "./dist/world-model.d.ts",
|
|
148
148
|
"import": "./dist/world-model.js"
|
|
149
149
|
},
|
|
150
|
+
"./world": {
|
|
151
|
+
"types": "./dist/world-extension.d.ts",
|
|
152
|
+
"import": "./dist/world-extension.js"
|
|
153
|
+
},
|
|
150
154
|
"./problem-selection": {
|
|
151
155
|
"types": "./dist/problem-selection.d.ts",
|
|
152
156
|
"import": "./dist/problem-selection.js"
|
|
@@ -329,7 +333,8 @@
|
|
|
329
333
|
"release:validate": "node scripts/npm-release.mjs validate",
|
|
330
334
|
"release:publish": "node scripts/npm-release.mjs publish",
|
|
331
335
|
"release:verify": "node scripts/npm-release.mjs verify",
|
|
332
|
-
"build:graph": "node scripts/build-project-graph.mjs"
|
|
336
|
+
"build:graph": "node scripts/build-project-graph.mjs",
|
|
337
|
+
"build:world": "node scripts/build-world-vendor.mjs"
|
|
333
338
|
},
|
|
334
339
|
"dependencies": {
|
|
335
340
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
@@ -344,6 +349,7 @@
|
|
|
344
349
|
"@electric-sql/pglite": "^0.5.8",
|
|
345
350
|
"@types/node": "^22.15.29",
|
|
346
351
|
"esbuild": "0.28.2",
|
|
352
|
+
"three": "0.186.1",
|
|
347
353
|
"tsx": "^4.19.4",
|
|
348
354
|
"typescript": "^5.8.3",
|
|
349
355
|
"vitepress": "^1.6.4",
|
|
@@ -352,5 +358,5 @@
|
|
|
352
358
|
"engines": {
|
|
353
359
|
"node": ">=20"
|
|
354
360
|
},
|
|
355
|
-
"gitHead": "
|
|
361
|
+
"gitHead": "bbcd694ef8e7231d88c1522d95c701421c9c5afa"
|
|
356
362
|
}
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
graphPageContext,
|
|
35
35
|
graphStateNotice,
|
|
36
36
|
hostReadOnlyNote,
|
|
37
|
+
hostSourceNotice,
|
|
37
38
|
isEdgeView,
|
|
38
39
|
isEntityView,
|
|
39
40
|
isRecord,
|
|
@@ -188,7 +189,8 @@ function normalizeProjectSummary(value) {
|
|
|
188
189
|
* プロジェクトを直す, `fields` limits its form, `edges: false` withholds 役割を直す and 関わりを終える /
|
|
189
190
|
* 終了日を直す, and `createEdges: false` withholds 関係者を加える. A withheld control is not drawn and the
|
|
190
191
|
* client refuses such a correction before sending it. Without it every correction is allowed.
|
|
191
|
-
* @param {string
|
|
192
|
+
* @param {string | ((record: { id: string, type: string | null, metadata: object } | null) => string | Element | null)} [options.readOnlyNote]
|
|
193
|
+
* With `canCorrect: false`, or where `correctionScope` withholds a
|
|
192
194
|
* control of the selected project, a note shown in the rail where the correction buttons would be.
|
|
193
195
|
* Without it the rail says nothing about corrections.
|
|
194
196
|
* @param {{ label?: string, text: string | Element }} [options.emptyNotice] Replaces the 未登録 notice
|
|
@@ -506,7 +508,10 @@ export function createGraphProjectsView({
|
|
|
506
508
|
const legacyChrome = [pageHeader];
|
|
507
509
|
const children = [pageHeader];
|
|
508
510
|
const dir = state.list.state === 'ok' ? textOrNull(state.list.payload.source?.dataDir) : null;
|
|
509
|
-
|
|
511
|
+
// The record a host's 出典 notice may point at: the selected project once its detail is read.
|
|
512
|
+
const sourceDetail = state.detail?.id === state.selectedId && state.detail.state === 'ok' ? state.detail.payload.project : null;
|
|
513
|
+
const sourceSubject = sourceDetail ? { id: sourceDetail.id, type: 'project', metadata: sourceDetail.metadata ?? {} } : null;
|
|
514
|
+
const sourceBlock = workspaceHostNotice(doc, hostSourceNotice(sourceNotice, sourceSubject)) ?? workspaceNotice(doc, {
|
|
510
515
|
label: '出典',
|
|
511
516
|
text: `このMacのGraph${dir ? `(${dir})` : ''}から読み、ここで直した内容もそこへ保存します。関係者は、プロジェクトに参加している人や責任を持つ人の記録で、ログインや共有の設定ではありません。`,
|
|
512
517
|
});
|
|
@@ -714,6 +719,8 @@ export function createGraphProjectsView({
|
|
|
714
719
|
const detail = state.detail;
|
|
715
720
|
const loaded = detail?.id === state.selectedId && detail.state === 'ok' ? detail.payload : null;
|
|
716
721
|
const project = loaded?.project ?? listItem(state.selectedId) ?? { id: state.selectedId, name: state.selectedId };
|
|
722
|
+
// The record a host's read-only note may point at.
|
|
723
|
+
const noteSubject = { id: project.id, type: 'project', metadata: loaded?.project?.metadata ?? {} };
|
|
717
724
|
const children = [workspaceRailHead(doc, { kicker: 'プロジェクト', title: project.name, sub: project.id })];
|
|
718
725
|
if (!loaded) {
|
|
719
726
|
const readState = detail?.id === state.selectedId
|
|
@@ -776,9 +783,9 @@ export function createGraphProjectsView({
|
|
|
776
783
|
if (!loaded.participants.some((edge) => edge.id === state.panelAt)) children.push(correction.render(doc));
|
|
777
784
|
// The host's reason for a control it withholds, where that control would be.
|
|
778
785
|
const withheld = !canAdd || projectFields.length === 0 || (!scope.edges && loaded.participants.length > 0);
|
|
779
|
-
if (withheld) children.push(hostReadOnlyNote(doc, readOnlyNote));
|
|
786
|
+
if (withheld) children.push(hostReadOnlyNote(doc, readOnlyNote, noteSubject));
|
|
780
787
|
} else {
|
|
781
|
-
children.push(hostReadOnlyNote(doc, readOnlyNote));
|
|
788
|
+
children.push(hostReadOnlyNote(doc, readOnlyNote, noteSubject));
|
|
782
789
|
}
|
|
783
790
|
children.push(...railExtensions(state.selectedId, loaded));
|
|
784
791
|
children.push(renderOtherRelations(loaded));
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
graphPageContext,
|
|
36
36
|
graphStateNotice,
|
|
37
37
|
hostReadOnlyNote,
|
|
38
|
+
hostSourceNotice,
|
|
38
39
|
isEdgeView,
|
|
39
40
|
isEntityView,
|
|
40
41
|
isRecord,
|
|
@@ -173,7 +174,8 @@ function textCell(value, fallback) {
|
|
|
173
174
|
* offer この記録を直す, `fields` limits its form, `edges: false` withholds この関係を直す and
|
|
174
175
|
* `createEdges: false` withholds 関係を加える. A withheld control is not drawn and the client refuses
|
|
175
176
|
* such a correction before sending it. Without it every correction is allowed.
|
|
176
|
-
* @param {string
|
|
177
|
+
* @param {string | ((record: { id: string, type: string | null, metadata: object } | null) => string | Element | null)} [options.readOnlyNote]
|
|
178
|
+
* With `canCorrect: false`, or where `correctionScope` withholds a
|
|
177
179
|
* control of the selected record, a note shown in the rail where the correction buttons would be.
|
|
178
180
|
* Without it the rail says nothing about corrections.
|
|
179
181
|
* @param {{ label?: string, text: string | Element }} [options.emptyNotice] Replaces the 未登録 notice
|
|
@@ -443,6 +445,12 @@ export function createGraphRegistryView({
|
|
|
443
445
|
return section;
|
|
444
446
|
}
|
|
445
447
|
|
|
448
|
+
/** The record a host's 出典 notice may point at: the selected record once it is read. */
|
|
449
|
+
function sourceSubject() {
|
|
450
|
+
const entity = state.detail?.state === 'ok' ? state.detail.payload.entity : null;
|
|
451
|
+
return entity ? { id: entity.id, type: entity.type, metadata: entity.metadata ?? {} } : null;
|
|
452
|
+
}
|
|
453
|
+
|
|
446
454
|
function renderWorkspace() {
|
|
447
455
|
const dir = state.search.state === 'ok' ? textOrNull(state.search.payload.source?.dataDir) : null;
|
|
448
456
|
return [
|
|
@@ -452,7 +460,7 @@ export function createGraphRegistryView({
|
|
|
452
460
|
lead: writable ? 'Graphに何がどう登録されているかを確かめ、誤りを直します。' : 'Graphに何がどう登録されているかを確かめます。',
|
|
453
461
|
source: context.source,
|
|
454
462
|
}),
|
|
455
|
-
workspaceHostNotice(doc, sourceNotice) ?? workspaceNotice(doc, {
|
|
463
|
+
workspaceHostNotice(doc, hostSourceNotice(sourceNotice, sourceSubject())) ?? workspaceNotice(doc, {
|
|
456
464
|
label: '出典',
|
|
457
465
|
text: `このMacのGraph${dir ? `(${dir})` : ''}だけを表示します。ここで直した内容は、同じGraphを読むMCPの search・get_context・resolve_entity で次から使われます。`,
|
|
458
466
|
}),
|
|
@@ -601,6 +609,8 @@ export function createGraphRegistryView({
|
|
|
601
609
|
children.push(graphStateNotice(doc, detail, { onRetry: () => controller.loadEntity(detail.id), loadingText: 'この記録を読み込んでいます。' }));
|
|
602
610
|
return children;
|
|
603
611
|
}
|
|
612
|
+
// The record a host's read-only note may point at.
|
|
613
|
+
const noteSubject = { id: entity.id, type: entity.type, metadata: loaded.entity?.metadata ?? {} };
|
|
604
614
|
const summaryRows = [
|
|
605
615
|
['別名', entity.aliases.length > 0 ? entity.aliases.join('、') : { text: 'なし', className: 'is-unrecorded' }],
|
|
606
616
|
['要約', textOrNull(entity.summary)],
|
|
@@ -647,9 +657,9 @@ export function createGraphRegistryView({
|
|
|
647
657
|
const inRelation = [...loaded.outgoing, ...loaded.incoming].some((edge) => edge.id === state.panelAt);
|
|
648
658
|
if (!inRelation) children.push(correction.render(doc));
|
|
649
659
|
// The host's reason for a control it withholds, where that control would be.
|
|
650
|
-
if (!canFix || (relatable && !scope.createEdges) || (related && !scope.edges)) children.push(hostReadOnlyNote(doc, readOnlyNote));
|
|
660
|
+
if (!canFix || (relatable && !scope.createEdges) || (related && !scope.edges)) children.push(hostReadOnlyNote(doc, readOnlyNote, noteSubject));
|
|
651
661
|
} else {
|
|
652
|
-
children.push(hostReadOnlyNote(doc, readOnlyNote));
|
|
662
|
+
children.push(hostReadOnlyNote(doc, readOnlyNote, noteSubject));
|
|
653
663
|
}
|
|
654
664
|
children.push(renderRelations(loaded, 'outgoing'), renderRelations(loaded, 'incoming'));
|
|
655
665
|
children.push(renderCorrectionHistory(doc, loaded.history));
|
package/ui/graph-view-shared.css
CHANGED
package/ui/graph-view-shared.js
CHANGED
|
@@ -459,9 +459,64 @@ function withinCorrectionScope(scope, body, recordType) {
|
|
|
459
459
|
return scope.entity(recordType) && scope.fields(changed).length === changed.length;
|
|
460
460
|
}
|
|
461
461
|
|
|
462
|
-
/**
|
|
463
|
-
|
|
464
|
-
|
|
462
|
+
/**
|
|
463
|
+
* A host's 出典 notice with its text resolved for the selected record: the host may pass `text` as a
|
|
464
|
+
* function of that record (`{ id, type, metadata }`, or null before one is read), for a notice that points
|
|
465
|
+
* at it. Returns the notice for `workspaceHostNotice`, or the value it was given.
|
|
466
|
+
*/
|
|
467
|
+
export function hostSourceNotice(sourceNotice, subject) {
|
|
468
|
+
if (!sourceNotice || typeof sourceNotice.text !== 'function') return sourceNotice;
|
|
469
|
+
return { ...sourceNotice, text: sourceNotice.text(subject ?? null) };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ---------------------------------------------------------------------------
|
|
473
|
+
// The organization's web (Inspector UX-20261004-01)
|
|
474
|
+
|
|
475
|
+
const ORGANIZATION_RECORD_ID = /^[A-Za-z0-9_.:-]{1,200}$/u;
|
|
476
|
+
|
|
477
|
+
/** The organization web's origin the host was told about, or null. */
|
|
478
|
+
export function organizationWebOrigin(organizationGraph) {
|
|
479
|
+
if (organizationGraph?.status !== 'connected') return null;
|
|
480
|
+
const origin = organizationGraph.web_url;
|
|
481
|
+
return typeof origin === 'string' && /^https:\/\/[^/?#@\s]+$/u.test(origin) ? origin : null;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Where a record opens in the organization web, `{ href, label }`, or null when it has no usable id. The
|
|
486
|
+
* organization web opens 情報と関係 by record id and プロジェクトと関係者 by project code, and keeps that record
|
|
487
|
+
* across sign-in. A project opens by its own code only: a sub-project's scope code is its parent's and
|
|
488
|
+
* would open the parent, so without its own code the record opens by id.
|
|
489
|
+
*/
|
|
490
|
+
export function organizationRecordLink(origin, screenId, record) {
|
|
491
|
+
const id = typeof record?.id === 'string' && ORGANIZATION_RECORD_ID.test(record.id) ? record.id : null;
|
|
492
|
+
const own = typeof record?.metadata?.code === 'string' ? record.metadata.code.trim() : '';
|
|
493
|
+
const code = own && ORGANIZATION_RECORD_ID.test(own) ? own : null;
|
|
494
|
+
if (screenId === 'projects' && record?.type === 'project' && code) {
|
|
495
|
+
return { href: `${origin}/?screen=projects&project=${encodeURIComponent(code)}`, label: '「プロジェクトと関係者」' };
|
|
496
|
+
}
|
|
497
|
+
return id ? { href: `${origin}/?screen=graph&entity_id=${encodeURIComponent(id)}`, label: '「情報と関係」' } : null;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export const ORGANIZATION_RECORD_LINK_TEXT = '組織版でこの記録を開く';
|
|
501
|
+
|
|
502
|
+
/** A link that opens `target` from organizationRecordLink in a new tab. */
|
|
503
|
+
export function organizationRecordAnchor(doc, target, className) {
|
|
504
|
+
return makeElement(doc, 'a', {
|
|
505
|
+
...(className ? { className } : {}),
|
|
506
|
+
text: ORGANIZATION_RECORD_LINK_TEXT,
|
|
507
|
+
attrs: { href: target.href, target: '_blank', rel: 'noopener noreferrer' },
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* A host's note that corrections are not available here, or null when it passed none. The host may
|
|
513
|
+
* pass a function of the selected record (`{ id, type, metadata }`) returning a string or an element,
|
|
514
|
+
* for a note that points at that record (for example a link to where it is corrected).
|
|
515
|
+
*/
|
|
516
|
+
export function hostReadOnlyNote(doc, readOnlyNote, subject) {
|
|
517
|
+
const value = typeof readOnlyNote === 'function' ? readOnlyNote(subject ?? null) : readOnlyNote;
|
|
518
|
+
if (value && typeof value === 'object' && typeof value.tagName === 'string') return workspaceNotice(doc, { text: value, tone: 'info' });
|
|
519
|
+
const text = textOrNull(value);
|
|
465
520
|
return text ? workspaceNotice(doc, { text, tone: 'info' }) : null;
|
|
466
521
|
}
|
|
467
522
|
|