@unson/brainbase-mcp 0.11.0 → 0.12.0

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.
@@ -1,15 +1,17 @@
1
1
  /*
2
- * Read-only World Model view (現状と見通し).
2
+ * World Model view (現状と見通し).
3
3
  *
4
4
  * Shows recorded observations apart from the variables and models that are
5
5
  * the owner's way of seeing them (which may be hypotheses), plus model
6
6
  * adoptions. The host injects the fetcher and base path; this module keeps
7
- * no global state and never writes. Each section loads on its own: a failed
7
+ * no global state. The empty state also offers the host's first-user
8
+ * variable-and-observation registration route. Each section loads on its own: a failed
8
9
  * or unverifiable section is reported in place and never shown as zero items.
9
10
  *
10
11
  * Drawn with the organization edition's screen pattern (workspace-kit): a
11
- * section title under the page head, then one read-only ledger per block.
12
- * Rows are not selectable, so the view never writes to the host's rail.
12
+ * section title under the page head, then one ledger per block. Rows are not
13
+ * selectable, so the view never writes to the host's rail; the only write is
14
+ * the explicit first-registration form shown for a confirmed empty state.
13
15
  *
14
16
  * A section the host cannot serve answers `{ status: 'unavailable', reason }`
15
17
  * (HTTP 200). It is shown as unavailable, never as zero items; a host with no
@@ -333,7 +335,138 @@ function unavailableSummary(doc, state, unavailableNotice) {
333
335
  }
334
336
 
335
337
  /**
336
- * Render the whole view into a host-owned root: a section title and read-only ledgers.
338
+ * The current Personal Web is a read-mostly World Model surface. Show the
339
+ * first-registration entry only after variables and observations have both
340
+ * been confirmed empty;
341
+ * an unknown, partial or unavailable section must not be presented as an
342
+ * empty World Model.
343
+ */
344
+ function registrationField(doc, { id, label, type = 'text', value = '', required = true, attrs = {}, options = [] } = {}) {
345
+ const field = makeElement(doc, 'div', { className: 'bb-wm-registration-field' });
346
+ const labelElement = makeElement(doc, 'label', { text: label, attrs: { for: id } });
347
+ const input = makeElement(doc, type === 'select' ? 'select' : 'input', {
348
+ className: 'bb-wm-registration-input',
349
+ attrs: { id, name: id, type: type === 'select' ? undefined : type, required, ...attrs },
350
+ });
351
+ if (type === 'select') {
352
+ for (const option of options) {
353
+ input.append(makeElement(doc, 'option', { text: option.label, attrs: { value: option.value } }));
354
+ }
355
+ input.value = value;
356
+ } else {
357
+ input.value = value;
358
+ }
359
+ field.append(labelElement, input);
360
+ return { field, input, id };
361
+ }
362
+
363
+ function registrationValue(raw, valueKind) {
364
+ const value = String(raw ?? '').trim();
365
+ if (!value) return { error: '最初の観測値を入力してください。' };
366
+ if (valueKind === 'number') {
367
+ const number = Number(value);
368
+ return Number.isFinite(number) ? { value: number } : { error: '数値の観測値を入力してください。' };
369
+ }
370
+ if (valueKind === 'boolean') {
371
+ if (value === 'true' || value === 'はい') return { value: true };
372
+ if (value === 'false' || value === 'いいえ') return { value: false };
373
+ return { error: '真偽値は「true」または「false」で入力してください。' };
374
+ }
375
+ return { value };
376
+ }
377
+
378
+ function emptyRegistrationEntry(doc, state, callbacks, registration = {}) {
379
+ if (state.variables?.state !== 'empty' || state.observations?.state !== 'empty') return null;
380
+ const formFields = {};
381
+ const form = makeElement(doc, 'form', {
382
+ className: 'bb-wm-registration-form',
383
+ attrs: { 'aria-label': '最初の変数と観測値を登録' },
384
+ });
385
+ const fields = [
386
+ registrationField(doc, { id: 'bb-wm-registration-meaning', label: '何を記録するか', attrs: { autocomplete: 'off' } }),
387
+ registrationField(doc, { id: 'bb-wm-registration-subject', label: '対象', attrs: { autocomplete: 'off' } }),
388
+ registrationField(doc, {
389
+ id: 'bb-wm-registration-value-kind', label: '値の種類', type: 'select', value: 'number',
390
+ options: [
391
+ { value: 'number', label: '数値' },
392
+ { value: 'boolean', label: 'はい/いいえ' },
393
+ { value: 'string', label: '文字' },
394
+ { value: 'state', label: '状態' },
395
+ ],
396
+ }),
397
+ registrationField(doc, { id: 'bb-wm-registration-unit', label: '単位(数値の場合)', required: false, attrs: { autocomplete: 'off' } }),
398
+ registrationField(doc, {
399
+ id: 'bb-wm-registration-aggregation', label: '集計方法', type: 'select', value: 'last',
400
+ options: [
401
+ { value: 'last', label: '最新の値' },
402
+ { value: 'sum', label: '合計' },
403
+ { value: 'average', label: '平均' },
404
+ { value: 'count', label: '件数' },
405
+ { value: 'none', label: '集計しない' },
406
+ ],
407
+ }),
408
+ registrationField(doc, { id: 'bb-wm-registration-granularity', label: '記録の単位', value: '日', attrs: { autocomplete: 'off' } }),
409
+ registrationField(doc, { id: 'bb-wm-registration-method', label: '測り方', attrs: { autocomplete: 'off' } }),
410
+ registrationField(doc, { id: 'bb-wm-registration-value', label: '最初の観測値', attrs: { autocomplete: 'off' } }),
411
+ ];
412
+ for (const { field, input, id } of fields) {
413
+ formFields[id] = input;
414
+ form.append(field);
415
+ }
416
+ const status = makeElement(doc, 'p', {
417
+ className: 'bb-wm-registration-status',
418
+ text: registration.message ?? '登録すると、この画面の記録として読み直します。',
419
+ attrs: { 'aria-live': 'polite' },
420
+ });
421
+ const submit = workspaceButton(doc, {
422
+ text: registration.state === 'saving' ? '登録しています…' : '変数と観測値を登録',
423
+ variant: 'primary',
424
+ disabled: registration.state === 'saving',
425
+ attrs: { type: 'submit' },
426
+ });
427
+ form.addEventListener('submit', (event) => {
428
+ event?.preventDefault?.();
429
+ const parsed = registrationValue(formFields['bb-wm-registration-value'].value, formFields['bb-wm-registration-value-kind'].value);
430
+ const payload = {
431
+ meaning: String(formFields['bb-wm-registration-meaning'].value ?? '').trim(),
432
+ subject: String(formFields['bb-wm-registration-subject'].value ?? '').trim(),
433
+ valueKind: String(formFields['bb-wm-registration-value-kind'].value ?? '').trim(),
434
+ unit: String(formFields['bb-wm-registration-unit'].value ?? '').trim(),
435
+ aggregation: String(formFields['bb-wm-registration-aggregation'].value ?? '').trim(),
436
+ granularity: String(formFields['bb-wm-registration-granularity'].value ?? '').trim(),
437
+ measurementMethod: String(formFields['bb-wm-registration-method'].value ?? '').trim(),
438
+ value: parsed.value,
439
+ };
440
+ if (!payload.meaning || !payload.subject || !payload.granularity || !payload.measurementMethod) {
441
+ status.textContent = '何を記録するか、対象、記録の単位、測り方を入力してください。';
442
+ return;
443
+ }
444
+ if (payload.valueKind === 'number' && !payload.unit) {
445
+ status.textContent = '数値を記録する場合は単位を入力してください。';
446
+ return;
447
+ }
448
+ if (parsed.error) {
449
+ status.textContent = parsed.error;
450
+ return;
451
+ }
452
+ return callbacks.onRegister?.(payload);
453
+ });
454
+ form.append(status, submit);
455
+ const guidance = workspaceNotice(doc, {
456
+ label: '最初の登録',
457
+ text: [
458
+ makeElement(doc, 'p', { text: 'ここから変数と最初の観測値を登録できます。登録者と記録日時は、この手元のWebホストが決めます。' }),
459
+ form,
460
+ makeElement(doc, 'p', { className: 'bb-wm-registration-boundary', text: 'モデルの作成・採用や、既存観測の訂正はこの初回登録の対象外です。確認と権限が必要な別の手順で扱います。' }),
461
+ ],
462
+ });
463
+ guidance.className += ' bb-wm-registration-guidance';
464
+ return guidance;
465
+ }
466
+
467
+ /**
468
+ * Render the whole view into a host-owned root: a section title, optional first-registration form,
469
+ * and ledgers.
337
470
  * `options.unavailableNotice` is the host's `{ title, guidance }` shown once
338
471
  * when every section is unavailable.
339
472
  */
@@ -343,7 +476,7 @@ export function renderWorldModelView(root, state, callbacks = {}, options = {})
343
476
  const surface = makeElement(doc, 'section', { className: 'bb-wm', attrs: { 'data-contract-version': WORLD_MODEL_VIEW_CONTRACT_VERSION, 'aria-label': '現状と見通し' } });
344
477
  surface.append(workspaceSectionTitle(doc, {
345
478
  title: '現状と見通し',
346
- lead: '見方(変数とモデル。仮説を含みます)と、記録された観測を分けて表示します。この欄は表示だけです。',
479
+ lead: '見方(変数とモデル。仮説を含みます)と、記録された観測を分けて表示します。最初の変数と観測値はここから登録できます。',
347
480
  }));
348
481
  const unavailable = unavailableSummary(doc, state, options.unavailableNotice);
349
482
  if (unavailable) {
@@ -352,10 +485,20 @@ export function renderWorldModelView(root, state, callbacks = {}, options = {})
352
485
  return surface;
353
486
  }
354
487
 
488
+ if (options.registration?.state === 'error' && text(options.registration.message)) {
489
+ surface.append(workspaceNotice(doc, {
490
+ label: '登録結果',
491
+ tone: 'danger',
492
+ text: options.registration.message,
493
+ }));
494
+ }
495
+
355
496
  const variables = namesOf(state.variables);
356
497
  const models = namesOf(state.models);
357
498
 
358
499
  const view = block(doc, '変数とモデル', '見方:変数とモデル', '何を測り、どう関係すると考えているか。認識の状態は、確かめた度合いです。観測ではありません。');
500
+ const registrationGuidance = emptyRegistrationEntry(doc, state, callbacks, options.registration);
501
+ if (registrationGuidance) view.append(registrationGuidance);
359
502
  const statuses = {
360
503
  variables: sectionStatus(doc, '変数', 'variables', state.variables, callbacks, '変数はまだ登録がありません。'),
361
504
  models: sectionStatus(doc, 'モデル', 'models', state.models, callbacks, 'モデルはまだ登録がありません。'),
@@ -407,13 +550,14 @@ async function readError(response) {
407
550
  }
408
551
 
409
552
  /**
410
- * Mounts the read-only World Model view.
553
+ * Mounts the World Model view.
411
554
  *
412
555
  * @param {object} options
413
556
  * @param {Element} options.root Where the view is drawn.
414
557
  * @param {Element} [options.rail] Accepted for the host's screen contract; the view never writes to it.
415
558
  * @param {Document} [options.document] The document to build elements with (default: the global one).
416
559
  * @param {Function} [options.fetcher] `fetch`-compatible function for the sections.
560
+ * @param {string} [options.token] Local Web write token sent with registration requests.
417
561
  * @param {string} [options.basePath='/api/world-model'] Where `{variables,models,observations,adoptions}` are served.
418
562
  * @param {boolean} [options.autoLoad=true] Reads every section on mount.
419
563
  * @param {{ title: string, guidance?: string, reasonLabels?: Record<string, string> }} [options.unavailableNotice]
@@ -425,10 +569,11 @@ async function readError(response) {
425
569
  */
426
570
  export function createWorldModelView({
427
571
  root,
428
- /** Accepted for the host's screen contract; the read-only view keeps the rail to the screen's editor. */
572
+ /** Accepted for the host's screen contract; the view keeps the rail to the screen's editor. */
429
573
  rail: _rail,
430
574
  document: explicitDocument,
431
575
  fetcher,
576
+ token,
432
577
  basePath = '/api/world-model',
433
578
  autoLoad = true,
434
579
  unavailableNotice,
@@ -441,12 +586,16 @@ export function createWorldModelView({
441
586
  const base = String(basePath).replace(/\/+$/u, '');
442
587
  const state = Object.fromEntries(WORLD_MODEL_SECTIONS.map((section) => [section, { state: 'loading', items: null }]));
443
588
 
444
- const callbacks = { onRetry: (section) => controller.loadSection(section) };
589
+ const registration = { state: 'idle', message: null };
590
+ const callbacks = {
591
+ onRetry: (section) => controller.loadSection(section),
592
+ onRegister: (payload) => controller.register(payload),
593
+ };
445
594
 
446
595
  const controller = {
447
596
  get state() { return state; },
448
597
  render() {
449
- renderWorldModelView(root, state, callbacks, { document: doc, unavailableNotice });
598
+ renderWorldModelView(root, state, callbacks, { document: doc, unavailableNotice, registration });
450
599
  return controller;
451
600
  },
452
601
  async loadSection(section) {
@@ -477,6 +626,62 @@ export function createWorldModelView({
477
626
  await Promise.all(WORLD_MODEL_SECTIONS.map((section) => controller.loadSection(section)));
478
627
  return state;
479
628
  },
629
+ async register(payload) {
630
+ if (!request) {
631
+ registration.state = 'error';
632
+ registration.message = '登録先が設定されていません。';
633
+ controller.render();
634
+ return;
635
+ }
636
+ registration.state = 'saving';
637
+ registration.message = '登録しています…';
638
+ controller.render();
639
+ const write = async (path, body) => {
640
+ const response = await request(`${base}${path}`, {
641
+ method: 'POST',
642
+ headers: {
643
+ 'Content-Type': 'application/json',
644
+ ...(token ? { 'X-Brainbase-Review-Token': token } : {}),
645
+ },
646
+ body: JSON.stringify(body),
647
+ });
648
+ if (!response.ok) {
649
+ const error = await readError(response);
650
+ throw new Error(error.message ?? error.code ?? `HTTP ${response.status}`);
651
+ }
652
+ return response.json();
653
+ };
654
+ let variableReference = null;
655
+ try {
656
+ const created = await write('/variables', {
657
+ meaning: payload.meaning,
658
+ subject: payload.subject,
659
+ valueKind: payload.valueKind,
660
+ ...(payload.unit ? { unit: payload.unit } : {}),
661
+ aggregation: payload.aggregation,
662
+ granularity: payload.granularity,
663
+ measurementMethod: payload.measurementMethod,
664
+ });
665
+ const reference = created?.reference;
666
+ if (!reference || typeof reference.id !== 'string' || typeof reference.revision !== 'string') {
667
+ throw new Error('変数の登録結果を読み取れませんでした');
668
+ }
669
+ variableReference = reference;
670
+ await write('/observations', { variableRef: reference, value: payload.value });
671
+ registration.state = 'idle';
672
+ registration.message = null;
673
+ await controller.load();
674
+ } catch (error) {
675
+ registration.state = 'error';
676
+ const detail = error instanceof Error ? error.message : '登録できませんでした';
677
+ registration.message = variableReference
678
+ ? `変数は登録されましたが、最初の観測値を保存できませんでした。${detail}`
679
+ : detail;
680
+ await controller.load();
681
+ controller.render();
682
+ }
683
+ return registration;
684
+ },
480
685
  };
481
686
  controller.render();
482
687
  if (autoLoad) void controller.load();