@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unson/brainbase-mcp",
3
- "version": "0.22.2",
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": "3a54233ed19d3ddae6b1f346955fecb878af962e"
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
- state.detail = result.state === 'error' && result.status === 404
1018
+ const detail = result.state === 'error' && result.status === 404
940
1019
  ? { id, state: 'not_found', reason: 'このプロジェクトは見つかりません。一覧を読み直してください。' }
941
1020
  : { id, ...normalizeProjectDetail(result) };
942
- controller.render();
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 gateZ = L / 2 + 4;
539
- const hallZ = -L / 2 - 7;
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
- streetStrip(lift, { x: 0, z: (gateZ + 8 + hallZ + 3) / 2, width: DISTRICT_STREETS.main * 2, length: gateZ + 8 - (hallZ + 3), centreLine: grown >= 2, dirt: grown === 0 });
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(3.6, 48), material(0xf1ece1, { roughness: 1 }), { y: 0.2, z: hallZ + 5.4, shadow: false });
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(3.6, 3.85, 48), material(0xcfc8b8, { roughness: 1 }), { y: 0.21, z: hallZ + 5.4, shadow: false });
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: hallZ + 5.4 }));
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: hallZ + 5.4, shadow: false });
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: hallZ + 5.4 }));
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: hallZ + 5.4, shadow: false });
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 = hallZ + 5.4 + Math.sin(angle) * 1.6;
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.