@unson/brainbase-mcp 0.11.0 → 0.13.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.
- package/contracts/judgment-value-proof/schema.json +19 -1
- package/dist/judgment-problem-snapshot-http.d.ts +40 -0
- package/dist/judgment-problem-snapshot-http.js +255 -0
- package/dist/judgment-value-proof.d.ts +15 -0
- package/dist/judgment-value-proof.js +17 -0
- package/dist/knowledge-promotion.d.ts +61 -3
- package/dist/knowledge-promotion.js +112 -1
- package/dist/local-web-host.d.ts +4 -4
- package/dist/local-web-host.js +137 -12
- package/package.json +6 -2
- package/ui/graph-registry-view.js +27 -1
- package/ui/local-web-shell.js +80 -8
- package/ui/project-workspace.js +1 -1
- package/ui/value-proof-review.css +54 -0
- package/ui/value-proof-review.js +127 -5
- package/ui/world-model-view.css +61 -0
- package/ui/world-model-view.js +215 -10
package/ui/value-proof-review.js
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* The host supplies a same-origin request function and a review token. A host
|
|
5
5
|
* that cannot read a local journal may replace the unavailable notice. This
|
|
6
6
|
* module owns presentation only: it never treats an unavailable journal as an
|
|
7
|
-
* empty list
|
|
8
|
-
* the
|
|
7
|
+
* empty list and never edits the judgment journal. Recorded basis target IDs
|
|
8
|
+
* are shown as trace metadata; no source URL is invented when the payload has
|
|
9
|
+
* no source-read contract.
|
|
9
10
|
*
|
|
10
11
|
* The screen follows the organization edition's pattern (workspace-kit): page
|
|
11
12
|
* head, the 記録の範囲 notice, filters, metrics, and the 委任の地図 as a ledger
|
|
@@ -547,11 +548,98 @@ function renderDelegationMap(doc, state, callbacks) {
|
|
|
547
548
|
}
|
|
548
549
|
|
|
549
550
|
function basisText(entry) {
|
|
550
|
-
const application = text(entry
|
|
551
|
-
const layer = BASIS_LAYER_LABELS[entry
|
|
551
|
+
const application = text(entry?.application) ?? '適用内容なし';
|
|
552
|
+
const layer = BASIS_LAYER_LABELS[entry?.layer];
|
|
552
553
|
return layer ? `[${layer}] ${application}` : application;
|
|
553
554
|
}
|
|
554
555
|
|
|
556
|
+
function basisTargetText(entry) {
|
|
557
|
+
const entityId = text(entry?.entity_id);
|
|
558
|
+
if (!entityId) return '対象の記録なし';
|
|
559
|
+
const version = text(entry?.version);
|
|
560
|
+
return version ? `${entityId}(版 ${version})` : entityId;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const SOURCE_READ_STATES = Object.freeze(['loading', 'available', 'not_found', 'ambiguous', 'forbidden', 'unavailable']);
|
|
564
|
+
const SOURCE_READ_LABELS = Object.freeze({
|
|
565
|
+
loading: '出典を確認中…',
|
|
566
|
+
not_found: '出典が見つかりません(未確認)',
|
|
567
|
+
ambiguous: '同じIDの出典が複数あります(未確認)',
|
|
568
|
+
forbidden: '出典を読む権限がありません(未確認)',
|
|
569
|
+
unavailable: '出典を確認できません(未確認)',
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
function sourceContract(entry) {
|
|
573
|
+
const source = entry?.source;
|
|
574
|
+
if (!isRecord(source) || source.kind !== 'local_graph') return null;
|
|
575
|
+
const entityId = text(source.entity_id);
|
|
576
|
+
const entityType = text(source.entity_type);
|
|
577
|
+
if (!entityId || !entityType || entityId !== text(entry?.entity_id)) return null;
|
|
578
|
+
return source;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function sourceKey(source) {
|
|
582
|
+
return [source.kind, source.entity_id, source.entity_type, source.version ?? '', source.digest ?? ''].join('\u0000');
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function sourceReadState(value) {
|
|
586
|
+
const state = text(value?.state);
|
|
587
|
+
return SOURCE_READ_STATES.includes(state) ? state : 'unavailable';
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function renderBasisTarget(doc, entry, state) {
|
|
591
|
+
const target = makeElement(doc, 'span', { className: 'bb-vpr-source-target' });
|
|
592
|
+
target.append(makeElement(doc, 'span', { text: basisTargetText(entry) }));
|
|
593
|
+
const source = sourceContract(entry);
|
|
594
|
+
if (!source) return target;
|
|
595
|
+
const key = sourceKey(source);
|
|
596
|
+
const read = state.sourceReads instanceof Map ? state.sourceReads.get(key) : null;
|
|
597
|
+
if (!read) {
|
|
598
|
+
target.append(makeElement(doc, 'span', {
|
|
599
|
+
className: 'bb-vpr-source-status is-unavailable',
|
|
600
|
+
text: '出典の読み取り先を確認できません(未確認)',
|
|
601
|
+
}));
|
|
602
|
+
return target;
|
|
603
|
+
}
|
|
604
|
+
const readState = sourceReadState(read);
|
|
605
|
+
if (readState === 'available') {
|
|
606
|
+
const link = makeElement(doc, 'a', {
|
|
607
|
+
className: 'bb-vpr-source-link',
|
|
608
|
+
text: '出典を開く',
|
|
609
|
+
attrs: { href: `#graph?entity_id=${encodeURIComponent(source.entity_id)}` },
|
|
610
|
+
});
|
|
611
|
+
target.append(link);
|
|
612
|
+
return target;
|
|
613
|
+
}
|
|
614
|
+
target.append(makeElement(doc, 'span', {
|
|
615
|
+
className: `bb-vpr-source-status is-${readState}`,
|
|
616
|
+
text: SOURCE_READ_LABELS[readState] ?? SOURCE_READ_LABELS.unavailable,
|
|
617
|
+
}));
|
|
618
|
+
return target;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function journalSetupGuide(doc) {
|
|
622
|
+
const guide = makeElement(doc, 'div', { className: 'bb-vpr-setup-guide' });
|
|
623
|
+
guide.append(makeElement(doc, 'strong', { text: '接続を直す' }));
|
|
624
|
+
guide.append(makeElement(doc, 'p', { text: '判断journalの場所を指定して画面を起動し直したあと、「再試行」を押してください。' }));
|
|
625
|
+
const list = makeElement(doc, 'ol');
|
|
626
|
+
const journalOption = makeElement(doc, 'li');
|
|
627
|
+
journalOption.append(
|
|
628
|
+
makeElement(doc, 'span', { text: '起動時に ' }),
|
|
629
|
+
makeElement(doc, 'code', { text: 'brainbase review:serve --journal <判断journalの場所>' }),
|
|
630
|
+
makeElement(doc, 'span', { text: ' を指定する' }),
|
|
631
|
+
);
|
|
632
|
+
const journalEnv = makeElement(doc, 'li');
|
|
633
|
+
journalEnv.append(
|
|
634
|
+
makeElement(doc, 'span', { text: 'または環境変数 ' }),
|
|
635
|
+
makeElement(doc, 'code', { text: 'BRAINBASE_JUDGMENT_JOURNAL_DIR=<判断journalの場所>' }),
|
|
636
|
+
makeElement(doc, 'span', { text: ' を設定する' }),
|
|
637
|
+
);
|
|
638
|
+
list.append(journalOption, journalEnv);
|
|
639
|
+
guide.append(list);
|
|
640
|
+
return guide;
|
|
641
|
+
}
|
|
642
|
+
|
|
555
643
|
function inheritanceText(inheritance) {
|
|
556
644
|
const sources = Array.isArray(inheritance?.sources) ? inheritance.sources : [];
|
|
557
645
|
if (sources.length === 0) return '引き継ぎの記録なし';
|
|
@@ -733,6 +821,7 @@ function renderJudgmentDetail(doc, state, item, callbacks) {
|
|
|
733
821
|
title: '根拠と引き継ぎ',
|
|
734
822
|
content: facts(doc, [
|
|
735
823
|
['根拠', basis.length > 0 ? basis.map(basisText).join(' / ') : '根拠の記録なし'],
|
|
824
|
+
['根拠の対象', basis.length > 0 ? basis.map((entry) => renderBasisTarget(doc, entry, state)) : '対象の記録なし'],
|
|
736
825
|
['過去の学習の再利用', reuse === true ? 'あり' : reuse === false ? 'なし' : '未確認'],
|
|
737
826
|
['引き継ぎ', inheritanceText(proof.decision.inheritance)],
|
|
738
827
|
]),
|
|
@@ -861,13 +950,18 @@ export function renderValueProofReview(root, state, callbacks = {}, options = {}
|
|
|
861
950
|
if (home.status === 'unavailable') {
|
|
862
951
|
const hostNotice = options.unavailableNotice;
|
|
863
952
|
const title = makeElement(doc, 'p', { className: 'bb-vpr-notice-title', text: text(hostNotice?.title) ?? '判断journalに接続できません' });
|
|
953
|
+
const retry = workspaceButton(doc, { text: '再試行', variant: 'primary', onClick: () => void callbacks.onReload?.() });
|
|
954
|
+
const defaultGuide = hostNotice ? null : journalSetupGuide(doc);
|
|
864
955
|
surface.append(workspaceNotice(doc, {
|
|
865
956
|
label: '記録の範囲',
|
|
866
957
|
tone: 'danger',
|
|
867
958
|
text: noticeBody(doc,
|
|
868
959
|
title,
|
|
869
960
|
home.root ? `場所: ${home.root}(${home.reason})` : `理由: ${home.reason}`,
|
|
870
|
-
|
|
961
|
+
text(hostNotice?.guidance) ?? (defaultGuide ? null : '接続元の案内に従って記録を読める状態にしてください。'),
|
|
962
|
+
defaultGuide,
|
|
963
|
+
retry,
|
|
964
|
+
'接続できる状態にしたら「再試行」を押してください。0件としては扱いません。'),
|
|
871
965
|
}));
|
|
872
966
|
return finish(null, unreadable);
|
|
873
967
|
}
|
|
@@ -934,6 +1028,8 @@ export function createValueProofReviewUI({
|
|
|
934
1028
|
clipboard,
|
|
935
1029
|
/** Optional `{ title, guidance }` for hosts where the local journal hint does not apply. */
|
|
936
1030
|
unavailableNotice,
|
|
1031
|
+
/** Optional host-owned reader for typed basis sources. It must not invent a URL. */
|
|
1032
|
+
sourceReader,
|
|
937
1033
|
autoLoad = true,
|
|
938
1034
|
} = {}) {
|
|
939
1035
|
if (!root) throw new TypeError('root is required');
|
|
@@ -958,6 +1054,30 @@ export function createValueProofReviewUI({
|
|
|
958
1054
|
save: { state: 'idle', message: '' },
|
|
959
1055
|
focusCard: false,
|
|
960
1056
|
consultMessage: '',
|
|
1057
|
+
sourceReads: new Map(),
|
|
1058
|
+
};
|
|
1059
|
+
|
|
1060
|
+
const readSelectedSources = async (item) => {
|
|
1061
|
+
if (typeof sourceReader !== 'function' || !item?.proof?.decision?.basis) return;
|
|
1062
|
+
const sources = item.proof.decision.basis.map(sourceContract).filter(Boolean);
|
|
1063
|
+
for (const source of sources) {
|
|
1064
|
+
const key = sourceKey(source);
|
|
1065
|
+
state.sourceReads.set(key, { state: 'loading' });
|
|
1066
|
+
}
|
|
1067
|
+
if (sources.length > 0) controller.render();
|
|
1068
|
+
await Promise.all(sources.map(async (source) => {
|
|
1069
|
+
const key = sourceKey(source);
|
|
1070
|
+
let result;
|
|
1071
|
+
try {
|
|
1072
|
+
result = await sourceReader(source);
|
|
1073
|
+
} catch {
|
|
1074
|
+
result = { state: 'unavailable' };
|
|
1075
|
+
}
|
|
1076
|
+
if (state.selectedKey !== itemKey(item.proof)) return;
|
|
1077
|
+
const normalized = isRecord(result) ? result : {};
|
|
1078
|
+
state.sourceReads.set(key, { ...normalized, state: sourceReadState(normalized) });
|
|
1079
|
+
}));
|
|
1080
|
+
if (state.selectedKey === itemKey(item.proof)) controller.render();
|
|
961
1081
|
};
|
|
962
1082
|
|
|
963
1083
|
const resetJudgmentInput = () => {
|
|
@@ -1000,6 +1120,8 @@ export function createValueProofReviewUI({
|
|
|
1000
1120
|
state.focusCard = true;
|
|
1001
1121
|
controller.render();
|
|
1002
1122
|
state.focusCard = false;
|
|
1123
|
+
const item = findItem(state, key);
|
|
1124
|
+
if (item) void readSelectedSources(item);
|
|
1003
1125
|
},
|
|
1004
1126
|
onDraft(patch) {
|
|
1005
1127
|
state.draft = { ...state.draft, ...patch };
|
package/ui/world-model-view.css
CHANGED
|
@@ -60,6 +60,59 @@
|
|
|
60
60
|
justify-self: start;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/* First-user registration stays in the empty-state notice, beside the fact it affects. */
|
|
64
|
+
.bb-wm-registration-form {
|
|
65
|
+
display: grid;
|
|
66
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
67
|
+
gap: var(--bb-space-3);
|
|
68
|
+
margin-top: var(--bb-space-2);
|
|
69
|
+
max-width: 680px;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.bb-wm-registration-field {
|
|
73
|
+
display: grid;
|
|
74
|
+
gap: 5px;
|
|
75
|
+
min-width: 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.bb-wm-registration-field > label {
|
|
79
|
+
color: var(--bb-color-ink);
|
|
80
|
+
font-size: 12px;
|
|
81
|
+
font-weight: 680;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.bb-wm-registration-input {
|
|
85
|
+
width: 100%;
|
|
86
|
+
min-height: 34px;
|
|
87
|
+
padding: 7px 9px;
|
|
88
|
+
border: 1px solid var(--bb-color-border-strong);
|
|
89
|
+
border-radius: var(--bb-radius-sm);
|
|
90
|
+
background: var(--bb-color-paper);
|
|
91
|
+
color: var(--bb-color-ink);
|
|
92
|
+
font: inherit;
|
|
93
|
+
font-size: 13px;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.bb-wm-registration-input:focus {
|
|
97
|
+
outline: 2px solid var(--bb-color-accent);
|
|
98
|
+
outline-offset: 1px;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.bb-wm-registration-status {
|
|
102
|
+
grid-column: 1 / -1;
|
|
103
|
+
color: var(--bb-color-muted);
|
|
104
|
+
font-size: 12px;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.bb-wm-registration-form .bb-ws-button {
|
|
108
|
+
justify-self: start;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.bb-wm-registration-boundary {
|
|
112
|
+
color: var(--bb-color-muted);
|
|
113
|
+
font-size: 12px;
|
|
114
|
+
}
|
|
115
|
+
|
|
63
116
|
/* The local shell resets `.bb-shell button { font: inherit }`, which outranks the kit's button type. */
|
|
64
117
|
.bb-wm .bb-ws-button {
|
|
65
118
|
font-size: 12px;
|
|
@@ -159,6 +212,14 @@
|
|
|
159
212
|
}
|
|
160
213
|
|
|
161
214
|
@media (max-width: 760px) {
|
|
215
|
+
.bb-wm-registration-form {
|
|
216
|
+
grid-template-columns: minmax(0, 1fr);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.bb-wm-registration-status {
|
|
220
|
+
grid-column: auto;
|
|
221
|
+
}
|
|
222
|
+
|
|
162
223
|
.bb-wm-view-ledger .bb-ws-ledger-row {
|
|
163
224
|
grid-template-columns: minmax(0, 1fr) 72px;
|
|
164
225
|
}
|
package/ui/world-model-view.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
/*
|
|
2
|
-
*
|
|
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
|
|
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
|
|
12
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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();
|