@unson/brainbase-mcp 0.25.0 → 0.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/graph-embedding-provider.d.ts +38 -0
- package/dist/graph-embedding-provider.js +374 -0
- package/dist/judgment-frame.d.ts +24 -2
- package/dist/judgment-frame.js +61 -0
- package/dist/world-extension.d.ts +12 -0
- package/dist/world-extension.js +24 -3
- package/package.json +6 -2
- package/ui/world/world-canvas-ui.js +3 -2
- package/ui/world/world-district.js +135 -8
- package/ui/world/world-exits.js +200 -0
- package/ui/world/world-view.css +79 -0
- package/ui/world/world-view.js +117 -15
package/ui/world/world-view.js
CHANGED
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
* review date passed); a dashed ring says the text names a person the assignee
|
|
21
21
|
* field does not. Above the cities, a sign counts the open work with such a
|
|
22
22
|
* gap, and a grey sign says the work could not be read (never zero).
|
|
23
|
+
*
|
|
24
|
+
* A host may also give the tools a business uses (`businessExits`, story-world-business-exits-v1):
|
|
25
|
+
* they are listed in the city details and stand as stations outside the district's gate. The world
|
|
26
|
+
* only links out to them; it never changes them.
|
|
23
27
|
*/
|
|
24
28
|
|
|
25
29
|
import { THREE, MapControls } from './world-vendor.js';
|
|
@@ -29,6 +33,7 @@ import { createWorldCanvasUI } from './world-canvas-ui.js';
|
|
|
29
33
|
import { workCanvasNotes } from './world-canvas-notes.js';
|
|
30
34
|
import { canvasTexture, drawFacade, FACADE_SIZES, FACADE_UNITS, gableRoof, hashUnit, muted, paintVertices, roundedPlate, valueNoise } from './world-scenery.js';
|
|
31
35
|
import { WORK_STATES, workCityBlocks, workGrowthBlock, workPickBlock, workSign, workSiteBlocks } from './world-work-rail.js';
|
|
36
|
+
import { exitLegend, exitsRailBlock, normalizeBusinessExits } from './world-exits.js';
|
|
32
37
|
import {
|
|
33
38
|
makeWorkspaceElement as el,
|
|
34
39
|
workspacePageHeader,
|
|
@@ -160,12 +165,19 @@ function makeLabel(doc, text, className) {
|
|
|
160
165
|
return label;
|
|
161
166
|
}
|
|
162
167
|
|
|
163
|
-
|
|
168
|
+
/**
|
|
169
|
+
* 'ok' when WebGL runs on a GPU, 'slow' when the browser would only draw it in software (building the
|
|
170
|
+
* scene then freezes the page for tens of seconds), 'none' when there is no WebGL at all.
|
|
171
|
+
*/
|
|
172
|
+
function webglSupport(doc) {
|
|
164
173
|
try {
|
|
174
|
+
const strict = { failIfMajorPerformanceCaveat: true };
|
|
165
175
|
const canvas = doc.createElement('canvas');
|
|
166
|
-
|
|
176
|
+
if (canvas.getContext('webgl2', strict) || canvas.getContext('webgl', strict)) return 'ok';
|
|
177
|
+
const fallback = doc.createElement('canvas');
|
|
178
|
+
return fallback.getContext('webgl2') || fallback.getContext('webgl') ? 'slow' : 'none';
|
|
167
179
|
} catch {
|
|
168
|
-
return
|
|
180
|
+
return 'none';
|
|
169
181
|
}
|
|
170
182
|
}
|
|
171
183
|
|
|
@@ -1380,12 +1392,15 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear,
|
|
|
1380
1392
|
};
|
|
1381
1393
|
}
|
|
1382
1394
|
|
|
1383
|
-
|
|
1395
|
+
/** `exitSlot(business)` gives the node to show under a business for its tools, when the host gives them. */
|
|
1396
|
+
function renderFallbackList(doc, root, businesses, rows, exitSlot = null) {
|
|
1384
1397
|
const list = el(doc, 'div', { className: 'bb-world-fallback' });
|
|
1385
1398
|
list.append(el(doc, 'h2', { text: '事業' }));
|
|
1386
1399
|
const ul = el(doc, 'ul');
|
|
1387
1400
|
for (const business of businesses) {
|
|
1388
|
-
|
|
1401
|
+
const item = el(doc, 'li', { text: `${business.name}(${kindEntry(business.kind).label}・案件${business.engagements.length}件)` });
|
|
1402
|
+
if (exitSlot) item.append(exitSlot(business));
|
|
1403
|
+
ul.append(item);
|
|
1389
1404
|
}
|
|
1390
1405
|
list.append(ul, el(doc, 'h2', { text: '判断の種類' }));
|
|
1391
1406
|
const ol = el(doc, 'ul');
|
|
@@ -1437,8 +1452,16 @@ function addressSelection() {
|
|
|
1437
1452
|
* host's task screen (to check or correct it). Without one the rail says it cannot be corrected here.
|
|
1438
1453
|
* @param {'standard' | 'canvas'} [options.presentation] Canvas owns its accessible overlays and ignores the host rail.
|
|
1439
1454
|
* @param {{ read(): { business: string | null, site: string | null }, write(selection): void }} [options.selection]
|
|
1455
|
+
* @param {(business: object) => (BusinessExitsAnswer | Promise<BusinessExitsAnswer>)} [options.businessExits] The tools
|
|
1456
|
+
* a business uses (story-world-business-exits-v1). The answer is
|
|
1457
|
+
* `{ status: 'complete'|'partial'|'failed'|'not_connected', read_at?, reason?, exits: [{ id, label, href,
|
|
1458
|
+
* state: 'available'|'restricted'|'unknown'|'unavailable', note?, action?: { label, href }, attention?: { count?,
|
|
1459
|
+
* label?, as_of? } }] }`. `href` and `action.href` must be `https:` or a link inside the host (`/…`, `?…`);
|
|
1460
|
+
* others are dropped and counted. Without it the world is drawn exactly as before.
|
|
1461
|
+
* @typedef {{ status: string, read_at?: string, reason?: string, exits: object[] }} BusinessExitsAnswer
|
|
1440
1462
|
*/
|
|
1441
|
-
export function createWorldView({ root, rail, page, presentation = 'standard', document: explicitDocument, fetcher, projectHref = (project) => `#projects?project=${encodeURIComponent(project.id)}`, taskHref = null, selection = addressSelection() }) {
|
|
1463
|
+
export function createWorldView({ root, rail, page, presentation = 'standard', document: explicitDocument, fetcher, projectHref = (project) => `#projects?project=${encodeURIComponent(project.id)}`, taskHref = null, selection = addressSelection(), businessExits = null }) {
|
|
1464
|
+
const exitsEnabled = typeof businessExits === 'function';
|
|
1442
1465
|
const doc = explicitDocument ?? globalThis.document;
|
|
1443
1466
|
const reducedMotion = Boolean(globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches);
|
|
1444
1467
|
const canvasMode = presentation === 'canvas';
|
|
@@ -1503,6 +1526,10 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
|
|
|
1503
1526
|
el(doc, 'span', { className: 'bb-world-chip is-ring', text: '破線の輪=本文に人物(担当欄は未接続)' }),
|
|
1504
1527
|
el(doc, 'span', { className: 'bb-world-chip is-road', text: '実線=記録された関係・破線=推定' }),
|
|
1505
1528
|
);
|
|
1529
|
+
// The stations' legend, in the owner's words once the vocabulary is read (AC-08).
|
|
1530
|
+
const exitLegendList = el(doc, 'ul', { className: 'bb-world-exit-legend', attrs: { 'aria-label': '駅の見方' } });
|
|
1531
|
+
const renderExitLegend = () => exitLegendList.replaceChildren(...exitLegend(worldVocabulary).map(([className, text]) => el(doc, 'li', { className, text })));
|
|
1532
|
+
if (exitsEnabled) renderExitLegend();
|
|
1506
1533
|
const stage = el(doc, 'div', { className: 'bb-world-stage' });
|
|
1507
1534
|
const labelsLayer = el(doc, 'div', { className: 'bb-world-labels' });
|
|
1508
1535
|
stage.append(labelsLayer);
|
|
@@ -1530,6 +1557,7 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
|
|
|
1530
1557
|
const list = el(doc, 'ul');
|
|
1531
1558
|
for (const [, text] of DISTRICT_LEGEND) list.append(el(doc, 'li', { text }));
|
|
1532
1559
|
help.append(list);
|
|
1560
|
+
if (exitsEnabled) help.append(exitLegendList);
|
|
1533
1561
|
if (page?.source) help.append(el(doc, 'p', { text: page.source }));
|
|
1534
1562
|
canvasUI.setHelp([help]);
|
|
1535
1563
|
} else {
|
|
@@ -1656,7 +1684,7 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
|
|
|
1656
1684
|
let workConnected = null;
|
|
1657
1685
|
const workCache = new Map();
|
|
1658
1686
|
const workRequests = new Map();
|
|
1659
|
-
const current = { code: null, siteId: null, level: 'world', focus: null, pendingSite: null, detachedSiteId: null };
|
|
1687
|
+
const current = { code: null, siteId: null, level: 'world', focus: null, pendingSite: null, detachedSiteId: null, exitId: null };
|
|
1660
1688
|
|
|
1661
1689
|
/** Inside a city the notices fold away (they would cover it); its rail carries the sources and times. */
|
|
1662
1690
|
function setLevel(level) {
|
|
@@ -1677,6 +1705,58 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
|
|
|
1677
1705
|
return knownBusinesses.find((business) => business.code === code) ?? null;
|
|
1678
1706
|
}
|
|
1679
1707
|
|
|
1708
|
+
// --- the tools a business uses (story-world-business-exits-v1) ----------------------------------
|
|
1709
|
+
/** Normalized answers by business code; a read in flight is kept so it is asked only once. */
|
|
1710
|
+
const exitsCache = new Map();
|
|
1711
|
+
const exitsRequests = new Map();
|
|
1712
|
+
/** Under each business in the list shown without 3D (AC-09). */
|
|
1713
|
+
const fallbackExitSlots = new Map();
|
|
1714
|
+
/** The stations of a business for the district: its read tools, or none until read. */
|
|
1715
|
+
const stationsFor = (code) => (exitsEnabled ? exitsCache.get(code)?.exits ?? [] : undefined);
|
|
1716
|
+
|
|
1717
|
+
function fallbackExitSlot(business) {
|
|
1718
|
+
const slot = el(doc, 'div', { className: 'bb-world-fallback-exits' });
|
|
1719
|
+
fallbackExitSlots.set(business.code, slot);
|
|
1720
|
+
slot.append(exitsRailBlock(doc, { result: exitsCache.get(business.code) ?? null, vocabulary: worldVocabulary }).block);
|
|
1721
|
+
return slot;
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
function loadExits(business) {
|
|
1725
|
+
if (!exitsEnabled || disposed) return;
|
|
1726
|
+
const { code } = business;
|
|
1727
|
+
if (exitsCache.has(code) || exitsRequests.has(code)) return;
|
|
1728
|
+
const request = (async () => {
|
|
1729
|
+
try {
|
|
1730
|
+
return normalizeBusinessExits(await businessExits(business));
|
|
1731
|
+
} catch (error) {
|
|
1732
|
+
return { ...normalizeBusinessExits({ status: 'failed', exits: [] }), reason: error instanceof Error ? error.message : 'request_failed' };
|
|
1733
|
+
}
|
|
1734
|
+
})();
|
|
1735
|
+
exitsRequests.set(code, request);
|
|
1736
|
+
void request.then((result) => {
|
|
1737
|
+
if (disposed || exitsRequests.get(code) !== request) return;
|
|
1738
|
+
exitsRequests.delete(code);
|
|
1739
|
+
exitsCache.set(code, result);
|
|
1740
|
+
fallbackExitSlots.get(code)?.replaceChildren(exitsRailBlock(doc, { result, vocabulary: worldVocabulary }).block);
|
|
1741
|
+
// AC-10: an answer for a city that is no longer open does not reopen it.
|
|
1742
|
+
if (current.code !== code) return;
|
|
1743
|
+
if (districtCode === code && district?.isOpen()) district.setExits(result.exits);
|
|
1744
|
+
if (canvasMode || current.level !== 'site') renderCityRail(business);
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
/** A picked station: the city details open with that tool's row selected; nothing leaves the world. */
|
|
1749
|
+
function selectExit(business, exitId) {
|
|
1750
|
+
current.exitId = exitId;
|
|
1751
|
+
if (current.level === 'site') {
|
|
1752
|
+
canvasUI?.closeTask();
|
|
1753
|
+
setCurrent(business.code, null);
|
|
1754
|
+
setLevel('city');
|
|
1755
|
+
}
|
|
1756
|
+
renderCityRail(business, { revealExit: true });
|
|
1757
|
+
canvasUI?.openRail();
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1680
1760
|
/** The registration, engagements, link out and judgments of a city (as before the work layer). */
|
|
1681
1761
|
function cityRegistrationBlocks(business) {
|
|
1682
1762
|
const blocks = [];
|
|
@@ -1730,7 +1810,7 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
|
|
|
1730
1810
|
}
|
|
1731
1811
|
}
|
|
1732
1812
|
|
|
1733
|
-
function renderCityRail(business, { revealFocus = false } = {}) {
|
|
1813
|
+
function renderCityRail(business, { revealFocus = false, revealExit = false } = {}) {
|
|
1734
1814
|
const blocks = [workspaceRailHead(doc, { kicker: `事業・${kindEntry(business.kind).label}`, title: business.name, lead: business.purpose ?? undefined })];
|
|
1735
1815
|
let focusBlock = null;
|
|
1736
1816
|
const work = workCache.get(business.code);
|
|
@@ -1762,8 +1842,15 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
|
|
|
1762
1842
|
blocks.push(directory);
|
|
1763
1843
|
}
|
|
1764
1844
|
blocks.push(...cityRegistrationBlocks(business));
|
|
1845
|
+
let exitRow = null;
|
|
1846
|
+
if (exitsEnabled) {
|
|
1847
|
+
const tools = exitsRailBlock(doc, { result: exitsCache.get(business.code) ?? null, vocabulary: worldVocabulary, selectedId: current.exitId });
|
|
1848
|
+
blocks.push(tools.block);
|
|
1849
|
+
exitRow = tools.selectedRow;
|
|
1850
|
+
}
|
|
1765
1851
|
showRail(blocks);
|
|
1766
1852
|
if (revealFocus) reveal(focusBlock ?? blocks[0]);
|
|
1853
|
+
if (revealExit && exitRow) reveal(exitRow);
|
|
1767
1854
|
}
|
|
1768
1855
|
|
|
1769
1856
|
/** The open tasks a focus picks: a kind of gap, or a recorded state; null for all. */
|
|
@@ -1831,7 +1918,7 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
|
|
|
1831
1918
|
updateDistrictNotes(work);
|
|
1832
1919
|
scene?.showWork(code, work);
|
|
1833
1920
|
if (districtCode === code && district?.isOpen()) {
|
|
1834
|
-
district.refresh(business, work, { neighbours: new Map(knownBusinesses.map((entry) => [entry.code, entry.name])) });
|
|
1921
|
+
district.refresh(business, work, { neighbours: new Map(knownBusinesses.map((entry) => [entry.code, entry.name])), exits: stationsFor(business.code) });
|
|
1835
1922
|
const growth = noteGrowth(code, work, previous);
|
|
1836
1923
|
district.celebrate(growth?.changes ?? null);
|
|
1837
1924
|
district.focus(focusIdsFor(work, current.focus));
|
|
@@ -1875,12 +1962,19 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
|
|
|
1875
1962
|
if (anchor.taskId === current.siteId && current.detachedSiteId !== current.siteId) canvasUI?.setTaskAnchor(anchor);
|
|
1876
1963
|
else if (!anchor.visible && !current.siteId) canvasUI?.setTaskAnchor({ visible: false });
|
|
1877
1964
|
},
|
|
1965
|
+
...(exitsEnabled ? {
|
|
1966
|
+
exitLegend: exitLegend(worldVocabulary),
|
|
1967
|
+
onPickExit: (exitId) => {
|
|
1968
|
+
const owner = businessOf(current.code);
|
|
1969
|
+
if (owner) selectExit(owner, exitId);
|
|
1970
|
+
},
|
|
1971
|
+
} : {}),
|
|
1878
1972
|
});
|
|
1879
1973
|
district.setSky(SKY_MODES[skyIndex][0]);
|
|
1880
1974
|
scene.setPaused(true);
|
|
1881
1975
|
if (!canvasMode) { legend.hidden = true; workLegend.hidden = true; }
|
|
1882
1976
|
if (districtCode !== business.code || !district.isOpen()) {
|
|
1883
|
-
district.show(business, work, { neighbours: new Map(knownBusinesses.map((entry) => [entry.code, entry.name])) });
|
|
1977
|
+
district.show(business, work, { neighbours: new Map(knownBusinesses.map((entry) => [entry.code, entry.name])), exits: stationsFor(business.code) });
|
|
1884
1978
|
districtCode = business.code;
|
|
1885
1979
|
const growth = growthFor(business.code) ?? noteGrowth(business.code, work, readSnapshot(business.code));
|
|
1886
1980
|
district.celebrate(growth?.changes ?? null);
|
|
@@ -1911,7 +2005,7 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
|
|
|
1911
2005
|
scene?.showWork(business.code, work);
|
|
1912
2006
|
if (current.level !== 'world') {
|
|
1913
2007
|
if (districtCode === business.code && district?.isOpen()) {
|
|
1914
|
-
district.refresh(business, work, { neighbours: new Map(knownBusinesses.map((entry) => [entry.code, entry.name])) });
|
|
2008
|
+
district.refresh(business, work, { neighbours: new Map(knownBusinesses.map((entry) => [entry.code, entry.name])), exits: stationsFor(business.code) });
|
|
1915
2009
|
district.focus(focusIdsFor(work, current.focus));
|
|
1916
2010
|
} else enterDistrict(business, work);
|
|
1917
2011
|
}
|
|
@@ -1931,6 +2025,7 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
|
|
|
1931
2025
|
current.pendingSite = null;
|
|
1932
2026
|
if (current.code !== business.code) {
|
|
1933
2027
|
current.focus = null;
|
|
2028
|
+
current.exitId = null;
|
|
1934
2029
|
leaveDistrict();
|
|
1935
2030
|
}
|
|
1936
2031
|
setCurrent(business.code, null);
|
|
@@ -1949,6 +2044,7 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
|
|
|
1949
2044
|
}
|
|
1950
2045
|
}
|
|
1951
2046
|
renderCityRail(business);
|
|
2047
|
+
loadExits(business);
|
|
1952
2048
|
if (!work) void loadWork(business);
|
|
1953
2049
|
}
|
|
1954
2050
|
|
|
@@ -2020,6 +2116,7 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
|
|
|
2020
2116
|
setCurrent(null, null);
|
|
2021
2117
|
setLevel('world');
|
|
2022
2118
|
current.focus = null;
|
|
2119
|
+
current.exitId = null;
|
|
2023
2120
|
showEmptyRail();
|
|
2024
2121
|
}
|
|
2025
2122
|
|
|
@@ -2240,6 +2337,7 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
|
|
|
2240
2337
|
const businessPayload = businessesResult.ok ? businessesResult.data : null;
|
|
2241
2338
|
const businesses = businessPayload?.status === 'ok' ? businessPayload.businesses : [];
|
|
2242
2339
|
if (businessPayload?.status === 'ok' && businessPayload.vocabulary) worldVocabulary = businessPayload.vocabulary;
|
|
2340
|
+
if (exitsEnabled) renderExitLegend();
|
|
2243
2341
|
if (!businessesResult.ok || businessPayload?.status !== 'ok') {
|
|
2244
2342
|
const reason = businessesResult.ok ? `${BUSINESS_STATE_TEXT[businessPayload?.status] ?? '状態不明'}(${businessPayload?.reason ?? '理由不明'})` : `取得に失敗しました(${businessesResult.error})`;
|
|
2245
2343
|
note('事業', `${reason}。事業の件数は未確認です。0件とは確認できません。`, 'warning', '事業:読めない(0件とは確認できません)');
|
|
@@ -2284,17 +2382,21 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
|
|
|
2284
2382
|
|
|
2285
2383
|
knownBusinesses = businesses;
|
|
2286
2384
|
canvasUI?.setCities(businesses, current.code);
|
|
2287
|
-
|
|
2288
|
-
|
|
2385
|
+
const webgl = webglSupport(doc);
|
|
2386
|
+
if (webgl !== 'ok') {
|
|
2387
|
+
if (webgl === 'slow') note('表示', 'この環境ではGPUを使えず、3Dの描画が非常に遅いため、一覧で出しています。', 'warning', '3D表示が遅い環境・一覧表示');
|
|
2388
|
+
else note('表示', 'この環境では3Dを表示できないため、一覧で出しています。', 'warning', '3D表示を利用できません・一覧表示');
|
|
2389
|
+
const exitSlot = exitsEnabled ? fallbackExitSlot : null;
|
|
2289
2390
|
if (canvasMode) {
|
|
2290
|
-
renderFallbackList(doc, stage, businesses, rows);
|
|
2391
|
+
renderFallbackList(doc, stage, businesses, rows, exitSlot);
|
|
2291
2392
|
showEmptyRail();
|
|
2292
2393
|
restoreSelection();
|
|
2293
2394
|
void loadWorkSummary(note);
|
|
2294
2395
|
} else {
|
|
2295
2396
|
stage.hidden = true;
|
|
2296
|
-
renderFallbackList(doc, wrap, businesses, rows);
|
|
2397
|
+
renderFallbackList(doc, wrap, businesses, rows, exitSlot);
|
|
2297
2398
|
}
|
|
2399
|
+
for (const business of exitsEnabled ? businesses : []) loadExits(business);
|
|
2298
2400
|
return;
|
|
2299
2401
|
}
|
|
2300
2402
|
scene = createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear: clearAll, onEscape: goUp });
|