@unson/brainbase-mcp 0.15.4 → 0.15.5
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.
|
@@ -130,6 +130,7 @@ export function projectOrganizationGraph(records, owner) {
|
|
|
130
130
|
...(type === 'project' && summary ? { goal: summary } : {}),
|
|
131
131
|
...(type === 'project' && status ? { status } : {}),
|
|
132
132
|
...(type === 'project' && text(payload.kind) ? { kind: text(payload.kind) } : {}),
|
|
133
|
+
...(type === 'decision' && text(payload.decided_at) ? { decided_at: text(payload.decided_at) } : {}),
|
|
133
134
|
...(type === 'project' ? projectPlacement(record, payload, catalogByCode) : {})
|
|
134
135
|
}
|
|
135
136
|
});
|
|
@@ -74,6 +74,15 @@ export interface WorldBusiness {
|
|
|
74
74
|
/** Repository names the Graph registers for this project (`metadata.repositories`). */
|
|
75
75
|
readonly repositories: readonly string[];
|
|
76
76
|
readonly engagements: readonly WorldEngagement[];
|
|
77
|
+
/** How much happened lately: decisions of this business (or its engagements) in the last 30 days. */
|
|
78
|
+
readonly activity: WorldBusinessActivity;
|
|
79
|
+
}
|
|
80
|
+
export interface WorldBusinessActivity {
|
|
81
|
+
readonly window_days: 30;
|
|
82
|
+
/** Decisions decided in the window, belonging to this business by scope code or a `governs` edge. */
|
|
83
|
+
readonly decisions: number;
|
|
84
|
+
/** The newest decision date of this business at any time, or null when it has none dated. */
|
|
85
|
+
readonly latest_decision_at: string | null;
|
|
77
86
|
}
|
|
78
87
|
export interface WorldBusinessProjection {
|
|
79
88
|
readonly businesses: readonly WorldBusiness[];
|
|
@@ -109,7 +118,9 @@ export type WorldBusinessesResponse = ({
|
|
|
109
118
|
* Nothing here knows any organization's words: kinds and statuses are the
|
|
110
119
|
* Graph's own values.
|
|
111
120
|
*/
|
|
112
|
-
export declare function projectWorldFromGraph(graph: Pick<GraphFileV2, 'entities'
|
|
121
|
+
export declare function projectWorldFromGraph(graph: Pick<GraphFileV2, 'entities'> & {
|
|
122
|
+
readonly edges?: GraphFileV2['edges'];
|
|
123
|
+
}, now?: Date): WorldBusinessProjection;
|
|
113
124
|
/**
|
|
114
125
|
* The vocabulary the world draws with: the host's kinds in their order, then
|
|
115
126
|
* kinds the Graph uses that the host did not name (under their own value),
|
|
@@ -135,6 +146,7 @@ export declare function readWorldBusinesses(context: Pick<LocalWebModuleContext,
|
|
|
135
146
|
*/
|
|
136
147
|
export declare function projectOrganizationWorld(records: {
|
|
137
148
|
readonly projects: readonly unknown[];
|
|
149
|
+
readonly decisions?: readonly unknown[];
|
|
138
150
|
readonly glossaryTerms?: readonly unknown[];
|
|
139
151
|
}, options: {
|
|
140
152
|
readonly server: string;
|
package/dist/world-extension.js
CHANGED
|
@@ -31,6 +31,7 @@ const DEFAULT_STATUSES = Object.freeze({
|
|
|
31
31
|
const KIND_PALETTE = ['#1261ad', '#b07a1f', '#35684c', '#6b4fa0', '#a8433a', '#2f7d86', '#7a6a2b', '#5b6470'];
|
|
32
32
|
const UNCLASSIFIED_COLOR = '#69746d';
|
|
33
33
|
const HEX_COLOR = /^#[0-9a-f]{6}$/iu;
|
|
34
|
+
const ACTIVITY_WINDOW_DAYS = 30;
|
|
34
35
|
function isRecord(value) {
|
|
35
36
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
36
37
|
}
|
|
@@ -93,6 +94,46 @@ export function projectWorldFromGraph(graph, now = new Date()) {
|
|
|
93
94
|
else
|
|
94
95
|
unplaced.push(engagementOf(entity));
|
|
95
96
|
}
|
|
97
|
+
// Which business a decision belongs to: its scope code (organization Graph) or the project it
|
|
98
|
+
// governs (local Graph), resolved to the city that project is or hangs under.
|
|
99
|
+
const cityOfKey = new Map();
|
|
100
|
+
for (const [cityId, { entity, engagements }] of cities) {
|
|
101
|
+
cityOfKey.set(entity.id, cityId);
|
|
102
|
+
const code = text(entity.metadata?.code);
|
|
103
|
+
if (code)
|
|
104
|
+
cityOfKey.set(code, cityId);
|
|
105
|
+
for (const engagement of engagements) {
|
|
106
|
+
cityOfKey.set(engagement.id, cityId);
|
|
107
|
+
if (engagement.code)
|
|
108
|
+
cityOfKey.set(engagement.code, cityId);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const governs = new Map();
|
|
112
|
+
for (const edge of graph.edges ?? []) {
|
|
113
|
+
if (edge.relation !== 'governs')
|
|
114
|
+
continue;
|
|
115
|
+
governs.set(edge.fromId, [...(governs.get(edge.fromId) ?? []), edge.toId]);
|
|
116
|
+
}
|
|
117
|
+
const windowStart = now.getTime() - ACTIVITY_WINDOW_DAYS * 24 * 60 * 60 * 1000;
|
|
118
|
+
const activity = new Map();
|
|
119
|
+
for (const decision of graph.entities) {
|
|
120
|
+
if (decision.type !== 'decision')
|
|
121
|
+
continue;
|
|
122
|
+
const at = text(decision.metadata?.decided_at) ?? text(decision.validFrom);
|
|
123
|
+
const time = at ? Date.parse(at) : Number.NaN;
|
|
124
|
+
if (!Number.isFinite(time))
|
|
125
|
+
continue;
|
|
126
|
+
const keys = [text(decision.metadata?.project_code), ...(governs.get(decision.id) ?? [])].filter((key) => key !== null);
|
|
127
|
+
const cityIds = new Set(keys.map((key) => cityOfKey.get(key)).filter((id) => id !== undefined));
|
|
128
|
+
for (const cityId of cityIds) {
|
|
129
|
+
const entry = activity.get(cityId) ?? { decisions: 0, latest: null };
|
|
130
|
+
if (time >= windowStart && time <= now.getTime())
|
|
131
|
+
entry.decisions += 1;
|
|
132
|
+
if (!entry.latest || time > Date.parse(entry.latest))
|
|
133
|
+
entry.latest = new Date(time).toISOString();
|
|
134
|
+
activity.set(cityId, entry);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
96
137
|
const businesses = [...cities.values()].map(({ entity, engagements }) => ({
|
|
97
138
|
id: entity.id,
|
|
98
139
|
code: text(entity.metadata?.code) ?? entity.id,
|
|
@@ -104,6 +145,11 @@ export function projectWorldFromGraph(graph, now = new Date()) {
|
|
|
104
145
|
? entity.metadata.repositories.map(text).filter((name) => name !== null)
|
|
105
146
|
: [],
|
|
106
147
|
engagements: engagements.sort((a, b) => a.name.localeCompare(b.name, 'ja')),
|
|
148
|
+
activity: {
|
|
149
|
+
window_days: ACTIVITY_WINDOW_DAYS,
|
|
150
|
+
decisions: activity.get(entity.id)?.decisions ?? 0,
|
|
151
|
+
latest_decision_at: activity.get(entity.id)?.latest ?? null,
|
|
152
|
+
},
|
|
107
153
|
}));
|
|
108
154
|
return {
|
|
109
155
|
businesses: businesses.sort((a, b) => a.name.localeCompare(b.name, 'ja')),
|
|
@@ -231,7 +277,7 @@ export async function readWorldBusinesses(context, options = {}) {
|
|
|
231
277
|
export function projectOrganizationWorld(records, options) {
|
|
232
278
|
const now = options.now ?? new Date();
|
|
233
279
|
const { graph } = projectOrganizationGraph({
|
|
234
|
-
entities: { person: [], org: [], project: records.projects, decision: [], raci_assignment: [] },
|
|
280
|
+
entities: { person: [], org: [], project: records.projects, decision: (records.decisions ?? []), raci_assignment: [] },
|
|
235
281
|
memberOf: [],
|
|
236
282
|
assignedTo: [],
|
|
237
283
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unson/brainbase-mcp",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.5",
|
|
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",
|
|
@@ -358,5 +358,5 @@
|
|
|
358
358
|
"engines": {
|
|
359
359
|
"node": ">=20"
|
|
360
360
|
},
|
|
361
|
-
"gitHead": "
|
|
361
|
+
"gitHead": "9c24218a7ff1058cdf997adf92048186cd0b5e23"
|
|
362
362
|
}
|
|
@@ -64,3 +64,32 @@ export function districtLots(count, step = 2.5) {
|
|
|
64
64
|
cols += 1;
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
+
|
|
68
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* How a city reads (story-world-size-and-activity-v1): its size is the engagements still going, its
|
|
72
|
+
* height and lit windows are what happened in the last 30 days (decisions the Graph records for it,
|
|
73
|
+
* and judgments placed in it when the host has them). Finished engagements stay as empty lots but do
|
|
74
|
+
* not make the city bigger.
|
|
75
|
+
*/
|
|
76
|
+
export function cityMeasures(business, { isFinished, judgments = [], now = Date.now() } = {}) {
|
|
77
|
+
const engagements = Array.isArray(business?.engagements) ? business.engagements : [];
|
|
78
|
+
const open = engagements.filter((engagement) => !isFinished?.(engagement.status)).length;
|
|
79
|
+
const since = now - (business?.activity?.window_days ?? 30) * DAY_MS;
|
|
80
|
+
const recentJudgments = judgments.filter((entry) => {
|
|
81
|
+
const at = Date.parse(entry?.item?.proof?.recorded_at ?? '');
|
|
82
|
+
return Number.isFinite(at) && at >= since && at <= now;
|
|
83
|
+
}).length;
|
|
84
|
+
const decisions = Number.isInteger(business?.activity?.decisions) ? business.activity.decisions : 0;
|
|
85
|
+
const recent = decisions + recentJudgments;
|
|
86
|
+
return {
|
|
87
|
+
open,
|
|
88
|
+
finished: engagements.length - open,
|
|
89
|
+
decisions,
|
|
90
|
+
judgments: recentJudgments,
|
|
91
|
+
recent,
|
|
92
|
+
towerHeight: 3.2 + Math.min(Math.log2(1 + recent) * 1.6, 5.5),
|
|
93
|
+
lit: recent > 0,
|
|
94
|
+
};
|
|
95
|
+
}
|
package/ui/world/world-view.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { THREE, MapControls } from './world-vendor.js';
|
|
18
|
-
import { districtLots, groupJudgmentPlaces, UNPLACED_REASON_TEXT } from './world-placement.js';
|
|
18
|
+
import { cityMeasures, districtLots, groupJudgmentPlaces, UNPLACED_REASON_TEXT } from './world-placement.js';
|
|
19
19
|
import {
|
|
20
20
|
makeWorkspaceElement as el,
|
|
21
21
|
workspacePageHeader,
|
|
@@ -135,7 +135,8 @@ export function layoutWorld(businesses) {
|
|
|
135
135
|
previousKind = business.kind;
|
|
136
136
|
}
|
|
137
137
|
angle += slot;
|
|
138
|
-
|
|
138
|
+
// The city's size is the engagements still going (finished ones stay as empty lots).
|
|
139
|
+
const n = business.engagements.filter((engagement) => !isFinished(engagement.status)).length;
|
|
139
140
|
const size = 7 + 2.6 * Math.ceil(Math.sqrt(Math.max(n, 1)));
|
|
140
141
|
const ring = CITY_RING + (index % 2) * 16 + size * 0.35;
|
|
141
142
|
const city = { business, kind: business.kind, size, angle, x: Math.cos(angle) * ring, z: Math.sin(angle) * ring };
|
|
@@ -414,19 +415,21 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear }
|
|
|
414
415
|
}
|
|
415
416
|
|
|
416
417
|
// --- cities ----------------------------------------------------------------
|
|
417
|
-
function landmark(form, color, height, status) {
|
|
418
|
+
function landmark(form, color, height, status, active = true) {
|
|
418
419
|
const group = new THREE.Group();
|
|
419
420
|
const phase = statusPhase(status);
|
|
420
421
|
const concept = phase === 'concept';
|
|
422
|
+
// Windows are lit only when something happened in the last 30 days.
|
|
423
|
+
const lit = !concept && active;
|
|
421
424
|
const tone = phase === 'maintenance' ? muted(color, 0.55) : new THREE.Color(color);
|
|
422
425
|
const options = concept ? { transparent: true, opacity: 0.38 } : {};
|
|
423
426
|
if (form === 'tower') {
|
|
424
|
-
const base = texturedBlock(3, height, 3, tone,
|
|
425
|
-
const mid = texturedBlock(2.2, height * 0.45, 2.2, tone,
|
|
427
|
+
const base = texturedBlock(3, height, 3, tone, lit, { roughness: 0.4, ...options });
|
|
428
|
+
const mid = texturedBlock(2.2, height * 0.45, 2.2, tone, lit, { roughness: 0.4, ...options });
|
|
426
429
|
mid.position.y += height;
|
|
427
430
|
group.add(base, mid, mesh(new THREE.CylinderGeometry(0.05, 0.05, 1.6, 6), standard(0x46524a), { y: height * 1.45 + 0.8 }));
|
|
428
431
|
} else if (form === 'hall') {
|
|
429
|
-
const hall = texturedBlock(4.4, height * 0.55, 3, tone,
|
|
432
|
+
const hall = texturedBlock(4.4, height * 0.55, 3, tone, lit, options);
|
|
430
433
|
const roof = mesh(gableRoof(4.4, 3, 1.4), standard(muted(color, 0.15).multiplyScalar(0.85), options), { y: height * 0.55 });
|
|
431
434
|
roof.rotation.y = Math.PI / 2;
|
|
432
435
|
group.add(hall, roof);
|
|
@@ -434,7 +437,7 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear }
|
|
|
434
437
|
group.add(mesh(new THREE.CylinderGeometry(1.9, 2.1, height * 0.5, 24), standard(tone, options), { y: height * 0.25 }));
|
|
435
438
|
group.add(mesh(new THREE.SphereGeometry(1.9, 24, 12, 0, Math.PI * 2, 0, Math.PI / 2), standard(0xf3f6f8, { roughness: 0.3, ...options }), { y: height * 0.5 }));
|
|
436
439
|
} else {
|
|
437
|
-
group.add(texturedBlock(3.6, height * 0.75, 2.8, tone,
|
|
440
|
+
group.add(texturedBlock(3.6, height * 0.75, 2.8, tone, lit, options));
|
|
438
441
|
group.add(mesh(new THREE.BoxGeometry(1.2, 0.6, 1), standard(0xb8c2bb), { y: height * 0.75 + 0.3 }));
|
|
439
442
|
}
|
|
440
443
|
if (concept) {
|
|
@@ -496,8 +499,9 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear }
|
|
|
496
499
|
group.position.set(x, 0, z);
|
|
497
500
|
const plateColor = new THREE.Color(0xf7f8f4).lerp(new THREE.Color(color), 0.1);
|
|
498
501
|
group.add(mesh(roundedPlate(size, size, 0.45, 1.6), standard(plateColor, { roughness: 0.95 })));
|
|
499
|
-
const
|
|
500
|
-
|
|
502
|
+
const measures = cityMeasures(business, { isFinished, judgments: judgmentsByBusiness.get(business.code) ?? [] });
|
|
503
|
+
const towerHeight = measures.towerHeight;
|
|
504
|
+
group.add(landmark(entry.form, color, towerHeight, business.status, measures.lit));
|
|
501
505
|
scene.add(group);
|
|
502
506
|
register(group, { kind: 'city', business });
|
|
503
507
|
const top = entry.form === 'tower' ? towerHeight * 1.45 + 1.8 : towerHeight + 1;
|
|
@@ -825,6 +829,8 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
|
|
|
825
829
|
let placement = { byBusiness: new Map(), unplaced: [] };
|
|
826
830
|
/** Judgments the host kept back because they are not this organization's (organization mode only). */
|
|
827
831
|
let withheld = 0;
|
|
832
|
+
/** Whether the host gave judgments at all (an organization web does not: they stay on each Mac). */
|
|
833
|
+
let judgmentsConnected = false;
|
|
828
834
|
|
|
829
835
|
function judgmentText(item, fallback) {
|
|
830
836
|
const textValue = item?.proof?.interruption?.question_display_text ?? item?.proof?.decision?.summary ?? fallback;
|
|
@@ -902,10 +908,17 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
|
|
|
902
908
|
blocks.push(workspaceRailBlock(doc, { title: '登録', content: workspaceDefinition(doc, [['状態', statusText(data.engagement.status)], ['Graph ID', data.engagement.id]]) }));
|
|
903
909
|
blocks.push(detailBlock(data.engagement, '「プロジェクトと関係者」でこの案件を開く'));
|
|
904
910
|
} else {
|
|
905
|
-
const
|
|
911
|
+
const measures = cityMeasures(business, { isFinished, judgments: placement.byBusiness.get(business.code) ?? [] });
|
|
906
912
|
const kind = kindEntry(business.kind);
|
|
907
913
|
const kindText = kind.definition ? `${kind.label}(${kind.definition})` : kind.label;
|
|
908
|
-
|
|
914
|
+
const judgmentText = judgmentsConnected ? `・判断${measures.judgments}件` : '(判断は未接続)';
|
|
915
|
+
blocks.push(workspaceRailBlock(doc, { title: '登録', content: workspaceDefinition(doc, [
|
|
916
|
+
['分類', kindText],
|
|
917
|
+
['状態', statusText(business.status)],
|
|
918
|
+
['コード', business.code],
|
|
919
|
+
['進行中の案件(都市の広さ)', `${measures.open}件${measures.finished ? `(完了・終了${measures.finished}件は更地)` : ''}`],
|
|
920
|
+
['最近30日の動き(高さ・明かり)', `決定${measures.decisions}件${judgmentText}`],
|
|
921
|
+
]) }));
|
|
909
922
|
const ul = el(doc, 'ul', { className: 'bb-world-rail-list' });
|
|
910
923
|
for (const engagement of business.engagements) {
|
|
911
924
|
const li = el(doc, 'li', { text: `${engagement.name}(${statusText(engagement.status)})` });
|
|
@@ -1034,6 +1047,7 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
|
|
|
1034
1047
|
else note('判断', `判断の記録を読めません(${reason})。0件ではありません。`, 'warning');
|
|
1035
1048
|
} else {
|
|
1036
1049
|
proofs = proofIndex(home);
|
|
1050
|
+
judgmentsConnected = true;
|
|
1037
1051
|
const waiting = [...proofs.values()].filter(isWaiting).length;
|
|
1038
1052
|
const places = placesResult.ok && placesResult.data?.status === 'available' ? placesResult.data.places : null;
|
|
1039
1053
|
if (places) {
|