@unson/brainbase-mcp 0.18.1 → 0.19.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.
|
@@ -8,6 +8,7 @@ import { type FoundationHttpCsrfVerifier, type FoundationHttpRouter } from './fo
|
|
|
8
8
|
*/
|
|
9
9
|
export declare const FOUNDATION_GRAPH_READY_SQL = "SELECT\n EXISTS (SELECT 1 FROM pg_class WHERE oid = to_regclass('public.graph_foundation_revisions') AND relrowsecurity)\n AND EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = to_regclass('public.graph_entities')\n AND tgname = 'graph_entities_foundation_history_capture' AND tgenabled = 'O' AND tgtype = 21\n AND tgfoid = to_regprocedure('public.capture_graph_foundation_revision()'))\n AND EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = to_regclass('public.graph_foundation_revisions')\n AND tgname = 'graph_foundation_revisions_guard' AND tgenabled = 'O' AND tgtype = 31\n AND tgfoid = to_regprocedure('public.guard_graph_foundation_revision_mutation()'))\n AND EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = to_regclass('public.graph_foundation_revisions')\n AND tgname = 'graph_foundation_revisions_truncate_guard' AND tgenabled = 'O' AND tgtype = 34\n AND tgfoid = to_regprocedure('public.guard_graph_foundation_revision_mutation()')) AS ready";
|
|
10
10
|
export declare const FOUNDATION_GRAPH_CONTRACT_VERSION: "foundation-graph-http.v1";
|
|
11
|
+
export declare const FOUNDATION_OVERVIEW_CONTRACT_VERSION: "foundation-overview.v1";
|
|
11
12
|
type MaybePromise<T> = T | Promise<T>;
|
|
12
13
|
export interface FoundationGraphTrustedIdentity {
|
|
13
14
|
/** Canonical principal supplied by the host authentication boundary. */
|
|
@@ -22,6 +22,8 @@ export const FOUNDATION_GRAPH_READY_SQL = `SELECT
|
|
|
22
22
|
AND tgname = 'graph_foundation_revisions_truncate_guard' AND tgenabled = 'O' AND tgtype = 34
|
|
23
23
|
AND tgfoid = to_regprocedure('public.guard_graph_foundation_revision_mutation()')) AS ready`;
|
|
24
24
|
export const FOUNDATION_GRAPH_CONTRACT_VERSION = 'foundation-graph-http.v1';
|
|
25
|
+
export const FOUNDATION_OVERVIEW_CONTRACT_VERSION = 'foundation-overview.v1';
|
|
26
|
+
const FOUNDATION_CATALOG_PATH = '/api/foundation/catalog';
|
|
25
27
|
const DEFAULT_BODY_LIMIT_BYTES = 65_536;
|
|
26
28
|
const DIRECT_FOUNDATION_REFERENCE_KINDS = new Set(['objective', 'variable', 'model', 'constraint', 'philosophy']);
|
|
27
29
|
const SNAPSHOT_REFERENCE_KINDS = new Set([
|
|
@@ -35,6 +37,15 @@ function jsonResponse(status, value) {
|
|
|
35
37
|
headers: { 'content-type': 'application/json' }
|
|
36
38
|
});
|
|
37
39
|
}
|
|
40
|
+
function methodNotAllowed(allow) {
|
|
41
|
+
return new Response(JSON.stringify({ error: { code: 'method_not_allowed' } }), {
|
|
42
|
+
status: 405,
|
|
43
|
+
headers: {
|
|
44
|
+
'content-type': 'application/json',
|
|
45
|
+
allow: allow.join(', ')
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
38
49
|
function isRecord(value) {
|
|
39
50
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
40
51
|
}
|
|
@@ -138,6 +149,28 @@ async function readJsonForScopeCheck(request, bodyLimitBytes) {
|
|
|
138
149
|
return null;
|
|
139
150
|
}
|
|
140
151
|
}
|
|
152
|
+
async function catalogRequestHasBody(request, bodyLimitBytes) {
|
|
153
|
+
const contentLength = Number(request.headers.get('content-length'));
|
|
154
|
+
if (Number.isFinite(contentLength) && contentLength > 0)
|
|
155
|
+
return true;
|
|
156
|
+
if (Number.isFinite(contentLength) && contentLength > bodyLimitBytes)
|
|
157
|
+
return true;
|
|
158
|
+
if (!request.body)
|
|
159
|
+
return false;
|
|
160
|
+
try {
|
|
161
|
+
return (await request.clone().text()).length > 0;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function catalogQueryIsStrict(url) {
|
|
168
|
+
for (const key of url.searchParams.keys()) {
|
|
169
|
+
if (key !== 'scope_id')
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
141
174
|
function identityIsValid(identity) {
|
|
142
175
|
if (!identity)
|
|
143
176
|
return false;
|
|
@@ -153,6 +186,34 @@ function selectedScopeIsAllowed(identity, scopeId) {
|
|
|
153
186
|
function readyResult(result) {
|
|
154
187
|
return result.rows[0]?.ready === true;
|
|
155
188
|
}
|
|
189
|
+
function createFoundationCatalogRoute(readers, scopeId, context) {
|
|
190
|
+
return {
|
|
191
|
+
name: 'foundation-catalog',
|
|
192
|
+
methods: ['GET'],
|
|
193
|
+
paths: [`GET ${FOUNDATION_CATALOG_PATH}`],
|
|
194
|
+
matches(request) {
|
|
195
|
+
return new URL(request.url).pathname === FOUNDATION_CATALOG_PATH;
|
|
196
|
+
},
|
|
197
|
+
async handle(request) {
|
|
198
|
+
if ((request.method || 'GET').toUpperCase() !== 'GET')
|
|
199
|
+
return methodNotAllowed(['GET']);
|
|
200
|
+
// Resolve all collections before constructing the response. A failed
|
|
201
|
+
// history/ACL read must never be represented as an empty partial catalog.
|
|
202
|
+
const objectives = await readers.store.list('objective', context);
|
|
203
|
+
const variables = await readers.store.list('variable', context);
|
|
204
|
+
const models = await readers.store.list('model', context);
|
|
205
|
+
const philosophies = await readers.listPhilosophies(context);
|
|
206
|
+
return jsonResponse(200, {
|
|
207
|
+
contractVersion: FOUNDATION_OVERVIEW_CONTRACT_VERSION,
|
|
208
|
+
scopeId,
|
|
209
|
+
objectives,
|
|
210
|
+
variables,
|
|
211
|
+
models,
|
|
212
|
+
philosophies
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
}
|
|
156
217
|
/**
|
|
157
218
|
* Create the reusable Graph-backed public Foundation handler.
|
|
158
219
|
*
|
|
@@ -194,6 +255,16 @@ export function createFoundationGraphHttpHandler(options) {
|
|
|
194
255
|
if (!selectedScopeIsAllowed(identity, scopeId)) {
|
|
195
256
|
return jsonResponse(403, { error: { code: 'scope_not_allowed' } });
|
|
196
257
|
}
|
|
258
|
+
const isCatalog = url.pathname === FOUNDATION_CATALOG_PATH;
|
|
259
|
+
if (isCatalog && (request.method || 'GET').toUpperCase() !== 'GET') {
|
|
260
|
+
return methodNotAllowed(['GET']);
|
|
261
|
+
}
|
|
262
|
+
if (isCatalog && !catalogQueryIsStrict(url)) {
|
|
263
|
+
return jsonResponse(400, { error: { code: 'unknown_query_parameter' } });
|
|
264
|
+
}
|
|
265
|
+
if (isCatalog && await catalogRequestHasBody(request, bodyLimitBytes)) {
|
|
266
|
+
return jsonResponse(400, { error: { code: 'body_not_allowed' } });
|
|
267
|
+
}
|
|
197
268
|
const body = await readJsonForScopeCheck(request, bodyLimitBytes);
|
|
198
269
|
if (hasFoundationScopeOverride(request, body, scopeId, identity.organizationId)) {
|
|
199
270
|
return jsonResponse(403, { error: { code: 'scope_not_allowed' } });
|
|
@@ -226,6 +297,7 @@ export function createFoundationGraphHttpHandler(options) {
|
|
|
226
297
|
});
|
|
227
298
|
const provider = createFoundationPublicProvider(readers);
|
|
228
299
|
const route = createFoundationPublicRoute(provider);
|
|
300
|
+
const catalogRoute = createFoundationCatalogRoute(readers, scopeId, context);
|
|
229
301
|
const objectiveRoute = createObjectiveFoundationRoute({
|
|
230
302
|
objectives: {
|
|
231
303
|
async createObjective() {
|
|
@@ -248,7 +320,7 @@ export function createFoundationGraphHttpHandler(options) {
|
|
|
248
320
|
const handler = createFoundationHttpRouter({
|
|
249
321
|
resolveContext: () => context,
|
|
250
322
|
csrf: options.csrf,
|
|
251
|
-
routes: [route, objectiveRoute],
|
|
323
|
+
routes: [catalogRoute, route, objectiveRoute],
|
|
252
324
|
bodyLimitBytes
|
|
253
325
|
});
|
|
254
326
|
return handler.handle(request);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type FoundationRevisionStore, type FoundationStoreContext } from './foundation-store.js';
|
|
2
|
-
import type { PhilosophyRevisionReader } from './philosophy-revision-reader.js';
|
|
2
|
+
import type { PhilosophyRevisionReader, PhilosophyRevisionRecord } from './philosophy-revision-reader.js';
|
|
3
|
+
import type { FoundationCatalogRecord } from './types.js';
|
|
3
4
|
/** Read contract for history that is joined to the current Graph row. */
|
|
4
5
|
export declare const GRAPH_FOUNDATION_READ_CONTRACT_VERSION: "graph-foundation-history-read.v1";
|
|
5
6
|
/**
|
|
@@ -25,6 +26,14 @@ export declare const GRAPH_FOUNDATION_LATEST_SQL = "\nSELECT\n history.entity_i
|
|
|
25
26
|
* scope identifier selected by the organization BFF.
|
|
26
27
|
*/
|
|
27
28
|
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";
|
|
29
|
+
/**
|
|
30
|
+
* List the newest immutable revision for each philosophy attached to the
|
|
31
|
+
* selected project. The current Graph row is still the authorization
|
|
32
|
+
* boundary; the transaction's RLS policies hide rows the caller cannot see.
|
|
33
|
+
* Unlike the Foundation list, this query requires a canonical project join so
|
|
34
|
+
* a missing project membership cannot be interpreted as a selected-scope hit.
|
|
35
|
+
*/
|
|
36
|
+
export declare const GRAPH_PHILOSOPHY_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\nJOIN 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.entity_type = 'philosophy'\n AND projects.code = $1\nORDER BY current_entity.id\n";
|
|
28
37
|
/** Flat row shape returned by GRAPH_FOUNDATION_READ_SQL. */
|
|
29
38
|
export interface GraphFoundationHistoryRow {
|
|
30
39
|
readonly entity_id?: unknown;
|
|
@@ -61,9 +70,23 @@ export interface GraphFoundationReaderOptions {
|
|
|
61
70
|
/** Canonical project code selected by the organization boundary. */
|
|
62
71
|
readonly selectedProjectCode?: string;
|
|
63
72
|
}
|
|
73
|
+
export interface FoundationObjectiveDetails {
|
|
74
|
+
readonly title?: unknown;
|
|
75
|
+
readonly criteria_text?: unknown;
|
|
76
|
+
readonly beneficiary_description?: unknown;
|
|
77
|
+
readonly evaluation_period_note?: unknown;
|
|
78
|
+
readonly evaluator_note?: unknown;
|
|
79
|
+
readonly current_state?: unknown;
|
|
80
|
+
}
|
|
81
|
+
/** Runtime shape used by the overview contract for Objective records. */
|
|
82
|
+
export interface FoundationOverviewObjectiveRecord extends FoundationCatalogRecord {
|
|
83
|
+
readonly details?: FoundationObjectiveDetails;
|
|
84
|
+
}
|
|
64
85
|
export interface GraphFoundationReaders {
|
|
65
86
|
readonly store: Pick<FoundationRevisionStore, 'read' | 'readLatest' | 'list'>;
|
|
66
87
|
readonly philosophyReader: PhilosophyRevisionReader;
|
|
88
|
+
/** List latest, current-visible philosophy revisions for the selected project. */
|
|
89
|
+
readonly listPhilosophies: (context: FoundationStoreContext) => Promise<PhilosophyRevisionRecord[]>;
|
|
67
90
|
}
|
|
68
91
|
/**
|
|
69
92
|
* Create both public read ports over one trusted, transaction-bound context.
|
|
@@ -142,6 +142,53 @@ WHERE ($1::text IS NULL OR current_entity.entity_type = $1)
|
|
|
142
142
|
AND current_entity.entity_type IN ('objective', 'variable', 'model', 'constraint')
|
|
143
143
|
ORDER BY current_entity.id, current_entity.entity_type
|
|
144
144
|
`;
|
|
145
|
+
/**
|
|
146
|
+
* List the newest immutable revision for each philosophy attached to the
|
|
147
|
+
* selected project. The current Graph row is still the authorization
|
|
148
|
+
* boundary; the transaction's RLS policies hide rows the caller cannot see.
|
|
149
|
+
* Unlike the Foundation list, this query requires a canonical project join so
|
|
150
|
+
* a missing project membership cannot be interpreted as a selected-scope hit.
|
|
151
|
+
*/
|
|
152
|
+
export const GRAPH_PHILOSOPHY_LIST_SQL = `
|
|
153
|
+
SELECT
|
|
154
|
+
history.entity_id,
|
|
155
|
+
history.entity_type,
|
|
156
|
+
history.revision,
|
|
157
|
+
history.payload,
|
|
158
|
+
history.project_id,
|
|
159
|
+
history.role_min,
|
|
160
|
+
history.sensitivity,
|
|
161
|
+
history.lifecycle_status,
|
|
162
|
+
history.storage_digest,
|
|
163
|
+
history.captured_at,
|
|
164
|
+
(
|
|
165
|
+
history.storage_digest = 'sha256:' ||
|
|
166
|
+
encode(sha256(convert_to(history.payload::text, 'UTF8')), 'hex')
|
|
167
|
+
) AS storage_digest_valid,
|
|
168
|
+
current_entity.id AS current_entity_id,
|
|
169
|
+
current_entity.entity_type AS current_entity_type,
|
|
170
|
+
current_entity.payload AS current_payload,
|
|
171
|
+
current_entity.project_id AS current_project_id,
|
|
172
|
+
current_entity.role_min AS current_role_min,
|
|
173
|
+
current_entity.sensitivity AS current_sensitivity,
|
|
174
|
+
current_entity.lifecycle_status AS current_lifecycle_status,
|
|
175
|
+
projects.code AS current_project_code,
|
|
176
|
+
true AS current_visible
|
|
177
|
+
FROM public.graph_entities AS current_entity
|
|
178
|
+
JOIN public.projects AS projects
|
|
179
|
+
ON projects.id = current_entity.project_id
|
|
180
|
+
LEFT JOIN LATERAL (
|
|
181
|
+
SELECT history.*
|
|
182
|
+
FROM public.graph_foundation_revisions AS history
|
|
183
|
+
WHERE history.entity_id = current_entity.id
|
|
184
|
+
AND history.entity_type = current_entity.entity_type
|
|
185
|
+
ORDER BY history.revision::numeric DESC
|
|
186
|
+
LIMIT 1
|
|
187
|
+
) AS history ON true
|
|
188
|
+
WHERE current_entity.entity_type = 'philosophy'
|
|
189
|
+
AND projects.code = $1
|
|
190
|
+
ORDER BY current_entity.id
|
|
191
|
+
`;
|
|
145
192
|
const FOUNDATION_TYPES = ['objective', 'variable', 'model', 'constraint'];
|
|
146
193
|
const REVISION_PATTERN = /^[1-9]\d*$/u;
|
|
147
194
|
const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/u;
|
|
@@ -214,7 +261,42 @@ export function createGraphFoundationReaders(options) {
|
|
|
214
261
|
return readPhilosophyRevision(input, context, query);
|
|
215
262
|
}
|
|
216
263
|
};
|
|
217
|
-
|
|
264
|
+
async function listPhilosophies(suppliedContext) {
|
|
265
|
+
assertTrustedPrincipal(context, suppliedContext);
|
|
266
|
+
if (selectedProjectCode === undefined) {
|
|
267
|
+
throw scopeViolation('A selected project is required to list philosophy revisions');
|
|
268
|
+
}
|
|
269
|
+
const rows = await executePhilosophyList(query, selectedProjectCode);
|
|
270
|
+
const seen = new Set();
|
|
271
|
+
return rows.map((row) => {
|
|
272
|
+
const id = requireString(row.entity_id, 'Philosophy history entity_id');
|
|
273
|
+
const revision = requireRevision(row.revision, 'Philosophy history revision');
|
|
274
|
+
if (row.current_project_code !== selectedProjectCode) {
|
|
275
|
+
throw scopeViolation('The current philosophy project is outside the selected project scope');
|
|
276
|
+
}
|
|
277
|
+
const key = `${id}:${revision}`;
|
|
278
|
+
if (seen.has(key))
|
|
279
|
+
throw corrupt('Philosophy list returned duplicate entity revisions');
|
|
280
|
+
seen.add(key);
|
|
281
|
+
const validated = validatePhilosophyHistoryAccess(row, id, revision, context);
|
|
282
|
+
return createPhilosophyRevisionRecord(validated, id, revision, context.principal);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
return { store, philosophyReader, listPhilosophies };
|
|
286
|
+
}
|
|
287
|
+
function createPhilosophyRevisionRecord(validated, id, revision, principal) {
|
|
288
|
+
return {
|
|
289
|
+
kind: 'philosophy',
|
|
290
|
+
id,
|
|
291
|
+
revision,
|
|
292
|
+
digest: validated.digest,
|
|
293
|
+
payload: validated.history.payload,
|
|
294
|
+
applicability: validated.history.applicability,
|
|
295
|
+
// This is a read-time projection of the current Graph permission. It
|
|
296
|
+
// does not mutate the canonical philosophy payload or Graph ACL.
|
|
297
|
+
currentAcl: privateCurrentAcl(principal),
|
|
298
|
+
currentScope: cloneJudgmentScope(validated.history.applicability.scope)
|
|
299
|
+
};
|
|
218
300
|
}
|
|
219
301
|
async function readCanonicalPhilosophyRevision(input, trustedContext, query) {
|
|
220
302
|
try {
|
|
@@ -399,6 +481,21 @@ async function executeList(query, type, selectedProjectCode) {
|
|
|
399
481
|
}
|
|
400
482
|
throw corrupt('Graph foundation list query returned an invalid result');
|
|
401
483
|
}
|
|
484
|
+
async function executePhilosophyList(query, selectedProjectCode) {
|
|
485
|
+
let result;
|
|
486
|
+
try {
|
|
487
|
+
result = await query(GRAPH_PHILOSOPHY_LIST_SQL, [selectedProjectCode]);
|
|
488
|
+
}
|
|
489
|
+
catch {
|
|
490
|
+
throw corrupt('Graph philosophy list query failed');
|
|
491
|
+
}
|
|
492
|
+
if (Array.isArray(result))
|
|
493
|
+
return result;
|
|
494
|
+
if (!Array.isArray(result) && 'rows' in result && Array.isArray(result.rows)) {
|
|
495
|
+
return result.rows;
|
|
496
|
+
}
|
|
497
|
+
throw corrupt('Graph philosophy list query returned an invalid result');
|
|
498
|
+
}
|
|
402
499
|
function validateFoundationRecord(row, reference, context, selectedProjectCode, requireCurrentRevision = false) {
|
|
403
500
|
const history = validateHistoryRow(row, reference.id, reference.type, reference.revision);
|
|
404
501
|
const current = validateCurrentRow(row, reference.id, reference.type);
|
|
@@ -418,10 +515,39 @@ function validateFoundationRecord(row, reference, context, selectedProjectCode,
|
|
|
418
515
|
if (history.projectId !== current.projectId) {
|
|
419
516
|
throw scopeViolation('Historical foundation project differs from current Graph project');
|
|
420
517
|
}
|
|
421
|
-
|
|
518
|
+
const record = {
|
|
422
519
|
definition: cloneFoundationDefinition(history.definition),
|
|
423
520
|
digest: digestFoundationDefinition(history.definition)
|
|
424
521
|
};
|
|
522
|
+
if (reference.type === 'objective') {
|
|
523
|
+
const details = projectObjectiveDetails(history.payload);
|
|
524
|
+
if (details !== undefined)
|
|
525
|
+
return { ...record, details };
|
|
526
|
+
}
|
|
527
|
+
return record;
|
|
528
|
+
}
|
|
529
|
+
const OBJECTIVE_DETAIL_KEYS = [
|
|
530
|
+
'title',
|
|
531
|
+
'criteria_text',
|
|
532
|
+
'beneficiary_description',
|
|
533
|
+
'evaluation_period_note',
|
|
534
|
+
'evaluator_note',
|
|
535
|
+
'current_state'
|
|
536
|
+
];
|
|
537
|
+
function projectObjectiveDetails(payload) {
|
|
538
|
+
const nestedDetails = isPlainRecord(payload.details) ? payload.details : undefined;
|
|
539
|
+
const foundation = isPlainRecord(payload.foundation) ? payload.foundation : undefined;
|
|
540
|
+
// Graphify payloads historically carried these descriptive fields at the
|
|
541
|
+
// payload root. Keep the two known nested forms compatible while exposing
|
|
542
|
+
// only the six fields in the overview contract.
|
|
543
|
+
const candidates = [payload, nestedDetails, foundation].filter((candidate) => candidate !== undefined);
|
|
544
|
+
const projected = {};
|
|
545
|
+
for (const key of OBJECTIVE_DETAIL_KEYS) {
|
|
546
|
+
const source = candidates.find((candidate) => Object.prototype.hasOwnProperty.call(candidate, key));
|
|
547
|
+
if (source !== undefined && source[key] !== undefined)
|
|
548
|
+
projected[key] = source[key];
|
|
549
|
+
}
|
|
550
|
+
return Object.keys(projected).length === 0 ? undefined : projected;
|
|
425
551
|
}
|
|
426
552
|
function rowReference(row) {
|
|
427
553
|
const id = requireString(row.entity_id, 'Foundation history entity_id');
|
|
@@ -441,7 +567,7 @@ function validateHistoryRow(row, id, type, revision) {
|
|
|
441
567
|
throw corrupt('Foundation history definition identity does not match the requested revision');
|
|
442
568
|
}
|
|
443
569
|
const projectId = requireString(row.project_id, 'Foundation history project_id');
|
|
444
|
-
return { definition, projectId };
|
|
570
|
+
return { definition, payload, projectId };
|
|
445
571
|
}
|
|
446
572
|
function validatePhilosophyHistoryRow(row, reference) {
|
|
447
573
|
assertRowIdentity(row, reference.id, 'philosophy', reference.revision);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unson/brainbase-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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",
|
|
@@ -228,6 +228,8 @@
|
|
|
228
228
|
"./ui/local-web-shell.css": "./ui/local-web-shell.css",
|
|
229
229
|
"./ui/workspace-kit": "./ui/workspace-kit.js",
|
|
230
230
|
"./ui/workspace-kit.css": "./ui/workspace-kit.css",
|
|
231
|
+
"./ui/foundation-overview": "./ui/foundation-overview.js",
|
|
232
|
+
"./ui/foundation-overview.css": "./ui/foundation-overview.css",
|
|
231
233
|
"./ui/graph-view-shared": "./ui/graph-view-shared.js",
|
|
232
234
|
"./ui/graph-view-shared.css": "./ui/graph-view-shared.css",
|
|
233
235
|
"./ui/project-icon": "./ui/project-icon.js",
|
|
@@ -361,5 +363,5 @@
|
|
|
361
363
|
"engines": {
|
|
362
364
|
"node": ">=20"
|
|
363
365
|
},
|
|
364
|
-
"gitHead": "
|
|
366
|
+
"gitHead": "7cd7be975882fd1cbc797b7992b27a221f9fc02f"
|
|
365
367
|
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Read-only Foundation overview. Load after brainbase-tokens.css and
|
|
3
|
+
* workspace-kit.css. The screen uses the shared workspace parts for page
|
|
4
|
+
* framing and adds only the cards, references, and pagination unique to this
|
|
5
|
+
* catalog projection.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
.bb-fov {
|
|
9
|
+
min-width: 0;
|
|
10
|
+
color: var(--bb-color-ink);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.bb-fov-section {
|
|
14
|
+
min-width: 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.bb-fov-objective-ledger .bb-ws-ledger-row {
|
|
18
|
+
grid-template-columns: minmax(0, 1.4fr) minmax(0, 1.4fr) minmax(100px, .7fr);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.bb-fov-objective-card,
|
|
22
|
+
.bb-fov-philosophy-card,
|
|
23
|
+
.bb-fov-model-card {
|
|
24
|
+
min-width: 0;
|
|
25
|
+
border-top: 1px solid var(--bb-color-border);
|
|
26
|
+
padding: 24px 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.bb-fov-objective-card h3,
|
|
30
|
+
.bb-fov-philosophy-card h3,
|
|
31
|
+
.bb-fov-model-card h3 {
|
|
32
|
+
margin: 0;
|
|
33
|
+
color: var(--bb-color-ink);
|
|
34
|
+
font-size: 17px;
|
|
35
|
+
line-height: 1.4;
|
|
36
|
+
overflow-wrap: anywhere;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.bb-fov-objective-card .bb-ws-definition,
|
|
40
|
+
.bb-fov-model-card .bb-ws-definition,
|
|
41
|
+
.bb-fov-philosophy-card .bb-ws-definition {
|
|
42
|
+
margin-top: 16px;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.bb-fov-status {
|
|
46
|
+
margin: 7px 0 0;
|
|
47
|
+
color: var(--bb-color-muted);
|
|
48
|
+
font-size: 11px;
|
|
49
|
+
line-height: 1.55;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.bb-fov-list {
|
|
53
|
+
display: grid;
|
|
54
|
+
gap: 5px;
|
|
55
|
+
margin: 0;
|
|
56
|
+
padding-left: 18px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.bb-fov-list li,
|
|
60
|
+
.bb-fov-ref-list li {
|
|
61
|
+
overflow-wrap: anywhere;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.bb-fov-criteria {
|
|
65
|
+
display: grid;
|
|
66
|
+
gap: 7px;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.bb-fov-criteria-text {
|
|
70
|
+
margin: 0;
|
|
71
|
+
color: var(--bb-color-muted);
|
|
72
|
+
line-height: 1.55;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.bb-fov-criteria-text-list {
|
|
76
|
+
color: var(--bb-color-muted);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.bb-fov-list .is-unrecorded,
|
|
80
|
+
.bb-fov-ref-list .is-unrecorded,
|
|
81
|
+
.bb-fov-recorded-notice.is-unrecorded,
|
|
82
|
+
.bb-fov-gap .is-unrecorded {
|
|
83
|
+
color: var(--bb-color-quiet);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.bb-fov-current-state {
|
|
87
|
+
display: grid;
|
|
88
|
+
gap: 6px;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.bb-fov-current-state strong {
|
|
92
|
+
color: var(--bb-color-ink);
|
|
93
|
+
font-weight: 680;
|
|
94
|
+
overflow-wrap: anywhere;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.bb-fov-current-state .bb-ws-definition {
|
|
98
|
+
margin: 4px 0 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.bb-fov-current-missing {
|
|
102
|
+
color: var(--bb-color-muted);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.bb-fov-recorded-notice {
|
|
106
|
+
margin: 0;
|
|
107
|
+
color: var(--bb-color-muted);
|
|
108
|
+
font-size: 11px;
|
|
109
|
+
line-height: 1.55;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.bb-fov-gap {
|
|
113
|
+
margin-top: 18px;
|
|
114
|
+
border-left: 3px solid var(--bb-color-warning);
|
|
115
|
+
padding: 10px 14px;
|
|
116
|
+
background: var(--bb-color-warning-soft);
|
|
117
|
+
color: var(--bb-color-muted);
|
|
118
|
+
font-size: 12px;
|
|
119
|
+
line-height: 1.55;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.bb-fov-gap strong {
|
|
123
|
+
color: var(--bb-color-warning);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.bb-fov-gap p {
|
|
127
|
+
margin: 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.bb-fov-gap .bb-fov-list {
|
|
131
|
+
margin-top: 5px;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.bb-fov-philosophy-grid,
|
|
135
|
+
.bb-fov-model-grid {
|
|
136
|
+
display: grid;
|
|
137
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
138
|
+
gap: 0 28px;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.bb-fov-philosophy-statement {
|
|
142
|
+
margin: 12px 0 0;
|
|
143
|
+
color: var(--bb-color-ink);
|
|
144
|
+
font-size: 15px;
|
|
145
|
+
line-height: 1.65;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.bb-fov-philosophy-card > p:not(.bb-fov-status):not(.bb-fov-philosophy-statement) {
|
|
149
|
+
margin: 8px 0 0;
|
|
150
|
+
color: var(--bb-color-muted);
|
|
151
|
+
line-height: 1.55;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.bb-fov-record-id {
|
|
155
|
+
display: block;
|
|
156
|
+
margin-top: 7px;
|
|
157
|
+
color: var(--bb-color-quiet);
|
|
158
|
+
font-family: var(--bb-font-mono);
|
|
159
|
+
font-size: 10px;
|
|
160
|
+
overflow-wrap: anywhere;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.bb-fov-ref-list {
|
|
164
|
+
display: grid;
|
|
165
|
+
gap: 5px;
|
|
166
|
+
margin: 0;
|
|
167
|
+
padding-left: 18px;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.bb-fov-model-search {
|
|
171
|
+
display: grid;
|
|
172
|
+
gap: 6px;
|
|
173
|
+
max-width: 520px;
|
|
174
|
+
margin: 0 0 18px;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.bb-fov-model-search label {
|
|
178
|
+
color: var(--bb-color-muted);
|
|
179
|
+
font-size: 12px;
|
|
180
|
+
font-weight: 680;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.bb-fov-search-input {
|
|
184
|
+
width: 100%;
|
|
185
|
+
min-height: 40px;
|
|
186
|
+
box-sizing: border-box;
|
|
187
|
+
border: 1px solid var(--bb-color-control-border);
|
|
188
|
+
border-radius: var(--bb-radius-sm);
|
|
189
|
+
padding: 9px 12px;
|
|
190
|
+
background: var(--bb-color-panel);
|
|
191
|
+
color: var(--bb-color-ink);
|
|
192
|
+
font: inherit;
|
|
193
|
+
font-size: 13px;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.bb-fov-search-input:focus {
|
|
197
|
+
outline: 2px solid var(--bb-color-focus);
|
|
198
|
+
outline-offset: 1px;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.bb-fov-pager {
|
|
202
|
+
display: flex;
|
|
203
|
+
align-items: center;
|
|
204
|
+
justify-content: flex-end;
|
|
205
|
+
gap: 10px;
|
|
206
|
+
border-top: 1px solid var(--bb-color-border);
|
|
207
|
+
padding-top: 14px;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.bb-fov-pager > span {
|
|
211
|
+
min-width: 48px;
|
|
212
|
+
color: var(--bb-color-muted);
|
|
213
|
+
font-size: 11px;
|
|
214
|
+
text-align: center;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.bb-fov [hidden] {
|
|
218
|
+
display: none;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
@media (max-width: 760px) {
|
|
222
|
+
.bb-fov-objective-ledger .bb-ws-ledger-row {
|
|
223
|
+
grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr) 84px;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.bb-fov-philosophy-grid,
|
|
227
|
+
.bb-fov-model-grid {
|
|
228
|
+
grid-template-columns: minmax(0, 1fr);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.bb-fov-objective-card .bb-ws-definition,
|
|
232
|
+
.bb-fov-model-card .bb-ws-definition,
|
|
233
|
+
.bb-fov-philosophy-card .bb-ws-definition {
|
|
234
|
+
grid-template-columns: minmax(90px, .55fr) minmax(0, 1fr);
|
|
235
|
+
}
|
|
236
|
+
}
|