@unson/brainbase-mcp 0.15.3 → 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
|
}
|
|
@@ -43,3 +43,53 @@ export function groupJudgmentPlaces(places, businesses) {
|
|
|
43
43
|
}
|
|
44
44
|
return { byBusiness, unplaced };
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The lots around a city's landmark for `count` districts: a square grid of `step` spacing whose cells
|
|
49
|
+
* outside the landmark's lot hold at least `count` districts (a 2x2 grid lies wholly inside it).
|
|
50
|
+
*/
|
|
51
|
+
export function districtLots(count, step = 2.5) {
|
|
52
|
+
let cols = Math.max(2, Math.ceil(Math.sqrt(count + 1)));
|
|
53
|
+
for (;;) {
|
|
54
|
+
const cells = [];
|
|
55
|
+
for (let row = 0; row < cols; row += 1) {
|
|
56
|
+
for (let col = 0; col < cols; col += 1) {
|
|
57
|
+
const cx = -((cols - 1) * step) / 2 + col * step;
|
|
58
|
+
const cz = -((cols - 1) * step) / 2 + row * step;
|
|
59
|
+
if (Math.abs(cx) < 2.2 && Math.abs(cz) < 2.2) continue; // the landmark's lot
|
|
60
|
+
cells.push([cx, cz]);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (cells.length >= count) return { cols, cells };
|
|
64
|
+
cols += 1;
|
|
65
|
+
}
|
|
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 { 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;
|
|
@@ -505,17 +509,8 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear }
|
|
|
505
509
|
cityLabel.node.style.borderColor = `${entry.color}80`;
|
|
506
510
|
cityLabel.business = business;
|
|
507
511
|
const n = business.engagements.length;
|
|
508
|
-
const cols = Math.max(2, Math.ceil(Math.sqrt(n + 1)));
|
|
509
512
|
const step = 2.5;
|
|
510
|
-
const cells =
|
|
511
|
-
for (let row = 0; row < cols; row += 1) {
|
|
512
|
-
for (let col = 0; col < cols; col += 1) {
|
|
513
|
-
const cx = -((cols - 1) * step) / 2 + col * step;
|
|
514
|
-
const cz = -((cols - 1) * step) / 2 + row * step;
|
|
515
|
-
if (Math.abs(cx) < 2.2 && Math.abs(cz) < 2.2) continue; // the landmark's lot
|
|
516
|
-
cells.push([cx, cz]);
|
|
517
|
-
}
|
|
518
|
-
}
|
|
513
|
+
const { cols, cells } = districtLots(n, step);
|
|
519
514
|
cells.sort((a, b) => Math.hypot(...a) - Math.hypot(...b));
|
|
520
515
|
business.engagements.forEach((engagement, index) => {
|
|
521
516
|
const [cx, cz] = cells[index % cells.length];
|
|
@@ -834,6 +829,8 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
|
|
|
834
829
|
let placement = { byBusiness: new Map(), unplaced: [] };
|
|
835
830
|
/** Judgments the host kept back because they are not this organization's (organization mode only). */
|
|
836
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;
|
|
837
834
|
|
|
838
835
|
function judgmentText(item, fallback) {
|
|
839
836
|
const textValue = item?.proof?.interruption?.question_display_text ?? item?.proof?.decision?.summary ?? fallback;
|
|
@@ -911,10 +908,17 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
|
|
|
911
908
|
blocks.push(workspaceRailBlock(doc, { title: '登録', content: workspaceDefinition(doc, [['状態', statusText(data.engagement.status)], ['Graph ID', data.engagement.id]]) }));
|
|
912
909
|
blocks.push(detailBlock(data.engagement, '「プロジェクトと関係者」でこの案件を開く'));
|
|
913
910
|
} else {
|
|
914
|
-
const
|
|
911
|
+
const measures = cityMeasures(business, { isFinished, judgments: placement.byBusiness.get(business.code) ?? [] });
|
|
915
912
|
const kind = kindEntry(business.kind);
|
|
916
913
|
const kindText = kind.definition ? `${kind.label}(${kind.definition})` : kind.label;
|
|
917
|
-
|
|
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
|
+
]) }));
|
|
918
922
|
const ul = el(doc, 'ul', { className: 'bb-world-rail-list' });
|
|
919
923
|
for (const engagement of business.engagements) {
|
|
920
924
|
const li = el(doc, 'li', { text: `${engagement.name}(${statusText(engagement.status)})` });
|
|
@@ -1043,6 +1047,7 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
|
|
|
1043
1047
|
else note('判断', `判断の記録を読めません(${reason})。0件ではありません。`, 'warning');
|
|
1044
1048
|
} else {
|
|
1045
1049
|
proofs = proofIndex(home);
|
|
1050
|
+
judgmentsConnected = true;
|
|
1046
1051
|
const waiting = [...proofs.values()].filter(isWaiting).length;
|
|
1047
1052
|
const places = placesResult.ok && placesResult.data?.status === 'available' ? placesResult.data.places : null;
|
|
1048
1053
|
if (places) {
|