@unson/brainbase-mcp 0.24.0 → 0.24.1
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/judgment-frame.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { type JudgmentModelOutcome, type JudgmentModelOutcomeSummary } from './judgment-model-outcome.js';
|
|
2
3
|
/**
|
|
3
4
|
* Judgment frame (brainbase-project ADR-014).
|
|
4
5
|
*
|
|
@@ -91,7 +92,9 @@ export interface JudgmentFrameIssue {
|
|
|
91
92
|
readonly message: string;
|
|
92
93
|
}
|
|
93
94
|
/** Exceptions that ADR-014 F5 returns to a human. The host decides when they block. */
|
|
94
|
-
export type JudgmentFrameEscalationCode = 'objective_unanchored' | 'chosen_violates_philosophy' | 'chosen_philosophy_tension'
|
|
95
|
+
export type JudgmentFrameEscalationCode = 'objective_unanchored' | 'chosen_violates_philosophy' | 'chosen_philosophy_tension'
|
|
96
|
+
/** The chosen option predicts with a world model refuted at its current revision (F5, F6). */
|
|
97
|
+
| 'chosen_uses_refuted_model';
|
|
95
98
|
export interface JudgmentFrameEscalation {
|
|
96
99
|
readonly code: JudgmentFrameEscalationCode;
|
|
97
100
|
readonly refs: readonly string[];
|
|
@@ -101,6 +104,8 @@ export interface JudgmentFrameValidation {
|
|
|
101
104
|
readonly issues: readonly JudgmentFrameIssue[];
|
|
102
105
|
readonly escalations: readonly JudgmentFrameEscalation[];
|
|
103
106
|
}
|
|
107
|
+
/** True when text looks like it carries a credential. Shared by public frames and outcome records. */
|
|
108
|
+
export declare function containsJudgmentFrameCredential(value: string): boolean;
|
|
104
109
|
/**
|
|
105
110
|
* Compute the digest for the exact two-field public projection.
|
|
106
111
|
* The response adds this digest; it is intentionally separate from the
|
|
@@ -118,13 +123,16 @@ export declare function judgmentFrameRecordDigest(value: JudgmentFrameRecord): `
|
|
|
118
123
|
* Excluded records are returned with a reason so a shrinking catalog is visible.
|
|
119
124
|
*/
|
|
120
125
|
export declare function buildJudgmentFrameCatalog(records: readonly JudgmentFrameSourceRecord[]): JudgmentFrameCatalogBuild;
|
|
121
|
-
/**
|
|
122
|
-
|
|
126
|
+
/**
|
|
127
|
+
* One line per item, grouped by kind, for the model-facing prompt. Outcome
|
|
128
|
+
* summaries, when given, annotate world models; they are not part of the digest.
|
|
129
|
+
*/
|
|
130
|
+
export declare function renderJudgmentFrameCatalog(catalog: JudgmentFrameCatalog, outcomes?: ReadonlyMap<string, JudgmentModelOutcomeSummary>): string;
|
|
123
131
|
/**
|
|
124
132
|
* Check that a frame record only uses catalog references and that every
|
|
125
133
|
* selection is used in at least one option with the role of its kind.
|
|
126
134
|
*/
|
|
127
|
-
export declare function validateJudgmentFrameRecord(value: unknown, catalog: JudgmentFrameCatalog): JudgmentFrameValidation;
|
|
135
|
+
export declare function validateJudgmentFrameRecord(value: unknown, catalog: JudgmentFrameCatalog, outcomes?: ReadonlyMap<string, JudgmentModelOutcomeSummary>): JudgmentFrameValidation;
|
|
128
136
|
export declare const judgmentFrameTools: Tool[];
|
|
129
137
|
export type JudgmentFrameRecordsPage = {
|
|
130
138
|
readonly status: 'ok';
|
|
@@ -134,9 +142,38 @@ export type JudgmentFrameRecordsPage = {
|
|
|
134
142
|
readonly code: string;
|
|
135
143
|
readonly message: string;
|
|
136
144
|
};
|
|
145
|
+
export type JudgmentModelOutcomesPage = {
|
|
146
|
+
readonly status: 'ok';
|
|
147
|
+
readonly records: readonly unknown[];
|
|
148
|
+
} | {
|
|
149
|
+
readonly status: 'unavailable' | 'error';
|
|
150
|
+
readonly code: string;
|
|
151
|
+
readonly message: string;
|
|
152
|
+
};
|
|
153
|
+
export type JudgmentModelOutcomeAppendResult = {
|
|
154
|
+
readonly status: 'ok';
|
|
155
|
+
readonly id: string;
|
|
156
|
+
readonly recorded_at: string;
|
|
157
|
+
} | {
|
|
158
|
+
readonly status: 'unavailable' | 'error';
|
|
159
|
+
readonly code: string;
|
|
160
|
+
readonly message: string;
|
|
161
|
+
};
|
|
137
162
|
export interface JudgmentFrameToolDependencies {
|
|
138
163
|
/** Read every record of one kind visible to the caller, at most `limit`. */
|
|
139
164
|
readonly loadRecords: (kind: JudgmentFrameKind, limit: number) => Promise<JudgmentFrameRecordsPage>;
|
|
165
|
+
/**
|
|
166
|
+
* Read every stored world-model outcome visible to the caller (ADR-014 F6).
|
|
167
|
+
* When omitted, the catalog and escalations behave as before.
|
|
168
|
+
*/
|
|
169
|
+
readonly loadModelOutcomes?: () => Promise<JudgmentModelOutcomesPage>;
|
|
170
|
+
}
|
|
171
|
+
export interface JudgmentModelOutcomeToolDependencies extends JudgmentFrameToolDependencies {
|
|
172
|
+
readonly loadModelOutcomes: () => Promise<JudgmentModelOutcomesPage>;
|
|
173
|
+
/** Append one validated outcome. The host owns id, time, author, authorization, and storage. */
|
|
174
|
+
readonly appendModelOutcome: (outcome: JudgmentModelOutcome & {
|
|
175
|
+
readonly digest: `sha256:${string}`;
|
|
176
|
+
}) => Promise<JudgmentModelOutcomeAppendResult>;
|
|
140
177
|
}
|
|
141
178
|
export type JudgmentFrameToolResult = {
|
|
142
179
|
readonly status: 'ok';
|
|
@@ -150,5 +187,13 @@ export type JudgmentFrameToolResult = {
|
|
|
150
187
|
};
|
|
151
188
|
};
|
|
152
189
|
export declare function handleJudgmentFrameToolCall(name: string, args: Record<string, unknown>, dependencies: JudgmentFrameToolDependencies): Promise<JudgmentFrameToolResult | null>;
|
|
190
|
+
/**
|
|
191
|
+
* Opt-in tool for hosts that store world-model outcomes (ADR-014 F6).
|
|
192
|
+
* Published separately so a host without storage does not expose it.
|
|
193
|
+
*/
|
|
194
|
+
export declare const judgmentModelOutcomeTools: Tool[];
|
|
195
|
+
export declare function handleJudgmentModelOutcomeToolCall(name: string, args: Record<string, unknown>, dependencies: JudgmentModelOutcomeToolDependencies): Promise<JudgmentFrameToolResult | null>;
|
|
196
|
+
export { JUDGMENT_MODEL_OUTCOME_ALTERNATIVES, JUDGMENT_MODEL_OUTCOME_RECORD_VERSION, judgmentModelOutcomeDigest, parseJudgmentModelOutcomeRecords, summarizeJudgmentModelOutcomes, validateJudgmentModelOutcome, } from './judgment-model-outcome.js';
|
|
197
|
+
export type { JudgmentModelOutcome, JudgmentModelOutcomeAlternative, JudgmentModelOutcomeEvidenceKind, JudgmentModelOutcomeEvidenceRef, JudgmentModelOutcomeIssue, JudgmentModelOutcomeIssueCode, JudgmentModelOutcomeStoredRecord, JudgmentModelOutcomeSummary, JudgmentModelOutcomeValidation, JudgmentModelOutcomeVerdict, } from './judgment-model-outcome.js';
|
|
153
198
|
export { judgmentFrameReadTools, handleJudgmentFrameReadToolCall, } from './judgment-frame-read.js';
|
|
154
199
|
export type { JudgmentFrameReadDependencies, JudgmentFrameReadRecord, } from './judgment-frame-read.js';
|
package/dist/judgment-frame.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { JUDGMENT_MODEL_OUTCOME_ALTERNATIVES, JUDGMENT_MODEL_OUTCOME_RECORD_VERSION, judgmentModelOutcomeDigest, parseJudgmentModelOutcomeRecords, summarizeJudgmentModelOutcomes, validateJudgmentModelOutcome, } from './judgment-model-outcome.js';
|
|
2
3
|
/**
|
|
3
4
|
* Judgment frame (brainbase-project ADR-014).
|
|
4
5
|
*
|
|
@@ -50,7 +51,8 @@ const PUBLIC_FRAME_SECRET_PATTERNS = [
|
|
|
50
51
|
/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/u,
|
|
51
52
|
];
|
|
52
53
|
const PUBLIC_FRAME_CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f]/u;
|
|
53
|
-
|
|
54
|
+
/** True when text looks like it carries a credential. Shared by public frames and outcome records. */
|
|
55
|
+
export function containsJudgmentFrameCredential(value) {
|
|
54
56
|
return PUBLIC_FRAME_SECRET_PATTERNS.some((pattern) => pattern.test(value));
|
|
55
57
|
}
|
|
56
58
|
function publicFrameRecord(value) {
|
|
@@ -164,13 +166,24 @@ const KIND_HEADINGS = {
|
|
|
164
166
|
objective: '目的(criterionとして使う)',
|
|
165
167
|
model: '世界モデル(predictionとして使う。仮説であり、検証済みとは限らない)',
|
|
166
168
|
};
|
|
167
|
-
|
|
168
|
-
|
|
169
|
+
function renderOutcomeSummary(summary) {
|
|
170
|
+
if (!summary)
|
|
171
|
+
return '';
|
|
172
|
+
const counts = `支持${summary.supports}・反証${summary.refutes}・判定不能${summary.inconclusive}`;
|
|
173
|
+
const refuted = summary.refuted_when.length > 0 ? `。反証の条件:${summary.refuted_when.join('/')}` : '';
|
|
174
|
+
return `(この版の結果:${counts}${refuted})`;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* One line per item, grouped by kind, for the model-facing prompt. Outcome
|
|
178
|
+
* summaries, when given, annotate world models; they are not part of the digest.
|
|
179
|
+
*/
|
|
180
|
+
export function renderJudgmentFrameCatalog(catalog, outcomes = new Map()) {
|
|
169
181
|
const lines = [`judgment frame catalog ${catalog.digest}`];
|
|
170
182
|
for (const kind of KINDS) {
|
|
171
183
|
lines.push('', `## ${KIND_HEADINGS[kind]}`);
|
|
172
184
|
for (const item of catalog.items.filter((candidate) => candidate.kind === kind)) {
|
|
173
|
-
|
|
185
|
+
const outcome = kind === 'model' ? renderOutcomeSummary(outcomes.get(item.id)) : '';
|
|
186
|
+
lines.push(`- ${item.id} [${item.status}] ${item.text}${item.applies_when ? `(適用:${item.applies_when})` : ''}${outcome}`);
|
|
174
187
|
}
|
|
175
188
|
}
|
|
176
189
|
return lines.join('\n');
|
|
@@ -179,7 +192,7 @@ export function renderJudgmentFrameCatalog(catalog) {
|
|
|
179
192
|
* Check that a frame record only uses catalog references and that every
|
|
180
193
|
* selection is used in at least one option with the role of its kind.
|
|
181
194
|
*/
|
|
182
|
-
export function validateJudgmentFrameRecord(value, catalog) {
|
|
195
|
+
export function validateJudgmentFrameRecord(value, catalog, outcomes = new Map()) {
|
|
183
196
|
const issues = [];
|
|
184
197
|
const issue = (code, path, message) => issues.push({ code, path, message });
|
|
185
198
|
if (!isRecord(value) || value.record_version !== JUDGMENT_FRAME_RECORD_VERSION) {
|
|
@@ -205,7 +218,7 @@ export function validateJudgmentFrameRecord(value, catalog) {
|
|
|
205
218
|
if (PUBLIC_FRAME_CONTROL_CHARACTERS.test(candidate)) {
|
|
206
219
|
issue('frame_public_invalid', `public_frame.${key}`, `${key} contains control characters`);
|
|
207
220
|
}
|
|
208
|
-
if (
|
|
221
|
+
if (containsJudgmentFrameCredential(candidate)) {
|
|
209
222
|
issue('frame_public_invalid', `public_frame.${key}`, `${key} contains a credential-like value`);
|
|
210
223
|
}
|
|
211
224
|
}
|
|
@@ -311,6 +324,12 @@ export function validateJudgmentFrameRecord(value, catalog) {
|
|
|
311
324
|
escalations.push({ code: 'chosen_violates_philosophy', refs: violated });
|
|
312
325
|
if (tension.length > 0)
|
|
313
326
|
escalations.push({ code: 'chosen_philosophy_tension', refs: tension });
|
|
327
|
+
const refuted = [...new Set(chosenUses
|
|
328
|
+
.filter((use) => isRecord(use) && use.role === 'prediction'
|
|
329
|
+
&& selected.get(String(use.ref)) === 'model' && (outcomes.get(String(use.ref))?.refutes ?? 0) > 0)
|
|
330
|
+
.map((use) => String(use.ref)))];
|
|
331
|
+
if (refuted.length > 0)
|
|
332
|
+
escalations.push({ code: 'chosen_uses_refuted_model', refs: refuted });
|
|
314
333
|
}
|
|
315
334
|
return { valid: issues.length === 0, issues, escalations };
|
|
316
335
|
}
|
|
@@ -415,6 +434,24 @@ async function loadFrameCatalog(dependencies) {
|
|
|
415
434
|
}
|
|
416
435
|
return buildJudgmentFrameCatalog(records);
|
|
417
436
|
}
|
|
437
|
+
async function loadOutcomeSummaries(dependencies, catalog) {
|
|
438
|
+
if (!dependencies.loadModelOutcomes)
|
|
439
|
+
return new Map();
|
|
440
|
+
let page;
|
|
441
|
+
try {
|
|
442
|
+
page = await dependencies.loadModelOutcomes();
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
return toolError('unavailable', 'judgment_model_outcomes_unavailable', `World model outcomes could not be read: ${error instanceof Error ? error.message : String(error)}`);
|
|
446
|
+
}
|
|
447
|
+
if (page.status !== 'ok')
|
|
448
|
+
return toolError(page.status, page.code, page.message);
|
|
449
|
+
const parsed = parseJudgmentModelOutcomeRecords(page.records);
|
|
450
|
+
if (!parsed.ok) {
|
|
451
|
+
return toolError('error', 'judgment_model_outcomes_invalid', `Stored world model outcome ${parsed.index} is invalid: ${parsed.message}`);
|
|
452
|
+
}
|
|
453
|
+
return summarizeJudgmentModelOutcomes(parsed.records, catalog);
|
|
454
|
+
}
|
|
418
455
|
export async function handleJudgmentFrameToolCall(name, args, dependencies) {
|
|
419
456
|
if (name !== 'brainbase_judgment_frame_catalog' && name !== 'brainbase_judgment_frame_record')
|
|
420
457
|
return null;
|
|
@@ -425,6 +462,9 @@ export async function handleJudgmentFrameToolCall(name, args, dependencies) {
|
|
|
425
462
|
if ('status' in loaded)
|
|
426
463
|
return loaded;
|
|
427
464
|
const { catalog, excluded } = loaded;
|
|
465
|
+
const outcomes = await loadOutcomeSummaries(dependencies, catalog);
|
|
466
|
+
if (!(outcomes instanceof Map))
|
|
467
|
+
return outcomes;
|
|
428
468
|
if (name === 'brainbase_judgment_frame_catalog') {
|
|
429
469
|
return {
|
|
430
470
|
status: 'ok',
|
|
@@ -433,11 +473,12 @@ export async function handleJudgmentFrameToolCall(name, args, dependencies) {
|
|
|
433
473
|
catalog_digest: catalog.digest,
|
|
434
474
|
counts: Object.fromEntries(KINDS.map((kind) => [kind, catalog.items.filter((item) => item.kind === kind).length])),
|
|
435
475
|
excluded_count: excluded.length,
|
|
436
|
-
|
|
476
|
+
...(dependencies.loadModelOutcomes ? { models_with_outcomes: outcomes.size } : {}),
|
|
477
|
+
catalog: renderJudgmentFrameCatalog(catalog, outcomes),
|
|
437
478
|
},
|
|
438
479
|
};
|
|
439
480
|
}
|
|
440
|
-
const validation = validateJudgmentFrameRecord(args, catalog);
|
|
481
|
+
const validation = validateJudgmentFrameRecord(args, catalog, outcomes);
|
|
441
482
|
if (!validation.valid) {
|
|
442
483
|
return toolError('error', 'judgment_frame_invalid', 'The judgment frame does not match the catalog contract', {
|
|
443
484
|
issues: validation.issues,
|
|
@@ -463,4 +504,100 @@ export async function handleJudgmentFrameToolCall(name, args, dependencies) {
|
|
|
463
504
|
},
|
|
464
505
|
};
|
|
465
506
|
}
|
|
507
|
+
const outcomeTextSchema = { type: 'string', minLength: 1, maxLength: 2_000 };
|
|
508
|
+
/**
|
|
509
|
+
* Opt-in tool for hosts that store world-model outcomes (ADR-014 F6).
|
|
510
|
+
* Published separately so a host without storage does not expose it.
|
|
511
|
+
*/
|
|
512
|
+
export const judgmentModelOutcomeTools = [{
|
|
513
|
+
name: 'brainbase_judgment_model_outcome_record',
|
|
514
|
+
description: 'After the result of a judgment is observed, record whether it supports or refutes a world model that the judgment used as a prediction. Read brainbase_judgment_frame_catalog first and pass its catalog_digest and the model id at its current revision. State the prediction made from the model, what was observed, the conditions (facility, period, state) under which the result holds, and at least one evidence reference. A refutation must say, for each of measurement_error, execution_difference, and external_change, why it does not explain the gap; a prediction gap alone is not a refutation. This does not change the model, its revision, or its adoption; adoption and verification stay human decisions. The tool does not judge whether the verdict is semantically right.',
|
|
515
|
+
inputSchema: {
|
|
516
|
+
type: 'object',
|
|
517
|
+
additionalProperties: false,
|
|
518
|
+
properties: {
|
|
519
|
+
record_version: { type: 'string', const: JUDGMENT_MODEL_OUTCOME_RECORD_VERSION },
|
|
520
|
+
catalog_digest: { type: 'string', pattern: '^sha256:[a-f0-9]{64}$' },
|
|
521
|
+
model: {
|
|
522
|
+
type: 'object',
|
|
523
|
+
additionalProperties: false,
|
|
524
|
+
properties: {
|
|
525
|
+
id: { type: 'string', minLength: 1, maxLength: 1_000 },
|
|
526
|
+
revision: { type: 'string', minLength: 1, maxLength: 64 },
|
|
527
|
+
},
|
|
528
|
+
required: ['id', 'revision'],
|
|
529
|
+
},
|
|
530
|
+
verdict: { type: 'string', enum: ['supports', 'refutes', 'inconclusive'] },
|
|
531
|
+
prediction: outcomeTextSchema,
|
|
532
|
+
observed: outcomeTextSchema,
|
|
533
|
+
conditions: outcomeTextSchema,
|
|
534
|
+
evidence_refs: {
|
|
535
|
+
type: 'array',
|
|
536
|
+
minItems: 1,
|
|
537
|
+
maxItems: 12,
|
|
538
|
+
items: {
|
|
539
|
+
type: 'object',
|
|
540
|
+
additionalProperties: false,
|
|
541
|
+
properties: {
|
|
542
|
+
kind: { type: 'string', enum: ['artifact', 'query', 'document', 'judgment'] },
|
|
543
|
+
ref: { type: 'string', minLength: 1, maxLength: 800 },
|
|
544
|
+
label: { type: 'string', minLength: 1, maxLength: 200 },
|
|
545
|
+
},
|
|
546
|
+
required: ['kind', 'ref', 'label'],
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
ruled_out: {
|
|
550
|
+
type: 'object',
|
|
551
|
+
additionalProperties: false,
|
|
552
|
+
description: 'Required only when verdict is refutes.',
|
|
553
|
+
properties: Object.fromEntries(JUDGMENT_MODEL_OUTCOME_ALTERNATIVES.map((key) => [key, { type: 'string', minLength: 1, maxLength: 1_000 }])),
|
|
554
|
+
required: [...JUDGMENT_MODEL_OUTCOME_ALTERNATIVES],
|
|
555
|
+
},
|
|
556
|
+
judgment_ref: {
|
|
557
|
+
type: 'object',
|
|
558
|
+
additionalProperties: false,
|
|
559
|
+
properties: { frame_digest: { type: 'string', pattern: '^sha256:[a-f0-9]{64}$' } },
|
|
560
|
+
required: ['frame_digest'],
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
required: ['record_version', 'catalog_digest', 'model', 'verdict', 'prediction', 'observed', 'conditions', 'evidence_refs'],
|
|
564
|
+
},
|
|
565
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
566
|
+
}];
|
|
567
|
+
export async function handleJudgmentModelOutcomeToolCall(name, args, dependencies) {
|
|
568
|
+
if (name !== 'brainbase_judgment_model_outcome_record')
|
|
569
|
+
return null;
|
|
570
|
+
const loaded = await loadFrameCatalog(dependencies);
|
|
571
|
+
if ('status' in loaded)
|
|
572
|
+
return loaded;
|
|
573
|
+
const validation = validateJudgmentModelOutcome(args, loaded.catalog, containsJudgmentFrameCredential);
|
|
574
|
+
if (!validation.valid) {
|
|
575
|
+
return toolError('error', 'judgment_model_outcome_invalid', 'The outcome does not match the current catalog contract', {
|
|
576
|
+
issues: validation.issues,
|
|
577
|
+
current_catalog_digest: loaded.catalog.digest,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
const digest = judgmentModelOutcomeDigest(validation.outcome);
|
|
581
|
+
let appended;
|
|
582
|
+
try {
|
|
583
|
+
appended = await dependencies.appendModelOutcome({ ...validation.outcome, digest });
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
return toolError('unavailable', 'judgment_model_outcome_unavailable', `The outcome could not be stored: ${error instanceof Error ? error.message : String(error)}`);
|
|
587
|
+
}
|
|
588
|
+
if (appended.status !== 'ok')
|
|
589
|
+
return toolError(appended.status, appended.code, appended.message);
|
|
590
|
+
return {
|
|
591
|
+
status: 'ok',
|
|
592
|
+
data: {
|
|
593
|
+
schema_version: 'brainbase-judgment-model-outcome-v1',
|
|
594
|
+
id: appended.id,
|
|
595
|
+
recorded_at: appended.recorded_at,
|
|
596
|
+
model: validation.outcome.model,
|
|
597
|
+
verdict: validation.outcome.verdict,
|
|
598
|
+
digest,
|
|
599
|
+
},
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
export { JUDGMENT_MODEL_OUTCOME_ALTERNATIVES, JUDGMENT_MODEL_OUTCOME_RECORD_VERSION, judgmentModelOutcomeDigest, parseJudgmentModelOutcomeRecords, summarizeJudgmentModelOutcomes, validateJudgmentModelOutcome, } from './judgment-model-outcome.js';
|
|
466
603
|
export { judgmentFrameReadTools, handleJudgmentFrameReadToolCall, } from './judgment-frame-read.js';
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { JudgmentFrameCatalog } from './judgment-frame.js';
|
|
2
|
+
/**
|
|
3
|
+
* Judgment outcome feedback for world models (brainbase-project ADR-014 F6).
|
|
4
|
+
*
|
|
5
|
+
* After a judgment that used a world model as a prediction, the observed
|
|
6
|
+
* result is appended as a separate record bound to the model's id and
|
|
7
|
+
* revision. The model itself (revision, validationState, adoptionState) is
|
|
8
|
+
* never changed here: adoption and promotion stay human decisions.
|
|
9
|
+
*
|
|
10
|
+
* Records are checked structurally against the current catalog. Whether a
|
|
11
|
+
* verdict is semantically right is not judged. A refutation must explain why
|
|
12
|
+
* measurement error, execution difference, and external change were ruled
|
|
13
|
+
* out, so a prediction gap alone is never recorded as a refutation.
|
|
14
|
+
*/
|
|
15
|
+
export declare const JUDGMENT_MODEL_OUTCOME_RECORD_VERSION: "judgment-model-outcome.v1";
|
|
16
|
+
export type JudgmentModelOutcomeVerdict = 'supports' | 'refutes' | 'inconclusive';
|
|
17
|
+
export type JudgmentModelOutcomeEvidenceKind = 'artifact' | 'query' | 'document' | 'judgment';
|
|
18
|
+
export declare const JUDGMENT_MODEL_OUTCOME_ALTERNATIVES: readonly ["measurement_error", "execution_difference", "external_change"];
|
|
19
|
+
export type JudgmentModelOutcomeAlternative = typeof JUDGMENT_MODEL_OUTCOME_ALTERNATIVES[number];
|
|
20
|
+
export interface JudgmentModelOutcomeEvidenceRef {
|
|
21
|
+
readonly kind: JudgmentModelOutcomeEvidenceKind;
|
|
22
|
+
readonly ref: string;
|
|
23
|
+
readonly label: string;
|
|
24
|
+
}
|
|
25
|
+
export interface JudgmentModelOutcome {
|
|
26
|
+
readonly record_version: typeof JUDGMENT_MODEL_OUTCOME_RECORD_VERSION;
|
|
27
|
+
readonly model: {
|
|
28
|
+
readonly id: string;
|
|
29
|
+
readonly revision: string;
|
|
30
|
+
};
|
|
31
|
+
readonly verdict: JudgmentModelOutcomeVerdict;
|
|
32
|
+
/** What the judgment predicted from this model for this case. */
|
|
33
|
+
readonly prediction: string;
|
|
34
|
+
/** What was actually observed. */
|
|
35
|
+
readonly observed: string;
|
|
36
|
+
/** Facility, period, and state under which the result holds. */
|
|
37
|
+
readonly conditions: string;
|
|
38
|
+
readonly evidence_refs: readonly JudgmentModelOutcomeEvidenceRef[];
|
|
39
|
+
/** Required only for refutes: why each alternative explanation was ruled out. */
|
|
40
|
+
readonly ruled_out?: Readonly<Record<JudgmentModelOutcomeAlternative, string>>;
|
|
41
|
+
readonly judgment_ref?: {
|
|
42
|
+
readonly frame_digest: `sha256:${string}`;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** A record as the host stored it: the outcome plus host-owned id, time, and content digest. */
|
|
46
|
+
export interface JudgmentModelOutcomeStoredRecord extends JudgmentModelOutcome {
|
|
47
|
+
readonly id: string;
|
|
48
|
+
readonly recorded_at: string;
|
|
49
|
+
readonly digest: `sha256:${string}`;
|
|
50
|
+
}
|
|
51
|
+
export interface JudgmentModelOutcomeSummary {
|
|
52
|
+
readonly supports: number;
|
|
53
|
+
readonly refutes: number;
|
|
54
|
+
readonly inconclusive: number;
|
|
55
|
+
/** Conditions of the most recent refutations, newest first. */
|
|
56
|
+
readonly refuted_when: readonly string[];
|
|
57
|
+
}
|
|
58
|
+
export type JudgmentModelOutcomeIssueCode = 'outcome_record_invalid' | 'outcome_catalog_mismatch' | 'outcome_model_unknown' | 'outcome_not_model' | 'outcome_revision_mismatch' | 'outcome_ruled_out_missing' | 'outcome_ruled_out_unexpected' | 'outcome_evidence_missing' | 'outcome_text_unsafe';
|
|
59
|
+
export interface JudgmentModelOutcomeIssue {
|
|
60
|
+
readonly code: JudgmentModelOutcomeIssueCode;
|
|
61
|
+
readonly path: string;
|
|
62
|
+
readonly message: string;
|
|
63
|
+
}
|
|
64
|
+
export type JudgmentModelOutcomeValidation = {
|
|
65
|
+
readonly valid: true;
|
|
66
|
+
readonly issues: readonly [];
|
|
67
|
+
readonly outcome: JudgmentModelOutcome;
|
|
68
|
+
} | {
|
|
69
|
+
readonly valid: false;
|
|
70
|
+
readonly issues: readonly JudgmentModelOutcomeIssue[];
|
|
71
|
+
};
|
|
72
|
+
/** Digest of the outcome content. Host-owned id and time are not part of it. */
|
|
73
|
+
export declare function judgmentModelOutcomeDigest(outcome: JudgmentModelOutcome): `sha256:${string}`;
|
|
74
|
+
/**
|
|
75
|
+
* Validate a new outcome against the catalog shown on this turn. The model
|
|
76
|
+
* must be a world model in the catalog, at its current revision.
|
|
77
|
+
*/
|
|
78
|
+
export declare function validateJudgmentModelOutcome(value: unknown, catalog: JudgmentFrameCatalog, unsafe?: (text: string) => boolean): JudgmentModelOutcomeValidation;
|
|
79
|
+
/**
|
|
80
|
+
* Parse records returned by the host store. A malformed record or a content
|
|
81
|
+
* digest mismatch is an error, never skipped silently.
|
|
82
|
+
*/
|
|
83
|
+
export declare function parseJudgmentModelOutcomeRecords(values: readonly unknown[]): {
|
|
84
|
+
readonly ok: true;
|
|
85
|
+
readonly records: readonly JudgmentModelOutcomeStoredRecord[];
|
|
86
|
+
} | {
|
|
87
|
+
readonly ok: false;
|
|
88
|
+
readonly index: number;
|
|
89
|
+
readonly message: string;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Count outcomes per world model at its current catalog revision. Outcomes
|
|
93
|
+
* recorded against an older revision are not counted: whether a refutation
|
|
94
|
+
* still holds after a revision is a separate judgment.
|
|
95
|
+
*/
|
|
96
|
+
export declare function summarizeJudgmentModelOutcomes(records: readonly JudgmentModelOutcomeStoredRecord[], catalog: JudgmentFrameCatalog): ReadonlyMap<string, JudgmentModelOutcomeSummary>;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* Judgment outcome feedback for world models (brainbase-project ADR-014 F6).
|
|
4
|
+
*
|
|
5
|
+
* After a judgment that used a world model as a prediction, the observed
|
|
6
|
+
* result is appended as a separate record bound to the model's id and
|
|
7
|
+
* revision. The model itself (revision, validationState, adoptionState) is
|
|
8
|
+
* never changed here: adoption and promotion stay human decisions.
|
|
9
|
+
*
|
|
10
|
+
* Records are checked structurally against the current catalog. Whether a
|
|
11
|
+
* verdict is semantically right is not judged. A refutation must explain why
|
|
12
|
+
* measurement error, execution difference, and external change were ruled
|
|
13
|
+
* out, so a prediction gap alone is never recorded as a refutation.
|
|
14
|
+
*/
|
|
15
|
+
export const JUDGMENT_MODEL_OUTCOME_RECORD_VERSION = 'judgment-model-outcome.v1';
|
|
16
|
+
export const JUDGMENT_MODEL_OUTCOME_ALTERNATIVES = ['measurement_error', 'execution_difference', 'external_change'];
|
|
17
|
+
const VERDICTS = ['supports', 'refutes', 'inconclusive'];
|
|
18
|
+
const EVIDENCE_KINDS = ['artifact', 'query', 'document', 'judgment'];
|
|
19
|
+
const MAX_STATEMENT = 2_000;
|
|
20
|
+
const MAX_RULED_OUT = 1_000;
|
|
21
|
+
const MAX_EVIDENCE = 12;
|
|
22
|
+
const MAX_EVIDENCE_REF = 800;
|
|
23
|
+
const MAX_EVIDENCE_LABEL = 200;
|
|
24
|
+
const MAX_REFUTED_WHEN = 3;
|
|
25
|
+
const MAX_REFUTED_WHEN_TEXT = 120;
|
|
26
|
+
const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
27
|
+
const CONTROL_CHARACTERS = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/u;
|
|
28
|
+
const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
29
|
+
const nonEmpty = (value, max) => typeof value === 'string' && value.trim().length > 0 && value.length <= max;
|
|
30
|
+
function canonicalJson(value) {
|
|
31
|
+
if (Array.isArray(value))
|
|
32
|
+
return `[${value.map(canonicalJson).join(',')}]`;
|
|
33
|
+
if (isRecord(value)) {
|
|
34
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
|
|
35
|
+
}
|
|
36
|
+
return JSON.stringify(value);
|
|
37
|
+
}
|
|
38
|
+
function clip(value, max) {
|
|
39
|
+
const chars = [...value];
|
|
40
|
+
return chars.length <= max ? value : `${chars.slice(0, max - 1).join('')}…`;
|
|
41
|
+
}
|
|
42
|
+
/** Digest of the outcome content. Host-owned id and time are not part of it. */
|
|
43
|
+
export function judgmentModelOutcomeDigest(outcome) {
|
|
44
|
+
return `sha256:${createHash('sha256').update(canonicalJson(outcomeContent(outcome)), 'utf8').digest('hex')}`;
|
|
45
|
+
}
|
|
46
|
+
function outcomeContent(value) {
|
|
47
|
+
return {
|
|
48
|
+
record_version: value.record_version,
|
|
49
|
+
model: { id: value.model.id, revision: value.model.revision },
|
|
50
|
+
verdict: value.verdict,
|
|
51
|
+
prediction: value.prediction,
|
|
52
|
+
observed: value.observed,
|
|
53
|
+
conditions: value.conditions,
|
|
54
|
+
evidence_refs: value.evidence_refs.map(({ kind, ref, label }) => ({ kind, ref, label })),
|
|
55
|
+
...(value.ruled_out === undefined ? {} : {
|
|
56
|
+
ruled_out: Object.fromEntries(JUDGMENT_MODEL_OUTCOME_ALTERNATIVES.map((key) => [key, value.ruled_out[key]])),
|
|
57
|
+
}),
|
|
58
|
+
...(value.judgment_ref === undefined ? {} : { judgment_ref: { frame_digest: value.judgment_ref.frame_digest } }),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function checkText(value, path, max, issue, unsafe) {
|
|
62
|
+
if (!nonEmpty(value, max)) {
|
|
63
|
+
issue('outcome_record_invalid', path, `${path} must be a non-empty string of at most ${max} characters`);
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
if (CONTROL_CHARACTERS.test(value)) {
|
|
67
|
+
issue('outcome_text_unsafe', path, `${path} contains control characters`);
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
if (unsafe(value)) {
|
|
71
|
+
issue('outcome_text_unsafe', path, `${path} contains a credential-like value`);
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
/** Structural checks shared by new input and stored records. Catalog checks are separate. */
|
|
77
|
+
function checkOutcomeShape(value, issue, unsafe) {
|
|
78
|
+
if (value.record_version !== JUDGMENT_MODEL_OUTCOME_RECORD_VERSION) {
|
|
79
|
+
issue('outcome_record_invalid', 'record_version', `record_version must be ${JUDGMENT_MODEL_OUTCOME_RECORD_VERSION}`);
|
|
80
|
+
}
|
|
81
|
+
const model = isRecord(value.model) ? value.model : null;
|
|
82
|
+
if (!model || !nonEmpty(model.id, 1_000) || !nonEmpty(model.revision, 64)) {
|
|
83
|
+
issue('outcome_record_invalid', 'model', 'model needs id and revision');
|
|
84
|
+
}
|
|
85
|
+
if (!VERDICTS.includes(value.verdict)) {
|
|
86
|
+
issue('outcome_record_invalid', 'verdict', 'verdict must be supports, refutes, or inconclusive');
|
|
87
|
+
}
|
|
88
|
+
checkText(value.prediction, 'prediction', MAX_STATEMENT, issue, unsafe);
|
|
89
|
+
checkText(value.observed, 'observed', MAX_STATEMENT, issue, unsafe);
|
|
90
|
+
checkText(value.conditions, 'conditions', MAX_STATEMENT, issue, unsafe);
|
|
91
|
+
const evidence = Array.isArray(value.evidence_refs) ? value.evidence_refs : null;
|
|
92
|
+
if (!evidence || evidence.length === 0) {
|
|
93
|
+
issue('outcome_evidence_missing', 'evidence_refs', 'an outcome needs at least one evidence reference');
|
|
94
|
+
}
|
|
95
|
+
else if (evidence.length > MAX_EVIDENCE) {
|
|
96
|
+
issue('outcome_record_invalid', 'evidence_refs', `evidence_refs allows at most ${MAX_EVIDENCE} entries`);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
evidence.forEach((entry, index) => {
|
|
100
|
+
const path = `evidence_refs[${index}]`;
|
|
101
|
+
if (!isRecord(entry) || !EVIDENCE_KINDS.includes(entry.kind)) {
|
|
102
|
+
issue('outcome_record_invalid', `${path}.kind`, 'evidence kind must be artifact, query, document, or judgment');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
checkText(entry.ref, `${path}.ref`, MAX_EVIDENCE_REF, issue, unsafe);
|
|
106
|
+
checkText(entry.label, `${path}.label`, MAX_EVIDENCE_LABEL, issue, unsafe);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (value.verdict === 'refutes') {
|
|
110
|
+
const ruledOut = isRecord(value.ruled_out) ? value.ruled_out : null;
|
|
111
|
+
for (const key of JUDGMENT_MODEL_OUTCOME_ALTERNATIVES) {
|
|
112
|
+
if (!ruledOut || ruledOut[key] === undefined) {
|
|
113
|
+
issue('outcome_ruled_out_missing', `ruled_out.${key}`, `a refutation must say why ${key} does not explain the gap`);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
checkText(ruledOut[key], `ruled_out.${key}`, MAX_RULED_OUT, issue, unsafe);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const key of Object.keys(ruledOut ?? {})) {
|
|
120
|
+
if (!JUDGMENT_MODEL_OUTCOME_ALTERNATIVES.includes(key)) {
|
|
121
|
+
issue('outcome_record_invalid', `ruled_out.${key}`, 'ruled_out contains an unknown field');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else if (value.ruled_out !== undefined) {
|
|
126
|
+
issue('outcome_ruled_out_unexpected', 'ruled_out', 'ruled_out is only for refutes');
|
|
127
|
+
}
|
|
128
|
+
if (value.judgment_ref !== undefined) {
|
|
129
|
+
const ref = isRecord(value.judgment_ref) ? value.judgment_ref : null;
|
|
130
|
+
if (!ref || typeof ref.frame_digest !== 'string' || !DIGEST_PATTERN.test(ref.frame_digest) || Object.keys(ref).length !== 1) {
|
|
131
|
+
issue('outcome_record_invalid', 'judgment_ref', 'judgment_ref needs only frame_digest as sha256');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Validate a new outcome against the catalog shown on this turn. The model
|
|
137
|
+
* must be a world model in the catalog, at its current revision.
|
|
138
|
+
*/
|
|
139
|
+
export function validateJudgmentModelOutcome(value, catalog, unsafe = () => false) {
|
|
140
|
+
const issues = [];
|
|
141
|
+
const issue = (code, path, message) => issues.push({ code, path, message });
|
|
142
|
+
if (!isRecord(value)) {
|
|
143
|
+
issue('outcome_record_invalid', '$', 'outcome must be an object');
|
|
144
|
+
return { valid: false, issues };
|
|
145
|
+
}
|
|
146
|
+
if (value.catalog_digest !== catalog.digest) {
|
|
147
|
+
issue('outcome_catalog_mismatch', 'catalog_digest', 'catalog_digest does not match the current catalog');
|
|
148
|
+
}
|
|
149
|
+
checkOutcomeShape(value, issue, unsafe);
|
|
150
|
+
const model = isRecord(value.model) ? value.model : {};
|
|
151
|
+
if (typeof model.id === 'string') {
|
|
152
|
+
const item = catalog.items.find((candidate) => candidate.id === model.id);
|
|
153
|
+
if (!item) {
|
|
154
|
+
issue('outcome_model_unknown', 'model.id', `${model.id} is not in the catalog`);
|
|
155
|
+
}
|
|
156
|
+
else if (item.kind !== 'model') {
|
|
157
|
+
issue('outcome_not_model', 'model.id', `${model.id} is a ${item.kind}; outcomes are recorded only for world models`);
|
|
158
|
+
}
|
|
159
|
+
else if (model.revision !== item.revision) {
|
|
160
|
+
issue('outcome_revision_mismatch', 'model.revision', `${model.id} is at revision ${item.revision}, not ${String(model.revision)}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (issues.length > 0)
|
|
164
|
+
return { valid: false, issues };
|
|
165
|
+
return { valid: true, issues: [], outcome: outcomeContent(value) };
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Parse records returned by the host store. A malformed record or a content
|
|
169
|
+
* digest mismatch is an error, never skipped silently.
|
|
170
|
+
*/
|
|
171
|
+
export function parseJudgmentModelOutcomeRecords(values) {
|
|
172
|
+
const records = [];
|
|
173
|
+
for (const [index, value] of values.entries()) {
|
|
174
|
+
if (!isRecord(value) || !nonEmpty(value.id, 200) || !nonEmpty(value.recorded_at, 64)
|
|
175
|
+
|| Number.isNaN(Date.parse(value.recorded_at)) || typeof value.digest !== 'string' || !DIGEST_PATTERN.test(value.digest)) {
|
|
176
|
+
return { ok: false, index, message: 'stored outcome needs id, recorded_at, and digest' };
|
|
177
|
+
}
|
|
178
|
+
const issues = [];
|
|
179
|
+
checkOutcomeShape(value, (code, path, message) => issues.push({ code, path, message }), () => false);
|
|
180
|
+
if (issues.length > 0)
|
|
181
|
+
return { ok: false, index, message: `${issues[0].path}: ${issues[0].message}` };
|
|
182
|
+
const content = outcomeContent(value);
|
|
183
|
+
if (judgmentModelOutcomeDigest(content) !== value.digest) {
|
|
184
|
+
return { ok: false, index, message: 'stored outcome digest does not match its content' };
|
|
185
|
+
}
|
|
186
|
+
records.push({ ...content, id: value.id, recorded_at: value.recorded_at, digest: value.digest });
|
|
187
|
+
}
|
|
188
|
+
return { ok: true, records };
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Count outcomes per world model at its current catalog revision. Outcomes
|
|
192
|
+
* recorded against an older revision are not counted: whether a refutation
|
|
193
|
+
* still holds after a revision is a separate judgment.
|
|
194
|
+
*/
|
|
195
|
+
export function summarizeJudgmentModelOutcomes(records, catalog) {
|
|
196
|
+
const current = new Map(catalog.items.filter((item) => item.kind === 'model').map((item) => [item.id, item.revision]));
|
|
197
|
+
const grouped = new Map();
|
|
198
|
+
for (const record of records) {
|
|
199
|
+
if (current.get(record.model.id) !== record.model.revision)
|
|
200
|
+
continue;
|
|
201
|
+
grouped.set(record.model.id, [...(grouped.get(record.model.id) ?? []), record]);
|
|
202
|
+
}
|
|
203
|
+
const summaries = new Map();
|
|
204
|
+
for (const [id, group] of grouped) {
|
|
205
|
+
const refutations = group
|
|
206
|
+
.filter((record) => record.verdict === 'refutes')
|
|
207
|
+
.sort((left, right) => Date.parse(right.recorded_at) - Date.parse(left.recorded_at) || right.id.localeCompare(left.id));
|
|
208
|
+
summaries.set(id, {
|
|
209
|
+
supports: group.filter((record) => record.verdict === 'supports').length,
|
|
210
|
+
refutes: refutations.length,
|
|
211
|
+
inconclusive: group.filter((record) => record.verdict === 'inconclusive').length,
|
|
212
|
+
refuted_when: refutations.slice(0, MAX_REFUTED_WHEN).map((record) => clip(record.conditions, MAX_REFUTED_WHEN_TEXT)),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return summaries;
|
|
216
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unson/brainbase-mcp",
|
|
3
|
-
"version": "0.24.
|
|
3
|
+
"version": "0.24.1",
|
|
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",
|
|
@@ -427,5 +427,5 @@
|
|
|
427
427
|
"engines": {
|
|
428
428
|
"node": ">=20"
|
|
429
429
|
},
|
|
430
|
-
"gitHead": "
|
|
430
|
+
"gitHead": "3a1e004ecb50844069ccca0ad19e05b57cb63f9d"
|
|
431
431
|
}
|
package/ui/judgment-history.css
CHANGED
package/ui/judgment-history.js
CHANGED
|
@@ -1089,7 +1089,11 @@ function appendNormalFeedbackComposer(doc, record, {
|
|
|
1089
1089
|
function renderNormalRail(doc, rail, row, detail, onClose, options = {}) {
|
|
1090
1090
|
if (!rail) return;
|
|
1091
1091
|
rail.replaceChildren();
|
|
1092
|
-
|
|
1092
|
+
// The home row keeps the canonical raw record so the selected references
|
|
1093
|
+
// remain readable while the detail request is pending or unavailable.
|
|
1094
|
+
// Once the detail readback succeeds, its normalized record takes priority.
|
|
1095
|
+
const homeRecord = row?.mode === 'normal' ? normalizeNormalRecord(row.record) : null;
|
|
1096
|
+
const record = detail ?? homeRecord ?? row;
|
|
1093
1097
|
rail.append(workspaceRailHead(doc, {
|
|
1094
1098
|
kicker: `${NORMAL_ENTRYPOINT_LABELS[record.entrypoint] ?? '入口不明'}の判断`,
|
|
1095
1099
|
title: text(record.judgment?.summary) || '判断内容は未記録',
|