@unson/brainbase-mcp 0.22.5 → 0.23.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/dist/judgment-frame-read.d.ts +34 -0
- package/dist/judgment-frame-read.js +223 -0
- package/dist/judgment-frame.d.ts +2 -0
- package/dist/judgment-frame.js +1 -0
- package/package.json +2 -2
- package/ui/foundation-overview.css +3 -0
- package/ui/foundation-overview.js +99 -23
- package/ui/objective-editor.css +2 -0
- package/ui/objective-editor.js +1 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { type JudgmentFrameKind, type JudgmentFrameRecordsPage } from './judgment-frame.js';
|
|
3
|
+
type JudgmentFrameReadResult = {
|
|
4
|
+
readonly status: 'ok';
|
|
5
|
+
readonly data: {
|
|
6
|
+
readonly schema_version: 'brainbase-judgment-frame-read-v1';
|
|
7
|
+
readonly catalog_digest: `sha256:${string}`;
|
|
8
|
+
readonly records: readonly JudgmentFrameReadRecord[];
|
|
9
|
+
};
|
|
10
|
+
} | {
|
|
11
|
+
readonly status: 'unavailable' | 'error';
|
|
12
|
+
readonly error: {
|
|
13
|
+
readonly code: string;
|
|
14
|
+
readonly message: string;
|
|
15
|
+
readonly details?: unknown;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export type JudgmentFrameReadRecord = {
|
|
19
|
+
readonly kind: JudgmentFrameKind;
|
|
20
|
+
readonly ref: string;
|
|
21
|
+
readonly body: string;
|
|
22
|
+
readonly body_digest: `sha256:${string}`;
|
|
23
|
+
readonly version: string;
|
|
24
|
+
readonly version_source: 'graph' | 'body_digest';
|
|
25
|
+
readonly applicability_scope: string;
|
|
26
|
+
readonly applicability_scope_source: 'graph' | 'unspecified';
|
|
27
|
+
};
|
|
28
|
+
export type JudgmentFrameReadDependencies = {
|
|
29
|
+
/** Read every record of one kind visible to the caller, at most `limit`. */
|
|
30
|
+
readonly loadRecords: (kind: JudgmentFrameKind, limit: number) => Promise<JudgmentFrameRecordsPage>;
|
|
31
|
+
};
|
|
32
|
+
export declare const judgmentFrameReadTools: Tool[];
|
|
33
|
+
export declare function handleJudgmentFrameReadToolCall(name: string, args: Record<string, unknown>, dependencies: JudgmentFrameReadDependencies): Promise<JudgmentFrameReadResult | null>;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { buildJudgmentFrameCatalog, } from './judgment-frame.js';
|
|
3
|
+
const KINDS = ['philosophy', 'objective', 'model'];
|
|
4
|
+
const FRAME_PAGE_LIMIT = 500;
|
|
5
|
+
const MAX_SELECTIONS = 24;
|
|
6
|
+
const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/u;
|
|
7
|
+
const MAX_REF_LENGTH = 1_000;
|
|
8
|
+
const UNSPECIFIED_APPLICABILITY_SCOPE = 'Graphに適用範囲の記載なし';
|
|
9
|
+
const isRecord = (value) => (Boolean(value) && typeof value === 'object' && !Array.isArray(value));
|
|
10
|
+
const text = (value) => (typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined);
|
|
11
|
+
const versionText = (value) => (text(value) ?? (typeof value === 'number' && Number.isFinite(value) ? String(value) : undefined));
|
|
12
|
+
function canonicalJson(value) {
|
|
13
|
+
if (Array.isArray(value))
|
|
14
|
+
return `[${value.map(canonicalJson).join(',')}]`;
|
|
15
|
+
if (isRecord(value)) {
|
|
16
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
|
|
17
|
+
}
|
|
18
|
+
const serialized = JSON.stringify(value);
|
|
19
|
+
return serialized === undefined ? 'null' : serialized;
|
|
20
|
+
}
|
|
21
|
+
function scopeText(value) {
|
|
22
|
+
const stringValue = text(value);
|
|
23
|
+
if (stringValue)
|
|
24
|
+
return stringValue;
|
|
25
|
+
if (!isRecord(value) && !Array.isArray(value))
|
|
26
|
+
return undefined;
|
|
27
|
+
const serialized = canonicalJson(value);
|
|
28
|
+
return serialized && serialized !== '{}' && serialized !== '[]' ? serialized : undefined;
|
|
29
|
+
}
|
|
30
|
+
function itemBody(kind, payload) {
|
|
31
|
+
const foundation = isRecord(payload.foundation) ? payload.foundation : {};
|
|
32
|
+
if (kind === 'philosophy') {
|
|
33
|
+
const statement = text(payload.statement) ?? text(foundation.meaning);
|
|
34
|
+
const name = text(payload.display_name) ?? text(payload.title);
|
|
35
|
+
if (!statement)
|
|
36
|
+
return undefined;
|
|
37
|
+
return name && name !== statement ? `${name}:${statement}` : statement;
|
|
38
|
+
}
|
|
39
|
+
if (kind === 'objective') {
|
|
40
|
+
return text(foundation.desiredState) ?? text(foundation.meaning) ?? text(payload.statement);
|
|
41
|
+
}
|
|
42
|
+
// A model title is a label, not a readable world-model body. Require a
|
|
43
|
+
// meaning/statement so a name-only catalog item cannot become evidence.
|
|
44
|
+
return text(foundation.meaning) ?? text(payload.statement);
|
|
45
|
+
}
|
|
46
|
+
function recordScope(kind, record) {
|
|
47
|
+
const payload = isRecord(record.payload) ? record.payload : {};
|
|
48
|
+
const foundation = isRecord(payload.foundation) ? payload.foundation : {};
|
|
49
|
+
const recordMetadata = record;
|
|
50
|
+
const candidates = kind === 'model'
|
|
51
|
+
? [
|
|
52
|
+
payload.applicability_text,
|
|
53
|
+
payload.applicability_scope,
|
|
54
|
+
foundation.applicability_text,
|
|
55
|
+
foundation.applicability_scope,
|
|
56
|
+
payload.applicability,
|
|
57
|
+
foundation.applicability,
|
|
58
|
+
]
|
|
59
|
+
: [
|
|
60
|
+
payload.applicability_scope,
|
|
61
|
+
payload.applicability_text,
|
|
62
|
+
foundation.applicability_scope,
|
|
63
|
+
foundation.applicability_text,
|
|
64
|
+
payload.applicability,
|
|
65
|
+
foundation.applicability,
|
|
66
|
+
];
|
|
67
|
+
candidates.push(recordMetadata.applicability_scope, recordMetadata.applicability_text);
|
|
68
|
+
for (const candidate of candidates) {
|
|
69
|
+
const value = scopeText(candidate);
|
|
70
|
+
if (value)
|
|
71
|
+
return { value, source: 'graph' };
|
|
72
|
+
}
|
|
73
|
+
return { value: UNSPECIFIED_APPLICABILITY_SCOPE, source: 'unspecified' };
|
|
74
|
+
}
|
|
75
|
+
function invalidInput() {
|
|
76
|
+
return {
|
|
77
|
+
status: 'error',
|
|
78
|
+
error: {
|
|
79
|
+
code: 'judgment_frame_read_input_invalid',
|
|
80
|
+
message: 'Provide one current catalog digest and one or more unique philosophy, objective, or model references.',
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function toolError(status, code, message, details) {
|
|
85
|
+
return { status, error: { code, message, ...(details === undefined ? {} : { details }) } };
|
|
86
|
+
}
|
|
87
|
+
function isSelection(value) {
|
|
88
|
+
if (!isRecord(value) || Object.keys(value).length !== 2
|
|
89
|
+
|| !Object.hasOwn(value, 'kind') || !Object.hasOwn(value, 'ref'))
|
|
90
|
+
return false;
|
|
91
|
+
return KINDS.includes(value.kind)
|
|
92
|
+
&& typeof value.ref === 'string'
|
|
93
|
+
&& value.ref.trim() === value.ref
|
|
94
|
+
&& value.ref.length > 0
|
|
95
|
+
&& value.ref.length <= MAX_REF_LENGTH;
|
|
96
|
+
}
|
|
97
|
+
function parseInput(args) {
|
|
98
|
+
if (!isRecord(args)
|
|
99
|
+
|| Object.keys(args).length !== 2
|
|
100
|
+
|| !Object.hasOwn(args, 'catalog_digest')
|
|
101
|
+
|| !Object.hasOwn(args, 'selections')
|
|
102
|
+
|| typeof args.catalog_digest !== 'string'
|
|
103
|
+
|| !DIGEST_PATTERN.test(args.catalog_digest)
|
|
104
|
+
|| !Array.isArray(args.selections)
|
|
105
|
+
|| args.selections.length === 0
|
|
106
|
+
|| args.selections.length > MAX_SELECTIONS
|
|
107
|
+
|| !args.selections.every(isSelection))
|
|
108
|
+
return null;
|
|
109
|
+
const selections = args.selections;
|
|
110
|
+
const keys = selections.map((selection) => `${selection.kind}:${selection.ref}`);
|
|
111
|
+
if (new Set(keys).size !== keys.length)
|
|
112
|
+
return null;
|
|
113
|
+
return { catalogDigest: args.catalog_digest, selections };
|
|
114
|
+
}
|
|
115
|
+
async function loadFrameCatalog(dependencies) {
|
|
116
|
+
const records = [];
|
|
117
|
+
for (const kind of KINDS) {
|
|
118
|
+
let page;
|
|
119
|
+
try {
|
|
120
|
+
page = await dependencies.loadRecords(kind, FRAME_PAGE_LIMIT);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
return toolError('unavailable', 'judgment_frame_catalog_unavailable', `Graph ${kind} records could not be read: ${error instanceof Error ? error.message : String(error)}`);
|
|
124
|
+
}
|
|
125
|
+
if (page.status !== 'ok')
|
|
126
|
+
return toolError(page.status, page.code, page.message);
|
|
127
|
+
if (page.records.length >= FRAME_PAGE_LIMIT) {
|
|
128
|
+
return toolError('error', 'judgment_frame_catalog_truncated', `Graph returned ${page.records.length} ${kind} records; the catalog may be incomplete`);
|
|
129
|
+
}
|
|
130
|
+
records.push(...page.records);
|
|
131
|
+
}
|
|
132
|
+
return { ...buildJudgmentFrameCatalog(records), records };
|
|
133
|
+
}
|
|
134
|
+
export const judgmentFrameReadTools = [{
|
|
135
|
+
name: 'brainbase_judgment_frame_read',
|
|
136
|
+
description: 'On every confirmed turn, after reading the judgment frame catalog, retrieve the full canonical Graph body, version, and applicability scope for every selected philosophy, objective, and world model reference. Pass the exact catalog_digest and selected kind/ref pairs. The read is authorized against the caller-visible Graph and fails on a stale catalog, unknown reference, or empty body. If Graph metadata has no applicability scope, the result says so explicitly; authorization scope is never presented as applicability scope.',
|
|
137
|
+
inputSchema: {
|
|
138
|
+
type: 'object',
|
|
139
|
+
additionalProperties: false,
|
|
140
|
+
properties: {
|
|
141
|
+
catalog_digest: { type: 'string', pattern: '^sha256:[a-f0-9]{64}$' },
|
|
142
|
+
selections: {
|
|
143
|
+
type: 'array',
|
|
144
|
+
minItems: 1,
|
|
145
|
+
maxItems: MAX_SELECTIONS,
|
|
146
|
+
items: {
|
|
147
|
+
type: 'object',
|
|
148
|
+
additionalProperties: false,
|
|
149
|
+
properties: {
|
|
150
|
+
kind: { type: 'string', enum: [...KINDS] },
|
|
151
|
+
ref: { type: 'string', minLength: 1, maxLength: MAX_REF_LENGTH },
|
|
152
|
+
},
|
|
153
|
+
required: ['kind', 'ref'],
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
required: ['catalog_digest', 'selections'],
|
|
158
|
+
},
|
|
159
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
160
|
+
}];
|
|
161
|
+
export async function handleJudgmentFrameReadToolCall(name, args, dependencies) {
|
|
162
|
+
if (name !== 'brainbase_judgment_frame_read')
|
|
163
|
+
return null;
|
|
164
|
+
const input = parseInput(args);
|
|
165
|
+
if (!input)
|
|
166
|
+
return invalidInput();
|
|
167
|
+
const loaded = await loadFrameCatalog(dependencies);
|
|
168
|
+
if ('status' in loaded)
|
|
169
|
+
return loaded;
|
|
170
|
+
const { catalog, records: sourceRecords } = loaded;
|
|
171
|
+
if (input.catalogDigest !== catalog.digest) {
|
|
172
|
+
return toolError('error', 'judgment_frame_catalog_mismatch', 'The supplied catalog_digest is stale or was produced from a different visible Graph.', { current_catalog_digest: catalog.digest });
|
|
173
|
+
}
|
|
174
|
+
const records = [];
|
|
175
|
+
const sourceByKey = new Map();
|
|
176
|
+
for (const record of sourceRecords) {
|
|
177
|
+
const key = `${record.entity_type}:${record.id}`;
|
|
178
|
+
if (!sourceByKey.has(key))
|
|
179
|
+
sourceByKey.set(key, record);
|
|
180
|
+
}
|
|
181
|
+
for (const selection of input.selections) {
|
|
182
|
+
const item = catalog.items.find((candidate) => candidate.kind === selection.kind && candidate.id === selection.ref);
|
|
183
|
+
if (!item) {
|
|
184
|
+
return toolError('error', 'judgment_frame_ref_unknown', `${selection.kind}/${selection.ref} is not in the current judgment frame catalog`);
|
|
185
|
+
}
|
|
186
|
+
const source = sourceByKey.get(`${selection.kind}:${selection.ref}`);
|
|
187
|
+
if (!source) {
|
|
188
|
+
return toolError('error', 'judgment_frame_body_unavailable', `${selection.kind}/${selection.ref} is no longer available in the authorized Graph`);
|
|
189
|
+
}
|
|
190
|
+
const payload = isRecord(source.payload) ? source.payload : {};
|
|
191
|
+
const semanticBody = itemBody(selection.kind, payload);
|
|
192
|
+
if (!semanticBody)
|
|
193
|
+
return toolError('error', 'judgment_frame_body_unavailable', `${selection.kind}/${selection.ref} has no readable body`);
|
|
194
|
+
// The catalog intentionally clips a compact summary. Return the full
|
|
195
|
+
// canonical payload so model predictions, constraints, and measurements
|
|
196
|
+
// are not silently discarded by the read step.
|
|
197
|
+
const body = canonicalJson(payload);
|
|
198
|
+
const bodyDigest = `sha256:${createHash('sha256').update(body, 'utf8').digest('hex')}`;
|
|
199
|
+
const foundation = isRecord(payload.foundation) ? payload.foundation : {};
|
|
200
|
+
const graphVersion = versionText(foundation.revision) ?? versionText(source.version);
|
|
201
|
+
const version = graphVersion ?? bodyDigest;
|
|
202
|
+
const versionSource = graphVersion ? 'graph' : 'body_digest';
|
|
203
|
+
const applicabilityScope = recordScope(selection.kind, source);
|
|
204
|
+
records.push({
|
|
205
|
+
kind: selection.kind,
|
|
206
|
+
ref: selection.ref,
|
|
207
|
+
body,
|
|
208
|
+
body_digest: bodyDigest,
|
|
209
|
+
version,
|
|
210
|
+
version_source: versionSource,
|
|
211
|
+
applicability_scope: applicabilityScope.value,
|
|
212
|
+
applicability_scope_source: applicabilityScope.source,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
status: 'ok',
|
|
217
|
+
data: {
|
|
218
|
+
schema_version: 'brainbase-judgment-frame-read-v1',
|
|
219
|
+
catalog_digest: catalog.digest,
|
|
220
|
+
records,
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
package/dist/judgment-frame.d.ts
CHANGED
|
@@ -125,3 +125,5 @@ export type JudgmentFrameToolResult = {
|
|
|
125
125
|
};
|
|
126
126
|
};
|
|
127
127
|
export declare function handleJudgmentFrameToolCall(name: string, args: Record<string, unknown>, dependencies: JudgmentFrameToolDependencies): Promise<JudgmentFrameToolResult | null>;
|
|
128
|
+
export { judgmentFrameReadTools, handleJudgmentFrameReadToolCall, } from './judgment-frame-read.js';
|
|
129
|
+
export type { JudgmentFrameReadDependencies, JudgmentFrameReadRecord, } from './judgment-frame-read.js';
|
package/dist/judgment-frame.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unson/brainbase-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
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": "25fb5aa910ba704d704768d3df5f271b085df5a2"
|
|
411
411
|
}
|
|
@@ -234,3 +234,6 @@
|
|
|
234
234
|
grid-template-columns: minmax(90px, .55fr) minmax(0, 1fr);
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
|
+
|
|
238
|
+
/* Catalog provenance is secondary to the topic, but remains visible on every card. */
|
|
239
|
+
.bb-fov-source { display: inline-block; margin: 0 0 12px; padding: 4px 10px; border-radius: 999px; background: var(--bb-surface-subtle, #f4f4f8); color: var(--bb-text-muted, #62626e); font-size: 12px; overflow-wrap: anywhere; }
|
|
@@ -415,7 +415,7 @@ function objectiveCard(doc, record, variableMap) {
|
|
|
415
415
|
return card;
|
|
416
416
|
}
|
|
417
417
|
|
|
418
|
-
function renderObjectives(doc, catalog, viewState, callbacks) {
|
|
418
|
+
function renderObjectives(doc, catalog, viewState, callbacks, recordContexts) {
|
|
419
419
|
const section = makeElement(doc, 'section', { className: 'bb-fov-section', attrs: { 'aria-label': '目的' } });
|
|
420
420
|
section.append(workspaceSectionTitle(doc, {
|
|
421
421
|
title: '目的',
|
|
@@ -429,7 +429,7 @@ function renderObjectives(doc, catalog, viewState, callbacks) {
|
|
|
429
429
|
const rows = catalog.objectives.map((record) => {
|
|
430
430
|
const definition = recordDefinition(record);
|
|
431
431
|
const details = detailsFor(record);
|
|
432
|
-
const key = `${definition.id}@${definition.revision}`;
|
|
432
|
+
const key = recordContexts?.get(record)?.key ?? `${definition.id}@${definition.revision}`;
|
|
433
433
|
return {
|
|
434
434
|
key,
|
|
435
435
|
selected: key === viewState.objectiveKey,
|
|
@@ -438,7 +438,7 @@ function renderObjectives(doc, catalog, viewState, callbacks) {
|
|
|
438
438
|
callbacks.render?.();
|
|
439
439
|
},
|
|
440
440
|
cells: [
|
|
441
|
-
{ primary: formatAny(firstValue(details, 'title')) ?? formatAny(definition.meaning) ?? definition.id, secondary: `${definition.id}@${definition.revision}` },
|
|
441
|
+
{ primary: formatAny(firstValue(details, 'title')) ?? formatAny(definition.meaning) ?? definition.id, secondary: recordContexts?.get(record)?.label ?? `${definition.id}@${definition.revision}` },
|
|
442
442
|
formatAny(definition.desiredState) ?? '目指す状態: 未記録',
|
|
443
443
|
formatAny(definition.adoptionState) ? (ADOPTION_LABELS[definition.adoptionState] ?? definition.adoptionState) : '状態未確認',
|
|
444
444
|
],
|
|
@@ -452,18 +452,21 @@ function renderObjectives(doc, catalog, viewState, callbacks) {
|
|
|
452
452
|
}));
|
|
453
453
|
const selected = catalog.objectives.find((record) => {
|
|
454
454
|
const definition = recordDefinition(record);
|
|
455
|
-
return `${definition.id}@${definition.revision}` === viewState.objectiveKey;
|
|
456
|
-
}) ?? catalog.objectives[0];
|
|
455
|
+
return (recordContexts?.get(record)?.key ?? `${definition.id}@${definition.revision}`) === viewState.objectiveKey;
|
|
456
|
+
}) ?? (recordContexts ? null : catalog.objectives[0]);
|
|
457
|
+
if (!selected && recordContexts) section.append(makeElement(doc, 'p', { className: 'bb-fov-recorded-notice', text: '目的の行を選ぶと、評価基準と記録上の現状を確認できます。' }));
|
|
457
458
|
if (selected) {
|
|
458
459
|
const selectedDefinition = recordDefinition(selected);
|
|
459
|
-
viewState.objectiveKey = `${selectedDefinition.id}@${selectedDefinition.revision}`;
|
|
460
|
+
viewState.objectiveKey = recordContexts?.get(selected)?.key ?? `${selectedDefinition.id}@${selectedDefinition.revision}`;
|
|
460
461
|
if (catalog.objectives.length > 1) {
|
|
461
462
|
section.append(workspaceNotice(doc, {
|
|
462
463
|
label: '詳細表示',
|
|
463
464
|
text: `目的${catalog.objectives.length}件のうち1件を選択して詳細を表示しています。`,
|
|
464
465
|
}));
|
|
465
466
|
}
|
|
466
|
-
|
|
467
|
+
const card = objectiveCard(doc, selected, recordContexts?.get(selected)?.variableMap ?? variableMap);
|
|
468
|
+
appendRecordSource(doc, card, selected, recordContexts);
|
|
469
|
+
section.append(card);
|
|
467
470
|
}
|
|
468
471
|
return section;
|
|
469
472
|
}
|
|
@@ -504,7 +507,7 @@ function philosophyCard(doc, record) {
|
|
|
504
507
|
return card;
|
|
505
508
|
}
|
|
506
509
|
|
|
507
|
-
function renderPhilosophies(doc, catalog) {
|
|
510
|
+
function renderPhilosophies(doc, catalog, recordContexts) {
|
|
508
511
|
const section = makeElement(doc, 'section', { className: 'bb-fov-section', attrs: { 'aria-label': '哲学' } });
|
|
509
512
|
section.append(workspaceSectionTitle(doc, {
|
|
510
513
|
title: '哲学',
|
|
@@ -515,7 +518,11 @@ function renderPhilosophies(doc, catalog) {
|
|
|
515
518
|
return section;
|
|
516
519
|
}
|
|
517
520
|
const grid = makeElement(doc, 'div', { className: 'bb-fov-philosophy-grid' });
|
|
518
|
-
for (const record of catalog.philosophies)
|
|
521
|
+
for (const record of catalog.philosophies) {
|
|
522
|
+
const card = philosophyCard(doc, record);
|
|
523
|
+
appendRecordSource(doc, card, record, recordContexts);
|
|
524
|
+
grid.append(card);
|
|
525
|
+
}
|
|
519
526
|
section.append(grid);
|
|
520
527
|
return section;
|
|
521
528
|
}
|
|
@@ -569,7 +576,7 @@ function modelCard(doc, record, variableMap) {
|
|
|
569
576
|
return card;
|
|
570
577
|
}
|
|
571
578
|
|
|
572
|
-
function renderModels(doc, catalog, viewState, callbacks) {
|
|
579
|
+
function renderModels(doc, catalog, viewState, callbacks, recordContexts) {
|
|
573
580
|
const section = makeElement(doc, 'section', { className: 'bb-fov-section', attrs: { 'aria-label': '世界モデル' } });
|
|
574
581
|
section.append(workspaceSectionTitle(doc, {
|
|
575
582
|
title: '世界モデル',
|
|
@@ -595,7 +602,7 @@ function renderModels(doc, catalog, viewState, callbacks) {
|
|
|
595
602
|
const renderResults = () => {
|
|
596
603
|
results.replaceChildren();
|
|
597
604
|
const query = viewState.query.trim().toLocaleLowerCase();
|
|
598
|
-
const filtered = query ? catalog.models.filter((record) => searchTextForModel(record).includes(query)) : catalog.models;
|
|
605
|
+
const filtered = query ? catalog.models.filter((record) => `${searchTextForModel(record)} ${recordContexts?.get(record)?.label ?? ""}`.toLocaleLowerCase().includes(query)) : catalog.models;
|
|
599
606
|
if (filtered.length === 0) {
|
|
600
607
|
results.append(workspaceNotice(doc, { label: '検索結果', text: query ? '条件に一致する世界モデルはありません。' : 'この対象範囲には登録された世界モデルがありません。' }));
|
|
601
608
|
return;
|
|
@@ -610,7 +617,11 @@ function renderModels(doc, catalog, viewState, callbacks) {
|
|
|
610
617
|
}));
|
|
611
618
|
const variableMap = new Map(catalog.variables.map((record) => [foundationKey(recordDefinition(record)), record]));
|
|
612
619
|
const cards = makeElement(doc, 'div', { className: 'bb-fov-model-grid' });
|
|
613
|
-
for (const record of pageItems)
|
|
620
|
+
for (const record of pageItems) {
|
|
621
|
+
const card = modelCard(doc, record, recordContexts?.get(record)?.variableMap ?? variableMap);
|
|
622
|
+
appendRecordSource(doc, card, record, recordContexts);
|
|
623
|
+
cards.append(card);
|
|
624
|
+
}
|
|
614
625
|
results.append(cards);
|
|
615
626
|
if (pages > 1) {
|
|
616
627
|
const pager = makeElement(doc, 'nav', { className: 'bb-fov-pager', attrs: { 'aria-label': '世界モデルのページ' } });
|
|
@@ -655,6 +666,8 @@ export function renderFoundationOverview(root, state, {
|
|
|
655
666
|
viewState = { query: '', page: 0 },
|
|
656
667
|
callbacks = {},
|
|
657
668
|
showHeader = true,
|
|
669
|
+
recordContexts,
|
|
670
|
+
partial = false,
|
|
658
671
|
} = {}) {
|
|
659
672
|
if (!root) throw new TypeError('root is required');
|
|
660
673
|
const doc = getDocument(explicitDocument);
|
|
@@ -671,7 +684,7 @@ export function renderFoundationOverview(root, state, {
|
|
|
671
684
|
source: `正本: ${scopeId}`,
|
|
672
685
|
}));
|
|
673
686
|
}
|
|
674
|
-
surface.append(workspaceNotice(doc, {
|
|
687
|
+
if (!recordContexts) surface.append(workspaceNotice(doc, {
|
|
675
688
|
label: '対象範囲',
|
|
676
689
|
text: scopeId,
|
|
677
690
|
}));
|
|
@@ -686,22 +699,85 @@ export function renderFoundationOverview(root, state, {
|
|
|
686
699
|
return surface;
|
|
687
700
|
}
|
|
688
701
|
const catalog = state.catalog;
|
|
689
|
-
surface.append(workspaceNotice(doc, {
|
|
690
|
-
|
|
691
|
-
text: '
|
|
692
|
-
|
|
702
|
+
if (partial) surface.append(workspaceNotice(doc, { label: '取得済みの範囲', text: '以下の件数・登録なし・検索結果は、取得できた記録だけを対象にしています。全体は未確認です。' }));
|
|
703
|
+
surface.append(recordContexts
|
|
704
|
+
? makeElement(doc, 'p', { className: 'bb-fov-recorded-notice', text: '記録上の現状は保存時点の記述です。登録状態と検証状態を分けて読みます。' })
|
|
705
|
+
: workspaceNotice(doc, { label: '読み方', text: '登録は達成・採用・検証済みを意味しません。記録上の現状は保存時点の記述です。' }));
|
|
693
706
|
surface.append(workspaceMetrics(doc, [
|
|
694
|
-
{ label: '目的', value: catalog.objectives.length, note: '登録された定義' },
|
|
695
|
-
{ label: '哲学', value: catalog.philosophies.length, note: '登録された版' },
|
|
696
|
-
{ label: '世界モデル', value: catalog.models.length, note: '検索対象' },
|
|
707
|
+
{ label: '目的', value: catalog.objectives.length, note: partial ? '取得済みの定義(全体は未確認)' : '登録された定義' },
|
|
708
|
+
{ label: '哲学', value: catalog.philosophies.length, note: partial ? '取得済みの版(全体は未確認)' : '登録された版' },
|
|
709
|
+
{ label: '世界モデル', value: catalog.models.length, note: partial ? '取得済みの検索対象(全体は未確認)' : '検索対象' },
|
|
697
710
|
], { ariaLabel: 'Foundationの登録数' }));
|
|
698
|
-
surface.append(renderObjectives(doc, catalog, viewState, callbacks));
|
|
699
|
-
surface.append(renderPhilosophies(doc, catalog));
|
|
700
|
-
surface.append(renderModels(doc, catalog, viewState, callbacks));
|
|
711
|
+
surface.append(renderObjectives(doc, catalog, viewState, callbacks, recordContexts));
|
|
712
|
+
surface.append(renderPhilosophies(doc, catalog, recordContexts));
|
|
713
|
+
surface.append(renderModels(doc, catalog, viewState, callbacks, recordContexts));
|
|
701
714
|
root.append(surface);
|
|
702
715
|
return surface;
|
|
703
716
|
}
|
|
704
717
|
|
|
718
|
+
function appendRecordSource(doc, card, record, contexts) {
|
|
719
|
+
const context = contexts?.get(record);
|
|
720
|
+
if (context) card.prepend(makeElement(doc, 'p', { className: 'bb-fov-source', text: context.label }));
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/** Compose independently validated catalogs by topic, preserving each reference's source context. */
|
|
724
|
+
export function renderFoundationCollectionOverview(root, sources, {
|
|
725
|
+
document: explicitDocument, viewState = { query: '', page: 0 }, callbacks = {}, scopeLabel = '全体',
|
|
726
|
+
} = {}) {
|
|
727
|
+
const doc = getDocument(explicitDocument);
|
|
728
|
+
root.replaceChildren();
|
|
729
|
+
const catalog = { objectives: [], variables: [], models: [], philosophies: [] };
|
|
730
|
+
const contexts = new WeakMap();
|
|
731
|
+
const seen = new Map();
|
|
732
|
+
let ready = 0;
|
|
733
|
+
for (const source of sources) {
|
|
734
|
+
if (source.state.status !== 'ready') {
|
|
735
|
+
const notice = workspaceNotice(doc, {
|
|
736
|
+
label: source.label,
|
|
737
|
+
text: source.state.status === 'loading' ? '目的・哲学・世界モデルを読み込んでいます。'
|
|
738
|
+
: `取得失敗: ${source.state.error?.message ?? 'この対象は未確認です。0件ではありません。'}`,
|
|
739
|
+
tone: source.state.status === 'loading' ? 'info' : 'danger',
|
|
740
|
+
});
|
|
741
|
+
notice.append(workspaceButton(doc, { text: '再試行', variant: 'quiet', onClick: callbacks.load }));
|
|
742
|
+
root.append(notice);
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
const value = normalizeFoundationCatalog(source.state.catalog, source.scopeId);
|
|
746
|
+
ready++;
|
|
747
|
+
const variableMap = new Map(value.variables.map((record) => [foundationKey(recordDefinition(record)), record]));
|
|
748
|
+
for (const kind of ['objectives', 'models', 'philosophies']) {
|
|
749
|
+
for (const original of value[kind]) {
|
|
750
|
+
const definition = original.definition ?? original;
|
|
751
|
+
const scope = original.scope ?? original.currentScope ?? definition.scope;
|
|
752
|
+
// Scope-less/project records remain tied to their authenticated catalog.
|
|
753
|
+
const shared = ['organization', 'organisation', 'company'].includes(scope?.type) && nonEmpty(scope?.id);
|
|
754
|
+
const key = JSON.stringify([kind, shared ? scope.type : 'project', shared ? scope.id : source.scopeId,
|
|
755
|
+
definition.id, definition.revision, original.digest]);
|
|
756
|
+
const existing = seen.get(key);
|
|
757
|
+
if (existing) {
|
|
758
|
+
const context = contexts.get(existing);
|
|
759
|
+
if (!context.sources.includes(source.label)) context.sources.push(source.label);
|
|
760
|
+
context.label = `${shared ? '組織共通 / ' : ''}${context.sources.join('・')}`;
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
const record = { ...original };
|
|
764
|
+
seen.set(key, record);
|
|
765
|
+
catalog[kind].push(record);
|
|
766
|
+
contexts.set(record, { key, variableMap, sources: [source.label], label: `${shared ? '組織共通 / ' : ''}${source.label}` });
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (ready) {
|
|
771
|
+
const mount = makeElement(doc, 'div');
|
|
772
|
+
renderFoundationOverview(mount, { status: 'ready', catalog }, {
|
|
773
|
+
document: doc, scopeId: scopeLabel, showHeader: false, viewState, callbacks,
|
|
774
|
+
recordContexts: contexts, partial: ready !== sources.length,
|
|
775
|
+
});
|
|
776
|
+
root.append(mount);
|
|
777
|
+
}
|
|
778
|
+
return root;
|
|
779
|
+
}
|
|
780
|
+
|
|
705
781
|
/** Mount the read-only overview and ignore responses from a destroyed generation. */
|
|
706
782
|
export function createFoundationOverview({ root, port, scopeId, document: explicitDocument, autoLoad = true, showHeader = true } = {}) {
|
|
707
783
|
if (!root) throw new TypeError('root is required');
|
package/ui/objective-editor.css
CHANGED
|
@@ -243,6 +243,8 @@ textarea.objective-editor-input { min-height: 5.4rem; resize: vertical; }
|
|
|
243
243
|
.bb-objective-rail { display: block; }
|
|
244
244
|
.bb-objective-rail > .bb-ws-notice { margin: var(--bb-space-4) 0 0; }
|
|
245
245
|
.bb-objective-rail > .bb-ws-actions { padding-top: 22px; }
|
|
246
|
+
.bb-objective-rail > .bb-objective-revision-note { margin: var(--bb-space-4) 0 0; }
|
|
247
|
+
.bb-objective-rail > .bb-objective-revision-note + .bb-ws-actions { padding-top: var(--bb-space-3); }
|
|
246
248
|
.bb-objective-rail .bb-ws-rail-block > p + p,
|
|
247
249
|
.bb-objective-rail .bb-objective-note { margin-top: var(--bb-space-2); }
|
|
248
250
|
.bb-objective-rail .bb-objective-note { color: var(--bb-color-muted); font-size: 12px; }
|
package/ui/objective-editor.js
CHANGED
|
@@ -1121,6 +1121,7 @@ function renderObjectiveDetail(doc, state, callbacks) {
|
|
|
1121
1121
|
parts.push(workspaceRailBlock(doc, { title: '制約の参照', className: 'bb-objective-constraints-block', content: constraintContent(doc, state, callbacks) }));
|
|
1122
1122
|
parts.push(workspaceRailBlock(doc, { title: '判断に使えるか', className: 'bb-objective-readiness-block', content: readinessContent(doc, state.readiness) }));
|
|
1123
1123
|
if (callbacks.canEdit) {
|
|
1124
|
+
parts.push(makeElement('p', { className: 'objective-editor-muted bb-objective-revision-note', text: '直して保存すると同じ目的の新しい版を作ります。保存結果を読み戻して確認し、確認できない場合はその状態を表示します。' }));
|
|
1124
1125
|
parts.push(workspaceActions(doc, [{ text: '目的を直す', variant: 'primary', onClick: callbacks.onEdit }]));
|
|
1125
1126
|
}
|
|
1126
1127
|
return parts;
|