@unson/brainbase-mcp 0.36.0 → 0.37.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.
@@ -65,6 +65,7 @@ export interface CanonicalTaskUpdateInput extends JsonRecord {
65
65
  waiting_on?: string | null;
66
66
  review_at?: string | null;
67
67
  source_refs?: unknown[];
68
+ source_refs_append?: unknown[];
68
69
  project_codes?: unknown;
69
70
  purpose_label?: string | null;
70
71
  }
@@ -24,6 +24,7 @@ const MAX_TITLE_LENGTH = 200;
24
24
  const MAX_SEARCH_LENGTH = 200;
25
25
  const DEFAULT_LIMIT = 20;
26
26
  const MAX_LIMIT = 100;
27
+ const MAX_SOURCE_REFS_APPEND_JSON_DEPTH = 128;
27
28
  const RESERVED_IDEMPOTENCY_PREFIXES = ['api:', 'workflow:'];
28
29
  const CREATE_FIELDS = new Set([
29
30
  'completion_contract',
@@ -49,6 +50,7 @@ const MUTABLE_FIELDS = new Set([
49
50
  'waiting_on',
50
51
  'review_at',
51
52
  'source_refs',
53
+ 'source_refs_append',
52
54
  'project_codes',
53
55
  'purpose_label',
54
56
  ]);
@@ -164,6 +166,90 @@ function normalizeSourceRefs(value) {
164
166
  fail('validation_error', 'source_refs must be an array', 400, { field: 'source_refs' });
165
167
  return value;
166
168
  }
169
+ function isPlainJsonObject(value) {
170
+ if (!value || typeof value !== 'object' || Array.isArray(value))
171
+ return false;
172
+ const prototype = Object.getPrototypeOf(value);
173
+ return prototype === Object.prototype || prototype === null;
174
+ }
175
+ function assertJsonValue(value, field, ancestors = new Set(), depth = 0) {
176
+ if (value === null || typeof value === 'string' || typeof value === 'boolean')
177
+ return;
178
+ if (typeof value === 'number') {
179
+ if (Number.isFinite(value))
180
+ return;
181
+ fail('validation_error', `${field} must contain JSON-serializable values`, 400, { field });
182
+ }
183
+ if (typeof value !== 'object') {
184
+ fail('validation_error', `${field} must contain JSON-serializable values`, 400, { field });
185
+ }
186
+ if (depth >= MAX_SOURCE_REFS_APPEND_JSON_DEPTH) {
187
+ fail('validation_error', `${field} exceeds the maximum JSON nesting depth`, 400, { field });
188
+ }
189
+ if (ancestors.has(value)) {
190
+ fail('validation_error', `${field} must contain JSON-serializable values`, 400, { field });
191
+ }
192
+ ancestors.add(value);
193
+ if (Array.isArray(value)) {
194
+ for (const key of Reflect.ownKeys(value)) {
195
+ if (key === 'length')
196
+ continue;
197
+ if (typeof key !== 'string' || !/^(?:0|[1-9]\d*)$/u.test(key)
198
+ || Number(key) >= value.length
199
+ || !Object.prototype.propertyIsEnumerable.call(value, key)) {
200
+ fail('validation_error', `${field} must contain JSON-serializable values`, 400, { field });
201
+ }
202
+ }
203
+ for (let index = 0; index < value.length; index += 1) {
204
+ if (!Object.prototype.hasOwnProperty.call(value, index)) {
205
+ fail('validation_error', `${field} must contain JSON-serializable values`, 400, { field });
206
+ }
207
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
208
+ if (!descriptor || !('value' in descriptor)) {
209
+ fail('validation_error', `${field} must contain JSON-serializable values`, 400, { field });
210
+ }
211
+ assertJsonValue(descriptor.value, `${field}[${index}]`, ancestors, depth + 1);
212
+ }
213
+ }
214
+ else if (isPlainJsonObject(value)) {
215
+ for (const key of Reflect.ownKeys(value)) {
216
+ if (typeof key !== 'string' || !Object.prototype.propertyIsEnumerable.call(value, key)) {
217
+ fail('validation_error', `${field} must contain JSON-serializable values`, 400, { field });
218
+ }
219
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
220
+ if (!descriptor || !('value' in descriptor)) {
221
+ fail('validation_error', `${field} must contain JSON-serializable values`, 400, { field });
222
+ }
223
+ assertJsonValue(descriptor.value, `${field}.${key}`, ancestors, depth + 1);
224
+ }
225
+ }
226
+ else {
227
+ fail('validation_error', `${field} must contain plain JSON objects`, 400, { field });
228
+ }
229
+ ancestors.delete(value);
230
+ }
231
+ function normalizeSourceRefsAppend(value) {
232
+ if (value === undefined)
233
+ return undefined;
234
+ if (!Array.isArray(value)) {
235
+ fail('validation_error', 'source_refs_append must be an array of plain JSON objects', 400, { field: 'source_refs_append' });
236
+ }
237
+ // Validate the array container itself so sparse, symbol-keyed, accessor, or
238
+ // otherwise non-JSON properties cannot bypass the per-reference checks.
239
+ assertJsonValue(value, 'source_refs_append');
240
+ for (let index = 0; index < value.length; index += 1) {
241
+ if (!Object.prototype.hasOwnProperty.call(value, index)) {
242
+ fail('validation_error', 'source_refs_append must be an array of plain JSON objects', 400, { field: 'source_refs_append' });
243
+ }
244
+ const ref = value[index];
245
+ const field = `source_refs_append[${index}]`;
246
+ if (!isPlainJsonObject(ref)) {
247
+ fail('validation_error', 'source_refs_append must be an array of plain JSON objects', 400, { field: 'source_refs_append' });
248
+ }
249
+ assertJsonValue(ref, field);
250
+ }
251
+ return value;
252
+ }
167
253
  function canonicalize(value) {
168
254
  if (Array.isArray(value))
169
255
  return value.map(canonicalize);
@@ -178,6 +264,50 @@ function canonicalize(value) {
178
264
  function fingerprint(value) {
179
265
  return createHash('sha256').update(JSON.stringify(canonicalize(value))).digest('hex');
180
266
  }
267
+ /**
268
+ * Appended source references use code-unit key ordering so the same JSON
269
+ * object has one fingerprint across runtimes and locale settings. Keep the
270
+ * existing canonicalizer above unchanged because its fingerprints are already
271
+ * persisted for older create/update/transition operations.
272
+ */
273
+ function canonicalizeSourceRefsAppend(value) {
274
+ if (Array.isArray(value))
275
+ return value.map(canonicalizeSourceRefsAppend);
276
+ if (value && typeof value === 'object') {
277
+ return Object.fromEntries(Object.entries(value)
278
+ .filter(([, entry]) => entry !== undefined)
279
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
280
+ .map(([key, entry]) => [key, canonicalizeSourceRefsAppend(entry)]));
281
+ }
282
+ return value;
283
+ }
284
+ function sourceRefsAppendFingerprint(value) {
285
+ return createHash('sha256').update(JSON.stringify(canonicalizeSourceRefsAppend(value))).digest('hex');
286
+ }
287
+ function mergeSourceRefs(existing, additions) {
288
+ if (existing != null && !Array.isArray(existing)) {
289
+ fail('validation_error', 'source_refs must be an array', 400, { field: 'source_refs' });
290
+ }
291
+ const existingRefs = Array.isArray(existing) ? existing : [];
292
+ const merged = [...existingRefs];
293
+ const known = new Set();
294
+ for (const ref of existingRefs) {
295
+ try {
296
+ known.add(sourceRefsAppendFingerprint(ref));
297
+ }
298
+ catch {
299
+ // Preserve legacy references exactly, but do not block valid additions.
300
+ }
301
+ }
302
+ for (const addition of additions) {
303
+ const key = sourceRefsAppendFingerprint(addition);
304
+ if (known.has(key))
305
+ continue;
306
+ known.add(key);
307
+ merged.push(addition);
308
+ }
309
+ return merged;
310
+ }
181
311
  function validHttpUrl(value) {
182
312
  if (typeof value !== 'string' || !value.trim())
183
313
  return null;
@@ -570,7 +700,11 @@ export class CanonicalTaskService {
570
700
  // Keep this fingerprint independent of the wall clock. A restart may
571
701
  // reconstruct the same command at a different instant, while the marker
572
702
  // must still identify the already-applied mutation.
573
- const operationFingerprint = fingerprint({
703
+ const fingerprintOperation = input.action === 'update'
704
+ && Object.prototype.hasOwnProperty.call(input.patch, 'source_refs_append')
705
+ ? sourceRefsAppendFingerprint
706
+ : fingerprint;
707
+ const operationFingerprint = fingerprintOperation({
574
708
  action: input.action,
575
709
  taskId,
576
710
  expectedVersion,
@@ -620,8 +754,17 @@ export class CanonicalTaskService {
620
754
  to: input.transitionStatus,
621
755
  }, { currentTask: this.normalizeResponse(latest) });
622
756
  }
757
+ const persistedPatch = { ...patch };
758
+ if (input.action === 'update' && 'source_refs_append' in input.patch) {
759
+ const additions = input.patch.source_refs_append;
760
+ if (!Array.isArray(additions)) {
761
+ fail('validation_error', 'source_refs_append must be an array of plain JSON objects', 400, { field: 'source_refs_append' });
762
+ }
763
+ delete persistedPatch.source_refs_append;
764
+ persistedPatch.source_refs = mergeSourceRefs(latest.source_refs, additions);
765
+ }
623
766
  const updated = await this.read(() => this.repository.update(taskId, {
624
- ...patch,
767
+ ...persistedPatch,
625
768
  _last_operation_key: operation,
626
769
  _last_operation_fingerprint: operationFingerprint,
627
770
  }, ...this.scopeArgs(input.context)));
@@ -689,6 +832,13 @@ export class CanonicalTaskService {
689
832
  normalizeUpdateInput(input) {
690
833
  if (!input || typeof input !== 'object')
691
834
  fail('validation_error', 'Task update is required');
835
+ const hasSourceRefsAppend = Object.prototype.hasOwnProperty.call(input, 'source_refs_append');
836
+ if ('source_refs_append' in input) {
837
+ const descriptor = Object.getOwnPropertyDescriptor(input, 'source_refs_append');
838
+ if (!hasSourceRefsAppend || !descriptor || !('value' in descriptor)) {
839
+ fail('validation_error', 'source_refs_append must be an own data property', 400, { field: 'source_refs_append' });
840
+ }
841
+ }
692
842
  const unknownFields = Object.keys(input).filter((field) => !MUTABLE_FIELDS.has(field));
693
843
  if (unknownFields.length > 0)
694
844
  fail('validation_error', 'Task update contains unsupported fields', 400, { fields: unknownFields });
@@ -713,8 +863,19 @@ export class CanonicalTaskService {
713
863
  patch.waiting_on = normalizeString(input.waiting_on, 'waiting_on', { max: 500 });
714
864
  if ('review_at' in input)
715
865
  patch.review_at = normalizeIsoDate(input.review_at, 'review_at');
866
+ if ('source_refs' in input && hasSourceRefsAppend) {
867
+ fail('validation_error', 'source_refs and source_refs_append cannot be combined', 400, {
868
+ fields: ['source_refs', 'source_refs_append'],
869
+ });
870
+ }
716
871
  if ('source_refs' in input)
717
872
  patch.source_refs = normalizeSourceRefs(input.source_refs) ?? [];
873
+ if (hasSourceRefsAppend) {
874
+ const descriptor = Object.getOwnPropertyDescriptor(input, 'source_refs_append');
875
+ // The descriptor was checked above before any value access. Reuse its
876
+ // data value so a getter cannot run between validation and normalization.
877
+ patch.source_refs_append = normalizeSourceRefsAppend(descriptor?.value) ?? [];
878
+ }
718
879
  if ('project_codes' in input)
719
880
  patch.project_codes = normalizeProjectCodes(input.project_codes) ?? [];
720
881
  if ('purpose_label' in input)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unson/brainbase-mcp",
3
- "version": "0.36.0",
3
+ "version": "0.37.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",
@@ -479,5 +479,5 @@
479
479
  "engines": {
480
480
  "node": ">=20"
481
481
  },
482
- "gitHead": "f7389b6d15e12a3fb7312d305be6f6f9660e8c79"
482
+ "gitHead": "55c2d213cafd20800d51c9669f4d7c8d727f7968"
483
483
  }