@unson/brainbase-mcp 0.41.11 → 0.41.13

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.
@@ -364,6 +364,38 @@
364
364
  margin-top: 14px;
365
365
  }
366
366
 
367
+ /* 根拠と履歴: one fold per record, after its relations (ledger D14). */
368
+ .bb-graph-rail > .bb-graph-evidence {
369
+ border-bottom: 1px solid var(--bb-color-border);
370
+ padding-block: 18px;
371
+ font-size: 13px;
372
+ }
373
+
374
+ .bb-graph-evidence > summary {
375
+ display: flex;
376
+ align-items: center;
377
+ gap: var(--bb-space-2);
378
+ color: var(--bb-color-ink);
379
+ font-weight: 690;
380
+ cursor: pointer;
381
+ }
382
+
383
+ .bb-graph-evidence-part {
384
+ margin-top: var(--bb-space-3);
385
+ }
386
+
387
+ .bb-graph-evidence-part > h4 {
388
+ margin: 0 0 var(--bb-space-1);
389
+ color: var(--bb-color-muted);
390
+ font-size: 12px;
391
+ font-weight: 690;
392
+ }
393
+
394
+ .bb-graph-evidence-part > p {
395
+ margin: 0 0 var(--bb-space-1);
396
+ overflow-wrap: anywhere;
397
+ }
398
+
367
399
  .bb-graph-rail > .bb-graph-history {
368
400
  border-bottom: 1px solid var(--bb-color-border);
369
401
  padding-block: 18px;
@@ -38,6 +38,45 @@ export const GRAPH_ENTITY_TYPE_LABELS = Object.freeze({
38
38
  org: '組織',
39
39
  project: 'プロジェクト',
40
40
  decision: '判断',
41
+ // The kinds an organization's Graph defines on top of the local four, so the
42
+ // screens never show an internal type id (ledger D14).
43
+ app: 'アプリ',
44
+ product: 'プロダクト',
45
+ brand: 'ブランド',
46
+ customer: '顧客',
47
+ partner: '協業先',
48
+ contact: '名刺の連絡先',
49
+ raci: '責任分担表',
50
+ raci_assignment: '責任の割り当て',
51
+ role_assignment: '役割',
52
+ glossary_term: '用語',
53
+ document: '文書',
54
+ frame: '考え方の枠組み',
55
+ story: 'ストーリー',
56
+ infrastructure_environment: '稼働環境',
57
+ speaking: '登壇',
58
+ media_appearance: 'メディア出演',
59
+ publication: '出版物',
60
+ press_mention: '報道',
61
+ initiative: '施策',
62
+ ai_query: 'AIへの問い合わせ',
63
+ ai_decision: 'AIを使った判断',
64
+ philosophy: '哲学',
65
+ capital: '資本',
66
+ contract: '契約',
67
+ database: 'データベース',
68
+ database_cluster: 'データベースのクラスタ',
69
+ diagnosis: '診断',
70
+ finance: '財務',
71
+ finance_account: '財務の口座',
72
+ org_alias: '組織の別名',
73
+ organization: '組織(旧い形式)',
74
+ person_alias: '人物の別名',
75
+ push_case: '推進案件',
76
+ objective: '目的',
77
+ variable: '変数',
78
+ model: '世界モデル',
79
+ constraint: '制約',
41
80
  });
42
81
 
43
82
  export const GRAPH_ENTITY_TYPE_MEANINGS = Object.freeze({
@@ -51,14 +90,72 @@ export const GRAPH_ENTITY_TYPE_MEANINGS = Object.freeze({
51
90
  * `outgoing` follows the counterpart name when this record is the start of the
52
91
  * relation; `incoming` follows it when this record is the end.
53
92
  */
93
+ const relation = (label, meaning, outgoing, incoming) => Object.freeze({ label, meaning, outgoing, incoming });
94
+
95
+ /*
96
+ * Every relation of the local Graph and of an organization's Graph has a plain
97
+ * Japanese name (ledger D14); a relation id is shown only in the details.
98
+ * Relations that mean the same to a reader share one name (所有 for owns and
99
+ * owned_by, 所属 for member_of and belongs_to), and a list of relation kinds
100
+ * shows them as one row (`groupRelationKinds`).
101
+ */
54
102
  export const GRAPH_RELATION_LABELS = Object.freeze({
55
- member_of: Object.freeze({ label: '所属', meaning: '人物が組織に所属している', outgoing: 'に所属', incoming: 'が所属' }),
56
- participates_in: Object.freeze({ label: '参加', meaning: '人物がプロジェクトに参加している', outgoing: 'に参加', incoming: 'が参加' }),
57
- accountable_for: Object.freeze({ label: '責任', meaning: '人物がプロジェクトの成果や判断に責任を持つ', outgoing: 'に責任を持つ', incoming: 'が責任を持つ' }),
58
- owned_by: Object.freeze({ label: '所有', meaning: 'プロジェクトを組織が所有している', outgoing: 'が所有', incoming: 'を所有' }),
59
- related_to: Object.freeze({ label: '関わる', meaning: '会社が顧客・協業先などとしてプロジェクトに関わっている', outgoing: 'に関わる', incoming: 'が関わる' }),
60
- governs: Object.freeze({ label: '方針', meaning: '判断や原則がプロジェクトの進め方を決める', outgoing: 'の進め方を決める', incoming: 'が進め方を決める' }),
61
- supersedes: Object.freeze({ label: '置き換え', meaning: '判断が以前の判断を置き換える', outgoing: 'を置き換える', incoming: 'に置き換えられた' }),
103
+ member_of: relation('所属', '人物が組織やプロジェクトに所属している', 'に所属', 'が所属'),
104
+ belongs_to: relation('所属', '記録が組織・プロダクト・ブランド・プロジェクトに含まれる', 'に所属', 'が所属'),
105
+ belongs_to_project: relation('所属', '記録がプロジェクトに含まれる', 'に所属', 'が所属'),
106
+ participates_in: relation('参加', '人物がプロジェクトに参加している', 'に参加', 'が参加'),
107
+ accountable_for: relation('責任', '人物や組織が成果や判断に最終的な責任を持つ', 'に責任を持つ', 'が責任を持つ'),
108
+ owned_by: relation('所有', 'プロジェクトなどを組織や人が所有している', 'が所有', 'を所有'),
109
+ owns: relation('所有', '組織がアプリ・プロダクト・ブランド・プロジェクトを所有している', 'を所有', 'が所有'),
110
+ owns_app: relation('所有', 'プロジェクトがアプリを持っている(旧い形式)', 'を所有', 'が所有'),
111
+ related_to: relation('関わる', '会社が顧客・協業先などとしてプロジェクトに関わっている', 'に関わる', 'が関わる'),
112
+ governs: relation('進め方を決める', '判断や原則が、対象の進め方を決める', 'の進め方を決める', 'が進め方を決める'),
113
+ governed_by: relation('従う原則', '推進案件が従う原則', 'に従う', 'が従う'),
114
+ supersedes: relation('置き換え', '判断が以前の判断を置き換える', 'を置き換える', 'に置き換えられた'),
115
+ superseded_by: relation('同一人物に統合', '古い人物の記録を、正しい人物の記録へまとめた', 'へ統合', 'を統合'),
116
+ derived_from: relation('由来', '記録が別の記録をもとにしている', 'に由来', 'が由来とする'),
117
+ depends_on: relation('依存', '稼働や成果が別の記録に依存している', 'に依存', 'が依存'),
118
+ assigned_to: relation('責任の割り当て', '責任分担で、人や組織に責任を割り当てている', 'に割り当て', 'で割り当て'),
119
+ requested_by: relation('問い合わせた人', 'AIへの問い合わせをした人', 'が問い合わせた', 'を問い合わせた'),
120
+ made_by: relation('判断した人', '判断をした人', 'が判断した', 'を判断した'),
121
+ references: relation('根拠として参照', '判断や問い合わせが、根拠として別の記録を参照している', 'を参照', 'が参照'),
122
+ evidenced_by: relation('根拠の文書', '判断の根拠になった文書', 'を根拠にする', 'が根拠にする'),
123
+ informs: relation('判断材料', '文書がプロジェクトの判断材料になる', 'の判断材料', 'が判断材料'),
124
+ mentions: relation('言及', '判断が協業先やプロダクトに言及している', 'に言及', 'が言及'),
125
+ alias_of: relation('別名', '人や組織の別の呼び名', 'の別名', 'が別名'),
126
+ applies_to: relation('適用先', '文書が適用される組織', 'に適用', 'が適用'),
127
+ describes: relation('説明', '文書が組織を説明している', 'を説明', 'が説明'),
128
+ brand_of: relation('ブランドの対象', 'ブランドが表す組織やプロジェクト', 'のブランド', 'がブランド'),
129
+ sub_brand_of: relation('上位ブランド', 'ブランドの上位にあるブランド', 'の下位ブランド', 'が下位ブランド'),
130
+ operates: relation('運営', '組織が運営している', 'を運営', 'が運営'),
131
+ operated_by: relation('運営', '組織が運営している', 'が運営', 'を運営'),
132
+ covers_engagement: relation('責任分担の対象', '責任分担表が対象とする組織や案件', 'の責任分担', 'が責任分担を示す'),
133
+ describes_org: relation('責任分担の対象', '責任分担表が対象とする組織や案件', 'の責任分担', 'が責任分担を示す'),
134
+ has_raci: relation('責任分担表', '組織やプロジェクトが持つ責任分担表', 'を持つ', 'が持つ'),
135
+ has_project: relation('含む', 'プロジェクトが子プロジェクト・案件・施策を含む', 'を含む', 'に含まれる'),
136
+ has_engagement: relation('含む', 'プロジェクトが子プロジェクト・案件・施策を含む', 'を含む', 'に含まれる'),
137
+ has_initiative: relation('含む', 'プロジェクトが子プロジェクト・案件・施策を含む', 'を含む', 'に含まれる'),
138
+ relates_to_project: relation('関連', 'プロジェクトどうしが関連している', 'と関連', 'と関連'),
139
+ serves_customer: relation('価値を届ける顧客', 'プロジェクトが価値を届ける顧客', 'に価値を届ける', 'が価値を届ける'),
140
+ org_unit_of: relation('上位の顧客', '部署が属する顧客の会社', 'の部署', 'が部署'),
141
+ supports_project: relation('支援', '協業先がプロジェクトを運営する組織を支援している', 'を支援', 'が支援'),
142
+ key_contact_for: relation('主な窓口', 'ブランドや推進案件の主な窓口', 'の主な窓口', 'が主な窓口'),
143
+ primary_contact_for: relation('主な窓口', 'ブランドや推進案件の主な窓口', 'の主な窓口', 'が主な窓口'),
144
+ has_push_case: relation('推進案件', '顧客について進めている推進案件', 'を推進案件に持つ', 'の推進案件'),
145
+ has_diagnosis: relation('診断', '推進案件についての診断', 'を持つ', 'が持つ'),
146
+ role_at: relation('役割', '人物の役割と、その役割の所属先', 'での役割', 'がある'),
147
+ holds_role: relation('役割', '人物の役割と、その役割の所属先', 'を持つ', 'が持つ'),
148
+ uses_app: relation('利用', 'プロジェクトやアプリが、アプリやデータベースを使う', 'を使う', 'が使う'),
149
+ uses_database: relation('利用', 'プロジェクトやアプリが、アプリやデータベースを使う', 'を使う', 'が使う'),
150
+ has_environment: relation('稼働環境', 'プロジェクトが稼働環境として持つアプリ', 'を稼働環境に持つ', 'の稼働環境'),
151
+ hosted_on: relation('置き場所', 'データベースが置かれているクラスタ', 'に置かれている', 'が置かれている'),
152
+ uses_secret_store: relation('秘密情報の保管先', 'アプリが使う(または使うべき)秘密情報の保管先', 'を保管先に使う', 'が保管先に使う'),
153
+ should_use_secret_store: relation('秘密情報の保管先', 'アプリが使う(または使うべき)秘密情報の保管先', 'を保管先に使うべき', 'が保管先に使うべき'),
154
+ created: relation('生み出した', '人物がプロダクトを生み出した', 'を生み出した', 'が生み出した'),
155
+ authored: relation('著作', '人物が出版物を著した', 'を著した', 'が著した'),
156
+ spoke_at: relation('登壇', '人物が登壇した', 'で登壇', 'が登壇'),
157
+ appeared_in: relation('出演', '人物がメディアに出演した', 'に出演', 'が出演'),
158
+ mentioned_in: relation('掲載', '人物が報道に掲載された', 'に掲載', 'が掲載'),
62
159
  });
63
160
 
64
161
  /** Relations a correction may add (the host refuses any other). */
@@ -213,9 +310,9 @@ export function facts(doc, rows, className = 'bb-graph-facts') {
213
310
  }
214
311
 
215
312
  /** Ids and digests stay in this disclosure, never in the visible copy. */
216
- export function recordDetails(doc, rows) {
313
+ export function recordDetails(doc, rows, { title = '記録の詳細' } = {}) {
217
314
  const details = makeElement(doc, 'details', { className: 'bb-graph-audit' });
218
- details.append(makeElement(doc, 'summary', { text: '記録の詳細' }), facts(doc, rows, 'bb-graph-facts is-mono'));
315
+ details.append(makeElement(doc, 'summary', { text: title }), facts(doc, rows, 'bb-graph-facts is-mono'));
219
316
  return details;
220
317
  }
221
318
 
@@ -307,9 +404,33 @@ export function ownLabel(table, key) {
307
404
  return typeof key === 'string' && Object.hasOwn(table, key) ? table[key] : undefined;
308
405
  }
309
406
 
310
- /** A relation's plain label; an unknown relation shows its raw id. */
311
- export function relationLabel(relation) {
312
- return ownLabel(GRAPH_RELATION_LABELS, relation)?.label ?? relation;
407
+ /**
408
+ * A relation's plain label. An unknown relation shows the meaning its host
409
+ * gave (when there is one) rather than its raw id.
410
+ */
411
+ export function relationLabel(relation, meaning) {
412
+ return ownLabel(GRAPH_RELATION_LABELS, relation)?.label ?? textOrNull(meaning) ?? relation;
413
+ }
414
+
415
+ /**
416
+ * The relation kinds of an ontology answer as rows a reader can tell apart:
417
+ * kinds with the same plain name are one row (所有 for owns, owned_by and
418
+ * owns_app), and each row keeps every kind with its start and end kinds. Rows
419
+ * keep the order in which their first kind appears.
420
+ */
421
+ export function groupRelationKinds(relations) {
422
+ const rows = new Map();
423
+ for (const item of Array.isArray(relations) ? relations : []) {
424
+ if (!isRecord(item) || typeof item.id !== 'string') continue;
425
+ const known = ownLabel(GRAPH_RELATION_LABELS, item.id);
426
+ const label = relationLabel(item.id, item.meaning);
427
+ const row = rows.get(label) ?? { label, meanings: [], kinds: [] };
428
+ const meaning = known?.meaning ?? textOrNull(item.meaning);
429
+ if (meaning && !row.meanings.includes(meaning)) row.meanings.push(meaning);
430
+ row.kinds.push(item);
431
+ rows.set(label, row);
432
+ }
433
+ return [...rows.values()];
313
434
  }
314
435
 
315
436
  /** A type's plain label; an unknown type (from another host's Graph) shows its raw id. */
@@ -620,6 +741,59 @@ export function renderGraphIssues(doc, issues) {
620
741
  });
621
742
  }
622
743
 
744
+ /**
745
+ * 根拠と履歴 of one record, folded: where the record came from (the host's 出典
746
+ * notice, which may carry its authorization), the source or correction records
747
+ * that could not be read, and the corrections made. None of these is a notice
748
+ * for the whole screen (ledger D14); records that could not be read are counted
749
+ * on the fold itself so they are not hidden.
750
+ *
751
+ * @param {Document} doc
752
+ * @param {{ source?: string | Element | null, issues?: unknown[], history?: unknown[] }} parts
753
+ */
754
+ export function renderEvidenceAndHistory(doc, { source = null, issues = [], history = [] } = {}) {
755
+ const unreadable = (Array.isArray(issues) ? issues : []).filter(isRecord);
756
+ const corrections = (Array.isArray(history) ? history : []).filter(isRecord);
757
+ const details = makeElement(doc, 'details', { className: 'bb-graph-evidence' });
758
+ const summary = makeElement(doc, 'summary', { text: '根拠と履歴' });
759
+ if (unreadable.length > 0) summary.append(badge(doc, `読めない記録 ${unreadable.length}件`, 'warning'));
760
+ details.append(summary);
761
+ const section = (title, ...children) => {
762
+ const block = makeElement(doc, 'section', { className: 'bb-graph-evidence-part' });
763
+ block.append(makeElement(doc, 'h4', { text: title }), ...children.filter(Boolean));
764
+ details.append(block);
765
+ };
766
+ if (source) {
767
+ const body = typeof source === 'object' && typeof source.tagName === 'string' ? source : makeElement(doc, 'p', { text: String(source) });
768
+ section('出どころ', body);
769
+ }
770
+ if (unreadable.length > 0) {
771
+ const list = makeElement(doc, 'ul', { className: 'bb-graph-issue-list' });
772
+ for (const issue of unreadable) {
773
+ const where = `${String(issue.file ?? '不明')}${Number.isInteger(issue.line) ? `(${issue.line}行目)` : ''}`;
774
+ list.append(makeElement(doc, 'li', { text: `${where}: ${String(issue.reason ?? '理由不明')}` }));
775
+ }
776
+ section(
777
+ '読めない記録',
778
+ makeElement(doc, 'p', { text: `この記録の出典や訂正の履歴のうち、壊れていて読めない記録が${unreadable.length}件あります。表示している内容は読めた記録だけから作っているため、最後の訂正や出典が抜けているかもしれません。` }),
779
+ list,
780
+ );
781
+ }
782
+ if (corrections.length > 0) {
783
+ const list = makeElement(doc, 'ol', { className: 'bb-graph-history-list' });
784
+ for (const record of corrections) {
785
+ const fields = Array.isArray(record.changedFields) ? record.changedFields.map((field) => FIELD_LABELS[field] ?? field).join('・') : '';
786
+ list.append(makeElement(doc, 'li', {
787
+ text: `${formatDateTime(record.at)} ${CORRECTION_KIND_LABELS[record.kind] ?? '直した'}${fields ? `(${fields})` : ''}。理由: ${String(record.reason ?? '')}`,
788
+ }));
789
+ }
790
+ section(`訂正の履歴(${corrections.length}件)`, list);
791
+ } else if (Array.isArray(history)) {
792
+ section('訂正の履歴', makeElement(doc, 'p', { className: 'bb-graph-empty', text: 'この記録は、まだ訂正されていません。' }));
793
+ }
794
+ return details;
795
+ }
796
+
623
797
  // ---------------------------------------------------------------------------
624
798
  // Layout
625
799
 
@@ -662,6 +836,11 @@ export function createGraphLayout(doc, { root, rail, label, className, contractV
662
836
  surface.append(...workspaceChildren.filter(Boolean));
663
837
  const detail = makeElement(doc, 'div', { className: `bb-graph-rail ${className}-rail` });
664
838
  detail.append(...railChildren.filter(Boolean));
839
+ // A view with nothing for the rail (its detail is inside the workspace) draws no inline rail.
840
+ if (!rail && !railChildren.some(Boolean)) {
841
+ root.replaceChildren(surface);
842
+ return;
843
+ }
665
844
  if (rail) {
666
845
  root.replaceChildren(surface);
667
846
  rail.replaceChildren(detail);
@@ -1,4 +1,5 @@
1
1
  @import './native-scrollbars.css';
2
+ @import './area-nav.css';
2
3
  /*
3
4
  * Shell of the local Web host, in the organization edition's layout: a white
4
5
  * left rail (brand, navigation, source) and a paper workspace. Uses only the
@@ -71,19 +72,6 @@ body {
71
72
  gap: 2px;
72
73
  }
73
74
 
74
- .bb-shell-nav-group {
75
- display: grid;
76
- gap: 2px;
77
- }
78
-
79
- .bb-shell-nav-group-head {
80
- margin: 14px 10px 4px;
81
- color: var(--bb-color-rail-muted);
82
- font-size: 11px;
83
- font-weight: 700;
84
- letter-spacing: .04em;
85
- }
86
-
87
75
  .bb-shell-nav-link {
88
76
  min-height: 38px;
89
77
  display: flex;
@@ -17,6 +17,7 @@
17
17
 
18
18
  import { createGraphProjectsView } from './graph-projects-view.js';
19
19
  import { createGraphRegistryView } from './graph-registry-view.js';
20
+ import { createGraphOntologyView } from './graph-ontology-view.js';
20
21
  import { createPeopleAndCompaniesView } from './people-and-companies-view.js';
21
22
  import { createObjectiveEditorController } from './objective-editor.js';
22
23
  import { createObjectiveEditorHttpPort } from './objective-editor-http-port.js';
@@ -27,6 +28,7 @@ import { createMeetingMinutesLineageExtension } from './meeting-minutes-lineage-
27
28
  import { createWorldModelView } from './world-model-view.js';
28
29
  import { createGraphClient } from './graph-view-shared.js';
29
30
  import { installNativeScrollbars } from './native-scrollbars.js';
31
+ import { browserAreaStorage, createAreaMemory, createAreaNav } from './area-nav.js';
30
32
 
31
33
  export const LOCAL_WEB_SHELL_CONTRACT_VERSION = 'brainbase.local-web-shell.v1';
32
34
 
@@ -51,22 +53,30 @@ const NAV_ICON_PATHS = Object.freeze({
51
53
  minutes: ['M6 3.5h9l3 3V20.5H6z', 'M15 3.5v4h3', 'M9 12h6m-6 3h6'],
52
54
  // Two people (the organization edition's 人と会社 uses the same shape).
53
55
  people: ['M9 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z', 'M2.5 20a6.5 6.5 0 0 1 13 0', 'M16 4.5a3.5 3.5 0 0 1 0 6.5', 'M18 14a6.5 6.5 0 0 1 3.5 6'],
56
+ // Kinds of information and how they connect (settings, ledger D14): three linked boxes.
57
+ ontology: ['M4 4h6v6H4z', 'M14 14h6v6h-6z', 'M14 4h6v6h-6z', 'M10 7h4', 'M17 10v4'],
54
58
  // World view (extension id `world`, ledger P17).
55
59
  world: ['M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z', 'M3 12h18', 'M12 3c2.5 2.6 3.8 5.6 3.8 9s-1.3 6.4-3.8 9c-2.5-2.6-3.8-5.6-3.8-9S9.5 5.6 12 3z'],
60
+ // Areas whose screens have their own icons (ledger D10, stage 2): a briefcase, and
61
+ // the organization edition's gear.
62
+ 'area:work': ['M4 8h16v11H4z', 'M9 8V5h6v3', 'M4 13h16'],
63
+ 'area:settings': ['M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915', 'M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z'],
56
64
  });
57
65
  // Screens without their own icon (for example a private product's extension).
58
66
  const DEFAULT_NAV_ICON_PATHS = Object.freeze(['M5 20V10m7 10V4m7 16v-7']);
59
67
  const SVG_NS = 'http://www.w3.org/2000/svg';
60
68
 
61
- // The menu's areas (brainbase-project ledger D10): the same order and headings
62
- // as the organization edition. 目的と現状 is the only item of its area, so it
63
- // has no heading. Inside an area, `order` lists the screens first in that order.
69
+ // The menu's areas (brainbase-project ledger D10): the same order and names as
70
+ // the organization edition. The left side lists only the areas; the screens of
71
+ // the current area are tabs above the page (stage 2). 目的と現状 is the only
72
+ // screen of its area and keeps its own name. Inside an area, `order` lists the
73
+ // screens first in that order; `defaultScreen` opens when nothing is remembered.
64
74
  const NAV_AREAS = Object.freeze([
65
- Object.freeze({ id: 'purpose', label: null, order: ['objectives'] }),
66
- Object.freeze({ id: 'work', label: '仕事', order: ['projects', 'minutes'] }),
67
- Object.freeze({ id: 'outcomes', label: '成果', order: ['value-proofs'] }),
68
- Object.freeze({ id: 'information', label: '情報と関係', order: ['graph', 'people'] }),
69
- Object.freeze({ id: 'settings', label: '設定', order: [] }),
75
+ Object.freeze({ id: 'purpose', label: null, order: ['objectives'], icon: 'objectives' }),
76
+ Object.freeze({ id: 'work', label: '仕事', order: ['projects', 'minutes'], defaultScreen: 'projects', icon: 'area:work' }),
77
+ Object.freeze({ id: 'outcomes', label: '成果', order: ['value-proofs'], icon: 'value-proofs' }),
78
+ Object.freeze({ id: 'information', label: '情報と関係', order: ['graph', 'people'], defaultScreen: 'graph', icon: 'graph' }),
79
+ Object.freeze({ id: 'settings', label: '設定', order: ['ontology'], defaultScreen: 'ontology', icon: 'area:settings' }),
70
80
  ]);
71
81
 
72
82
  // Menu order only: the screens' own order (and so the first screen) stays as given.
@@ -88,6 +98,23 @@ function navLayout(screens) {
88
98
  return { leading, areas, trailing };
89
99
  }
90
100
 
101
+ // Every menu line is an area: a screen without an area is an area of its own.
102
+ function navAreas(screens) {
103
+ const layout = navLayout(screens);
104
+ const own = (screen) => ({ id: screen.id, label: null, screenIds: [screen.id], icon: screen.id });
105
+ return [
106
+ ...layout.leading.map(own),
107
+ ...layout.areas.map(({ area, screens: members }) => ({
108
+ id: area.id,
109
+ label: area.label,
110
+ defaultScreen: area.defaultScreen,
111
+ screenIds: members.map((screen) => screen.id),
112
+ icon: area.icon,
113
+ })),
114
+ ...layout.trailing.map(own),
115
+ ];
116
+ }
117
+
91
118
  function makeIcon(doc, id) {
92
119
  const paths = NAV_ICON_PATHS[id] ?? DEFAULT_NAV_ICON_PATHS;
93
120
  const create = (tag) => (typeof doc.createElementNS === 'function' ? doc.createElementNS(SVG_NS, tag) : doc.createElement(tag));
@@ -299,7 +326,7 @@ function mountGraphScreen(screenId, createView, extra = {}) {
299
326
  fetcher: context.fetcher,
300
327
  token: context.token,
301
328
  initialEntityId: context.initialEntityId,
302
- ...extra,
329
+ ...(typeof extra === 'function' ? extra(context) : extra),
303
330
  });
304
331
  };
305
332
  }
@@ -309,8 +336,13 @@ export const LOCAL_WEB_SCREENS = Object.freeze([
309
336
  Object.freeze({ id: 'minutes', label: '議事録', area: 'work', usesGraph: false, rail: true, source: '正本の議事録', mount: mountMeetingMinutes }),
310
337
  Object.freeze({ id: 'objectives', label: '目的と現状', area: 'purpose', usesGraph: true, rail: true, source: '手元のGraph', mount: mountObjectives }),
311
338
  Object.freeze({ id: 'projects', label: 'プロジェクトと関係者', area: 'work', usesGraph: true, rail: true, source: '手元のGraph', mount: mountGraphScreen('projects', createGraphProjectsView) }),
312
- Object.freeze({ id: 'graph', label: '情報と関係', area: 'information', usesGraph: true, rail: true, source: '手元のGraph', mount: mountGraphScreen('graph', createGraphRegistryView) }),
339
+ // A record of 情報と関係 opens as its own screen at `#graph?entity_id=…`; the list is `#graph`.
340
+ Object.freeze({ id: 'graph', label: '情報と関係', area: 'information', usesGraph: true, rail: false, source: '手元のGraph', mount: mountGraphScreen('graph', createGraphRegistryView, (context) => ({
341
+ onRouteChange: (entityId, options) => context.navigate?.(entityId ? `graph?entity_id=${encodeURIComponent(entityId)}` : 'graph', options),
342
+ })) }),
313
343
  Object.freeze({ id: 'people', label: '人と会社', area: 'information', usesGraph: true, rail: true, source: '手元のGraph', mount: mountGraphScreen('people', createPeopleAndCompaniesView, { canCorrect: true }) }),
344
+ // The definitions behind 情報と関係: infrequent management work, so under settings (ledger D14).
345
+ Object.freeze({ id: 'ontology', label: '情報の種類とつながり方', area: 'settings', usesGraph: true, rail: false, source: '手元のGraph', mount: mountGraphScreen('ontology', createGraphOntologyView, { canCorrect: true }) }),
314
346
  ]);
315
347
 
316
348
  function renderGraphGate(doc, slot, status, onRecheck, label) {
@@ -394,6 +426,8 @@ export function createLocalWebShell({
394
426
  initialScreen,
395
427
  meetingMinutesStorage = null,
396
428
  meetingMinutesLineage = null,
429
+ areaMemory = createAreaMemory(browserAreaStorage()),
430
+ onNavigate,
397
431
  } = {}) {
398
432
  if (!root) throw new TypeError('root is required');
399
433
  const doc = explicitDocument ?? (typeof document === 'undefined' ? null : document);
@@ -401,12 +435,13 @@ export function createLocalWebShell({
401
435
  const request = typeof fetcher === 'function'
402
436
  ? fetcher
403
437
  : typeof globalThis.fetch === 'function' ? (path, init) => globalThis.fetch(path, init) : null;
404
- const context = Object.freeze({ document: doc, fetcher: request, token, meetingMinutesStorage, meetingMinutesLineage });
438
+ // `navigate(target, { back })` lets a screen give its own state an address (the host's hash).
439
+ const navigate = typeof onNavigate === 'function' ? (target, options = {}) => onNavigate(target, options) : null;
440
+ const context = Object.freeze({ document: doc, fetcher: request, token, meetingMinutesStorage, meetingMinutesLineage, navigate });
405
441
 
406
442
  const status = { phase: 'loading', data: null, error: null };
407
443
  const mounted = new Map();
408
444
  const slots = new Map();
409
- const links = new Map();
410
445
  const pendingEntityIds = new Map();
411
446
  let active = null;
412
447
 
@@ -427,37 +462,27 @@ export function createLocalWebShell({
427
462
  };
428
463
  menuButton.addEventListener('click', () => setMenuOpen(!sidebar.className.includes('is-open')));
429
464
  backdrop.addEventListener('click', () => setMenuOpen(false));
430
- const navLink = (screen) => {
431
- const link = makeElement(doc, 'a', { className: 'bb-shell-nav-link', text: screen.label, attrs: { href: `#${screen.id}` } });
432
- // The label stays the link's own text; the icon goes in front where the DOM allows it.
433
- if (typeof link.prepend === 'function') link.prepend(makeIcon(doc, screen.id));
434
- link.addEventListener('click', () => {
465
+ const areaList = navAreas(screens);
466
+ const areaNav = createAreaNav({
467
+ document: doc,
468
+ areas: areaList,
469
+ screens: screens.map((screen) => ({ id: screen.id, label: screen.label })),
470
+ memory: areaMemory,
471
+ linkClassName: 'bb-shell-nav-link',
472
+ iconFor: (area) => makeIcon(doc, areaList.find((entry) => entry.id === area.id)?.icon ?? area.id),
473
+ onSelect: (screenId) => {
435
474
  setMenuOpen(false);
436
- controller.show(screen.id);
437
- });
438
- links.set(screen.id, link);
439
- return link;
440
- };
441
- const layout = navLayout(screens);
442
- for (const screen of layout.leading) nav.append(navLink(screen));
443
- for (const { area, screens: members } of layout.areas) {
444
- if (!area.label) {
445
- for (const screen of members) nav.append(navLink(screen));
446
- continue;
447
- }
448
- const headId = `bb-shell-nav-area-${area.id}`;
449
- const group = makeElement(doc, 'div', { className: 'bb-shell-nav-group', attrs: { role: 'group', 'aria-labelledby': headId } });
450
- group.append(makeElement(doc, 'div', { className: 'bb-shell-nav-group-head', text: area.label, attrs: { id: headId } }));
451
- for (const screen of members) group.append(navLink(screen));
452
- nav.append(group);
453
- }
454
- for (const screen of layout.trailing) nav.append(navLink(screen));
475
+ controller.show(screenId);
476
+ },
477
+ });
478
+ nav.append(areaNav.nav);
455
479
  const source = makeElement(doc, 'section', { className: 'bb-shell-source', attrs: { 'aria-label': '出典' } });
456
480
  sidebar.append(brand, nav, source);
457
481
  const workspace = makeElement(doc, 'div', { className: 'bb-shell-workspace' });
458
482
  const mobileBar = makeElement(doc, 'div', { className: 'bb-shell-mobile-bar' });
459
483
  mobileBar.append(makeElement(doc, 'strong', { text: 'Brainbase' }), menuButton);
460
484
  const main = makeElement(doc, 'main', { className: 'bb-shell-main' });
485
+ main.append(areaNav.tabs);
461
486
  for (const screen of screens) {
462
487
  const slot = makeElement(doc, 'section', { className: 'bb-shell-screen', attrs: { 'data-screen': screen.id, 'aria-label': screen.label } });
463
488
  slot.hidden = true;
@@ -493,6 +518,8 @@ export function createLocalWebShell({
493
518
  if (mounted.has(screen.id)) {
494
519
  const view = mounted.get(screen.id);
495
520
  if (entityId && screen.id === 'graph' && typeof view?.openEntity === 'function') void view.openEntity(entityId);
521
+ // `#graph` without a record is the list: a record still open there closes (the browser's back).
522
+ if (!entityId && screen.id === 'graph' && view?.state?.detail && typeof view.closeEntity === 'function') view.closeEntity();
496
523
  if (entityId && screen.id === 'value-proofs' && typeof view?.openDecision === 'function') view.openDecision(entityId);
497
524
  if (entityId && screen.id === 'projects' && typeof view?.openProject === 'function') void view.openProject(entityId);
498
525
  return;
@@ -526,10 +553,7 @@ export function createLocalWebShell({
526
553
  : screen.id === 'projects' ? parsed.projectId ?? null : null);
527
554
  active = screen.id;
528
555
  for (const [screenId, slot] of slots) slot.hidden = screenId !== screen.id;
529
- for (const [screenId, link] of links) {
530
- if (screenId === screen.id) link.setAttribute('aria-current', 'page');
531
- else link.removeAttribute('aria-current');
532
- }
556
+ areaNav.show(screen.id);
533
557
  ensureMounted(screen, pendingEntityIds.get(screen.id));
534
558
  updateRail();
535
559
  return controller;
@@ -84,6 +84,9 @@ const SCOPE_LABELS = Object.freeze({
84
84
 
85
85
  const LIFECYCLE_STATUS_LABELS = Object.freeze({
86
86
  active: '有効',
87
+ decided: '決定済み',
88
+ active_reference: '参照用',
89
+ unknown: UNKNOWN,
87
90
  inactive: '無効',
88
91
  retired: '失効',
89
92
  superseded: '置換済み',
@@ -1480,7 +1483,8 @@ export function createKnowledgeOverviewController(options = {}) {
1480
1483
  const { root, api, onSelectProject } = options;
1481
1484
  if (!root || typeof root.replaceChildren !== 'function') throw new Error('knowledge_root_unavailable');
1482
1485
  const labels = resolveKnowledgeUiLabels(options.labels);
1483
- const itemsBase = optionalText(options.itemsPath).trim() || `/api/knowledge/${KNOWLEDGE_ROUTES.items}`;
1486
+ // A host path may already carry conditions (for example `?limit=100`); keep them and add the filters.
1487
+ const [itemsBase, itemsBaseQuery = ''] = (optionalText(options.itemsPath).trim() || `/api/knowledge/${KNOWLEDGE_ROUTES.items}`).split(/\?(.*)/s);
1484
1488
  const state = {
1485
1489
  filters: { q: '', scope: 'all', status: 'active' },
1486
1490
  list: { state: 'loading', records: null, absence_confirmed: false, searched_scope: null },
@@ -1491,7 +1495,7 @@ export function createKnowledgeOverviewController(options = {}) {
1491
1495
  const projectName = (code) => projectOptions(options.projects).find((option) => option.code === code)?.name ?? null;
1492
1496
 
1493
1497
  function itemsPath() {
1494
- const query = new URLSearchParams();
1498
+ const query = new URLSearchParams(itemsBaseQuery);
1495
1499
  if (state.filters.q) query.set('q', state.filters.q);
1496
1500
  if (state.filters.scope) query.set('scope', state.filters.scope);
1497
1501
  if (state.filters.status) query.set('status', state.filters.status);