@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,688 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { readFile, stat } from 'node:fs/promises';
3
+ import { createServer } from 'node:http';
4
+ import { join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { createCompanyOsObjectives } from './company-os-objectives.js';
7
+ import { foundationCatalogOrEmpty, foundationKey } from './foundation-catalog.js';
8
+ import { createFoundationHttpRouter, createObjectiveFoundationRoute, FOUNDATION_HTTP_PREFIX, FOUNDATION_OBJECTIVE_ROUTE_PREFIX } from './foundation-http.js';
9
+ import { createFoundationRevisionStore, FoundationStoreError } from './foundation-store.js';
10
+ import { createGraphWebHttpHandler } from './graph-web-http.js';
11
+ import { defaultJudgmentJournalRoot } from './judgment-value-proof-review.js';
12
+ import { nodeRequestToFetch, writeFetchResponse } from './local-web-fetch-bridge.js';
13
+ import { assertSameOrigin, isLoopbackHost, LOCAL_WEB_CONTENT_SECURITY_POLICY, LocalWebHttpError, requestUrl, tokenMatches, writeHttpError, writeJson } from './local-web-security.js';
14
+ import { resolveDataDir } from './paths.js';
15
+ import { loadPersonalOs, readPersonalOsSidecar } from './ssot.js';
16
+ import { createValueProofReviewHttpHandler, VALUE_PROOF_REVIEW_TOKEN_HEADER } from './value-proof-review-http.js';
17
+ import { createWorldModelStore, WORLD_MODEL_EVIDENCE_SIDECAR, WorldModelStoreError } from './world-model.js';
18
+ /**
19
+ * General local single-owner Web host (`brainbase web:serve`).
20
+ *
21
+ * One loopback server carries every local screen. The host owns the
22
+ * protections, the trusted principal and the static allowlist; each screen's
23
+ * server side is a `LocalWebModule`. To add a screen, add its module to
24
+ * `defaultLocalWebModules()` and its nav entry to `LOCAL_WEB_SCREENS` in
25
+ * `ui/local-web-shell.js`.
26
+ */
27
+ export const LOCAL_WEB_HOST_VERSION = 'local-web-host.v1';
28
+ /** One per-launch token guards every write on this host, including value-proof feedback. */
29
+ export const LOCAL_WEB_TOKEN_HEADER = VALUE_PROOF_REVIEW_TOKEN_HEADER;
30
+ export const LOCAL_WEB_DEFAULT_PORT = 31080;
31
+ /** Onboarding registers the owner as `self`; a graph without an owner id uses the same principal. */
32
+ export const LOCAL_WEB_DEFAULT_OWNER_ID = 'self';
33
+ export const LOCAL_WEB_STATUS_PATH = '/api/local/status';
34
+ export const LOCAL_WEB_WORLD_MODEL_PREFIX = '/api/world-model';
35
+ export const LOCAL_WEB_GRAPH_PREFIX = '/api/graph';
36
+ /**
37
+ * An Objective is a few KB of text and criteria. 64 KiB leaves room for long
38
+ * Japanese text (about 20k characters) while keeping a loopback request that
39
+ * holds the SSOT lock far below the Fetch router's 1 MiB default.
40
+ */
41
+ export const LOCAL_WEB_FOUNDATION_BODY_LIMIT_BYTES = 64 * 1024;
42
+ export const LOCAL_WEB_PROVENANCE_SOURCE_ID = 'brainbase-local-web';
43
+ const CANONICAL_FILES = ['graph.json', 'relationships.json', 'personal-kg.jsonl', 'decisions.jsonl'];
44
+ const BASE_UI_FILES = ['brainbase-tokens.css', 'local-web-shell.js', 'local-web-shell.css', 'workspace-kit.js', 'workspace-kit.css'];
45
+ const CONTENT_TYPES = Object.freeze({
46
+ '.js': 'text/javascript; charset=utf-8',
47
+ '.css': 'text/css; charset=utf-8'
48
+ });
49
+ /** True only for a write that carries the launch token from this origin. */
50
+ export function isTrustedLocalWrite(request, token) {
51
+ try {
52
+ assertSameOrigin(request);
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ return tokenMatches(token, request.headers[LOCAL_WEB_TOKEN_HEADER]);
58
+ }
59
+ function rejectUntrustedWrite(request, token) {
60
+ try {
61
+ assertSameOrigin(request);
62
+ }
63
+ catch (error) {
64
+ return error instanceof LocalWebHttpError ? error : new LocalWebHttpError(403, 'cross_origin_rejected', 'Origin is not allowed');
65
+ }
66
+ if (!tokenMatches(token, request.headers[LOCAL_WEB_TOKEN_HEADER])) {
67
+ return new LocalWebHttpError(403, 'web_token_required', 'A valid launch token is required');
68
+ }
69
+ return null;
70
+ }
71
+ /** Reads the Graph format and owner without creating or migrating anything. */
72
+ export async function readLocalGraphState(dataDir) {
73
+ const present = [];
74
+ for (const file of CANONICAL_FILES) {
75
+ try {
76
+ await stat(join(dataDir, file));
77
+ present.push(file);
78
+ }
79
+ catch (error) {
80
+ if (error.code !== 'ENOENT') {
81
+ return { status: 'unreadable', format: null, ownerId: null, message: errorMessage(error) };
82
+ }
83
+ }
84
+ }
85
+ if (present.length === 0)
86
+ return { status: 'not_initialized', format: null, ownerId: null };
87
+ let os;
88
+ try {
89
+ os = await loadPersonalOs(dataDir);
90
+ }
91
+ catch (error) {
92
+ return { status: 'unreadable', format: null, ownerId: null, message: errorMessage(error) };
93
+ }
94
+ if (os.graph.version === 1)
95
+ return { status: 'migration_required', format: 'v1', ownerId: null };
96
+ const ownerId = os.graph.owner?.id;
97
+ return {
98
+ status: 'ready',
99
+ format: 'v2',
100
+ ownerId: typeof ownerId === 'string' && ownerId.trim() ? ownerId : LOCAL_WEB_DEFAULT_OWNER_ID
101
+ };
102
+ }
103
+ function shellArg(value) {
104
+ return /^[A-Za-z0-9_./:@%+=,-]+$/u.test(value) ? value : JSON.stringify(value);
105
+ }
106
+ function graphCommands(state, dataDir) {
107
+ const dir = shellArg(dataDir);
108
+ if (state.status === 'migration_required') {
109
+ // The CLI refuses a write without the digest of a fresh preview.
110
+ return [
111
+ `brainbase ontology:migrate --dir ${dir}`,
112
+ `brainbase ontology:migrate --dir ${dir} --write --expected-input-digest <1行目で表示されたinputDigest>`
113
+ ];
114
+ }
115
+ if (state.status === 'not_initialized')
116
+ return [`brainbase onboard:init --dir ${dir}`];
117
+ return [];
118
+ }
119
+ function writeGraphUnavailable(response, state, dataDir) {
120
+ const code = state.status === 'migration_required' ? 'graph_migration_required'
121
+ : state.status === 'not_initialized' ? 'personal_os_not_initialized'
122
+ : 'personal_os_unreadable';
123
+ const message = state.status === 'migration_required'
124
+ ? 'Graph v1 must be migrated to v2 before objectives and the world model can be read or saved. This host never migrates automatically.'
125
+ : state.status === 'not_initialized'
126
+ ? 'The personal OS data directory has not been initialized.'
127
+ : state.message ?? 'The personal OS data could not be read.';
128
+ writeJson(response, 503, { error: { code, message, commands: graphCommands(state, dataDir) } });
129
+ }
130
+ export function createLocalStatusModule(context) {
131
+ return {
132
+ id: 'local-status',
133
+ uiFiles: [],
134
+ async handle(request, response) {
135
+ if (requestUrl(request).pathname !== LOCAL_WEB_STATUS_PATH)
136
+ return false;
137
+ if (request.method !== 'GET') {
138
+ writeJson(response, 405, { error: { code: 'method_not_allowed', message: 'Use GET' } });
139
+ return true;
140
+ }
141
+ const graph = await readLocalGraphState(context.dataDir);
142
+ writeJson(response, 200, {
143
+ version: LOCAL_WEB_HOST_VERSION,
144
+ data_dir: context.dataDir,
145
+ journal_root: context.journalRoot,
146
+ graph: {
147
+ status: graph.status,
148
+ format: graph.format,
149
+ ...(graph.message ? { message: graph.message } : {}),
150
+ commands: graphCommands(graph, context.dataDir)
151
+ },
152
+ // This host reads only the local data directory (C1 is not connected here).
153
+ organization_graph: { status: 'not_connected' }
154
+ });
155
+ return true;
156
+ }
157
+ };
158
+ }
159
+ export function createValueProofModule(context) {
160
+ const handler = createValueProofReviewHttpHandler({
161
+ journalRoot: context.journalRoot,
162
+ dataDir: context.dataDir,
163
+ token: context.token,
164
+ now: context.now
165
+ });
166
+ return {
167
+ id: 'value-proofs',
168
+ uiFiles: ['value-proof-review.js', 'value-proof-review.css'],
169
+ handle: handler
170
+ };
171
+ }
172
+ const NOTHING_UNREADABLE = Object.freeze({ count: 0, codes: [] });
173
+ function isFoundationDenial(error) {
174
+ return error instanceof FoundationStoreError && (error.code === 'authorization_denied' || error.code === 'scope_violation');
175
+ }
176
+ /** Per-record refusals while reading one observation or adoption (its Variable/Model read included). */
177
+ function isWorldModelDenial(error) {
178
+ return (error instanceof WorldModelStoreError && (error.code === 'authorization_denied' || error.code === 'invalid_input'))
179
+ || isFoundationDenial(error);
180
+ }
181
+ /**
182
+ * `FoundationRevisionStore.list` throws when one record is not readable. One
183
+ * foreign record must not hide every readable record, so on a denial each
184
+ * latest record is read on its own and the unreadable ones are counted.
185
+ */
186
+ async function listReadableFoundation(store, dataDir, type, context) {
187
+ try {
188
+ return { records: await store.list(type, context), unreadable: NOTHING_UNREADABLE };
189
+ }
190
+ catch (error) {
191
+ if (!isFoundationDenial(error))
192
+ throw error;
193
+ }
194
+ const os = await loadPersonalOs(dataDir);
195
+ if (os.graph.version !== 2)
196
+ throw new FoundationStoreError('unsupported_graph', 'Foundation reads require canonical Graph v2');
197
+ const catalog = foundationCatalogOrEmpty(os.graph.foundation);
198
+ const latest = catalog.records.filter((record) => record.definition.type === type
199
+ && catalog.latest[foundationKey(record.definition)]?.revision === record.definition.revision);
200
+ const records = [];
201
+ const codes = new Set();
202
+ let count = 0;
203
+ for (const record of latest) {
204
+ try {
205
+ const readable = await store.readLatest(type, record.definition.id, context);
206
+ if (readable)
207
+ records.push(readable);
208
+ }
209
+ catch (error) {
210
+ if (!isFoundationDenial(error))
211
+ throw error;
212
+ count += 1;
213
+ codes.add(error.code);
214
+ }
215
+ }
216
+ return { records, unreadable: { count, codes: [...codes] } };
217
+ }
218
+ function collectionPayload(key, collection) {
219
+ const { records, unreadable } = collection;
220
+ const state = records.length > 0
221
+ ? unreadable.count > 0 ? 'partial' : 'ready'
222
+ : unreadable.count > 0 ? 'unknown' : 'empty';
223
+ // Absence is confirmed only when every stored record was readable.
224
+ return { state, [key]: records, absence_confirmed: unreadable.count === 0, unreadable };
225
+ }
226
+ function fetchJson(status, body) {
227
+ return new Response(JSON.stringify(body), {
228
+ status,
229
+ headers: { 'Content-Type': 'application/json; charset=utf-8' }
230
+ });
231
+ }
232
+ const OBJECTIVE_DOMAIN_FIELDS = [
233
+ 'meaning',
234
+ 'beneficiaryIds',
235
+ 'desiredState',
236
+ 'criteria',
237
+ 'evaluationPeriod',
238
+ 'accountableId',
239
+ 'adoptionState',
240
+ 'epistemicState'
241
+ ];
242
+ function isBlank(value) {
243
+ return value === undefined || value === null || (typeof value === 'string' && value.trim() === '');
244
+ }
245
+ /** Copies only Objective domain fields; authority fields never come from the body. */
246
+ function objectiveDomainFields(body) {
247
+ const picked = {};
248
+ for (const key of OBJECTIVE_DOMAIN_FIELDS)
249
+ if (body[key] !== undefined)
250
+ picked[key] = body[key];
251
+ const period = picked.evaluationPeriod;
252
+ // An untouched period in the editor arrives as two empty strings: not set.
253
+ if (period && typeof period === 'object' && !Array.isArray(period)
254
+ && isBlank(period.from) && isBlank(period.until)) {
255
+ delete picked.evaluationPeriod;
256
+ }
257
+ if (isBlank(picked.accountableId))
258
+ delete picked.accountableId;
259
+ return picked;
260
+ }
261
+ /**
262
+ * Trusted authority fields for a single owner. The owner writes through the
263
+ * launch-token browser session, so a new Objective is private to the owner,
264
+ * scoped to the owner, and usable for drafting, judgment and evaluation. It is
265
+ * never authorized for execution from the Web. Updates keep every authority
266
+ * field byte-identical to the current revision.
267
+ */
268
+ export function createLocalObjectiveDefinitionBuilder(now) {
269
+ // Domain fields are proposals; the store validates them as a draft on save.
270
+ return ({ operation, body, context, current }) => {
271
+ const domain = objectiveDomainFields(body);
272
+ if (operation === 'update') {
273
+ if (!current)
274
+ throw new FoundationStoreError('not_found', 'The current Objective is required for an update');
275
+ return {
276
+ ...domain,
277
+ id: current.id,
278
+ type: 'objective',
279
+ revision: current.revision,
280
+ acl: current.acl,
281
+ scope: current.scope,
282
+ storage: current.storage,
283
+ provenance: current.provenance,
284
+ authorizedUses: current.authorizedUses
285
+ };
286
+ }
287
+ const id = typeof body.id === 'string' ? body.id.trim() : '';
288
+ if (!id)
289
+ throw new FoundationStoreError('invalid_input', 'id is required');
290
+ return {
291
+ ...domain,
292
+ id,
293
+ type: 'objective',
294
+ revision: '1',
295
+ acl: { ownerId: context.principal, visibility: 'private', readerIds: [], writerIds: [] },
296
+ scope: { subjectIds: [context.principal], validFrom: now().toISOString() },
297
+ storage: 'ontology',
298
+ provenance: [{ sourceId: LOCAL_WEB_PROVENANCE_SOURCE_ID, sourceKind: 'document', evidenceIds: [] }],
299
+ authorizedUses: ['draft', 'judgment', 'evaluation']
300
+ };
301
+ };
302
+ }
303
+ async function listObjectiveConstraintRefs(store, dataDir, reference, context) {
304
+ const os = await loadPersonalOs(dataDir);
305
+ if (os.graph.version !== 2)
306
+ throw new FoundationStoreError('unsupported_graph', 'Foundation reads require canonical Graph v2');
307
+ const catalog = foundationCatalogOrEmpty(os.graph.foundation);
308
+ // `applies_to` names an exact Objective revision; it does not extend to other revisions.
309
+ const sources = new Map();
310
+ for (const relation of catalog.relations) {
311
+ if (relation.relation !== 'applies_to' || relation.source.type !== 'constraint')
312
+ continue;
313
+ if (relation.target.type !== 'objective' || relation.target.id !== reference.id || relation.target.revision !== reference.revision)
314
+ continue;
315
+ if (typeof relation.source.revision !== 'string')
316
+ continue;
317
+ const source = { id: relation.source.id, type: 'constraint', revision: relation.source.revision };
318
+ sources.set(JSON.stringify([source.id, source.revision]), source);
319
+ }
320
+ const refs = [];
321
+ const codes = new Set();
322
+ let count = 0;
323
+ for (const source of sources.values()) {
324
+ try {
325
+ const record = await store.read(source, context);
326
+ if (!record) {
327
+ count += 1;
328
+ codes.add('not_found');
329
+ continue;
330
+ }
331
+ refs.push({ ...source, digest: record.digest, meaning: record.definition.meaning });
332
+ }
333
+ catch (error) {
334
+ if (!isFoundationDenial(error))
335
+ throw error;
336
+ count += 1;
337
+ codes.add(error.code);
338
+ }
339
+ }
340
+ return collectionPayload('refs', { records: refs, unreadable: { count, codes: [...codes] } });
341
+ }
342
+ /**
343
+ * Objective routes of `createObjectiveFoundationRoute`, wired to the local
344
+ * Graph. The principal is the Graph owner; the CSRF verifier is the host's
345
+ * token and same-origin check on the original Node request. Constraint links
346
+ * are read-only because no OSS writer can replace them.
347
+ */
348
+ export function createObjectiveFoundationModule(context) {
349
+ const store = createFoundationRevisionStore({ dataDir: context.dataDir });
350
+ const objectives = createCompanyOsObjectives(store);
351
+ const principals = new WeakMap();
352
+ const origins = new WeakMap();
353
+ const listRoute = {
354
+ name: 'local-objective-list',
355
+ methods: ['GET'],
356
+ paths: ['GET /api/foundation/objectives'],
357
+ matches(request) {
358
+ return (request.method || 'GET').toUpperCase() === 'GET'
359
+ && new URL(request.url, 'http://localhost').pathname === FOUNDATION_OBJECTIVE_ROUTE_PREFIX;
360
+ },
361
+ async handle(_request, input) {
362
+ const listed = await listReadableFoundation(store, context.dataDir, 'objective', input.context);
363
+ const records = [];
364
+ for (const record of listed.records) {
365
+ try {
366
+ const readiness = await objectives.checkObjectiveReadiness(record.definition.id, input.context, record.definition.revision);
367
+ records.push({ ...record, readiness });
368
+ }
369
+ catch {
370
+ // Readiness stays unknown for this record; the list is still shown.
371
+ records.push({ ...record });
372
+ }
373
+ }
374
+ return fetchJson(200, collectionPayload('records', { records, unreadable: listed.unreadable }));
375
+ }
376
+ };
377
+ const router = createFoundationHttpRouter({
378
+ resolveContext: (request) => principals.get(request) ?? null,
379
+ csrf: {
380
+ verify(request) {
381
+ const original = origins.get(request);
382
+ return original !== undefined && isTrustedLocalWrite(original, context.token);
383
+ }
384
+ },
385
+ bodyLimitBytes: LOCAL_WEB_FOUNDATION_BODY_LIMIT_BYTES,
386
+ routes: [
387
+ listRoute,
388
+ createObjectiveFoundationRoute({
389
+ objectives: {
390
+ createObjective: objectives.createObjective.bind(objectives),
391
+ readObjective: objectives.readObjective.bind(objectives),
392
+ updateObjective: objectives.updateObjective.bind(objectives),
393
+ checkObjectiveReadiness: objectives.checkObjectiveReadiness.bind(objectives),
394
+ list: (type, listContext) => store.list(type, listContext)
395
+ },
396
+ buildDefinition: createLocalObjectiveDefinitionBuilder(context.now),
397
+ listObjectiveConstraintRefs: (reference, listContext) => listObjectiveConstraintRefs(store, context.dataDir, reference, listContext),
398
+ bodyLimitBytes: LOCAL_WEB_FOUNDATION_BODY_LIMIT_BYTES
399
+ })
400
+ ]
401
+ });
402
+ return {
403
+ id: 'foundation-objectives',
404
+ uiFiles: ['objective-editor.js', 'objective-editor.css', 'objective-editor-http-port.js'],
405
+ async handle(request, response) {
406
+ const path = requestUrl(request).pathname;
407
+ if (path !== FOUNDATION_HTTP_PREFIX && !path.startsWith(`${FOUNDATION_HTTP_PREFIX}/`))
408
+ return false;
409
+ const graph = await readLocalGraphState(context.dataDir);
410
+ if (graph.status !== 'ready' || !graph.ownerId) {
411
+ writeGraphUnavailable(response, graph, context.dataDir);
412
+ return true;
413
+ }
414
+ const fetchRequest = await nodeRequestToFetch(request, { limitBytes: LOCAL_WEB_FOUNDATION_BODY_LIMIT_BYTES });
415
+ principals.set(fetchRequest, { principal: graph.ownerId });
416
+ origins.set(fetchRequest, request);
417
+ await writeFetchResponse(response, await router.handle(fetchRequest));
418
+ return true;
419
+ }
420
+ };
421
+ }
422
+ function worldModelError(error) {
423
+ if (error instanceof WorldModelStoreError) {
424
+ const status = error.code === 'authorization_denied' ? 403
425
+ : error.code === 'not_found' ? 404
426
+ : error.code === 'unsupported_graph' || error.code === 'approval_reference_unresolved' ? 503
427
+ : error.code === 'invalid_input' ? 400
428
+ : 500;
429
+ return new LocalWebHttpError(status, error.code, error.message);
430
+ }
431
+ if (error instanceof FoundationStoreError) {
432
+ const status = error.code === 'authorization_denied' || error.code === 'scope_violation' ? 403
433
+ : error.code === 'unsupported_graph' ? 503
434
+ : error.code === 'invalid_input' ? 400
435
+ : 500;
436
+ return new LocalWebHttpError(status, error.code, error.message);
437
+ }
438
+ if (error instanceof LocalWebHttpError)
439
+ return error;
440
+ return new LocalWebHttpError(500, 'internal_error', 'World model read failed');
441
+ }
442
+ async function listReadableSidecarRecords(input) {
443
+ try {
444
+ return { records: [...await input.listAll()], unreadable: NOTHING_UNREADABLE };
445
+ }
446
+ catch (error) {
447
+ if (!isWorldModelDenial(error))
448
+ throw error;
449
+ }
450
+ // The listing stopped at one unreadable record. Enumerate the stored ids
451
+ // and read each record through the store so its checks still apply.
452
+ const raw = await readPersonalOsSidecar(input.dataDir, WORLD_MODEL_EVIDENCE_SIDECAR);
453
+ const parsed = raw === undefined ? {} : JSON.parse(raw);
454
+ const stored = Array.isArray(parsed[input.key]) ? parsed[input.key] : [];
455
+ const records = [];
456
+ const codes = new Set();
457
+ let count = 0;
458
+ for (const item of stored) {
459
+ const id = item?.[input.idField];
460
+ if (typeof id !== 'string')
461
+ continue;
462
+ try {
463
+ const record = await input.readOne(id);
464
+ if (record)
465
+ records.push(record);
466
+ }
467
+ catch (error) {
468
+ if (!isWorldModelDenial(error))
469
+ throw error;
470
+ count += 1;
471
+ codes.add(error.code);
472
+ }
473
+ }
474
+ return { records, unreadable: { count, codes: [...codes] } };
475
+ }
476
+ /**
477
+ * Read-only World Model routes. Writes (observations, corrections, model
478
+ * adoption) are not on the Web in v1. Without an approval reader an approved
479
+ * adoption cannot be verified, so only the adoptions route reports
480
+ * `approval_reference_unresolved`; the other routes stay readable.
481
+ */
482
+ export function createWorldModelReadModule(context) {
483
+ const foundationStore = createFoundationRevisionStore({ dataDir: context.dataDir });
484
+ const worldModel = createWorldModelStore({ dataDir: context.dataDir, foundationStore });
485
+ const resources = new Set(['variables', 'models', 'observations', 'adoptions']);
486
+ async function read(resource, principal) {
487
+ switch (resource) {
488
+ case 'variables':
489
+ return collectionPayload('records', await listReadableFoundation(foundationStore, context.dataDir, 'variable', principal));
490
+ case 'models':
491
+ return collectionPayload('records', await listReadableFoundation(foundationStore, context.dataDir, 'model', principal));
492
+ case 'observations':
493
+ return collectionPayload('observations', await listReadableSidecarRecords({
494
+ listAll: () => worldModel.listObservations(undefined, principal),
495
+ dataDir: context.dataDir,
496
+ key: 'observations',
497
+ idField: 'id',
498
+ readOne: (id) => worldModel.readObservation(id, principal)
499
+ }));
500
+ default:
501
+ return collectionPayload('adoptions', await listReadableSidecarRecords({
502
+ listAll: () => worldModel.listModelAdoptions(principal),
503
+ dataDir: context.dataDir,
504
+ key: 'adoptions',
505
+ idField: 'adoptionId',
506
+ readOne: (id) => worldModel.readModelAdoption(id, principal)
507
+ }));
508
+ }
509
+ }
510
+ return {
511
+ id: 'world-model',
512
+ uiFiles: ['world-model-view.js', 'world-model-view.css'],
513
+ async handle(request, response) {
514
+ const path = requestUrl(request).pathname;
515
+ if (path !== LOCAL_WEB_WORLD_MODEL_PREFIX && !path.startsWith(`${LOCAL_WEB_WORLD_MODEL_PREFIX}/`))
516
+ return false;
517
+ const resource = path.slice(LOCAL_WEB_WORLD_MODEL_PREFIX.length + 1);
518
+ if (!resources.has(resource)) {
519
+ writeJson(response, 404, { error: { code: 'not_found', message: 'Not found' } });
520
+ return true;
521
+ }
522
+ if (request.method !== 'GET') {
523
+ writeJson(response, 405, { error: { code: 'method_not_allowed', message: 'The world model is read-only here' } });
524
+ return true;
525
+ }
526
+ const graph = await readLocalGraphState(context.dataDir);
527
+ if (graph.status !== 'ready' || !graph.ownerId) {
528
+ writeGraphUnavailable(response, graph, context.dataDir);
529
+ return true;
530
+ }
531
+ try {
532
+ writeJson(response, 200, await read(resource, { principal: graph.ownerId }));
533
+ }
534
+ catch (error) {
535
+ writeHttpError(response, worldModelError(error));
536
+ }
537
+ return true;
538
+ }
539
+ };
540
+ }
541
+ /**
542
+ * 「プロジェクトと関係者」 and 「情報と関係」: the local Graph read and correction
543
+ * routes under `/api/graph`. Graph v1 and a missing data set are answered by
544
+ * the handler itself (`migration_required` / `not_initialized`), never as zero
545
+ * items. The host has already required the launch token and the same origin
546
+ * for every write; the handler checks both again before it reads a body, so a
547
+ * correction is never saved through a path that skipped the host check.
548
+ */
549
+ export function createGraphWebModule(context) {
550
+ const handler = createGraphWebHttpHandler({
551
+ dataDir: context.dataDir,
552
+ basePath: LOCAL_WEB_GRAPH_PREFIX,
553
+ now: context.now,
554
+ assertWriteAllowed(request) {
555
+ if (isTrustedLocalWrite(request, context.token))
556
+ return;
557
+ throw rejectUntrustedWrite(request, context.token)
558
+ ?? new LocalWebHttpError(403, 'web_token_required', 'A valid launch token is required');
559
+ }
560
+ });
561
+ return {
562
+ id: 'graph',
563
+ uiFiles: [
564
+ 'graph-view-shared.js',
565
+ 'graph-view-shared.css',
566
+ 'graph-projects-view.js',
567
+ 'graph-projects-view.css',
568
+ 'graph-registry-view.js',
569
+ 'graph-registry-view.css'
570
+ ],
571
+ handle: handler
572
+ };
573
+ }
574
+ /** 今日 (value proofs), 目的と現状 (objectives + world model), プロジェクトと関係者 and 情報と関係 (Graph). */
575
+ export function defaultLocalWebModules(context) {
576
+ return [
577
+ createLocalStatusModule(context),
578
+ createValueProofModule(context),
579
+ createObjectiveFoundationModule(context),
580
+ createWorldModelReadModule(context),
581
+ createGraphWebModule(context)
582
+ ];
583
+ }
584
+ function escapeAttribute(value) {
585
+ return value.replace(/[&"<>]/gu, (character) => ({ '&': '&amp;', '"': '&quot;', '<': '&lt;', '>': '&gt;' }[character] ?? character));
586
+ }
587
+ function shellHtml(token, stylesheets) {
588
+ return `<!doctype html>
589
+ <html lang="ja">
590
+ <head>
591
+ <meta charset="utf-8">
592
+ <meta name="viewport" content="width=device-width, initial-scale=1">
593
+ <meta name="brainbase-web-token" content="${escapeAttribute(token)}">
594
+ <title>Brainbase</title>
595
+ ${stylesheets.map((file) => `<link rel="stylesheet" href="/ui/${escapeAttribute(file)}">`).join('\n')}
596
+ </head>
597
+ <body>
598
+ <div id="brainbase-local-web"></div>
599
+ <script type="module" src="/app.js"></script>
600
+ </body>
601
+ </html>
602
+ `;
603
+ }
604
+ const BOOTSTRAP_JS = `import { createLocalWebShell } from '/ui/local-web-shell.js';
605
+ const meta = (name) => document.querySelector(\`meta[name="\${name}"]\`)?.getAttribute('content') ?? '';
606
+ const screenFromHash = () => decodeURIComponent(location.hash.replace(/^#/, '')) || undefined;
607
+ const shell = createLocalWebShell({
608
+ root: document.getElementById('brainbase-local-web'),
609
+ token: meta('brainbase-web-token'),
610
+ initialScreen: screenFromHash()
611
+ });
612
+ addEventListener('hashchange', () => shell.show(screenFromHash()));
613
+ `;
614
+ /** Local single-owner host. The caller owns `listen()` and must bind to a loopback address. */
615
+ export function createLocalWebHost(options = {}) {
616
+ const token = options.token ?? randomBytes(24).toString('base64url');
617
+ if (token.length < 16)
618
+ throw new TypeError('token must be at least 16 characters');
619
+ const dataDir = resolveDataDir(options.dataDir);
620
+ const journalRoot = options.journalRoot ?? defaultJudgmentJournalRoot(dataDir);
621
+ const uiDir = options.uiDir ?? fileURLToPath(new URL('../ui/', import.meta.url));
622
+ const context = { dataDir, journalRoot, token, now: options.now ?? (() => new Date()) };
623
+ const modules = defaultLocalWebModules(context);
624
+ const uiFiles = new Map();
625
+ for (const file of [...BASE_UI_FILES, ...modules.flatMap((module) => module.uiFiles)]) {
626
+ const extension = file.slice(file.lastIndexOf('.'));
627
+ const type = CONTENT_TYPES[extension];
628
+ if (!type || file.includes('/'))
629
+ throw new TypeError(`Unsupported UI file ${file}`);
630
+ uiFiles.set(`/ui/${file}`, type);
631
+ }
632
+ const stylesheets = [...uiFiles.keys()].filter((path) => path.endsWith('.css')).map((path) => path.slice('/ui/'.length));
633
+ async function handle(request, response) {
634
+ if (!isLoopbackHost(request)) {
635
+ writeJson(response, 403, { error: { code: 'host_rejected', message: 'Host must be a loopback address for this server' } });
636
+ return;
637
+ }
638
+ if (request.method !== 'GET') {
639
+ const rejected = rejectUntrustedWrite(request, token);
640
+ if (rejected) {
641
+ writeHttpError(response, rejected);
642
+ return;
643
+ }
644
+ }
645
+ for (const module of modules) {
646
+ if (await module.handle(request, response))
647
+ return;
648
+ }
649
+ const path = requestUrl(request).pathname;
650
+ if (request.method !== 'GET') {
651
+ writeJson(response, 405, { error: { code: 'method_not_allowed', message: 'Use GET' } });
652
+ return;
653
+ }
654
+ response.setHeader('Cache-Control', 'no-store');
655
+ response.setHeader('X-Content-Type-Options', 'nosniff');
656
+ if (path === '/') {
657
+ response.setHeader('Content-Type', 'text/html; charset=utf-8');
658
+ response.setHeader('Content-Security-Policy', LOCAL_WEB_CONTENT_SECURITY_POLICY);
659
+ response.setHeader('Referrer-Policy', 'no-referrer');
660
+ response.end(shellHtml(token, stylesheets));
661
+ return;
662
+ }
663
+ if (path === '/app.js') {
664
+ response.setHeader('Content-Type', 'text/javascript; charset=utf-8');
665
+ response.end(BOOTSTRAP_JS);
666
+ return;
667
+ }
668
+ const type = uiFiles.get(path);
669
+ if (!type) {
670
+ writeJson(response, 404, { error: { code: 'not_found', message: 'Not found' } });
671
+ return;
672
+ }
673
+ response.setHeader('Content-Type', type);
674
+ response.end(await readFile(join(uiDir, path.slice('/ui/'.length))));
675
+ }
676
+ const server = createServer((request, response) => {
677
+ void handle(request, response).catch((error) => {
678
+ if (error instanceof LocalWebHttpError)
679
+ writeHttpError(response, error);
680
+ else
681
+ writeJson(response, 500, { error: { code: 'internal_error', message: 'Unexpected error' } });
682
+ });
683
+ });
684
+ return { server, token, dataDir, journalRoot };
685
+ }
686
+ function errorMessage(error) {
687
+ return error instanceof Error ? error.message : String(error);
688
+ }