@unson/brainbase-mcp 0.40.0 → 0.41.1

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.
@@ -655,6 +655,145 @@ function todayActionItems(items, now = new Date()) {
655
655
  }));
656
656
  }
657
657
 
658
+ const DENSE_PARTICIPANT_STATUS_VALUES = new Set(['available', 'empty', 'partial', 'unavailable', 'unknown', 'stale', 'failed']);
659
+ const DENSE_PARTICIPANT_FRESHNESS_VALUES = new Set(['fresh', 'stale', 'unknown']);
660
+
661
+ function denseProjectPeople(payload) {
662
+ const people = payload?.project?.people;
663
+ if (!Array.isArray(people)) return [];
664
+ return people.map((person) => {
665
+ const id = textOrNull(person?.id);
666
+ const name = textOrNull(person?.name) ?? textOrNull(person?.title);
667
+ if (!id || !name || typeof person?.accountable !== 'boolean') return null;
668
+ return { ...person, id, name };
669
+ }).filter(Boolean);
670
+ }
671
+
672
+ function denseParticipantStatus(payload) {
673
+ const raw = [payload?.participantStatus, payload?.participants_status, payload?.participantsStatus]
674
+ .find((value) => value !== undefined && value !== null);
675
+ if (raw === 'ready') return 'available';
676
+ if (typeof raw === 'string' && DENSE_PARTICIPANT_STATUS_VALUES.has(raw)) return raw;
677
+ return (Array.isArray(payload?.participants) && payload.participants.length > 0) || denseProjectPeople(payload).length > 0
678
+ ? 'available'
679
+ : 'unknown';
680
+ }
681
+
682
+ function denseParticipantFreshness(payload) {
683
+ const raw = [payload?.participantFreshness, payload?.participants_freshness, payload?.participantsFreshness]
684
+ .find((value) => value !== undefined && value !== null);
685
+ return typeof raw === 'string' && DENSE_PARTICIPANT_FRESHNESS_VALUES.has(raw) ? raw : null;
686
+ }
687
+
688
+ function denseParticipantAbsenceConfirmed(payload) {
689
+ return payload?.participantAbsenceConfirmed === true
690
+ || payload?.participants_absence_confirmed === true
691
+ || payload?.participantsAbsenceConfirmed === true;
692
+ }
693
+
694
+ function denseParticipantAsOf(payload) {
695
+ const raw = [
696
+ payload?.participantsAsOf,
697
+ payload?.participantAsOf,
698
+ payload?.participants_as_of,
699
+ payload?.participant_as_of,
700
+ payload?.participantsCheckedAt,
701
+ payload?.participants_checked_at,
702
+ payload?.participantCheckedAt,
703
+ payload?.participant_checked_at,
704
+ ].map(textOrNull).find(Boolean);
705
+ if (!raw || Number.isNaN(Date.parse(raw))) return null;
706
+ return formatDateTime(raw);
707
+ }
708
+
709
+ function denseParticipantReason(payload, state, freshness) {
710
+ const explicit = textOrNull(payload?.participantUnavailableReason)
711
+ ?? textOrNull(payload?.participants_unavailable_reason)
712
+ ?? textOrNull(payload?.participantsUnavailableReason);
713
+ if (explicit) return explicit;
714
+ if (Array.isArray(payload?.warnings)) {
715
+ const warning = payload.warnings
716
+ .map((item) => textOrNull(item) ?? textOrNull(item?.reason) ?? textOrNull(item?.message))
717
+ .find(Boolean);
718
+ if (warning) return warning;
719
+ }
720
+ if (state === 'failed') return '関係者の詳細を取得できませんでした。';
721
+ if (state === 'stale' || freshness === 'stale') {
722
+ const checkedAt = denseParticipantAsOf(payload);
723
+ return checkedAt
724
+ ? `以前の情報です(確認時点: ${checkedAt})。現在の関係者の詳細を未確認です。`
725
+ : '以前の情報です。確認時点は未確認です。現在の関係者の詳細を未確認です。';
726
+ }
727
+ if (state === 'partial') return '一部の関係者だけを読み取っています。';
728
+ return '関係者の詳細を未確認です。登録なしとは判断できません。';
729
+ }
730
+
731
+ function denseParticipantCountsZero(payload) {
732
+ const project = payload?.project ?? {};
733
+ return Number.isInteger(project.participantCount)
734
+ && Number.isInteger(project.accountableCount)
735
+ && project.participantCount === 0
736
+ && project.accountableCount === 0;
737
+ }
738
+
739
+ function denseParticipantStrictZero(payload) {
740
+ return denseParticipantCountsZero(payload)
741
+ && Array.isArray(payload?.participants)
742
+ && payload.participants.length === 0;
743
+ }
744
+
745
+ function denseParticipantSnapshot(payload) {
746
+ const state = denseParticipantStatus(payload);
747
+ const freshness = denseParticipantFreshness(payload);
748
+ const absenceConfirmed = denseParticipantAbsenceConfirmed(payload);
749
+ const confirmedEmpty = absenceConfirmed
750
+ && (state === 'available' || state === 'empty')
751
+ && freshness === 'fresh'
752
+ && denseParticipantStrictZero(payload);
753
+ const uncertain = !confirmedEmpty && (state === 'unknown' || state === 'unavailable' || state === 'partial' || state === 'stale' || state === 'failed' || freshness === 'unknown' || freshness === 'stale');
754
+ return { state, freshness, absenceConfirmed, confirmedEmpty, uncertain, reason: denseParticipantReason(payload, state, freshness) };
755
+ }
756
+
757
+ function denseParticipantCountSummary(payload, snapshot) {
758
+ const project = payload?.project ?? {};
759
+ const counts = [];
760
+ if (Number.isInteger(project.participantCount) && project.participantCount > 0) counts.push(`関係者${project.participantCount}人`);
761
+ if (Number.isInteger(project.accountableCount) && project.accountableCount > 0) counts.push(`責任者${project.accountableCount}人`);
762
+ if (counts.length === 0) {
763
+ if (snapshot.confirmedEmpty && denseParticipantStrictZero(payload)) return '確認済みの件数: 関係者0人、責任者0人。';
764
+ return '件数は未確認です。';
765
+ }
766
+ if (snapshot.uncertain) return `既知の件数(現在未確認): ${counts.join('、')}。`;
767
+ return `確認済みの件数: ${counts.join('、')}。`;
768
+ }
769
+
770
+ function denseParticipantMetricValue(payload, participantEdges, snapshot) {
771
+ const project = payload?.project ?? {};
772
+ if (Number.isInteger(project.participantCount) && project.participantCount > 0) return `${project.participantCount}人`;
773
+ if (snapshot.confirmedEmpty && denseParticipantStrictZero(payload)) return '0人';
774
+ if (participantEdges.length > 0) return `${participantEdges.length}人`;
775
+ return '未確認';
776
+ }
777
+
778
+ function denseParticipantCountMismatch(payload, participantEdges, snapshot) {
779
+ if (snapshot.uncertain) return false;
780
+ const project = payload?.project ?? {};
781
+ const declared = [project.participantCount, project.accountableCount]
782
+ .some((value) => Number.isInteger(value) && value > 0);
783
+ const namedPeople = denseProjectPeople(payload);
784
+ if (denseParticipantCountsZero(payload) && participantEdges.length > 0) return true;
785
+ if (participantEdges.length === 0 && namedPeople.length > 0) {
786
+ const participantMismatch = Number.isInteger(project.participantCount)
787
+ && project.participantCount >= 0
788
+ && project.participantCount !== namedPeople.length;
789
+ const accountableMismatch = Number.isInteger(project.accountableCount)
790
+ && project.accountableCount >= 0
791
+ && project.accountableCount !== namedPeople.filter((person) => person.accountable).length;
792
+ return participantMismatch || accountableMismatch;
793
+ }
794
+ return participantEdges.length === 0 && declared;
795
+ }
796
+
658
797
  function renderDenseOverview(doc, detail, context, entities, actions = {}) {
659
798
  const payload = projectDetailPayload(detail);
660
799
  const taskSection = contextTaskSection(context);
@@ -664,7 +803,18 @@ function renderDenseOverview(doc, detail, context, entities, actions = {}) {
664
803
  const participantEdges = [...new Map((payload?.participants ?? [])
665
804
  .filter((edge) => edge?.counterpart?.id && (edge.counterpart.name || edge.counterpart.title))
666
805
  .map((edge) => [edge.counterpart.id, edge])).values()];
667
- const participants = participantEdges.slice(0, 6);
806
+ const participantStatus = denseParticipantStatus(payload);
807
+ const participantPeople = participantStatus === 'available'
808
+ ? denseProjectPeople(payload).map((person) => ({
809
+ counterpart: person,
810
+ relation: person.accountable ? 'accountable_for' : 'participates_in',
811
+ role: person.accountable ? '責任者' : null,
812
+ }))
813
+ : [];
814
+ const participants = (participantEdges.length > 0 ? participantEdges : participantPeople).slice(0, 6);
815
+ const participantSnapshot = denseParticipantSnapshot(payload);
816
+ const participantCountSummary = denseParticipantCountSummary(payload, participantSnapshot);
817
+ const participantCountMismatch = denseParticipantCountMismatch(payload, participantEdges, participantSnapshot);
668
818
  const recentRecords = records.length > 0 ? sortDenseItems(records).slice(0, 4) : graphRecords.slice(0, 4);
669
819
  const updated = [...tasks, ...records]
670
820
  .filter((item) => item.updatedAt || item.eventAt)
@@ -683,7 +833,7 @@ function renderDenseOverview(doc, detail, context, entities, actions = {}) {
683
833
  denseMetric(doc, '仕事', taskMetric, taskNote),
684
834
  denseMetric(doc, '今日扱う仕事', taskSection?.state === 'ok' ? `${due.length}件` : '未確認', taskSection?.state === 'ok' ? '期限・更新が確認できる順' : 'タスクの状態を確認中'),
685
835
  denseMetric(doc, '前回からの変化', updated.length > 0 ? `${updated.length}件` : '未確認', updated.length > 0 ? '更新時点がある記録' : '更新時点が未確認'),
686
- denseMetric(doc, '関係者', participantEdges.length > 0 ? `${participantEdges.length}人` : '未確認', 'Graphに記録された参加関係'),
836
+ denseMetric(doc, '関係者', denseParticipantMetricValue(payload, participantEdges, participantSnapshot), participantCountSummary ?? '現在の関係者の詳細を確認中'),
687
837
  );
688
838
  panel.append(metrics);
689
839
 
@@ -735,8 +885,25 @@ function renderDenseOverview(doc, detail, context, entities, actions = {}) {
735
885
  const peopleHead = makeElement(doc, 'div', { className: 'bb-pkw-dense-card-head' });
736
886
  peopleHead.append(makeElement(doc, 'h4', { text: '関係者' }), makeElement(doc, 'span', { className: 'bb-pkw-muted', text: participants.length > 0 ? `${participants.length}人を表示` : '未確認' }));
737
887
  people.append(peopleHead);
738
- if (participants.length === 0) people.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: 'Graphで確認できる関係者は未確認です。' }));
739
- else {
888
+ if (participantCountSummary) people.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: participantCountSummary }));
889
+ if (participantSnapshot.uncertain) {
890
+ people.append(workspaceNotice(doc, {
891
+ label: participantSnapshot.state === 'failed' ? '読み取り失敗' : '未確認',
892
+ text: participantSnapshot.reason,
893
+ tone: participantSnapshot.state === 'failed' ? 'danger' : 'info',
894
+ }));
895
+ } else if (participantCountMismatch) {
896
+ people.append(workspaceNotice(doc, {
897
+ label: '詳細未確認',
898
+ text: '既知の件数と取得した関係者が一致しません。現在の関係者の詳細を読み取れていません。',
899
+ tone: 'warning',
900
+ }));
901
+ } else if (participants.length === 0 && !participantSnapshot.confirmedEmpty) {
902
+ people.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: '関係者の詳細を未確認です。登録なしとは判断できません。' }));
903
+ } else if (participants.length === 0 && participantSnapshot.confirmedEmpty) {
904
+ people.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: '確認できた関係者はありません。' }));
905
+ }
906
+ if (participants.length > 0) {
740
907
  const list = makeElement(doc, 'ul', { className: 'bb-pkw-dense-list' });
741
908
  for (const edge of participants) {
742
909
  const person = edge.counterpart;
@@ -749,6 +916,9 @@ function renderDenseOverview(doc, detail, context, entities, actions = {}) {
749
916
  }
750
917
  people.append(list);
751
918
  }
919
+ if (typeof actions.retryParticipants === 'function' && (participantSnapshot.uncertain || participantCountMismatch || (participants.length === 0 && !participantSnapshot.confirmedEmpty))) {
920
+ people.append(denseButton(doc, '再試行', 'bb-pkw-dense-action', actions.retryParticipants, { 'aria-label': '関係者の読み取りを再試行' }));
921
+ }
752
922
  supportingGrid.append(people);
753
923
  panel.append(supportingGrid);
754
924
  return panel;
@@ -1033,11 +1203,13 @@ export function createProjectKnowledgeWorkspace({
1033
1203
  mountGraph,
1034
1204
  readEntity,
1035
1205
  onTabChange,
1206
+ onRetryProject,
1036
1207
  } = {}) {
1037
1208
  const doc = getDocument(explicitDocument);
1038
1209
  const payload = projectDetailPayload(detail);
1039
1210
  const project = payload?.project;
1040
1211
  if (!project) throw new TypeError('project detail is required');
1212
+ const workspaceProjectId = cleanId(project.id);
1041
1213
  // Keep the project-bound projection separate from the mutable exploration
1042
1214
  // graph. Reading a related entity may reveal arbitrary neighboring records,
1043
1215
  // but those records are not evidence that they belong to this project.
@@ -1390,6 +1562,7 @@ export function createProjectKnowledgeWorkspace({
1390
1562
  tabs.replaceChildren(tabButton('概要', 'overview'), tabButton('情報を探す', 'graph'));
1391
1563
  panels.replaceChildren();
1392
1564
  const actions = { openOverview, openWorkList, openWorkDetail, openRecordDetail, openSectionList, openGraphEntity: selectAndExplore,
1565
+ retryParticipants: typeof onRetryProject === 'function' ? () => onRetryProject(workspaceProjectId) : null,
1393
1566
  recordSectionId: selectedSectionId,
1394
1567
  recordSectionTitle: CONTEXT_CATEGORIES.find(([id]) => id === selectedSectionId)?.[1] ?? contextSectionById(currentContext, [selectedSectionId])?.title,
1395
1568
  };
@@ -60,6 +60,31 @@
60
60
  align-items: center;
61
61
  pointer-events: auto;
62
62
  }
63
+ .bb-world-canvas-ui .bb-world-canvas-context {
64
+ position: absolute;
65
+ top: calc(var(--bb-world-toolbar-top) + 50px);
66
+ right: var(--bb-world-right-inset);
67
+ width: min(360px, calc(100% - var(--bb-world-right-inset) - 8px));
68
+ max-width: calc(100% - var(--bb-world-right-inset));
69
+ min-width: 0;
70
+ padding: 6px 10px;
71
+ border: 1px solid rgb(13 25 46 / 14%);
72
+ border-radius: 8px;
73
+ background: rgb(255 255 255 / 92%);
74
+ color: #0d192e;
75
+ font-size: 11px;
76
+ line-height: 1.5;
77
+ overflow-wrap: anywhere;
78
+ word-break: break-word;
79
+ pointer-events: none;
80
+ }
81
+ .bb-world-canvas-ui .bb-world-canvas-context p {
82
+ margin: 0;
83
+ }
84
+ .bb-world-canvas-ui .bb-world-canvas-context-next-action {
85
+ margin-top: 3px;
86
+ color: rgb(13 25 46 / 72%);
87
+ }
63
88
  .bb-world-canvas-ui .bb-world-canvas-control {
64
89
  min-width: 0;
65
90
  height: 40px;
@@ -318,6 +343,13 @@
318
343
  --bb-world-panel-top: 72px;
319
344
  }
320
345
  .bb-world-canvas-ui .bb-world-canvas-toolbar { grid-template-columns: auto minmax(0, 1fr) 34px; }
346
+ .bb-world-canvas-ui .bb-world-canvas-context {
347
+ top: calc(var(--bb-world-toolbar-top) + 46px);
348
+ width: calc(100% - var(--bb-world-right-inset) - 4px);
349
+ max-width: calc(100% - var(--bb-world-right-inset));
350
+ padding: 5px 8px;
351
+ font-size: 10px;
352
+ }
321
353
  .bb-world-canvas-ui .bb-world-canvas-control { height: 36px; padding: 6px; font-size: 10px; }
322
354
  .bb-world-canvas-ui .bb-world-canvas-sky { padding: 0; }
323
355
  .bb-world-canvas-ui .bb-world-canvas-sky-icon { font-size: 20px; }
@@ -26,6 +26,7 @@ const nodeList = (nodes) => (Array.isArray(nodes) ? nodes : [nodes]).filter((nod
26
26
  * @param {(code: string) => void} [options.onCity] Enter a known business.
27
27
  * @param {(mode: 'auto'|'day'|'dusk'|'night') => void} [options.onSky]
28
28
  * @param {() => void} [options.onDismissTask] User dismissed task details.
29
+ * @param {{purpose?: string, knownCityAction?: string, unavailableCity?: string}} [options.context]
29
30
  *
30
31
  * showRail/showTask/setHelp accept actual DOM nodes, preserving their events and
31
32
  * all source details. showRail never opens the drawer; openRail opens it without
@@ -34,7 +35,16 @@ const nodeList = (nodes) => (Array.isArray(nodes) ? nodes : [nodes]).filter((nod
34
35
  * addNote shows a quest only for an explicit nonempty summary; its full text
35
36
  * remains in help. No summary is inferred from a long or nonurgent notice.
36
37
  */
37
- export function createWorldCanvasUI({ doc, stage, reducedMotion = false, onReset, onCity, onSky, onDismissTask }) {
38
+ export function createWorldCanvasUI({
39
+ doc,
40
+ stage,
41
+ reducedMotion = false,
42
+ onReset,
43
+ onCity,
44
+ onSky,
45
+ onDismissTask,
46
+ context = {},
47
+ }) {
38
48
  const id = `bb-world-canvas-${++instanceId}`;
39
49
  const listeners = [];
40
50
  let disposed = false;
@@ -50,6 +60,11 @@ export function createWorldCanvasUI({ doc, stage, reducedMotion = false, onReset
50
60
  let businesses = new Set();
51
61
  let overlayOrder = [];
52
62
  let overlayLayer = 10;
63
+ const worldContext = {
64
+ purpose: context.purpose ?? '事業と案件、関連する仕事を地図で確認します。',
65
+ knownCityAction: context.knownCityAction ?? '都市を選ぶと、その事業の区画に入ります。',
66
+ unavailableCity: context.unavailableCity ?? '現在、選べる事業を確認できません。',
67
+ };
53
68
 
54
69
  function element(tag, className, text, attrs = {}) {
55
70
  const node = doc.createElement(tag);
@@ -127,6 +142,16 @@ export function createWorldCanvasUI({ doc, stage, reducedMotion = false, onReset
127
142
  const skyIcon = element('span', 'bb-world-canvas-sky-icon', SKY_MODES[0].icon, { 'aria-hidden': 'true' });
128
143
  sky.append(skyIcon);
129
144
  toolbar.append(reset, city, sky);
145
+ const contextRoot = element('div', 'bb-world-canvas-context', null, {
146
+ 'aria-label': 'この画面で確認できること',
147
+ });
148
+ const contextPurpose = element('p', 'bb-world-canvas-context-purpose', worldContext.purpose, {
149
+ 'data-world-context': 'purpose',
150
+ });
151
+ const contextNextAction = element('p', 'bb-world-canvas-context-next-action', worldContext.unavailableCity, {
152
+ 'data-world-context': 'next-action',
153
+ });
154
+ contextRoot.append(contextPurpose, contextNextAction);
130
155
  const skyMenu = element('div', 'bb-world-canvas-sky-menu', null, { id: `${id}-sky`, role: 'menu', 'aria-label': '空の時間帯' });
131
156
  skyMenu.hidden = true;
132
157
  const skyOptions = SKY_MODES.map((mode) => {
@@ -176,7 +201,7 @@ export function createWorldCanvasUI({ doc, stage, reducedMotion = false, onReset
176
201
  const notes = element('div', 'bb-world-canvas-notes');
177
202
  helpBody.append(helpContent, notes);
178
203
  help.append(helpTop, helpBody);
179
- root.append(toolbar, skyMenu, quests, edge, rail, task, helpButton, help);
204
+ root.append(toolbar, contextRoot, skyMenu, quests, edge, rail, task, helpButton, help);
180
205
  stage.append(root);
181
206
 
182
207
  function setCities(items = [], selectedCode = null) {
@@ -195,6 +220,9 @@ export function createWorldCanvasUI({ doc, stage, reducedMotion = false, onReset
195
220
  }
196
221
  city.replaceChildren(...options);
197
222
  city.disabled = businesses.size === 0;
223
+ contextNextAction.textContent = businesses.size === 0
224
+ ? worldContext.unavailableCity
225
+ : worldContext.knownCityAction;
198
226
  setCity(selectedCode);
199
227
  }
200
228
  function setCity(code) {
@@ -1658,6 +1658,11 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
1658
1658
  wrap.append(stage);
1659
1659
  canvasUI = createWorldCanvasUI({
1660
1660
  doc, stage, reducedMotion,
1661
+ context: {
1662
+ purpose: '事業と案件、関連する仕事を地図で確認します。',
1663
+ knownCityAction: '都市を選ぶと、その事業の区画に入ります。',
1664
+ unavailableCity: '現在、選べる事業を確認できません。詳しい状況は「世界の見方」で確認できます。',
1665
+ },
1661
1666
  onReset: () => { goToWorld(); clearAll(); },
1662
1667
  onCity: (code) => {
1663
1668
  if (!code) { goToWorld(); clearAll(); return; }