@unson/brainbase-mcp 0.10.13 → 0.10.15

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.
@@ -0,0 +1,127 @@
1
+ import type { Tool } from '@modelcontextprotocol/sdk/types.js';
2
+ /**
3
+ * Judgment frame (brainbase-project ADR-014).
4
+ *
5
+ * A judgment-bearing turn selects the philosophy, objectives, and world models
6
+ * that bear on the decision from a compact catalog, and records how each
7
+ * selected reference was used for each option. The catalog is small enough to
8
+ * show in full; the record is checked structurally, never for semantic quality.
9
+ *
10
+ * Status is a label, not a gate: draft objectives and unverified models stay
11
+ * usable. Only superseded or inactive records are excluded from the catalog.
12
+ */
13
+ export declare const JUDGMENT_FRAME_CATALOG_VERSION: "judgment-frame-catalog.v1";
14
+ export declare const JUDGMENT_FRAME_RECORD_VERSION: "judgment-frame-record.v1";
15
+ export type JudgmentFrameKind = 'philosophy' | 'objective' | 'model';
16
+ export type JudgmentFrameStatus = 'active' | 'draft' | 'adopted_unverified' | 'verified';
17
+ export type JudgmentFrameRole = 'constraint' | 'criterion' | 'prediction';
18
+ export type JudgmentFrameConstraintVerdict = 'satisfied' | 'violated' | 'tension';
19
+ /** Each kind is used in exactly one way inside an option evaluation. */
20
+ export declare const JUDGMENT_FRAME_ROLE_BY_KIND: Readonly<Record<JudgmentFrameKind, JudgmentFrameRole>>;
21
+ export interface JudgmentFrameSourceRecord {
22
+ readonly id: string;
23
+ readonly entity_type: string;
24
+ readonly version?: number | string;
25
+ readonly lifecycle_status?: string;
26
+ readonly payload?: Record<string, unknown>;
27
+ }
28
+ export interface JudgmentFrameCatalogItem {
29
+ readonly id: string;
30
+ readonly kind: JudgmentFrameKind;
31
+ readonly revision: string;
32
+ readonly status: JudgmentFrameStatus;
33
+ readonly text: string;
34
+ readonly applies_when?: string;
35
+ }
36
+ export interface JudgmentFrameCatalog {
37
+ readonly catalog_version: typeof JUDGMENT_FRAME_CATALOG_VERSION;
38
+ readonly digest: `sha256:${string}`;
39
+ readonly items: readonly JudgmentFrameCatalogItem[];
40
+ }
41
+ export interface JudgmentFrameExclusion {
42
+ readonly id: string;
43
+ readonly reason: 'inactive' | 'superseded' | 'restates_source_philosophy' | 'missing_text' | 'unsupported_type';
44
+ }
45
+ export interface JudgmentFrameCatalogBuild {
46
+ readonly catalog: JudgmentFrameCatalog;
47
+ readonly excluded: readonly JudgmentFrameExclusion[];
48
+ }
49
+ export interface JudgmentFrameSelection {
50
+ readonly ref: string;
51
+ readonly kind: JudgmentFrameKind;
52
+ readonly why: string;
53
+ }
54
+ export interface JudgmentFrameUse {
55
+ readonly ref: string;
56
+ readonly role: JudgmentFrameRole;
57
+ readonly note: string;
58
+ /** Required for constraint uses: how the option stands against the philosophy. */
59
+ readonly verdict?: JudgmentFrameConstraintVerdict;
60
+ }
61
+ export interface JudgmentFrameOption {
62
+ readonly label: string;
63
+ readonly uses: readonly JudgmentFrameUse[];
64
+ }
65
+ export interface JudgmentFrameRecord {
66
+ readonly record_version: typeof JUDGMENT_FRAME_RECORD_VERSION;
67
+ readonly catalog_digest: string;
68
+ readonly selections: readonly JudgmentFrameSelection[];
69
+ /** Why no reference of a kind applies. Required for each kind without a selection. */
70
+ readonly none?: Partial<Record<JudgmentFrameKind, string>>;
71
+ readonly options: readonly JudgmentFrameOption[];
72
+ readonly chosen_option: string;
73
+ }
74
+ export type JudgmentFrameIssueCode = 'frame_record_invalid' | 'frame_catalog_mismatch' | 'frame_ref_unknown' | 'frame_kind_mismatch' | 'frame_selection_duplicate' | 'frame_kind_unaddressed' | 'frame_kind_contradiction' | 'frame_use_unselected' | 'frame_role_mismatch' | 'frame_verdict_missing' | 'frame_selection_unused' | 'frame_chosen_unknown';
75
+ export interface JudgmentFrameIssue {
76
+ readonly code: JudgmentFrameIssueCode;
77
+ readonly path: string;
78
+ readonly message: string;
79
+ }
80
+ /** Exceptions that ADR-014 F5 returns to a human. The host decides when they block. */
81
+ export type JudgmentFrameEscalationCode = 'objective_unanchored' | 'chosen_violates_philosophy' | 'chosen_philosophy_tension';
82
+ export interface JudgmentFrameEscalation {
83
+ readonly code: JudgmentFrameEscalationCode;
84
+ readonly refs: readonly string[];
85
+ }
86
+ export interface JudgmentFrameValidation {
87
+ readonly valid: boolean;
88
+ readonly issues: readonly JudgmentFrameIssue[];
89
+ readonly escalations: readonly JudgmentFrameEscalation[];
90
+ }
91
+ /**
92
+ * Build the compact catalog shown to the model on a judgment-bearing turn.
93
+ * Excluded records are returned with a reason so a shrinking catalog is visible.
94
+ */
95
+ export declare function buildJudgmentFrameCatalog(records: readonly JudgmentFrameSourceRecord[]): JudgmentFrameCatalogBuild;
96
+ /** One line per item, grouped by kind, for the model-facing prompt. */
97
+ export declare function renderJudgmentFrameCatalog(catalog: JudgmentFrameCatalog): string;
98
+ /**
99
+ * Check that a frame record only uses catalog references and that every
100
+ * selection is used in at least one option with the role of its kind.
101
+ */
102
+ export declare function validateJudgmentFrameRecord(value: unknown, catalog: JudgmentFrameCatalog): JudgmentFrameValidation;
103
+ export declare const judgmentFrameTools: Tool[];
104
+ export type JudgmentFrameRecordsPage = {
105
+ readonly status: 'ok';
106
+ readonly records: readonly JudgmentFrameSourceRecord[];
107
+ } | {
108
+ readonly status: 'unavailable' | 'error';
109
+ readonly code: string;
110
+ readonly message: string;
111
+ };
112
+ export interface JudgmentFrameToolDependencies {
113
+ /** Read every record of one kind visible to the caller, at most `limit`. */
114
+ readonly loadRecords: (kind: JudgmentFrameKind, limit: number) => Promise<JudgmentFrameRecordsPage>;
115
+ }
116
+ export type JudgmentFrameToolResult = {
117
+ readonly status: 'ok';
118
+ readonly data: Record<string, unknown>;
119
+ } | {
120
+ readonly status: 'unavailable' | 'error';
121
+ readonly error: {
122
+ code: string;
123
+ message: string;
124
+ details?: unknown;
125
+ };
126
+ };
127
+ export declare function handleJudgmentFrameToolCall(name: string, args: Record<string, unknown>, dependencies: JudgmentFrameToolDependencies): Promise<JudgmentFrameToolResult | null>;
@@ -0,0 +1,383 @@
1
+ import { createHash } from 'node:crypto';
2
+ /**
3
+ * Judgment frame (brainbase-project ADR-014).
4
+ *
5
+ * A judgment-bearing turn selects the philosophy, objectives, and world models
6
+ * that bear on the decision from a compact catalog, and records how each
7
+ * selected reference was used for each option. The catalog is small enough to
8
+ * show in full; the record is checked structurally, never for semantic quality.
9
+ *
10
+ * Status is a label, not a gate: draft objectives and unverified models stay
11
+ * usable. Only superseded or inactive records are excluded from the catalog.
12
+ */
13
+ export const JUDGMENT_FRAME_CATALOG_VERSION = 'judgment-frame-catalog.v1';
14
+ export const JUDGMENT_FRAME_RECORD_VERSION = 'judgment-frame-record.v1';
15
+ /** Each kind is used in exactly one way inside an option evaluation. */
16
+ export const JUDGMENT_FRAME_ROLE_BY_KIND = Object.freeze({
17
+ philosophy: 'constraint',
18
+ objective: 'criterion',
19
+ model: 'prediction',
20
+ });
21
+ const KINDS = ['philosophy', 'objective', 'model'];
22
+ const MAX_TEXT = 400;
23
+ const MAX_APPLIES_WHEN = 160;
24
+ const MAX_NOTE = 2_000;
25
+ const MAX_SELECTIONS = 24;
26
+ const MAX_OPTIONS = 8;
27
+ const MAX_USES = 32;
28
+ const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
29
+ const text = (value) => typeof value === 'string' && value.trim() ? value.trim() : undefined;
30
+ const nonEmpty = (value, max) => typeof value === 'string' && value.trim().length > 0 && value.length <= max;
31
+ function clip(value, max) {
32
+ const chars = [...value];
33
+ return chars.length <= max ? value : `${chars.slice(0, max - 1).join('')}…`;
34
+ }
35
+ function canonicalJson(value) {
36
+ if (Array.isArray(value))
37
+ return `[${value.map(canonicalJson).join(',')}]`;
38
+ if (isRecord(value)) {
39
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
40
+ }
41
+ return JSON.stringify(value);
42
+ }
43
+ function itemText(kind, payload) {
44
+ const foundation = isRecord(payload.foundation) ? payload.foundation : {};
45
+ if (kind === 'philosophy') {
46
+ const statement = text(payload.statement) ?? text(foundation.meaning);
47
+ const name = text(payload.display_name) ?? text(payload.title);
48
+ if (!statement)
49
+ return undefined;
50
+ return name && name !== statement ? `${name}:${statement}` : statement;
51
+ }
52
+ if (kind === 'objective')
53
+ return text(foundation.desiredState) ?? text(foundation.meaning) ?? text(payload.statement);
54
+ return text(foundation.meaning) ?? text(payload.statement) ?? text(payload.title);
55
+ }
56
+ function itemStatus(kind, payload) {
57
+ const foundation = isRecord(payload.foundation) ? payload.foundation : {};
58
+ if (kind === 'model') {
59
+ if (foundation.validationState === 'verified')
60
+ return 'verified';
61
+ if (foundation.adoptionState === 'approved')
62
+ return 'adopted_unverified';
63
+ return 'draft';
64
+ }
65
+ const state = text(foundation.adoptionState) ?? text(payload.status);
66
+ return state === 'active' || state === 'approved' ? 'active' : 'draft';
67
+ }
68
+ function isSuperseded(payload) {
69
+ const foundation = isRecord(payload.foundation) ? payload.foundation : {};
70
+ return [payload.status, foundation.adoptionState, foundation.lifecycle]
71
+ .some((value) => typeof value === 'string' && /^(superseded|retired|rejected)/.test(value));
72
+ }
73
+ /**
74
+ * Build the compact catalog shown to the model on a judgment-bearing turn.
75
+ * Excluded records are returned with a reason so a shrinking catalog is visible.
76
+ */
77
+ export function buildJudgmentFrameCatalog(records) {
78
+ const philosophyIds = new Set(records.filter((record) => record.entity_type === 'philosophy').map((record) => record.id));
79
+ const items = [];
80
+ const excluded = [];
81
+ for (const record of records) {
82
+ const kind = KINDS.find((candidate) => candidate === record.entity_type);
83
+ if (!kind) {
84
+ excluded.push({ id: record.id, reason: 'unsupported_type' });
85
+ continue;
86
+ }
87
+ const payload = isRecord(record.payload) ? record.payload : {};
88
+ if (record.lifecycle_status !== undefined && record.lifecycle_status !== 'active') {
89
+ excluded.push({ id: record.id, reason: 'inactive' });
90
+ continue;
91
+ }
92
+ if (isSuperseded(payload)) {
93
+ excluded.push({ id: record.id, reason: 'superseded' });
94
+ continue;
95
+ }
96
+ const decomposition = isRecord(payload.decomposition) ? payload.decomposition : {};
97
+ const sourcePin = isRecord(decomposition.sourcePin) ? decomposition.sourcePin : {};
98
+ if (kind === 'philosophy' && typeof sourcePin.id === 'string' && sourcePin.id !== record.id && philosophyIds.has(sourcePin.id)) {
99
+ excluded.push({ id: record.id, reason: 'restates_source_philosophy' });
100
+ continue;
101
+ }
102
+ const body = itemText(kind, payload);
103
+ if (!body) {
104
+ excluded.push({ id: record.id, reason: 'missing_text' });
105
+ continue;
106
+ }
107
+ const foundation = isRecord(payload.foundation) ? payload.foundation : {};
108
+ const appliesWhen = kind === 'model' ? text(payload.applicability_text) : undefined;
109
+ items.push({
110
+ id: record.id,
111
+ kind,
112
+ revision: text(foundation.revision) ?? String(record.version ?? '1'),
113
+ status: itemStatus(kind, payload),
114
+ text: clip(body, MAX_TEXT),
115
+ ...(appliesWhen ? { applies_when: clip(appliesWhen, MAX_APPLIES_WHEN) } : {}),
116
+ });
117
+ }
118
+ items.sort((left, right) => KINDS.indexOf(left.kind) - KINDS.indexOf(right.kind) || left.id.localeCompare(right.id));
119
+ const digest = `sha256:${createHash('sha256').update(canonicalJson(items)).digest('hex')}`;
120
+ return { catalog: { catalog_version: JUDGMENT_FRAME_CATALOG_VERSION, digest, items }, excluded };
121
+ }
122
+ const KIND_HEADINGS = {
123
+ philosophy: '哲学(constraintとして使う。verdictを付ける)',
124
+ objective: '目的(criterionとして使う)',
125
+ model: '世界モデル(predictionとして使う。仮説であり、検証済みとは限らない)',
126
+ };
127
+ /** One line per item, grouped by kind, for the model-facing prompt. */
128
+ export function renderJudgmentFrameCatalog(catalog) {
129
+ const lines = [`judgment frame catalog ${catalog.digest}`];
130
+ for (const kind of KINDS) {
131
+ lines.push('', `## ${KIND_HEADINGS[kind]}`);
132
+ for (const item of catalog.items.filter((candidate) => candidate.kind === kind)) {
133
+ lines.push(`- ${item.id} [${item.status}] ${item.text}${item.applies_when ? `(適用:${item.applies_when})` : ''}`);
134
+ }
135
+ }
136
+ return lines.join('\n');
137
+ }
138
+ /**
139
+ * Check that a frame record only uses catalog references and that every
140
+ * selection is used in at least one option with the role of its kind.
141
+ */
142
+ export function validateJudgmentFrameRecord(value, catalog) {
143
+ const issues = [];
144
+ const issue = (code, path, message) => issues.push({ code, path, message });
145
+ if (!isRecord(value) || value.record_version !== JUDGMENT_FRAME_RECORD_VERSION) {
146
+ issue('frame_record_invalid', '$', `record_version must be ${JUDGMENT_FRAME_RECORD_VERSION}`);
147
+ return { valid: false, issues, escalations: [] };
148
+ }
149
+ if (value.catalog_digest !== catalog.digest) {
150
+ issue('frame_catalog_mismatch', 'catalog_digest', 'catalog_digest does not match the catalog shown on this turn');
151
+ }
152
+ const byId = new Map(catalog.items.map((item) => [item.id, item]));
153
+ const selections = Array.isArray(value.selections) ? value.selections : null;
154
+ if (!selections || selections.length > MAX_SELECTIONS) {
155
+ issue('frame_record_invalid', 'selections', `selections must be a list of at most ${MAX_SELECTIONS}`);
156
+ }
157
+ const selected = new Map();
158
+ (selections ?? []).forEach((selection, index) => {
159
+ const path = `selections[${index}]`;
160
+ if (!isRecord(selection) || !nonEmpty(selection.ref, 1_000) || !nonEmpty(selection.why, MAX_NOTE)
161
+ || !KINDS.includes(selection.kind)) {
162
+ issue('frame_record_invalid', path, 'selection needs ref, kind, and why');
163
+ return;
164
+ }
165
+ const item = byId.get(selection.ref);
166
+ if (!item) {
167
+ issue('frame_ref_unknown', `${path}.ref`, `${selection.ref} is not in the catalog`);
168
+ return;
169
+ }
170
+ if (item.kind !== selection.kind) {
171
+ issue('frame_kind_mismatch', `${path}.kind`, `${selection.ref} is a ${item.kind}, not a ${String(selection.kind)}`);
172
+ return;
173
+ }
174
+ if (selected.has(selection.ref)) {
175
+ issue('frame_selection_duplicate', `${path}.ref`, `${selection.ref} is selected twice`);
176
+ return;
177
+ }
178
+ selected.set(selection.ref, item.kind);
179
+ });
180
+ const none = isRecord(value.none) ? value.none : {};
181
+ for (const kind of KINDS) {
182
+ const hasSelection = [...selected.values()].includes(kind);
183
+ const reason = none[kind];
184
+ if (hasSelection && reason !== undefined) {
185
+ issue('frame_kind_contradiction', `none.${kind}`, `${kind} has both a selection and a none reason`);
186
+ }
187
+ else if (!hasSelection && !nonEmpty(reason, MAX_NOTE)) {
188
+ issue('frame_kind_unaddressed', `none.${kind}`, `select a ${kind} or give the reason none applies`);
189
+ }
190
+ }
191
+ const options = Array.isArray(value.options) ? value.options : null;
192
+ if (!options || options.length === 0 || options.length > MAX_OPTIONS) {
193
+ issue('frame_record_invalid', 'options', `options must list 1 to ${MAX_OPTIONS} evaluated options`);
194
+ }
195
+ const used = new Set();
196
+ const labels = new Set();
197
+ (options ?? []).forEach((option, optionIndex) => {
198
+ const path = `options[${optionIndex}]`;
199
+ if (!isRecord(option) || !nonEmpty(option.label, 400) || !Array.isArray(option.uses) || option.uses.length > MAX_USES) {
200
+ issue('frame_record_invalid', path, `option needs a label and at most ${MAX_USES} uses`);
201
+ return;
202
+ }
203
+ labels.add(option.label);
204
+ option.uses.forEach((use, useIndex) => {
205
+ const usePath = `${path}.uses[${useIndex}]`;
206
+ if (!isRecord(use) || !nonEmpty(use.ref, 1_000) || !nonEmpty(use.note, MAX_NOTE)) {
207
+ issue('frame_record_invalid', usePath, 'use needs ref, role, and note');
208
+ return;
209
+ }
210
+ const kind = selected.get(use.ref);
211
+ if (!kind) {
212
+ issue('frame_use_unselected', `${usePath}.ref`, `${use.ref} is used but not selected`);
213
+ return;
214
+ }
215
+ if (use.role !== JUDGMENT_FRAME_ROLE_BY_KIND[kind]) {
216
+ issue('frame_role_mismatch', `${usePath}.role`, `a ${kind} is used as ${JUDGMENT_FRAME_ROLE_BY_KIND[kind]}`);
217
+ return;
218
+ }
219
+ if (use.role === 'constraint' && !['satisfied', 'violated', 'tension'].includes(use.verdict)) {
220
+ issue('frame_verdict_missing', `${usePath}.verdict`, 'a constraint use needs verdict satisfied, violated, or tension');
221
+ return;
222
+ }
223
+ used.add(use.ref);
224
+ });
225
+ });
226
+ for (const ref of selected.keys()) {
227
+ if (!used.has(ref))
228
+ issue('frame_selection_unused', 'selections', `${ref} is selected but not used for any option`);
229
+ }
230
+ const chosen = (options ?? []).find((option) => isRecord(option) && option.label === value.chosen_option);
231
+ if (!nonEmpty(value.chosen_option, 400) || !labels.has(value.chosen_option)) {
232
+ issue('frame_chosen_unknown', 'chosen_option', 'chosen_option must name one of the option labels');
233
+ }
234
+ const escalations = [];
235
+ if (![...selected.values()].includes('objective') && nonEmpty(none.objective, MAX_NOTE)) {
236
+ escalations.push({ code: 'objective_unanchored', refs: [] });
237
+ }
238
+ const chosenUses = isRecord(chosen) && Array.isArray(chosen.uses) ? chosen.uses : [];
239
+ if (chosenUses.length > 0) {
240
+ const verdictRefs = (verdict) => chosenUses
241
+ .filter((use) => isRecord(use) && use.role === 'constraint' && use.verdict === verdict)
242
+ .map((use) => String(use.ref));
243
+ const violated = verdictRefs('violated');
244
+ const tension = verdictRefs('tension');
245
+ if (violated.length > 0)
246
+ escalations.push({ code: 'chosen_violates_philosophy', refs: violated });
247
+ if (tension.length > 0)
248
+ escalations.push({ code: 'chosen_philosophy_tension', refs: tension });
249
+ }
250
+ return { valid: issues.length === 0, issues, escalations };
251
+ }
252
+ // ---------------------------------------------------------------------------
253
+ // MCP tools. The host injects how Graph records are read; these definitions
254
+ // and the validation stay shared so every host enforces the same contract.
255
+ const FRAME_PAGE_LIMIT = 500;
256
+ const frameUseSchema = {
257
+ type: 'object',
258
+ additionalProperties: false,
259
+ properties: {
260
+ ref: { type: 'string', minLength: 1, maxLength: 1_000 },
261
+ role: { type: 'string', enum: ['constraint', 'criterion', 'prediction'] },
262
+ note: { type: 'string', minLength: 1, maxLength: MAX_NOTE },
263
+ verdict: { type: 'string', enum: ['satisfied', 'violated', 'tension'], description: 'Required when role is constraint.' },
264
+ },
265
+ required: ['ref', 'role', 'note'],
266
+ };
267
+ export const judgmentFrameTools = [{
268
+ name: 'brainbase_judgment_frame_catalog',
269
+ description: 'On a judgment-bearing turn, read the catalog of philosophy, objectives, and world models visible to you. Select only what bears on this decision. Status is a label (draft objectives and adopted_unverified models are still usable as hypotheses); it is not a reason to skip them. Then call brainbase_judgment_frame_record with the returned catalog_digest.',
270
+ inputSchema: { type: 'object', additionalProperties: false, properties: {} },
271
+ annotations: { readOnlyHint: true, destructiveHint: false },
272
+ }, {
273
+ name: 'brainbase_judgment_frame_record',
274
+ description: 'Record the judgment frame: which catalog references you selected and why, and for each evaluated option how each selected reference was used. Philosophy is a constraint (give verdict satisfied, violated, or tension), an objective is a criterion, a world model is a prediction. Every selection must be used by at least one option. For a kind with no applicable reference, give the reason in none instead of selecting. Name the option you chose. The tool rejects references outside the catalog; it does not judge semantic quality. If escalations are returned, ask the human before acting on the chosen option.',
275
+ inputSchema: {
276
+ type: 'object',
277
+ additionalProperties: false,
278
+ properties: {
279
+ record_version: { type: 'string', const: JUDGMENT_FRAME_RECORD_VERSION },
280
+ catalog_digest: { type: 'string', pattern: '^sha256:[a-f0-9]{64}$' },
281
+ selections: {
282
+ type: 'array',
283
+ maxItems: MAX_SELECTIONS,
284
+ items: {
285
+ type: 'object',
286
+ additionalProperties: false,
287
+ properties: {
288
+ ref: { type: 'string', minLength: 1, maxLength: 1_000 },
289
+ kind: { type: 'string', enum: [...KINDS] },
290
+ why: { type: 'string', minLength: 1, maxLength: MAX_NOTE },
291
+ },
292
+ required: ['ref', 'kind', 'why'],
293
+ },
294
+ },
295
+ none: {
296
+ type: 'object',
297
+ additionalProperties: false,
298
+ properties: Object.fromEntries(KINDS.map((kind) => [kind, { type: 'string', minLength: 1, maxLength: MAX_NOTE }])),
299
+ },
300
+ options: {
301
+ type: 'array',
302
+ minItems: 1,
303
+ maxItems: MAX_OPTIONS,
304
+ items: {
305
+ type: 'object',
306
+ additionalProperties: false,
307
+ properties: {
308
+ label: { type: 'string', minLength: 1, maxLength: 400 },
309
+ uses: { type: 'array', maxItems: MAX_USES, items: frameUseSchema },
310
+ },
311
+ required: ['label', 'uses'],
312
+ },
313
+ },
314
+ chosen_option: { type: 'string', minLength: 1, maxLength: 400 },
315
+ },
316
+ required: ['record_version', 'catalog_digest', 'selections', 'options', 'chosen_option'],
317
+ },
318
+ annotations: { readOnlyHint: true, destructiveHint: false },
319
+ }];
320
+ const toolError = (status, code, message, details) => ({
321
+ status, error: { code, message, ...(details === undefined ? {} : { details }) },
322
+ });
323
+ async function loadFrameCatalog(dependencies) {
324
+ const records = [];
325
+ for (const kind of KINDS) {
326
+ let page;
327
+ try {
328
+ page = await dependencies.loadRecords(kind, FRAME_PAGE_LIMIT);
329
+ }
330
+ catch (error) {
331
+ return toolError('unavailable', 'judgment_frame_catalog_unavailable', `Graph ${kind} records could not be read: ${error instanceof Error ? error.message : String(error)}`);
332
+ }
333
+ if (page.status !== 'ok')
334
+ return toolError(page.status, page.code, page.message);
335
+ // A full page may be truncated; a silently shorter catalog would hide references.
336
+ if (page.records.length >= FRAME_PAGE_LIMIT) {
337
+ return toolError('error', 'judgment_frame_catalog_truncated', `Graph returned ${page.records.length} ${kind} records; the catalog may be incomplete`);
338
+ }
339
+ records.push(...page.records);
340
+ }
341
+ return buildJudgmentFrameCatalog(records);
342
+ }
343
+ export async function handleJudgmentFrameToolCall(name, args, dependencies) {
344
+ if (name !== 'brainbase_judgment_frame_catalog' && name !== 'brainbase_judgment_frame_record')
345
+ return null;
346
+ if (name === 'brainbase_judgment_frame_catalog' && Object.keys(args).length > 0) {
347
+ return toolError('error', 'judgment_frame_catalog_input_invalid', 'brainbase_judgment_frame_catalog takes no arguments');
348
+ }
349
+ const loaded = await loadFrameCatalog(dependencies);
350
+ if ('status' in loaded)
351
+ return loaded;
352
+ const { catalog, excluded } = loaded;
353
+ if (name === 'brainbase_judgment_frame_catalog') {
354
+ return {
355
+ status: 'ok',
356
+ data: {
357
+ schema_version: 'brainbase-judgment-frame-catalog-v1',
358
+ catalog_digest: catalog.digest,
359
+ counts: Object.fromEntries(KINDS.map((kind) => [kind, catalog.items.filter((item) => item.kind === kind).length])),
360
+ excluded_count: excluded.length,
361
+ catalog: renderJudgmentFrameCatalog(catalog),
362
+ },
363
+ };
364
+ }
365
+ const validation = validateJudgmentFrameRecord(args, catalog);
366
+ if (!validation.valid) {
367
+ return toolError('error', 'judgment_frame_invalid', 'The judgment frame does not match the catalog contract', {
368
+ issues: validation.issues,
369
+ current_catalog_digest: catalog.digest,
370
+ });
371
+ }
372
+ return {
373
+ status: 'ok',
374
+ data: {
375
+ schema_version: 'brainbase-judgment-frame-v1',
376
+ catalog_digest: catalog.digest,
377
+ selections: args.selections.map(({ ref, kind }) => ({ ref, kind })),
378
+ option_count: args.options.length,
379
+ chosen_option: args.chosen_option,
380
+ escalations: validation.escalations,
381
+ },
382
+ };
383
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unson/brainbase-mcp",
3
- "version": "0.10.13",
3
+ "version": "0.10.15",
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",
@@ -27,6 +27,10 @@
27
27
  "types": "./dist/judgment-dag-composition.d.ts",
28
28
  "import": "./dist/judgment-dag-composition.js"
29
29
  },
30
+ "./judgment-frame": {
31
+ "types": "./dist/judgment-frame.d.ts",
32
+ "import": "./dist/judgment-frame.js"
33
+ },
30
34
  "./judgment-value-proof": {
31
35
  "types": "./dist/judgment-value-proof.d.ts",
32
36
  "import": "./dist/judgment-value-proof.js"
@@ -340,5 +344,5 @@
340
344
  "engines": {
341
345
  "node": ">=20"
342
346
  },
343
- "gitHead": "e064a40fee8b023550daab1a28ca28fecb55f171"
347
+ "gitHead": "04f1192c75877ca241970ee379c274e37c6693cc"
344
348
  }