@unson/brainbase-mcp 0.11.0 → 0.13.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.
@@ -10,7 +10,7 @@ import { createFoundationRevisionStore, FoundationStoreError } from './foundatio
10
10
  import { createGraphWebHttpHandler } from './graph-web-http.js';
11
11
  import { defaultJudgmentJournalRoot } from './judgment-value-proof-review.js';
12
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';
13
+ import { assertSameOrigin, isLoopbackHost, LOCAL_WEB_CONTENT_SECURITY_POLICY, LocalWebHttpError, readJsonObjectBody, requestUrl, tokenMatches, writeHttpError, writeJson } from './local-web-security.js';
14
14
  import { resolveDataDir } from './paths.js';
15
15
  import { loadPersonalOs, readPersonalOsSidecar } from './ssot.js';
16
16
  import { createValueProofReviewHttpHandler, VALUE_PROOF_REVIEW_TOKEN_HEADER } from './value-proof-review-http.js';
@@ -437,7 +437,7 @@ function worldModelError(error) {
437
437
  }
438
438
  if (error instanceof LocalWebHttpError)
439
439
  return error;
440
- return new LocalWebHttpError(500, 'internal_error', 'World model read failed');
440
+ return new LocalWebHttpError(500, 'internal_error', 'World model request failed');
441
441
  }
442
442
  async function listReadableSidecarRecords(input) {
443
443
  try {
@@ -473,11 +473,112 @@ async function listReadableSidecarRecords(input) {
473
473
  }
474
474
  return { records, unreadable: { count, codes: [...codes] } };
475
475
  }
476
+ const LOCAL_WEB_WORLD_MODEL_SCOPE_START = '1970-01-01T00:00:00.000Z';
477
+ const WORLD_MODEL_VALUE_KINDS = new Set(['number', 'boolean', 'string', 'state']);
478
+ const WORLD_MODEL_AGGREGATIONS = new Set(['none', 'sum', 'average', 'count', 'min', 'max', 'last', 'custom']);
479
+ const WORLD_MODEL_VARIABLE_FIELDS = new Set([
480
+ 'meaning', 'subject', 'valueKind', 'unit', 'aggregation', 'granularity', 'measurementMethod'
481
+ ]);
482
+ const WORLD_MODEL_OBSERVATION_FIELDS = new Set(['variableRef', 'value']);
483
+ const WORLD_MODEL_AUTHORITY_FIELDS = new Set([
484
+ 'acl', 'scope', 'storage', 'provenance', 'authorizedUses', 'ownerId', 'readerIds', 'writerIds',
485
+ 'principal', 'subjectId', 'recordedAt', 'occurredAt', 'period', 'id', 'revision', 'type'
486
+ ]);
487
+ function worldModelInvalid(message) {
488
+ return new LocalWebHttpError(400, 'invalid_input', message);
489
+ }
490
+ function assertWorldModelBodyFields(body, allowed) {
491
+ for (const key of Object.keys(body)) {
492
+ if (allowed.has(key))
493
+ continue;
494
+ if (WORLD_MODEL_AUTHORITY_FIELDS.has(key)) {
495
+ throw new LocalWebHttpError(400, 'authority_field_in_body', `${key} is decided by the local Web host`);
496
+ }
497
+ throw worldModelInvalid(`Unknown World Model field ${key}`);
498
+ }
499
+ }
500
+ function requiredWorldModelText(body, key) {
501
+ const value = body[key];
502
+ if (typeof value !== 'string' || value.trim() === '')
503
+ throw worldModelInvalid(`${key} is required`);
504
+ return value.trim();
505
+ }
506
+ function worldModelVariableFromBody(body, principal) {
507
+ assertWorldModelBodyFields(body, WORLD_MODEL_VARIABLE_FIELDS);
508
+ const valueKind = requiredWorldModelText(body, 'valueKind');
509
+ if (!WORLD_MODEL_VALUE_KINDS.has(valueKind))
510
+ throw worldModelInvalid(`Unknown valueKind ${valueKind}`);
511
+ const aggregation = requiredWorldModelText(body, 'aggregation');
512
+ if (!WORLD_MODEL_AGGREGATIONS.has(aggregation))
513
+ throw worldModelInvalid(`Unknown aggregation ${aggregation}`);
514
+ const unit = body.unit === undefined ? undefined : requiredWorldModelText(body, 'unit');
515
+ if (valueKind === 'number' && !unit)
516
+ throw worldModelInvalid('unit is required for number variables');
517
+ return {
518
+ id: `web.variable.${randomBytes(10).toString('hex')}`,
519
+ type: 'variable',
520
+ revision: '1',
521
+ meaning: requiredWorldModelText(body, 'meaning'),
522
+ epistemicState: 'unverified',
523
+ adoptionState: 'draft',
524
+ authorizedUses: ['draft', 'judgment', 'evaluation'],
525
+ acl: { ownerId: principal, visibility: 'private', readerIds: [], writerIds: [] },
526
+ storage: 'ontology',
527
+ provenance: [{ sourceId: LOCAL_WEB_PROVENANCE_SOURCE_ID, sourceKind: 'observation', evidenceIds: [] }],
528
+ scope: { subjectIds: [principal], validFrom: LOCAL_WEB_WORLD_MODEL_SCOPE_START },
529
+ subject: requiredWorldModelText(body, 'subject'),
530
+ valueKind: valueKind,
531
+ ...(unit ? { unit } : {}),
532
+ aggregation: aggregation,
533
+ granularity: requiredWorldModelText(body, 'granularity'),
534
+ measurementMethod: requiredWorldModelText(body, 'measurementMethod'),
535
+ // The local host stamps the current date into observations; a broad owner-only
536
+ // validity window avoids asking the first-time user for hidden scope fields.
537
+ };
538
+ }
539
+ function worldModelReference(value) {
540
+ if (!value || typeof value !== 'object' || Array.isArray(value))
541
+ throw worldModelInvalid('variableRef is required');
542
+ const reference = value;
543
+ if (reference.type !== 'variable' || typeof reference.id !== 'string' || reference.id.trim() === ''
544
+ || typeof reference.revision !== 'string' || !/^[1-9]\d*$/u.test(reference.revision)) {
545
+ throw worldModelInvalid('variableRef must reference a variable revision');
546
+ }
547
+ if (Object.keys(reference).some((key) => !['id', 'type', 'revision'].includes(key))) {
548
+ throw worldModelInvalid('variableRef contains an unsupported field');
549
+ }
550
+ return { id: reference.id.trim(), type: 'variable', revision: reference.revision };
551
+ }
552
+ function worldModelObservationFromBody(body, principal, now) {
553
+ assertWorldModelBodyFields(body, WORLD_MODEL_OBSERVATION_FIELDS);
554
+ if (!Object.hasOwn(body, 'value') || body.value === null || body.value === undefined)
555
+ throw worldModelInvalid('value is required');
556
+ const value = body.value;
557
+ if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
558
+ throw worldModelInvalid('value must be a string, number, or boolean');
559
+ }
560
+ if (typeof value === 'number' && !Number.isFinite(value)) {
561
+ throw worldModelInvalid('value must be a finite number');
562
+ }
563
+ const nowIso = now.toISOString();
564
+ const day = nowIso.slice(0, 10);
565
+ const observationId = `web.observation.${randomBytes(10).toString('hex')}`;
566
+ return {
567
+ id: observationId,
568
+ variableRef: worldModelReference(body.variableRef),
569
+ subjectId: principal,
570
+ value,
571
+ occurredAt: nowIso,
572
+ period: { from: `${day}T00:00:00.000Z`, until: `${day}T23:59:59.999Z` },
573
+ recordedAt: nowIso,
574
+ sourceRef: { sourceId: LOCAL_WEB_PROVENANCE_SOURCE_ID, sourceKind: 'observation', evidenceIds: [observationId] }
575
+ };
576
+ }
476
577
  /**
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.
578
+ * World Model routes. Reads retain the explicit absence/partial contract;
579
+ * the first-user Web entry writes only a trusted Variable definition and an
580
+ * initial Observation. Models, adoption and corrections stay out of this
581
+ * focused route until their own authority and review contract exists.
481
582
  */
482
583
  export function createWorldModelReadModule(context) {
483
584
  const foundationStore = createFoundationRevisionStore({ dataDir: context.dataDir });
@@ -519,17 +620,39 @@ export function createWorldModelReadModule(context) {
519
620
  writeJson(response, 404, { error: { code: 'not_found', message: 'Not found' } });
520
621
  return true;
521
622
  }
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
623
  const graph = await readLocalGraphState(context.dataDir);
527
624
  if (graph.status !== 'ready' || !graph.ownerId) {
528
625
  writeGraphUnavailable(response, graph, context.dataDir);
529
626
  return true;
530
627
  }
628
+ const principal = { principal: graph.ownerId };
629
+ if (request.method === 'POST' && (resource === 'variables' || resource === 'observations')) {
630
+ try {
631
+ const body = await readJsonObjectBody(request, LOCAL_WEB_FOUNDATION_BODY_LIMIT_BYTES);
632
+ if (resource === 'variables') {
633
+ const definition = worldModelVariableFromBody(body, graph.ownerId);
634
+ const reference = await worldModel.createVariable(definition, principal);
635
+ writeJson(response, 201, {
636
+ reference: { id: reference.id, type: reference.type, revision: reference.revision },
637
+ digest: reference.digest
638
+ });
639
+ }
640
+ else {
641
+ const observation = await worldModel.saveObservation(worldModelObservationFromBody(body, graph.ownerId, context.now()), principal);
642
+ writeJson(response, 201, { observation });
643
+ }
644
+ }
645
+ catch (error) {
646
+ writeHttpError(response, worldModelError(error));
647
+ }
648
+ return true;
649
+ }
650
+ if (request.method !== 'GET') {
651
+ writeJson(response, 405, { error: { code: 'method_not_allowed', message: 'この入口では変数と観測値を登録できます' } });
652
+ return true;
653
+ }
531
654
  try {
532
- writeJson(response, 200, await read(resource, { principal: graph.ownerId }));
655
+ writeJson(response, 200, await read(resource, principal));
533
656
  }
534
657
  catch (error) {
535
658
  writeHttpError(response, worldModelError(error));
@@ -619,7 +742,9 @@ if (extensionScreens.some((screen, index) => screen?.id !== extensionIds[index]
619
742
  throw new Error('Invalid extension screen id');
620
743
  }
621
744
  const meta = (name) => document.querySelector(\`meta[name="\${name}"]\`)?.getAttribute('content') ?? '';
622
- const screenFromHash = () => decodeURIComponent(location.hash.replace(/^#/, '')) || undefined;
745
+ // Keep the query portion intact; the shell parses the Graph entity query and
746
+ // only treats the entity id as a target of the Graph screen.
747
+ const screenFromHash = () => location.hash.replace(/^#/, '') || undefined;
623
748
  const shell = createLocalWebShell({
624
749
  root: document.getElementById('brainbase-local-web'),
625
750
  token: meta('brainbase-web-token'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unson/brainbase-mcp",
3
- "version": "0.11.0",
3
+ "version": "0.13.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",
@@ -155,6 +155,10 @@
155
155
  "types": "./dist/judgment-problem-snapshot.d.ts",
156
156
  "import": "./dist/judgment-problem-snapshot.js"
157
157
  },
158
+ "./judgment-problem-snapshot-http": {
159
+ "types": "./dist/judgment-problem-snapshot-http.d.ts",
160
+ "import": "./dist/judgment-problem-snapshot-http.js"
161
+ },
158
162
  "./personal-knowledge": {
159
163
  "types": "./dist/personal-knowledge.d.ts",
160
164
  "import": "./dist/personal-knowledge.js"
@@ -348,5 +352,5 @@
348
352
  "engines": {
349
353
  "node": ">=20"
350
354
  },
351
- "gitHead": "070f39f8014fbbe224bb4615ef89999aeeb8d0a5"
355
+ "gitHead": "c9b9adbff2dd409f77ed868d9c3939b3269f2d4f"
352
356
  }
@@ -163,6 +163,7 @@ function textCell(value, fallback) {
163
163
  * @param {string} [options.token] Launch token sent with a correction.
164
164
  * @param {string} [options.tokenHeader] Header name for the token.
165
165
  * @param {boolean} [options.autoLoad=true] Runs the empty search and reads the kinds on mount.
166
+ * @param {string} [options.initialEntityId] Opens this exact entity after the initial Graph read.
166
167
  * @param {() => Date} [options.now] The owner's clock.
167
168
  * @param {boolean} [options.canCorrect=true] When false, no correction control is drawn anywhere
168
169
  * (この記録を直す, 関係を加える, この関係を直す, and この画面で直せること under the kinds of
@@ -204,6 +205,7 @@ export function createGraphRegistryView({
204
205
  token,
205
206
  tokenHeader,
206
207
  autoLoad = true,
208
+ initialEntityId,
207
209
  now = () => new Date(),
208
210
  canCorrect = true,
209
211
  correctionScope,
@@ -617,6 +619,23 @@ export function createGraphRegistryView({
617
619
  const canFix = scope.entity(entity.type) && scope.fields(graphEntityCorrectionFields(entity.type)).length > 0;
618
620
  const relatable = newRelationOptions(entity.type).length > 0;
619
621
  const related = loaded.outgoing.length + loaded.incoming.length > 0;
622
+ const correctionEntries = [
623
+ canFix ? '「この記録を直す」' : null,
624
+ relatable && scope.createEdges ? '「関係を加える」' : null,
625
+ related && scope.edges ? '関係ごとの「この関係を直す」' : null,
626
+ ].filter(Boolean);
627
+ if (correctionEntries.length > 0) {
628
+ const lastEntry = correctionEntries.at(-1);
629
+ const entries = correctionEntries.length === 1
630
+ ? lastEntry
631
+ : `${correctionEntries.slice(0, -1).join('、')}、または${lastEntry}`;
632
+ children.push(workspaceRailBlock(doc, {
633
+ title: '訂正の流れ',
634
+ content: makeElement(doc, 'p', {
635
+ text: `誤りを見つけたときの訂正入口です。${entries}から変更案を入力します。承認待ちの提出ではなく、保存するとその場でGraphに反映され、理由と変更前後が履歴に残ります。保存後に読み直して一致を確かめ、確認できた内容は、次のMCPの search・get_context・resolve_entity で使われます。`,
636
+ }),
637
+ }));
638
+ }
620
639
  const actions = [
621
640
  canFix ? workspaceButton(doc, { text: 'この記録を直す', onClick: () => openCorrection('entity', () => correction.openEntity(entity, { title: 'この記録を直す' })) }) : null,
622
641
  relatable && scope.createEdges
@@ -797,7 +816,14 @@ export function createGraphRegistryView({
797
816
  },
798
817
  };
799
818
  controller.render();
800
- if (autoLoad) void controller.load();
819
+ if (autoLoad) {
820
+ void controller.load().then(() => {
821
+ if (initialEntityId) return controller.openEntity(initialEntityId);
822
+ return undefined;
823
+ });
824
+ } else if (initialEntityId) {
825
+ void controller.openEntity(initialEntityId);
826
+ }
801
827
  return controller;
802
828
  }
803
829
 
@@ -21,6 +21,7 @@ import { createObjectiveEditorController } from './objective-editor.js';
21
21
  import { createObjectiveEditorHttpPort } from './objective-editor-http-port.js';
22
22
  import { createValueProofReviewUI } from './value-proof-review.js';
23
23
  import { createWorldModelView } from './world-model-view.js';
24
+ import { createGraphClient } from './graph-view-shared.js';
24
25
 
25
26
  export const LOCAL_WEB_SHELL_CONTRACT_VERSION = 'brainbase.local-web-shell.v1';
26
27
 
@@ -75,6 +76,60 @@ function isRecord(value) {
75
76
  return value !== null && typeof value === 'object' && !Array.isArray(value);
76
77
  }
77
78
 
79
+ /**
80
+ * Parses the local shell's hash target without treating query data as a
81
+ * screen id. Only the Graph screen accepts an entity target; all other
82
+ * screens deliberately discard query parameters.
83
+ */
84
+ export function parseLocalWebTarget(target) {
85
+ const raw = typeof target === 'string' ? target.replace(/^#/, '') : '';
86
+ const separator = raw.indexOf('?');
87
+ const screenId = separator < 0 ? raw : raw.slice(0, separator);
88
+ if (!screenId) return { screenId: null, entityId: null };
89
+ const query = separator < 0 ? '' : raw.slice(separator + 1);
90
+ const params = new URLSearchParams(query);
91
+ return {
92
+ screenId,
93
+ entityId: screenId === 'graph' ? params.get('entity_id')?.trim() || null : null,
94
+ };
95
+ }
96
+
97
+ function graphSourceReader(context) {
98
+ const client = createGraphClient({ fetcher: context.fetcher, basePath: '/api/graph', token: context.token });
99
+ return async (source) => {
100
+ if (!isRecord(source) || source.kind !== 'local_graph'
101
+ || typeof source.entity_id !== 'string' || !source.entity_id.trim()
102
+ || typeof source.entity_type !== 'string' || !source.entity_type.trim()) {
103
+ return { state: 'unavailable', reason: 'source_contract_invalid' };
104
+ }
105
+ const result = await client.read(`/entities/${encodeURIComponent(source.entity_id)}`);
106
+ if (result.state === 'error') {
107
+ if (result.status === 404 || result.code === 'entity_not_found') return { state: 'not_found' };
108
+ if (result.status === 403 || result.code === 'authorization_denied' || result.code === 'scope_violation') return { state: 'forbidden' };
109
+ if (result.code === 'entity_id_ambiguous') return { state: 'ambiguous' };
110
+ return { state: 'unavailable', reason: result.code };
111
+ }
112
+ if (result.state !== 'ok' || !isRecord(result.payload?.entity)) return { state: 'unavailable', reason: 'source_response_invalid' };
113
+ const entity = result.payload.entity;
114
+ if (entity.id !== source.entity_id || entity.type !== source.entity_type) {
115
+ return { state: 'unavailable', reason: 'source_target_mismatch' };
116
+ }
117
+ if (source.digest && entity.digest !== source.digest) {
118
+ return { state: 'unavailable', reason: 'source_digest_mismatch' };
119
+ }
120
+ // The local Graph response currently exposes a content digest, not a
121
+ // revision number. A requested version is therefore linkable only when
122
+ // the provider returns the same explicit version field.
123
+ if (source.version) {
124
+ const currentVersion = entity.version ?? entity.revision ?? null;
125
+ if (!currentVersion || currentVersion !== source.version) {
126
+ return { state: 'unavailable', reason: 'source_version_mismatch' };
127
+ }
128
+ }
129
+ return { state: 'available' };
130
+ };
131
+ }
132
+
78
133
  function makePage(container, context) {
79
134
  const page = makeElement(context.document, 'div', { className: 'bb-shell-page' });
80
135
  container.append(page);
@@ -89,6 +144,7 @@ function mountToday(container, context) {
89
144
  document: context.document,
90
145
  fetcher: context.fetcher,
91
146
  token: context.token,
147
+ sourceReader: graphSourceReader(context),
92
148
  });
93
149
  }
94
150
 
@@ -129,7 +185,13 @@ function mountObjectives(container, context) {
129
185
  constraintsEditable: false,
130
186
  storyLinks: false,
131
187
  });
132
- const worldModel = createWorldModelView({ root: worldRoot, rail: context.rail, document: doc, fetcher: context.fetcher });
188
+ const worldModel = createWorldModelView({
189
+ root: worldRoot,
190
+ rail: context.rail,
191
+ document: doc,
192
+ fetcher: context.fetcher,
193
+ token: context.token,
194
+ });
133
195
  return { editor, worldModel };
134
196
  }
135
197
 
@@ -144,6 +206,7 @@ function mountGraphScreen(createView) {
144
206
  document: context.document,
145
207
  fetcher: context.fetcher,
146
208
  token: context.token,
209
+ initialEntityId: context.initialEntityId,
147
210
  });
148
211
  };
149
212
  }
@@ -248,6 +311,7 @@ export function createLocalWebShell({
248
311
  const mounted = new Map();
249
312
  const slots = new Map();
250
313
  const links = new Map();
314
+ const pendingEntityIds = new Map();
251
315
  let active = null;
252
316
 
253
317
  const shell = makeElement(doc, 'div', { className: 'bb-shell', attrs: { 'data-contract-version': LOCAL_WEB_SHELL_CONTRACT_VERSION } });
@@ -311,8 +375,14 @@ export function createLocalWebShell({
311
375
  for (const [screenId, railSlot] of rails) railSlot.hidden = !(showRail && screenId === active);
312
376
  }
313
377
 
314
- function ensureMounted(screen) {
315
- if (mounted.has(screen.id)) return;
378
+ function ensureMounted(screen, entityId = pendingEntityIds.get(screen.id) ?? null) {
379
+ if (mounted.has(screen.id)) {
380
+ if (entityId && screen.id === 'graph') {
381
+ const view = mounted.get(screen.id);
382
+ if (typeof view?.openEntity === 'function') void view.openEntity(entityId);
383
+ }
384
+ return;
385
+ }
316
386
  const slot = slots.get(screen.id);
317
387
  if (screen.usesGraph && !(status.phase === 'ready' && status.data.graph.status === 'ready')) {
318
388
  renderGraphGate(doc, slot, status, controller.refreshStatus, screen.label);
@@ -322,7 +392,7 @@ export function createLocalWebShell({
322
392
  const rail = rails.get(screen.id) ?? null;
323
393
  rail?.replaceChildren();
324
394
  const page = Object.freeze({ crumbs: Object.freeze(['あなたのBrainbase', screen.label]), source: screen.source ?? null });
325
- mounted.set(screen.id, screen.mount(slot, Object.freeze({ ...context, rail, page })) ?? true);
395
+ mounted.set(screen.id, screen.mount(slot, Object.freeze({ ...context, rail, page, initialEntityId: entityId })) ?? true);
326
396
  updateRail();
327
397
  }
328
398
 
@@ -330,15 +400,17 @@ export function createLocalWebShell({
330
400
  get state() {
331
401
  return { active, status, mounted: [...mounted.keys()] };
332
402
  },
333
- show(id) {
334
- const screen = screens.find((candidate) => candidate.id === id) ?? screens[0];
403
+ show(target) {
404
+ const parsed = parseLocalWebTarget(target);
405
+ const screen = screens.find((candidate) => candidate.id === parsed.screenId) ?? screens[0];
406
+ pendingEntityIds.set(screen.id, screen.id === 'graph' ? parsed.entityId : null);
335
407
  active = screen.id;
336
408
  for (const [screenId, slot] of slots) slot.hidden = screenId !== screen.id;
337
409
  for (const [screenId, link] of links) {
338
410
  if (screenId === screen.id) link.setAttribute('aria-current', 'page');
339
411
  else link.removeAttribute('aria-current');
340
412
  }
341
- ensureMounted(screen);
413
+ ensureMounted(screen, pendingEntityIds.get(screen.id));
342
414
  updateRail();
343
415
  return controller;
344
416
  },
@@ -362,7 +434,7 @@ export function createLocalWebShell({
362
434
  renderSource(doc, source, status, controller.refreshStatus);
363
435
  // Re-evaluate Graph-backed screens that are waiting behind the gate.
364
436
  for (const screen of screens) {
365
- if (screen.usesGraph && !mounted.has(screen.id) && (screen.id === active)) ensureMounted(screen);
437
+ if (screen.usesGraph && !mounted.has(screen.id) && (screen.id === active)) ensureMounted(screen, pendingEntityIds.get(screen.id));
366
438
  }
367
439
  return status;
368
440
  },
@@ -667,7 +667,7 @@ function renderDenseOverview(doc, detail, context, entities, actions = {}) {
667
667
  todayHead.append(makeElement(doc, 'h4', { text: '今日判断・対応すること' }));
668
668
  if (typeof actions.openWorkList === 'function') todayHead.append(denseButton(doc, 'すべて見る', 'bb-pkw-dense-action', actions.openWorkList));
669
669
  today.append(todayHead);
670
- const todayNotice = denseContextStateNotice(doc, taskSection ?? (context?.state !== 'ok' ? context : null), 'タスクを読み取れませんでした。');
670
+ const todayNotice = denseContextStateNotice(doc, taskSection ?? (context?.state && context.state !== 'ok' ? context : { state: 'unknown' }), 'タスクを読み取れませんでした。');
671
671
  if (todayNotice) today.append(todayNotice);
672
672
  if (!todayNotice && due.length === 0) today.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: '表示できる仕事はありません。' }));
673
673
  if (!todayNotice && due.length > 0) {
@@ -93,6 +93,28 @@ body:has(#brainbase-value-proof-review) {
93
93
  font-weight: 700;
94
94
  }
95
95
 
96
+ .bb-vpr-setup-guide {
97
+ display: grid;
98
+ gap: var(--bb-space-1);
99
+ padding: var(--bb-space-2) var(--bb-space-3);
100
+ border: 1px solid var(--bb-color-border, #d8dee9);
101
+ background: var(--bb-color-panel, #ffffff);
102
+ }
103
+
104
+ .bb-vpr-setup-guide p,
105
+ .bb-vpr-setup-guide ol {
106
+ margin: 0;
107
+ }
108
+
109
+ .bb-vpr-setup-guide ol {
110
+ padding-left: var(--bb-space-5);
111
+ }
112
+
113
+ .bb-vpr-setup-guide code {
114
+ overflow-wrap: anywhere;
115
+ font-size: .92em;
116
+ }
117
+
96
118
  .bb-vpr-rejected summary {
97
119
  cursor: pointer;
98
120
  }
@@ -225,6 +247,38 @@ body:has(#brainbase-value-proof-review) {
225
247
  font-size: 13px;
226
248
  }
227
249
 
250
+ /* A source-bearing target is still trace metadata until the host has read the
251
+ * exact Graph record. Only the same-origin link is styled as an action. */
252
+ .bb-vpr-source-target {
253
+ display: inline-flex;
254
+ flex-wrap: wrap;
255
+ gap: var(--bb-space-1, 4px);
256
+ align-items: baseline;
257
+ }
258
+
259
+ .bb-vpr-source-link {
260
+ color: var(--vpr-action);
261
+ font-weight: 680;
262
+ text-decoration: underline;
263
+ text-underline-offset: 2px;
264
+ }
265
+
266
+ .bb-vpr-source-status {
267
+ color: var(--vpr-muted);
268
+ font-size: 12px;
269
+ }
270
+
271
+ .bb-vpr-source-status.is-loading {
272
+ color: var(--vpr-muted);
273
+ }
274
+
275
+ .bb-vpr-source-status.is-not_found,
276
+ .bb-vpr-source-status.is-ambiguous,
277
+ .bb-vpr-source-status.is-forbidden,
278
+ .bb-vpr-source-status.is-unavailable {
279
+ color: var(--vpr-warning);
280
+ }
281
+
228
282
  /* Detail: in the rail, or after the workspace when the host has none. */
229
283
  .bb-vpr-inline-detail {
230
284
  max-width: 760px;