@unson/brainbase-mcp 0.41.7 → 0.41.9

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.
@@ -34,6 +34,8 @@ const nodeList = (nodes) => (Array.isArray(nodes) ? nodes : [nodes]).filter((nod
34
34
  * focus unless its optional second argument is { focus: true }.
35
35
  * addNote shows a quest only for an explicit nonempty summary; its full text
36
36
  * remains in help. No summary is inferred from a long or nonurgent notice.
37
+ * A quest with a `link` or an `action` is pressable as a whole row; one without
38
+ * is drawn flat and may say its `reason` (why nothing opens from it).
37
39
  */
38
40
  export function createWorldCanvasUI({
39
41
  doc,
@@ -385,15 +387,17 @@ export function createWorldCanvasUI({
385
387
  activate('help', help);
386
388
  focus(helpTitle);
387
389
  }
388
- function addNote({ label = '', text = '', tone = '', summary, link = null } = {}) {
390
+ function addNote({ label = '', text = '', tone = '', summary, link = null, action = null, reason = null } = {}) {
389
391
  if (disposed) return;
390
392
  const note = element('section', 'bb-world-canvas-note');
391
393
  if (label) note.append(element('h3', '', label));
392
394
  if (text || summary) note.append(element('p', '', text || summary));
393
395
  if (label || text || summary) notes.append(note);
394
396
  if (typeof summary !== 'string' || !summary.trim()) return;
395
- // A link (already checked by the caller) follows the summary so the quest is a way down, not only a sign.
396
- const quest = element('p', 'bb-world-canvas-quest');
397
+ // A quest is a way down (a link already checked by the caller, or an action in this page) and then the
398
+ // whole row is pressable; without one it is drawn flat, so it never looks pressable (story-world-clickable-honesty-v1).
399
+ const way = link?.href ? 'link' : typeof action?.onClick === 'function' ? 'action' : null;
400
+ const quest = element('p', `bb-world-canvas-quest ${way ? 'has-way' : 'is-static'}`);
397
401
  const icon = tone === 'attention' || tone === 'warning' ? '!' : 'i';
398
402
  quest.append(
399
403
  element('span', 'bb-world-canvas-quest-symbol', icon, { 'aria-hidden': 'true' }),
@@ -402,7 +406,16 @@ export function createWorldCanvasUI({
402
406
  // The accessible name contains the unabridged summary even when visual text
403
407
  // is ellipsized on a narrow canvas; the long explanation remains in help.
404
408
  quest.setAttribute('aria-label', summary.trim());
405
- if (link?.href) quest.append(element('a', link.className ?? '', link.label ?? summary.trim(), { href: link.href }));
409
+ if (way === 'link') {
410
+ quest.append(element('a', link.className ?? '', link.label ?? summary.trim(), { href: link.href }));
411
+ } else if (way === 'action') {
412
+ const press = element('button', `bb-world-canvas-quest-action ${action.className ?? ''}`.trim(), action.label ?? summary.trim(), { type: 'button' });
413
+ listen(press, 'click', () => action.onClick());
414
+ quest.append(press);
415
+ } else if (typeof reason === 'string' && reason.trim()) {
416
+ // Why nothing opens from here (未接続 and the like), said in the row instead of only in help.
417
+ quest.append(element('span', 'bb-world-canvas-quest-reason', reason.trim()));
418
+ }
406
419
  quests.append(quest);
407
420
  }
408
421
  function clearNotes() {
@@ -98,8 +98,11 @@ const STATION_YARD = Object.freeze({ xFrom: 6, xTo: 21.5, zFrom: 1.5, zTo: 12.5
98
98
  * `exitLegend` (lines as DISTRICT_LEGEND) turns on the station yard for the tools of a business:
99
99
  * stations come from `setExits`, and a picked station calls `onPickExit(id)`. Without it nothing of
100
100
  * the yard is drawn or listed.
101
+ * `onPickStreet({ label, count })` makes each street's sign pressable (label null for the unlabelled work by
102
+ * the gate), and `onPickHall()` the hall, the plaza in front of it and the hall's name; without them they
103
+ * are scenery, as before (story-world-clickable-honesty-v1).
101
104
  */
102
- export function createDistrictView({ doc, stage, reducedMotion = false, focusSelection = true, showChrome = true, onPick, onClear, onEscape, onProjectSelection, exitLegend = null, onPickExit }) {
105
+ export function createDistrictView({ doc, stage, reducedMotion = false, focusSelection = true, showChrome = true, onPick, onClear, onEscape, onProjectSelection, exitLegend = null, onPickExit, onPickStreet, onPickHall }) {
103
106
  const stationsEnabled = Array.isArray(exitLegend);
104
107
  const renderer = new THREE.WebGLRenderer({ antialias: true });
105
108
  renderer.setPixelRatio(Math.min(globalThis.devicePixelRatio ?? 1, 2));
@@ -349,6 +352,8 @@ export function createDistrictView({ doc, stage, reducedMotion = false, focusSel
349
352
  // The tools of the business (from the host), and their stations once the district is built.
350
353
  let exitsData = [];
351
354
  let stations = new Map();
355
+ /** The hall and its plaza, pressable when the host listens (`onPickHall`). */
356
+ let civic = [];
352
357
  let stationLayer = null;
353
358
  let stationLabels = [];
354
359
  let gateAt = 0;
@@ -380,6 +385,17 @@ export function createDistrictView({ doc, stage, reducedMotion = false, focusSel
380
385
  return label;
381
386
  }
382
387
 
388
+ /** A label that is a way in: pointer, a title saying where it goes, and a press. */
389
+ function enterable(label, title, onPress, enter) {
390
+ label.node.classList.add('is-enterable');
391
+ label.node.title = title;
392
+ label.node.setAttribute('data-enter', enter);
393
+ label.node.addEventListener('click', (event) => {
394
+ event.stopPropagation?.();
395
+ onPress();
396
+ });
397
+ }
398
+
383
399
  // --- people (records: worker, person named in the text; atmosphere: walkers) ---------------------
384
400
  function person(clothes, { helmet = false } = {}) {
385
401
  const group = new THREE.Group();
@@ -613,6 +629,7 @@ export function createDistrictView({ doc, stage, reducedMotion = false, focusSel
613
629
  labels = [];
614
630
  lots = new Map();
615
631
  stations = new Map();
632
+ civic = [];
616
633
  stationLayer = null;
617
634
  stationLabels = [];
618
635
  walkers = [];
@@ -705,7 +722,9 @@ export function createDistrictView({ doc, stage, reducedMotion = false, focusSel
705
722
  root.add(mesh(new THREE.CylinderGeometry(0.06, 0.08, 2.5, 8), material(0x3f4844, { roughness: 0.6, metalness: 0.4 }), { x: -DISTRICT_STREETS.main - 0.5, y: 1.25, z: signZ }));
706
723
  root.add(mesh(new THREE.BoxGeometry(1.1, 0.34, 0.05), material(block.label ? 0x2f6e4f : 0xbfb8a8, { roughness: 0.6 }), { x: -DISTRICT_STREETS.main - 0.5, y: 2.2, z: signZ }));
707
724
  const text = block.label ? `${block.label}(${block.count}件)` : `未分類の空き地:何のための仕事か未記録(${block.count}件)`;
708
- addLabel(text, new THREE.Vector3(-DISTRICT_STREETS.main - 0.5, 2.9, signZ), `is-street${block.label ? '' : ' is-unlabelled'}`, { priority: 2 });
725
+ const sign = addLabel(text, new THREE.Vector3(-DISTRICT_STREETS.main - 0.5, 2.9, signZ), `is-street${block.label ? '' : ' is-unlabelled'}`, { priority: 2 });
726
+ // The count is a way down: pressing the sign lists the work of this street (AC-05).
727
+ if (onPickStreet) enterable(sign, `${block.label ?? '何のための仕事か未記録の空き地'}の仕事${block.count}件を見る`, () => onPickStreet({ label: block.label, count: block.count }), '仕事を見る');
709
728
  });
710
729
  // Trees along the main street and lamps between them; a belt of trees round the district (scenery).
711
730
  const trees = [];
@@ -754,10 +773,17 @@ export function createDistrictView({ doc, stage, reducedMotion = false, focusSel
754
773
  root.add(box(0.4, 0.9, 0.4, 0x9c7a62, { x: 2.8, y: 4.3, z: hallZ - 1.2 }));
755
774
  const hallChimney = new THREE.Vector3(2.8, 5.2, hallZ - 1.2);
756
775
  const purposeText = work?.status === 'ok' ? (work.purpose.text ?? '目的は未登録') : '目的を読めません';
757
- addLabel(`庁舎:${business.name}(${purposeText})`, new THREE.Vector3(0, 5.6, hallZ), 'is-plaza', { priority: 1 });
776
+ const hallLabel = addLabel(`庁舎:${business.name}(${purposeText})`, new THREE.Vector3(0, 5.6, hallZ), 'is-plaza', { priority: 1 });
758
777
  const plaza = mesh(new THREE.CircleGeometry(plazaRadius - 0.25, 48), material(0xf1ece1, { roughness: 1 }), { y: 0.24, z: plazaZ, shadow: false });
759
778
  plaza.rotation.x = -Math.PI / 2;
760
779
  root.add(plaza);
780
+ if (onPickHall) {
781
+ // The hall, its plaza and its name open what this business is for and its policies (AC-06).
782
+ hall.userData = { kind: 'hall' };
783
+ plaza.userData = { kind: 'hall' };
784
+ civic = [hall, plaza];
785
+ enterable(hallLabel, `${business.name}の目的と方針を見る`, () => onPickHall(), '目的と方針を見る');
786
+ }
761
787
  const ring = mesh(new THREE.RingGeometry(plazaRadius - 0.25, plazaRadius, 48), material(0xcfc8b8, { roughness: 1 }), { y: 0.25, z: plazaZ, shadow: false });
762
788
  ring.rotation.x = -Math.PI / 2;
763
789
  root.add(ring);
@@ -897,7 +923,7 @@ export function createDistrictView({ doc, stage, reducedMotion = false, focusSel
897
923
  pointer.x = ((clientX - rect.left) / rect.width) * 2 - 1;
898
924
  pointer.y = -((clientY - rect.top) / rect.height) * 2 + 1;
899
925
  raycaster.setFromCamera(pointer, camera);
900
- const targets = [...lots.values(), ...stations.values()];
926
+ const targets = [...lots.values(), ...stations.values(), ...civic];
901
927
  for (const hit of raycaster.intersectObjects(targets, true)) {
902
928
  let object = hit.object;
903
929
  while (object && !targets.includes(object)) object = object.parent;
@@ -915,7 +941,10 @@ export function createDistrictView({ doc, stage, reducedMotion = false, focusSel
915
941
  renderer.domElement.addEventListener('pointerup', (event) => {
916
942
  if (!downAt || Math.hypot(event.clientX - downAt.x, event.clientY - downAt.y) > 5) return;
917
943
  const group = pickAt(event.clientX, event.clientY);
918
- if (group?.userData.kind === 'exit') {
944
+ if (group?.userData.kind === 'hall') {
945
+ select(null);
946
+ onPickHall?.();
947
+ } else if (group?.userData.kind === 'exit') {
919
948
  // A station selects its tool in the city details; it never leaves the world.
920
949
  select(group, { focus: false });
921
950
  onPickExit?.(group.userData.exit.id);
@@ -159,6 +159,12 @@
159
159
  color: #ffffff;
160
160
  }
161
161
 
162
+ /* Other names that are ways in (the plaza, a street's sign, the hall) say where they go (story-world-clickable-honesty-v1). */
163
+ .bb-world-label.is-enterable[data-enter]:hover::after,
164
+ .bb-world-label.is-enterable[data-enter].is-hover::after {
165
+ content: ' ' attr(data-enter) ' ›';
166
+ }
167
+
162
168
  /* With nothing chosen, the rail lists the ways in (AC-19). */
163
169
  .bb-world-entrances {
164
170
  list-style: none;
@@ -672,7 +672,7 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear,
672
672
  const highlightedMaterials = new Set();
673
673
 
674
674
  // --- plaza ---------------------------------------------------------------
675
- function buildPlaza(rows, rowItems, waitingOf, now) {
675
+ function buildPlaza(rows, rowItems, waitingOf, now, { state = null } = {}) {
676
676
  const plaza = new THREE.Group();
677
677
  plaza.add(mesh(new THREE.CylinderGeometry(PLAZA_RADIUS + 2.2, PLAZA_RADIUS + 2.6, 0.3, 64), standard(0x9fbf9d, { roughness: 1 }), { y: 0.15 }));
678
678
  plaza.add(mesh(new THREE.CylinderGeometry(PLAZA_RADIUS, PLAZA_RADIUS + 0.4, 0.7, 64), standard(0xf3efe6, { roughness: 0.9 }), { y: 0.35 }));
@@ -694,7 +694,16 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear,
694
694
  }
695
695
  scene.add(plaza);
696
696
  register(plaza, { kind: 'plaza' });
697
- addLabel('広場(判断)', new THREE.Vector3(0, 0.8, PLAZA_RADIUS + 2.8), 'is-plaza', { priority: LABEL_PRIORITY.plaza });
697
+ // The name says when the judgments are not there to stand (未接続), so an empty plaza is not read as zero.
698
+ const plazaLabel = addLabel(state ? `広場(判断:${state})` : '広場(判断)', new THREE.Vector3(0, 0.8, PLAZA_RADIUS + 2.8), 'is-plaza', { priority: LABEL_PRIORITY.plaza, owner: plaza });
699
+ // The name is a way in too, as a city's name is: pressing it opens what the plaza is.
700
+ plazaLabel.node.classList.add('is-enterable');
701
+ plazaLabel.node.title = '広場が何かを見る';
702
+ plazaLabel.node.setAttribute('data-enter', '広場を見る');
703
+ plazaLabel.node.addEventListener('click', () => {
704
+ select(plaza);
705
+ onPick(plaza.userData, new THREE.Vector3(0, 0, 0));
706
+ });
698
707
  for (let i = 0; i < 10; i += 1) {
699
708
  const angle = (i / 10) * Math.PI * 2 + 0.3;
700
709
  tree(Math.cos(angle) * (PLAZA_RADIUS + 1.3), Math.sin(angle) * (PLAZA_RADIUS + 1.3), 0.7);
@@ -1413,6 +1422,10 @@ function renderFallbackList(doc, root, businesses, rows, exitSlot = null) {
1413
1422
  const JUDGMENT_UNAVAILABLE_TEXT = Object.freeze({
1414
1423
  judgment_journal_not_connected: '判断の記録は各メンバーのMacにあり、組織版には送らないため未接続です',
1415
1424
  });
1425
+ /** The same, short enough to stand in the row of the waiting list (story-world-clickable-honesty-v1 AC-03). */
1426
+ const JUDGMENT_UNAVAILABLE_SHORT = Object.freeze({
1427
+ judgment_journal_not_connected: '各メンバーのMacにあり、組織版には送っていません',
1428
+ });
1416
1429
 
1417
1430
  /**
1418
1431
  * Where the selected city and work site are kept: the page address (`world_business`, `world_site`),
@@ -1602,9 +1615,25 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
1602
1615
  };
1603
1616
  function updateDistrictNotes(work) {
1604
1617
  for (const key of canvasNotes.keys()) if (key.startsWith('district:')) canvasNotes.delete(key);
1605
- if (work) for (const [index, note] of workCanvasNotes(work).entries()) canvasNotes.set(`district:${index}`, note);
1618
+ if (work) {
1619
+ for (const [index, { opens, ...note }] of workCanvasNotes(work).entries()) {
1620
+ canvasNotes.set(`district:${index}`, opens === 'city' ? { ...note, action: { label: '詳細を開く', onClick: openCurrentCityRail } } : note);
1621
+ }
1622
+ }
1606
1623
  renderCanvasNotes();
1607
1624
  }
1625
+ /** The details of the city the viewer is in, in the opened rail (a way down from its quests and its hall). */
1626
+ function openCurrentCityRail() {
1627
+ const business = current.code ? businessOf(current.code) : null;
1628
+ if (!business) return;
1629
+ // The whole city, not a street or a gap chosen before.
1630
+ if (current.focus) {
1631
+ current.focus = null;
1632
+ district?.focus(null);
1633
+ }
1634
+ renderCityRail(business, { revealFocus: true });
1635
+ canvasUI?.openRail();
1636
+ }
1608
1637
  root.append(wrap);
1609
1638
  let scene = null;
1610
1639
 
@@ -1742,6 +1771,13 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
1742
1771
  // The businesses are not known yet here (the list is drawn again once the world is built).
1743
1772
  showRail([workspaceDetailEmpty(doc, { mark: '◇', title: '都市や建物を選ぶと、ここに出ます', text: 'ドラッグで移動、ホイールで寄る・離れる、右ドラッグで回転します。Escで全体に戻ります。' })]);
1744
1773
 
1774
+ /** The entrances of the districts, the ones with the most work to check first, in the opened rail. */
1775
+ function showEntrances() {
1776
+ if (current.level !== 'world') goToWorld();
1777
+ clearAll();
1778
+ canvasUI?.openRail();
1779
+ }
1780
+
1745
1781
  function enterFromRail(business) {
1746
1782
  const picked = scene?.selectKey({ code: business.code });
1747
1783
  openCity(business, picked?.center ?? null);
@@ -1753,6 +1789,8 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
1753
1789
  let withheld = 0;
1754
1790
  /** Whether the host gave judgments at all (an organization web does not: they stay on each Mac). */
1755
1791
  let judgmentsConnected = false;
1792
+ /** Why the judgments could not be read, for the plaza: `{ title, text, short }`, or null when read. */
1793
+ let judgmentsUnavailable = null;
1756
1794
 
1757
1795
  function judgmentText(item, fallback) {
1758
1796
  const textValue = item?.proof?.interruption?.question_display_text ?? item?.proof?.decision?.summary ?? fallback;
@@ -1760,6 +1798,11 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
1760
1798
  return `${textValue}${suffix}`;
1761
1799
  }
1762
1800
 
1801
+ /** 判断の履歴 itself, by the host's own route (`#value-proofs` here, `?screen=value-proofs` in an organization web). */
1802
+ function historyLink() {
1803
+ return el(doc, 'a', { className: 'bb-world-rail-link', text: '「判断の履歴」で開く', attrs: { href: judgmentHref(null) } });
1804
+ }
1805
+
1763
1806
  /** Descends to the same judgment in the host's 判断の履歴. */
1764
1807
  function decisionLink(decisionAttemptId) {
1765
1808
  const link = el(doc, 'a', { className: 'bb-world-rail-link', text: '「判断の履歴」でこの判断を開く', attrs: { href: judgmentHref(decisionAttemptId) } });
@@ -1930,7 +1973,17 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
1930
1973
  /** Brings a rail block the viewer just asked for into view (the rail may be scrolled down, or below the city). */
1931
1974
  function reveal(node) {
1932
1975
  try {
1933
- node?.scrollIntoView?.({ block: 'start', behavior: reducedMotion ? 'auto' : 'smooth' });
1976
+ // Only the rail's own scroller moves: scrollIntoView also scrolled the stage (overflow: hidden) and
1977
+ // shifted the whole map up. Without such a scroller (a host rail in the page) the page scrolls.
1978
+ const style = (box) => globalThis.getComputedStyle?.(box)?.overflowY ?? '';
1979
+ let box = node?.parentElement ?? null;
1980
+ while (box && !/^(auto|scroll)$/u.test(style(box))) box = box.parentElement;
1981
+ if (!box) {
1982
+ node?.scrollIntoView?.({ block: 'start', behavior: reducedMotion ? 'auto' : 'smooth' });
1983
+ return;
1984
+ }
1985
+ const top = node.getBoundingClientRect().top - box.getBoundingClientRect().top + box.scrollTop;
1986
+ box.scrollTo({ top, behavior: reducedMotion ? 'auto' : 'smooth' });
1934
1987
  } catch {
1935
1988
  // Without scrolling the block is still in the rail.
1936
1989
  }
@@ -1941,13 +1994,13 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
1941
1994
  let focusBlock = null;
1942
1995
  const work = workCache.get(business.code);
1943
1996
  if (current.focus && work?.status === 'ok') {
1944
- const ids = new Set(current.focus.kind
1945
- ? work.summary.gaps?.[current.focus.kind] ?? []
1946
- : work.sites.filter((site) => site.work.open && site.work.status === current.focus.status).map((site) => site.task_id));
1997
+ const ids = new Set(focusIdsFor(work, current.focus));
1947
1998
  const sites = work.sites.filter((site) => ids.has(site.task_id));
1948
- const label = current.focus.kind
1949
- ? sites[0]?.gaps.find((gap) => gap.kind === current.focus.kind)?.label ?? current.focus.kind
1950
- : `記録上の状態「${WORK_STATES.find((state) => state.key === current.focus.status)?.label ?? current.focus.status}」`;
1999
+ const label = 'purpose' in current.focus
2000
+ ? current.focus.purpose ? `通り「${current.focus.purpose}」の仕事` : '何のための仕事か未記録の仕事'
2001
+ : current.focus.kind
2002
+ ? sites[0]?.gaps.find((gap) => gap.kind === current.focus.kind)?.label ?? current.focus.kind
2003
+ : `記録上の状態「${WORK_STATES.find((state) => state.key === current.focus.status)?.label ?? current.focus.status}」`;
1951
2004
  blocks.push(focusBlock = workPickBlock(doc, {
1952
2005
  title: `${label} ${sites.length}件`,
1953
2006
  sites,
@@ -1982,11 +2035,18 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
1982
2035
  /** The open tasks a focus picks: a kind of gap, or a recorded state; null for all. */
1983
2036
  function focusIdsFor(work, focus) {
1984
2037
  if (!focus || work?.status !== 'ok') return null;
2038
+ // A street's sign (story-world-clickable-honesty-v1 AC-05): the work the sign counts, done or not.
2039
+ if ('purpose' in focus) return work.sites.filter((site) => site.work.status !== 'cancelled' && purposeOf(site) === focus.purpose).map((site) => site.task_id);
1985
2040
  return focus.kind
1986
2041
  ? work.summary.gaps?.[focus.kind] ?? []
1987
2042
  : work.sites.filter((site) => site.work.open && site.work.status === focus.status).map((site) => site.task_id);
1988
2043
  }
1989
2044
 
2045
+ /** What a work is for, as the district groups it into streets (null for the unlabelled by the gate). */
2046
+ function purposeOf(site) {
2047
+ return typeof site.purpose_label === 'string' && site.purpose_label.trim() ? site.purpose_label.trim() : null;
2048
+ }
2049
+
1990
2050
  function showFocus(business, focus) {
1991
2051
  current.focus = focus;
1992
2052
  district?.focus(focusIdsFor(workCache.get(business.code), focus));
@@ -2088,6 +2148,18 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
2088
2148
  if (anchor.taskId === current.siteId && current.detachedSiteId !== current.siteId) canvasUI?.setTaskAnchor(anchor);
2089
2149
  else if (!anchor.visible && !current.siteId) canvasUI?.setTaskAnchor({ visible: false });
2090
2150
  },
2151
+ // A street's sign lists its work; the hall and its plaza open what the business is for.
2152
+ onPickStreet: ({ label }) => {
2153
+ const owner = businessOf(current.code);
2154
+ if (!owner) return;
2155
+ if (current.level === 'site') goUp();
2156
+ showFocus(owner, { purpose: label });
2157
+ canvasUI?.openRail();
2158
+ },
2159
+ onPickHall: () => {
2160
+ if (current.level === 'site') goUp();
2161
+ openCurrentCityRail();
2162
+ },
2091
2163
  ...(exitsEnabled ? {
2092
2164
  exitLegend: exitLegend(worldVocabulary),
2093
2165
  onPickExit: (exitId) => {
@@ -2325,7 +2397,7 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
2325
2397
  if (!openKnown) note('仕事の件数', '読めた仕事の要約に未完了件数の記録がありません。0件とは確認できません。', 'warning', '未完了の件数:未確認');
2326
2398
  if (unreadable) note('仕事の読込', `仕事を読めなかった事業${unreadable}件。0件とは確認できません。`, 'warning', `仕事が読めない事業 ${unreadable}件`);
2327
2399
  if (partial) note('仕事の範囲', `一部だけ読めた事業${partial}件。件数は読めた範囲です。`, 'warning', `仕事が一部だけ読めた事業 ${partial}件`);
2328
- if (needs) note('仕事の記録', `読めた未完了の仕事${needs}件(${cities}事業)に、記録から把握できていないことがあります。仕事の失敗や停止を意味しません。`, 'attention', `記録の要確認 ${needs}件・${cities}事業`);
2400
+ if (needs) note('仕事の記録', `読めた未完了の仕事${needs}件(${cities}事業)に、記録から把握できていないことがあります。仕事の失敗や停止を意味しません。`, 'attention', `記録の要確認 ${needs}件・${cities}事業`, null, { action: { label: '区画を選ぶ', onClick: showEntrances } });
2329
2401
  }
2330
2402
  }
2331
2403
 
@@ -2359,6 +2431,7 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
2359
2431
  workspaceRailBlock(doc, { title: '', content: workspaceButton(doc, { text: `${business.name}の区画に入る`, variant: 'primary', onClick: () => openCity(business, position) }) }),
2360
2432
  cityJudgmentBlock(business),
2361
2433
  ]);
2434
+ canvasUI?.openRail();
2362
2435
  return;
2363
2436
  }
2364
2437
  if (data.kind === 'cityJudgments') {
@@ -2368,8 +2441,9 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
2368
2441
  showRail([
2369
2442
  workspaceRailHead(doc, { kicker: `${business.name} の判断所`, title: `判断 ${data.judgments.length}件`, lead: business.purpose ?? undefined }),
2370
2443
  cityJudgmentBlock(business),
2371
- workspaceRailBlock(doc, { title: '詳しく見る', content: workspaceButton(doc, { text: '「判断の履歴」で開く', variant: 'primary', onClick: () => { globalThis.location.hash = judgmentHref(null); } }) }),
2444
+ workspaceRailBlock(doc, { title: '詳しく見る', content: historyLink() }),
2372
2445
  ]);
2446
+ canvasUI?.openRail();
2373
2447
  return;
2374
2448
  }
2375
2449
  if (data.kind === 'judgment') {
@@ -2399,9 +2473,10 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
2399
2473
  blocks.push(workspaceRailBlock(doc, { title: '判断(新しい順)', content: items.length ? ul : { text: '判断はありません' } }));
2400
2474
  blocks.push(workspaceRailBlock(doc, {
2401
2475
  title: '詳しく見る',
2402
- content: workspaceButton(doc, { text: '「判断の履歴」で開く', variant: 'primary', onClick: () => { globalThis.location.hash = judgmentHref(null); } }),
2476
+ content: historyLink(),
2403
2477
  }));
2404
2478
  showRail(blocks);
2479
+ canvasUI?.openRail();
2405
2480
  return;
2406
2481
  }
2407
2482
  if (data.kind === 'plaza') {
@@ -2410,8 +2485,18 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
2410
2485
  const blocks = [workspaceRailHead(doc, {
2411
2486
  kicker: '広場',
2412
2487
  title: '判断の種類(全体)',
2413
- lead: '建物1つが判断の種類1つで、全事業の判断をまとめています。高さは件数、色は任せ方の状態です。事業ごとの判断は、各都市の判断所にあります。',
2488
+ lead: '広場には、判断の種類1つにつき建物が1つ建ちます。全事業の判断をまとめたもので、高さは件数、色は任せ方の状態です。事業ごとの判断は、各都市の判断所にあります。',
2414
2489
  })];
2490
+ if (judgmentsUnavailable) {
2491
+ // Not connected or unreadable: an empty plaza here is not zero judgments (P9).
2492
+ blocks.push(workspaceRailBlock(doc, {
2493
+ title: judgmentsUnavailable.title,
2494
+ content: { text: `${judgmentsUnavailable.text}広場に建物が無いのは、判断が0件だからではありません。` },
2495
+ }));
2496
+ showRail(blocks);
2497
+ canvasUI?.openRail();
2498
+ return;
2499
+ }
2415
2500
  blocks.push(workspaceRailBlock(doc, {
2416
2501
  title: `どの事業にも置けない判断(${placement.unplaced.length}件)`,
2417
2502
  content: placement.unplaced.length
@@ -2424,7 +2509,9 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
2424
2509
  content: { text: 'この組織の事業で作業したと確かめられない判断です。ほかの会社の仕事が混ざるため、内容も作業場所も出しません。「判断の履歴」では見られます。' },
2425
2510
  }));
2426
2511
  }
2512
+ blocks.push(workspaceRailBlock(doc, { title: '詳しく見る', content: historyLink() }));
2427
2513
  showRail(blocks);
2514
+ canvasUI?.openRail();
2428
2515
  }
2429
2516
  }
2430
2517
 
@@ -2453,9 +2540,9 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
2453
2540
  }
2454
2541
  if (disposed) return;
2455
2542
  notices.replaceChildren();
2456
- const note = (label, textValue, tone = 'info', summary = null, link = null) => {
2543
+ const note = (label, textValue, tone = 'info', summary = null, link = null, { action = null, reason = null } = {}) => {
2457
2544
  if (canvasUI) {
2458
- setCanvasNote(`${label}:${summary ?? 'detail'}`, { label, text: textValue, tone, summary, link });
2545
+ setCanvasNote(`${label}:${summary ?? 'detail'}`, { label, text: textValue, tone, summary, link, action, reason });
2459
2546
  return;
2460
2547
  }
2461
2548
  const line = el(doc, 'p', { className: `bb-world-hud-line is-${tone}`, attrs: tone === 'info' ? {} : { role: 'alert' } });
@@ -2489,8 +2576,11 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
2489
2576
  const rows = readable ? home.delegation_map.rows : [];
2490
2577
  if (!homeResult.ok || !readable) {
2491
2578
  const reason = homeResult.ok ? home?.reason ?? home?.status ?? '状態不明' : homeResult.error;
2492
- if (JUDGMENT_UNAVAILABLE_TEXT[reason]) note('判断', `${JUDGMENT_UNAVAILABLE_TEXT[reason]}。事業だけを描いています。`, 'warning', '判断の記録:未接続');
2493
- else note('判断', `判断の記録を読めません(${reason})。判断の記録の件数は未確認です。0件とは確認できません。`, 'warning', '判断の記録:読めない');
2579
+ judgmentsUnavailable = JUDGMENT_UNAVAILABLE_TEXT[reason]
2580
+ ? { title: '判断の記録:未接続', text: `${JUDGMENT_UNAVAILABLE_TEXT[reason]}。`, short: JUDGMENT_UNAVAILABLE_SHORT[reason] }
2581
+ : { title: '判断の記録:読めない', text: `判断の記録を読めません(${reason})。判断の記録の件数は未確認です。0件とは確認できません。`, short: `理由:${reason}` };
2582
+ if (JUDGMENT_UNAVAILABLE_TEXT[reason]) note('判断', `${JUDGMENT_UNAVAILABLE_TEXT[reason]}。事業だけを描いています。`, 'warning', '判断の記録:未接続', null, { reason: judgmentsUnavailable.short });
2583
+ else note('判断', judgmentsUnavailable.text, 'warning', '判断の記録:読めない', null, { reason: judgmentsUnavailable.short });
2494
2584
  } else {
2495
2585
  proofs = proofIndex(home);
2496
2586
  judgmentsConnected = true;
@@ -2539,7 +2629,7 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
2539
2629
  }
2540
2630
  scene = createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear: clearAll, onEscape: goUp });
2541
2631
  scene.setSky(SKY_MODES[skyIndex][0]);
2542
- scene.buildPlaza(rows, rowItems, (row) => (rowItems.get(row.key) ?? []).filter((ref) => isWaiting(proofs.get(ref.decision_attempt_id))).length, Date.now());
2632
+ scene.buildPlaza(rows, rowItems, (row) => (rowItems.get(row.key) ?? []).filter((ref) => isWaiting(proofs.get(ref.decision_attempt_id))).length, Date.now(), { state: judgmentsUnavailable ? judgmentsUnavailable.title.replace('判断の記録:', '') : null });
2543
2633
  scene.buildCities(layoutWorld(businesses), placement.byBusiness);
2544
2634
  showChangesSinceLastVisit(note, businesses, rows);
2545
2635
  knownBusinesses = businesses;
@@ -108,10 +108,20 @@ function linkLine(doc, link, label, extra) {
108
108
  return li;
109
109
  }
110
110
 
111
+ /** The record fields a gap is read from, in the words of the task screen. */
112
+ const SOURCE_FIELD_TEXT = Object.freeze({
113
+ assignee_person_id: '担当欄',
114
+ description: '本文',
115
+ source_refs: '出典リンク',
116
+ review_at: '見直し予定',
117
+ updated_at: '最終更新',
118
+ relation: '関係',
119
+ });
120
+
111
121
  function sourceText(source, readAt) {
112
122
  if (!source) return null;
113
123
  const system = source.system === 'task_api' ? 'タスクの記録' : '組織のGraph';
114
- const fields = source.fields?.length && !source.fields.includes('*') ? `の ${source.fields.join('・')}` : '';
124
+ const fields = source.fields?.length && !source.fields.includes('*') ? `の ${source.fields.map((field) => SOURCE_FIELD_TEXT[field] ?? '記録の欄').join('・')}` : '';
115
125
  const at = readAt ? `(${shortTime(readAt)}に読み込み)` : '';
116
126
  return `${system}${fields}${at}`;
117
127
  }
@@ -332,7 +342,10 @@ export function workSiteBlocks(doc, { business, site, readAt, taskHref }) {
332
342
  ? [el(doc, 'a', { className: 'bb-world-rail-link', text: '「タスク」でこの仕事を開く', attrs: { href } }), { text: '担当・期限・状態はタスク画面で直せます。保存先はタスクの正本で、世界にはもう一度読むと反映されます。本文の人物を担当欄へ接続するのも、そこで行います。', className: 'bb-world-rail-note' }]
333
343
  : { text: 'この画面からは直せません。タスクを管理している画面で直してください。', className: 'bb-world-rail-note' },
334
344
  }));
335
- blocks.push(workspaceRailBlock(doc, { title: '出典', content: workspaceDefinition(doc, [['記録', `${site.task_id}(版 ${site.source.version ?? '不明'})`], ['事業', business.name]]) }));
345
+ // The task's id and version are for tracing, so they wait behind the record details.
346
+ const record = el(doc, 'details', { className: 'bb-world-original' });
347
+ record.append(el(doc, 'summary', { text: '記録の詳細' }), workspaceDefinition(doc, [['タスクID', site.task_id], ['版', site.source.version ?? '不明']]));
348
+ blocks.push(workspaceRailBlock(doc, { title: '出典', content: [workspaceDefinition(doc, [['記録', 'タスクの記録'], ['事業', business.name]]), record] }));
336
349
  return blocks;
337
350
  }
338
351