@unson/brainbase-mcp 0.10.3 → 0.10.5
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/foundation-graph-http.js +24 -2
- package/dist/graph-foundation-reader.d.ts +15 -1
- package/dist/graph-foundation-reader.js +222 -15
- package/package.json +2 -2
- package/ui/project-workspace.js +101 -52
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { FOUNDATION_HTTP_CONTRACT_VERSION, createFoundationHttpRouter } from './foundation-http.js';
|
|
1
|
+
import { FOUNDATION_HTTP_CONTRACT_VERSION, createFoundationHttpRouter, createObjectiveFoundationRoute } from './foundation-http.js';
|
|
2
|
+
import { checkObjectiveReadiness } from './company-os-objectives.js';
|
|
2
3
|
import { createFoundationPublicProvider, createFoundationPublicRoute } from './foundation-public-provider.js';
|
|
3
4
|
import { createGraphFoundationReaders } from './graph-foundation-reader.js';
|
|
5
|
+
import { FoundationStoreError } from './foundation-store.js';
|
|
4
6
|
/**
|
|
5
7
|
* Readiness probe for the Graph history contract used by the public adapter.
|
|
6
8
|
*
|
|
@@ -217,16 +219,36 @@ export function createFoundationGraphHttpHandler(options) {
|
|
|
217
219
|
}
|
|
218
220
|
const readers = createGraphFoundationReaders({
|
|
219
221
|
context,
|
|
222
|
+
selectedProjectCode: scopeId,
|
|
220
223
|
query: async (text, values) => ({
|
|
221
224
|
rows: (await client.query(text, values ?? [])).rows
|
|
222
225
|
})
|
|
223
226
|
});
|
|
224
227
|
const provider = createFoundationPublicProvider(readers);
|
|
225
228
|
const route = createFoundationPublicRoute(provider);
|
|
229
|
+
const objectiveRoute = createObjectiveFoundationRoute({
|
|
230
|
+
objectives: {
|
|
231
|
+
async createObjective() {
|
|
232
|
+
throw new FoundationStoreError('unsupported_graph', 'Objective writes are not configured for the Graph read adapter');
|
|
233
|
+
},
|
|
234
|
+
async readObjective(id, objectiveContext, revision) {
|
|
235
|
+
return revision === undefined
|
|
236
|
+
? readers.store.readLatest('objective', id, objectiveContext)
|
|
237
|
+
: readers.store.read({ id, type: 'objective', revision }, objectiveContext);
|
|
238
|
+
},
|
|
239
|
+
async updateObjective() {
|
|
240
|
+
throw new FoundationStoreError('unsupported_graph', 'Objective writes are not configured for the Graph read adapter');
|
|
241
|
+
},
|
|
242
|
+
async checkObjectiveReadiness(id, objectiveContext, revision) {
|
|
243
|
+
return checkObjectiveReadiness(readers.store, id, objectiveContext, revision);
|
|
244
|
+
},
|
|
245
|
+
list: (type, objectiveContext) => readers.store.list(type, objectiveContext)
|
|
246
|
+
}
|
|
247
|
+
});
|
|
226
248
|
const handler = createFoundationHttpRouter({
|
|
227
249
|
resolveContext: () => context,
|
|
228
250
|
csrf: options.csrf,
|
|
229
|
-
routes: [route],
|
|
251
|
+
routes: [route, objectiveRoute],
|
|
230
252
|
bodyLimitBytes
|
|
231
253
|
});
|
|
232
254
|
return handler.handle(request);
|
|
@@ -13,6 +13,18 @@ export declare const GRAPH_FOUNDATION_READ_CONTRACT_VERSION: "graph-foundation-h
|
|
|
13
13
|
* future providers fail closed when they do not prove current-row visibility.
|
|
14
14
|
*/
|
|
15
15
|
export declare const GRAPH_FOUNDATION_READ_SQL = "\nSELECT\n history.entity_id,\n history.entity_type,\n history.revision,\n history.payload,\n history.project_id,\n history.role_min,\n history.sensitivity,\n history.lifecycle_status,\n history.storage_digest,\n history.captured_at,\n (\n history.storage_digest = 'sha256:' ||\n encode(sha256(convert_to(history.payload::text, 'UTF8')), 'hex')\n ) AS storage_digest_valid,\n current_entity.id AS current_entity_id,\n current_entity.entity_type AS current_entity_type,\n current_entity.payload AS current_payload,\n current_entity.project_id AS current_project_id,\n current_entity.role_min AS current_role_min,\n current_entity.sensitivity AS current_sensitivity,\n current_entity.lifecycle_status AS current_lifecycle_status,\n projects.code AS current_project_code,\n true AS current_visible\nFROM public.graph_foundation_revisions AS history\nJOIN public.graph_entities AS current_entity\n ON current_entity.id = history.entity_id\n AND current_entity.entity_type = history.entity_type\nJOIN public.projects AS projects\n ON projects.id = current_entity.project_id\nWHERE history.entity_id = $1\n AND history.entity_type = $2\n AND history.revision = $3\n";
|
|
16
|
+
/**
|
|
17
|
+
* Read the newest immutable revision for one Foundation entity. The current
|
|
18
|
+
* Graph row remains part of the query so RLS and the adapter's current-row
|
|
19
|
+
* checks apply identically to latest and historical reads.
|
|
20
|
+
*/
|
|
21
|
+
export declare const GRAPH_FOUNDATION_LATEST_SQL = "\nSELECT\n history.entity_id,\n history.entity_type,\n history.revision,\n history.payload,\n history.project_id,\n history.role_min,\n history.sensitivity,\n history.lifecycle_status,\n history.storage_digest,\n history.captured_at,\n (\n history.storage_digest = 'sha256:' ||\n encode(sha256(convert_to(history.payload::text, 'UTF8')), 'hex')\n ) AS storage_digest_valid,\n current_entity.id AS current_entity_id,\n current_entity.entity_type AS current_entity_type,\n current_entity.payload AS current_payload,\n current_entity.project_id AS current_project_id,\n current_entity.role_min AS current_role_min,\n current_entity.sensitivity AS current_sensitivity,\n current_entity.lifecycle_status AS current_lifecycle_status,\n projects.code AS current_project_code,\n true AS current_visible\nFROM public.graph_entities AS current_entity\nLEFT JOIN public.projects AS projects\n ON projects.id = current_entity.project_id\nLEFT JOIN LATERAL (\n SELECT history.*\n FROM public.graph_foundation_revisions AS history\n WHERE history.entity_id = current_entity.id\n AND history.entity_type = current_entity.entity_type\n ORDER BY history.revision::numeric DESC\n LIMIT 1\n) AS history ON true\nWHERE current_entity.id = $1\n AND current_entity.entity_type = $2\nORDER BY history.revision::numeric DESC NULLS LAST\nLIMIT 1\n";
|
|
22
|
+
/**
|
|
23
|
+
* List the newest revision of each Foundation entity visible in the selected
|
|
24
|
+
* project. The project filter is by canonical project code, which is the
|
|
25
|
+
* scope identifier selected by the organization BFF.
|
|
26
|
+
*/
|
|
27
|
+
export declare const GRAPH_FOUNDATION_LIST_SQL = "\nSELECT\n history.entity_id,\n history.entity_type,\n history.revision,\n history.payload,\n history.project_id,\n history.role_min,\n history.sensitivity,\n history.lifecycle_status,\n history.storage_digest,\n history.captured_at,\n (\n history.storage_digest = 'sha256:' ||\n encode(sha256(convert_to(history.payload::text, 'UTF8')), 'hex')\n ) AS storage_digest_valid,\n current_entity.id AS current_entity_id,\n current_entity.entity_type AS current_entity_type,\n current_entity.payload AS current_payload,\n current_entity.project_id AS current_project_id,\n current_entity.role_min AS current_role_min,\n current_entity.sensitivity AS current_sensitivity,\n current_entity.lifecycle_status AS current_lifecycle_status,\n projects.code AS current_project_code,\n true AS current_visible\nFROM public.graph_entities AS current_entity\nLEFT JOIN public.projects AS projects\n ON projects.id = current_entity.project_id\nLEFT JOIN LATERAL (\n SELECT history.*\n FROM public.graph_foundation_revisions AS history\n WHERE history.entity_id = current_entity.id\n AND history.entity_type = current_entity.entity_type\n ORDER BY history.revision::numeric DESC\n LIMIT 1\n) AS history ON true\nWHERE ($1::text IS NULL OR current_entity.entity_type = $1)\n AND ($2::text IS NULL OR projects.code = $2 OR projects.id IS NULL)\n AND current_entity.entity_type IN ('objective', 'variable', 'model', 'constraint')\nORDER BY current_entity.id, current_entity.entity_type\n";
|
|
16
28
|
/** Flat row shape returned by GRAPH_FOUNDATION_READ_SQL. */
|
|
17
29
|
export interface GraphFoundationHistoryRow {
|
|
18
30
|
readonly entity_id?: unknown;
|
|
@@ -46,9 +58,11 @@ export interface GraphFoundationReaderOptions {
|
|
|
46
58
|
readonly context: FoundationStoreContext;
|
|
47
59
|
/** Query callback bound to the host's already-authenticated transaction. */
|
|
48
60
|
readonly query: GraphFoundationQuery;
|
|
61
|
+
/** Canonical project code selected by the organization boundary. */
|
|
62
|
+
readonly selectedProjectCode?: string;
|
|
49
63
|
}
|
|
50
64
|
export interface GraphFoundationReaders {
|
|
51
|
-
readonly store: Pick<FoundationRevisionStore, 'read'>;
|
|
65
|
+
readonly store: Pick<FoundationRevisionStore, 'read' | 'readLatest' | 'list'>;
|
|
52
66
|
readonly philosophyReader: PhilosophyRevisionReader;
|
|
53
67
|
}
|
|
54
68
|
/**
|
|
@@ -50,6 +50,98 @@ WHERE history.entity_id = $1
|
|
|
50
50
|
AND history.entity_type = $2
|
|
51
51
|
AND history.revision = $3
|
|
52
52
|
`;
|
|
53
|
+
/**
|
|
54
|
+
* Read the newest immutable revision for one Foundation entity. The current
|
|
55
|
+
* Graph row remains part of the query so RLS and the adapter's current-row
|
|
56
|
+
* checks apply identically to latest and historical reads.
|
|
57
|
+
*/
|
|
58
|
+
export const GRAPH_FOUNDATION_LATEST_SQL = `
|
|
59
|
+
SELECT
|
|
60
|
+
history.entity_id,
|
|
61
|
+
history.entity_type,
|
|
62
|
+
history.revision,
|
|
63
|
+
history.payload,
|
|
64
|
+
history.project_id,
|
|
65
|
+
history.role_min,
|
|
66
|
+
history.sensitivity,
|
|
67
|
+
history.lifecycle_status,
|
|
68
|
+
history.storage_digest,
|
|
69
|
+
history.captured_at,
|
|
70
|
+
(
|
|
71
|
+
history.storage_digest = 'sha256:' ||
|
|
72
|
+
encode(sha256(convert_to(history.payload::text, 'UTF8')), 'hex')
|
|
73
|
+
) AS storage_digest_valid,
|
|
74
|
+
current_entity.id AS current_entity_id,
|
|
75
|
+
current_entity.entity_type AS current_entity_type,
|
|
76
|
+
current_entity.payload AS current_payload,
|
|
77
|
+
current_entity.project_id AS current_project_id,
|
|
78
|
+
current_entity.role_min AS current_role_min,
|
|
79
|
+
current_entity.sensitivity AS current_sensitivity,
|
|
80
|
+
current_entity.lifecycle_status AS current_lifecycle_status,
|
|
81
|
+
projects.code AS current_project_code,
|
|
82
|
+
true AS current_visible
|
|
83
|
+
FROM public.graph_entities AS current_entity
|
|
84
|
+
LEFT JOIN public.projects AS projects
|
|
85
|
+
ON projects.id = current_entity.project_id
|
|
86
|
+
LEFT JOIN LATERAL (
|
|
87
|
+
SELECT history.*
|
|
88
|
+
FROM public.graph_foundation_revisions AS history
|
|
89
|
+
WHERE history.entity_id = current_entity.id
|
|
90
|
+
AND history.entity_type = current_entity.entity_type
|
|
91
|
+
ORDER BY history.revision::numeric DESC
|
|
92
|
+
LIMIT 1
|
|
93
|
+
) AS history ON true
|
|
94
|
+
WHERE current_entity.id = $1
|
|
95
|
+
AND current_entity.entity_type = $2
|
|
96
|
+
ORDER BY history.revision::numeric DESC NULLS LAST
|
|
97
|
+
LIMIT 1
|
|
98
|
+
`;
|
|
99
|
+
/**
|
|
100
|
+
* List the newest revision of each Foundation entity visible in the selected
|
|
101
|
+
* project. The project filter is by canonical project code, which is the
|
|
102
|
+
* scope identifier selected by the organization BFF.
|
|
103
|
+
*/
|
|
104
|
+
export const GRAPH_FOUNDATION_LIST_SQL = `
|
|
105
|
+
SELECT
|
|
106
|
+
history.entity_id,
|
|
107
|
+
history.entity_type,
|
|
108
|
+
history.revision,
|
|
109
|
+
history.payload,
|
|
110
|
+
history.project_id,
|
|
111
|
+
history.role_min,
|
|
112
|
+
history.sensitivity,
|
|
113
|
+
history.lifecycle_status,
|
|
114
|
+
history.storage_digest,
|
|
115
|
+
history.captured_at,
|
|
116
|
+
(
|
|
117
|
+
history.storage_digest = 'sha256:' ||
|
|
118
|
+
encode(sha256(convert_to(history.payload::text, 'UTF8')), 'hex')
|
|
119
|
+
) AS storage_digest_valid,
|
|
120
|
+
current_entity.id AS current_entity_id,
|
|
121
|
+
current_entity.entity_type AS current_entity_type,
|
|
122
|
+
current_entity.payload AS current_payload,
|
|
123
|
+
current_entity.project_id AS current_project_id,
|
|
124
|
+
current_entity.role_min AS current_role_min,
|
|
125
|
+
current_entity.sensitivity AS current_sensitivity,
|
|
126
|
+
current_entity.lifecycle_status AS current_lifecycle_status,
|
|
127
|
+
projects.code AS current_project_code,
|
|
128
|
+
true AS current_visible
|
|
129
|
+
FROM public.graph_entities AS current_entity
|
|
130
|
+
LEFT JOIN public.projects AS projects
|
|
131
|
+
ON projects.id = current_entity.project_id
|
|
132
|
+
LEFT JOIN LATERAL (
|
|
133
|
+
SELECT history.*
|
|
134
|
+
FROM public.graph_foundation_revisions AS history
|
|
135
|
+
WHERE history.entity_id = current_entity.id
|
|
136
|
+
AND history.entity_type = current_entity.entity_type
|
|
137
|
+
ORDER BY history.revision::numeric DESC
|
|
138
|
+
LIMIT 1
|
|
139
|
+
) AS history ON true
|
|
140
|
+
WHERE ($1::text IS NULL OR current_entity.entity_type = $1)
|
|
141
|
+
AND ($2::text IS NULL OR projects.code = $2 OR projects.id IS NULL)
|
|
142
|
+
AND current_entity.entity_type IN ('objective', 'variable', 'model', 'constraint')
|
|
143
|
+
ORDER BY current_entity.id, current_entity.entity_type
|
|
144
|
+
`;
|
|
53
145
|
const FOUNDATION_TYPES = ['objective', 'variable', 'model', 'constraint'];
|
|
54
146
|
const REVISION_PATTERN = /^[1-9]\d*$/u;
|
|
55
147
|
const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/u;
|
|
@@ -65,6 +157,7 @@ export function createGraphFoundationReaders(options) {
|
|
|
65
157
|
assertReaderOptions(options);
|
|
66
158
|
const context = cloneContext(options.context);
|
|
67
159
|
const query = options.query;
|
|
160
|
+
const selectedProjectCode = options.selectedProjectCode;
|
|
68
161
|
const store = {
|
|
69
162
|
async read(reference, suppliedContext) {
|
|
70
163
|
assertTrustedPrincipal(context, suppliedContext);
|
|
@@ -75,20 +168,42 @@ export function createGraphFoundationReaders(options) {
|
|
|
75
168
|
if (rows.length !== 1) {
|
|
76
169
|
throw corrupt('Graph foundation history returned multiple rows for one revision');
|
|
77
170
|
}
|
|
171
|
+
return validateFoundationRecord(rows[0], reference, context, selectedProjectCode);
|
|
172
|
+
},
|
|
173
|
+
async readLatest(type, id, suppliedContext) {
|
|
174
|
+
assertTrustedPrincipal(context, suppliedContext);
|
|
175
|
+
assertFoundationType(type);
|
|
176
|
+
assertEntityId(id);
|
|
177
|
+
const rows = await executeLatestRead(query, id, type);
|
|
178
|
+
if (rows.length === 0)
|
|
179
|
+
return null;
|
|
180
|
+
if (rows.length !== 1) {
|
|
181
|
+
throw corrupt('Graph foundation latest query returned multiple rows');
|
|
182
|
+
}
|
|
78
183
|
const row = rows[0];
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
// A history row remains readable only through the current Graph row,
|
|
83
|
-
// including its current project and current scope. The old payload is
|
|
84
|
-
// returned unchanged after these current-row checks.
|
|
85
|
-
if (history.projectId !== current.projectId) {
|
|
86
|
-
throw scopeViolation('Historical foundation project differs from current Graph project');
|
|
184
|
+
const reference = rowReference(row);
|
|
185
|
+
if (reference.id !== id || reference.type !== type) {
|
|
186
|
+
throw corrupt('Graph foundation latest row identity does not match the request');
|
|
87
187
|
}
|
|
88
|
-
return
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
188
|
+
return validateFoundationRecord(row, reference, context, selectedProjectCode, true);
|
|
189
|
+
},
|
|
190
|
+
async list(type, suppliedContext) {
|
|
191
|
+
assertTrustedPrincipal(context, suppliedContext);
|
|
192
|
+
if (type !== undefined)
|
|
193
|
+
assertFoundationType(type);
|
|
194
|
+
const rows = await executeList(query, type, selectedProjectCode);
|
|
195
|
+
const seen = new Set();
|
|
196
|
+
return rows.map((row) => {
|
|
197
|
+
const reference = rowReference(row);
|
|
198
|
+
if (type !== undefined && reference.type !== type) {
|
|
199
|
+
throw corrupt('Graph foundation list returned an unexpected entity type');
|
|
200
|
+
}
|
|
201
|
+
const key = `${reference.type}:${reference.id}`;
|
|
202
|
+
if (seen.has(key))
|
|
203
|
+
throw corrupt('Graph foundation list returned duplicate entity rows');
|
|
204
|
+
seen.add(key);
|
|
205
|
+
return validateFoundationRecord(row, reference, context, selectedProjectCode, true);
|
|
206
|
+
});
|
|
92
207
|
}
|
|
93
208
|
};
|
|
94
209
|
const philosophyReader = {
|
|
@@ -254,6 +369,66 @@ async function executeRead(query, id, type, revision) {
|
|
|
254
369
|
}
|
|
255
370
|
throw corrupt('Graph foundation history query returned an invalid result');
|
|
256
371
|
}
|
|
372
|
+
async function executeLatestRead(query, id, type) {
|
|
373
|
+
let result;
|
|
374
|
+
try {
|
|
375
|
+
result = await query(GRAPH_FOUNDATION_LATEST_SQL, [id, type]);
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
throw corrupt('Graph foundation latest query failed');
|
|
379
|
+
}
|
|
380
|
+
if (Array.isArray(result))
|
|
381
|
+
return result;
|
|
382
|
+
if (!Array.isArray(result) && 'rows' in result && Array.isArray(result.rows)) {
|
|
383
|
+
return result.rows;
|
|
384
|
+
}
|
|
385
|
+
throw corrupt('Graph foundation latest query returned an invalid result');
|
|
386
|
+
}
|
|
387
|
+
async function executeList(query, type, selectedProjectCode) {
|
|
388
|
+
let result;
|
|
389
|
+
try {
|
|
390
|
+
result = await query(GRAPH_FOUNDATION_LIST_SQL, [type ?? null, selectedProjectCode ?? null]);
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
throw corrupt('Graph foundation list query failed');
|
|
394
|
+
}
|
|
395
|
+
if (Array.isArray(result))
|
|
396
|
+
return result;
|
|
397
|
+
if (!Array.isArray(result) && 'rows' in result && Array.isArray(result.rows)) {
|
|
398
|
+
return result.rows;
|
|
399
|
+
}
|
|
400
|
+
throw corrupt('Graph foundation list query returned an invalid result');
|
|
401
|
+
}
|
|
402
|
+
function validateFoundationRecord(row, reference, context, selectedProjectCode, requireCurrentRevision = false) {
|
|
403
|
+
const history = validateHistoryRow(row, reference.id, reference.type, reference.revision);
|
|
404
|
+
const current = validateCurrentRow(row, reference.id, reference.type);
|
|
405
|
+
if (requireCurrentRevision && current.projectCode === undefined) {
|
|
406
|
+
throw corrupt('Current Graph project code is missing');
|
|
407
|
+
}
|
|
408
|
+
authorizeCurrentFoundation(current, context, selectedProjectCode);
|
|
409
|
+
// Latest reads must prove that the current Graph payload points at the
|
|
410
|
+
// revision returned by the history query. Otherwise a missing newer history
|
|
411
|
+
// row could silently fall back to an older immutable revision.
|
|
412
|
+
if (requireCurrentRevision && current.foundation?.revision !== reference.revision) {
|
|
413
|
+
throw corrupt('Current Graph foundation revision does not match the latest history revision');
|
|
414
|
+
}
|
|
415
|
+
// A history row remains readable only through the current Graph row,
|
|
416
|
+
// including its current project and current scope. The old payload is
|
|
417
|
+
// returned unchanged after these current-row checks.
|
|
418
|
+
if (history.projectId !== current.projectId) {
|
|
419
|
+
throw scopeViolation('Historical foundation project differs from current Graph project');
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
definition: cloneFoundationDefinition(history.definition),
|
|
423
|
+
digest: digestFoundationDefinition(history.definition)
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
function rowReference(row) {
|
|
427
|
+
const id = requireString(row.entity_id, 'Foundation history entity_id');
|
|
428
|
+
const type = parseFoundationType(row.entity_type);
|
|
429
|
+
const revision = requireRevision(row.revision, 'Foundation history revision');
|
|
430
|
+
return { id, type, revision };
|
|
431
|
+
}
|
|
257
432
|
function validateHistoryRow(row, id, type, revision) {
|
|
258
433
|
assertRowIdentity(row, id, type, revision);
|
|
259
434
|
assertStorageDigest(row);
|
|
@@ -301,21 +476,27 @@ function validateCurrentRow(row, id, type) {
|
|
|
301
476
|
throw new FoundationStoreError('authorization_denied', 'The current Graph row is not active');
|
|
302
477
|
}
|
|
303
478
|
const projectId = requireString(row.current_project_id, 'Current Graph project_id');
|
|
479
|
+
const projectCode = typeof row.current_project_code === 'string' && row.current_project_code.length > 0
|
|
480
|
+
? row.current_project_code
|
|
481
|
+
: undefined;
|
|
304
482
|
const payload = requireObject(row.current_payload, 'Current Graph payload');
|
|
305
483
|
if (type === 'philosophy') {
|
|
306
484
|
const applicability = parseJudgmentApplicability(payload);
|
|
307
|
-
return { payload, projectId, philosophy: { applicability } };
|
|
485
|
+
return { payload, projectId, projectCode, philosophy: { applicability } };
|
|
308
486
|
}
|
|
309
487
|
const definition = requireObject(payload.foundation, 'Current Graph payload.foundation');
|
|
310
488
|
const result = validateFoundationDefinition(definition, { use: 'draft' });
|
|
311
489
|
if (!result.valid || definition.id !== id || definition.type !== type) {
|
|
312
490
|
throw corrupt('Current Graph foundation definition is invalid');
|
|
313
491
|
}
|
|
314
|
-
return { payload, projectId, foundation: definition };
|
|
492
|
+
return { payload, projectId, projectCode, foundation: definition };
|
|
315
493
|
}
|
|
316
|
-
function authorizeCurrentFoundation(current, context) {
|
|
494
|
+
function authorizeCurrentFoundation(current, context, selectedProjectCode) {
|
|
317
495
|
if (!current.foundation)
|
|
318
496
|
throw corrupt('Current Graph foundation definition is missing');
|
|
497
|
+
if (selectedProjectCode !== undefined && current.projectCode !== selectedProjectCode) {
|
|
498
|
+
throw scopeViolation('The current Graph project is outside the selected project scope');
|
|
499
|
+
}
|
|
319
500
|
const definition = current.foundation;
|
|
320
501
|
if (!canRead(definition.acl, context.principal)) {
|
|
321
502
|
throw new FoundationStoreError('authorization_denied', 'The current foundation ACL does not permit this principal');
|
|
@@ -334,6 +515,28 @@ function assertRowIdentity(row, id, type, revision) {
|
|
|
334
515
|
throw corrupt('Foundation history identity does not match the requested revision');
|
|
335
516
|
}
|
|
336
517
|
}
|
|
518
|
+
function parseFoundationType(value) {
|
|
519
|
+
if (typeof value === 'string' && FOUNDATION_TYPES.includes(value)) {
|
|
520
|
+
return value;
|
|
521
|
+
}
|
|
522
|
+
throw corrupt('Foundation history entity_type is invalid');
|
|
523
|
+
}
|
|
524
|
+
function requireRevision(value, label) {
|
|
525
|
+
if (typeof value !== 'string' || !REVISION_PATTERN.test(value)) {
|
|
526
|
+
throw corrupt(`${label} must be a positive revision`);
|
|
527
|
+
}
|
|
528
|
+
return value;
|
|
529
|
+
}
|
|
530
|
+
function assertEntityId(value) {
|
|
531
|
+
if (typeof value !== 'string' || value.trim().length === 0 || value.includes('\0')) {
|
|
532
|
+
throw new FoundationStoreError('invalid_input', 'A foundation entity id is required');
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
function assertFoundationType(value) {
|
|
536
|
+
if (!FOUNDATION_TYPES.includes(value)) {
|
|
537
|
+
throw new FoundationStoreError('invalid_input', 'A supported foundation type is required');
|
|
538
|
+
}
|
|
539
|
+
}
|
|
337
540
|
function assertFoundationReference(reference) {
|
|
338
541
|
if (!isPlainRecord(reference) || typeof reference.id !== 'string' || reference.id.length === 0 ||
|
|
339
542
|
!FOUNDATION_TYPES.includes(reference.type) ||
|
|
@@ -365,6 +568,10 @@ function assertReaderOptions(options) {
|
|
|
365
568
|
if (!isValidFoundationScope(scope))
|
|
366
569
|
throw new TypeError('A trusted Graph foundation scope is invalid');
|
|
367
570
|
}
|
|
571
|
+
if (options.selectedProjectCode !== undefined
|
|
572
|
+
&& (typeof options.selectedProjectCode !== 'string' || options.selectedProjectCode.trim().length === 0)) {
|
|
573
|
+
throw new TypeError('A selected Graph project code is invalid');
|
|
574
|
+
}
|
|
368
575
|
}
|
|
369
576
|
function assertTrustedPrincipal(trusted, supplied) {
|
|
370
577
|
if (!supplied || supplied.principal !== trusted.principal) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unson/brainbase-mcp",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.5",
|
|
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",
|
|
@@ -336,5 +336,5 @@
|
|
|
336
336
|
"engines": {
|
|
337
337
|
"node": ">=20"
|
|
338
338
|
},
|
|
339
|
-
"gitHead": "
|
|
339
|
+
"gitHead": "8cfad04bef7bf934165d96b979e9e5d52dd837dc"
|
|
340
340
|
}
|
package/ui/project-workspace.js
CHANGED
|
@@ -325,16 +325,62 @@ function contextSectionSkeleton(doc, { title = null, message = null } = {}) {
|
|
|
325
325
|
return group;
|
|
326
326
|
}
|
|
327
327
|
|
|
328
|
+
function renderProjectContextGroup(doc, item) {
|
|
329
|
+
const group = makeElement(doc, 'section', {
|
|
330
|
+
className: `bb-pkw-context-group${item.state === 'loading' ? ' bb-pkw-context-group-loading' : ''}`,
|
|
331
|
+
attrs: { 'aria-label': item.title, ...(item.state === 'loading' ? { 'aria-busy': 'true' } : {}) },
|
|
332
|
+
});
|
|
333
|
+
const heading = makeElement(doc, 'div', { className: 'bb-pkw-context-heading' });
|
|
334
|
+
heading.append(makeElement(doc, 'h4', { text: item.title }), badge(doc, item.state === 'ok' ? '利用可能' : item.state === 'loading' ? '読み込み中' : item.state === 'failed' ? '読み取り失敗' : '未接続', item.state === 'ok' ? 'success' : item.state === 'failed' ? 'danger' : 'muted'));
|
|
335
|
+
group.append(heading);
|
|
336
|
+
if (item.message && !['failed', 'unavailable'].includes(item.state)) group.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: item.message }));
|
|
337
|
+
if (item.asOf) group.append(makeElement(doc, 'small', { className: 'bb-pkw-muted', text: `読み取った時点: ${formatDateTime(item.asOf)}` }));
|
|
338
|
+
if (item.state === 'loading') {
|
|
339
|
+
group.append(skeletonBars(doc));
|
|
340
|
+
} else if (item.state === 'failed') {
|
|
341
|
+
group.append(workspaceNotice(doc, { label: '読み取り失敗', text: item.message ?? 'このセクションを読み取れませんでした。', tone: 'danger', role: 'alert' }));
|
|
342
|
+
} else if (item.state === 'unavailable') {
|
|
343
|
+
group.append(workspaceNotice(doc, { label: '未接続', text: item.message ?? 'このセクションは接続されていません。', tone: 'warning' }));
|
|
344
|
+
} else if (item.items.length > 0) {
|
|
345
|
+
const list = makeElement(doc, 'ul', { className: 'bb-pkw-context-list' });
|
|
346
|
+
for (const entry of item.items) {
|
|
347
|
+
const row = makeElement(doc, 'li');
|
|
348
|
+
row.append(makeElement(doc, 'strong', { text: entry.title }));
|
|
349
|
+
if (entry.status) row.append(badge(doc, entry.status, 'muted'));
|
|
350
|
+
const facts = [entry.summary, entry.owner ? `担当: ${entry.owner}` : null, entry.dueAt ? `期限: ${entry.dueAt}` : null, entry.updatedAt ? `更新: ${entry.updatedAt}` : null, entry.source ? `出典: ${entry.source}` : null].filter(Boolean);
|
|
351
|
+
if (facts.length > 0) row.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: facts.join(' · ') }));
|
|
352
|
+
list.append(row);
|
|
353
|
+
}
|
|
354
|
+
group.append(list);
|
|
355
|
+
} else if (item.state === 'ok') {
|
|
356
|
+
group.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: 'このセクションに表示できる記録はありません。' }));
|
|
357
|
+
}
|
|
358
|
+
return group;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function projectContextInsertionAnchor(doc) {
|
|
362
|
+
return makeElement(doc, 'span', {
|
|
363
|
+
className: 'bb-pkw-context-anchor',
|
|
364
|
+
attrs: { 'aria-hidden': 'true', hidden: 'true' },
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
328
368
|
/** Renders supplementary context while preserving each section's own state. */
|
|
329
|
-
export function renderProjectContext(doc, context
|
|
369
|
+
export function renderProjectContext(doc, context, {
|
|
370
|
+
sections: selectedSections = null,
|
|
371
|
+
includeStatus = true,
|
|
372
|
+
includeHeading = true,
|
|
373
|
+
} = {}) {
|
|
330
374
|
if (!context) return null;
|
|
331
|
-
const sections = Array.isArray(
|
|
375
|
+
const sections = Array.isArray(selectedSections)
|
|
376
|
+
? selectedSections
|
|
377
|
+
: Array.isArray(context.sections) ? context.sections : [];
|
|
332
378
|
const section = makeElement(doc, 'section', {
|
|
333
379
|
className: 'bb-pkw-section bb-pkw-context',
|
|
334
380
|
attrs: { 'aria-label': '補足情報' },
|
|
335
381
|
});
|
|
336
|
-
section.append(makeElement(doc, 'h3', { text: '補足情報' }));
|
|
337
|
-
if (context.state !== 'ok') {
|
|
382
|
+
if (includeHeading) section.append(makeElement(doc, 'h3', { text: '補足情報' }));
|
|
383
|
+
if (includeStatus && context.state !== 'ok') {
|
|
338
384
|
if (context.state === 'loading') {
|
|
339
385
|
section.append(workspaceNotice(doc, { label: '読み込み中', text: context.message ?? '補足情報を読み込んでいます。', tone: 'info', role: 'status' }));
|
|
340
386
|
if (sections.length === 0) section.append(contextSectionSkeleton(doc));
|
|
@@ -342,42 +388,11 @@ export function renderProjectContext(doc, context) {
|
|
|
342
388
|
section.append(workspaceNotice(doc, { label: context.state === 'failed' ? '読み取り失敗' : '未接続', text: context.message ?? '補足情報を確認できません。', tone: context.state === 'failed' ? 'danger' : 'warning' }));
|
|
343
389
|
}
|
|
344
390
|
}
|
|
345
|
-
if (context.state === 'ok' && sections.length === 0) {
|
|
391
|
+
if (includeStatus && context.state === 'ok' && sections.length === 0) {
|
|
346
392
|
section.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: '補足情報のセクションはありません。' }));
|
|
347
393
|
return section;
|
|
348
394
|
}
|
|
349
|
-
for (const item of sections)
|
|
350
|
-
const group = makeElement(doc, 'section', {
|
|
351
|
-
className: `bb-pkw-context-group${item.state === 'loading' ? ' bb-pkw-context-group-loading' : ''}`,
|
|
352
|
-
attrs: { 'aria-label': item.title, ...(item.state === 'loading' ? { 'aria-busy': 'true' } : {}) },
|
|
353
|
-
});
|
|
354
|
-
const heading = makeElement(doc, 'div', { className: 'bb-pkw-context-heading' });
|
|
355
|
-
heading.append(makeElement(doc, 'h4', { text: item.title }), badge(doc, item.state === 'ok' ? '利用可能' : item.state === 'loading' ? '読み込み中' : item.state === 'failed' ? '読み取り失敗' : '未接続', item.state === 'ok' ? 'success' : item.state === 'failed' ? 'danger' : 'muted'));
|
|
356
|
-
group.append(heading);
|
|
357
|
-
if (item.message && !['failed', 'unavailable'].includes(item.state)) group.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: item.message }));
|
|
358
|
-
if (item.asOf) group.append(makeElement(doc, 'small', { className: 'bb-pkw-muted', text: `読み取った時点: ${formatDateTime(item.asOf)}` }));
|
|
359
|
-
if (item.state === 'loading') {
|
|
360
|
-
group.append(skeletonBars(doc));
|
|
361
|
-
} else if (item.state === 'failed') {
|
|
362
|
-
group.append(workspaceNotice(doc, { label: '読み取り失敗', text: item.message ?? 'このセクションを読み取れませんでした。', tone: 'danger', role: 'alert' }));
|
|
363
|
-
} else if (item.state === 'unavailable') {
|
|
364
|
-
group.append(workspaceNotice(doc, { label: '未接続', text: item.message ?? 'このセクションは接続されていません。', tone: 'warning' }));
|
|
365
|
-
} else if (item.items.length > 0) {
|
|
366
|
-
const list = makeElement(doc, 'ul', { className: 'bb-pkw-context-list' });
|
|
367
|
-
for (const entry of item.items) {
|
|
368
|
-
const row = makeElement(doc, 'li');
|
|
369
|
-
row.append(makeElement(doc, 'strong', { text: entry.title }));
|
|
370
|
-
if (entry.status) row.append(badge(doc, entry.status, 'muted'));
|
|
371
|
-
const facts = [entry.summary, entry.owner ? `担当: ${entry.owner}` : null, entry.dueAt ? `期限: ${entry.dueAt}` : null, entry.updatedAt ? `更新: ${entry.updatedAt}` : null, entry.source ? `出典: ${entry.source}` : null].filter(Boolean);
|
|
372
|
-
if (facts.length > 0) row.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: facts.join(' · ') }));
|
|
373
|
-
list.append(row);
|
|
374
|
-
}
|
|
375
|
-
group.append(list);
|
|
376
|
-
} else if (item.state === 'ok') {
|
|
377
|
-
group.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: 'このセクションに表示できる記録はありません。' }));
|
|
378
|
-
}
|
|
379
|
-
section.append(group);
|
|
380
|
-
}
|
|
395
|
+
for (const item of sections) section.append(renderProjectContextGroup(doc, item));
|
|
381
396
|
return section;
|
|
382
397
|
}
|
|
383
398
|
|
|
@@ -387,6 +402,12 @@ export function renderProjectKnowledgeSkeleton(doc, { context = null } = {}) {
|
|
|
387
402
|
className: 'bb-pkw-panel bb-pkw-overview bb-pkw-overview-loading',
|
|
388
403
|
attrs: { 'aria-label': '概要' },
|
|
389
404
|
});
|
|
405
|
+
const contextSections = Array.isArray(context?.sections) ? context.sections : [];
|
|
406
|
+
const readySections = contextSections.filter((item) => item?.state === 'ok' && Array.isArray(item.items) && item.items.length > 0);
|
|
407
|
+
const deferredSections = contextSections.filter((item) => !readySections.includes(item));
|
|
408
|
+
if (readySections.length > 0) {
|
|
409
|
+
panel.append(renderProjectContext(doc, context, { sections: readySections, includeStatus: false }));
|
|
410
|
+
}
|
|
390
411
|
const columns = makeElement(doc, 'div', { className: 'bb-pkw-columns' });
|
|
391
412
|
for (const title of ['目的と現在地', '関係者と役割']) {
|
|
392
413
|
const item = makeElement(doc, 'section', { className: 'bb-pkw-section bb-pkw-skeleton-section', attrs: { 'aria-label': title, 'aria-busy': 'true' } });
|
|
@@ -399,7 +420,9 @@ export function renderProjectKnowledgeSkeleton(doc, { context = null } = {}) {
|
|
|
399
420
|
item.append(makeElement(doc, 'h3', { text: title }), skeletonBars(doc, 2));
|
|
400
421
|
panel.append(item);
|
|
401
422
|
}
|
|
402
|
-
const contextBlock =
|
|
423
|
+
const contextBlock = readySections.length > 0
|
|
424
|
+
? (deferredSections.length > 0 ? renderProjectContext(doc, context, { sections: deferredSections, includeHeading: false }) : null)
|
|
425
|
+
: renderProjectContext(doc, context ?? { state: 'loading', sections: [] });
|
|
403
426
|
if (contextBlock) panel.append(contextBlock);
|
|
404
427
|
return panel;
|
|
405
428
|
}
|
|
@@ -439,6 +462,16 @@ function renderOverview(doc, detail, context, edges, entities, onSelect, onConte
|
|
|
439
462
|
columns.append(people);
|
|
440
463
|
panel.append(columns);
|
|
441
464
|
|
|
465
|
+
const contextBlock = renderProjectContext(doc, context);
|
|
466
|
+
if (contextBlock) {
|
|
467
|
+
panel.append(contextBlock);
|
|
468
|
+
if (typeof onContext === 'function') onContext(contextBlock, null);
|
|
469
|
+
} else {
|
|
470
|
+
const contextAnchor = projectContextInsertionAnchor(doc);
|
|
471
|
+
panel.append(contextAnchor);
|
|
472
|
+
if (typeof onContext === 'function') onContext(null, contextAnchor);
|
|
473
|
+
}
|
|
474
|
+
|
|
442
475
|
const decisions = makeElement(doc, 'section', { className: 'bb-pkw-section', attrs: { 'aria-label': '記録された判断' } });
|
|
443
476
|
decisions.append(makeElement(doc, 'h3', { text: '記録された判断' }));
|
|
444
477
|
const decisionEdges = edges.filter((edge) => entities.get(edge.source)?.type === 'decision' || entities.get(edge.target)?.type === 'decision');
|
|
@@ -493,11 +526,6 @@ function renderOverview(doc, detail, context, edges, entities, onSelect, onConte
|
|
|
493
526
|
missing.append(list);
|
|
494
527
|
panel.append(missing);
|
|
495
528
|
}
|
|
496
|
-
const contextBlock = renderProjectContext(doc, context);
|
|
497
|
-
if (contextBlock) {
|
|
498
|
-
panel.append(contextBlock);
|
|
499
|
-
if (typeof onContext === 'function') onContext(contextBlock);
|
|
500
|
-
}
|
|
501
529
|
return panel;
|
|
502
530
|
}
|
|
503
531
|
|
|
@@ -543,8 +571,16 @@ export function createProjectKnowledgeWorkspace({
|
|
|
543
571
|
let selectedPanel = null;
|
|
544
572
|
let overviewPanel = null;
|
|
545
573
|
let contextBlock = null;
|
|
546
|
-
|
|
547
|
-
|
|
574
|
+
let contextAnchor = null;
|
|
575
|
+
|
|
576
|
+
const root = makeElement(doc, 'section', {
|
|
577
|
+
className: 'bb-pkw',
|
|
578
|
+
attrs: {
|
|
579
|
+
'data-contract-version': PROJECT_WORKSPACE_CONTRACT_VERSION,
|
|
580
|
+
'data-project-id': cleanId(project.id),
|
|
581
|
+
'aria-label': 'プロジェクトの知識',
|
|
582
|
+
},
|
|
583
|
+
});
|
|
548
584
|
const body = makeElement(doc, 'div', { className: 'bb-pkw-body' });
|
|
549
585
|
const tabs = makeElement(doc, 'div', { className: 'bb-pkw-tabs', attrs: { role: 'tablist', 'aria-label': 'プロジェクト情報の表示' } });
|
|
550
586
|
const panels = makeElement(doc, 'div', { className: 'bb-pkw-panels' });
|
|
@@ -779,8 +815,15 @@ export function createProjectKnowledgeWorkspace({
|
|
|
779
815
|
if (destroyed) return;
|
|
780
816
|
const nextBlock = renderProjectContext(doc, currentContext);
|
|
781
817
|
if (!nextBlock) {
|
|
782
|
-
if (contextBlock?.parentNode
|
|
783
|
-
|
|
818
|
+
if (contextBlock?.parentNode && typeof contextBlock.parentNode.replaceChild === 'function') {
|
|
819
|
+
const nextAnchor = projectContextInsertionAnchor(doc);
|
|
820
|
+
contextBlock.parentNode.replaceChild(nextAnchor, contextBlock);
|
|
821
|
+
contextBlock = null;
|
|
822
|
+
contextAnchor = nextAnchor;
|
|
823
|
+
} else if (contextBlock?.parentNode?.removeChild) {
|
|
824
|
+
contextBlock.parentNode.removeChild(contextBlock);
|
|
825
|
+
contextBlock = null;
|
|
826
|
+
}
|
|
784
827
|
return;
|
|
785
828
|
}
|
|
786
829
|
if (contextBlock?.parentNode && typeof contextBlock.parentNode.replaceChild === 'function') {
|
|
@@ -788,14 +831,16 @@ export function createProjectKnowledgeWorkspace({
|
|
|
788
831
|
contextBlock = nextBlock;
|
|
789
832
|
return;
|
|
790
833
|
}
|
|
791
|
-
if (
|
|
792
|
-
|
|
834
|
+
if (contextAnchor?.parentNode && typeof contextAnchor.parentNode.replaceChild === 'function') {
|
|
835
|
+
contextAnchor.parentNode.replaceChild(nextBlock, contextAnchor);
|
|
793
836
|
contextBlock = nextBlock;
|
|
837
|
+
contextAnchor = null;
|
|
794
838
|
return;
|
|
795
839
|
}
|
|
796
|
-
if (
|
|
797
|
-
|
|
840
|
+
if (contextBlock?.replaceWith) {
|
|
841
|
+
contextBlock.replaceWith(nextBlock);
|
|
798
842
|
contextBlock = nextBlock;
|
|
843
|
+
return;
|
|
799
844
|
}
|
|
800
845
|
}
|
|
801
846
|
|
|
@@ -806,7 +851,11 @@ export function createProjectKnowledgeWorkspace({
|
|
|
806
851
|
tabs.replaceChildren(tabButton('概要', 'overview'), tabButton('情報を探す', 'graph'));
|
|
807
852
|
panels.replaceChildren();
|
|
808
853
|
contextBlock = null;
|
|
809
|
-
|
|
854
|
+
contextAnchor = null;
|
|
855
|
+
const overview = renderOverview(doc, payload, currentContext, overviewEdges, overviewEntities, selectAndExplore, (node, anchor) => {
|
|
856
|
+
contextBlock = node;
|
|
857
|
+
contextAnchor = anchor;
|
|
858
|
+
});
|
|
810
859
|
overview.id = 'bb-pkw-panel-overview';
|
|
811
860
|
overview.hidden = activeTab !== 'overview';
|
|
812
861
|
overviewPanel = overview;
|