@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/dist/local-web-host.js
CHANGED
|
@@ -10,7 +10,7 @@ import { createFoundationRevisionStore, FoundationStoreError } from './foundatio
|
|
|
10
10
|
import { createGraphWebHttpHandler } from './graph-web-http.js';
|
|
11
11
|
import { defaultJudgmentJournalRoot } from './judgment-value-proof-review.js';
|
|
12
12
|
import { nodeRequestToFetch, writeFetchResponse } from './local-web-fetch-bridge.js';
|
|
13
|
-
import { assertSameOrigin, isLoopbackHost, LOCAL_WEB_CONTENT_SECURITY_POLICY, LocalWebHttpError, requestUrl, tokenMatches, writeHttpError, writeJson } from './local-web-security.js';
|
|
13
|
+
import { assertSameOrigin, isLoopbackHost, LOCAL_WEB_CONTENT_SECURITY_POLICY, LocalWebHttpError, readJsonObjectBody, requestUrl, tokenMatches, writeHttpError, writeJson } from './local-web-security.js';
|
|
14
14
|
import { resolveDataDir } from './paths.js';
|
|
15
15
|
import { loadPersonalOs, readPersonalOsSidecar } from './ssot.js';
|
|
16
16
|
import { createValueProofReviewHttpHandler, VALUE_PROOF_REVIEW_TOKEN_HEADER } from './value-proof-review-http.js';
|
|
@@ -437,7 +437,7 @@ function worldModelError(error) {
|
|
|
437
437
|
}
|
|
438
438
|
if (error instanceof LocalWebHttpError)
|
|
439
439
|
return error;
|
|
440
|
-
return new LocalWebHttpError(500, 'internal_error', 'World model
|
|
440
|
+
return new LocalWebHttpError(500, 'internal_error', 'World model request failed');
|
|
441
441
|
}
|
|
442
442
|
async function listReadableSidecarRecords(input) {
|
|
443
443
|
try {
|
|
@@ -473,11 +473,112 @@ async function listReadableSidecarRecords(input) {
|
|
|
473
473
|
}
|
|
474
474
|
return { records, unreadable: { count, codes: [...codes] } };
|
|
475
475
|
}
|
|
476
|
+
const LOCAL_WEB_WORLD_MODEL_SCOPE_START = '1970-01-01T00:00:00.000Z';
|
|
477
|
+
const WORLD_MODEL_VALUE_KINDS = new Set(['number', 'boolean', 'string', 'state']);
|
|
478
|
+
const WORLD_MODEL_AGGREGATIONS = new Set(['none', 'sum', 'average', 'count', 'min', 'max', 'last', 'custom']);
|
|
479
|
+
const WORLD_MODEL_VARIABLE_FIELDS = new Set([
|
|
480
|
+
'meaning', 'subject', 'valueKind', 'unit', 'aggregation', 'granularity', 'measurementMethod'
|
|
481
|
+
]);
|
|
482
|
+
const WORLD_MODEL_OBSERVATION_FIELDS = new Set(['variableRef', 'value']);
|
|
483
|
+
const WORLD_MODEL_AUTHORITY_FIELDS = new Set([
|
|
484
|
+
'acl', 'scope', 'storage', 'provenance', 'authorizedUses', 'ownerId', 'readerIds', 'writerIds',
|
|
485
|
+
'principal', 'subjectId', 'recordedAt', 'occurredAt', 'period', 'id', 'revision', 'type'
|
|
486
|
+
]);
|
|
487
|
+
function worldModelInvalid(message) {
|
|
488
|
+
return new LocalWebHttpError(400, 'invalid_input', message);
|
|
489
|
+
}
|
|
490
|
+
function assertWorldModelBodyFields(body, allowed) {
|
|
491
|
+
for (const key of Object.keys(body)) {
|
|
492
|
+
if (allowed.has(key))
|
|
493
|
+
continue;
|
|
494
|
+
if (WORLD_MODEL_AUTHORITY_FIELDS.has(key)) {
|
|
495
|
+
throw new LocalWebHttpError(400, 'authority_field_in_body', `${key} is decided by the local Web host`);
|
|
496
|
+
}
|
|
497
|
+
throw worldModelInvalid(`Unknown World Model field ${key}`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function requiredWorldModelText(body, key) {
|
|
501
|
+
const value = body[key];
|
|
502
|
+
if (typeof value !== 'string' || value.trim() === '')
|
|
503
|
+
throw worldModelInvalid(`${key} is required`);
|
|
504
|
+
return value.trim();
|
|
505
|
+
}
|
|
506
|
+
function worldModelVariableFromBody(body, principal) {
|
|
507
|
+
assertWorldModelBodyFields(body, WORLD_MODEL_VARIABLE_FIELDS);
|
|
508
|
+
const valueKind = requiredWorldModelText(body, 'valueKind');
|
|
509
|
+
if (!WORLD_MODEL_VALUE_KINDS.has(valueKind))
|
|
510
|
+
throw worldModelInvalid(`Unknown valueKind ${valueKind}`);
|
|
511
|
+
const aggregation = requiredWorldModelText(body, 'aggregation');
|
|
512
|
+
if (!WORLD_MODEL_AGGREGATIONS.has(aggregation))
|
|
513
|
+
throw worldModelInvalid(`Unknown aggregation ${aggregation}`);
|
|
514
|
+
const unit = body.unit === undefined ? undefined : requiredWorldModelText(body, 'unit');
|
|
515
|
+
if (valueKind === 'number' && !unit)
|
|
516
|
+
throw worldModelInvalid('unit is required for number variables');
|
|
517
|
+
return {
|
|
518
|
+
id: `web.variable.${randomBytes(10).toString('hex')}`,
|
|
519
|
+
type: 'variable',
|
|
520
|
+
revision: '1',
|
|
521
|
+
meaning: requiredWorldModelText(body, 'meaning'),
|
|
522
|
+
epistemicState: 'unverified',
|
|
523
|
+
adoptionState: 'draft',
|
|
524
|
+
authorizedUses: ['draft', 'judgment', 'evaluation'],
|
|
525
|
+
acl: { ownerId: principal, visibility: 'private', readerIds: [], writerIds: [] },
|
|
526
|
+
storage: 'ontology',
|
|
527
|
+
provenance: [{ sourceId: LOCAL_WEB_PROVENANCE_SOURCE_ID, sourceKind: 'observation', evidenceIds: [] }],
|
|
528
|
+
scope: { subjectIds: [principal], validFrom: LOCAL_WEB_WORLD_MODEL_SCOPE_START },
|
|
529
|
+
subject: requiredWorldModelText(body, 'subject'),
|
|
530
|
+
valueKind: valueKind,
|
|
531
|
+
...(unit ? { unit } : {}),
|
|
532
|
+
aggregation: aggregation,
|
|
533
|
+
granularity: requiredWorldModelText(body, 'granularity'),
|
|
534
|
+
measurementMethod: requiredWorldModelText(body, 'measurementMethod'),
|
|
535
|
+
// The local host stamps the current date into observations; a broad owner-only
|
|
536
|
+
// validity window avoids asking the first-time user for hidden scope fields.
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
function worldModelReference(value) {
|
|
540
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
541
|
+
throw worldModelInvalid('variableRef is required');
|
|
542
|
+
const reference = value;
|
|
543
|
+
if (reference.type !== 'variable' || typeof reference.id !== 'string' || reference.id.trim() === ''
|
|
544
|
+
|| typeof reference.revision !== 'string' || !/^[1-9]\d*$/u.test(reference.revision)) {
|
|
545
|
+
throw worldModelInvalid('variableRef must reference a variable revision');
|
|
546
|
+
}
|
|
547
|
+
if (Object.keys(reference).some((key) => !['id', 'type', 'revision'].includes(key))) {
|
|
548
|
+
throw worldModelInvalid('variableRef contains an unsupported field');
|
|
549
|
+
}
|
|
550
|
+
return { id: reference.id.trim(), type: 'variable', revision: reference.revision };
|
|
551
|
+
}
|
|
552
|
+
function worldModelObservationFromBody(body, principal, now) {
|
|
553
|
+
assertWorldModelBodyFields(body, WORLD_MODEL_OBSERVATION_FIELDS);
|
|
554
|
+
if (!Object.hasOwn(body, 'value') || body.value === null || body.value === undefined)
|
|
555
|
+
throw worldModelInvalid('value is required');
|
|
556
|
+
const value = body.value;
|
|
557
|
+
if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
|
|
558
|
+
throw worldModelInvalid('value must be a string, number, or boolean');
|
|
559
|
+
}
|
|
560
|
+
if (typeof value === 'number' && !Number.isFinite(value)) {
|
|
561
|
+
throw worldModelInvalid('value must be a finite number');
|
|
562
|
+
}
|
|
563
|
+
const nowIso = now.toISOString();
|
|
564
|
+
const day = nowIso.slice(0, 10);
|
|
565
|
+
const observationId = `web.observation.${randomBytes(10).toString('hex')}`;
|
|
566
|
+
return {
|
|
567
|
+
id: observationId,
|
|
568
|
+
variableRef: worldModelReference(body.variableRef),
|
|
569
|
+
subjectId: principal,
|
|
570
|
+
value,
|
|
571
|
+
occurredAt: nowIso,
|
|
572
|
+
period: { from: `${day}T00:00:00.000Z`, until: `${day}T23:59:59.999Z` },
|
|
573
|
+
recordedAt: nowIso,
|
|
574
|
+
sourceRef: { sourceId: LOCAL_WEB_PROVENANCE_SOURCE_ID, sourceKind: 'observation', evidenceIds: [observationId] }
|
|
575
|
+
};
|
|
576
|
+
}
|
|
476
577
|
/**
|
|
477
|
-
*
|
|
478
|
-
*
|
|
479
|
-
*
|
|
480
|
-
*
|
|
578
|
+
* World Model routes. Reads retain the explicit absence/partial contract;
|
|
579
|
+
* the first-user Web entry writes only a trusted Variable definition and an
|
|
580
|
+
* initial Observation. Models, adoption and corrections stay out of this
|
|
581
|
+
* focused route until their own authority and review contract exists.
|
|
481
582
|
*/
|
|
482
583
|
export function createWorldModelReadModule(context) {
|
|
483
584
|
const foundationStore = createFoundationRevisionStore({ dataDir: context.dataDir });
|
|
@@ -519,17 +620,39 @@ export function createWorldModelReadModule(context) {
|
|
|
519
620
|
writeJson(response, 404, { error: { code: 'not_found', message: 'Not found' } });
|
|
520
621
|
return true;
|
|
521
622
|
}
|
|
522
|
-
if (request.method !== 'GET') {
|
|
523
|
-
writeJson(response, 405, { error: { code: 'method_not_allowed', message: 'The world model is read-only here' } });
|
|
524
|
-
return true;
|
|
525
|
-
}
|
|
526
623
|
const graph = await readLocalGraphState(context.dataDir);
|
|
527
624
|
if (graph.status !== 'ready' || !graph.ownerId) {
|
|
528
625
|
writeGraphUnavailable(response, graph, context.dataDir);
|
|
529
626
|
return true;
|
|
530
627
|
}
|
|
628
|
+
const principal = { principal: graph.ownerId };
|
|
629
|
+
if (request.method === 'POST' && (resource === 'variables' || resource === 'observations')) {
|
|
630
|
+
try {
|
|
631
|
+
const body = await readJsonObjectBody(request, LOCAL_WEB_FOUNDATION_BODY_LIMIT_BYTES);
|
|
632
|
+
if (resource === 'variables') {
|
|
633
|
+
const definition = worldModelVariableFromBody(body, graph.ownerId);
|
|
634
|
+
const reference = await worldModel.createVariable(definition, principal);
|
|
635
|
+
writeJson(response, 201, {
|
|
636
|
+
reference: { id: reference.id, type: reference.type, revision: reference.revision },
|
|
637
|
+
digest: reference.digest
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
const observation = await worldModel.saveObservation(worldModelObservationFromBody(body, graph.ownerId, context.now()), principal);
|
|
642
|
+
writeJson(response, 201, { observation });
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
catch (error) {
|
|
646
|
+
writeHttpError(response, worldModelError(error));
|
|
647
|
+
}
|
|
648
|
+
return true;
|
|
649
|
+
}
|
|
650
|
+
if (request.method !== 'GET') {
|
|
651
|
+
writeJson(response, 405, { error: { code: 'method_not_allowed', message: 'この入口では変数と観測値を登録できます' } });
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
531
654
|
try {
|
|
532
|
-
writeJson(response, 200, await read(resource,
|
|
655
|
+
writeJson(response, 200, await read(resource, principal));
|
|
533
656
|
}
|
|
534
657
|
catch (error) {
|
|
535
658
|
writeHttpError(response, worldModelError(error));
|
|
@@ -619,7 +742,9 @@ if (extensionScreens.some((screen, index) => screen?.id !== extensionIds[index]
|
|
|
619
742
|
throw new Error('Invalid extension screen id');
|
|
620
743
|
}
|
|
621
744
|
const meta = (name) => document.querySelector(\`meta[name="\${name}"]\`)?.getAttribute('content') ?? '';
|
|
622
|
-
|
|
745
|
+
// Keep the query portion intact; the shell parses the Graph entity query and
|
|
746
|
+
// only treats the entity id as a target of the Graph screen.
|
|
747
|
+
const screenFromHash = () => location.hash.replace(/^#/, '') || undefined;
|
|
623
748
|
const shell = createLocalWebShell({
|
|
624
749
|
root: document.getElementById('brainbase-local-web'),
|
|
625
750
|
token: meta('brainbase-web-token'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unson/brainbase-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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",
|
|
@@ -155,6 +155,10 @@
|
|
|
155
155
|
"types": "./dist/judgment-problem-snapshot.d.ts",
|
|
156
156
|
"import": "./dist/judgment-problem-snapshot.js"
|
|
157
157
|
},
|
|
158
|
+
"./judgment-problem-snapshot-http": {
|
|
159
|
+
"types": "./dist/judgment-problem-snapshot-http.d.ts",
|
|
160
|
+
"import": "./dist/judgment-problem-snapshot-http.js"
|
|
161
|
+
},
|
|
158
162
|
"./personal-knowledge": {
|
|
159
163
|
"types": "./dist/personal-knowledge.d.ts",
|
|
160
164
|
"import": "./dist/personal-knowledge.js"
|
|
@@ -348,5 +352,5 @@
|
|
|
348
352
|
"engines": {
|
|
349
353
|
"node": ">=20"
|
|
350
354
|
},
|
|
351
|
-
"gitHead": "
|
|
355
|
+
"gitHead": "c9b9adbff2dd409f77ed868d9c3939b3269f2d4f"
|
|
352
356
|
}
|
|
@@ -163,6 +163,7 @@ function textCell(value, fallback) {
|
|
|
163
163
|
* @param {string} [options.token] Launch token sent with a correction.
|
|
164
164
|
* @param {string} [options.tokenHeader] Header name for the token.
|
|
165
165
|
* @param {boolean} [options.autoLoad=true] Runs the empty search and reads the kinds on mount.
|
|
166
|
+
* @param {string} [options.initialEntityId] Opens this exact entity after the initial Graph read.
|
|
166
167
|
* @param {() => Date} [options.now] The owner's clock.
|
|
167
168
|
* @param {boolean} [options.canCorrect=true] When false, no correction control is drawn anywhere
|
|
168
169
|
* (この記録を直す, 関係を加える, この関係を直す, and この画面で直せること under the kinds of
|
|
@@ -204,6 +205,7 @@ export function createGraphRegistryView({
|
|
|
204
205
|
token,
|
|
205
206
|
tokenHeader,
|
|
206
207
|
autoLoad = true,
|
|
208
|
+
initialEntityId,
|
|
207
209
|
now = () => new Date(),
|
|
208
210
|
canCorrect = true,
|
|
209
211
|
correctionScope,
|
|
@@ -617,6 +619,23 @@ export function createGraphRegistryView({
|
|
|
617
619
|
const canFix = scope.entity(entity.type) && scope.fields(graphEntityCorrectionFields(entity.type)).length > 0;
|
|
618
620
|
const relatable = newRelationOptions(entity.type).length > 0;
|
|
619
621
|
const related = loaded.outgoing.length + loaded.incoming.length > 0;
|
|
622
|
+
const correctionEntries = [
|
|
623
|
+
canFix ? '「この記録を直す」' : null,
|
|
624
|
+
relatable && scope.createEdges ? '「関係を加える」' : null,
|
|
625
|
+
related && scope.edges ? '関係ごとの「この関係を直す」' : null,
|
|
626
|
+
].filter(Boolean);
|
|
627
|
+
if (correctionEntries.length > 0) {
|
|
628
|
+
const lastEntry = correctionEntries.at(-1);
|
|
629
|
+
const entries = correctionEntries.length === 1
|
|
630
|
+
? lastEntry
|
|
631
|
+
: `${correctionEntries.slice(0, -1).join('、')}、または${lastEntry}`;
|
|
632
|
+
children.push(workspaceRailBlock(doc, {
|
|
633
|
+
title: '訂正の流れ',
|
|
634
|
+
content: makeElement(doc, 'p', {
|
|
635
|
+
text: `誤りを見つけたときの訂正入口です。${entries}から変更案を入力します。承認待ちの提出ではなく、保存するとその場でGraphに反映され、理由と変更前後が履歴に残ります。保存後に読み直して一致を確かめ、確認できた内容は、次のMCPの search・get_context・resolve_entity で使われます。`,
|
|
636
|
+
}),
|
|
637
|
+
}));
|
|
638
|
+
}
|
|
620
639
|
const actions = [
|
|
621
640
|
canFix ? workspaceButton(doc, { text: 'この記録を直す', onClick: () => openCorrection('entity', () => correction.openEntity(entity, { title: 'この記録を直す' })) }) : null,
|
|
622
641
|
relatable && scope.createEdges
|
|
@@ -797,7 +816,14 @@ export function createGraphRegistryView({
|
|
|
797
816
|
},
|
|
798
817
|
};
|
|
799
818
|
controller.render();
|
|
800
|
-
if (autoLoad)
|
|
819
|
+
if (autoLoad) {
|
|
820
|
+
void controller.load().then(() => {
|
|
821
|
+
if (initialEntityId) return controller.openEntity(initialEntityId);
|
|
822
|
+
return undefined;
|
|
823
|
+
});
|
|
824
|
+
} else if (initialEntityId) {
|
|
825
|
+
void controller.openEntity(initialEntityId);
|
|
826
|
+
}
|
|
801
827
|
return controller;
|
|
802
828
|
}
|
|
803
829
|
|
package/ui/local-web-shell.js
CHANGED
|
@@ -21,6 +21,7 @@ import { createObjectiveEditorController } from './objective-editor.js';
|
|
|
21
21
|
import { createObjectiveEditorHttpPort } from './objective-editor-http-port.js';
|
|
22
22
|
import { createValueProofReviewUI } from './value-proof-review.js';
|
|
23
23
|
import { createWorldModelView } from './world-model-view.js';
|
|
24
|
+
import { createGraphClient } from './graph-view-shared.js';
|
|
24
25
|
|
|
25
26
|
export const LOCAL_WEB_SHELL_CONTRACT_VERSION = 'brainbase.local-web-shell.v1';
|
|
26
27
|
|
|
@@ -75,6 +76,60 @@ function isRecord(value) {
|
|
|
75
76
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Parses the local shell's hash target without treating query data as a
|
|
81
|
+
* screen id. Only the Graph screen accepts an entity target; all other
|
|
82
|
+
* screens deliberately discard query parameters.
|
|
83
|
+
*/
|
|
84
|
+
export function parseLocalWebTarget(target) {
|
|
85
|
+
const raw = typeof target === 'string' ? target.replace(/^#/, '') : '';
|
|
86
|
+
const separator = raw.indexOf('?');
|
|
87
|
+
const screenId = separator < 0 ? raw : raw.slice(0, separator);
|
|
88
|
+
if (!screenId) return { screenId: null, entityId: null };
|
|
89
|
+
const query = separator < 0 ? '' : raw.slice(separator + 1);
|
|
90
|
+
const params = new URLSearchParams(query);
|
|
91
|
+
return {
|
|
92
|
+
screenId,
|
|
93
|
+
entityId: screenId === 'graph' ? params.get('entity_id')?.trim() || null : null,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function graphSourceReader(context) {
|
|
98
|
+
const client = createGraphClient({ fetcher: context.fetcher, basePath: '/api/graph', token: context.token });
|
|
99
|
+
return async (source) => {
|
|
100
|
+
if (!isRecord(source) || source.kind !== 'local_graph'
|
|
101
|
+
|| typeof source.entity_id !== 'string' || !source.entity_id.trim()
|
|
102
|
+
|| typeof source.entity_type !== 'string' || !source.entity_type.trim()) {
|
|
103
|
+
return { state: 'unavailable', reason: 'source_contract_invalid' };
|
|
104
|
+
}
|
|
105
|
+
const result = await client.read(`/entities/${encodeURIComponent(source.entity_id)}`);
|
|
106
|
+
if (result.state === 'error') {
|
|
107
|
+
if (result.status === 404 || result.code === 'entity_not_found') return { state: 'not_found' };
|
|
108
|
+
if (result.status === 403 || result.code === 'authorization_denied' || result.code === 'scope_violation') return { state: 'forbidden' };
|
|
109
|
+
if (result.code === 'entity_id_ambiguous') return { state: 'ambiguous' };
|
|
110
|
+
return { state: 'unavailable', reason: result.code };
|
|
111
|
+
}
|
|
112
|
+
if (result.state !== 'ok' || !isRecord(result.payload?.entity)) return { state: 'unavailable', reason: 'source_response_invalid' };
|
|
113
|
+
const entity = result.payload.entity;
|
|
114
|
+
if (entity.id !== source.entity_id || entity.type !== source.entity_type) {
|
|
115
|
+
return { state: 'unavailable', reason: 'source_target_mismatch' };
|
|
116
|
+
}
|
|
117
|
+
if (source.digest && entity.digest !== source.digest) {
|
|
118
|
+
return { state: 'unavailable', reason: 'source_digest_mismatch' };
|
|
119
|
+
}
|
|
120
|
+
// The local Graph response currently exposes a content digest, not a
|
|
121
|
+
// revision number. A requested version is therefore linkable only when
|
|
122
|
+
// the provider returns the same explicit version field.
|
|
123
|
+
if (source.version) {
|
|
124
|
+
const currentVersion = entity.version ?? entity.revision ?? null;
|
|
125
|
+
if (!currentVersion || currentVersion !== source.version) {
|
|
126
|
+
return { state: 'unavailable', reason: 'source_version_mismatch' };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return { state: 'available' };
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
78
133
|
function makePage(container, context) {
|
|
79
134
|
const page = makeElement(context.document, 'div', { className: 'bb-shell-page' });
|
|
80
135
|
container.append(page);
|
|
@@ -89,6 +144,7 @@ function mountToday(container, context) {
|
|
|
89
144
|
document: context.document,
|
|
90
145
|
fetcher: context.fetcher,
|
|
91
146
|
token: context.token,
|
|
147
|
+
sourceReader: graphSourceReader(context),
|
|
92
148
|
});
|
|
93
149
|
}
|
|
94
150
|
|
|
@@ -129,7 +185,13 @@ function mountObjectives(container, context) {
|
|
|
129
185
|
constraintsEditable: false,
|
|
130
186
|
storyLinks: false,
|
|
131
187
|
});
|
|
132
|
-
const worldModel = createWorldModelView({
|
|
188
|
+
const worldModel = createWorldModelView({
|
|
189
|
+
root: worldRoot,
|
|
190
|
+
rail: context.rail,
|
|
191
|
+
document: doc,
|
|
192
|
+
fetcher: context.fetcher,
|
|
193
|
+
token: context.token,
|
|
194
|
+
});
|
|
133
195
|
return { editor, worldModel };
|
|
134
196
|
}
|
|
135
197
|
|
|
@@ -144,6 +206,7 @@ function mountGraphScreen(createView) {
|
|
|
144
206
|
document: context.document,
|
|
145
207
|
fetcher: context.fetcher,
|
|
146
208
|
token: context.token,
|
|
209
|
+
initialEntityId: context.initialEntityId,
|
|
147
210
|
});
|
|
148
211
|
};
|
|
149
212
|
}
|
|
@@ -248,6 +311,7 @@ export function createLocalWebShell({
|
|
|
248
311
|
const mounted = new Map();
|
|
249
312
|
const slots = new Map();
|
|
250
313
|
const links = new Map();
|
|
314
|
+
const pendingEntityIds = new Map();
|
|
251
315
|
let active = null;
|
|
252
316
|
|
|
253
317
|
const shell = makeElement(doc, 'div', { className: 'bb-shell', attrs: { 'data-contract-version': LOCAL_WEB_SHELL_CONTRACT_VERSION } });
|
|
@@ -311,8 +375,14 @@ export function createLocalWebShell({
|
|
|
311
375
|
for (const [screenId, railSlot] of rails) railSlot.hidden = !(showRail && screenId === active);
|
|
312
376
|
}
|
|
313
377
|
|
|
314
|
-
function ensureMounted(screen) {
|
|
315
|
-
if (mounted.has(screen.id))
|
|
378
|
+
function ensureMounted(screen, entityId = pendingEntityIds.get(screen.id) ?? null) {
|
|
379
|
+
if (mounted.has(screen.id)) {
|
|
380
|
+
if (entityId && screen.id === 'graph') {
|
|
381
|
+
const view = mounted.get(screen.id);
|
|
382
|
+
if (typeof view?.openEntity === 'function') void view.openEntity(entityId);
|
|
383
|
+
}
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
316
386
|
const slot = slots.get(screen.id);
|
|
317
387
|
if (screen.usesGraph && !(status.phase === 'ready' && status.data.graph.status === 'ready')) {
|
|
318
388
|
renderGraphGate(doc, slot, status, controller.refreshStatus, screen.label);
|
|
@@ -322,7 +392,7 @@ export function createLocalWebShell({
|
|
|
322
392
|
const rail = rails.get(screen.id) ?? null;
|
|
323
393
|
rail?.replaceChildren();
|
|
324
394
|
const page = Object.freeze({ crumbs: Object.freeze(['あなたのBrainbase', screen.label]), source: screen.source ?? null });
|
|
325
|
-
mounted.set(screen.id, screen.mount(slot, Object.freeze({ ...context, rail, page })) ?? true);
|
|
395
|
+
mounted.set(screen.id, screen.mount(slot, Object.freeze({ ...context, rail, page, initialEntityId: entityId })) ?? true);
|
|
326
396
|
updateRail();
|
|
327
397
|
}
|
|
328
398
|
|
|
@@ -330,15 +400,17 @@ export function createLocalWebShell({
|
|
|
330
400
|
get state() {
|
|
331
401
|
return { active, status, mounted: [...mounted.keys()] };
|
|
332
402
|
},
|
|
333
|
-
show(
|
|
334
|
-
const
|
|
403
|
+
show(target) {
|
|
404
|
+
const parsed = parseLocalWebTarget(target);
|
|
405
|
+
const screen = screens.find((candidate) => candidate.id === parsed.screenId) ?? screens[0];
|
|
406
|
+
pendingEntityIds.set(screen.id, screen.id === 'graph' ? parsed.entityId : null);
|
|
335
407
|
active = screen.id;
|
|
336
408
|
for (const [screenId, slot] of slots) slot.hidden = screenId !== screen.id;
|
|
337
409
|
for (const [screenId, link] of links) {
|
|
338
410
|
if (screenId === screen.id) link.setAttribute('aria-current', 'page');
|
|
339
411
|
else link.removeAttribute('aria-current');
|
|
340
412
|
}
|
|
341
|
-
ensureMounted(screen);
|
|
413
|
+
ensureMounted(screen, pendingEntityIds.get(screen.id));
|
|
342
414
|
updateRail();
|
|
343
415
|
return controller;
|
|
344
416
|
},
|
|
@@ -362,7 +434,7 @@ export function createLocalWebShell({
|
|
|
362
434
|
renderSource(doc, source, status, controller.refreshStatus);
|
|
363
435
|
// Re-evaluate Graph-backed screens that are waiting behind the gate.
|
|
364
436
|
for (const screen of screens) {
|
|
365
|
-
if (screen.usesGraph && !mounted.has(screen.id) && (screen.id === active)) ensureMounted(screen);
|
|
437
|
+
if (screen.usesGraph && !mounted.has(screen.id) && (screen.id === active)) ensureMounted(screen, pendingEntityIds.get(screen.id));
|
|
366
438
|
}
|
|
367
439
|
return status;
|
|
368
440
|
},
|
package/ui/project-workspace.js
CHANGED
|
@@ -667,7 +667,7 @@ function renderDenseOverview(doc, detail, context, entities, actions = {}) {
|
|
|
667
667
|
todayHead.append(makeElement(doc, 'h4', { text: '今日判断・対応すること' }));
|
|
668
668
|
if (typeof actions.openWorkList === 'function') todayHead.append(denseButton(doc, 'すべて見る', 'bb-pkw-dense-action', actions.openWorkList));
|
|
669
669
|
today.append(todayHead);
|
|
670
|
-
const todayNotice = denseContextStateNotice(doc, taskSection ?? (context?.state !== 'ok' ? context :
|
|
670
|
+
const todayNotice = denseContextStateNotice(doc, taskSection ?? (context?.state && context.state !== 'ok' ? context : { state: 'unknown' }), 'タスクを読み取れませんでした。');
|
|
671
671
|
if (todayNotice) today.append(todayNotice);
|
|
672
672
|
if (!todayNotice && due.length === 0) today.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: '表示できる仕事はありません。' }));
|
|
673
673
|
if (!todayNotice && due.length > 0) {
|
|
@@ -93,6 +93,28 @@ body:has(#brainbase-value-proof-review) {
|
|
|
93
93
|
font-weight: 700;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
.bb-vpr-setup-guide {
|
|
97
|
+
display: grid;
|
|
98
|
+
gap: var(--bb-space-1);
|
|
99
|
+
padding: var(--bb-space-2) var(--bb-space-3);
|
|
100
|
+
border: 1px solid var(--bb-color-border, #d8dee9);
|
|
101
|
+
background: var(--bb-color-panel, #ffffff);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.bb-vpr-setup-guide p,
|
|
105
|
+
.bb-vpr-setup-guide ol {
|
|
106
|
+
margin: 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.bb-vpr-setup-guide ol {
|
|
110
|
+
padding-left: var(--bb-space-5);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.bb-vpr-setup-guide code {
|
|
114
|
+
overflow-wrap: anywhere;
|
|
115
|
+
font-size: .92em;
|
|
116
|
+
}
|
|
117
|
+
|
|
96
118
|
.bb-vpr-rejected summary {
|
|
97
119
|
cursor: pointer;
|
|
98
120
|
}
|
|
@@ -225,6 +247,38 @@ body:has(#brainbase-value-proof-review) {
|
|
|
225
247
|
font-size: 13px;
|
|
226
248
|
}
|
|
227
249
|
|
|
250
|
+
/* A source-bearing target is still trace metadata until the host has read the
|
|
251
|
+
* exact Graph record. Only the same-origin link is styled as an action. */
|
|
252
|
+
.bb-vpr-source-target {
|
|
253
|
+
display: inline-flex;
|
|
254
|
+
flex-wrap: wrap;
|
|
255
|
+
gap: var(--bb-space-1, 4px);
|
|
256
|
+
align-items: baseline;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.bb-vpr-source-link {
|
|
260
|
+
color: var(--vpr-action);
|
|
261
|
+
font-weight: 680;
|
|
262
|
+
text-decoration: underline;
|
|
263
|
+
text-underline-offset: 2px;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.bb-vpr-source-status {
|
|
267
|
+
color: var(--vpr-muted);
|
|
268
|
+
font-size: 12px;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.bb-vpr-source-status.is-loading {
|
|
272
|
+
color: var(--vpr-muted);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.bb-vpr-source-status.is-not_found,
|
|
276
|
+
.bb-vpr-source-status.is-ambiguous,
|
|
277
|
+
.bb-vpr-source-status.is-forbidden,
|
|
278
|
+
.bb-vpr-source-status.is-unavailable {
|
|
279
|
+
color: var(--vpr-warning);
|
|
280
|
+
}
|
|
281
|
+
|
|
228
282
|
/* Detail: in the rail, or after the workspace when the host has none. */
|
|
229
283
|
.bb-vpr-inline-detail {
|
|
230
284
|
max-width: 760px;
|