@unson/brainbase-mcp 0.7.0 → 0.8.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/contracts/foundation/graph-history.sql +634 -0
- 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 -3
- 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/outcome-mana.css +91 -14
- package/ui/outcome-mana.js +958 -61
- 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
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Shell of the local single-owner Web host (`brainbase web:serve`).
|
|
3
|
+
*
|
|
4
|
+
* It renders the organization edition's layout — a dark left rail with the
|
|
5
|
+
* brand, the navigation and the source (data directory, Graph format,
|
|
6
|
+
* organization Graph), a paper workspace, and a right detail rail for the
|
|
7
|
+
* selected item — and mounts one screen per nav item. Each screen gets its own
|
|
8
|
+
* rail slot and the page context (breadcrumb and source label) to put in its
|
|
9
|
+
* page head, as the organization edition's screens do. Only
|
|
10
|
+
* screens that exist are listed. A screen that reads the Graph is not
|
|
11
|
+
* mounted until the host reports Graph v2; a v1 Graph shows the migration
|
|
12
|
+
* commands instead of zero items, and the host never migrates by itself.
|
|
13
|
+
*
|
|
14
|
+
* To add a screen: append `{ id, label, usesGraph, mount(container, context) }`
|
|
15
|
+
* to LOCAL_WEB_SCREENS (or pass `screens`) and add its server module to
|
|
16
|
+
* `defaultLocalWebModules()` in `src/local-web-host.ts`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { createGraphProjectsView } from './graph-projects-view.js';
|
|
20
|
+
import { createGraphRegistryView } from './graph-registry-view.js';
|
|
21
|
+
import { createObjectiveEditorController } from './objective-editor.js';
|
|
22
|
+
import { createObjectiveEditorHttpPort } from './objective-editor-http-port.js';
|
|
23
|
+
import { createValueProofReviewUI } from './value-proof-review.js';
|
|
24
|
+
import { createWorldModelView } from './world-model-view.js';
|
|
25
|
+
|
|
26
|
+
export const LOCAL_WEB_SHELL_CONTRACT_VERSION = 'brainbase.local-web-shell.v1';
|
|
27
|
+
|
|
28
|
+
const GRAPH_FORMAT_LABELS = Object.freeze({
|
|
29
|
+
ready: (graph) => (graph.format === 'v2' ? 'v2' : String(graph.format ?? '不明')),
|
|
30
|
+
migration_required: () => 'v1(移行が必要)',
|
|
31
|
+
not_initialized: () => '未作成',
|
|
32
|
+
unreadable: () => '読み取れません',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const UNREADABLE_LABELS = Object.freeze({
|
|
36
|
+
authorization_denied: '読む権限がない',
|
|
37
|
+
scope_violation: '扱える範囲の外',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Stroke icons from the organization edition's sprite (apps/web/public/index.html).
|
|
41
|
+
const NAV_ICON_PATHS = Object.freeze({
|
|
42
|
+
today: ['M5 20V10m7 10V4m7 16v-7'],
|
|
43
|
+
objectives: ['M12 4 3 20h18z', 'M12 9v5m0 3h.01'],
|
|
44
|
+
projects: ['M3 6.5h7l2 2h9v10H3z', 'M3 6.5v-2h7l2 2'],
|
|
45
|
+
graph: ['M10 13a5 5 0 0 0 7.5.5l2-2a5 5 0 0 0-7-7l-1.2 1.2', 'M14 11a5 5 0 0 0-7.5-.5l-2 2a5 5 0 0 0 7 7l1.2-1.2'],
|
|
46
|
+
});
|
|
47
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
48
|
+
|
|
49
|
+
function makeIcon(doc, id) {
|
|
50
|
+
const paths = NAV_ICON_PATHS[id] ?? NAV_ICON_PATHS.today;
|
|
51
|
+
const create = (tag) => (typeof doc.createElementNS === 'function' ? doc.createElementNS(SVG_NS, tag) : doc.createElement(tag));
|
|
52
|
+
const svg = create('svg');
|
|
53
|
+
svg.setAttribute('class', 'bb-shell-icon');
|
|
54
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
55
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
56
|
+
for (const d of paths) {
|
|
57
|
+
const path = create('path');
|
|
58
|
+
path.setAttribute('d', d);
|
|
59
|
+
svg.append(path);
|
|
60
|
+
}
|
|
61
|
+
return svg;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function makeElement(doc, tag, { className, text, attrs = {} } = {}) {
|
|
65
|
+
const element = doc.createElement(tag);
|
|
66
|
+
if (className) element.className = className;
|
|
67
|
+
if (text !== undefined && text !== null) element.textContent = String(text);
|
|
68
|
+
for (const [name, value] of Object.entries(attrs)) {
|
|
69
|
+
if (value === undefined || value === null || value === false) continue;
|
|
70
|
+
element.setAttribute(name, value === true ? '' : String(value));
|
|
71
|
+
}
|
|
72
|
+
return element;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isRecord(value) {
|
|
76
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function makePage(container, context) {
|
|
80
|
+
const page = makeElement(context.document, 'div', { className: 'bb-shell-page' });
|
|
81
|
+
container.append(page);
|
|
82
|
+
return page;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function mountToday(container, context) {
|
|
86
|
+
return createValueProofReviewUI({
|
|
87
|
+
root: makePage(container, context),
|
|
88
|
+
rail: context.rail,
|
|
89
|
+
page: context.page,
|
|
90
|
+
document: context.document,
|
|
91
|
+
fetcher: context.fetcher,
|
|
92
|
+
token: context.token,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function renderUnreadableObjectives(doc, slot, payload) {
|
|
97
|
+
slot.replaceChildren();
|
|
98
|
+
const count = Number.isInteger(payload?.unreadable?.count) ? payload.unreadable.count : 0;
|
|
99
|
+
if (count <= 0) return;
|
|
100
|
+
const reasons = (Array.isArray(payload.unreadable.codes) ? payload.unreadable.codes : [])
|
|
101
|
+
.map((code) => UNREADABLE_LABELS[code] ?? code).join('、');
|
|
102
|
+
slot.append(makeElement(doc, 'p', {
|
|
103
|
+
className: 'bb-shell-notice is-warning',
|
|
104
|
+
text: `読めない目的が${count}件あります${reasons ? `(${reasons})` : ''}。読めた目的だけを一覧にしています。`,
|
|
105
|
+
attrs: { role: 'alert' },
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function mountObjectives(container, context) {
|
|
110
|
+
const doc = context.document;
|
|
111
|
+
const page = makePage(container, context);
|
|
112
|
+
const unreadable = makeElement(doc, 'div', { className: 'bb-shell-slot' });
|
|
113
|
+
const editorRoot = makeElement(doc, 'div', { className: 'bb-shell-objectives' });
|
|
114
|
+
const worldRoot = makeElement(doc, 'div', { className: 'bb-shell-world-model' });
|
|
115
|
+
// The unreadable-objectives notice belongs under the objectives, not above the page head.
|
|
116
|
+
page.append(editorRoot, unreadable, worldRoot);
|
|
117
|
+
const port = createObjectiveEditorHttpPort({
|
|
118
|
+
fetcher: context.fetcher,
|
|
119
|
+
token: context.token,
|
|
120
|
+
onListResult: (payload) => renderUnreadableObjectives(doc, unreadable, payload),
|
|
121
|
+
});
|
|
122
|
+
const editor = createObjectiveEditorController({
|
|
123
|
+
root: editorRoot,
|
|
124
|
+
rail: context.rail,
|
|
125
|
+
page: context.page,
|
|
126
|
+
port,
|
|
127
|
+
// The principal is decided by the host from the local Graph owner.
|
|
128
|
+
context: {},
|
|
129
|
+
canEdit: true,
|
|
130
|
+
constraintsEditable: false,
|
|
131
|
+
storyLinks: false,
|
|
132
|
+
});
|
|
133
|
+
const worldModel = createWorldModelView({ root: worldRoot, rail: context.rail, document: doc, fetcher: context.fetcher });
|
|
134
|
+
return { editor, worldModel };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function mountGraphScreen(createView) {
|
|
138
|
+
return (container, context) => {
|
|
139
|
+
const viewRoot = makeElement(context.document, 'div');
|
|
140
|
+
makePage(container, context).append(viewRoot);
|
|
141
|
+
return createView({
|
|
142
|
+
root: viewRoot,
|
|
143
|
+
rail: context.rail,
|
|
144
|
+
page: context.page,
|
|
145
|
+
document: context.document,
|
|
146
|
+
fetcher: context.fetcher,
|
|
147
|
+
token: context.token,
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export const LOCAL_WEB_SCREENS = Object.freeze([
|
|
153
|
+
Object.freeze({ id: 'today', label: '今日', usesGraph: false, rail: true, source: '判断journal', mount: mountToday }),
|
|
154
|
+
Object.freeze({ id: 'objectives', label: '目的と現状', usesGraph: true, rail: true, source: '手元のGraph', mount: mountObjectives }),
|
|
155
|
+
Object.freeze({ id: 'projects', label: 'プロジェクトと関係者', usesGraph: true, rail: true, source: '手元のGraph', mount: mountGraphScreen(createGraphProjectsView) }),
|
|
156
|
+
Object.freeze({ id: 'graph', label: '情報と関係', usesGraph: true, rail: true, source: '手元のGraph', mount: mountGraphScreen(createGraphRegistryView) }),
|
|
157
|
+
]);
|
|
158
|
+
|
|
159
|
+
function renderGraphGate(doc, slot, status, onRecheck, label) {
|
|
160
|
+
slot.replaceChildren();
|
|
161
|
+
const box = makeElement(doc, 'section', { className: 'bb-shell-gate', attrs: { role: 'alert' } });
|
|
162
|
+
if (status.phase !== 'ready') {
|
|
163
|
+
box.append(
|
|
164
|
+
makeElement(doc, 'h2', { text: status.phase === 'loading' ? 'データの状態を確認しています' : 'データの状態を確認できません' }),
|
|
165
|
+
makeElement(doc, 'p', { text: status.phase === 'loading' ? '確認が終わるまで、この画面の内容は表示しません。' : `理由: ${status.error ?? '不明'}。0件ではありません。` }),
|
|
166
|
+
);
|
|
167
|
+
} else {
|
|
168
|
+
const graph = status.data.graph;
|
|
169
|
+
const title = graph.status === 'migration_required' ? 'Graphの移行が必要です'
|
|
170
|
+
: graph.status === 'not_initialized' ? 'Brainbaseのデータがまだありません'
|
|
171
|
+
: 'データを読み取れません';
|
|
172
|
+
const lead = graph.status === 'migration_required'
|
|
173
|
+
? `このデータはGraph v1のため、「${label}」を読み書きできません。0件ではありません。ホストは自動で移行しません。次のコマンドで、内容を確かめてから移行してください。`
|
|
174
|
+
: graph.status === 'not_initialized'
|
|
175
|
+
? `データの場所(${status.data.data_dir})に正本のファイルがありません。次のコマンドで作成できます。`
|
|
176
|
+
: `理由: ${graph.message ?? '不明'}。0件ではありません。`;
|
|
177
|
+
box.append(makeElement(doc, 'h2', { text: title }), makeElement(doc, 'p', { text: lead }));
|
|
178
|
+
const commands = Array.isArray(graph.commands) ? graph.commands.filter((command) => typeof command === 'string') : [];
|
|
179
|
+
if (commands.length > 0) {
|
|
180
|
+
const list = makeElement(doc, 'ol', { className: 'bb-shell-commands' });
|
|
181
|
+
for (const command of commands) {
|
|
182
|
+
const item = makeElement(doc, 'li');
|
|
183
|
+
item.append(makeElement(doc, 'code', { text: command }));
|
|
184
|
+
list.append(item);
|
|
185
|
+
}
|
|
186
|
+
box.append(list);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (status.phase !== 'loading') {
|
|
190
|
+
const recheck = makeElement(doc, 'button', { className: 'bb-shell-button', text: '再確認', attrs: { type: 'button' } });
|
|
191
|
+
recheck.addEventListener('click', () => void onRecheck());
|
|
192
|
+
box.append(recheck);
|
|
193
|
+
}
|
|
194
|
+
slot.append(box);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function renderSource(doc, bar, status, onRecheck) {
|
|
198
|
+
bar.replaceChildren();
|
|
199
|
+
if (status.phase === 'loading') {
|
|
200
|
+
bar.append(makeElement(doc, 'span', { text: 'データの場所を確認しています。' }));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (status.phase === 'error') {
|
|
204
|
+
bar.append(makeElement(doc, 'span', { className: 'is-danger', text: `データの状態を確認できません(${status.error ?? '不明'})。` }));
|
|
205
|
+
const retry = makeElement(doc, 'button', { className: 'bb-shell-link-button', text: '再確認', attrs: { type: 'button' } });
|
|
206
|
+
retry.addEventListener('click', () => void onRecheck());
|
|
207
|
+
bar.append(retry);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const { data } = status;
|
|
211
|
+
const graphLabel = (GRAPH_FORMAT_LABELS[data.graph.status] ?? (() => '不明'))(data.graph);
|
|
212
|
+
const fact = (label, value, className) => {
|
|
213
|
+
const item = makeElement(doc, 'span', { className: `bb-shell-fact${className ? ` ${className}` : ''}` });
|
|
214
|
+
item.append(makeElement(doc, 'span', { className: 'bb-shell-fact-label', text: label }), makeElement(doc, 'span', { className: 'bb-shell-fact-value', text: value }));
|
|
215
|
+
return item;
|
|
216
|
+
};
|
|
217
|
+
bar.append(
|
|
218
|
+
fact('データ', data.data_dir),
|
|
219
|
+
fact('Graph', graphLabel, data.graph.status === 'ready' ? '' : 'is-warning'),
|
|
220
|
+
fact('組織のGraph', data.organization_graph?.status === 'connected' ? '読んでいます' : '読んでいません'),
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function normalizeStatus(payload) {
|
|
225
|
+
if (!isRecord(payload) || typeof payload.data_dir !== 'string' || !isRecord(payload.graph) || typeof payload.graph.status !== 'string') {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
return payload;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function createLocalWebShell({
|
|
232
|
+
root,
|
|
233
|
+
document: explicitDocument,
|
|
234
|
+
fetcher,
|
|
235
|
+
token,
|
|
236
|
+
statusPath = '/api/local/status',
|
|
237
|
+
screens = LOCAL_WEB_SCREENS,
|
|
238
|
+
initialScreen,
|
|
239
|
+
} = {}) {
|
|
240
|
+
if (!root) throw new TypeError('root is required');
|
|
241
|
+
const doc = explicitDocument ?? (typeof document === 'undefined' ? null : document);
|
|
242
|
+
if (!doc || typeof doc.createElement !== 'function') throw new Error('document_unavailable');
|
|
243
|
+
const request = typeof fetcher === 'function'
|
|
244
|
+
? fetcher
|
|
245
|
+
: typeof globalThis.fetch === 'function' ? (path, init) => globalThis.fetch(path, init) : null;
|
|
246
|
+
const context = Object.freeze({ document: doc, fetcher: request, token });
|
|
247
|
+
|
|
248
|
+
const status = { phase: 'loading', data: null, error: null };
|
|
249
|
+
const mounted = new Map();
|
|
250
|
+
const slots = new Map();
|
|
251
|
+
const links = new Map();
|
|
252
|
+
let active = null;
|
|
253
|
+
|
|
254
|
+
const shell = makeElement(doc, 'div', { className: 'bb-shell', attrs: { 'data-contract-version': LOCAL_WEB_SHELL_CONTRACT_VERSION } });
|
|
255
|
+
const sidebar = makeElement(doc, 'aside', { className: 'bb-shell-sidebar', attrs: { id: 'bb-shell-sidebar', 'aria-label': '主ナビゲーション' } });
|
|
256
|
+
const brand = makeElement(doc, 'div', { className: 'bb-shell-brand', text: 'Brainbase' });
|
|
257
|
+
const nav = makeElement(doc, 'nav', { className: 'bb-shell-nav', attrs: { 'aria-label': '画面' } });
|
|
258
|
+
const menuButton = makeElement(doc, 'button', {
|
|
259
|
+
className: 'bb-shell-menu-button',
|
|
260
|
+
text: 'メニュー',
|
|
261
|
+
attrs: { type: 'button', 'aria-controls': 'bb-shell-sidebar', 'aria-expanded': 'false' },
|
|
262
|
+
});
|
|
263
|
+
const backdrop = makeElement(doc, 'button', { className: 'bb-shell-backdrop', attrs: { type: 'button', 'aria-label': 'メニューを閉じる', tabindex: '-1' } });
|
|
264
|
+
const setMenuOpen = (open) => {
|
|
265
|
+
sidebar.className = open ? 'bb-shell-sidebar is-open' : 'bb-shell-sidebar';
|
|
266
|
+
backdrop.className = open ? 'bb-shell-backdrop is-open' : 'bb-shell-backdrop';
|
|
267
|
+
menuButton.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
268
|
+
};
|
|
269
|
+
menuButton.addEventListener('click', () => setMenuOpen(!sidebar.className.includes('is-open')));
|
|
270
|
+
backdrop.addEventListener('click', () => setMenuOpen(false));
|
|
271
|
+
for (const screen of screens) {
|
|
272
|
+
const link = makeElement(doc, 'a', { className: 'bb-shell-nav-link', text: screen.label, attrs: { href: `#${screen.id}` } });
|
|
273
|
+
// The label stays the link's own text; the icon goes in front where the DOM allows it.
|
|
274
|
+
if (typeof link.prepend === 'function') link.prepend(makeIcon(doc, screen.id));
|
|
275
|
+
link.addEventListener('click', () => {
|
|
276
|
+
setMenuOpen(false);
|
|
277
|
+
controller.show(screen.id);
|
|
278
|
+
});
|
|
279
|
+
links.set(screen.id, link);
|
|
280
|
+
nav.append(link);
|
|
281
|
+
}
|
|
282
|
+
const source = makeElement(doc, 'section', { className: 'bb-shell-source', attrs: { 'aria-label': '出典' } });
|
|
283
|
+
sidebar.append(brand, nav, source);
|
|
284
|
+
const workspace = makeElement(doc, 'div', { className: 'bb-shell-workspace' });
|
|
285
|
+
const mobileBar = makeElement(doc, 'div', { className: 'bb-shell-mobile-bar' });
|
|
286
|
+
mobileBar.append(makeElement(doc, 'strong', { text: 'Brainbase' }), menuButton);
|
|
287
|
+
const main = makeElement(doc, 'main', { className: 'bb-shell-main' });
|
|
288
|
+
for (const screen of screens) {
|
|
289
|
+
const slot = makeElement(doc, 'section', { className: 'bb-shell-screen', attrs: { 'data-screen': screen.id, 'aria-label': screen.label } });
|
|
290
|
+
slot.hidden = true;
|
|
291
|
+
slots.set(screen.id, slot);
|
|
292
|
+
main.append(slot);
|
|
293
|
+
}
|
|
294
|
+
workspace.append(mobileBar, main);
|
|
295
|
+
const railAside = makeElement(doc, 'aside', { className: 'bb-shell-rail', attrs: { 'aria-label': '選択中の項目' } });
|
|
296
|
+
const rails = new Map();
|
|
297
|
+
for (const screen of screens) {
|
|
298
|
+
if (!screen.rail) continue;
|
|
299
|
+
const railSlot = makeElement(doc, 'div', { className: 'bb-shell-rail-slot', attrs: { 'data-rail': screen.id } });
|
|
300
|
+
railSlot.hidden = true;
|
|
301
|
+
rails.set(screen.id, railSlot);
|
|
302
|
+
railAside.append(railSlot);
|
|
303
|
+
}
|
|
304
|
+
shell.append(sidebar, backdrop, workspace, railAside);
|
|
305
|
+
root.replaceChildren(shell);
|
|
306
|
+
|
|
307
|
+
// The rail column shows only while the active screen is mounted and has one.
|
|
308
|
+
function updateRail() {
|
|
309
|
+
const showRail = rails.has(active) && mounted.has(active);
|
|
310
|
+
shell.className = showRail ? 'bb-shell has-rail' : 'bb-shell';
|
|
311
|
+
railAside.hidden = !showRail;
|
|
312
|
+
for (const [screenId, railSlot] of rails) railSlot.hidden = !(showRail && screenId === active);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function ensureMounted(screen) {
|
|
316
|
+
if (mounted.has(screen.id)) return;
|
|
317
|
+
const slot = slots.get(screen.id);
|
|
318
|
+
if (screen.usesGraph && !(status.phase === 'ready' && status.data.graph.status === 'ready')) {
|
|
319
|
+
renderGraphGate(doc, slot, status, controller.refreshStatus, screen.label);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
slot.replaceChildren();
|
|
323
|
+
const rail = rails.get(screen.id) ?? null;
|
|
324
|
+
rail?.replaceChildren();
|
|
325
|
+
const page = Object.freeze({ crumbs: Object.freeze(['あなたのBrainbase', screen.label]), source: screen.source ?? null });
|
|
326
|
+
mounted.set(screen.id, screen.mount(slot, Object.freeze({ ...context, rail, page })) ?? true);
|
|
327
|
+
updateRail();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const controller = {
|
|
331
|
+
get state() {
|
|
332
|
+
return { active, status, mounted: [...mounted.keys()] };
|
|
333
|
+
},
|
|
334
|
+
show(id) {
|
|
335
|
+
const screen = screens.find((candidate) => candidate.id === id) ?? screens[0];
|
|
336
|
+
active = screen.id;
|
|
337
|
+
for (const [screenId, slot] of slots) slot.hidden = screenId !== screen.id;
|
|
338
|
+
for (const [screenId, link] of links) {
|
|
339
|
+
if (screenId === screen.id) link.setAttribute('aria-current', 'page');
|
|
340
|
+
else link.removeAttribute('aria-current');
|
|
341
|
+
}
|
|
342
|
+
ensureMounted(screen);
|
|
343
|
+
updateRail();
|
|
344
|
+
return controller;
|
|
345
|
+
},
|
|
346
|
+
async refreshStatus() {
|
|
347
|
+
status.phase = 'loading';
|
|
348
|
+
status.error = null;
|
|
349
|
+
renderSource(doc, source, status, controller.refreshStatus);
|
|
350
|
+
try {
|
|
351
|
+
if (!request) throw new Error('取得先が設定されていません');
|
|
352
|
+
const response = await request(statusPath);
|
|
353
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
354
|
+
const data = normalizeStatus(await response.json());
|
|
355
|
+
if (!data) throw new Error('応答の形式が不正です');
|
|
356
|
+
status.phase = 'ready';
|
|
357
|
+
status.data = data;
|
|
358
|
+
} catch (error) {
|
|
359
|
+
status.phase = 'error';
|
|
360
|
+
status.data = null;
|
|
361
|
+
status.error = error instanceof Error ? error.message : 'request_failed';
|
|
362
|
+
}
|
|
363
|
+
renderSource(doc, source, status, controller.refreshStatus);
|
|
364
|
+
// Re-evaluate Graph-backed screens that are waiting behind the gate.
|
|
365
|
+
for (const screen of screens) {
|
|
366
|
+
if (screen.usesGraph && !mounted.has(screen.id) && (screen.id === active)) ensureMounted(screen);
|
|
367
|
+
}
|
|
368
|
+
return status;
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
renderSource(doc, source, status, controller.refreshStatus);
|
|
373
|
+
controller.show(initialScreen);
|
|
374
|
+
void controller.refreshStatus();
|
|
375
|
+
return controller;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export default createLocalWebShell;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* HTTP adapter for the Objective editor port.
|
|
3
|
+
*
|
|
4
|
+
* It maps ObjectiveEditorPort methods onto the Foundation Objective routes
|
|
5
|
+
* (`GET/POST /objectives`, `GET/PUT /objectives/:id`, `/readiness`,
|
|
6
|
+
* `/constraints`). The host injects the fetcher, base path and write token;
|
|
7
|
+
* this module keeps no global state. Authority fields (ACL, scope, storage,
|
|
8
|
+
* provenance, authorized uses) are decided by the host from its trusted
|
|
9
|
+
* context, so they are removed before a definition leaves the browser.
|
|
10
|
+
* Constraint links are only read here; there is no replace method.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const OBJECTIVE_EDITOR_HTTP_PORT_VERSION = 'brainbase.objective-editor-http-port.v1';
|
|
14
|
+
|
|
15
|
+
/** Top-level fields the Foundation route rejects with `authority_field_in_body`. */
|
|
16
|
+
export const OBJECTIVE_AUTHORITY_FIELDS = Object.freeze([
|
|
17
|
+
'acl', 'scope', 'storage', 'provenance', 'authorizedUses', 'authorized_uses',
|
|
18
|
+
'ownerId', 'owner_id', 'userId', 'user_id', 'memberId', 'member_id',
|
|
19
|
+
'readerIds', 'reader_ids', 'writerIds', 'writer_ids', 'tenant', 'subjectIds', 'subject_ids',
|
|
20
|
+
'tenantId', 'tenant_id', 'organization', 'organization_id', 'organizationId', 'org', 'orgId', 'org_id',
|
|
21
|
+
'workspace', 'workspaceId', 'workspace_id', 'principal', 'actor', 'actorId', 'actor_id',
|
|
22
|
+
'role', 'roles', 'raci', 'authorization', 'permission', 'permissions',
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
const AUTHORITY_FIELD_SET = new Set(OBJECTIVE_AUTHORITY_FIELDS);
|
|
26
|
+
|
|
27
|
+
const ERROR_MESSAGES = Object.freeze({
|
|
28
|
+
revision_conflict: '別の保存で版が進んでいます。入力は残しています。再読込して今の版を確かめてください。',
|
|
29
|
+
authority_field_in_body: '権限・範囲・保存先・由来はホストが決めるため、画面からは送れません。',
|
|
30
|
+
authority_field_change: '権限・範囲・保存先・由来は変更できません。',
|
|
31
|
+
authorization_denied: 'この目的を読み書きする権限がありません。',
|
|
32
|
+
scope_violation: 'この目的は扱える範囲の外にあります。',
|
|
33
|
+
csrf_failed: '起動時のトークンを確かめられません。ページを開き直してください。',
|
|
34
|
+
web_token_required: '起動時のトークンを確かめられません。ページを開き直してください。',
|
|
35
|
+
cross_origin_rejected: '別のサイトからの保存は受け付けません。',
|
|
36
|
+
graph_migration_required: 'Graphの移行が必要です。',
|
|
37
|
+
body_too_large: '入力が大きすぎます。',
|
|
38
|
+
payload_too_large: '入力が大きすぎます。',
|
|
39
|
+
network_error: 'ホストに接続できません。',
|
|
40
|
+
invalid_response: 'ホストの応答を読み取れません。',
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
function isRecord(value) {
|
|
44
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Error thrown by the port. `code`/`status`/`currentRevision` follow the editor's error contract. */
|
|
48
|
+
export class ObjectiveEditorHttpError extends Error {
|
|
49
|
+
constructor({ code, status, message, currentRevision = null, detail = null }) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.name = 'ObjectiveEditorHttpError';
|
|
52
|
+
this.code = code;
|
|
53
|
+
this.status = status;
|
|
54
|
+
this.statusCode = status;
|
|
55
|
+
this.currentRevision = currentRevision;
|
|
56
|
+
this.detail = detail;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function errorFromResponse(status, payload) {
|
|
61
|
+
const error = isRecord(payload?.error) ? payload.error : {};
|
|
62
|
+
const code = typeof error.code === 'string' && error.code ? error.code : `http_${status}`;
|
|
63
|
+
const detail = typeof error.message === 'string' && error.message ? error.message : null;
|
|
64
|
+
const base = ERROR_MESSAGES[code];
|
|
65
|
+
const message = code === 'invalid_input' || code === 'invalid_json'
|
|
66
|
+
? `入力が保存の条件を満たしていません${detail ? `(${detail})` : ''}`
|
|
67
|
+
: base ?? detail ?? `HTTP ${status}`;
|
|
68
|
+
return new ObjectiveEditorHttpError({
|
|
69
|
+
code,
|
|
70
|
+
status,
|
|
71
|
+
message,
|
|
72
|
+
currentRevision: typeof error.currentRevision === 'string' ? error.currentRevision : null,
|
|
73
|
+
detail,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Shallow copy of a draft without the fields the host owns. */
|
|
78
|
+
export function stripObjectiveAuthorityFields(definition) {
|
|
79
|
+
if (!isRecord(definition)) return definition;
|
|
80
|
+
return Object.fromEntries(Object.entries(definition).filter(([key]) => !AUTHORITY_FIELD_SET.has(key)));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function createObjectiveEditorHttpPort({
|
|
84
|
+
fetcher,
|
|
85
|
+
basePath = '/api/foundation',
|
|
86
|
+
token,
|
|
87
|
+
tokenHeader = 'X-Brainbase-Review-Token',
|
|
88
|
+
/** Called with each list payload, e.g. to show how many records could not be read. */
|
|
89
|
+
onListResult,
|
|
90
|
+
} = {}) {
|
|
91
|
+
const request = typeof fetcher === 'function'
|
|
92
|
+
? fetcher
|
|
93
|
+
: typeof globalThis.fetch === 'function' ? (path, init) => globalThis.fetch(path, init) : null;
|
|
94
|
+
const base = String(basePath).replace(/\/+$/u, '');
|
|
95
|
+
|
|
96
|
+
async function call(path, init) {
|
|
97
|
+
if (!request) throw new ObjectiveEditorHttpError({ code: 'network_error', status: 0, message: ERROR_MESSAGES.network_error });
|
|
98
|
+
let response;
|
|
99
|
+
try {
|
|
100
|
+
response = await request(path, init);
|
|
101
|
+
} catch {
|
|
102
|
+
throw new ObjectiveEditorHttpError({ code: 'network_error', status: 0, message: ERROR_MESSAGES.network_error });
|
|
103
|
+
}
|
|
104
|
+
let payload = null;
|
|
105
|
+
try {
|
|
106
|
+
payload = await response.json();
|
|
107
|
+
} catch {
|
|
108
|
+
payload = null;
|
|
109
|
+
}
|
|
110
|
+
if (!response.ok) throw errorFromResponse(response.status, payload);
|
|
111
|
+
if (payload === null) throw new ObjectiveEditorHttpError({ code: 'invalid_response', status: response.status, message: ERROR_MESSAGES.invalid_response });
|
|
112
|
+
return payload;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const objectivePath = (id, suffix = '') => `${base}/objectives/${encodeURIComponent(id)}${suffix}`;
|
|
116
|
+
const withRevision = (path, revision) => (revision ? `${path}?revision=${encodeURIComponent(revision)}` : path);
|
|
117
|
+
const writeInit = (method, body) => ({
|
|
118
|
+
method,
|
|
119
|
+
headers: { 'Content-Type': 'application/json', ...(token ? { [tokenHeader]: token } : {}) },
|
|
120
|
+
body: JSON.stringify(body),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
contractVersion: OBJECTIVE_EDITOR_HTTP_PORT_VERSION,
|
|
125
|
+
async listObjectives() {
|
|
126
|
+
const payload = await call(`${base}/objectives`);
|
|
127
|
+
onListResult?.(payload);
|
|
128
|
+
return payload;
|
|
129
|
+
},
|
|
130
|
+
async readObjective(id, _context, revision) {
|
|
131
|
+
try {
|
|
132
|
+
return await call(withRevision(objectivePath(id), revision));
|
|
133
|
+
} catch (error) {
|
|
134
|
+
// A missing Objective is a readback miss, not a failure of the request.
|
|
135
|
+
if (error instanceof ObjectiveEditorHttpError && error.status === 404 && error.code === 'not_found') return null;
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
checkObjectiveReadiness(id, _context, revision) {
|
|
140
|
+
return call(withRevision(objectivePath(id, '/readiness'), revision));
|
|
141
|
+
},
|
|
142
|
+
createObjective(definition) {
|
|
143
|
+
return call(`${base}/objectives`, writeInit('POST', stripObjectiveAuthorityFields(definition)));
|
|
144
|
+
},
|
|
145
|
+
updateObjective(id, expectedRevision, definition) {
|
|
146
|
+
return call(objectivePath(id), writeInit('PUT', { expectedRevision, definition: stripObjectiveAuthorityFields(definition) }));
|
|
147
|
+
},
|
|
148
|
+
listObjectiveConstraintRefs(reference) {
|
|
149
|
+
return call(withRevision(objectivePath(reference.id, '/constraints'), reference.revision));
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export default createObjectiveEditorHttpPort;
|