@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
- return { store, philosophyReader };
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
- return {
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.18.1",
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": "9037a15719140c0e5c4e28f29975f98aa970c557"
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
+ }