@unson/brainbase-mcp 0.41.10 → 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.
- package/README.md +1 -1
- package/contracts/judgment-dag/digest.json +2 -2
- package/dist/judgment-history.js +76 -3
- package/dist/local-web-host.js +19 -2
- package/package.json +6 -2
- package/ui/area-nav.css +79 -0
- package/ui/area-nav.js +249 -0
- package/ui/foundation-overview.css +9 -0
- package/ui/foundation-overview.js +75 -5
- package/ui/graph-ontology-view.css +88 -0
- package/ui/graph-ontology-view.js +248 -0
- package/ui/graph-own-share.js +167 -0
- package/ui/graph-registry-view.css +272 -50
- package/ui/graph-registry-view.js +539 -456
- package/ui/graph-view-shared.css +32 -0
- package/ui/graph-view-shared.js +191 -12
- package/ui/judgment-history.css +155 -2
- package/ui/judgment-history.js +649 -241
- package/ui/local-web-shell.css +1 -13
- package/ui/local-web-shell.js +80 -40
- package/ui/meeting-minutes.css +87 -1
- package/ui/meeting-minutes.js +275 -30
- package/ui/outcome-knowledge.js +6 -2
- package/ui/people-and-companies-view.css +166 -15
- package/ui/people-and-companies-view.js +307 -93
- package/ui/world/world-canvas-notes.js +3 -2
- package/ui/world/world-view.css +19 -0
- package/ui/world/world-view.js +52 -10
- package/ui/world/world-work-rail.js +114 -8
package/README.md
CHANGED
|
@@ -105,7 +105,7 @@ brainbase onboard:candidates
|
|
|
105
105
|
|
|
106
106
|
### Google Workspace local-first adopter
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
常時稼働のローカルホスト(例: Mac mini)、Google Workspace、必要なら補助Gmail、許可したDrive/local folder、Calendar/notesに散在するタスクを想定した例です。使わないタスクツール(例: Notion)は`--inactive-task-tool`で非アクティブ扱いにできます。全Driveやhome directory全体は走査しません。
|
|
109
109
|
|
|
110
110
|
```bash
|
|
111
111
|
brainbase onboard:plan --profile google-workspace-local --host mac-mini --email google-workspace --secondary-email gmail --calendar google-calendar --drive google-drive --drive-folder "<folder-id>" --local-folder "/Users/owner/Notes" --tasks scattered-calendar-notes --inactive-task-tool notion
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"path": "README.md",
|
|
28
|
-
"sha256": "
|
|
28
|
+
"sha256": "26eacd2a057380b4fd386b2176bd6e790a41c0bb8d684857c53bb5cd947e2d28"
|
|
29
29
|
}
|
|
30
30
|
],
|
|
31
|
-
"digest": "
|
|
31
|
+
"digest": "38bf5aff53993b1ee737071ca7952d44e36ac2e5ed3a7adcac65fdd86e8954af"
|
|
32
32
|
}
|
package/dist/judgment-history.js
CHANGED
|
@@ -793,6 +793,79 @@ function groupFor(groups, sessionRef, key) {
|
|
|
793
793
|
groups.set(groupKey, group);
|
|
794
794
|
return group;
|
|
795
795
|
}
|
|
796
|
+
const FRAME_USE_ROLES = new Set(['constraint', 'criterion', 'prediction']);
|
|
797
|
+
const FRAME_USE_VERDICTS = new Set(['satisfied', 'violated', 'tension']);
|
|
798
|
+
const FRAME_PREDICTION_CHECKS = new Set(['held', 'failed', 'unchecked']);
|
|
799
|
+
/**
|
|
800
|
+
* The usage value shared by every writer of judgment history references
|
|
801
|
+
* (docs/specs/judgment-history-frame-basis.md): the role in the chosen option,
|
|
802
|
+
* then the constraint verdict or the prediction check.
|
|
803
|
+
*/
|
|
804
|
+
function frameUseUsage(use) {
|
|
805
|
+
const role = text(use.role);
|
|
806
|
+
if (!role || !FRAME_USE_ROLES.has(role))
|
|
807
|
+
return null;
|
|
808
|
+
if (role === 'prediction') {
|
|
809
|
+
const check = isObject(use.check) ? text(use.check.status) : null;
|
|
810
|
+
return `prediction:${check && FRAME_PREDICTION_CHECKS.has(check) ? check : 'unchecked'}`;
|
|
811
|
+
}
|
|
812
|
+
const verdict = text(use.verdict);
|
|
813
|
+
if (role === 'constraint')
|
|
814
|
+
return verdict && FRAME_USE_VERDICTS.has(verdict) ? `constraint:${verdict}` : null;
|
|
815
|
+
return verdict && FRAME_USE_VERDICTS.has(verdict) ? `criterion:${verdict}` : 'criterion';
|
|
816
|
+
}
|
|
817
|
+
/** Carry how the chosen option used each selected reference; rejected options never leak in. */
|
|
818
|
+
function frameReferencesWithUsage(frame) {
|
|
819
|
+
const references = frame.selected_references;
|
|
820
|
+
if (!Array.isArray(references))
|
|
821
|
+
return references;
|
|
822
|
+
const chosenLabel = text(frame.chosen_option);
|
|
823
|
+
const chosen = Array.isArray(frame.alternatives)
|
|
824
|
+
? frame.alternatives.find((option) => isObject(option) && chosenLabel !== null && option.label === chosenLabel)
|
|
825
|
+
: null;
|
|
826
|
+
const uses = isObject(chosen) && Array.isArray(chosen.uses) ? chosen.uses.filter(isObject) : [];
|
|
827
|
+
return references.map((reference) => {
|
|
828
|
+
if (!isObject(reference))
|
|
829
|
+
return reference;
|
|
830
|
+
const ref = text(reference.ref);
|
|
831
|
+
const use = ref ? uses.find((candidate) => text(candidate.ref) === ref) : undefined;
|
|
832
|
+
const usage = use ? frameUseUsage(use) : null;
|
|
833
|
+
return usage && reference.usage === undefined ? { ...reference, usage } : reference;
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Personal KG records the owner's answer applied in this turn. Only resolved
|
|
838
|
+
* answers count; their content never enters the history record.
|
|
839
|
+
*/
|
|
840
|
+
function personalKnowledgeReferences(events) {
|
|
841
|
+
const references = [];
|
|
842
|
+
const seen = new Set();
|
|
843
|
+
for (const event of events) {
|
|
844
|
+
if (event.success !== true || text(event.event_kind) !== 'personal_answer')
|
|
845
|
+
continue;
|
|
846
|
+
const metadata = isObject(event.safe_metadata) ? event.safe_metadata : null;
|
|
847
|
+
const answer = metadata && isObject(metadata.personal_answer) ? metadata.personal_answer : null;
|
|
848
|
+
if (!answer || text(answer.status) !== 'resolved' || !Array.isArray(answer.reference_ids))
|
|
849
|
+
continue;
|
|
850
|
+
for (const value of answer.reference_ids) {
|
|
851
|
+
const ref = safeId(value);
|
|
852
|
+
if (!ref || seen.has(ref))
|
|
853
|
+
continue;
|
|
854
|
+
seen.add(ref);
|
|
855
|
+
references.push({ ref, kind: 'personal_knowledge', version: null, digest: null, why: null, usage: 'applied', availability: 'unknown' });
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return references;
|
|
859
|
+
}
|
|
860
|
+
function withPersonalKnowledge(judgment, events) {
|
|
861
|
+
const personal = personalKnowledgeReferences(events);
|
|
862
|
+
if (personal.length === 0)
|
|
863
|
+
return judgment;
|
|
864
|
+
const existing = judgment.selected_references ?? [];
|
|
865
|
+
const known = new Set(existing.map((reference) => `${reference.kind ?? ''}\u0000${reference.ref}`));
|
|
866
|
+
const added = personal.filter((reference) => !known.has(`${reference.kind}\u0000${reference.ref}`));
|
|
867
|
+
return added.length === 0 ? judgment : { ...judgment, selected_references: [...existing, ...added] };
|
|
868
|
+
}
|
|
796
869
|
function sourceBlocks(value) {
|
|
797
870
|
if (!value)
|
|
798
871
|
return [];
|
|
@@ -808,7 +881,7 @@ function sourceBlocks(value) {
|
|
|
808
881
|
const frame = isObject(metadata.judgment_frame) ? metadata.judgment_frame : null;
|
|
809
882
|
if (frame)
|
|
810
883
|
blocks.push({
|
|
811
|
-
selected_references: frame
|
|
884
|
+
selected_references: frameReferencesWithUsage(frame),
|
|
812
885
|
alternatives: Array.isArray(frame.alternatives) ? frame.alternatives.map((option) => isObject(option) ? {
|
|
813
886
|
label: option.label,
|
|
814
887
|
evaluation: null,
|
|
@@ -1018,7 +1091,7 @@ function normalizeFinalRecord(final, sessionRef, key, configuredEntrypoint, turn
|
|
|
1018
1091
|
recorded_at: recordedAt,
|
|
1019
1092
|
project_code: projectCode,
|
|
1020
1093
|
turn_ref: `${sessionRef}/${sha256(turnId)}`,
|
|
1021
|
-
judgment: mergeJudgment(judgment, blocks, field(blocks[0] ?? null, 'missing_fields')),
|
|
1094
|
+
judgment: withPersonalKnowledge(mergeJudgment(judgment, blocks, field(blocks[0] ?? null, 'missing_fields')), events),
|
|
1022
1095
|
execution: mergeExecution({ ...execution, status: finalExecutionStatus }, [...executionBlocks(final), ...events.flatMap((event) => executionBlocks(event)).reverse()], completed)
|
|
1023
1096
|
};
|
|
1024
1097
|
return { ...baseRecord, missing_fields: missingFields(baseRecord, firstPresent(blocks, 'missing_fields')) };
|
|
@@ -1034,7 +1107,7 @@ function mergeRecordWithFinal(adoptionRecord, final, sessionRef, key, turnInput,
|
|
|
1034
1107
|
recorded_at: projected.recorded_at,
|
|
1035
1108
|
project_code: adoptionRecord.project_code ?? projected.project_code,
|
|
1036
1109
|
entrypoint: adoptionRecord.entrypoint === 'unknown' ? projected.entrypoint : adoptionRecord.entrypoint,
|
|
1037
|
-
judgment: mergeJudgment(adoptionRecord.judgment, blocks, undefined),
|
|
1110
|
+
judgment: withPersonalKnowledge(mergeJudgment(adoptionRecord.judgment, blocks, undefined), events),
|
|
1038
1111
|
execution
|
|
1039
1112
|
};
|
|
1040
1113
|
return { ...baseRecord, missing_fields: missingFields(baseRecord, firstPresent(blocks, 'missing_fields')) };
|
package/dist/local-web-host.js
CHANGED
|
@@ -52,7 +52,7 @@ export const LOCAL_WEB_JUDGMENT_HISTORY_PREFIX = JUDGMENT_HISTORY_HTTP_PREFIX;
|
|
|
52
52
|
export const LOCAL_WEB_FOUNDATION_BODY_LIMIT_BYTES = 64 * 1024;
|
|
53
53
|
export const LOCAL_WEB_PROVENANCE_SOURCE_ID = 'brainbase-local-web';
|
|
54
54
|
const CANONICAL_FILES = ['graph.json', 'relationships.json', 'personal-kg.jsonl', 'decisions.jsonl'];
|
|
55
|
-
const BASE_UI_FILES = ['brainbase-tokens.css', 'local-web-shell.js', 'local-web-shell.css', 'workspace-kit.js', 'workspace-kit.css', 'native-scrollbars.js', 'native-scrollbars.css'];
|
|
55
|
+
const BASE_UI_FILES = ['brainbase-tokens.css', 'local-web-shell.js', 'local-web-shell.css', 'workspace-kit.js', 'workspace-kit.css', 'native-scrollbars.js', 'native-scrollbars.css', 'area-nav.js', 'area-nav.css'];
|
|
56
56
|
const CONTENT_TYPES = Object.freeze({
|
|
57
57
|
'.js': 'text/javascript; charset=utf-8',
|
|
58
58
|
'.css': 'text/css; charset=utf-8'
|
|
@@ -883,6 +883,8 @@ export function createGraphWebModule(context) {
|
|
|
883
883
|
'project-graph-vendor.js',
|
|
884
884
|
'graph-registry-view.js',
|
|
885
885
|
'graph-registry-view.css',
|
|
886
|
+
'graph-ontology-view.js',
|
|
887
|
+
'graph-ontology-view.css',
|
|
886
888
|
'people-and-companies-view.js',
|
|
887
889
|
'people-and-companies-view.css'
|
|
888
890
|
],
|
|
@@ -938,10 +940,25 @@ const meta = (name) => document.querySelector(\`meta[name="\${name}"]\`)?.getAtt
|
|
|
938
940
|
// Keep the query portion intact; the shell parses the Graph entity query and
|
|
939
941
|
// only treats the entity id as a target of the Graph screen.
|
|
940
942
|
const screenFromHash = () => location.hash.replace(/^#/, '') || undefined;
|
|
941
|
-
|
|
943
|
+
// A screen that opened its own address (a record of 情報と関係) goes back with the
|
|
944
|
+
// browser's history, so 「一覧に戻る」 and the back button land on the same list.
|
|
945
|
+
// Each entry remembers how many records deep it is, which stays right when the
|
|
946
|
+
// owner also uses the browser's back and forward.
|
|
947
|
+
const depth = () => (Number.isInteger(history.state?.bbDepth) ? history.state.bbDepth : 0);
|
|
948
|
+
let shell = null;
|
|
949
|
+
const navigate = (target, { back = false } = {}) => {
|
|
950
|
+
if (back && depth() > 0) {
|
|
951
|
+
history.go(-depth());
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
history.pushState({ bbDepth: back ? 0 : depth() + 1 }, '', \`#\${target}\`);
|
|
955
|
+
shell?.show(target);
|
|
956
|
+
};
|
|
957
|
+
shell = createLocalWebShell({
|
|
942
958
|
root: document.getElementById('brainbase-local-web'),
|
|
943
959
|
token: meta('brainbase-web-token'),
|
|
944
960
|
initialScreen: screenFromHash(),
|
|
961
|
+
onNavigate: navigate,
|
|
945
962
|
meetingMinutesStorage: ${JSON.stringify(meetingMinutesStorage ?? null)},
|
|
946
963
|
meetingMinutesLineage: ${JSON.stringify(meetingMinutesLineage ?? null)},
|
|
947
964
|
screens: ${leading ? `[${leading}, ...LOCAL_WEB_SCREENS${trailing ? `, ${trailing}` : ''}]` : '[...LOCAL_WEB_SCREENS, ...extensionScreens]'}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unson/brainbase-mcp",
|
|
3
|
-
"version": "0.41.
|
|
3
|
+
"version": "0.41.13",
|
|
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",
|
|
@@ -328,6 +328,8 @@
|
|
|
328
328
|
"./ui/world-model-view.css": "./ui/world-model-view.css",
|
|
329
329
|
"./ui/people-and-companies-view": "./ui/people-and-companies-view.js",
|
|
330
330
|
"./ui/people-and-companies-view.css": "./ui/people-and-companies-view.css",
|
|
331
|
+
"./ui/area-nav": "./ui/area-nav.js",
|
|
332
|
+
"./ui/area-nav.css": "./ui/area-nav.css",
|
|
331
333
|
"./ui/local-web-shell": "./ui/local-web-shell.js",
|
|
332
334
|
"./ui/local-web-shell.css": "./ui/local-web-shell.css",
|
|
333
335
|
"./ui/workspace-kit": "./ui/workspace-kit.js",
|
|
@@ -342,6 +344,8 @@
|
|
|
342
344
|
"./ui/graph-projects-view.css": "./ui/graph-projects-view.css",
|
|
343
345
|
"./ui/graph-registry-view": "./ui/graph-registry-view.js",
|
|
344
346
|
"./ui/graph-registry-view.css": "./ui/graph-registry-view.css",
|
|
347
|
+
"./ui/graph-ontology-view": "./ui/graph-ontology-view.js",
|
|
348
|
+
"./ui/graph-ontology-view.css": "./ui/graph-ontology-view.css",
|
|
345
349
|
"./ui/meeting-minutes-storage": "./ui/meeting-minutes-storage.js",
|
|
346
350
|
"./ui/meeting-minutes-storage.css": "./ui/meeting-minutes-storage.css",
|
|
347
351
|
"./ui/brainbase-tokens.css": "./ui/brainbase-tokens.css",
|
|
@@ -483,5 +487,5 @@
|
|
|
483
487
|
"engines": {
|
|
484
488
|
"node": ">=20"
|
|
485
489
|
},
|
|
486
|
-
"gitHead": "
|
|
490
|
+
"gitHead": "fcfea3958913012579029adf0bf785c45643c77b"
|
|
487
491
|
}
|
package/ui/area-nav.css
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Area navigation (ledger D10, stage 2): the area links on the left and the
|
|
3
|
+
* tabs of the current area above the page. Uses only the shared tokens in
|
|
4
|
+
* brainbase-tokens.css; a host gives the area links its own look through
|
|
5
|
+
* `linkClassName`.
|
|
6
|
+
*/
|
|
7
|
+
.bb-area-nav {
|
|
8
|
+
display: grid;
|
|
9
|
+
gap: 2px;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.bb-area-link[hidden],
|
|
13
|
+
.bb-area-tabs[hidden] {
|
|
14
|
+
display: none;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.bb-area-link-label {
|
|
18
|
+
min-width: 0;
|
|
19
|
+
flex: 1 1 auto;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.bb-area-count {
|
|
23
|
+
min-width: 20px;
|
|
24
|
+
flex: 0 0 auto;
|
|
25
|
+
border-radius: 999px;
|
|
26
|
+
padding: 0 6px;
|
|
27
|
+
background: var(--bb-color-accent-soft);
|
|
28
|
+
color: var(--bb-color-accent);
|
|
29
|
+
font-size: 11px;
|
|
30
|
+
font-weight: 700;
|
|
31
|
+
line-height: 18px;
|
|
32
|
+
text-align: center;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.bb-area-tabs {
|
|
36
|
+
display: flex;
|
|
37
|
+
gap: var(--bb-space-1);
|
|
38
|
+
overflow-x: auto;
|
|
39
|
+
border-bottom: 1px solid var(--bb-color-border);
|
|
40
|
+
padding: 0 var(--bb-space-5);
|
|
41
|
+
background: var(--bb-color-paper);
|
|
42
|
+
scrollbar-width: none;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.bb-area-tab {
|
|
46
|
+
min-height: 44px;
|
|
47
|
+
display: inline-flex;
|
|
48
|
+
flex: 0 0 auto;
|
|
49
|
+
align-items: center;
|
|
50
|
+
gap: var(--bb-space-2);
|
|
51
|
+
border-bottom: 2px solid transparent;
|
|
52
|
+
padding: 0 var(--bb-space-3);
|
|
53
|
+
color: var(--bb-color-muted);
|
|
54
|
+
font-size: 13px;
|
|
55
|
+
font-weight: 620;
|
|
56
|
+
text-decoration: none;
|
|
57
|
+
white-space: nowrap;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.bb-area-tab:hover {
|
|
61
|
+
color: var(--bb-color-ink);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.bb-area-tab[aria-current="page"] {
|
|
65
|
+
border-bottom-color: var(--bb-color-accent);
|
|
66
|
+
color: var(--bb-color-accent);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.bb-area-tab:focus-visible,
|
|
70
|
+
.bb-area-link:focus-visible {
|
|
71
|
+
outline: 2px solid var(--bb-color-accent);
|
|
72
|
+
outline-offset: -2px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@media (max-width: 760px) {
|
|
76
|
+
.bb-area-tabs {
|
|
77
|
+
padding: 0 var(--bb-space-4);
|
|
78
|
+
}
|
|
79
|
+
}
|
package/ui/area-nav.js
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Area navigation shared by OSS Personal and the organization edition
|
|
3
|
+
* (brainbase-project ledger D10, stage 2).
|
|
4
|
+
*
|
|
5
|
+
* The left side lists only the areas; the screens of the current area are tabs
|
|
6
|
+
* above the page. An area link opens the screen last used in that area, which
|
|
7
|
+
* is remembered in the viewer's browser only (a display preference, not a fact
|
|
8
|
+
* of the organization). Every area link and tab is a link to a screen, so
|
|
9
|
+
* screen addresses never change. The part draws links only: showing and
|
|
10
|
+
* loading screens stays with the caller (`show` in, `onSelect` out).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const AREA_NAV_CONTRACT_VERSION = 'brainbase.area-nav.v1';
|
|
14
|
+
export const AREA_NAV_MEMORY_KEY = 'brainbase.area-nav.last-screen.v1';
|
|
15
|
+
|
|
16
|
+
function makeElement(doc, tag, { className, text, attrs = {} } = {}) {
|
|
17
|
+
const element = doc.createElement(tag);
|
|
18
|
+
if (className) element.className = className;
|
|
19
|
+
if (text !== undefined && text !== null) element.textContent = String(text);
|
|
20
|
+
for (const [name, value] of Object.entries(attrs)) {
|
|
21
|
+
if (value === undefined || value === null || value === false) continue;
|
|
22
|
+
element.setAttribute(name, value === true ? '' : String(value));
|
|
23
|
+
}
|
|
24
|
+
return element;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The last screen per area, kept in the given storage (for example the
|
|
29
|
+
* browser's localStorage). A storage that is missing, full or blocked behaves
|
|
30
|
+
* as if nothing was remembered.
|
|
31
|
+
*/
|
|
32
|
+
export function createAreaMemory(storage, key = AREA_NAV_MEMORY_KEY) {
|
|
33
|
+
const read = () => {
|
|
34
|
+
try {
|
|
35
|
+
const raw = storage?.getItem?.(key);
|
|
36
|
+
const value = raw ? JSON.parse(raw) : null;
|
|
37
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
38
|
+
} catch {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
return Object.freeze({
|
|
43
|
+
get(areaId) {
|
|
44
|
+
const value = read()[areaId];
|
|
45
|
+
return typeof value === 'string' ? value : null;
|
|
46
|
+
},
|
|
47
|
+
set(areaId, screenId) {
|
|
48
|
+
try {
|
|
49
|
+
storage?.setItem?.(key, JSON.stringify({ ...read(), [areaId]: screenId }));
|
|
50
|
+
} catch {
|
|
51
|
+
// Not remembered; the area opens its default screen next time.
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The browser's localStorage when it can be reached, otherwise null. */
|
|
58
|
+
export function browserAreaStorage() {
|
|
59
|
+
try {
|
|
60
|
+
return globalThis.localStorage ?? null;
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The screen an area link opens: the remembered screen if it is still shown
|
|
68
|
+
* there, else the area's default, else its first shown screen. External links
|
|
69
|
+
* (opened in another tab) are never a target.
|
|
70
|
+
*/
|
|
71
|
+
export function resolveAreaTarget(area, { remembered = null, isShown = () => true } = {}) {
|
|
72
|
+
const candidates = area.screens.filter((screen) => !screen.external && isShown(screen.id)).map((screen) => screen.id);
|
|
73
|
+
if (remembered && candidates.includes(remembered)) return remembered;
|
|
74
|
+
if (area.defaultScreen && candidates.includes(area.defaultScreen)) return area.defaultScreen;
|
|
75
|
+
return candidates[0] ?? null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {object} options
|
|
80
|
+
* @param {Document} options.document
|
|
81
|
+
* @param {Array<{id: string, label: string|null, defaultScreen?: string, screenIds: string[]}>} options.areas
|
|
82
|
+
* In menu order. A null label takes the label of the area's only screen.
|
|
83
|
+
* @param {Array<{id: string, label: string, href?: string, external?: boolean, hidden?: boolean}>} options.screens
|
|
84
|
+
* @param {{get(areaId: string): string|null, set(areaId: string, screenId: string): void}} [options.memory]
|
|
85
|
+
* @param {(screenId: string) => string} [options.hrefFor] Address of a screen (default `#<id>`).
|
|
86
|
+
* @param {(screenId: string, event: Event) => void} [options.onSelect]
|
|
87
|
+
* @param {(area: object) => Node|null} [options.iconFor] Icon in front of an area link.
|
|
88
|
+
* @param {string} [options.linkClassName] Extra class for the area links (the host's look).
|
|
89
|
+
*/
|
|
90
|
+
export function createAreaNav({
|
|
91
|
+
document: doc,
|
|
92
|
+
areas: areaInput,
|
|
93
|
+
screens: screenInput,
|
|
94
|
+
memory = createAreaMemory(null),
|
|
95
|
+
hrefFor = (screenId) => `#${screenId}`,
|
|
96
|
+
onSelect,
|
|
97
|
+
iconFor,
|
|
98
|
+
linkClassName = '',
|
|
99
|
+
label = '画面',
|
|
100
|
+
} = {}) {
|
|
101
|
+
if (!doc || typeof doc.createElement !== 'function') throw new Error('document_unavailable');
|
|
102
|
+
const screens = new Map((screenInput ?? []).map((screen) => [screen.id, {
|
|
103
|
+
id: screen.id,
|
|
104
|
+
label: screen.label,
|
|
105
|
+
href: screen.href ?? null,
|
|
106
|
+
external: screen.external === true,
|
|
107
|
+
hidden: screen.hidden === true,
|
|
108
|
+
count: null,
|
|
109
|
+
waiting: false,
|
|
110
|
+
}]));
|
|
111
|
+
const areas = (areaInput ?? []).map((area) => ({
|
|
112
|
+
id: area.id,
|
|
113
|
+
label: area.label ?? null,
|
|
114
|
+
defaultScreen: area.defaultScreen ?? null,
|
|
115
|
+
screens: area.screenIds.map((id) => screens.get(id)).filter(Boolean),
|
|
116
|
+
})).filter((area) => area.screens.length > 0);
|
|
117
|
+
const areaByScreen = new Map();
|
|
118
|
+
for (const area of areas) for (const screen of area.screens) areaByScreen.set(screen.id, area);
|
|
119
|
+
|
|
120
|
+
let activeScreenId = null;
|
|
121
|
+
const isShown = (screenId) => screens.get(screenId)?.hidden === false;
|
|
122
|
+
const shownScreens = (area) => area.screens.filter((screen) => isShown(screen.id));
|
|
123
|
+
const targetFor = (areaId) => {
|
|
124
|
+
const area = areas.find((candidate) => candidate.id === areaId);
|
|
125
|
+
return area ? resolveAreaTarget(area, { remembered: memory.get(area.id), isShown }) : null;
|
|
126
|
+
};
|
|
127
|
+
const screenHref = (screen) => screen.href ?? hrefFor(screen.id);
|
|
128
|
+
const areaLabel = (area) => area.label ?? area.screens[0].label;
|
|
129
|
+
|
|
130
|
+
const countBadge = (count, ariaLabel) => makeElement(doc, 'span', {
|
|
131
|
+
className: 'bb-area-count',
|
|
132
|
+
text: count,
|
|
133
|
+
attrs: { 'aria-label': ariaLabel },
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const nav = makeElement(doc, 'nav', { className: 'bb-area-nav', attrs: { 'aria-label': label, 'data-contract-version': AREA_NAV_CONTRACT_VERSION } });
|
|
137
|
+
const tabs = makeElement(doc, 'nav', { className: 'bb-area-tabs' });
|
|
138
|
+
tabs.hidden = true;
|
|
139
|
+
const areaLinks = new Map();
|
|
140
|
+
|
|
141
|
+
for (const area of areas) {
|
|
142
|
+
const link = makeElement(doc, 'a', {
|
|
143
|
+
className: ['bb-area-link', linkClassName].filter(Boolean).join(' '),
|
|
144
|
+
attrs: { 'data-area': area.id },
|
|
145
|
+
});
|
|
146
|
+
link.addEventListener('click', (event) => {
|
|
147
|
+
const target = targetFor(area.id);
|
|
148
|
+
if (target) onSelect?.(target, event);
|
|
149
|
+
});
|
|
150
|
+
areaLinks.set(area.id, link);
|
|
151
|
+
nav.append(link);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function renderAreaLink(area) {
|
|
155
|
+
const link = areaLinks.get(area.id);
|
|
156
|
+
const shown = shownScreens(area);
|
|
157
|
+
link.hidden = !shown.some((screen) => !screen.external);
|
|
158
|
+
const target = targetFor(area.id);
|
|
159
|
+
if (target) link.setAttribute('href', screenHref(screens.get(target)));
|
|
160
|
+
else link.removeAttribute('href');
|
|
161
|
+
if (areaByScreen.get(activeScreenId) === area) link.setAttribute('aria-current', 'page');
|
|
162
|
+
else link.removeAttribute('aria-current');
|
|
163
|
+
const waiting = shown.reduce((sum, screen) => sum + (screen.waiting && Number.isInteger(screen.count) ? screen.count : 0), 0);
|
|
164
|
+
const icon = iconFor?.(area) ?? null;
|
|
165
|
+
link.replaceChildren(...[
|
|
166
|
+
icon,
|
|
167
|
+
makeElement(doc, 'span', { className: 'bb-area-link-label', text: areaLabel(area) }),
|
|
168
|
+
waiting > 0 ? countBadge(waiting, `待っているもの${waiting}件`) : null,
|
|
169
|
+
].filter(Boolean));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function renderTabs() {
|
|
173
|
+
const area = areaByScreen.get(activeScreenId) ?? null;
|
|
174
|
+
const shown = area ? shownScreens(area) : [];
|
|
175
|
+
if (!area || shown.length < 2) {
|
|
176
|
+
tabs.hidden = true;
|
|
177
|
+
tabs.replaceChildren();
|
|
178
|
+
tabs.removeAttribute('aria-label');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
tabs.hidden = false;
|
|
182
|
+
tabs.setAttribute('aria-label', `${areaLabel(area)}の画面`);
|
|
183
|
+
tabs.replaceChildren(...shown.map((screen) => {
|
|
184
|
+
const tab = makeElement(doc, 'a', {
|
|
185
|
+
className: 'bb-area-tab',
|
|
186
|
+
attrs: {
|
|
187
|
+
href: screenHref(screen),
|
|
188
|
+
'data-screen-tab': screen.id,
|
|
189
|
+
'aria-current': screen.id === activeScreenId ? 'page' : null,
|
|
190
|
+
target: screen.external ? '_blank' : null,
|
|
191
|
+
rel: screen.external ? 'noopener' : null,
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
tab.append(makeElement(doc, 'span', { text: screen.label }));
|
|
195
|
+
if (Number.isInteger(screen.count)) {
|
|
196
|
+
tab.append(countBadge(screen.count, screen.waiting ? `待っているもの${screen.count}件` : `${screen.count}件`));
|
|
197
|
+
}
|
|
198
|
+
if (!screen.external) tab.addEventListener('click', (event) => onSelect?.(screen.id, event));
|
|
199
|
+
return tab;
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function render() {
|
|
204
|
+
for (const area of areas) renderAreaLink(area);
|
|
205
|
+
renderTabs();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const controller = {
|
|
209
|
+
nav,
|
|
210
|
+
tabs,
|
|
211
|
+
get state() {
|
|
212
|
+
const area = areaByScreen.get(activeScreenId) ?? null;
|
|
213
|
+
return { activeScreenId, activeAreaId: area?.id ?? null };
|
|
214
|
+
},
|
|
215
|
+
areaOf(screenId) {
|
|
216
|
+
return areaByScreen.get(screenId)?.id ?? null;
|
|
217
|
+
},
|
|
218
|
+
targetFor,
|
|
219
|
+
/** Marks the shown screen; a screen in an area becomes that area's last screen. */
|
|
220
|
+
show(screenId) {
|
|
221
|
+
activeScreenId = screenId;
|
|
222
|
+
const area = areaByScreen.get(screenId);
|
|
223
|
+
const screen = screens.get(screenId);
|
|
224
|
+
if (area && screen && !screen.external && isShown(screenId)) memory.set(area.id, screenId);
|
|
225
|
+
render();
|
|
226
|
+
return controller;
|
|
227
|
+
},
|
|
228
|
+
setHidden(screenId, hidden) {
|
|
229
|
+
const screen = screens.get(screenId);
|
|
230
|
+
if (!screen) return controller;
|
|
231
|
+
screen.hidden = hidden === true;
|
|
232
|
+
render();
|
|
233
|
+
return controller;
|
|
234
|
+
},
|
|
235
|
+
/** `count` null removes it. `waiting` counts are added up on the area link too. */
|
|
236
|
+
setCount(screenId, count, { waiting = false } = {}) {
|
|
237
|
+
const screen = screens.get(screenId);
|
|
238
|
+
if (!screen) return controller;
|
|
239
|
+
screen.count = Number.isInteger(count) && count >= 0 ? count : null;
|
|
240
|
+
screen.waiting = waiting === true;
|
|
241
|
+
render();
|
|
242
|
+
return controller;
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
render();
|
|
246
|
+
return controller;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export default createAreaNav;
|
|
@@ -248,3 +248,12 @@
|
|
|
248
248
|
.bb-fov-philosophy-card .bb-fov-original { border-top: 0; padding-top: 0; }
|
|
249
249
|
.bb-fov-model-card .bb-fov-original { margin-top: 12px; padding-top: 10px; overflow-wrap: anywhere; }
|
|
250
250
|
.bb-fov-objective-ledger .bb-ws-ledger-row:focus-visible { outline: 2px solid var(--bb-color-focus); outline-offset: 2px; }
|
|
251
|
+
/* Whose what an objective waits for, and what can be done with it (story-objective-row-actions-v1). */
|
|
252
|
+
.bb-fov-waiting { display: grid; gap: 2px; min-width: 0; }
|
|
253
|
+
.bb-fov-waiting strong { font-weight: 600; }
|
|
254
|
+
.bb-fov-waiting small { color: var(--bb-color-muted); font-size: 12px; line-height: 1.4; }
|
|
255
|
+
.bb-fov-ledger-hint { margin: 8px 0 0; color: var(--bb-color-muted); font-size: 12px; }
|
|
256
|
+
.bb-fov-actions { display: grid; gap: 8px; justify-items: start; padding: 12px; border: 1px solid var(--bb-color-border); border-radius: 8px; background: var(--bb-color-surface-subtle, rgb(0 0 0 / 2%)); }
|
|
257
|
+
.bb-fov-actions h3 { margin: 0; }
|
|
258
|
+
.bb-fov-action-note { margin: 0; color: var(--bb-color-muted); font-size: 13px; }
|
|
259
|
+
.bb-fov-action-note.is-unavailable { color: var(--bb-color-ink); }
|
|
@@ -507,13 +507,79 @@ function objectiveCard(doc, record, variableMap) {
|
|
|
507
507
|
return card;
|
|
508
508
|
}
|
|
509
509
|
|
|
510
|
-
|
|
510
|
+
/**
|
|
511
|
+
* Whose what an objective waits for (story-objective-row-actions-v1), from its own record: the items its
|
|
512
|
+
* current state says were not confirmed when it was saved, or else the fields it has not recorded, and
|
|
513
|
+
* the accountable person who would settle them. Nothing is waited for when the record names nothing.
|
|
514
|
+
*/
|
|
515
|
+
export function objectiveWaiting(record) {
|
|
516
|
+
const definition = recordDefinition(record);
|
|
517
|
+
const current = currentStateFor(record);
|
|
518
|
+
const accountable = nonEmpty(definition.accountableId);
|
|
519
|
+
const items = current.missing.length ? current.missing : missingObjectiveFields(record);
|
|
520
|
+
if (items.length === 0) return null;
|
|
521
|
+
const what = `${current.missing.length ? '未確認' : '未記録'}:${items.join('・')}`;
|
|
522
|
+
const who = accountable ? '責任者の確認待ち' : '責任者が未記録のため、確かめる人が決まっていません';
|
|
523
|
+
return { what, who, items, kind: current.missing.length ? 'unconfirmed' : 'unrecorded', accountableRecorded: Boolean(accountable) };
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function waitingElement(doc, waiting) {
|
|
527
|
+
const cell = makeElement(doc, 'span', { className: 'bb-fov-waiting' });
|
|
528
|
+
if (!waiting) {
|
|
529
|
+
cell.append(makeElement(doc, 'span', { className: 'bb-fov-missing', text: '記録上はありません' }));
|
|
530
|
+
return cell;
|
|
531
|
+
}
|
|
532
|
+
cell.append(makeElement(doc, 'strong', { className: 'bb-fov-missing', text: waiting.what }), makeElement(doc, 'small', { text: waiting.who }));
|
|
533
|
+
return cell;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* What the reader can do with this objective, from the host (`callbacks.objectiveAction`): a button that
|
|
538
|
+
* goes where it is corrected, or the reason it cannot be from here. Without a host action the screen says
|
|
539
|
+
* it only reads; it never shows a control that does nothing.
|
|
540
|
+
*/
|
|
541
|
+
function objectiveActionElement(doc, record, recordContexts, callbacks, dialog) {
|
|
542
|
+
const definition = recordDefinition(record), details = detailsFor(record);
|
|
543
|
+
const part = makeElement(doc, 'section', { className: 'bb-fov-actions' });
|
|
544
|
+
part.append(makeElement(doc, 'h3', { text: 'この目的でできること' }));
|
|
545
|
+
const context = recordContexts?.get(record);
|
|
546
|
+
const action = typeof callbacks?.objectiveAction === 'function'
|
|
547
|
+
? callbacks.objectiveAction({
|
|
548
|
+
objectiveId: definition.id ?? null,
|
|
549
|
+
revision: definition.revision ?? null,
|
|
550
|
+
title: formatAny(details.title) ?? formatAny(definition.meaning) ?? null,
|
|
551
|
+
scopeIds: context?.scopeIds ?? [],
|
|
552
|
+
record,
|
|
553
|
+
})
|
|
554
|
+
: null;
|
|
555
|
+
if (action && typeof action.onSelect === 'function') {
|
|
556
|
+
part.append(workspaceButton(doc, {
|
|
557
|
+
text: `${action.label ?? '目的を直す・現状を記録する'} ›`,
|
|
558
|
+
variant: 'primary',
|
|
559
|
+
onClick: () => { dialog()?.close(); action.onSelect(); },
|
|
560
|
+
}));
|
|
561
|
+
part.append(makeElement(doc, 'p', { className: 'bb-fov-action-note', text: action.description ?? '目指す状態・評価基準・現状を直し、新しい版として保存します。保存すると、この一覧の「待っていること」が変わります。' }));
|
|
562
|
+
} else {
|
|
563
|
+
part.append(makeElement(doc, 'p', {
|
|
564
|
+
className: 'bb-fov-action-note is-unavailable',
|
|
565
|
+
text: nonEmpty(action?.unavailable) ?? 'この画面は読むだけです。ここからは記録を変えられません。',
|
|
566
|
+
}));
|
|
567
|
+
}
|
|
568
|
+
return part;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function objectiveReading(doc, record, variableMap, recordContexts, callbacks = {}, dialog = () => null) {
|
|
511
572
|
const definition = recordDefinition(record), details = detailsFor(record);
|
|
512
573
|
const body = makeElement(doc, 'div', { className: 'bb-fov-reading' });
|
|
513
574
|
const section = (title, content) => {
|
|
514
575
|
const part = makeElement(doc, 'section');
|
|
515
576
|
part.append(makeElement(doc, 'h3', { text: title }), content); body.append(part);
|
|
516
577
|
};
|
|
578
|
+
const waiting = objectiveWaiting(record);
|
|
579
|
+
section('待っていること', waiting
|
|
580
|
+
? workspaceDefinition(doc, [['何を', waiting.what], ['誰が', waiting.who]])
|
|
581
|
+
: makeElement(doc, 'p', { text: '記録上、待っていることはありません。達成・採用・検証済みを意味しません。' }));
|
|
582
|
+
body.append(objectiveActionElement(doc, record, recordContexts, callbacks, dialog));
|
|
517
583
|
section('達成条件', workspaceDefinition(doc, [
|
|
518
584
|
['目指す状態', formatAny(definition.desiredState)],
|
|
519
585
|
['評価基準', criteriaElement(doc, definition, details, variableMap)],
|
|
@@ -560,18 +626,21 @@ function renderObjectives(doc, catalog, viewState, callbacks, recordContexts) {
|
|
|
560
626
|
viewState.dialog?.close();
|
|
561
627
|
viewState.dialog = workspaceDialog(doc, {
|
|
562
628
|
title: formatAny(details.title) ?? formatAny(definition.meaning) ?? UNNAMED_RECORD, opener,
|
|
563
|
-
content: objectiveReading(doc, record, recordContexts?.get(record)?.variableMap ?? variableMap, recordContexts),
|
|
629
|
+
content: objectiveReading(doc, record, recordContexts?.get(record)?.variableMap ?? variableMap, recordContexts, callbacks, () => viewState.dialog),
|
|
564
630
|
});
|
|
565
631
|
},
|
|
566
632
|
cells: [
|
|
567
633
|
{ primary: formatAny(details.title) ?? formatAny(definition.meaning) ?? UNNAMED_RECORD },
|
|
568
634
|
current.text ?? current.status ?? '未記録',
|
|
569
|
-
|
|
635
|
+
waitingElement(doc, objectiveWaiting(record)),
|
|
570
636
|
formatDate(current.asOf),
|
|
571
637
|
],
|
|
572
638
|
};
|
|
573
639
|
});
|
|
574
|
-
section.append(workspaceLedger(doc, { className: 'bb-fov-objective-ledger', ariaLabel: '登録された目的', columns: ['目的', '現状', '
|
|
640
|
+
section.append(workspaceLedger(doc, { className: 'bb-fov-objective-ledger', ariaLabel: '登録された目的', columns: ['目的', '現状', '待っていること', '確認日'], rows }));
|
|
641
|
+
section.append(makeElement(doc, 'p', { className: 'bb-fov-ledger-hint', text: typeof callbacks?.objectiveAction === 'function'
|
|
642
|
+
? '行を押すと、目的の詳細と、この目的でできること(直す・現状を記録する)が開きます。'
|
|
643
|
+
: '行を押すと、目的の詳細が開きます。この画面は読むだけです。' }));
|
|
575
644
|
return section;
|
|
576
645
|
}
|
|
577
646
|
|
|
@@ -912,13 +981,14 @@ export function renderFoundationCollectionOverview(root, sources, {
|
|
|
912
981
|
if (existing) {
|
|
913
982
|
const context = contexts.get(existing);
|
|
914
983
|
if (!context.sources.includes(source.label)) context.sources.push(source.label);
|
|
984
|
+
if (!context.scopeIds.includes(source.scopeId)) context.scopeIds.push(source.scopeId);
|
|
915
985
|
context.label = `${shared ? '組織共通 / ' : ''}${context.sources.join('・')}`;
|
|
916
986
|
continue;
|
|
917
987
|
}
|
|
918
988
|
const record = { ...original };
|
|
919
989
|
seen.set(key, record);
|
|
920
990
|
catalog[kind].push(record);
|
|
921
|
-
contexts.set(record, { key, variableMap, sources: [source.label], label: `${shared ? '組織共通 / ' : ''}${source.label}` });
|
|
991
|
+
contexts.set(record, { key, variableMap, sources: [source.label], scopeIds: [source.scopeId], label: `${shared ? '組織共通 / ' : ''}${source.label}` });
|
|
922
992
|
}
|
|
923
993
|
}
|
|
924
994
|
}
|