@unson/brainbase-mcp 0.30.0 → 0.32.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.
@@ -1,4 +1,5 @@
1
1
  import { type JudgmentAutonomyEvaluation, type JudgmentAutonomyResolver } from './judgment-autonomy.js';
2
+ import { canonicalJson } from './judgment-resolution-contract.js';
2
3
  type Environment = Record<string, string | undefined>;
3
4
  export interface ConversationMessage {
4
5
  sequence?: number;
@@ -204,7 +205,7 @@ export interface JudgmentStopBlock {
204
205
  block_kind?: 'autonomy_continue' | 'audit_repair';
205
206
  reason: string;
206
207
  }
207
- export declare function canonicalJson(value: unknown): string;
208
+ export { canonicalJson };
208
209
  export declare function buildJudgmentRequest(payload: JudgmentHookPayload, { env, trustedConversationMessages }?: {
209
210
  env?: Environment;
210
211
  trustedConversationMessages?: ConversationMessage[];
@@ -223,4 +224,3 @@ export declare function blockedJudgmentOutput(reason: string): {
223
224
  suppressOutput: false;
224
225
  stopReason: string;
225
226
  };
226
- export {};
@@ -3,6 +3,7 @@ import { closeSync, constants as fsConstants, fsyncSync, linkSync, mkdirSync, op
3
3
  import { homedir } from 'node:os';
4
4
  import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
5
5
  import { evaluateJudgmentAutonomy, renderJudgmentAutonomyContinuation } from './judgment-autonomy.js';
6
+ import { canonicalJson } from './judgment-resolution-contract.js';
6
7
  const RUNTIME_VERSION = 'portable-judgment-runtime-1.0.0';
7
8
  const ZERO_CALL_DISPLAY_LINE = '📚 Brainbase未参照: 必須参照なし・実呼び出し0回 ✓';
8
9
  const PORTABLE_KNOWLEDGE_TOOLS = {
@@ -62,35 +63,8 @@ const DOMAIN_TERMS = [
62
63
  ];
63
64
  const FOLLOW_UP_TERMS = ['that', 'it', 'continue', 'do that', 'それ', 'その形', 'これ', 'こちら', '続けて', 'もう一度', 'では', 'それでいい', 'そうして'];
64
65
  const AUTHORITY_INTENTS = new Set(['implement', 'operate']);
65
- export function canonicalJson(value) {
66
- if (value === null || typeof value === 'boolean' || typeof value === 'string')
67
- return JSON.stringify(value);
68
- if (typeof value === 'number') {
69
- if (!Number.isFinite(value))
70
- throw new TypeError('canonical JSON only supports finite numbers');
71
- return JSON.stringify(value);
72
- }
73
- if (Array.isArray(value))
74
- return `[${value.map((entry) => canonicalJson(entry)).join(',')}]`;
75
- if (value && typeof value === 'object') {
76
- return `{${Object.keys(value).sort(compareCodePoints).map((key) => {
77
- const entry = value[key];
78
- if (entry === undefined)
79
- throw new TypeError('canonical JSON does not support undefined');
80
- return `${JSON.stringify(key)}:${canonicalJson(entry)}`;
81
- }).join(',')}}`;
82
- }
83
- throw new TypeError(`canonical JSON does not support ${typeof value}`);
84
- }
85
- function compareCodePoints(left, right) {
86
- const a = Array.from(left, (value) => value.codePointAt(0));
87
- const b = Array.from(right, (value) => value.codePointAt(0));
88
- for (let index = 0; index < Math.min(a.length, b.length); index += 1) {
89
- if (a[index] !== b[index])
90
- return a[index] - b[index];
91
- }
92
- return a.length - b.length;
93
- }
66
+ // canonicalJson is the shared judgment resolution contract; re-exported so existing importers keep working.
67
+ export { canonicalJson };
94
68
  function sha256(value) {
95
69
  return createHash('sha256').update(value).digest('hex');
96
70
  }
@@ -0,0 +1,14 @@
1
+ export declare function canonicalJson(value: unknown): string;
2
+ export declare function sha256Hex(value: string): string;
3
+ export declare function computeRequestDigest(rawInput: unknown): string;
4
+ export declare class JudgmentResolutionError extends Error {
5
+ readonly code: string;
6
+ readonly status: number;
7
+ constructor(code: string, message: string, status?: number);
8
+ toJSON(): {
9
+ name: string;
10
+ code: string;
11
+ status: number;
12
+ message: string;
13
+ };
14
+ }
@@ -0,0 +1,53 @@
1
+ import { createHash } from 'node:crypto';
2
+ // Shared contract between the judgment Host and the judgment resolver.
3
+ // The resolver's HTTP mount and its service must throw and recognize the
4
+ // same error class, and both sides must produce identical canonical bytes,
5
+ // so these live in one public module instead of being copied per caller.
6
+ function compareCodePoints(left, right) {
7
+ const a = Array.from(left, (value) => value.codePointAt(0));
8
+ const b = Array.from(right, (value) => value.codePointAt(0));
9
+ for (let index = 0; index < Math.min(a.length, b.length); index += 1) {
10
+ if (a[index] !== b[index])
11
+ return a[index] - b[index];
12
+ }
13
+ return a.length - b.length;
14
+ }
15
+ export function canonicalJson(value) {
16
+ if (value === null || typeof value === 'boolean' || typeof value === 'string')
17
+ return JSON.stringify(value);
18
+ if (typeof value === 'number') {
19
+ if (!Number.isFinite(value))
20
+ throw new TypeError('canonical JSON only supports finite numbers');
21
+ return JSON.stringify(value);
22
+ }
23
+ if (Array.isArray(value))
24
+ return `[${value.map((entry) => canonicalJson(entry)).join(',')}]`;
25
+ if (value && typeof value === 'object') {
26
+ return `{${Object.keys(value).sort(compareCodePoints).map((key) => {
27
+ const entry = value[key];
28
+ if (entry === undefined)
29
+ throw new TypeError('canonical JSON does not support undefined');
30
+ return `${JSON.stringify(key)}:${canonicalJson(entry)}`;
31
+ }).join(',')}}`;
32
+ }
33
+ throw new TypeError(`canonical JSON does not support ${typeof value}`);
34
+ }
35
+ export function sha256Hex(value) {
36
+ return createHash('sha256').update(value).digest('hex');
37
+ }
38
+ export function computeRequestDigest(rawInput) {
39
+ return sha256Hex(canonicalJson(rawInput));
40
+ }
41
+ export class JudgmentResolutionError extends Error {
42
+ code;
43
+ status;
44
+ constructor(code, message, status = 400) {
45
+ super(message);
46
+ this.name = 'JudgmentResolutionError';
47
+ this.code = code;
48
+ this.status = status;
49
+ }
50
+ toJSON() {
51
+ return { name: this.name, code: this.code, status: this.status, message: this.message };
52
+ }
53
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unson/brainbase-mcp",
3
- "version": "0.30.0",
3
+ "version": "0.32.0",
4
4
  "description": "Local-first Brainbase judgment OS that lets supported AI agents reuse a user's approved goals, priorities, decisions, and judgment criteria.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -183,6 +183,10 @@
183
183
  "types": "./dist/knowledge-resolution-service.d.ts",
184
184
  "import": "./dist/knowledge-resolution-service.js"
185
185
  },
186
+ "./judgment-resolution-contract": {
187
+ "types": "./dist/judgment-resolution-contract.d.ts",
188
+ "import": "./dist/judgment-resolution-contract.js"
189
+ },
186
190
  "./canonical-document-writer": {
187
191
  "types": "./dist/canonical-document-writer.d.ts",
188
192
  "import": "./dist/canonical-document-writer.js"
@@ -451,5 +455,5 @@
451
455
  "engines": {
452
456
  "node": ">=20"
453
457
  },
454
- "gitHead": "6e6cc04041ae0745272e8e61f53fe93cc06d3e40"
458
+ "gitHead": "3f91401cb7221305fe1dfafc6126482bc8b55170"
455
459
  }
@@ -146,6 +146,14 @@
146
146
  font-weight: 700;
147
147
  }
148
148
  .bb-world-canvas-ui .bb-world-canvas-quest-text { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
149
+ /* A quest with a way down (候補の確認) is clickable even though the quest list lets the map take the pointer. */
150
+ .bb-world-canvas-ui .bb-world-canvas-quest a {
151
+ flex: none;
152
+ color: #b5f2ab;
153
+ text-decoration: underline;
154
+ text-underline-offset: 2px;
155
+ pointer-events: auto;
156
+ }
149
157
 
150
158
  .bb-world-canvas-ui .bb-world-canvas-edge {
151
159
  position: absolute;
@@ -357,13 +357,14 @@ export function createWorldCanvasUI({ doc, stage, reducedMotion = false, onReset
357
357
  activate('help', help);
358
358
  focus(helpTitle);
359
359
  }
360
- function addNote({ label = '', text = '', tone = '', summary } = {}) {
360
+ function addNote({ label = '', text = '', tone = '', summary, link = null } = {}) {
361
361
  if (disposed) return;
362
362
  const note = element('section', 'bb-world-canvas-note');
363
363
  if (label) note.append(element('h3', '', label));
364
364
  if (text || summary) note.append(element('p', '', text || summary));
365
365
  if (label || text || summary) notes.append(note);
366
366
  if (typeof summary !== 'string' || !summary.trim()) return;
367
+ // A link (already checked by the caller) follows the summary so the quest is a way down, not only a sign.
367
368
  const quest = element('p', 'bb-world-canvas-quest');
368
369
  const icon = tone === 'attention' || tone === 'warning' ? '!' : 'i';
369
370
  quest.append(
@@ -373,6 +374,7 @@ export function createWorldCanvasUI({ doc, stage, reducedMotion = false, onReset
373
374
  // The accessible name contains the unabridged summary even when visual text
374
375
  // is ellipsized on a narrow canvas; the long explanation remains in help.
375
376
  quest.setAttribute('aria-label', summary.trim());
377
+ if (link?.href) quest.append(element('a', link.className ?? '', link.label ?? summary.trim(), { href: link.href }));
376
378
  quests.append(quest);
377
379
  }
378
380
  function clearNotes() {
@@ -40,6 +40,7 @@
40
40
  }
41
41
 
42
42
  .bb-world-hud-line.is-attention { border-left-color: var(--bb-color-danger); }
43
+ .bb-world-hud-line a { margin-left: 0.5em; color: inherit; text-decoration: underline; }
43
44
  .bb-world-hud-line.is-warning {
44
45
  border-left-color: var(--bb-color-warning);
45
46
  background: rgb(255 240 199 / 95%);
@@ -33,7 +33,7 @@ import { createWorldCanvasUI } from './world-canvas-ui.js';
33
33
  import { workCanvasNotes } from './world-canvas-notes.js';
34
34
  import { canvasTexture, drawFacade, FACADE_SIZES, FACADE_UNITS, gableRoof, hashUnit, muted, paintVertices, roundedPlate, valueNoise } from './world-scenery.js';
35
35
  import { WORK_STATES, workCityBlocks, workGrowthBlock, workPickBlock, workSign, workSiteBlocks } from './world-work-rail.js';
36
- import { exitLegend, exitsRailBlock, normalizeBusinessExits } from './world-exits.js';
36
+ import { exitHrefKind, exitLegend, exitsRailBlock, normalizeBusinessExits } from './world-exits.js';
37
37
  import {
38
38
  makeWorkspaceElement as el,
39
39
  workspacePageHeader,
@@ -1462,13 +1462,64 @@ function addressSelection() {
1462
1462
  * label?, as_of? } }] }`. `href` and `action.href` must be `https:` or a link inside the host (`/…`, `?…`);
1463
1463
  * others are dropped and counted. Without it the world is drawn exactly as before.
1464
1464
  * @typedef {{ status: string, read_at?: string, reason?: string, exits: object[] }} BusinessExitsAnswer
1465
+ * @param {() => (MemoryStatusAnswer | Promise<MemoryStatusAnswer>)} [options.memoryStatus] How the memory loop stands
1466
+ * (story-world-memory-status-v1, ledger P18): last night's organizing and the memory candidates that wait for the
1467
+ * owner. The answer is `{ status: 'available'|'partial'|'not_connected', night?: { status:
1468
+ * 'completed'|'partial'|'failed'|'missing', finished_at? } | null, candidates?: { count } | null, review_href? }`.
1469
+ * A part that is null was not read and is shown as unknown, never as zero. Without it the world says 未接続.
1470
+ * @typedef {{ status: string, night?: object | null, candidates?: object | null, review_href?: string }} MemoryStatusAnswer
1465
1471
  */
1472
+ async function readMemoryStatus(memoryStatus) {
1473
+ if (typeof memoryStatus !== 'function') return { status: 'not_connected' };
1474
+ try {
1475
+ const answer = await memoryStatus();
1476
+ return answer && typeof answer === 'object' ? answer : { status: 'failed', reason: 'invalid_answer' };
1477
+ } catch (error) {
1478
+ return { status: 'failed', reason: error instanceof Error ? error.message : 'request_failed' };
1479
+ }
1480
+ }
1481
+
1482
+ const NIGHT_STATES = {
1483
+ completed: { text: '済み', tone: 'info' },
1484
+ partial: { text: '一部だけ(確かめられていない範囲があります)', tone: 'warning' },
1485
+ failed: { text: 'できなかった(整理できなかった夜)', tone: 'attention' },
1486
+ missing: { text: '動いていない(整理できなかった夜)', tone: 'attention' },
1487
+ };
1488
+
1489
+ /** The memory loop as world notes (story-world-memory-status-v1). Unknown is never shown as zero. */
1490
+ function memoryLines(answer) {
1491
+ if (answer.status === 'not_connected') {
1492
+ return [{ text: '記憶の整理:未接続。この画面は夜の整理の結果と記憶の候補を受け取れません。候補の件数は0件とは確認できません。', tone: 'info', summary: '記憶の整理:未接続' }];
1493
+ }
1494
+ if (answer.status !== 'available' && answer.status !== 'partial') {
1495
+ return [{ text: `記憶の整理:読めない(${answer.reason ?? '理由不明'})。候補の件数は0件とは確認できません。`, tone: 'warning', summary: '記憶の整理:読めない' }];
1496
+ }
1497
+ const lines = [];
1498
+ const night = answer.night && NIGHT_STATES[answer.night.status];
1499
+ if (night) {
1500
+ const label = `昨夜の記憶の整理:${night.text}`;
1501
+ lines.push({ text: `${label}。`, tone: night.tone, summary: night.tone === 'attention' ? '整理できなかった夜' : label });
1502
+ } else {
1503
+ lines.push({ text: '昨夜の記憶の整理:読めない。', tone: 'warning', summary: '昨夜の記憶の整理:読めない' });
1504
+ }
1505
+ const count = answer.candidates?.count;
1506
+ if (Number.isInteger(count) && count >= 0) {
1507
+ const href = typeof answer.review_href === 'string' && exitHrefKind(answer.review_href) ? answer.review_href : null;
1508
+ const link = href && count > 0 ? { href, label: '候補の確認を開く', className: 'bb-world-memory-link' } : null;
1509
+ if (count > 0) lines.push({ text: `あなたが決める記憶の候補 ${count}件。昇格したものだけが正本になります。`, tone: 'attention', summary: `あなたが決める記憶の候補 ${count}件`, link });
1510
+ else lines.push({ text: 'あなたが決める記憶の候補はありません。', tone: 'info', summary: null });
1511
+ } else {
1512
+ lines.push({ text: '記憶の候補:読めない。件数は0件とは確認できません。', tone: 'warning', summary: '記憶の候補:読めない' });
1513
+ }
1514
+ return lines;
1515
+ }
1516
+
1466
1517
  /** 判断の履歴 in the local web (it was 今日, `#today`, until 2026-10-11; the shell still opens old links). */
1467
1518
  function defaultJudgmentHref(decisionAttemptId) {
1468
1519
  return decisionAttemptId ? `#value-proofs?decision=${encodeURIComponent(decisionAttemptId)}` : '#value-proofs';
1469
1520
  }
1470
1521
 
1471
- export function createWorldView({ root, rail, page, presentation = 'standard', document: explicitDocument, fetcher, projectHref = (project) => `#projects?project=${encodeURIComponent(project.id)}`, taskHref = null, judgmentHref = defaultJudgmentHref, selection = addressSelection(), businessExits = null }) {
1522
+ export function createWorldView({ root, rail, page, presentation = 'standard', document: explicitDocument, fetcher, projectHref = (project) => `#projects?project=${encodeURIComponent(project.id)}`, taskHref = null, judgmentHref = defaultJudgmentHref, selection = addressSelection(), businessExits = null, memoryStatus = null }) {
1472
1523
  const exitsEnabled = typeof businessExits === 'function';
1473
1524
  const doc = explicitDocument ?? globalThis.document;
1474
1525
  const reducedMotion = Boolean(globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches);
@@ -2311,6 +2362,7 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
2311
2362
  }
2312
2363
 
2313
2364
  async function load() {
2365
+ const memoryRead = readMemoryStatus(memoryStatus);
2314
2366
  const businessesResult = await readJson(fetcher, '/api/extensions/world/businesses');
2315
2367
  if (disposed) return;
2316
2368
  // A world drawn from an organization's Graph shows only that organization's judgments: the host keeps
@@ -2333,13 +2385,14 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
2333
2385
  }
2334
2386
  if (disposed) return;
2335
2387
  notices.replaceChildren();
2336
- const note = (label, textValue, tone = 'info', summary = null) => {
2388
+ const note = (label, textValue, tone = 'info', summary = null, link = null) => {
2337
2389
  if (canvasUI) {
2338
- setCanvasNote(`${label}:${summary ?? 'detail'}`, { label, text: textValue, tone, summary });
2390
+ setCanvasNote(`${label}:${summary ?? 'detail'}`, { label, text: textValue, tone, summary, link });
2339
2391
  return;
2340
2392
  }
2341
2393
  const line = el(doc, 'p', { className: `bb-world-hud-line is-${tone}`, attrs: tone === 'info' ? {} : { role: 'alert' } });
2342
2394
  line.append(el(doc, 'strong', { text: label }), el(doc, 'span', { text: textValue }));
2395
+ if (link) line.append(el(doc, 'a', { className: link.className, text: link.label, attrs: { href: link.href } }));
2343
2396
  notices.append(line);
2344
2397
  };
2345
2398
  const businessPayload = businessesResult.ok ? businessesResult.data : null;
@@ -2387,6 +2440,9 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
2387
2440
  }
2388
2441
  }
2389
2442
  const rowItems = new Map(rows.map((row) => [row.key, Array.isArray(row.items) ? row.items : []]));
2443
+ const memory = await memoryRead;
2444
+ if (disposed) return;
2445
+ for (const line of memoryLines(memory)) note('記憶', line.text, line.tone, line.summary, line.link);
2390
2446
 
2391
2447
  knownBusinesses = businesses;
2392
2448
  canvasUI?.setCities(businesses, current.code);