@unson/brainbase-mcp 0.6.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.
Files changed (148) hide show
  1. package/contracts/foundation/graph-history.sql +634 -0
  2. package/contracts/judgment-value-proof/schema.json +43 -1
  3. package/dist/canonical-task-contract.d.ts +9 -0
  4. package/dist/canonical-task-contract.js +39 -0
  5. package/dist/canonical-task-principal.d.ts +16 -0
  6. package/dist/canonical-task-principal.js +36 -0
  7. package/dist/canonical-task-service.d.ts +251 -0
  8. package/dist/canonical-task-service.js +820 -0
  9. package/dist/cli.d.ts +2 -0
  10. package/dist/cli.js +107 -0
  11. package/dist/company-os-evaluation.d.ts +193 -0
  12. package/dist/company-os-evaluation.js +1150 -0
  13. package/dist/company-os-impact-review.d.ts +248 -0
  14. package/dist/company-os-impact-review.js +663 -0
  15. package/dist/company-os-learning-adoption.d.ts +288 -0
  16. package/dist/company-os-learning-adoption.js +944 -0
  17. package/dist/company-os-objectives.d.ts +63 -0
  18. package/dist/company-os-objectives.js +234 -0
  19. package/dist/company-os-receipt-adapter.d.ts +109 -0
  20. package/dist/company-os-receipt-adapter.js +636 -0
  21. package/dist/constraint-exception-store.d.ts +23 -0
  22. package/dist/constraint-exception-store.js +108 -0
  23. package/dist/constraint-resolution.d.ts +175 -0
  24. package/dist/constraint-resolution.js +922 -0
  25. package/dist/decision-adapter.d.ts +145 -0
  26. package/dist/decision-adapter.js +586 -0
  27. package/dist/durable-wait-http.d.ts +51 -0
  28. package/dist/durable-wait-http.js +497 -0
  29. package/dist/durable-waits.d.ts +234 -0
  30. package/dist/durable-waits.js +944 -0
  31. package/dist/execution-authority-http.d.ts +28 -0
  32. package/dist/execution-authority-http.js +359 -0
  33. package/dist/execution-authority.d.ts +201 -0
  34. package/dist/execution-authority.js +634 -0
  35. package/dist/foundation-authenticated-tools.d.ts +56 -0
  36. package/dist/foundation-authenticated-tools.js +347 -0
  37. package/dist/foundation-catalog.d.ts +26 -0
  38. package/dist/foundation-catalog.js +206 -0
  39. package/dist/foundation-constraint-store.d.ts +50 -0
  40. package/dist/foundation-constraint-store.js +215 -0
  41. package/dist/foundation-graph-http.d.ts +43 -0
  42. package/dist/foundation-graph-http.js +243 -0
  43. package/dist/foundation-graph-write.d.ts +90 -0
  44. package/dist/foundation-graph-write.js +447 -0
  45. package/dist/foundation-http.d.ts +90 -0
  46. package/dist/foundation-http.js +439 -0
  47. package/dist/foundation-public-provider.d.ts +140 -0
  48. package/dist/foundation-public-provider.js +192 -0
  49. package/dist/foundation-store.d.ts +132 -0
  50. package/dist/foundation-store.js +654 -0
  51. package/dist/graph-corrections.d.ts +157 -0
  52. package/dist/graph-corrections.js +620 -0
  53. package/dist/graph-foundation-reader.d.ts +60 -0
  54. package/dist/graph-foundation-reader.js +494 -0
  55. package/dist/graph-retrieval.d.ts +6 -1
  56. package/dist/graph-revision-reader.d.ts +80 -0
  57. package/dist/graph-revision-reader.js +246 -0
  58. package/dist/graph-web-http.d.ts +26 -0
  59. package/dist/graph-web-http.js +215 -0
  60. package/dist/graph-web.d.ts +364 -0
  61. package/dist/graph-web.js +639 -0
  62. package/dist/judgment-dag-composition.d.ts +224 -0
  63. package/dist/judgment-dag-composition.js +731 -0
  64. package/dist/judgment-dag.d.ts +2 -0
  65. package/dist/judgment-dag.js +1 -0
  66. package/dist/judgment-problem-snapshot.d.ts +213 -0
  67. package/dist/judgment-problem-snapshot.js +1073 -0
  68. package/dist/judgment-value-proof-review.d.ts +166 -0
  69. package/dist/judgment-value-proof-review.js +437 -0
  70. package/dist/judgment-value-proof.d.ts +28 -1
  71. package/dist/judgment-value-proof.js +53 -8
  72. package/dist/judgment-view-http.d.ts +22 -0
  73. package/dist/judgment-view-http.js +128 -0
  74. package/dist/judgment-view.d.ts +222 -0
  75. package/dist/judgment-view.js +627 -0
  76. package/dist/knowledge-adapter.d.ts +112 -0
  77. package/dist/knowledge-adapter.js +691 -0
  78. package/dist/knowledge-continuation.d.ts +127 -0
  79. package/dist/knowledge-continuation.js +446 -0
  80. package/dist/knowledge-lookup.d.ts +178 -0
  81. package/dist/knowledge-lookup.js +758 -0
  82. package/dist/local-web-fetch-bridge.d.ts +5 -0
  83. package/dist/local-web-fetch-bridge.js +37 -0
  84. package/dist/local-web-host.d.ts +109 -0
  85. package/dist/local-web-host.js +688 -0
  86. package/dist/local-web-security.d.ts +25 -0
  87. package/dist/local-web-security.js +95 -0
  88. package/dist/ontology-foundation.d.ts +209 -0
  89. package/dist/ontology-foundation.js +1000 -0
  90. package/dist/ontology.d.ts +13 -0
  91. package/dist/ontology.js +14 -0
  92. package/dist/organization-connection.d.ts +164 -0
  93. package/dist/organization-connection.js +428 -0
  94. package/dist/organization-graph.d.ts +15 -0
  95. package/dist/organization-graph.js +24 -4
  96. package/dist/personal-knowledge.d.ts +132 -0
  97. package/dist/personal-knowledge.js +779 -0
  98. package/dist/personal-memory-handover.d.ts +108 -0
  99. package/dist/personal-memory-handover.js +369 -0
  100. package/dist/philosophy-revision-reader.d.ts +85 -0
  101. package/dist/philosophy-revision-reader.js +241 -0
  102. package/dist/portable-graph.d.ts +24 -0
  103. package/dist/portable-graph.js +77 -0
  104. package/dist/problem-candidates.d.ts +149 -0
  105. package/dist/problem-candidates.js +898 -0
  106. package/dist/problem-selection.d.ts +238 -0
  107. package/dist/problem-selection.js +995 -0
  108. package/dist/resource-reservation-http.d.ts +31 -0
  109. package/dist/resource-reservation-http.js +371 -0
  110. package/dist/resource-reservations.d.ts +221 -0
  111. package/dist/resource-reservations.js +577 -0
  112. package/dist/server.d.ts +104 -2
  113. package/dist/server.js +186 -4
  114. package/dist/ssot.d.ts +6 -1
  115. package/dist/ssot.js +63 -5
  116. package/dist/streamable-http.d.ts +42 -0
  117. package/dist/streamable-http.js +288 -0
  118. package/dist/types.d.ts +20 -0
  119. package/dist/value-proof-review-http.d.ts +25 -0
  120. package/dist/value-proof-review-http.js +185 -0
  121. package/dist/world-model.d.ts +199 -0
  122. package/dist/world-model.js +955 -0
  123. package/package.json +230 -3
  124. package/ui/brainbase-tokens.css +56 -0
  125. package/ui/graph-own-share.js +367 -0
  126. package/ui/graph-projects-view.css +23 -0
  127. package/ui/graph-projects-view.js +730 -0
  128. package/ui/graph-registry-view.css +101 -0
  129. package/ui/graph-registry-view.js +804 -0
  130. package/ui/graph-view-shared.css +522 -0
  131. package/ui/graph-view-shared.js +1227 -0
  132. package/ui/judgment-view.css +312 -0
  133. package/ui/judgment-view.js +508 -0
  134. package/ui/local-web-shell.css +396 -0
  135. package/ui/local-web-shell.js +378 -0
  136. package/ui/objective-editor-http-port.js +154 -0
  137. package/ui/objective-editor.css +286 -0
  138. package/ui/objective-editor.js +1586 -0
  139. package/ui/outcome-knowledge.css +50 -14
  140. package/ui/outcome-knowledge.js +15 -1
  141. package/ui/outcome-mana.css +91 -14
  142. package/ui/outcome-mana.js +958 -61
  143. package/ui/value-proof-review.css +504 -0
  144. package/ui/value-proof-review.js +1104 -0
  145. package/ui/workspace-kit.css +485 -0
  146. package/ui/workspace-kit.js +229 -0
  147. package/ui/world-model-view.css +188 -0
  148. package/ui/world-model-view.js +486 -0
@@ -0,0 +1,804 @@
1
+ /*
2
+ * 「情報と関係」: what the local Graph holds, how records are connected, and
3
+ * where to correct a mistake so the next MCP use sees the fix.
4
+ *
5
+ * Built on the organization edition's screen pattern (workspace kit): the
6
+ * workspace has the page head, the source notice, the search, the results
7
+ * ledger and the kinds of information as two ledgers; the right rail has the
8
+ * selected record — its outline, the relations going out and coming in, and
9
+ * the corrections.
10
+ *
11
+ * Reads `GET {base}/search`, `GET {base}/entities/:id` and `GET {base}/ontology`
12
+ * and corrects through `POST {base}/corrections` (see `graph-view-shared.js`).
13
+ * The kinds of information and relations are shown read-only; there is no
14
+ * change proposal here. The part reads only the Graph routes the host serves
15
+ * at its base path (the local Graph on the OSS host). The host injects the
16
+ * fetcher, base path, launch token, right rail and page context; this module
17
+ * keeps no global state. Another host composes it unchanged and adds only its
18
+ * own controls through the options of `createGraphRegistryView`, including the
19
+ * owner's own share in a separate section (`graph-own-share.js`).
20
+ */
21
+
22
+ import {
23
+ activityBadge,
24
+ badge,
25
+ button,
26
+ createGraphClient,
27
+ createGraphCorrection,
28
+ createGraphLayout,
29
+ dayToRfc3339,
30
+ focusLedgerRow,
31
+ getDocument,
32
+ graphCommandList,
33
+ graphCorrectionScope,
34
+ graphEntityCorrectionFields,
35
+ graphPageContext,
36
+ graphStateNotice,
37
+ hostReadOnlyNote,
38
+ isEdgeView,
39
+ isEntityView,
40
+ isRecord,
41
+ makeElement,
42
+ newRelationOptions,
43
+ ownLabel,
44
+ readOnlyGraphClient,
45
+ recordDetails,
46
+ relationLabel,
47
+ renderCorrectionHistory,
48
+ renderGraphIssues,
49
+ renderProvenance,
50
+ shellArg,
51
+ textOrNull,
52
+ typeLabel,
53
+ validityLabel,
54
+ validityText,
55
+ GRAPH_CORRECTION_FIELDS,
56
+ GRAPH_ENTITY_TYPE_LABELS,
57
+ GRAPH_ENTITY_TYPE_MEANINGS,
58
+ GRAPH_RELATION_LABELS,
59
+ graphHostEmptyNotice,
60
+ } from './graph-view-shared.js';
61
+ import { graphOwnShareSource, ownEntityDetail, ownShareSection, ownShareStateNotice } from './graph-own-share.js';
62
+ import {
63
+ workspaceActions,
64
+ workspaceButton,
65
+ workspaceDefinition,
66
+ workspaceDetailEmpty,
67
+ workspaceHostNotice,
68
+ workspaceLedger,
69
+ workspaceNotice,
70
+ workspacePageHeader,
71
+ workspaceRailBlock,
72
+ workspaceRailHead,
73
+ workspaceSectionTitle,
74
+ } from './workspace-kit.js';
75
+
76
+ export const GRAPH_REGISTRY_VIEW_CONTRACT_VERSION = 'brainbase.graph-registry-view.v2';
77
+ export const GRAPH_SEARCH_LIMIT = 50;
78
+ export const GRAPH_ENTITY_LEDGER_COLUMNS = Object.freeze(['名前', '種類', '要約', '有効期間']);
79
+ export const GRAPH_RELATION_TYPE_LEDGER_COLUMNS = Object.freeze(['関係', '起点 → 終点', '意味', '件数']);
80
+ export const GRAPH_ENTITY_TYPE_LEDGER_COLUMNS = Object.freeze(['種類', '意味', '件数']);
81
+
82
+ const INVALID = Object.freeze({ state: 'invalid', reason: '応答の形式が不正です' });
83
+ const ALL_TYPES = Object.freeze(['', 'すべての種類']);
84
+ const TYPE_FILTERS = Object.freeze([ALL_TYPES, ...Object.entries(GRAPH_ENTITY_TYPE_LABELS)]);
85
+
86
+ /**
87
+ * The kinds the search can filter by: the entity types the host's `/ontology`
88
+ * answer lists (an unknown type shows its raw id), or the local Graph's four
89
+ * kinds until that answer has been read.
90
+ */
91
+ function graphTypeFilters(ontologyState) {
92
+ const types = ontologyState?.state === 'ok' && Array.isArray(ontologyState.payload?.entityTypes) ? ontologyState.payload.entityTypes : [];
93
+ if (types.length === 0) return TYPE_FILTERS;
94
+ return [ALL_TYPES, ...types.map((item) => [item.id, typeLabel(item.id)])];
95
+ }
96
+
97
+ /** A type's meaning: the plain Japanese one for a local kind, otherwise what the host's answer says. */
98
+ function typeMeaning(item) {
99
+ return ownLabel(GRAPH_ENTITY_TYPE_MEANINGS, item.id) ?? textOrNull(item.meaning) ?? '';
100
+ }
101
+
102
+ /** Results are `ok` when well formed; no match is shown only when the whole Graph was searched. */
103
+ export function normalizeSearch(result) {
104
+ if (result?.state !== 'ok') return result ?? INVALID;
105
+ const payload = result.payload;
106
+ if (!Array.isArray(payload.results) || !payload.results.every(isEntityView)) return INVALID;
107
+ if (payload.results.length === 0 && payload.absenceConfirmed !== true) return { state: 'unknown' };
108
+ return { state: 'ok', payload };
109
+ }
110
+
111
+ export function normalizeEntityDetail(result) {
112
+ if (result?.state !== 'ok') return result ?? INVALID;
113
+ const payload = result.payload;
114
+ if (!isEntityView(payload.entity)) return INVALID;
115
+ for (const key of ['outgoing', 'incoming']) {
116
+ if (!Array.isArray(payload[key]) || !payload[key].every(isEdgeView)) return INVALID;
117
+ }
118
+ return { state: 'ok', payload };
119
+ }
120
+
121
+ // A host that could not finish counting sends null (with countStatus
122
+ // 'unknown', or 'at_least' plus atLeast) rather than a number it cannot vouch for.
123
+ function isCount(value) {
124
+ return value === null || Number.isInteger(value);
125
+ }
126
+
127
+ function countText(item, key = 'count') {
128
+ if (Number.isInteger(item[key])) return `${item[key]}件`;
129
+ if (key === 'count' && item.countStatus === 'at_least' && Number.isInteger(item.atLeast)) return `${item.atLeast}件以上`;
130
+ return '件数は未確認';
131
+ }
132
+
133
+ export function normalizeOntology(result) {
134
+ if (result?.state !== 'ok') return result ?? INVALID;
135
+ const payload = result.payload;
136
+ const entityTypesOk = Array.isArray(payload.entityTypes) && payload.entityTypes.every((item) => isRecord(item)
137
+ && typeof item.id === 'string' && isCount(item.count));
138
+ const relationsOk = Array.isArray(payload.relations) && payload.relations.every((item) => isRecord(item)
139
+ && typeof item.id === 'string' && typeof item.from === 'string' && typeof item.to === 'string'
140
+ && isCount(item.count) && isCount(item.activeCount));
141
+ if (!entityTypesOk || !relationsOk || !isRecord(payload.ontology)) return INVALID;
142
+ return { state: 'ok', payload };
143
+ }
144
+
145
+ function fieldNames(fields) {
146
+ return (Array.isArray(fields) ? fields : []).map((field) => GRAPH_CORRECTION_FIELDS[field]?.label ?? field).join('・');
147
+ }
148
+
149
+ function textCell(value, fallback) {
150
+ return textOrNull(value) ?? { text: fallback, className: 'is-unresolved' };
151
+ }
152
+
153
+ /**
154
+ * Mounts 「情報と関係」.
155
+ *
156
+ * @param {object} options
157
+ * @param {Element} options.root Where the workspace is drawn.
158
+ * @param {Element} [options.rail] The host's right rail; without it the rail content follows the workspace inside `root`.
159
+ * @param {{ crumbs?: string[], source?: string }} [options.page] Breadcrumb and source label of the page head.
160
+ * @param {Document} [options.document] The document to build elements with (default: the global one).
161
+ * @param {Function} [options.fetcher] `fetch`-compatible function for the Graph routes.
162
+ * @param {string} [options.basePath='/api/graph'] Where the Graph routes are served.
163
+ * @param {string} [options.token] Launch token sent with a correction.
164
+ * @param {string} [options.tokenHeader] Header name for the token.
165
+ * @param {boolean} [options.autoLoad=true] Runs the empty search and reads the kinds on mount.
166
+ * @param {() => Date} [options.now] The owner's clock.
167
+ * @param {boolean} [options.canCorrect=true] When false, no correction control is drawn anywhere
168
+ * (この記録を直す, 関係を加える, この関係を直す, and この画面で直せること under the kinds of
169
+ * information), and the client refuses every correction before sending it.
170
+ * @param {{ entityTypes?: string[], fields?: string[], edges?: boolean, createEdges?: boolean }} [options.correctionScope]
171
+ * With corrections allowed, only these (see `graphCorrectionScope`): `entityTypes` lists the kinds that
172
+ * offer この記録を直す, `fields` limits its form, `edges: false` withholds この関係を直す and
173
+ * `createEdges: false` withholds 関係を加える. A withheld control is not drawn and the client refuses
174
+ * such a correction before sending it. Without it every correction is allowed.
175
+ * @param {string} [options.readOnlyNote] With `canCorrect: false`, or where `correctionScope` withholds a
176
+ * control of the selected record, a note shown in the rail where the correction buttons would be.
177
+ * Without it the rail says nothing about corrections.
178
+ * @param {{ label?: string, text: string | Element }} [options.emptyNotice] Replaces the 未登録 notice
179
+ * and its `brainbase onboard:*` commands when the host's Graph has nothing registered.
180
+ * @param {{ label?: string, text: string | Element }} [options.sourceNotice] Replaces the default 出典 notice.
181
+ * @param {{ basePath: string, fetcher?: Function, label?: string }} [options.ownShare]
182
+ * The owner's own snapshot the host keeps for them (`graph-own-share.js`): the same search runs on
183
+ * `{basePath}/search` and its results are shown read-only in a separate section after the host's
184
+ * results, labelled 「引き継いだ自分の分(組織には未反映)」 unless `label` says otherwise; a record
185
+ * opens below them from `{basePath}/entities/:id`, with a decision's judgment record. They never
186
+ * enter the host's results or rail, and a failed read shows only in that section. The host's
187
+ * results are then captioned as the organization's. Without it nothing of the kind is drawn or read.
188
+ *
189
+ * The type filter lists the entity types of the `/ontology` answer once it has
190
+ * been read (the four local kinds until then). A type without a plain label
191
+ * shows its raw id in the filter, the results ledger and the rail.
192
+ *
193
+ * @returns The controller: `state`, `correction`, `render()`, `load()`, `search()`,
194
+ * `refreshResults()`, `loadEntity(id)`, `openEntity(id)`, `back()`, `loadOntology()`, and with
195
+ * `ownShare` `searchOwn()` and `openOwnEntity(id)`.
196
+ */
197
+ export function createGraphRegistryView({
198
+ root,
199
+ rail,
200
+ page,
201
+ document: explicitDocument,
202
+ fetcher,
203
+ basePath = '/api/graph',
204
+ token,
205
+ tokenHeader,
206
+ autoLoad = true,
207
+ now = () => new Date(),
208
+ canCorrect = true,
209
+ correctionScope,
210
+ readOnlyNote,
211
+ sourceNotice,
212
+ emptyNotice,
213
+ ownShare,
214
+ } = {}) {
215
+ if (!root) throw new TypeError('root is required');
216
+ const doc = getDocument(explicitDocument);
217
+ const graphClient = createGraphClient({ fetcher, basePath, token, ...(tokenHeader ? { tokenHeader } : {}) });
218
+ const own = graphOwnShareSource(ownShare, { fetcher });
219
+ const writable = canCorrect !== false;
220
+ const client = writable ? graphClient : readOnlyGraphClient(graphClient);
221
+ const scope = graphCorrectionScope(correctionScope);
222
+ const edgesWritable = writable && scope.edges;
223
+ const context = graphPageContext(page);
224
+ const layout = createGraphLayout(doc, {
225
+ root,
226
+ rail,
227
+ label: '情報と関係',
228
+ className: 'bb-gr',
229
+ contractVersion: GRAPH_REGISTRY_VIEW_CONTRACT_VERSION,
230
+ });
231
+ const state = {
232
+ // `query` follows the form; `applied` is the search the results were read with.
233
+ query: { q: '', type: '', asOfDay: '' },
234
+ applied: { q: '', type: '', asOfDay: '' },
235
+ queryMessage: null,
236
+ search: { state: 'loading' },
237
+ // `detail` is the record in the rail: { id, state, payload?, pending? }.
238
+ detail: null,
239
+ trail: [],
240
+ ontology: { state: 'loading' },
241
+ // Where the open correction shows: a relation id (inside that relation) or anything else (below the record actions).
242
+ panelAt: null,
243
+ // The owner's own share: the same search on its own routes, and its own open record.
244
+ own: own ? { search: { state: 'loading' }, detail: null } : null,
245
+ };
246
+ let focusRow = null;
247
+
248
+ const correction = createGraphCorrection({
249
+ client,
250
+ now,
251
+ scope: correctionScope,
252
+ rerender: () => controller.render(),
253
+ onSaved: async () => {
254
+ await Promise.all([
255
+ state.detail ? controller.loadEntity(state.detail.id, { keep: true }) : null,
256
+ controller.refreshResults(),
257
+ controller.loadOntology({ keep: true }),
258
+ ]);
259
+ },
260
+ onConflict: async () => {
261
+ if (state.detail) await controller.loadEntity(state.detail.id, { keep: true });
262
+ },
263
+ });
264
+
265
+ function asOfParam(day = state.applied.asOfDay) {
266
+ return day ? dayToRfc3339(day) : null;
267
+ }
268
+
269
+ function openCorrection(at, open) {
270
+ state.panelAt = at;
271
+ open();
272
+ }
273
+
274
+ // -------------------------------------------------------------------------
275
+ // Workspace
276
+
277
+ function renderSearchForm() {
278
+ const section = makeElement(doc, 'section', { className: 'bb-gr-search-section', attrs: { 'aria-label': '検索' } });
279
+ const form = makeElement(doc, 'form', { className: 'bb-graph-form bb-gr-search', attrs: { role: 'search', novalidate: true } });
280
+ form.addEventListener('submit', (event) => {
281
+ event?.preventDefault?.();
282
+ return controller.search();
283
+ });
284
+ const field = (id, label, control) => {
285
+ const wrap = makeElement(doc, 'div', { className: 'bb-graph-field' });
286
+ wrap.append(makeElement(doc, 'label', { text: label, attrs: { for: id } }), control);
287
+ return wrap;
288
+ };
289
+ const bind = (control, key) => {
290
+ const update = (event) => { state.query[key] = event?.target?.value ?? control.value; };
291
+ control.addEventListener('input', update);
292
+ control.addEventListener('change', update);
293
+ };
294
+ const q = makeElement(doc, 'input', { attrs: { id: 'bb-gr-q', name: 'q', type: 'search', maxlength: 200, placeholder: '例: 田中' } });
295
+ q.value = state.query.q;
296
+ bind(q, 'q');
297
+ const type = makeElement(doc, 'select', { attrs: { id: 'bb-gr-type', name: 'type' } });
298
+ for (const [value, label] of graphTypeFilters(state.ontology)) {
299
+ const option = makeElement(doc, 'option', { text: label, attrs: { value } });
300
+ if (value === state.query.type) {
301
+ option.setAttribute('selected', 'selected');
302
+ option.selected = true;
303
+ }
304
+ type.append(option);
305
+ }
306
+ type.value = state.query.type;
307
+ bind(type, 'type');
308
+ const asOf = makeElement(doc, 'input', { attrs: { id: 'bb-gr-as-of', name: 'as_of', type: 'date' } });
309
+ asOf.value = state.query.asOfDay;
310
+ bind(asOf, 'asOfDay');
311
+ form.append(
312
+ field('bb-gr-q', '名前・別名', q),
313
+ field('bb-gr-type', '種類', type),
314
+ field('bb-gr-as-of', '有効時点(空なら今)', asOf),
315
+ makeElement(doc, 'button', { className: 'bb-ws-button is-primary bb-gr-search-submit', text: '探す', attrs: { type: 'submit' } }),
316
+ );
317
+ section.append(form);
318
+ if (state.queryMessage) section.append(workspaceNotice(doc, { label: '入力', text: state.queryMessage, tone: 'danger' }));
319
+ return section;
320
+ }
321
+
322
+ function renderResults() {
323
+ const section = makeElement(doc, 'section', { className: 'bb-gr-results', attrs: { 'aria-label': '検索結果' } });
324
+ const stateNotice = graphStateNotice(doc, state.search, { onRetry: () => controller.search(), loadingText: '記録を探しています。' });
325
+ if (stateNotice) {
326
+ section.append(stateNotice);
327
+ return section;
328
+ }
329
+ const { payload } = state.search;
330
+ if (payload.graphEmpty === true) {
331
+ const hostEmpty = graphHostEmptyNotice(doc, emptyNotice);
332
+ if (hostEmpty) {
333
+ section.append(hostEmpty);
334
+ return section;
335
+ }
336
+ const dir = textOrNull(payload.source?.dataDir);
337
+ const body = makeElement(doc, 'div', { className: 'bb-graph-notice-body' });
338
+ body.append(
339
+ makeElement(doc, 'p', { text: 'まだ登録がありません。このGraphには、人物・組織・プロジェクト・判断がまだ登録されていません。次のコマンドで始められます。CodexやClaude Codeからは、MCPのオンボーディング(brainbase_onboarding_start)で資料から候補を作り、確かめてから登録できます。' }),
340
+ graphCommandList(doc, [`brainbase onboard:start${dir ? ` --dir ${shellArg(dir)}` : ''}`]),
341
+ );
342
+ section.append(workspaceNotice(doc, { label: '未登録', text: body, tone: 'info', role: 'status' }));
343
+ return section;
344
+ }
345
+ const asOf = payload.query?.asOf ?? null;
346
+ // The day the owner picked; the host may answer the same instant in another time zone.
347
+ const asOfDay = asOf ? state.applied.asOfDay || String(asOf).slice(0, 10) : null;
348
+ section.append(makeElement(doc, 'p', {
349
+ className: 'bb-gr-caption',
350
+ // Next to the owner's own share, these results are named as the organization's.
351
+ text: `${own ? '組織のGraphの' : ''}${Number.isInteger(payload.total) ? `検索結果 ${payload.total}件` : `検索結果 ${payload.results.length}件を表示(全体の件数は未確認)`}${asOfDay ? `(${asOfDay} の時点で有効なもの)` : ''}`,
352
+ }));
353
+ section.append(workspaceLedger(doc, {
354
+ className: 'bb-graph-entity-ledger',
355
+ ariaLabel: '検索結果の一覧',
356
+ columns: GRAPH_ENTITY_LEDGER_COLUMNS,
357
+ empty: '条件に合う記録はありません。名前や別名、種類、有効時点を変えて探してください。',
358
+ rows: payload.results.map((entity) => ({
359
+ key: entity.id,
360
+ selected: entity.id === state.detail?.id,
361
+ className: entity.active ? '' : 'is-ended',
362
+ onSelect: (id) => {
363
+ focusRow = id;
364
+ void controller.openEntity(id);
365
+ },
366
+ cells: [
367
+ { primary: entity.name, secondary: entity.id },
368
+ typeLabel(entity.type),
369
+ textCell(entity.summary, '要約なし'),
370
+ validityLabel(entity, asOf ?? now().toISOString()),
371
+ ],
372
+ })),
373
+ }));
374
+ if (payload.truncated) {
375
+ section.append(workspaceNotice(doc, { label: '一部だけ', text: Number.isInteger(payload.total)
376
+ ? `ほかに${payload.total - payload.results.length}件あります。条件を絞って探してください。`
377
+ : 'ほかにもある可能性があります。条件を絞って探してください。', tone: 'info', role: 'status' }));
378
+ }
379
+ return section;
380
+ }
381
+
382
+ function renderOntology() {
383
+ const section = makeElement(doc, 'section', { className: 'bb-gr-ontology', attrs: { 'aria-label': '情報の種類とつながり方' } });
384
+ section.append(workspaceSectionTitle(doc, {
385
+ title: '情報の種類とつながり方',
386
+ lead: 'Graphに登録できる情報の種類と、種類どうしのつながり方です。この欄は表示だけです。',
387
+ }));
388
+ const stateNotice = graphStateNotice(doc, state.ontology, { onRetry: () => controller.loadOntology(), loadingText: '情報の種類を読み込んでいます。' });
389
+ if (stateNotice) {
390
+ section.append(stateNotice);
391
+ return section;
392
+ }
393
+ const { payload } = state.ontology;
394
+ const ontology = payload.ontology;
395
+ section.append(makeElement(doc, 'p', {
396
+ className: 'bb-gr-caption',
397
+ text: `定義の版 ${ontology.upToDate === false
398
+ ? `${String(ontology.version)}(最新ではありません。最新は ${String(ontology.currentVersion)})`
399
+ : `${String(ontology.version)}(最新)`}`,
400
+ }));
401
+ section.append(makeElement(doc, 'h3', { className: 'bb-gr-subheading', text: 'つながり方' }));
402
+ section.append(workspaceLedger(doc, {
403
+ className: 'bb-graph-relation-type-ledger',
404
+ ariaLabel: 'つながり方',
405
+ columns: GRAPH_RELATION_TYPE_LEDGER_COLUMNS,
406
+ rows: payload.relations.map((item) => ({
407
+ key: item.id,
408
+ cells: [
409
+ relationLabel(item.id),
410
+ `${typeLabel(item.from)} → ${typeLabel(item.to)}`,
411
+ ownLabel(GRAPH_RELATION_LABELS, item.id)?.meaning ?? textOrNull(item.meaning) ?? '',
412
+ Number.isInteger(item.count) && Number.isInteger(item.activeCount)
413
+ ? `${item.count}件(うち有効 ${item.activeCount}件)`
414
+ : countText(item),
415
+ ],
416
+ })),
417
+ }));
418
+ section.append(makeElement(doc, 'h3', { className: 'bb-gr-subheading', text: '情報の種類' }));
419
+ section.append(workspaceLedger(doc, {
420
+ className: 'bb-graph-entity-type-ledger',
421
+ ariaLabel: '情報の種類',
422
+ columns: GRAPH_ENTITY_TYPE_LEDGER_COLUMNS,
423
+ rows: payload.entityTypes.map((item) => ({
424
+ key: item.id,
425
+ cells: [typeLabel(item.id), typeMeaning(item), countText(item)],
426
+ })),
427
+ }));
428
+ const corrections = writable && isRecord(payload.corrections) ? payload.corrections : null;
429
+ if (corrections) {
430
+ section.append(makeElement(doc, 'h3', { className: 'bb-gr-subheading', text: 'この画面で直せること' }));
431
+ const list = makeElement(doc, 'ul', { className: 'bb-graph-plain-list' });
432
+ list.append(
433
+ makeElement(doc, 'li', { text: `記録: ${fieldNames(corrections.entityFields)}(プロジェクトは${fieldNames(corrections.projectFields)}も)` }),
434
+ makeElement(doc, 'li', { text: `関係: ${fieldNames(corrections.edgeFields)}` }),
435
+ makeElement(doc, 'li', { text: `新しく加えられる関係: ${(Array.isArray(corrections.newEdgeRelations) ? corrections.newEdgeRelations : []).map(relationLabel).join('・')}` }),
436
+ makeElement(doc, 'li', { text: '記録と関係は削除しません。終わったものは終了日で表します。関係の種類や相手を変えるときは、今の関係を終えてから新しく加えます。' }),
437
+ );
438
+ section.append(list);
439
+ }
440
+ section.append(recordDetails(doc, [['定義ID', ontology.id], ['定義の版', ontology.version], ['release digest', ontology.releaseDigest]]));
441
+ return section;
442
+ }
443
+
444
+ function renderWorkspace() {
445
+ const dir = state.search.state === 'ok' ? textOrNull(state.search.payload.source?.dataDir) : null;
446
+ return [
447
+ workspacePageHeader(doc, {
448
+ crumbs: context.crumbs,
449
+ title: '情報と関係',
450
+ lead: writable ? 'Graphに何がどう登録されているかを確かめ、誤りを直します。' : 'Graphに何がどう登録されているかを確かめます。',
451
+ source: context.source,
452
+ }),
453
+ workspaceHostNotice(doc, sourceNotice) ?? workspaceNotice(doc, {
454
+ label: '出典',
455
+ text: `このMacのGraph${dir ? `(${dir})` : ''}だけを表示します。ここで直した内容は、同じGraphを読むMCPの search・get_context・resolve_entity で次から使われます。`,
456
+ }),
457
+ state.detail?.state === 'ok' ? renderGraphIssues(doc, state.detail.payload.issues) : null,
458
+ renderSearchForm(),
459
+ renderResults(),
460
+ own ? renderOwnShare() : null,
461
+ renderOntology(),
462
+ ];
463
+ }
464
+
465
+ /** The owner's own share: the same search on its own routes, never among the host's results. */
466
+ function renderOwnShare() {
467
+ const ownState = state.own;
468
+ const notice = ownShareStateNotice(doc, ownState.search, { onRetry: () => controller.searchOwn(), loadingText: '引き継いだ自分の分を探しています。' });
469
+ if (notice) return ownShareSection(doc, { label: own.label, children: [notice] });
470
+ const { payload } = ownState.search;
471
+ const asOf = payload.query?.asOf ?? null;
472
+ const children = [makeElement(doc, 'p', {
473
+ className: 'bb-gr-caption',
474
+ text: `${payload.graphEmpty === true ? '束に記録がありません' : `検索結果 ${payload.total}件`}${payload.truncated ? `(うち${payload.results.length}件を表示)` : ''}`,
475
+ })];
476
+ if (payload.graphEmpty !== true) {
477
+ children.push(workspaceLedger(doc, {
478
+ className: 'bb-graph-entity-ledger bb-graph-own-ledger',
479
+ ariaLabel: `${own.label}の検索結果`,
480
+ columns: GRAPH_ENTITY_LEDGER_COLUMNS,
481
+ empty: '条件に合う記録は、この束にはありません。',
482
+ rows: payload.results.map((entity) => ({
483
+ key: entity.id,
484
+ selected: entity.id === ownState.detail?.id,
485
+ className: entity.active ? '' : 'is-ended',
486
+ onSelect: (id) => { void controller.openOwnEntity(id); },
487
+ cells: [
488
+ { primary: entity.name, secondary: entity.id },
489
+ typeLabel(entity.type),
490
+ textCell(entity.summary, '要約なし'),
491
+ validityLabel(entity, asOf ?? now().toISOString()),
492
+ ],
493
+ })),
494
+ }));
495
+ }
496
+ if (ownState.detail) {
497
+ children.push(ownEntityDetail(doc, ownState.detail, {
498
+ onClose: () => controller.openOwnEntity(null),
499
+ onRetry: () => controller.openOwnEntity(ownState.detail.id),
500
+ onOpen: (id) => controller.openOwnEntity(id),
501
+ }));
502
+ }
503
+ return ownShareSection(doc, { label: own.label, handover: payload.handover, children });
504
+ }
505
+
506
+ // -------------------------------------------------------------------------
507
+ // Right rail
508
+
509
+ function relationItem(edge, entity, asOf) {
510
+ const labels = ownLabel(GRAPH_RELATION_LABELS, edge.relation);
511
+ const item = makeElement(doc, 'li', { className: `bb-graph-compact${edge.active ? '' : ' is-ended'}` });
512
+ const head = makeElement(doc, 'div', { className: 'bb-graph-compact-head' });
513
+ const phrase = makeElement(doc, 'span', { className: 'bb-graph-relation' });
514
+ phrase.append(
515
+ badge(doc, relationLabel(edge.relation), 'accent'),
516
+ button(doc, edge.counterpart.name, () => controller.openEntity(edge.counterpart.id, {
517
+ from: entity,
518
+ pending: { id: edge.counterpart.id, name: edge.counterpart.name, type: edge.counterpart.type },
519
+ }), { className: 'bb-graph-link' }),
520
+ makeElement(doc, 'span', { text: labels ? (edge.direction === 'outgoing' ? labels.outgoing : labels.incoming) : '' }),
521
+ );
522
+ head.append(phrase, activityBadge(doc, edge, asOf));
523
+ item.append(head, workspaceDefinition(doc, [
524
+ ['関係の意味', labels?.meaning ?? edge.relation],
525
+ ['相手の種類', typeLabel(edge.counterpart.type)],
526
+ ['役割', textOrNull(edge.role)],
527
+ ...(textOrNull(edge.context) ? [['文脈', edge.context]] : []),
528
+ ['有効期間', validityText(edge.validFrom, edge.validTo)],
529
+ ['出典', renderProvenance(doc, edge.provenance)],
530
+ ]));
531
+ const subject = edge.direction === 'outgoing'
532
+ ? `${entity.name} → ${relationLabel(edge.relation)} → ${edge.counterpart.name}`
533
+ : `${edge.counterpart.name} → ${relationLabel(edge.relation)} → ${entity.name}`;
534
+ if (edgesWritable) {
535
+ item.append(workspaceActions(doc, [
536
+ workspaceButton(doc, { text: 'この関係を直す', variant: 'quiet', onClick: () => openCorrection(edge.id, () => correction.openEdge(edge, { mode: 'edit', subject })) }),
537
+ ]));
538
+ }
539
+ item.append(recordDetails(doc, [
540
+ ['関係ID', edge.id],
541
+ ['関係の種類ID', edge.relation],
542
+ ['起点ID', edge.fromId],
543
+ ['終点ID', edge.toId],
544
+ ['出典ID', edge.provenance.sourceId],
545
+ ['digest', edge.digest],
546
+ ]));
547
+ if (edgesWritable && state.panelAt === edge.id) {
548
+ const panel = correction.render(doc);
549
+ if (panel) item.append(panel);
550
+ }
551
+ return item;
552
+ }
553
+
554
+ function renderRelations(payload, direction) {
555
+ const { entity } = payload;
556
+ const kind = typeLabel(entity.type);
557
+ const outgoing = direction === 'outgoing';
558
+ const content = [makeElement(doc, 'p', {
559
+ className: 'bb-graph-block-lead',
560
+ text: outgoing
561
+ ? `この${kind}が起点になっている関係です。相手の名前を押すと、その記録を開きます。`
562
+ : `ほかの記録からこの${kind}へ向かう関係です。相手の名前を押すと、その記録を開きます。`,
563
+ })];
564
+ const edges = payload[direction];
565
+ if (edges.length === 0) {
566
+ content.push(makeElement(doc, 'p', { className: 'bb-graph-empty', text: outgoing ? 'この記録から出る関係はありません。' : 'この記録へ入る関係はありません。' }));
567
+ } else {
568
+ const list = makeElement(doc, 'ul', { className: 'bb-graph-compact-list' });
569
+ for (const edge of edges) list.append(relationItem(edge, entity, payload.asOf));
570
+ content.push(list);
571
+ }
572
+ return workspaceRailBlock(doc, { title: outgoing ? '出る関係' : '入る関係', content });
573
+ }
574
+
575
+ function validityValue(entity, asOf) {
576
+ const wrap = makeElement(doc, 'span', { className: 'bb-graph-inline-value' });
577
+ wrap.append(makeElement(doc, 'span', { text: validityText(entity.validFrom, entity.validTo) }), activityBadge(doc, entity, asOf));
578
+ return wrap;
579
+ }
580
+
581
+ function renderRail() {
582
+ const detail = state.detail;
583
+ if (!detail) {
584
+ return [workspaceDetailEmpty(doc, {
585
+ mark: 'G',
586
+ title: '記録を選択',
587
+ text: '検索結果から選ぶと、別名・要約と、出る関係・入る関係を出典つきで表示します。',
588
+ })];
589
+ }
590
+ const children = [];
591
+ const previous = state.trail[state.trail.length - 1];
592
+ if (previous) {
593
+ children.push(button(doc, `← 前の記録(${previous.name})に戻る`, () => controller.back(), { className: 'bb-graph-link bb-graph-back' }));
594
+ }
595
+ const loaded = detail.state === 'ok' ? detail.payload : null;
596
+ const entity = loaded?.entity ?? detail.pending ?? { id: detail.id, name: detail.id, type: null };
597
+ children.push(workspaceRailHead(doc, { kicker: entity.type ? typeLabel(entity.type) : '記録', title: entity.name, sub: entity.id }));
598
+ if (!loaded) {
599
+ children.push(graphStateNotice(doc, detail, { onRetry: () => controller.loadEntity(detail.id), loadingText: 'この記録を読み込んでいます。' }));
600
+ return children;
601
+ }
602
+ const summaryRows = [
603
+ ['別名', entity.aliases.length > 0 ? entity.aliases.join('、') : { text: 'なし', className: 'is-unrecorded' }],
604
+ ['要約', textOrNull(entity.summary)],
605
+ ['有効期間', validityValue(entity, loaded.asOf)],
606
+ ...(entity.type === 'project' ? [['目的', textOrNull(entity.goal)], ['状態', textOrNull(entity.status)]] : []),
607
+ ...(state.applied.asOfDay ? [['見ている時点', `${state.applied.asOfDay}(有効時点で絞っています)`]] : []),
608
+ ];
609
+ children.push(workspaceRailBlock(doc, {
610
+ title: '概要',
611
+ content: [
612
+ workspaceDefinition(doc, summaryRows),
613
+ recordDetails(doc, [['記録ID', entity.id], ['種類ID', entity.type], ['digest', entity.digest], ['読み取った時点', loaded.asOf]]),
614
+ ],
615
+ }));
616
+ if (writable) {
617
+ const canFix = scope.entity(entity.type) && scope.fields(graphEntityCorrectionFields(entity.type)).length > 0;
618
+ const relatable = newRelationOptions(entity.type).length > 0;
619
+ const related = loaded.outgoing.length + loaded.incoming.length > 0;
620
+ const actions = [
621
+ canFix ? workspaceButton(doc, { text: 'この記録を直す', onClick: () => openCorrection('entity', () => correction.openEntity(entity, { title: 'この記録を直す' })) }) : null,
622
+ relatable && scope.createEdges
623
+ ? workspaceButton(doc, { text: '関係を加える', onClick: () => openCorrection('entity', () => correction.openCreate(entity, { title: '関係を加える' })) })
624
+ : null,
625
+ ].filter(Boolean);
626
+ if (actions.length > 0) children.push(workspaceActions(doc, actions));
627
+ // A correction opened on a relation shows inside that relation; any other shows here.
628
+ const inRelation = [...loaded.outgoing, ...loaded.incoming].some((edge) => edge.id === state.panelAt);
629
+ if (!inRelation) children.push(correction.render(doc));
630
+ // The host's reason for a control it withholds, where that control would be.
631
+ if (!canFix || (relatable && !scope.createEdges) || (related && !scope.edges)) children.push(hostReadOnlyNote(doc, readOnlyNote));
632
+ } else {
633
+ children.push(hostReadOnlyNote(doc, readOnlyNote));
634
+ }
635
+ children.push(renderRelations(loaded, 'outgoing'), renderRelations(loaded, 'incoming'));
636
+ children.push(renderCorrectionHistory(doc, loaded.history));
637
+ return children;
638
+ }
639
+
640
+ function closeCorrection() {
641
+ if (correction.form) correction.close();
642
+ state.panelAt = null;
643
+ }
644
+
645
+ function searchParams(query) {
646
+ return { q: query.q.trim(), type: query.type, as_of: asOfParam(query.asOfDay), limit: GRAPH_SEARCH_LIMIT };
647
+ }
648
+
649
+ async function readSearch(query) {
650
+ return normalizeSearch(await client.read('/search', searchParams(query)));
651
+ }
652
+
653
+ /** Keeps the record in the rail only when it is among the new results. */
654
+ async function followResults() {
655
+ const detail = state.detail;
656
+ if (!detail) return;
657
+ const listed = state.search.state === 'ok' && state.search.payload.results.some((entity) => entity.id === detail.id);
658
+ if (!listed) {
659
+ closeCorrection();
660
+ state.detail = null;
661
+ state.trail = [];
662
+ return;
663
+ }
664
+ await controller.loadEntity(detail.id, { keep: true });
665
+ }
666
+
667
+ const controller = {
668
+ get state() { return state; },
669
+ get correction() { return correction; },
670
+ render() {
671
+ layout.render(renderWorkspace(), renderRail());
672
+ if (focusRow) {
673
+ focusLedgerRow(root, focusRow);
674
+ focusRow = null;
675
+ }
676
+ return controller;
677
+ },
678
+ /** Runs the search in the form. The record in the rail stays while it is among the results. */
679
+ async search() {
680
+ if (state.query.asOfDay && !dayToRfc3339(state.query.asOfDay)) {
681
+ state.queryMessage = '有効時点の日付の形式が正しくありません。';
682
+ controller.render();
683
+ return state.search;
684
+ }
685
+ state.queryMessage = null;
686
+ state.applied = { ...state.query };
687
+ state.search = { state: 'loading' };
688
+ controller.render();
689
+ const applied = state.applied;
690
+ // The owner's own share answers the same search on its own; neither waits on the other's failure.
691
+ const owning = own ? controller.searchOwn() : null;
692
+ const result = await readSearch(applied);
693
+ if (state.applied !== applied) {
694
+ await owning;
695
+ return state.search;
696
+ }
697
+ state.search = result;
698
+ await followResults();
699
+ controller.render();
700
+ await owning;
701
+ return state.search;
702
+ },
703
+ /** Runs the applied search on the owner's own share (with `ownShare`); a failure stays in that section. */
704
+ async searchOwn() {
705
+ if (!own) return null;
706
+ const applied = state.applied;
707
+ state.own.search = { state: 'loading' };
708
+ controller.render();
709
+ const result = await own.client.read('/search', searchParams(applied));
710
+ if (state.applied !== applied) return state.own.search;
711
+ // An empty answer is an answer here: the host confirms the whole snapshot was searched.
712
+ state.own.search = result.state === 'ok' && !(Array.isArray(result.payload.results) && result.payload.results.every(isEntityView)) ? INVALID : result;
713
+ const openId = state.own.detail?.id;
714
+ if (openId && !(state.own.search.state === 'ok' && state.own.search.payload.results.some((entity) => entity.id === openId))) state.own.detail = null;
715
+ controller.render();
716
+ return state.own.search;
717
+ },
718
+ /** Opens (or with null closes) a record of the owner's own share below its results. */
719
+ async openOwnEntity(id) {
720
+ if (!own) return null;
721
+ const target = textOrNull(id);
722
+ if (!target) {
723
+ state.own.detail = null;
724
+ controller.render();
725
+ return null;
726
+ }
727
+ const listed = state.own.search.state === 'ok' ? state.own.search.payload.results.find((entity) => entity.id === target) : null;
728
+ const pending = listed ?? (state.own.detail?.state === 'ok' ? [...state.own.detail.payload.outgoing, ...state.own.detail.payload.incoming]
729
+ .map((edge) => edge.counterpart).find((counterpart) => counterpart.id === target) : null) ?? null;
730
+ state.own.detail = { id: target, state: 'loading', pending: pending ? { id: target, name: pending.name, type: pending.type } : null };
731
+ controller.render();
732
+ const result = await own.client.read(`/entities/${encodeURIComponent(target)}`, { as_of: asOfParam() });
733
+ if (state.own.detail?.id !== target) return state.own.detail;
734
+ const base = { id: target, pending: state.own.detail.pending };
735
+ state.own.detail = result.state === 'error' && result.status === 404
736
+ ? { ...base, state: 'not_found', reason: 'この記録は束の中に見つかりません。' }
737
+ : { ...base, ...normalizeEntityDetail(result) };
738
+ controller.render();
739
+ return state.own.detail;
740
+ },
741
+ /** Reads the last search again without showing it as loading (after a correction). */
742
+ async refreshResults() {
743
+ const applied = state.applied;
744
+ const result = await readSearch(applied);
745
+ if (state.applied !== applied) return state.search;
746
+ state.search = result;
747
+ controller.render();
748
+ return state.search;
749
+ },
750
+ /** `keep` shows the current record until the new one has been read. */
751
+ async loadEntity(id, { keep = false, pending } = {}) {
752
+ const previous = state.detail;
753
+ if (!(keep && previous?.id === id && previous.state === 'ok')) {
754
+ // What is already known about the record (name, kind) heads the rail while it loads.
755
+ const known = pending ?? (previous?.id === id ? previous.payload?.entity ?? previous.pending ?? null : null);
756
+ state.detail = { id, state: 'loading', pending: known ? { id, name: known.name, type: known.type } : null };
757
+ controller.render();
758
+ }
759
+ const result = await client.read(`/entities/${encodeURIComponent(id)}`, { as_of: asOfParam() });
760
+ if (state.detail?.id !== id) return state.detail;
761
+ const base = { id, pending: state.detail.pending ?? null };
762
+ state.detail = result.state === 'error' && result.status === 404
763
+ ? { ...base, state: 'not_found', reason: 'この記録は見つかりません。検索結果から探し直してください。' }
764
+ : { ...base, ...normalizeEntityDetail(result) };
765
+ controller.render();
766
+ return state.detail;
767
+ },
768
+ /**
769
+ * Opens a record in the rail. `from` (the record being left) makes 戻る
770
+ * return to it; a row of the results starts a new trail.
771
+ */
772
+ async openEntity(id, { from, pending } = {}) {
773
+ closeCorrection();
774
+ if (from) state.trail.push({ id: from.id, name: from.name, type: from.type });
775
+ else state.trail = [];
776
+ const listed = state.search.state === 'ok' ? state.search.payload.results.find((entity) => entity.id === id) : null;
777
+ return controller.loadEntity(id, { keep: true, pending: pending ?? listed ?? null });
778
+ },
779
+ async back() {
780
+ closeCorrection();
781
+ const previous = state.trail.pop();
782
+ if (!previous) return state.detail;
783
+ return controller.loadEntity(previous.id, { pending: previous });
784
+ },
785
+ async loadOntology({ keep = false } = {}) {
786
+ if (!(keep && state.ontology.state === 'ok')) {
787
+ state.ontology = { state: 'loading' };
788
+ controller.render();
789
+ }
790
+ state.ontology = normalizeOntology(await client.read('/ontology'));
791
+ controller.render();
792
+ return state.ontology;
793
+ },
794
+ async load() {
795
+ await Promise.all([controller.search(), controller.loadOntology()]);
796
+ return state;
797
+ },
798
+ };
799
+ controller.render();
800
+ if (autoLoad) void controller.load();
801
+ return controller;
802
+ }
803
+
804
+ export default createGraphRegistryView;