@unson/brainbase-mcp 0.15.1 → 0.15.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,6 +9,8 @@ export interface CanonicalTaskContext extends JsonRecord {
9
9
  auditPrincipal?: CanonicalTaskPrincipal | null;
10
10
  auditAuthSource?: string | null;
11
11
  idempotencyKey?: string | null;
12
+ /** Opaque host-policy scope, not authority or task input. */
13
+ storageScope?: string;
12
14
  }
13
15
  export interface CanonicalTaskRecord extends JsonRecord {
14
16
  id: string;
@@ -102,13 +104,13 @@ export interface CanonicalTaskAuditEntry extends JsonRecord {
102
104
  source_refs: unknown[];
103
105
  }
104
106
  export interface CanonicalTaskRepository {
105
- list(filters: CanonicalTaskListFilters): Promise<CanonicalTaskPage>;
106
- search(filters: CanonicalTaskSearchFilters): Promise<CanonicalTaskPage>;
107
- get(taskId: string): Promise<CanonicalTaskRecord | null>;
108
- findByIdempotencyKey(key: string): Promise<CanonicalTaskRecord | null>;
109
- create(input: JsonRecord): Promise<CanonicalTaskRecord>;
110
- update(taskId: string, patch: JsonRecord): Promise<CanonicalTaskRecord>;
111
- delete(taskId: string, expectedVersion: number): Promise<void>;
107
+ list(filters: CanonicalTaskListFilters, storageScope?: string): Promise<CanonicalTaskPage>;
108
+ search(filters: CanonicalTaskSearchFilters, storageScope?: string): Promise<CanonicalTaskPage>;
109
+ get(taskId: string, storageScope?: string): Promise<CanonicalTaskRecord | null>;
110
+ findByIdempotencyKey(key: string, storageScope?: string): Promise<CanonicalTaskRecord | null>;
111
+ create(input: JsonRecord, storageScope?: string): Promise<CanonicalTaskRecord>;
112
+ update(taskId: string, patch: JsonRecord, storageScope?: string): Promise<CanonicalTaskRecord>;
113
+ delete(taskId: string, expectedVersion: number, storageScope?: string): Promise<void>;
112
114
  }
113
115
  export interface CanonicalTaskAuditRepository {
114
116
  upsertAuditLog(entry: CanonicalTaskAuditEntry): Promise<void>;
@@ -159,6 +161,7 @@ export interface CanonicalTaskPreparedDeleteRequest<T> {
159
161
  versionFingerprint: string;
160
162
  versionClaimKey: string;
161
163
  principalNamespace: string;
164
+ storageScope?: string;
162
165
  prepare: () => Promise<{
163
166
  result: T;
164
167
  task?: CanonicalTaskRecord | null;
@@ -237,6 +240,7 @@ export declare class CanonicalTaskService {
237
240
  private normalizeUpdateInput;
238
241
  private normalizeListFilters;
239
242
  private normalizeSearchFilters;
243
+ private scopeArgs;
240
244
  private normalizeContext;
241
245
  private authorize;
242
246
  private scopeFilters;
@@ -147,8 +147,12 @@ function validHttpUrl(value) {
147
147
  return null;
148
148
  }
149
149
  }
150
- function operationKey(principal, clientKey, prefix) {
151
- return `${prefix}${principalNamespace(principal)}:${clientKey}`;
150
+ function scopedNamespace(context) {
151
+ return context.storageScope === undefined ? principalNamespace(context.principal)
152
+ : `v2.${Buffer.from(JSON.stringify({ actor: context.principal, storageScope: context.storageScope })).toString('base64url')}`;
153
+ }
154
+ function operationKey(context, clientKey, prefix) {
155
+ return `${prefix}${scopedNamespace(context)}:${clientKey}`;
152
156
  }
153
157
  function extractPersistedFingerprint(task) {
154
158
  const value = task.payload_fingerprint ?? task._payload_fingerprint;
@@ -224,7 +228,7 @@ export class CanonicalTaskService {
224
228
  const normalizedFilters = this.normalizeListFilters(filters);
225
229
  await this.authorize('list', normalizedContext, undefined, normalizedFilters);
226
230
  const scoped = await this.scopeFilters('list', normalizedContext, normalizedFilters);
227
- const page = await this.read(() => this.repository.list(scoped));
231
+ const page = await this.read(() => this.repository.list(scoped, ...this.scopeArgs(normalizedContext)));
228
232
  return {
229
233
  ...normalizePage(page, (task) => this.normalizeResponse(task)),
230
234
  as_of: this.clock().toISOString(),
@@ -238,7 +242,7 @@ export class CanonicalTaskService {
238
242
  if (typeof this.repository.search !== 'function') {
239
243
  throw new CanonicalTaskError('task_search_unavailable', 'Canonical Task search is unavailable', 503);
240
244
  }
241
- const page = await this.read(() => this.repository.search(scoped));
245
+ const page = await this.read(() => this.repository.search(scoped, ...this.scopeArgs(normalizedContext)));
242
246
  const normalizedPage = normalizePage(page, (task) => this.normalizeResponse(task));
243
247
  return {
244
248
  ...normalizedPage,
@@ -250,7 +254,7 @@ export class CanonicalTaskService {
250
254
  async getTask(taskId, context) {
251
255
  const normalizedId = normalizeString(taskId, 'task_id', { required: true, max: 200 });
252
256
  const normalizedContext = await this.normalizeContext(context);
253
- const task = await this.read(() => this.repository.get(normalizedId));
257
+ const task = await this.read(() => this.repository.get(normalizedId, ...this.scopeArgs(normalizedContext)));
254
258
  if (!task)
255
259
  throw new CanonicalTaskError('task_not_found', 'Task was not found', 404, { task_id: normalizedId });
256
260
  await this.authorize('read', normalizedContext, task);
@@ -261,10 +265,10 @@ export class CanonicalTaskService {
261
265
  const clientKey = this.requireIdempotencyKey(normalizedContext);
262
266
  const payload = this.normalizeCreateInput(input);
263
267
  await this.authorize('create', normalizedContext, undefined, payload);
264
- const operation = operationKey(normalizedContext.principal, clientKey, 'api:');
268
+ const operation = operationKey(normalizedContext, clientKey, 'api:');
265
269
  const inputFingerprint = fingerprint(payload);
266
270
  const recover = async () => {
267
- const existing = await this.read(() => this.repository.findByIdempotencyKey(operation));
271
+ const existing = await this.read(() => this.repository.findByIdempotencyKey(operation, ...this.scopeArgs(normalizedContext)));
268
272
  if (!existing)
269
273
  return { recovered: false };
270
274
  const existingFingerprint = extractPersistedFingerprint(existing);
@@ -288,7 +292,7 @@ export class CanonicalTaskService {
288
292
  version: 1,
289
293
  idempotency_key: operation,
290
294
  payload_fingerprint: inputFingerprint,
291
- }));
295
+ }, ...this.scopeArgs(normalizedContext)));
292
296
  return this.normalizeResponse(created);
293
297
  };
294
298
  const result = await this.executeOperation({
@@ -299,6 +303,8 @@ export class CanonicalTaskService {
299
303
  recover,
300
304
  run,
301
305
  });
306
+ if (normalizedContext.storageScope !== undefined)
307
+ await this.authorize('read', normalizedContext, result);
302
308
  const normalized = this.normalizeResponse(result);
303
309
  // Audit each invocation, including a replay/recovery. The deterministic
304
310
  // entry id lets an upsert sink collapse retries without losing evidence
@@ -367,9 +373,11 @@ export class CanonicalTaskService {
367
373
  // operation may legitimately recover after the delete removed the row.
368
374
  // The fallback path still performs the ordinary read and authorization
369
375
  // checks before invoking the local operation runner.
376
+ if (normalizedContext.storageScope !== undefined)
377
+ await this.authorize('delete', normalizedContext);
370
378
  let task = null;
371
379
  if (!this.operationRepository?.executePreparedDelete) {
372
- task = await this.read(() => this.repository.get(normalizedId));
380
+ task = await this.read(() => this.repository.get(normalizedId, ...this.scopeArgs(normalizedContext)));
373
381
  if (!task)
374
382
  throw new CanonicalTaskError('task_not_found', 'Task was not found', 404, { task_id: normalizedId });
375
383
  await this.authorize('delete', normalizedContext, task);
@@ -378,16 +386,19 @@ export class CanonicalTaskService {
378
386
  // The client key identifies the delete command. The task/version claim is
379
387
  // carried separately so an operation coordinator can atomically claim the
380
388
  // exact row version without allowing another actor to replay the command.
381
- const operation = operationKey(normalizedContext.principal, clientKey, 'delete:');
382
- const versionClaimKey = `task-version:${normalizedId}:${version}`;
389
+ const operation = operationKey(normalizedContext, clientKey, 'delete:');
390
+ const scopePrefix = normalizedContext.storageScope === undefined ? '' : `${Buffer.from(normalizedContext.storageScope).toString('base64url')}:`;
391
+ const versionClaimKey = `task-version:${scopePrefix}${normalizedId}:${version}`;
383
392
  const versionFingerprint = fingerprint({
384
393
  kind: 'delete',
394
+ ...(normalizedContext.storageScope === undefined ? {} : { storageScope: normalizedContext.storageScope }),
385
395
  taskId: normalizedId,
386
396
  expectedVersion: version,
387
397
  });
388
- const actorNamespace = principalNamespace(normalizedContext.principal);
398
+ const actorNamespace = scopedNamespace(normalizedContext);
389
399
  const operationFingerprint = fingerprint({
390
400
  kind: 'delete',
401
+ ...(normalizedContext.storageScope === undefined ? {} : { storageScope: normalizedContext.storageScope }),
391
402
  taskId: normalizedId,
392
403
  expectedVersion: version,
393
404
  principalNamespace: actorNamespace,
@@ -398,7 +409,7 @@ export class CanonicalTaskService {
398
409
  version: version + 1,
399
410
  };
400
411
  const prepare = async () => {
401
- const current = await this.read(() => this.repository.get(normalizedId));
412
+ const current = await this.read(() => this.repository.get(normalizedId, ...this.scopeArgs(normalizedContext)));
402
413
  if (!current)
403
414
  throw new CanonicalTaskError('task_not_found', 'Task was not found', 404, { task_id: normalizedId });
404
415
  await this.authorize('delete', normalizedContext, current);
@@ -408,23 +419,25 @@ export class CanonicalTaskService {
408
419
  result: {
409
420
  ...result,
410
421
  _audit_source_refs: Array.isArray(current.source_refs) ? current.source_refs : [],
422
+ ...(normalizedContext.storageScope === undefined ? {} : { _storage_scope: normalizedContext.storageScope }),
411
423
  },
412
424
  task: current,
413
425
  authorizationSnapshot: {
414
426
  task_id: normalizedId,
415
427
  task_version: current.version,
428
+ ...(normalizedContext.storageScope === undefined ? {} : { storage_scope: normalizedContext.storageScope }),
416
429
  actor: normalizedContext.principal,
417
430
  auth_source: normalizedContext.authSource ?? null,
418
431
  },
419
432
  };
420
433
  };
421
- const findTask = async () => this.read(() => this.repository.get(normalizedId)).then((current) => {
434
+ const findTask = async () => this.read(() => this.repository.get(normalizedId, ...this.scopeArgs(normalizedContext))).then((current) => {
422
435
  if (current)
423
436
  task = current;
424
437
  return current;
425
438
  });
426
439
  const removeTask = async (current) => {
427
- const taskToRemove = current ?? await this.read(() => this.repository.get(normalizedId));
440
+ const taskToRemove = current ?? await this.read(() => this.repository.get(normalizedId, ...this.scopeArgs(normalizedContext)));
428
441
  if (!taskToRemove)
429
442
  throw new CanonicalTaskError('task_not_found', 'Task was not found', 404, { task_id: normalizedId });
430
443
  task = taskToRemove;
@@ -433,7 +446,7 @@ export class CanonicalTaskService {
433
446
  // actor cannot delete a task using a previously prepared authorization.
434
447
  await this.authorize('delete', normalizedContext, taskToRemove);
435
448
  this.assertVersion(taskToRemove, version);
436
- await this.read(() => this.repository.delete(normalizedId, version));
449
+ await this.read(() => this.repository.delete(normalizedId, version, ...this.scopeArgs(normalizedContext)));
437
450
  };
438
451
  let deleted;
439
452
  if (this.operationRepository?.executePreparedDelete) {
@@ -444,6 +457,7 @@ export class CanonicalTaskService {
444
457
  versionFingerprint,
445
458
  versionClaimKey,
446
459
  principalNamespace: actorNamespace,
460
+ ...(normalizedContext.storageScope === undefined ? {} : { storageScope: normalizedContext.storageScope }),
447
461
  prepare,
448
462
  findTask,
449
463
  removeTask,
@@ -463,6 +477,9 @@ export class CanonicalTaskService {
463
477
  },
464
478
  });
465
479
  }
480
+ if (normalizedContext.storageScope !== undefined && deleted._storage_scope !== normalizedContext.storageScope) {
481
+ throw new CanonicalTaskError('task_not_found', 'Task was not found', 404);
482
+ }
466
483
  // A completed replay may return without prepare/findTask, because the
467
484
  // durable operation result is sufficient. Keep the audit target stable in
468
485
  // that case while avoiding a second task-store read that would turn a
@@ -494,11 +511,11 @@ export class CanonicalTaskService {
494
511
  async versionedMutation(input) {
495
512
  const taskId = normalizeString(input.taskId, 'task_id', { required: true, max: 200 });
496
513
  const expectedVersion = this.requireExpectedVersion(input.expectedVersion);
497
- const current = await this.read(() => this.repository.get(taskId));
514
+ const current = await this.read(() => this.repository.get(taskId, ...this.scopeArgs(input.context)));
498
515
  if (!current)
499
516
  throw new CanonicalTaskError('task_not_found', 'Task was not found', 404, { task_id: taskId });
500
517
  await this.authorize(input.action, input.context, current, input.patch);
501
- const operation = operationKey(input.context.principal, `${taskId}:${expectedVersion}`, `${input.action}:`);
518
+ const operation = operationKey(input.context, `${taskId}:${expectedVersion}`, `${input.action}:`);
502
519
  const mutationTimestamp = this.clock().toISOString();
503
520
  const patch = {
504
521
  ...input.patch,
@@ -518,6 +535,7 @@ export class CanonicalTaskService {
518
535
  patch: input.patch,
519
536
  transitionStatus: input.transitionStatus ?? null,
520
537
  principal: input.context.principal,
538
+ ...(input.context.storageScope === undefined ? {} : { storageScope: input.context.storageScope }),
521
539
  });
522
540
  const wasApplied = (candidate) => candidate.version === expectedVersion + 1
523
541
  && candidate._last_operation_key === operation
@@ -532,7 +550,7 @@ export class CanonicalTaskService {
532
550
  }
533
551
  }
534
552
  const recover = async () => {
535
- const latest = await this.read(() => this.repository.get(taskId));
553
+ const latest = await this.read(() => this.repository.get(taskId, ...this.scopeArgs(input.context)));
536
554
  if (!latest)
537
555
  throw new CanonicalTaskError('task_not_found', 'Task was not found', 404, { task_id: taskId });
538
556
  await this.authorize(input.action, input.context, latest, input.patch);
@@ -548,7 +566,7 @@ export class CanonicalTaskService {
548
566
  return { recovered: false };
549
567
  };
550
568
  const run = async () => {
551
- const latest = await this.read(() => this.repository.get(taskId));
569
+ const latest = await this.read(() => this.repository.get(taskId, ...this.scopeArgs(input.context)));
552
570
  if (!latest)
553
571
  throw new CanonicalTaskError('task_not_found', 'Task was not found', 404, { task_id: taskId });
554
572
  if (wasApplied(latest))
@@ -564,7 +582,7 @@ export class CanonicalTaskService {
564
582
  ...patch,
565
583
  _last_operation_key: operation,
566
584
  _last_operation_fingerprint: operationFingerprint,
567
- }));
585
+ }, ...this.scopeArgs(input.context)));
568
586
  return this.normalizeResponse(updated);
569
587
  };
570
588
  const result = await this.executeOperation({
@@ -575,6 +593,8 @@ export class CanonicalTaskService {
575
593
  recover,
576
594
  run,
577
595
  });
596
+ if (input.context.storageScope !== undefined)
597
+ await this.authorize(input.action, input.context, result, input.patch);
578
598
  const normalized = this.normalizeResponse(result);
579
599
  const auditAction = input.action === 'update'
580
600
  ? 'canonical_task.updated'
@@ -684,6 +704,9 @@ export class CanonicalTaskService {
684
704
  const limit = normalizeLimit(filters.limit, 20);
685
705
  return { ...normalized, q, query: q, tokens, limit };
686
706
  }
707
+ scopeArgs(context) {
708
+ return context.storageScope === undefined ? [] : [context.storageScope];
709
+ }
687
710
  async normalizeContext(context) {
688
711
  if (!context || typeof context !== 'object') {
689
712
  throw new CanonicalTaskError('unauthenticated_context', 'A task context is required', 401);
@@ -702,6 +725,10 @@ export class CanonicalTaskService {
702
725
  ? await this.policy.normalizeContext(normalized)
703
726
  : normalized;
704
727
  try {
728
+ if (fromPolicy.storageScope !== undefined && (typeof fromPolicy.storageScope !== 'string'
729
+ || !fromPolicy.storageScope.trim() || /\p{Cc}/u.test(fromPolicy.storageScope))) {
730
+ throw new Error('Storage scope is invalid');
731
+ }
705
732
  return { ...fromPolicy, principal: normalizeCanonicalTaskPrincipal(fromPolicy.principal) };
706
733
  }
707
734
  catch (error) {
@@ -759,6 +786,7 @@ export class CanonicalTaskService {
759
786
  actor_type: actor.type,
760
787
  actor_principal: actor,
761
788
  actor_namespace: principalNamespace(actor),
789
+ ...(context.storageScope === undefined ? {} : { storage_scope: context.storageScope }),
762
790
  auth_source: context.auditAuthSource ?? context.authSource ?? null,
763
791
  action,
764
792
  target_type: 'canonical_task',
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import { createOrganizationGraphConfig, createOrganizationGraphClient } from './
4
4
  import { listLocalMemories, memoryListJson, registerSelectedMemories, registrationPreviewJson, renderMemoryList, renderRegistrationPreview, renderRegistrationResult, selectLocalMemories } from './personal-memory-handover.js';
5
5
  import { constants, realpathSync } from 'node:fs';
6
6
  import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
7
- import { delimiter, dirname, isAbsolute, join } from 'node:path';
7
+ import { delimiter, dirname, isAbsolute, join, resolve } from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import { initializePersonalOs, loadPersonalOs, migrateCanonicalGraph, mutatePersonalOs } from './ssot.js';
10
10
  import { diagnoseGraph } from './graph-diagnosis.js';
@@ -21,6 +21,8 @@ import { renderGuidedFirstRun } from './guided-onboarding.js';
21
21
  import { blockedJudgmentOutput, processJudgmentHook } from './judgment-host.js';
22
22
  import { applyCanonicalWrites, buildCanonicalEdge } from './canonical-edge-builder.js';
23
23
  import { defaultJudgmentJournalRoot } from './judgment-value-proof-review.js';
24
+ import { createWorldExtension, readWorldVocabulary } from './world-extension.js';
25
+ import { createOrganizationGraphSource } from './organization-graph-web.js';
24
26
  import { createLocalWebHost, LOCAL_WEB_DEFAULT_PORT } from './local-web-host.js';
25
27
  export async function runCli(argv = process.argv.slice(2), io = process) {
26
28
  const parsed = parseArgs(argv);
@@ -1043,7 +1045,38 @@ async function webServe(parsed, io, command) {
1043
1045
  if (!Number.isInteger(port) || port < 0 || port > 65535) {
1044
1046
  throw new Error(`${command} requires --port to be an integer between 0 and 65535`);
1045
1047
  }
1046
- const { server } = createLocalWebHost({ dataDir, journalRoot });
1048
+ // World view (ledger P17): the businesses as a city above the home. Off unless the flag is given.
1049
+ const world = parsed.flags.has('world');
1050
+ // The owner's words for project kinds and statuses (labels, buildings, lifecycle phases).
1051
+ const worldVocabularyPath = first(parsed, 'world-vocabulary');
1052
+ if (worldVocabularyPath !== undefined && !world)
1053
+ throw new Error(`${command} --world-vocabulary requires --world`);
1054
+ const worldVocabulary = worldVocabularyPath === undefined ? undefined : await readWorldVocabulary(resolve(worldVocabularyPath));
1055
+ // Ledger C1: the Graph screens read the owner's organization Graph read only.
1056
+ let organizationGraph = null;
1057
+ if (parsed.flags.has('organization-graph')) {
1058
+ const source = await createOrganizationGraphSource();
1059
+ if ('reason' in source) {
1060
+ throw new Error(`${command} --organization-graph cannot read the organization Graph (${source.reason}); sign in with brainbase auth first`);
1061
+ }
1062
+ organizationGraph = source;
1063
+ }
1064
+ // Where the organization's records are corrected; the read-only screens link to it.
1065
+ const organizationWeb = first(parsed, 'organization-web');
1066
+ if (organizationWeb !== undefined) {
1067
+ if (!organizationGraph)
1068
+ throw new Error(`${command} --organization-web requires --organization-graph`);
1069
+ if (!/^https:\/\/[^/?#@\s]+$/u.test(organizationWeb)) {
1070
+ throw new Error(`${command} --organization-web must be an https origin such as https://org.example.com`);
1071
+ }
1072
+ organizationGraph = { ...organizationGraph, webUrl: organizationWeb };
1073
+ }
1074
+ const { server } = createLocalWebHost({
1075
+ dataDir,
1076
+ journalRoot,
1077
+ extensions: world ? [createWorldExtension(worldVocabulary ? { vocabulary: worldVocabulary } : {})] : [],
1078
+ ...(organizationGraph ? { organizationGraph } : {})
1079
+ });
1047
1080
  await new Promise((resolveListen, rejectListen) => {
1048
1081
  server.once('error', rejectListen);
1049
1082
  server.listen(port, '127.0.0.1', () => resolveListen());
@@ -1057,6 +1090,9 @@ async function webServe(parsed, io, command) {
1057
1090
  `- 目的と現状: ${origin}/#objectives`,
1058
1091
  `- プロジェクトと関係者: ${origin}/#projects`,
1059
1092
  `- 情報と関係: ${origin}/#graph`,
1093
+ ...(world ? [`- 世界: ${origin}/#world`] : []),
1094
+ ...(organizationGraph ? [`組織のGraph(読み取りのみ): ${organizationGraph.server}`] : []),
1095
+ ...(organizationGraph?.webUrl ? [`組織版(訂正の入口): ${organizationGraph.webUrl}`] : []),
1060
1096
  `データ: ${dataDir}`,
1061
1097
  `判断journal: ${journalRoot}`,
1062
1098
  '終了: Ctrl+C',
@@ -1213,7 +1249,7 @@ function usage() {
1213
1249
  brainbase judgment:install --target codex [--autonomy-mode off|canary|on] [--autonomy-project code] [--dry-run] [--output path]
1214
1250
  brainbase judgment:hook [--autonomy-mode off|canary|on] [--autonomy-project code]
1215
1251
  brainbase doctor [--dir path] [--judgment-hooks path]
1216
- brainbase web:serve [--dir path] [--journal path] [--port n]
1252
+ brainbase web:serve [--dir path] [--journal path] [--port n] [--world [--world-vocabulary file]] [--organization-graph [--organization-web https-origin]]
1217
1253
  brainbase review:serve [--dir path] [--journal path] [--port n] (web:serveの別名)
1218
1254
  `;
1219
1255
  }
@@ -1,4 +1,5 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { type GraphWebAnySource, type InMemoryGraphReader } from './graph-web.js';
2
3
  export declare const GRAPH_WEB_HTTP_VERSION: "graph-web-http.v1";
3
4
  export interface GraphWebHttpOptions {
4
5
  /** Personal OS data directory chosen by the host. Never taken from the request. */
@@ -11,6 +12,12 @@ export interface GraphWebHttpOptions {
11
12
  */
12
13
  readonly assertWriteAllowed?: (request: IncomingMessage) => void | Promise<void>;
13
14
  readonly now?: () => Date;
15
+ /**
16
+ * Ledger C1: read every route from a Graph the host holds in memory (the
17
+ * owner's organization Graph) instead of the data directory. Corrections
18
+ * are refused as read only.
19
+ */
20
+ readonly readGraph?: () => Promise<InMemoryGraphReader<GraphWebAnySource>>;
14
21
  }
15
22
  export type GraphWebHttpHandler = (request: IncomingMessage, response: ServerResponse) => Promise<boolean>;
16
23
  /**
@@ -52,6 +52,9 @@ export function createGraphWebHttpHandler(options) {
52
52
  if (route.name === 'corrections') {
53
53
  if (request.method !== 'POST')
54
54
  throw methodNotAllowed('POST');
55
+ if (options.readGraph) {
56
+ throw new HttpError(403, 'organization_graph_read_only', 'This host reads the organization Graph read only; correct it in the organization edition');
57
+ }
55
58
  await assertWriteAllowed(options.assertWriteAllowed, request);
56
59
  const body = await readJsonBody(request);
57
60
  const result = await applyGraphCorrection(dataDir, body, { now: now() });
@@ -62,6 +65,17 @@ export function createGraphWebHttpHandler(options) {
62
65
  throw methodNotAllowed('GET');
63
66
  const asOf = queryValue(url, 'as_of');
64
67
  const readOptions = { ...(asOf !== undefined ? { asOf } : {}), now: now() };
68
+ if (options.readGraph) {
69
+ const reader = await options.readGraph();
70
+ const body = route.name === 'status' ? reader.status(readOptions)
71
+ : route.name === 'projects' ? reader.listProjects(readOptions)
72
+ : route.name === 'project' ? reader.readProject(route.id, readOptions)
73
+ : route.name === 'search' ? reader.search({ q: queryValue(url, 'q'), type: queryValue(url, 'type'), asOf, limit: parseLimit(queryValue(url, 'limit')), now: readOptions.now })
74
+ : route.name === 'entity' ? reader.readEntity(route.id, readOptions)
75
+ : reader.ontology(readOptions);
76
+ writeJson(response, 200, body);
77
+ return true;
78
+ }
65
79
  switch (route.name) {
66
80
  case 'status':
67
81
  writeJson(response, 200, await readGraphWebStatus(dataDir, readOptions));
@@ -1,7 +1,7 @@
1
1
  import { GRAPH_CORRECTIONS_FILE, type GraphCorrectionRecord } from './graph-corrections.js';
2
2
  import { type PortableGraphOwner } from './portable-graph.js';
3
3
  import type { FoundationAdoptionState, FoundationOperator } from './ontology-foundation.js';
4
- import type { CanonicalEdge, CanonicalEntityKind, CoreRelation } from './types.js';
4
+ import type { CanonicalEdge, CanonicalEntityKind, CoreRelation, GraphFileV2 } from './types.js';
5
5
  /**
6
6
  * Read views of the canonical local Graph for the local Web screens
7
7
  * "プロジェクトと関係者" and "情報と関係".
@@ -34,7 +34,20 @@ export interface GraphWebOwnerPrivateSource {
34
34
  graphFormat: 2;
35
35
  authority: 'owner_private';
36
36
  }
37
- export type GraphWebAnySource = GraphWebSource | GraphWebOwnerPrivateSource;
37
+ /**
38
+ * The owner's organization Graph read through the host (ledger C1): read only,
39
+ * held in memory, never written to the data directory.
40
+ */
41
+ export interface GraphWebOrganizationSource {
42
+ dataDir: null;
43
+ graphFormat: 2;
44
+ authority: 'organization_graph';
45
+ /** Organization server origin, without credentials. */
46
+ server: string;
47
+ /** When the host last read the organization Graph. */
48
+ readAt: string;
49
+ }
50
+ export type GraphWebAnySource = GraphWebSource | GraphWebOwnerPrivateSource | GraphWebOrganizationSource;
38
51
  export interface GraphMigrationRequired {
39
52
  status: 'migration_required';
40
53
  source: GraphWebSource;
@@ -132,10 +145,10 @@ export interface GraphEdgeView {
132
145
  };
133
146
  digest: string;
134
147
  }
135
- export interface GraphWebStatus {
148
+ export interface GraphWebStatus<S extends GraphWebAnySource = GraphWebSource> {
136
149
  status: 'ok';
137
150
  version: typeof GRAPH_WEB_VERSION;
138
- source: GraphWebSource;
151
+ source: S;
139
152
  asOf: string;
140
153
  ontology: {
141
154
  id: string;
@@ -289,9 +302,9 @@ export interface GraphObjectiveList<S extends GraphWebAnySource = GraphWebSource
289
302
  /** True when the foundation was read and holds no objective. */
290
303
  absenceConfirmed: boolean;
291
304
  }
292
- export interface GraphOntologySummary {
305
+ export interface GraphOntologySummary<S extends GraphWebAnySource = GraphWebSource> {
293
306
  status: 'ok';
294
- source: GraphWebSource;
307
+ source: S;
295
308
  asOf: string;
296
309
  ontology: {
297
310
  id: string;
@@ -361,4 +374,21 @@ export interface OwnerPrivateGraph {
361
374
  * machine, so a relation's source ID is shown unresolved.
362
375
  */
363
376
  export declare function openOwnerPrivateGraph(bundle: unknown, owner: PortableGraphOwner): OwnerPrivateGraph;
377
+ /** Read views of one in-memory Graph v2 held by the host (no data directory, no corrections). */
378
+ export interface InMemoryGraphReader<S extends GraphWebAnySource> {
379
+ readonly source: S;
380
+ status(options?: GraphWebReadOptions): GraphWebStatus<S>;
381
+ listProjects(options?: GraphWebReadOptions): GraphProjectList<S>;
382
+ readProject(projectId: string, options?: GraphWebReadOptions): GraphProjectDetail<S>;
383
+ search(input?: GraphEntitySearchInput): GraphEntitySearchResult<S>;
384
+ readEntity(entityId: string, options?: GraphWebReadOptions): GraphEntityDetail<S>;
385
+ ontology(options?: GraphWebReadOptions): GraphOntologySummary<S>;
386
+ }
387
+ /**
388
+ * The same read views as the local Graph routes over a Graph v2 the host
389
+ * holds in memory (the organization Graph read under C1). The Graph is
390
+ * checked with the canonical validator first; nothing is written, and there
391
+ * are no correction records or relation sources to resolve.
392
+ */
393
+ export declare function openInMemoryGraph<S extends GraphWebAnySource>(graph: GraphFileV2, source: S): InMemoryGraphReader<S>;
364
394
  export {};
package/dist/graph-web.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { access, readdir, readFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
- import { isActiveAt } from './canonical-graph.js';
3
+ import { isActiveAt, validateCanonicalGraph } from './canonical-graph.js';
4
4
  import { assertFoundationCatalog, findFoundationRecord } from './foundation-catalog.js';
5
5
  import { GRAPH_CORRECTION_EDGE_FIELDS, GRAPH_CORRECTION_ENTITY_FIELDS, GRAPH_CORRECTION_NEW_EDGE_RELATIONS, GRAPH_CORRECTION_PROJECT_FIELDS, GRAPH_CORRECTIONS_FILE, graphRecordDigest, readGraphCorrectionHistory } from './graph-corrections.js';
6
6
  import { ONTOLOGY_VERSION, portableOntology } from './ontology.js';
@@ -207,12 +207,13 @@ export async function readGraphOntology(dataDir, options = {}) {
207
207
  const loaded = await loadForRead(dataDir);
208
208
  if ('status' in loaded)
209
209
  return loaded;
210
- const asOf = resolveAsOf(options);
211
- const { graph } = loaded;
210
+ return ontologyOf(loaded.graph, loaded.source, resolveAsOf(options));
211
+ }
212
+ function ontologyOf(graph, source, asOf) {
212
213
  const concepts = new Map(portableOntology.domains.types.concepts.map((concept) => [concept.id, concept.meaning]));
213
214
  return {
214
215
  status: 'ok',
215
- source: loaded.source,
216
+ source,
216
217
  asOf,
217
218
  ontology: { ...graph.ontology, currentVersion: ONTOLOGY_VERSION, upToDate: graph.ontology.version === ONTOLOGY_VERSION },
218
219
  entityTypes: ENTITY_KINDS.map((kind) => ({
@@ -304,6 +305,46 @@ export function openOwnerPrivateGraph(bundle, owner) {
304
305
  }
305
306
  });
306
307
  }
308
+ /**
309
+ * The same read views as the local Graph routes over a Graph v2 the host
310
+ * holds in memory (the organization Graph read under C1). The Graph is
311
+ * checked with the canonical validator first; nothing is written, and there
312
+ * are no correction records or relation sources to resolve.
313
+ */
314
+ export function openInMemoryGraph(graph, source) {
315
+ try {
316
+ validateCanonicalGraph(graph);
317
+ }
318
+ catch (error) {
319
+ throw new GraphWebError('unavailable', 'graph_invalid', errorMessage(error));
320
+ }
321
+ return Object.freeze({
322
+ source,
323
+ status: (options = {}) => {
324
+ const asOf = resolveAsOf(options);
325
+ const entities = Object.fromEntries(ENTITY_KINDS.map((kind) => [kind, graph.entities.filter((entity) => entity.type === kind).length]));
326
+ return {
327
+ status: 'ok',
328
+ version: GRAPH_WEB_VERSION,
329
+ source,
330
+ asOf,
331
+ ontology: { ...graph.ontology, currentVersion: ONTOLOGY_VERSION },
332
+ counts: {
333
+ entities: { ...entities, total: graph.entities.length },
334
+ edges: { total: graph.edges.length, active: graph.edges.filter((edge) => isActiveAt(edge, asOf)).length },
335
+ relationships: 0,
336
+ corrections: 0
337
+ },
338
+ issues: []
339
+ };
340
+ },
341
+ listProjects: (options = {}) => projectListOf(graph, source, resolveAsOf(options)),
342
+ readProject: (projectId, options = {}) => projectDetailOf(graph, source, projectId, resolveAsOf(options), NO_REFERENCES),
343
+ search: (input = {}) => entitySearchOf(graph, source, parseSearchInput(input)),
344
+ readEntity: (entityId, options = {}) => entityDetailOf(graph, source, entityId, resolveAsOf(options), NO_REFERENCES),
345
+ ontology: (options = {}) => ontologyOf(graph, source, resolveAsOf(options))
346
+ });
347
+ }
307
348
  function decisionRecordView(record) {
308
349
  return {
309
350
  title: record.title,
@@ -12,6 +12,11 @@ export declare const JUDGMENT_VALUE_PROOF_FEEDBACK_LAYERS: readonly JudgmentValu
12
12
  export interface JudgmentValueProofJournalEntry {
13
13
  readonly file: string;
14
14
  readonly proof: JudgmentValueProof;
15
+ /**
16
+ * For a judgment waiting for the owner: when another turn of the same conversation was first finalized
17
+ * after it, i.e. the conversation moved on (the owner answered there or changed the subject); null otherwise.
18
+ */
19
+ readonly conversation_moved_on_at?: string | null;
15
20
  }
16
21
  export interface JudgmentValueProofJournalRejection {
17
22
  readonly file: string;
@@ -88,6 +93,8 @@ export interface JudgmentValueProofReviewItem {
88
93
  readonly feedback_history: readonly JudgmentValueProofFeedbackRecord[];
89
94
  /** The owner's recorded answer to a judgment returned to them; null when there is none. */
90
95
  readonly answer?: JudgmentValueProofAnswerRecord | null;
96
+ /** See `JudgmentValueProofJournalEntry.conversation_moved_on_at`. Such a judgment is no longer answered here. */
97
+ readonly conversation_moved_on_at?: string | null;
91
98
  }
92
99
  export type JudgmentValueProofReviewHome = {
93
100
  readonly status: 'unavailable';
@@ -157,12 +164,29 @@ export interface JudgmentDelegationMap {
157
164
  readonly continued_after_ask: readonly JudgmentDelegationItemRef[];
158
165
  }
159
166
  export declare function defaultJudgmentJournalRoot(dataDir?: string): string;
167
+ /**
168
+ * Remembers which value proofs each folder of a judgment journal held, so that a later read lists only
169
+ * the folders that changed. A folder is listed again when its inode, mtime or ctime differs from the
170
+ * listing, or when it was listed within a few seconds of its last change. The judgment Host creates
171
+ * value proofs and finals by linking a new file and never rewrites them, so every new or removed file
172
+ * changes the folder's mtime. It keeps no proof contents; every read still reads each proof.
173
+ * Use one cache per journal.
174
+ */
175
+ export declare class JudgmentValueProofJournalCache {
176
+ #private;
177
+ /** The value proof files in `folder`, reusing the last listing while the folder is unchanged. */
178
+ valueProofFiles(folder: string): Promise<readonly string[]>;
179
+ /** Forgets folders that are no longer in the journal. */
180
+ retainOnly(folders: ReadonlySet<string>): void;
181
+ }
160
182
  /**
161
183
  * Reads saved judgment value proofs from `<root>/*.value-proof.json` and
162
184
  * `<root>/<session>/*.value-proof.json`. The journal is read-only here.
185
+ * With `cache`, folders unchanged since an earlier read are not listed again; without it every folder is listed.
163
186
  */
164
187
  export declare function readJudgmentValueProofJournal(options?: {
165
188
  readonly root?: string;
189
+ readonly cache?: JudgmentValueProofJournalCache;
166
190
  }): Promise<JudgmentValueProofJournalRead>;
167
191
  export declare function classifyJudgmentValueProof(proof: JudgmentValueProof): JudgmentValueProofReviewSection;
168
192
  /** Reads all recorded feedback in append order. A missing file means no feedback has been recorded. */