@unson/brainbase-mcp 0.10.15 → 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.
- package/contracts/judgment-value-proof/schema.json +19 -1
- package/dist/execution-authority-http.d.ts +4 -2
- package/dist/execution-authority-http.js +101 -6
- package/dist/execution-authority.d.ts +89 -8
- package/dist/execution-authority.js +360 -127
- 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/dist/server-support.d.ts +140 -0
- package/dist/server-support.js +158 -0
- 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
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small HTTP-host helpers shared by Brainbase servers: a structured error with
|
|
3
|
+
* client-safe JSON, an async route wrapper, and a JSON logger that masks
|
|
4
|
+
* secrets. They carry no organization, tenant, or deployment concept.
|
|
5
|
+
*/
|
|
6
|
+
/** Standard error codes. `code` is safe to expose to clients. */
|
|
7
|
+
export const ErrorCodes = {
|
|
8
|
+
VALIDATION_ERROR: { code: 'VALIDATION_ERROR', statusCode: 400 },
|
|
9
|
+
INVALID_DATE_FORMAT: { code: 'INVALID_DATE_FORMAT', statusCode: 400 },
|
|
10
|
+
MISSING_REQUIRED_FIELD: { code: 'MISSING_REQUIRED_FIELD', statusCode: 400 },
|
|
11
|
+
UNSUPPORTED_FORMAT: { code: 'UNSUPPORTED_FORMAT', statusCode: 400 },
|
|
12
|
+
UNAUTHORIZED: { code: 'UNAUTHORIZED', statusCode: 401 },
|
|
13
|
+
FORBIDDEN: { code: 'FORBIDDEN', statusCode: 403 },
|
|
14
|
+
CROSS_TENANT_CANDIDATE: { code: 'CROSS_TENANT_CANDIDATE', statusCode: 403 },
|
|
15
|
+
PROJECT_NOT_FOUND: { code: 'PROJECT_NOT_FOUND', statusCode: 404 },
|
|
16
|
+
SESSION_NOT_FOUND: { code: 'SESSION_NOT_FOUND', statusCode: 404 },
|
|
17
|
+
TASK_NOT_FOUND: { code: 'TASK_NOT_FOUND', statusCode: 404 },
|
|
18
|
+
SCHEDULE_NOT_FOUND: { code: 'SCHEDULE_NOT_FOUND', statusCode: 404 },
|
|
19
|
+
WORKTREE_NOT_FOUND: { code: 'WORKTREE_NOT_FOUND', statusCode: 404 },
|
|
20
|
+
EVENT_NOT_FOUND: { code: 'EVENT_NOT_FOUND', statusCode: 404 },
|
|
21
|
+
CONFLICT: { code: 'CONFLICT', statusCode: 409 },
|
|
22
|
+
PORT_IN_USE: { code: 'PORT_IN_USE', statusCode: 409 },
|
|
23
|
+
INTERNAL_ERROR: { code: 'INTERNAL_ERROR', statusCode: 500 },
|
|
24
|
+
DATABASE_ERROR: { code: 'DATABASE_ERROR', statusCode: 500 },
|
|
25
|
+
PROJECT_CATALOG_UNAVAILABLE: { code: 'PROJECT_CATALOG_UNAVAILABLE', statusCode: 503 },
|
|
26
|
+
TIMEOUT: { code: 'TIMEOUT', statusCode: 504 },
|
|
27
|
+
};
|
|
28
|
+
/** A structured error: the message and code go to clients, details stay in logs. */
|
|
29
|
+
export class AppError extends Error {
|
|
30
|
+
code;
|
|
31
|
+
statusCode;
|
|
32
|
+
timestamp;
|
|
33
|
+
details;
|
|
34
|
+
cause;
|
|
35
|
+
constructor(message, errorCode, options = {}) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = 'AppError';
|
|
38
|
+
this.code = errorCode.code;
|
|
39
|
+
this.statusCode = errorCode.statusCode;
|
|
40
|
+
this.timestamp = new Date().toISOString();
|
|
41
|
+
this.details = options.details || null;
|
|
42
|
+
if (options.cause)
|
|
43
|
+
this.cause = options.cause;
|
|
44
|
+
}
|
|
45
|
+
/** Client-safe JSON. It never includes details. */
|
|
46
|
+
toJSON() {
|
|
47
|
+
return { error: { code: this.code, message: this.message, timestamp: this.timestamp } };
|
|
48
|
+
}
|
|
49
|
+
/** Log object, including details and the cause message. */
|
|
50
|
+
toLog() {
|
|
51
|
+
const log = {
|
|
52
|
+
code: this.code,
|
|
53
|
+
message: this.message,
|
|
54
|
+
statusCode: this.statusCode,
|
|
55
|
+
timestamp: this.timestamp,
|
|
56
|
+
};
|
|
57
|
+
if (this.details)
|
|
58
|
+
log.details = this.details;
|
|
59
|
+
if (this.cause)
|
|
60
|
+
log.cause = this.cause instanceof Error ? this.cause.message : String(this.cause);
|
|
61
|
+
return log;
|
|
62
|
+
}
|
|
63
|
+
static isAppError(error) {
|
|
64
|
+
return error instanceof AppError;
|
|
65
|
+
}
|
|
66
|
+
static notFound(resource, id) {
|
|
67
|
+
const dynamicCode = ErrorCodes[`${resource.toUpperCase()}_NOT_FOUND`];
|
|
68
|
+
return new AppError(`${resource} '${id}' not found`, dynamicCode || ErrorCodes.SESSION_NOT_FOUND);
|
|
69
|
+
}
|
|
70
|
+
static validation(message, details) {
|
|
71
|
+
return new AppError(message, ErrorCodes.VALIDATION_ERROR, { details });
|
|
72
|
+
}
|
|
73
|
+
static unauthorized(message = 'Unauthorized') {
|
|
74
|
+
return new AppError(message, ErrorCodes.UNAUTHORIZED);
|
|
75
|
+
}
|
|
76
|
+
static forbidden(message = 'Forbidden', details) {
|
|
77
|
+
return new AppError(message, ErrorCodes.FORBIDDEN, { details });
|
|
78
|
+
}
|
|
79
|
+
static conflict(message, details) {
|
|
80
|
+
return new AppError(message, ErrorCodes.CONFLICT, { details });
|
|
81
|
+
}
|
|
82
|
+
static internal(message = 'Internal server error', cause) {
|
|
83
|
+
return new AppError(message, ErrorCodes.INTERNAL_ERROR, { cause });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Wraps an async route handler so that a rejection reaches the error middleware. */
|
|
87
|
+
export function asyncHandler(fn) {
|
|
88
|
+
return (req, res, next) => {
|
|
89
|
+
Promise.resolve(fn(req, res, next)).catch((error) => {
|
|
90
|
+
next(error);
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const SENSITIVE_KEYS = [
|
|
95
|
+
'password', 'token', 'secret', 'key', 'credential', 'auth', 'apikey', 'api_key',
|
|
96
|
+
'access_token', 'refresh_token', 'private', 'cookie', 'session',
|
|
97
|
+
];
|
|
98
|
+
const SENSITIVE_PATH_PATTERNS = [/\/\.env/, /\/credentials/, /\/secrets/, /\/\.ssh/, /\/\.gnupg/];
|
|
99
|
+
function redact(value, depth = 0) {
|
|
100
|
+
if (depth > 10)
|
|
101
|
+
return '[MAX_DEPTH]';
|
|
102
|
+
if (value === null || value === undefined)
|
|
103
|
+
return value;
|
|
104
|
+
if (typeof value !== 'object') {
|
|
105
|
+
if (typeof value === 'string') {
|
|
106
|
+
if (/^eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+$/.test(value))
|
|
107
|
+
return '[JWT_REDACTED]';
|
|
108
|
+
if (/^[a-fA-F0-9]{32,}$/.test(value))
|
|
109
|
+
return '[HEX_KEY_REDACTED]';
|
|
110
|
+
if (SENSITIVE_PATH_PATTERNS.some((pattern) => pattern.test(value)))
|
|
111
|
+
return '[SENSITIVE_PATH_REDACTED]';
|
|
112
|
+
}
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
if (Array.isArray(value))
|
|
116
|
+
return value.map((item) => redact(item, depth + 1));
|
|
117
|
+
const result = {};
|
|
118
|
+
for (const [key, item] of Object.entries(value)) {
|
|
119
|
+
const lowerKey = key.toLowerCase();
|
|
120
|
+
result[key] = SENSITIVE_KEYS.some((sensitive) => lowerKey.includes(sensitive)) ? '[REDACTED]' : redact(item, depth + 1);
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
function formatLog(level, msg, data = {}) {
|
|
125
|
+
const masked = redact(data);
|
|
126
|
+
return JSON.stringify({
|
|
127
|
+
timestamp: new Date().toISOString(),
|
|
128
|
+
level,
|
|
129
|
+
msg,
|
|
130
|
+
...(masked && typeof masked === 'object' && !Array.isArray(masked) ? masked : {}),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/** JSON-line logger that masks secrets. DEBUG enables debug lines. */
|
|
134
|
+
export const logger = {
|
|
135
|
+
info(msg, data = {}) {
|
|
136
|
+
console.log(formatLog('info', msg, data));
|
|
137
|
+
},
|
|
138
|
+
warn(msg, data = {}) {
|
|
139
|
+
console.warn(formatLog('warn', msg, data));
|
|
140
|
+
},
|
|
141
|
+
error(msg, data = {}) {
|
|
142
|
+
let entry;
|
|
143
|
+
if (data instanceof Error) {
|
|
144
|
+
entry = { error: data.message, stack: data.stack };
|
|
145
|
+
}
|
|
146
|
+
else if (data.error instanceof Error) {
|
|
147
|
+
entry = { ...data, error: data.error.message, stack: data.error.stack };
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
entry = data;
|
|
151
|
+
}
|
|
152
|
+
console.error(formatLog('error', msg, entry));
|
|
153
|
+
},
|
|
154
|
+
debug(msg, data = {}) {
|
|
155
|
+
if (process.env.DEBUG)
|
|
156
|
+
console.log(formatLog('debug', msg, data));
|
|
157
|
+
},
|
|
158
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unson/brainbase-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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",
|
|
@@ -191,6 +191,10 @@
|
|
|
191
191
|
"types": "./dist/execution-authority-http.d.ts",
|
|
192
192
|
"import": "./dist/execution-authority-http.js"
|
|
193
193
|
},
|
|
194
|
+
"./server-support": {
|
|
195
|
+
"types": "./dist/server-support.d.ts",
|
|
196
|
+
"import": "./dist/server-support.js"
|
|
197
|
+
},
|
|
194
198
|
"./company-os-impact-review": {
|
|
195
199
|
"types": "./dist/company-os-impact-review.d.ts",
|
|
196
200
|
"import": "./dist/company-os-impact-review.js"
|
|
@@ -344,5 +348,5 @@
|
|
|
344
348
|
"engines": {
|
|
345
349
|
"node": ">=20"
|
|
346
350
|
},
|
|
347
|
-
"gitHead": "
|
|
351
|
+
"gitHead": "084154257b751ba7bf6b303aa889c42ac551c9b9"
|
|
348
352
|
}
|
|
@@ -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;
|
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 };
|