@unson/brainbase-mcp 0.22.2 → 0.22.4
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/package.json +2 -2
- package/ui/graph-projects-view.js +102 -6
- package/ui/world/world-district.js +12 -11
- package/ui/world/world-placement.js +17 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unson/brainbase-mcp",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.4",
|
|
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",
|
|
@@ -407,5 +407,5 @@
|
|
|
407
407
|
"engines": {
|
|
408
408
|
"node": ">=20"
|
|
409
409
|
},
|
|
410
|
-
"gitHead": "
|
|
410
|
+
"gitHead": "8c472153bac9ae0d439f8275a2c640f46ae03699"
|
|
411
411
|
}
|
|
@@ -298,6 +298,9 @@ export function createGraphProjectsView({
|
|
|
298
298
|
let contextRequest = 0;
|
|
299
299
|
let detailRequest = 0;
|
|
300
300
|
let hostListRequest = 0;
|
|
301
|
+
// An explicitly selected project may be read in parallel with the host
|
|
302
|
+
// list, but the list remains authoritative for whether its detail is shown.
|
|
303
|
+
let initialSelectionGate = null;
|
|
301
304
|
let viewDestroyed = false;
|
|
302
305
|
|
|
303
306
|
const correction = createGraphCorrection({
|
|
@@ -415,7 +418,7 @@ export function createGraphProjectsView({
|
|
|
415
418
|
}
|
|
416
419
|
|
|
417
420
|
/** Reads supplementary sections independently of the Graph project detail. */
|
|
418
|
-
async function loadProjectContextFor(id, { keep = false } = {}) {
|
|
421
|
+
async function loadProjectContextFor(id, { keep = false, deferUntilHostList = null } = {}) {
|
|
419
422
|
if (viewDestroyed) return state.context;
|
|
420
423
|
if (typeof loadProjectContext !== 'function') {
|
|
421
424
|
if (state.context?.projectId !== id) state.context = null;
|
|
@@ -425,9 +428,16 @@ export function createGraphProjectsView({
|
|
|
425
428
|
const request = ++contextRequest;
|
|
426
429
|
state.context = normalizeProjectContext(id, { state: 'loading' });
|
|
427
430
|
controller.render();
|
|
428
|
-
const applyContext = (snapshot) => {
|
|
431
|
+
const applyContext = (snapshot, { flushDeferred = false } = {}) => {
|
|
429
432
|
const normalized = normalizeProjectContext(id, snapshot);
|
|
430
433
|
if (viewDestroyed || request !== contextRequest || state.selectedId !== id) return false;
|
|
434
|
+
if (deferUntilHostList && !deferUntilHostList.confirmed && !flushDeferred) {
|
|
435
|
+
if (!deferUntilHostList.cancelled && initialSelectionGate === deferUntilHostList) {
|
|
436
|
+
deferUntilHostList.pendingContext = normalized;
|
|
437
|
+
deferUntilHostList.flushContext = () => applyContext(normalized, { flushDeferred: true });
|
|
438
|
+
}
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
431
441
|
state.context = normalized;
|
|
432
442
|
// Context progress must not rebuild the workspace: rebuilding destroys
|
|
433
443
|
// Sigma's viewport and resets the user's active tab/selection. Detail
|
|
@@ -796,6 +806,37 @@ export function createGraphProjectsView({
|
|
|
796
806
|
return children;
|
|
797
807
|
}
|
|
798
808
|
|
|
809
|
+
function cancelInitialSelectionGate(gate, { reset = true } = {}) {
|
|
810
|
+
if (!gate || gate.cancelled || gate.confirmed) return;
|
|
811
|
+
gate.cancelled = true;
|
|
812
|
+
gate.pendingContext = null;
|
|
813
|
+
gate.pendingDetail = null;
|
|
814
|
+
gate.flushContext = null;
|
|
815
|
+
gate.flushDetail = null;
|
|
816
|
+
if (initialSelectionGate === gate) initialSelectionGate = null;
|
|
817
|
+
// A caller may have selected another project while this gate was pending.
|
|
818
|
+
// Do not invalidate that newer project's reads in that case.
|
|
819
|
+
if (!reset || state.selectedId !== gate.id) return;
|
|
820
|
+
detailRequest += 1;
|
|
821
|
+
contextRequest += 1;
|
|
822
|
+
if (state.detail?.id === gate.id && state.detail.state === 'loading') state.detail = null;
|
|
823
|
+
if (state.context?.projectId === gate.id && state.context.state === 'loading') state.context = null;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function confirmInitialSelectionGate(gate) {
|
|
827
|
+
if (!gate || gate.cancelled) return;
|
|
828
|
+
gate.confirmed = true;
|
|
829
|
+
if (initialSelectionGate === gate) initialSelectionGate = null;
|
|
830
|
+
// Responses which finished before the list are safe to expose now. A
|
|
831
|
+
// response still in flight will apply through its normal completion path.
|
|
832
|
+
gate.flushDetail?.();
|
|
833
|
+
gate.flushContext?.();
|
|
834
|
+
gate.pendingDetail = null;
|
|
835
|
+
gate.pendingContext = null;
|
|
836
|
+
gate.flushDetail = null;
|
|
837
|
+
gate.flushContext = null;
|
|
838
|
+
}
|
|
839
|
+
|
|
799
840
|
/** Reads the host's project list (see `load`). */
|
|
800
841
|
async function loadHostList({ keep = false } = {}) {
|
|
801
842
|
const request = ++hostListRequest;
|
|
@@ -804,14 +845,45 @@ export function createGraphProjectsView({
|
|
|
804
845
|
state.list = { state: 'loading' };
|
|
805
846
|
controller.render();
|
|
806
847
|
}
|
|
848
|
+
let selectionGate = null;
|
|
849
|
+
let selectionDetailReading = null;
|
|
850
|
+
const selectedBeforeList = state.selectedId;
|
|
851
|
+
if (selectedBeforeList) {
|
|
852
|
+
if (initialSelectionGate?.id === selectedBeforeList && !initialSelectionGate.cancelled && !initialSelectionGate.confirmed) {
|
|
853
|
+
selectionGate = initialSelectionGate;
|
|
854
|
+
selectionDetailReading = selectionGate.detailReading;
|
|
855
|
+
} else if (state.detail === null) {
|
|
856
|
+
selectionGate = {
|
|
857
|
+
id: selectedBeforeList,
|
|
858
|
+
confirmed: false,
|
|
859
|
+
cancelled: false,
|
|
860
|
+
pendingContext: null,
|
|
861
|
+
pendingDetail: null,
|
|
862
|
+
flushContext: null,
|
|
863
|
+
flushDetail: null,
|
|
864
|
+
detailReading: null,
|
|
865
|
+
};
|
|
866
|
+
initialSelectionGate = selectionGate;
|
|
867
|
+
selectionDetailReading = controller.loadDetail(selectedBeforeList, {
|
|
868
|
+
keep,
|
|
869
|
+
deferUntilHostList: selectionGate,
|
|
870
|
+
});
|
|
871
|
+
selectionGate.detailReading = selectionDetailReading;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
807
874
|
const result = await client.read('/projects');
|
|
808
875
|
if (viewDestroyed || request !== hostListRequest) return state.list;
|
|
809
876
|
state.list = normalizeProjectList(result);
|
|
810
877
|
if (state.list.state !== 'ok') {
|
|
878
|
+
cancelInitialSelectionGate(selectionGate);
|
|
811
879
|
controller.render();
|
|
812
880
|
return state.list;
|
|
813
881
|
}
|
|
814
882
|
const { projects } = state.list.payload;
|
|
883
|
+
if (selectionGate && (selectionGate.cancelled || state.selectedId !== selectionGate.id)) {
|
|
884
|
+
cancelInitialSelectionGate(selectionGate, { reset: false });
|
|
885
|
+
selectionGate = null;
|
|
886
|
+
}
|
|
815
887
|
if (!state.selectedId) {
|
|
816
888
|
if (correction.form) correction.close();
|
|
817
889
|
state.panelAt = null;
|
|
@@ -832,6 +904,7 @@ export function createGraphProjectsView({
|
|
|
832
904
|
// never be replaced with the first row when the refreshed list no
|
|
833
905
|
// longer contains it. Keeping the id makes the missing target visible
|
|
834
906
|
// and lets the host decide whether its identity or access changed.
|
|
907
|
+
cancelInitialSelectionGate(selectionGate);
|
|
835
908
|
if (correction.form) correction.close();
|
|
836
909
|
detailRequest += 1;
|
|
837
910
|
state.panelAt = null;
|
|
@@ -842,6 +915,11 @@ export function createGraphProjectsView({
|
|
|
842
915
|
};
|
|
843
916
|
state.context = null;
|
|
844
917
|
contextRequest += 1;
|
|
918
|
+
} else if (selectionGate && selectionGate.id === state.selectedId) {
|
|
919
|
+
confirmInitialSelectionGate(selectionGate);
|
|
920
|
+
if (selectionDetailReading) await selectionDetailReading;
|
|
921
|
+
if (viewDestroyed || request !== hostListRequest) return state.list;
|
|
922
|
+
return state.list;
|
|
845
923
|
} else if (state.detail?.id !== state.selectedId || state.detail?.state === 'not_found') {
|
|
846
924
|
// The project the host asked for is listed; read it for the rail.
|
|
847
925
|
if (viewDestroyed || request !== hostListRequest) return state.list;
|
|
@@ -876,6 +954,7 @@ export function createGraphProjectsView({
|
|
|
876
954
|
hostListRequest += 1;
|
|
877
955
|
detailRequest += 1;
|
|
878
956
|
contextRequest += 1;
|
|
957
|
+
initialSelectionGate = null;
|
|
879
958
|
knowledgeWorkspace?.destroy();
|
|
880
959
|
knowledgeWorkspace = null;
|
|
881
960
|
correction.close();
|
|
@@ -925,7 +1004,7 @@ export function createGraphProjectsView({
|
|
|
925
1004
|
return state.own.detail;
|
|
926
1005
|
},
|
|
927
1006
|
/** `keep` shows the current detail until the new one has been read. */
|
|
928
|
-
async loadDetail(id, { keep = false } = {}) {
|
|
1007
|
+
async loadDetail(id, { keep = false, deferUntilHostList = null } = {}) {
|
|
929
1008
|
if (viewDestroyed) return state.detail;
|
|
930
1009
|
const request = ++detailRequest;
|
|
931
1010
|
const previous = state.detail;
|
|
@@ -933,13 +1012,26 @@ export function createGraphProjectsView({
|
|
|
933
1012
|
state.detail = { id, state: 'loading' };
|
|
934
1013
|
controller.render();
|
|
935
1014
|
}
|
|
936
|
-
const contextReading = loadProjectContextFor(id, { keep });
|
|
1015
|
+
const contextReading = loadProjectContextFor(id, { keep, deferUntilHostList });
|
|
937
1016
|
const result = await client.read(`/projects/${encodeURIComponent(id)}`);
|
|
938
1017
|
if (viewDestroyed || request !== detailRequest || state.selectedId !== id) return state.detail;
|
|
939
|
-
|
|
1018
|
+
const detail = result.state === 'error' && result.status === 404
|
|
940
1019
|
? { id, state: 'not_found', reason: 'このプロジェクトは見つかりません。一覧を読み直してください。' }
|
|
941
1020
|
: { id, ...normalizeProjectDetail(result) };
|
|
942
|
-
|
|
1021
|
+
const applyDetail = () => {
|
|
1022
|
+
if (viewDestroyed || request !== detailRequest || state.selectedId !== id) return false;
|
|
1023
|
+
state.detail = detail;
|
|
1024
|
+
controller.render();
|
|
1025
|
+
return true;
|
|
1026
|
+
};
|
|
1027
|
+
if (deferUntilHostList && !deferUntilHostList.confirmed) {
|
|
1028
|
+
if (!deferUntilHostList.cancelled && initialSelectionGate === deferUntilHostList) {
|
|
1029
|
+
deferUntilHostList.pendingDetail = detail;
|
|
1030
|
+
deferUntilHostList.flushDetail = applyDetail;
|
|
1031
|
+
}
|
|
1032
|
+
return state.detail;
|
|
1033
|
+
}
|
|
1034
|
+
applyDetail();
|
|
943
1035
|
// The context is intentionally independent. Its completion may trigger
|
|
944
1036
|
// one more render, but never changes the Graph detail result.
|
|
945
1037
|
void contextReading;
|
|
@@ -947,6 +1039,10 @@ export function createGraphProjectsView({
|
|
|
947
1039
|
},
|
|
948
1040
|
/** Selects a row: the rail shows that project, and an open correction is closed. */
|
|
949
1041
|
async selectProject(id) {
|
|
1042
|
+
// A host selection supersedes a prefetched initial detail. Cancel the
|
|
1043
|
+
// gate before changing the id so A -> B -> A cannot make a later list
|
|
1044
|
+
// response wait for the first A read.
|
|
1045
|
+
cancelInitialSelectionGate(initialSelectionGate, { reset: false });
|
|
950
1046
|
if (correction.form) correction.close();
|
|
951
1047
|
state.panelAt = null;
|
|
952
1048
|
state.selectedId = id;
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { THREE, MapControls } from './world-vendor.js';
|
|
17
|
-
import { DISTRICT_STREETS, districtStage, districtStreetLots, skyAt } from './world-placement.js';
|
|
17
|
+
import { DISTRICT_STREETS, districtGroundPlan, districtStage, districtStreetLots, skyAt } from './world-placement.js';
|
|
18
18
|
import { canvasTexture, drawFacade, FACADE_SIZES, FACADE_UNITS, gableRoof, hashUnit, paintVertices, roundedPlate, valueNoise } from './world-scenery.js';
|
|
19
19
|
import { makeWorkspaceElement as el } from '../../workspace-kit.js';
|
|
20
20
|
|
|
@@ -535,8 +535,8 @@ export function createDistrictView({ doc, stage, reducedMotion = false, onPick,
|
|
|
535
535
|
// The stage of the district (AC-24): counted from permanent buildings, it sets how built-up it looks.
|
|
536
536
|
const stage = districtStage(shown);
|
|
537
537
|
const grown = stage.level;
|
|
538
|
-
const
|
|
539
|
-
const hallZ
|
|
538
|
+
const plan = districtGroundPlan(L);
|
|
539
|
+
const { gateZ, hallZ, plazaZ, plazaRadius } = plan;
|
|
540
540
|
// Land as in the world: gentle, non-repeating shades of grass (scenery).
|
|
541
541
|
const grass = { low: new THREE.Color(0x9fbf8c), high: new THREE.Color(0xd3e4bd), warm: new THREE.Color(0xd9d6a6) };
|
|
542
542
|
const land = mesh(paintVertices(new THREE.CircleGeometry(150, 96, 0, Math.PI * 2), (color, x, y) => {
|
|
@@ -554,7 +554,8 @@ export function createDistrictView({ doc, stage, reducedMotion = false, onPick,
|
|
|
554
554
|
const lift = new THREE.Group();
|
|
555
555
|
lift.position.y = 0.17;
|
|
556
556
|
root.add(lift);
|
|
557
|
-
|
|
557
|
+
// The main street ends at the plaza's edge; it never runs under the plaza (that overlap flickered).
|
|
558
|
+
streetStrip(lift, { x: 0, z: (plan.street.from + plan.street.to) / 2, width: DISTRICT_STREETS.main * 2, length: plan.street.to - plan.street.from, centreLine: grown >= 2, dirt: grown === 0 });
|
|
558
559
|
streetStrip(lift, { x: -DISTRICT_STREETS.backStreet, z: 0, width: 1.6, length: L + 2, dirt: grown < 2 });
|
|
559
560
|
streetStrip(lift, { x: DISTRICT_STREETS.backStreet, z: 0, width: 1.6, length: L + 2, dirt: grown < 2 });
|
|
560
561
|
// A cross street between the blocks, and at the hall end of each block a sign saying what its work is for.
|
|
@@ -616,20 +617,20 @@ export function createDistrictView({ doc, stage, reducedMotion = false, onPick,
|
|
|
616
617
|
const hallChimney = new THREE.Vector3(2.8, 5.2, hallZ - 1.2);
|
|
617
618
|
const purposeText = work?.status === 'ok' ? (work.purpose.text ?? '目的は未登録') : '目的を読めません';
|
|
618
619
|
addLabel(`庁舎:${business.name}(${purposeText})`, new THREE.Vector3(0, 5.6, hallZ), 'is-plaza', { priority: 1 });
|
|
619
|
-
const plaza = mesh(new THREE.CircleGeometry(
|
|
620
|
+
const plaza = mesh(new THREE.CircleGeometry(plazaRadius - 0.25, 48), material(0xf1ece1, { roughness: 1 }), { y: 0.24, z: plazaZ, shadow: false });
|
|
620
621
|
plaza.rotation.x = -Math.PI / 2;
|
|
621
622
|
root.add(plaza);
|
|
622
|
-
const ring = mesh(new THREE.RingGeometry(
|
|
623
|
+
const ring = mesh(new THREE.RingGeometry(plazaRadius - 0.25, plazaRadius, 48), material(0xcfc8b8, { roughness: 1 }), { y: 0.25, z: plazaZ, shadow: false });
|
|
623
624
|
ring.rotation.x = -Math.PI / 2;
|
|
624
625
|
root.add(ring);
|
|
625
626
|
if (grown >= 3) {
|
|
626
627
|
// A fountain in the plaza once the district is a 街.
|
|
627
628
|
const basin = material(0xd9d3c6, { roughness: 0.8 });
|
|
628
|
-
root.add(mesh(new THREE.CylinderGeometry(1.0, 1.1, 0.35, 32), basin, { y: 0.37, z:
|
|
629
|
-
const water = mesh(new THREE.CylinderGeometry(0.88, 0.88, 0.04, 32), material(0x7fc4d6, { roughness: 0.15, metalness: 0.1, transparent: true, opacity: 0.85 }), { y: 0.52, z:
|
|
629
|
+
root.add(mesh(new THREE.CylinderGeometry(1.0, 1.1, 0.35, 32), basin, { y: 0.37, z: plazaZ }));
|
|
630
|
+
const water = mesh(new THREE.CylinderGeometry(0.88, 0.88, 0.04, 32), material(0x7fc4d6, { roughness: 0.15, metalness: 0.1, transparent: true, opacity: 0.85 }), { y: 0.52, z: plazaZ, shadow: false });
|
|
630
631
|
root.add(water);
|
|
631
|
-
root.add(mesh(new THREE.CylinderGeometry(0.12, 0.16, 0.7, 12), basin, { y: 0.7, z:
|
|
632
|
-
const jet = mesh(new THREE.ConeGeometry(0.22, 0.6, 12, 1, true), material(0xcfeef5, { transparent: true, opacity: 0.6, emissive: new THREE.Color(0x6fb6c9), emissiveIntensity: 0.2 }), { y: 1.25, z:
|
|
632
|
+
root.add(mesh(new THREE.CylinderGeometry(0.12, 0.16, 0.7, 12), basin, { y: 0.7, z: plazaZ }));
|
|
633
|
+
const jet = mesh(new THREE.ConeGeometry(0.22, 0.6, 12, 1, true), material(0xcfeef5, { transparent: true, opacity: 0.6, emissive: new THREE.Color(0x6fb6c9), emissiveIntensity: 0.2 }), { y: 1.25, z: plazaZ, shadow: false });
|
|
633
634
|
jet.rotation.x = Math.PI;
|
|
634
635
|
root.add(jet);
|
|
635
636
|
fountainJet = jet;
|
|
@@ -648,7 +649,7 @@ export function createDistrictView({ doc, stage, reducedMotion = false, onPick,
|
|
|
648
649
|
policies.forEach((policy, index) => {
|
|
649
650
|
const angle = Math.PI * (0.15 + (0.7 * index) / Math.max(policies.length - 1, 1));
|
|
650
651
|
const x = Math.cos(angle) * 2.8;
|
|
651
|
-
const z =
|
|
652
|
+
const z = plazaZ + Math.sin(angle) * 1.6;
|
|
652
653
|
if (policy.link === 'recorded') {
|
|
653
654
|
root.add(box(0.5, 1.2, 0.22, 0xa8a49a, { x, y: 0.8, z }, { roughness: 0.9 }));
|
|
654
655
|
} else {
|
|
@@ -194,6 +194,23 @@ export function districtStreetLots(sites, { minRows = 4 } = {}) {
|
|
|
194
194
|
return { lots, streets, rows, length };
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Where the gate, the plaza and the hall stand around the blocks of a district (`length` from
|
|
199
|
+
* `districtStreetLots`). The main street runs from beyond the gate to the plaza's edge and never into
|
|
200
|
+
* it, and the plaza stays clear of the first row of lots.
|
|
201
|
+
*/
|
|
202
|
+
export function districtGroundPlan(length) {
|
|
203
|
+
const plazaRadius = 3.85;
|
|
204
|
+
const plazaZ = -length / 2 - 1.5 - plazaRadius;
|
|
205
|
+
return {
|
|
206
|
+
gateZ: length / 2 + 4,
|
|
207
|
+
plazaZ,
|
|
208
|
+
plazaRadius,
|
|
209
|
+
hallZ: plazaZ - 5.4,
|
|
210
|
+
street: { from: plazaZ + plazaRadius, to: length / 2 + 12 },
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
197
214
|
/*
|
|
198
215
|
* The town grows (story-world-work-sites-and-gaps-v1, AC-22〜24). It grows only from evidenced outcomes
|
|
199
216
|
* and closed gaps, never from activity: counting completions or edits would let splitting work inflate it.
|