@unson/brainbase-mcp 0.23.5 → 0.23.7

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.
@@ -12,6 +12,14 @@ export interface CanonicalTaskContext extends JsonRecord {
12
12
  /** Opaque host-policy scope, not authority or task input. */
13
13
  storageScope?: string;
14
14
  }
15
+ /** Conditions to review; this metadata is not evidence of completion. */
16
+ export interface CanonicalTaskCompletionContract {
17
+ goal: string;
18
+ criteria: Array<{
19
+ id: string;
20
+ condition: string;
21
+ }>;
22
+ }
15
23
  export interface CanonicalTaskRecord extends JsonRecord {
16
24
  id: string;
17
25
  version: number;
@@ -29,11 +37,13 @@ export interface CanonicalTaskRecord extends JsonRecord {
29
37
  project_codes?: string[];
30
38
  /** What the work is for, in a few words (one per task); null when unlabelled. */
31
39
  purpose_label?: string | null;
40
+ completion_contract?: CanonicalTaskCompletionContract | null;
32
41
  created_at?: string | null;
33
42
  updated_at?: string | null;
34
43
  web_url?: string | null;
35
44
  }
36
45
  export interface CanonicalTaskCreateInput extends JsonRecord {
46
+ completion_contract?: unknown;
37
47
  title: string;
38
48
  description?: string | null;
39
49
  priority?: CanonicalTaskPriority | string | null;
@@ -46,6 +56,7 @@ export interface CanonicalTaskCreateInput extends JsonRecord {
46
56
  purpose_label?: string | null;
47
57
  }
48
58
  export interface CanonicalTaskUpdateInput extends JsonRecord {
59
+ completion_contract?: unknown;
49
60
  title?: string;
50
61
  description?: string | null;
51
62
  priority?: CanonicalTaskPriority | string | null;
@@ -26,6 +26,7 @@ const DEFAULT_LIMIT = 20;
26
26
  const MAX_LIMIT = 100;
27
27
  const RESERVED_IDEMPOTENCY_PREFIXES = ['api:', 'workflow:'];
28
28
  const CREATE_FIELDS = new Set([
29
+ 'completion_contract',
29
30
  'title',
30
31
  'description',
31
32
  'priority',
@@ -38,6 +39,7 @@ const CREATE_FIELDS = new Set([
38
39
  'purpose_label',
39
40
  ]);
40
41
  const MUTABLE_FIELDS = new Set([
42
+ 'completion_contract',
41
43
  'expected_version',
42
44
  'title',
43
45
  'description',
@@ -79,6 +81,39 @@ const MAX_PURPOSE_LABEL_LENGTH = 30;
79
81
  function normalizePurposeLabel(value) {
80
82
  return normalizeString(value, 'purpose_label', { max: MAX_PURPOSE_LABEL_LENGTH });
81
83
  }
84
+ function normalizeCompletionContract(value) {
85
+ if (value == null)
86
+ return null;
87
+ const object = (candidate, keys) => {
88
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)
89
+ || ![Object.prototype, null].includes(Object.getPrototypeOf(candidate))
90
+ || Object.keys(candidate).some((key) => !keys.includes(key))) {
91
+ fail('validation_error', 'completion_contract contains an invalid object');
92
+ }
93
+ return candidate;
94
+ };
95
+ const text = (candidate, field) => {
96
+ if (typeof candidate !== 'string')
97
+ fail('validation_error', `${field} must be a string`);
98
+ return normalizeString(candidate, field, { required: true, max: 2000, multiline: true });
99
+ };
100
+ const contract = object(value, ['goal', 'criteria']);
101
+ const goal = text(contract.goal, 'completion_contract.goal');
102
+ if (!Array.isArray(contract.criteria) || contract.criteria.length < 1 || contract.criteria.length > 50) {
103
+ fail('validation_error', 'completion_contract.criteria must contain 1 to 50 conditions');
104
+ }
105
+ const ids = new Set();
106
+ const criteria = Array.from(contract.criteria, (candidate) => {
107
+ const criterion = object(candidate, ['id', 'condition']);
108
+ if (typeof criterion.id !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u.test(criterion.id)
109
+ || ids.has(criterion.id)) {
110
+ fail('validation_error', 'completion_contract criterion id is invalid or duplicated');
111
+ }
112
+ ids.add(criterion.id);
113
+ return { id: criterion.id, condition: text(criterion.condition, 'completion_contract.criteria.condition') };
114
+ });
115
+ return { goal, criteria };
116
+ }
82
117
  function normalizeIsoDate(value, field) {
83
118
  const text = normalizeString(value, field);
84
119
  if (text == null)
@@ -635,7 +670,9 @@ export class CanonicalTaskService {
635
670
  if (!isCanonicalTaskPriority(priority))
636
671
  fail('validation_error', 'priority is invalid', 400, { field: 'priority' });
637
672
  const purposeLabel = normalizePurposeLabel(input.purpose_label);
673
+ const completionContract = normalizeCompletionContract(input.completion_contract);
638
674
  return {
675
+ ...(completionContract ? { completion_contract: completionContract } : {}),
639
676
  title,
640
677
  description: normalizeString(input.description, 'description', { max: 10000, multiline: true }),
641
678
  priority,
@@ -656,6 +693,8 @@ export class CanonicalTaskService {
656
693
  if (unknownFields.length > 0)
657
694
  fail('validation_error', 'Task update contains unsupported fields', 400, { fields: unknownFields });
658
695
  const patch = {};
696
+ if ('completion_contract' in input)
697
+ patch.completion_contract = normalizeCompletionContract(input.completion_contract);
659
698
  if ('title' in input)
660
699
  patch.title = normalizeString(input.title, 'title', { required: true, max: MAX_TITLE_LENGTH });
661
700
  if ('description' in input)
@@ -857,6 +896,12 @@ export class CanonicalTaskService {
857
896
  response.project_codes = [];
858
897
  if (response.purpose_label === undefined)
859
898
  response.purpose_label = null;
899
+ try {
900
+ response.completion_contract = normalizeCompletionContract(response.completion_contract);
901
+ }
902
+ catch {
903
+ fail('task_store_invalid', 'Stored Task completion contract is invalid', 503);
904
+ }
860
905
  return response;
861
906
  }
862
907
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unson/brainbase-mcp",
3
- "version": "0.23.5",
3
+ "version": "0.23.7",
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",
@@ -423,5 +423,5 @@
423
423
  "engines": {
424
424
  "node": ">=20"
425
425
  },
426
- "gitHead": "98835ea4b24370a96ea81e80716653b416bcd7c3"
426
+ "gitHead": "5dd3fb22d8a4b559ccb0e0ee14a6e9cdac6a4b4a"
427
427
  }
@@ -606,7 +606,9 @@ function aggregateNormalJudgmentHistory(home, { now = new Date(), period = PERIO
606
606
  confirmedOutcomes: null,
607
607
  },
608
608
  visibleCount: 0,
609
- totalCount: home.coverage?.total ?? (home.coverage?.complete === true && home.records.length === 0 ? 0 : null),
609
+ retrievedCount: 0,
610
+ countComplete: false,
611
+ totalCount: home.status === 'available' && home.coverage?.complete === true ? nonNegativeInteger(home.coverage?.total) : null,
610
612
  period: normalizedPeriod(period),
611
613
  query: text(query),
612
614
  status: home.status,
@@ -660,7 +662,10 @@ function aggregateNormalJudgmentHistory(home, { now = new Date(), period = PERIO
660
662
  }
661
663
  }
662
664
  const metricsUnconfirmed = home.status === 'partial' && rows.length === 0;
663
- const judgments = metricsUnconfirmed ? null : rows.length;
665
+ // A page length is a retrieved count, never proof of the whole range.
666
+ const countComplete = home.status === 'available' && home.coverage?.complete === true
667
+ && nonNegativeInteger(home.coverage?.total) !== null && !needle;
668
+ const judgments = countComplete ? home.coverage.total : null;
664
669
  const referenceCount = metricsUnconfirmed || referencesUnconfirmed ? null : references.size;
665
670
  const confirmedOutcomes = metricsUnconfirmed
666
671
  ? null
@@ -680,7 +685,9 @@ function aggregateNormalJudgmentHistory(home, { now = new Date(), period = PERIO
680
685
  confirmedOutcomes,
681
686
  },
682
687
  visibleCount: rows.length,
683
- totalCount: home.coverage?.total ?? (home.coverage?.complete === true && home.records.length === 0 ? 0 : null),
688
+ retrievedCount: rows.length,
689
+ countComplete,
690
+ totalCount: home.status === 'available' && home.coverage?.complete === true ? nonNegativeInteger(home.coverage?.total) : null,
684
691
  };
685
692
  }
686
693
 
@@ -1328,7 +1335,7 @@ function paginationControls(doc, aggregate, onNext) {
1328
1335
  const total = aggregate?.totalCount;
1329
1336
  if (!pagination?.hasNext && total === null) return null;
1330
1337
  const wrap = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-pagination' });
1331
- const count = total === null ? '同じ範囲の件数は未確認' : `同じ範囲の件数: ${total}件`;
1338
+ const count = total === null ? '同じ範囲の件数は未確認' : `${aggregate.query ? '検索前の全件数' : '同じ範囲の件数'}: ${total}件`;
1332
1339
  wrap.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-pagination-count', text: count }));
1333
1340
  if (pagination?.hasNext) {
1334
1341
  wrap.append(workspaceButton(doc, { text: '次のページ', variant: 'quiet', onClick: onNext, attrs: { 'data-action': 'next-page' } }));
@@ -1709,7 +1716,12 @@ export function createJudgmentHistoryUI({
1709
1716
  const stats = aggregate.stats ?? {};
1710
1717
  const metricCount = (value, suffix) => value === null || value === undefined ? '未確認' : `${value}${suffix}`;
1711
1718
  wrapper.append(workspaceMetrics(doc, normalMode() ? [
1712
- { label: '判断件数', value: metricCount(stats.judgments, '件'), note: 'この期間・絞り込みの履歴' },
1719
+ {
1720
+ label: aggregate.query ? '表示対象' : aggregate.countComplete ? '判断件数' : '取得済み',
1721
+ value: metricCount(aggregate.countComplete ? stats.judgments : aggregate.retrievedCount, '件'),
1722
+ note: aggregate.countComplete ? 'この期間・絞り込みの全件数'
1723
+ : aggregate.query ? '取得済み履歴の検索結果・全件数未確認' : '全件数未確認',
1724
+ },
1713
1725
  { label: '使った参照', value: metricCount(stats.references, '件'), note: '当時の記録にある一意の参照' },
1714
1726
  { label: '確認済みの結果', value: metricCount(stats.confirmedOutcomes, '件'), note: '成果が確認済みと記録された判断' },
1715
1727
  ] : [
@@ -1717,8 +1729,8 @@ export function createJudgmentHistoryUI({
1717
1729
  { label: 'そこで使った参照', value: metricCount(stats.references, '件'), note: '代理判断に含まれる一意の参照' },
1718
1730
  { label: '判断回数にすると', value: metricCount(stats.equivalent, '回分相当'), note: '代理判断1件を1回分として表示' },
1719
1731
  ], { ariaLabel: '判断履歴の集計' }));
1720
- const emptyText = state.home?.status === 'partial'
1721
- ? 'この期間に取得できた判断履歴はありません。未接続・一部取得のため0件とは確認できません。'
1732
+ const emptyText = state.home?.status === 'partial' || (normalMode() && !aggregate.countComplete)
1733
+ ? 'この期間に取得できた判断履歴はありません。全件数が未確認のため0件とは確認できません。'
1722
1734
  : 'この期間の判断履歴はありません。';
1723
1735
  wrapper.append(workspaceLedger(doc, {
1724
1736
  className: 'bb-jh-ledger',