@unson/brainbase-mcp 0.21.2 → 0.22.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.
Files changed (32) hide show
  1. package/dist/local-web-fetch-bridge.js +1 -1
  2. package/dist/local-web-host.d.ts +59 -0
  3. package/dist/local-web-host.js +171 -4
  4. package/dist/meeting-minutes-http.d.ts +22 -0
  5. package/dist/meeting-minutes-http.js +252 -0
  6. package/dist/meeting-minutes-lineage-adapters.d.ts +157 -0
  7. package/dist/meeting-minutes-lineage-adapters.js +410 -0
  8. package/dist/meeting-minutes-lineage-http.d.ts +20 -0
  9. package/dist/meeting-minutes-lineage-http.js +221 -0
  10. package/dist/meeting-minutes-lineage-lifecycle.d.ts +58 -0
  11. package/dist/meeting-minutes-lineage-lifecycle.js +146 -0
  12. package/dist/meeting-minutes-lineage.d.ts +307 -0
  13. package/dist/meeting-minutes-lineage.js +1030 -0
  14. package/dist/meeting-minutes-storage-controller.d.ts +182 -0
  15. package/dist/meeting-minutes-storage-controller.js +366 -0
  16. package/dist/meeting-minutes-storage-http.d.ts +25 -0
  17. package/dist/meeting-minutes-storage-http.js +312 -0
  18. package/dist/meeting-minutes-storage.d.ts +169 -0
  19. package/dist/meeting-minutes-storage.js +402 -0
  20. package/dist/meeting-minutes.d.ts +223 -0
  21. package/dist/meeting-minutes.js +1172 -0
  22. package/package.json +46 -3
  23. package/ui/brainbase-tokens.css +35 -35
  24. package/ui/local-web-shell.css +20 -17
  25. package/ui/local-web-shell.js +34 -1
  26. package/ui/meeting-minutes-lineage-http.js +247 -0
  27. package/ui/meeting-minutes-lineage.css +177 -0
  28. package/ui/meeting-minutes-lineage.js +485 -0
  29. package/ui/meeting-minutes-storage.css +170 -0
  30. package/ui/meeting-minutes-storage.js +388 -0
  31. package/ui/meeting-minutes.css +215 -0
  32. package/ui/meeting-minutes.js +622 -0
@@ -0,0 +1,1030 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { initializePersonalOs, mutatePersonalOsWithSidecar, readPersonalOsSidecar } from './ssot.js';
3
+ /**
4
+ * The minutes lineage ledger stores references and lifecycle events only.
5
+ * Meeting body content, judgment state, Task state, and execution receipts
6
+ * remain owned by their existing providers.
7
+ */
8
+ export const MEETING_MINUTES_LINEAGE_CONTRACT_VERSION = '0.1.0';
9
+ export const MEETING_MINUTES_LINEAGE_CATALOG_VERSION = 1;
10
+ export const MEETING_MINUTES_LINEAGE_SIDECAR = 'evidence/meeting-minutes-lineage.json';
11
+ export class MeetingMinutesLineageError extends Error {
12
+ code;
13
+ constructor(code, message) {
14
+ super(message);
15
+ this.name = 'MeetingMinutesLineageError';
16
+ this.code = code;
17
+ }
18
+ }
19
+ const EMPTY_LEDGER = {
20
+ version: MEETING_MINUTES_LINEAGE_CATALOG_VERSION,
21
+ candidates: [],
22
+ confirmations: [],
23
+ adoptions: [],
24
+ executions: [],
25
+ results: [],
26
+ corrections: [],
27
+ };
28
+ export function createMeetingMinutesLineageStore(options) {
29
+ if (!options || !isNonEmptyString(options.dataDir))
30
+ throw new MeetingMinutesLineageError('invalid_input', 'dataDir is required');
31
+ if (!options.minutes || typeof options.minutes.readExact !== 'function') {
32
+ throw new MeetingMinutesLineageError('invalid_input', 'minutes.readExact is required');
33
+ }
34
+ return new GraphMeetingMinutesLineageStore(options);
35
+ }
36
+ class GraphMeetingMinutesLineageStore {
37
+ options;
38
+ now;
39
+ constructor(options) {
40
+ this.options = options;
41
+ this.now = options.now ?? (() => new Date().toISOString());
42
+ }
43
+ async createCandidate(request) {
44
+ const access = assertAccess(request.access);
45
+ const input = normalizeCandidateRequest(request);
46
+ await this.readExactMinutes(input.evidence, access);
47
+ const requestDigest = digestJson({
48
+ id: input.id,
49
+ idempotencyKey: input.idempotencyKey,
50
+ actor: input.actor,
51
+ principal: access.principal,
52
+ evidence: input.evidence,
53
+ kind: input.kind,
54
+ proposal: input.proposal,
55
+ epistemicStatus: input.epistemicStatus,
56
+ });
57
+ const candidate = {
58
+ version: MEETING_MINUTES_LINEAGE_CATALOG_VERSION,
59
+ id: input.id,
60
+ idempotencyKey: input.idempotencyKey,
61
+ requestDigest,
62
+ actor: cloneJson(input.actor),
63
+ evidence: cloneJson(input.evidence),
64
+ evidenceDigest: digestJson(input.evidence),
65
+ kind: input.kind,
66
+ proposal: cloneJson(input.proposal),
67
+ proposalDigest: digestJson(input.proposal),
68
+ epistemicStatus: input.epistemicStatus,
69
+ createdAt: this.now(),
70
+ digest: '',
71
+ };
72
+ const complete = withDigest(candidate);
73
+ let committed;
74
+ await this.mutateLedger((ledger) => {
75
+ const byId = ledger.candidates.find((item) => item.id === complete.id);
76
+ const byKey = ledger.candidates.find((item) => item.idempotencyKey === complete.idempotencyKey);
77
+ if (byId || byKey) {
78
+ const existing = byId ?? byKey;
79
+ if (existing.requestDigest !== complete.requestDigest) {
80
+ throw new MeetingMinutesLineageError('revision_conflict', `Candidate ${existing.id} already exists with different content`);
81
+ }
82
+ committed = cloneJson(existing);
83
+ return { ledger, result: committed };
84
+ }
85
+ committed = complete;
86
+ return { ledger: { ...ledger, candidates: [...ledger.candidates, complete] }, result: complete };
87
+ });
88
+ if (!committed)
89
+ throw new MeetingMinutesLineageError('readback_mismatch', `Candidate ${input.id} was not committed`);
90
+ const readback = await this.readCandidate(committed.id, access);
91
+ if (!readback || readback.digest !== committed.digest) {
92
+ throw new MeetingMinutesLineageError('readback_mismatch', `Candidate ${input.id} was not readable after commit`);
93
+ }
94
+ return readback;
95
+ }
96
+ async readCandidate(id, access) {
97
+ const context = assertAccess(access);
98
+ const candidateId = requireId(id, 'candidate id');
99
+ const ledger = await this.readLedger();
100
+ const candidate = ledger.candidates.find((item) => item.id === candidateId);
101
+ if (!candidate)
102
+ return null;
103
+ assertDigest(candidate, 'candidate');
104
+ await this.readExactMinutes(candidate.evidence, context);
105
+ return cloneJson(candidate);
106
+ }
107
+ async confirmCandidate(request) {
108
+ const access = assertAccess(request.access);
109
+ const input = normalizeConfirmationRequest(request);
110
+ const candidate = await this.requireCandidate(input.candidateId, access);
111
+ if (input.evidenceDigest !== candidate.evidenceDigest) {
112
+ throw new MeetingMinutesLineageError('integrity_mismatch', `Candidate ${candidate.id} evidence digest does not match`);
113
+ }
114
+ const requestDigest = digestJson({
115
+ id: input.id,
116
+ idempotencyKey: input.idempotencyKey,
117
+ candidateId: input.candidateId,
118
+ candidateDigest: candidate.digest,
119
+ actor: input.actor,
120
+ principal: access.principal,
121
+ evidenceDigest: input.evidenceDigest,
122
+ });
123
+ const confirmation = {
124
+ version: MEETING_MINUTES_LINEAGE_CATALOG_VERSION,
125
+ id: input.id,
126
+ idempotencyKey: input.idempotencyKey,
127
+ requestDigest,
128
+ candidateId: candidate.id,
129
+ candidateDigest: candidate.digest,
130
+ actor: cloneJson(input.actor),
131
+ evidenceDigest: input.evidenceDigest,
132
+ confirmedAt: this.now(),
133
+ digest: '',
134
+ };
135
+ const complete = withDigest(confirmation);
136
+ let committed;
137
+ await this.mutateLedger((ledger) => {
138
+ const existing = ledger.confirmations.find((item) => item.idempotencyKey === complete.idempotencyKey || item.id === complete.id);
139
+ if (existing) {
140
+ if (existing.requestDigest !== complete.requestDigest)
141
+ throw new MeetingMinutesLineageError('revision_conflict', `Confirmation ${existing.id} already exists with different content`);
142
+ committed = cloneJson(existing);
143
+ return { ledger, result: committed };
144
+ }
145
+ const existingForCandidate = ledger.confirmations.find((item) => item.candidateId === complete.candidateId);
146
+ if (existingForCandidate) {
147
+ if (existingForCandidate.candidateDigest !== complete.candidateDigest)
148
+ throw new MeetingMinutesLineageError('integrity_mismatch', `Candidate ${candidate.id} changed before confirmation`);
149
+ committed = cloneJson(existingForCandidate);
150
+ return { ledger, result: committed };
151
+ }
152
+ committed = complete;
153
+ return { ledger: { ...ledger, confirmations: [...ledger.confirmations, complete] }, result: complete };
154
+ });
155
+ if (!committed)
156
+ throw new MeetingMinutesLineageError('readback_mismatch', `Confirmation ${input.id} was not committed`);
157
+ return committed;
158
+ }
159
+ async adoptJudgment(request) {
160
+ return this.adopt(request, 'judgment');
161
+ }
162
+ async adoptTask(request) {
163
+ return this.adopt(request, 'task');
164
+ }
165
+ async adopt(request, kind) {
166
+ const access = assertAccess(request.access);
167
+ const input = normalizeAdoptionRequest(request);
168
+ const candidate = await this.requireCandidate(input.candidateId, access);
169
+ if (candidate.kind !== kind)
170
+ throw new MeetingMinutesLineageError('invalid_input', `Candidate ${candidate.id} is not a ${kind} candidate`);
171
+ const confirmation = await this.readConfirmation(candidate.id, access);
172
+ if (!confirmation)
173
+ throw new MeetingMinutesLineageError('invalid_input', `Candidate ${candidate.id} requires explicit confirmation before adoption`);
174
+ const requestDigest = digestJson({
175
+ id: input.id,
176
+ idempotencyKey: input.idempotencyKey,
177
+ candidateId: candidate.id,
178
+ candidateDigest: candidate.digest,
179
+ kind,
180
+ actor: input.actor,
181
+ principal: access.principal,
182
+ });
183
+ const ledger = await this.readLedger();
184
+ const existingByKey = ledger.adoptions.find((item) => item.idempotencyKey === input.idempotencyKey);
185
+ if (existingByKey) {
186
+ if (existingByKey.requestDigest !== requestDigest)
187
+ throw new MeetingMinutesLineageError('revision_conflict', `Adoption key ${input.idempotencyKey} was used with different content`);
188
+ await this.assertTargetReadable(existingByKey.target, access);
189
+ return cloneJson(existingByKey);
190
+ }
191
+ // A candidate is the unit of adoption idempotency. The same minutes
192
+ // version may intentionally produce several candidates and targets, but
193
+ // retrying one candidate with a new transport key must not call the
194
+ // provider a second time.
195
+ const existingForCandidate = ledger.adoptions.find((item) => item.candidateId === candidate.id);
196
+ if (existingForCandidate) {
197
+ if (existingForCandidate.kind !== kind)
198
+ throw new MeetingMinutesLineageError('revision_conflict', `Candidate ${candidate.id} already has a different adoption kind`);
199
+ await this.assertTargetReadable(existingForCandidate.target, access);
200
+ return cloneJson(existingForCandidate);
201
+ }
202
+ let adopted;
203
+ try {
204
+ if (kind === 'judgment') {
205
+ const provider = this.options.judgment;
206
+ if (!provider)
207
+ throw new MeetingMinutesLineageError('provider_unavailable', 'judgment adoption provider is not connected');
208
+ adopted = await provider.adopt({ candidate, idempotencyKey: providerIdempotencyKey(kind, candidate), actor: input.actor, access });
209
+ }
210
+ else {
211
+ const provider = this.options.task;
212
+ if (!provider)
213
+ throw new MeetingMinutesLineageError('provider_unavailable', 'task adoption provider is not connected');
214
+ adopted = await provider.create({ candidate, idempotencyKey: providerIdempotencyKey(kind, candidate), actor: input.actor, access });
215
+ }
216
+ }
217
+ catch (error) {
218
+ throw mapProviderError(error, `${kind} adoption provider is unavailable`, 'target_unavailable');
219
+ }
220
+ const target = normalizeTarget(adopted?.target);
221
+ if (target.kind !== kind)
222
+ throw new MeetingMinutesLineageError('integrity_mismatch', `${kind} adoption returned a different target kind`);
223
+ const adoptionReference = normalizeAdoptionReference(adopted?.adoption, kind);
224
+ await this.assertTargetReadable(target, access);
225
+ const record = {
226
+ version: MEETING_MINUTES_LINEAGE_CATALOG_VERSION,
227
+ id: input.id,
228
+ idempotencyKey: input.idempotencyKey,
229
+ requestDigest,
230
+ candidateId: candidate.id,
231
+ candidateDigest: candidate.digest,
232
+ kind,
233
+ target,
234
+ adoption: adoptionReference,
235
+ actor: cloneJson(input.actor),
236
+ adoptedAt: this.now(),
237
+ digest: '',
238
+ };
239
+ const complete = withDigest(record);
240
+ let committed;
241
+ await this.mutateLedger((nextLedger) => {
242
+ const sameKey = nextLedger.adoptions.find((item) => item.idempotencyKey === complete.idempotencyKey);
243
+ if (sameKey) {
244
+ if (sameKey.requestDigest !== complete.requestDigest)
245
+ throw new MeetingMinutesLineageError('revision_conflict', `Adoption key ${input.idempotencyKey} was used with different content`);
246
+ committed = cloneJson(sameKey);
247
+ return { ledger: nextLedger, result: committed };
248
+ }
249
+ const sameCandidate = nextLedger.adoptions.find((item) => item.candidateId === complete.candidateId);
250
+ if (sameCandidate) {
251
+ if (sameCandidate.target.kind !== complete.target.kind || !sameTarget(sameCandidate.target, complete.target)) {
252
+ throw new MeetingMinutesLineageError('revision_conflict', `Candidate ${candidate.id} already has a different adoption`);
253
+ }
254
+ committed = cloneJson(sameCandidate);
255
+ return { ledger: nextLedger, result: committed };
256
+ }
257
+ committed = complete;
258
+ return { ledger: { ...nextLedger, adoptions: [...nextLedger.adoptions, complete] }, result: complete };
259
+ });
260
+ if (!committed)
261
+ throw new MeetingMinutesLineageError('readback_mismatch', `Adoption ${input.id} was not committed`);
262
+ const readback = await this.readAdoption(committed.id, access);
263
+ if (!readback || readback.digest !== committed.digest)
264
+ throw new MeetingMinutesLineageError('readback_mismatch', `Adoption ${input.id} was not readable after commit`);
265
+ void confirmation;
266
+ return readback;
267
+ }
268
+ async recordExecution(request) {
269
+ const access = assertAccess(request.access);
270
+ const input = normalizeExecutionRequest(request);
271
+ const candidate = await this.requireCandidate(input.candidateId, access);
272
+ const adoption = await this.requireAdoption(candidate.id, access);
273
+ if (!this.options.receipt)
274
+ throw new MeetingMinutesLineageError('provider_unavailable', 'Execution receipt provider is not connected');
275
+ let receipt;
276
+ try {
277
+ receipt = await this.options.receipt.read(input.receiptRef, access);
278
+ }
279
+ catch (error) {
280
+ throw mapProviderError(error, 'Execution receipt provider is unavailable', 'target_unavailable');
281
+ }
282
+ if (!receipt)
283
+ throw new MeetingMinutesLineageError('target_unavailable', `Execution receipt ${input.receiptRef.id} is unavailable`);
284
+ const receiptReference = normalizeReceiptReference(receipt.reference);
285
+ if (!sameJson(receiptReference, input.receiptRef))
286
+ throw new MeetingMinutesLineageError('integrity_mismatch', `Execution receipt ${input.receiptRef.id} changed`);
287
+ const receiptTarget = normalizeTarget(receipt.target);
288
+ if (!sameTarget(receiptTarget, adoption.target))
289
+ throw new MeetingMinutesLineageError('integrity_mismatch', `Execution receipt ${input.receiptRef.id} used a different target version`);
290
+ const runId = requireId(receipt.runId, 'receipt runId');
291
+ const requestDigest = digestJson({
292
+ id: input.id,
293
+ idempotencyKey: input.idempotencyKey,
294
+ candidateId: input.candidateId,
295
+ adoptionId: adoption.id,
296
+ target: adoption.target,
297
+ receiptRef: input.receiptRef,
298
+ runId,
299
+ actor: input.actor,
300
+ principal: access.principal,
301
+ });
302
+ const record = {
303
+ version: MEETING_MINUTES_LINEAGE_CATALOG_VERSION,
304
+ id: input.id,
305
+ idempotencyKey: input.idempotencyKey,
306
+ requestDigest,
307
+ candidateId: candidate.id,
308
+ adoptionId: adoption.id,
309
+ target: cloneJson(adoption.target),
310
+ receiptRef: receiptReference,
311
+ runId,
312
+ actor: cloneJson(input.actor),
313
+ executedAt: this.now(),
314
+ digest: '',
315
+ };
316
+ const complete = withDigest(record);
317
+ let committed;
318
+ await this.mutateLedger((ledger) => {
319
+ const existing = ledger.executions.find((item) => item.idempotencyKey === complete.idempotencyKey || item.id === complete.id);
320
+ if (existing) {
321
+ if (existing.requestDigest !== complete.requestDigest)
322
+ throw new MeetingMinutesLineageError('revision_conflict', `Execution ${existing.id} already exists with different content`);
323
+ committed = cloneJson(existing);
324
+ return { ledger, result: committed };
325
+ }
326
+ committed = complete;
327
+ return { ledger: { ...ledger, executions: [...ledger.executions, complete] }, result: complete };
328
+ });
329
+ if (!committed)
330
+ throw new MeetingMinutesLineageError('readback_mismatch', `Execution ${input.id} was not committed`);
331
+ const readback = await this.readExecution(committed.id, access);
332
+ if (!readback || readback.digest !== committed.digest)
333
+ throw new MeetingMinutesLineageError('readback_mismatch', `Execution ${input.id} was not readable after commit`);
334
+ return readback;
335
+ }
336
+ async acceptResult(request) {
337
+ const access = assertAccess(request.access);
338
+ const input = normalizeResultRequest(request);
339
+ const execution = await this.requireExecution(input.executionId, access);
340
+ if (!this.options.result)
341
+ throw new MeetingMinutesLineageError('provider_unavailable', 'Result provider is not connected');
342
+ let result;
343
+ try {
344
+ result = await this.options.result.read(input.resultRef, access);
345
+ }
346
+ catch (error) {
347
+ throw mapProviderError(error, 'Result provider is unavailable', 'target_unavailable');
348
+ }
349
+ if (!result)
350
+ throw new MeetingMinutesLineageError('target_unavailable', `Result ${input.resultRef.id} is unavailable`);
351
+ const resultReference = normalizeResultReference(result.reference);
352
+ if (!sameJson(resultReference, input.resultRef))
353
+ throw new MeetingMinutesLineageError('integrity_mismatch', `Result ${input.resultRef.id} changed`);
354
+ const resultTarget = normalizeTarget(result.target);
355
+ if (!sameTarget(resultTarget, execution.target))
356
+ throw new MeetingMinutesLineageError('integrity_mismatch', `Result ${input.resultRef.id} points to a different target version`);
357
+ const requestDigest = digestJson({
358
+ id: input.id,
359
+ idempotencyKey: input.idempotencyKey,
360
+ executionId: input.executionId,
361
+ resultRef: input.resultRef,
362
+ target: execution.target,
363
+ actor: input.actor,
364
+ principal: access.principal,
365
+ });
366
+ const record = {
367
+ version: MEETING_MINUTES_LINEAGE_CATALOG_VERSION,
368
+ id: input.id,
369
+ idempotencyKey: input.idempotencyKey,
370
+ requestDigest,
371
+ executionId: execution.id,
372
+ resultRef: resultReference,
373
+ target: cloneJson(execution.target),
374
+ actor: cloneJson(input.actor),
375
+ acceptedAt: this.now(),
376
+ digest: '',
377
+ };
378
+ const complete = withDigest(record);
379
+ let committed;
380
+ await this.mutateLedger((ledger) => {
381
+ const existing = ledger.results.find((item) => item.idempotencyKey === complete.idempotencyKey || item.id === complete.id);
382
+ if (existing) {
383
+ if (existing.requestDigest !== complete.requestDigest)
384
+ throw new MeetingMinutesLineageError('revision_conflict', `Result ${existing.id} already exists with different content`);
385
+ committed = cloneJson(existing);
386
+ return { ledger, result: committed };
387
+ }
388
+ const existingExecution = ledger.results.find((item) => item.executionId === complete.executionId);
389
+ if (existingExecution) {
390
+ if (!sameJson(existingExecution.resultRef, complete.resultRef))
391
+ throw new MeetingMinutesLineageError('revision_conflict', `Execution ${complete.executionId} already has a different accepted result`);
392
+ committed = cloneJson(existingExecution);
393
+ return { ledger, result: committed };
394
+ }
395
+ committed = complete;
396
+ return { ledger: { ...ledger, results: [...ledger.results, complete] }, result: complete };
397
+ });
398
+ if (!committed)
399
+ throw new MeetingMinutesLineageError('readback_mismatch', `Result ${input.id} was not committed`);
400
+ const readback = await this.readResult(committed.id, access);
401
+ if (!readback || readback.digest !== committed.digest)
402
+ throw new MeetingMinutesLineageError('readback_mismatch', `Result ${input.id} was not readable after commit`);
403
+ return readback;
404
+ }
405
+ async markCorrection(request) {
406
+ const access = assertAccess(request.access);
407
+ const input = normalizeCorrectionRequest(request);
408
+ await this.readExactMinutes(input.previousVersion, access);
409
+ await this.readExactMinutes(input.replacementVersion, access);
410
+ if (sameMinutesReference(input.previousVersion, input.replacementVersion)) {
411
+ throw new MeetingMinutesLineageError('invalid_input', 'A correction must point to a different exact minutes version');
412
+ }
413
+ const ledger = await this.readLedger();
414
+ const affectedCandidateIds = ledger.candidates
415
+ .filter((candidate) => sameMinutesReference(candidate.evidence, input.previousVersion))
416
+ .map((candidate) => candidate.id);
417
+ const requestDigest = digestJson({
418
+ id: input.id,
419
+ idempotencyKey: input.idempotencyKey,
420
+ previousVersion: input.previousVersion,
421
+ replacementVersion: input.replacementVersion,
422
+ affectedCandidateIds,
423
+ actor: input.actor,
424
+ principal: access.principal,
425
+ reason: input.reason,
426
+ });
427
+ const record = {
428
+ version: MEETING_MINUTES_LINEAGE_CATALOG_VERSION,
429
+ id: input.id,
430
+ idempotencyKey: input.idempotencyKey,
431
+ requestDigest,
432
+ previousVersion: cloneJson(input.previousVersion),
433
+ replacementVersion: cloneJson(input.replacementVersion),
434
+ affectedCandidateIds: [...affectedCandidateIds],
435
+ actor: cloneJson(input.actor),
436
+ reason: input.reason,
437
+ correctedAt: this.now(),
438
+ digest: '',
439
+ };
440
+ const complete = withDigest(record);
441
+ let committed;
442
+ await this.mutateLedger((nextLedger) => {
443
+ const existing = nextLedger.corrections.find((item) => item.idempotencyKey === complete.idempotencyKey || item.id === complete.id);
444
+ if (existing) {
445
+ if (existing.requestDigest !== complete.requestDigest)
446
+ throw new MeetingMinutesLineageError('revision_conflict', `Correction ${existing.id} already exists with different content`);
447
+ committed = cloneJson(existing);
448
+ return { ledger: nextLedger, result: committed };
449
+ }
450
+ committed = complete;
451
+ return { ledger: { ...nextLedger, corrections: [...nextLedger.corrections, complete] }, result: complete };
452
+ });
453
+ if (!committed)
454
+ throw new MeetingMinutesLineageError('readback_mismatch', `Correction ${input.id} was not committed`);
455
+ return committed;
456
+ }
457
+ async readByMinutesVersion(reference, access) {
458
+ const context = assertAccess(access);
459
+ const evidence = normalizeMinutesReference(reference);
460
+ await this.readExactMinutes(evidence, context);
461
+ const ledger = await this.readLedger();
462
+ const candidates = ledger.candidates.filter((candidate) => sameMinutesReference(candidate.evidence, evidence));
463
+ const views = await Promise.all(candidates.map((candidate) => this.projectCandidate(candidate, ledger, context)));
464
+ const corrections = ledger.corrections.filter((correction) => sameMinutesReference(correction.previousVersion, evidence) || sameMinutesReference(correction.replacementVersion, evidence));
465
+ return { evidence: cloneJson(evidence), candidates: views, corrections: cloneJson(corrections) };
466
+ }
467
+ async readByTarget(target, access) {
468
+ const context = assertAccess(access);
469
+ const normalizedTarget = normalizeTarget(target);
470
+ await this.assertTargetReadable(normalizedTarget, context);
471
+ const ledger = await this.readLedger();
472
+ const adoptions = ledger.adoptions.filter((item) => sameTarget(item.target, normalizedTarget));
473
+ const candidates = [];
474
+ const sourceStatuses = {};
475
+ for (const adoption of adoptions) {
476
+ const candidate = ledger.candidates.find((item) => item.id === adoption.candidateId);
477
+ if (!candidate)
478
+ throw new MeetingMinutesLineageError('integrity_mismatch', `Adoption ${adoption.id} points to missing candidate`);
479
+ try {
480
+ await this.readExactMinutes(candidate.evidence, context);
481
+ sourceStatuses[candidate.id] = { status: 'available' };
482
+ }
483
+ catch (error) {
484
+ // Reverse lookup must not become a side channel after a source ACL is
485
+ // revoked. The candidate contains the proposal, exact source ids,
486
+ // and provider locator, so returning it alongside a denied status
487
+ // would disclose the very records that the source denied.
488
+ if (error instanceof MeetingMinutesLineageError && error.code === 'authorization_denied') {
489
+ throw new MeetingMinutesLineageError('authorization_denied', 'Meeting minutes source is not available for this principal');
490
+ }
491
+ sourceStatuses[candidate.id] = statusFromError(error);
492
+ }
493
+ candidates.push(await this.projectCandidate(candidate, ledger, context, { targetAlreadyRead: true }));
494
+ }
495
+ return { target: normalizedTarget, adoptions: cloneJson(adoptions), candidates, sourceStatuses };
496
+ }
497
+ async projectCandidate(candidate, ledger, access, options = {}) {
498
+ assertDigest(candidate, 'candidate');
499
+ const confirmation = ledger.confirmations.find((item) => item.candidateId === candidate.id);
500
+ if (confirmation)
501
+ assertDigest(confirmation, 'confirmation');
502
+ const adoption = ledger.adoptions.find((item) => item.candidateId === candidate.id);
503
+ if (adoption)
504
+ assertDigest(adoption, 'adoption');
505
+ const storedExecution = adoption ? ledger.executions.find((item) => item.adoptionId === adoption.id) : undefined;
506
+ if (storedExecution)
507
+ assertDigest(storedExecution, 'execution');
508
+ let execution;
509
+ let executionStatus = 'unrecorded';
510
+ if (storedExecution) {
511
+ try {
512
+ execution = (await this.readExecution(storedExecution.id, access)) ?? undefined;
513
+ if (execution)
514
+ executionStatus = 'actual';
515
+ }
516
+ catch {
517
+ // An execution is actual only while its exact receipt and target can
518
+ // be read under the current ACL. A stale sidecar entry must never
519
+ // make a projection look successful after the provider changes.
520
+ execution = undefined;
521
+ }
522
+ }
523
+ const storedResult = execution ? ledger.results.find((item) => item.executionId === execution.id) : undefined;
524
+ if (storedResult)
525
+ assertDigest(storedResult, 'result');
526
+ let result;
527
+ let resultStatus = 'unrecorded';
528
+ if (execution && storedResult) {
529
+ try {
530
+ result = (await this.readResult(storedResult.id, access, execution)) ?? undefined;
531
+ if (result)
532
+ resultStatus = 'accepted';
533
+ }
534
+ catch {
535
+ // Results are accepted only while their exact provider record remains
536
+ // readable and points at the same execution target.
537
+ result = undefined;
538
+ }
539
+ }
540
+ const reviewRequired = ledger.corrections.some((correction) => correction.affectedCandidateIds.includes(candidate.id));
541
+ let targetStatus;
542
+ if (adoption && !options.targetAlreadyRead)
543
+ targetStatus = await this.targetStatus(adoption.target, access);
544
+ return {
545
+ candidate: cloneJson(candidate),
546
+ ...(confirmation ? { confirmation: cloneJson(confirmation) } : {}),
547
+ ...(adoption ? { adoption: cloneJson(adoption) } : {}),
548
+ ...(execution ? { execution: cloneJson(execution) } : {}),
549
+ ...(result ? { result: cloneJson(result) } : {}),
550
+ confirmationStatus: confirmation ? 'confirmed' : 'unconfirmed',
551
+ adoptionStatus: adoption ? 'adopted' : 'not_adopted',
552
+ executionStatus,
553
+ resultStatus,
554
+ reviewStatus: reviewRequired ? 'review_required' : 'clear',
555
+ ...(targetStatus ? { targetStatus } : {}),
556
+ };
557
+ }
558
+ async readConfirmation(candidateId, access) {
559
+ const ledger = await this.readLedger();
560
+ const confirmation = ledger.confirmations.find((item) => item.candidateId === candidateId);
561
+ if (!confirmation)
562
+ return null;
563
+ const candidate = await this.requireCandidate(candidateId, access);
564
+ if (confirmation.candidateDigest !== candidate.digest)
565
+ throw new MeetingMinutesLineageError('integrity_mismatch', `Confirmation ${confirmation.id} points to an old candidate`);
566
+ assertDigest(confirmation, 'confirmation');
567
+ return cloneJson(confirmation);
568
+ }
569
+ async readAdoption(id, access) {
570
+ const adoptionId = requireId(id, 'adoption id');
571
+ const ledger = await this.readLedger();
572
+ const adoption = ledger.adoptions.find((item) => item.id === adoptionId);
573
+ if (!adoption)
574
+ return null;
575
+ const candidate = await this.requireCandidate(adoption.candidateId, access);
576
+ if (adoption.candidateDigest !== candidate.digest)
577
+ throw new MeetingMinutesLineageError('integrity_mismatch', `Adoption ${adoption.id} points to an old candidate`);
578
+ await this.assertTargetReadable(adoption.target, access);
579
+ assertDigest(adoption, 'adoption');
580
+ return cloneJson(adoption);
581
+ }
582
+ async readExecution(id, access) {
583
+ const executionId = requireId(id, 'execution id');
584
+ const ledger = await this.readLedger();
585
+ const execution = ledger.executions.find((item) => item.id === executionId);
586
+ if (!execution)
587
+ return null;
588
+ const adoption = await this.requireAdoption(execution.candidateId, access);
589
+ if (adoption.id !== execution.adoptionId || !sameTarget(adoption.target, execution.target))
590
+ throw new MeetingMinutesLineageError('integrity_mismatch', `Execution ${execution.id} points to a different adoption`);
591
+ await this.assertTargetReadable(execution.target, access);
592
+ if (!this.options.receipt)
593
+ throw new MeetingMinutesLineageError('provider_unavailable', 'Execution receipt provider is not connected');
594
+ let receipt;
595
+ try {
596
+ receipt = await this.options.receipt.read(execution.receiptRef, access);
597
+ }
598
+ catch (error) {
599
+ throw mapProviderError(error, 'Execution receipt provider is unavailable', 'target_unavailable');
600
+ }
601
+ if (!receipt || !sameJson(normalizeReceiptReference(receipt.reference), execution.receiptRef) || !sameTarget(normalizeTarget(receipt.target), execution.target)) {
602
+ throw new MeetingMinutesLineageError('integrity_mismatch', `Execution receipt ${execution.receiptRef.id} is not readable at the exact version`);
603
+ }
604
+ assertDigest(execution, 'execution');
605
+ return cloneJson(execution);
606
+ }
607
+ async readResult(id, access, verifiedExecution) {
608
+ const resultId = requireId(id, 'result id');
609
+ const ledger = await this.readLedger();
610
+ const result = ledger.results.find((item) => item.id === resultId);
611
+ if (!result)
612
+ return null;
613
+ assertDigest(result, 'result');
614
+ const execution = verifiedExecution ?? await this.requireExecution(result.executionId, access);
615
+ if (execution.id !== result.executionId || !sameTarget(execution.target, result.target)) {
616
+ throw new MeetingMinutesLineageError('integrity_mismatch', `Result ${result.id} points to a different execution target`);
617
+ }
618
+ if (!this.options.result)
619
+ throw new MeetingMinutesLineageError('provider_unavailable', 'Result provider is not connected');
620
+ let providerResult;
621
+ try {
622
+ providerResult = await this.options.result.read(result.resultRef, access);
623
+ }
624
+ catch (error) {
625
+ throw mapProviderError(error, 'Result provider is unavailable', 'target_unavailable');
626
+ }
627
+ if (!providerResult ||
628
+ !sameJson(normalizeResultReference(providerResult.reference), result.resultRef) ||
629
+ !sameTarget(normalizeTarget(providerResult.target), execution.target)) {
630
+ throw new MeetingMinutesLineageError('integrity_mismatch', `Result ${result.resultRef.id} is not readable at the exact version`);
631
+ }
632
+ return cloneJson(result);
633
+ }
634
+ async requireExecution(id, access) {
635
+ const execution = await this.readExecution(id, access);
636
+ if (!execution)
637
+ throw new MeetingMinutesLineageError('not_found', `Execution ${id} was not found`);
638
+ return execution;
639
+ }
640
+ async requireCandidate(id, access) {
641
+ const candidate = await this.readCandidate(id, access);
642
+ if (!candidate)
643
+ throw new MeetingMinutesLineageError('not_found', `Candidate ${id} was not found`);
644
+ return candidate;
645
+ }
646
+ async requireAdoption(candidateId, access) {
647
+ const ledger = await this.readLedger();
648
+ const adoption = ledger.adoptions.find((item) => item.candidateId === candidateId);
649
+ if (!adoption)
650
+ throw new MeetingMinutesLineageError('not_found', `Candidate ${candidateId} has no adopted target`);
651
+ const readback = await this.readAdoption(adoption.id, access);
652
+ if (!readback)
653
+ throw new MeetingMinutesLineageError('readback_mismatch', `Adoption ${adoption.id} was not readable`);
654
+ return readback;
655
+ }
656
+ async assertTargetReadable(target, access) {
657
+ const provider = target.kind === 'judgment' ? this.options.judgment : this.options.task;
658
+ if (!provider)
659
+ throw new MeetingMinutesLineageError('provider_unavailable', `${target.kind} provider is not connected`);
660
+ try {
661
+ const record = await provider.readExact(target, access);
662
+ if (!record)
663
+ throw new MeetingMinutesLineageError('target_unavailable', `${target.kind} ${target.id} is unavailable`);
664
+ }
665
+ catch (error) {
666
+ if (error instanceof MeetingMinutesLineageError)
667
+ throw error;
668
+ throw mapProviderError(error, `${target.kind} ${target.id} is unavailable`, 'target_unavailable');
669
+ }
670
+ }
671
+ async targetStatus(target, access) {
672
+ try {
673
+ await this.assertTargetReadable(target, access);
674
+ return { status: 'available' };
675
+ }
676
+ catch (error) {
677
+ return statusFromError(error);
678
+ }
679
+ }
680
+ async readExactMinutes(reference, access) {
681
+ let resolved;
682
+ try {
683
+ resolved = await this.options.minutes.readExact(reference, access);
684
+ }
685
+ catch (error) {
686
+ throw mapProviderError(error, `Minutes version ${reference.versionId} is unavailable`);
687
+ }
688
+ if (!resolved)
689
+ throw new MeetingMinutesLineageError('source_not_found', `Minutes version ${reference.versionId} was not found`);
690
+ const actual = normalizeMinutesReference(resolved.reference);
691
+ if (!sameMinutesReference(actual, reference)) {
692
+ throw new MeetingMinutesLineageError('integrity_mismatch', `Minutes version ${reference.versionId} was not returned at the requested exact version`);
693
+ }
694
+ return { reference: actual };
695
+ }
696
+ async readLedger() {
697
+ const content = await readPersonalOsSidecar(this.options.dataDir, MEETING_MINUTES_LINEAGE_SIDECAR);
698
+ return parseLedger(content);
699
+ }
700
+ async mutateLedger(mutator) {
701
+ let result;
702
+ let completed = false;
703
+ await mutatePersonalOsWithSidecar(this.options.dataDir, MEETING_MINUTES_LINEAGE_SIDECAR, (current, content) => {
704
+ const ledger = parseLedger(content);
705
+ const outcome = mutator(ledger);
706
+ assertLedger(outcome.ledger);
707
+ result = cloneJson(outcome.result);
708
+ completed = true;
709
+ return { next: current, sidecarContent: serializeLedger(outcome.ledger), result: outcome.result };
710
+ });
711
+ if (!completed)
712
+ throw new MeetingMinutesLineageError('readback_mismatch', 'Lineage mutation did not complete');
713
+ return result;
714
+ }
715
+ }
716
+ export async function initializeMeetingMinutesLineageStore(dataDir) {
717
+ await initializePersonalOs(dataDir);
718
+ const content = await readPersonalOsSidecar(dataDir, MEETING_MINUTES_LINEAGE_SIDECAR);
719
+ if (content === undefined) {
720
+ await mutatePersonalOsWithSidecar(dataDir, MEETING_MINUTES_LINEAGE_SIDECAR, (current) => ({
721
+ next: current,
722
+ sidecarContent: serializeLedger(EMPTY_LEDGER),
723
+ result: undefined,
724
+ }));
725
+ }
726
+ else {
727
+ parseLedger(content);
728
+ }
729
+ }
730
+ function normalizeCandidateRequest(request) {
731
+ if (!request || typeof request !== 'object')
732
+ throw new MeetingMinutesLineageError('invalid_input', 'candidate request is required');
733
+ return {
734
+ id: requireId(request.id, 'candidate id'),
735
+ idempotencyKey: requireId(request.idempotencyKey, 'candidate idempotencyKey'),
736
+ actor: normalizeActor(request.actor),
737
+ evidence: normalizeMinutesReference(request.evidence),
738
+ kind: requireEnum(request.kind, ['judgment', 'task'], 'candidate kind'),
739
+ proposal: cloneJson(request.proposal),
740
+ epistemicStatus: requireEnum(request.epistemicStatus, ['confirmed', 'inferred'], 'epistemicStatus'),
741
+ };
742
+ }
743
+ function normalizeConfirmationRequest(request) {
744
+ return {
745
+ id: requireId(request?.id, 'confirmation id'),
746
+ idempotencyKey: requireId(request?.idempotencyKey, 'confirmation idempotencyKey'),
747
+ candidateId: requireId(request?.candidateId, 'candidate id'),
748
+ actor: normalizeActor(request?.actor),
749
+ evidenceDigest: requireDigest(request?.evidenceDigest, 'evidenceDigest'),
750
+ };
751
+ }
752
+ function normalizeAdoptionRequest(request) {
753
+ return {
754
+ id: requireId(request?.id, 'adoption id'),
755
+ idempotencyKey: requireId(request?.idempotencyKey, 'adoption idempotencyKey'),
756
+ candidateId: requireId(request?.candidateId, 'candidate id'),
757
+ actor: normalizeActor(request?.actor),
758
+ };
759
+ }
760
+ function normalizeExecutionRequest(request) {
761
+ return {
762
+ id: requireId(request?.id, 'execution id'),
763
+ idempotencyKey: requireId(request?.idempotencyKey, 'execution idempotencyKey'),
764
+ candidateId: requireId(request?.candidateId, 'candidate id'),
765
+ receiptRef: normalizeReceiptReference(request?.receiptRef),
766
+ actor: normalizeActor(request?.actor),
767
+ };
768
+ }
769
+ function normalizeResultRequest(request) {
770
+ return {
771
+ id: requireId(request?.id, 'result id'),
772
+ idempotencyKey: requireId(request?.idempotencyKey, 'result idempotencyKey'),
773
+ executionId: requireId(request?.executionId, 'execution id'),
774
+ resultRef: normalizeResultReference(request?.resultRef),
775
+ actor: normalizeActor(request?.actor),
776
+ };
777
+ }
778
+ function normalizeCorrectionRequest(request) {
779
+ const reason = requireString(request?.reason, 'correction reason');
780
+ if (reason.length > 4000)
781
+ throw new MeetingMinutesLineageError('invalid_input', 'correction reason is too long');
782
+ return {
783
+ id: requireId(request?.id, 'correction id'),
784
+ idempotencyKey: requireId(request?.idempotencyKey, 'correction idempotencyKey'),
785
+ previousVersion: normalizeMinutesReference(request?.previousVersion),
786
+ replacementVersion: normalizeMinutesReference(request?.replacementVersion),
787
+ actor: normalizeActor(request?.actor),
788
+ reason,
789
+ };
790
+ }
791
+ function normalizeMinutesReference(value) {
792
+ if (!value || typeof value !== 'object')
793
+ throw new MeetingMinutesLineageError('invalid_input', 'exact minutes reference is required');
794
+ return {
795
+ meetingId: requireId(value.meetingId, 'meetingId'),
796
+ minutesId: requireId(value.minutesId, 'minutesId'),
797
+ versionId: requireId(value.versionId, 'versionId'),
798
+ contentDigest: value.contentDigest === null ? null : requireDigest(value.contentDigest, 'contentDigest'),
799
+ locator: cloneJson(value.locator),
800
+ provenance: normalizeProvenance(value.provenance),
801
+ };
802
+ }
803
+ function normalizeProvenance(value) {
804
+ if (!value || typeof value !== 'object')
805
+ throw new MeetingMinutesLineageError('invalid_input', 'minutes provenance is required');
806
+ return {
807
+ providerKind: requireString(value.providerKind, 'provenance.providerKind'),
808
+ providerId: requireString(value.providerId, 'provenance.providerId'),
809
+ ...(value.revision === undefined ? {} : { revision: requireString(value.revision, 'provenance.revision') }),
810
+ ...(value.digest === undefined ? {} : { digest: requireDigest(value.digest, 'provenance.digest') }),
811
+ };
812
+ }
813
+ function normalizeActor(value) {
814
+ if (!value || typeof value !== 'object')
815
+ throw new MeetingMinutesLineageError('invalid_input', 'actor is required');
816
+ return {
817
+ type: requireEnum(value.type, ['person', 'agent', 'service', 'system'], 'actor.type'),
818
+ id: requireId(value.id, 'actor.id'),
819
+ };
820
+ }
821
+ function normalizeTarget(value) {
822
+ if (!value || typeof value !== 'object')
823
+ throw new MeetingMinutesLineageError('invalid_input', 'exact target reference is required');
824
+ if (value.kind === 'judgment') {
825
+ return {
826
+ kind: 'judgment',
827
+ id: requireId(value.id, 'judgment target id'),
828
+ revision: requireId(value.revision, 'judgment target revision'),
829
+ digest: requireDigest(value.digest, 'judgment target digest'),
830
+ };
831
+ }
832
+ if (value.kind === 'task') {
833
+ if (!Number.isInteger(value.version) || value.version < 1)
834
+ throw new MeetingMinutesLineageError('invalid_input', 'task target version must be a positive integer');
835
+ return {
836
+ kind: 'task',
837
+ id: requireId(value.id, 'task target id'),
838
+ version: value.version,
839
+ ...(value.digest === undefined ? {} : { digest: requireDigest(value.digest, 'task target digest') }),
840
+ };
841
+ }
842
+ throw new MeetingMinutesLineageError('invalid_input', 'target kind must be judgment or task');
843
+ }
844
+ function normalizeAdoptionReference(value, kind) {
845
+ if (!value || typeof value !== 'object')
846
+ throw new MeetingMinutesLineageError('invalid_input', 'adoption provider must return an exact adoption reference');
847
+ const expected = kind === 'judgment' ? 'judgment_adoption' : 'task_adoption';
848
+ if (value.kind !== expected)
849
+ throw new MeetingMinutesLineageError('integrity_mismatch', `Adoption reference kind must be ${expected}`);
850
+ return { kind: expected, id: requireId(value.id, 'adoption reference id'), revision: requireId(value.revision, 'adoption reference revision'), digest: requireDigest(value.digest, 'adoption reference digest') };
851
+ }
852
+ function normalizeReceiptReference(value) {
853
+ if (!value || typeof value !== 'object')
854
+ throw new MeetingMinutesLineageError('invalid_input', 'receipt reference is required');
855
+ return { id: requireId(value.id, 'receipt id'), digest: requireDigest(value.digest, 'receipt digest') };
856
+ }
857
+ function normalizeResultReference(value) {
858
+ if (!value || typeof value !== 'object')
859
+ throw new MeetingMinutesLineageError('invalid_input', 'result reference is required');
860
+ return { id: requireId(value.id, 'result id'), digest: requireDigest(value.digest, 'result digest') };
861
+ }
862
+ function parseLedger(content) {
863
+ if (content === undefined)
864
+ return EMPTY_LEDGER;
865
+ let parsed;
866
+ try {
867
+ parsed = JSON.parse(content);
868
+ }
869
+ catch {
870
+ throw new MeetingMinutesLineageError('corrupt_record', 'Meeting minutes lineage sidecar is not valid JSON');
871
+ }
872
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
873
+ throw new MeetingMinutesLineageError('corrupt_record', 'Meeting minutes lineage sidecar must be an object');
874
+ const value = parsed;
875
+ if (value.version !== MEETING_MINUTES_LINEAGE_CATALOG_VERSION)
876
+ throw new MeetingMinutesLineageError('corrupt_record', 'Unsupported meeting minutes lineage catalog version');
877
+ for (const field of ['candidates', 'confirmations', 'adoptions', 'executions', 'results', 'corrections']) {
878
+ if (!Array.isArray(value[field]))
879
+ throw new MeetingMinutesLineageError('corrupt_record', `Lineage sidecar field ${field} must be an array`);
880
+ }
881
+ const ledger = {
882
+ version: MEETING_MINUTES_LINEAGE_CATALOG_VERSION,
883
+ candidates: cloneJson(value.candidates),
884
+ confirmations: cloneJson(value.confirmations),
885
+ adoptions: cloneJson(value.adoptions),
886
+ executions: cloneJson(value.executions),
887
+ results: cloneJson(value.results),
888
+ corrections: cloneJson(value.corrections),
889
+ };
890
+ assertLedger(ledger);
891
+ return ledger;
892
+ }
893
+ function assertLedger(ledger) {
894
+ if (!ledger || ledger.version !== MEETING_MINUTES_LINEAGE_CATALOG_VERSION)
895
+ throw new MeetingMinutesLineageError('corrupt_record', 'Invalid meeting minutes lineage ledger version');
896
+ for (const collection of [ledger.candidates, ledger.confirmations, ledger.adoptions, ledger.executions, ledger.results, ledger.corrections]) {
897
+ if (!Array.isArray(collection))
898
+ throw new MeetingMinutesLineageError('corrupt_record', 'Invalid meeting minutes lineage ledger collection');
899
+ for (const record of collection)
900
+ assertDigest(record, 'lineage record');
901
+ }
902
+ }
903
+ function serializeLedger(ledger) {
904
+ return `${JSON.stringify(ledger, null, 2)}\n`;
905
+ }
906
+ function withDigest(record) {
907
+ return { ...record, digest: digestJson({ ...record, digest: '' }) };
908
+ }
909
+ function assertDigest(value, label) {
910
+ if (!value || typeof value !== 'object' || Array.isArray(value) || typeof value.digest !== 'string') {
911
+ throw new MeetingMinutesLineageError('corrupt_record', `${label} is malformed`);
912
+ }
913
+ const record = value;
914
+ if (digestJson({ ...record, digest: '' }) !== record.digest)
915
+ throw new MeetingMinutesLineageError('corrupt_record', `${label} digest does not match`);
916
+ }
917
+ function sameMinutesReference(left, right) {
918
+ return sameJson(normalizeMinutesReference(left), normalizeMinutesReference(right));
919
+ }
920
+ function sameTarget(left, right) {
921
+ return sameJson(normalizeTarget(left), normalizeTarget(right));
922
+ }
923
+ function digestJson(value) {
924
+ return `sha256:${createHash('sha256').update(canonicalJson(value)).digest('hex')}`;
925
+ }
926
+ function sameJson(left, right) {
927
+ return canonicalJson(left) === canonicalJson(right);
928
+ }
929
+ function canonicalJson(value) {
930
+ assertJsonValue(value);
931
+ return JSON.stringify(sortJson(value));
932
+ }
933
+ function sortJson(value) {
934
+ if (Array.isArray(value))
935
+ return value.map(sortJson);
936
+ if (value !== null && typeof value === 'object') {
937
+ return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, sortJson(entry)]));
938
+ }
939
+ return value;
940
+ }
941
+ function assertJsonValue(value, path = 'value') {
942
+ if (value === null || typeof value === 'string' || typeof value === 'boolean')
943
+ return;
944
+ if (typeof value === 'number') {
945
+ if (Number.isFinite(value))
946
+ return;
947
+ throw new MeetingMinutesLineageError('invalid_input', `${path} must be finite`);
948
+ }
949
+ if (Array.isArray(value)) {
950
+ value.forEach((entry, index) => assertJsonValue(entry, `${path}[${index}]`));
951
+ return;
952
+ }
953
+ if (typeof value === 'object') {
954
+ if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
955
+ throw new MeetingMinutesLineageError('invalid_input', `${path} must be JSON data`);
956
+ for (const [key, entry] of Object.entries(value)) {
957
+ if (entry === undefined)
958
+ throw new MeetingMinutesLineageError('invalid_input', `${path}.${key} cannot be undefined`);
959
+ assertJsonValue(entry, `${path}.${key}`);
960
+ }
961
+ return;
962
+ }
963
+ throw new MeetingMinutesLineageError('invalid_input', `${path} must be JSON data`);
964
+ }
965
+ function cloneJson(value) {
966
+ assertJsonValue(value);
967
+ return JSON.parse(JSON.stringify(value));
968
+ }
969
+ function assertAccess(value) {
970
+ if (!value || typeof value !== 'object')
971
+ throw new MeetingMinutesLineageError('authorization_denied', 'Current access context is required');
972
+ return { ...value, principal: requireId(value.principal, 'access principal') };
973
+ }
974
+ function statusFromError(error) {
975
+ if (error instanceof MeetingMinutesLineageError) {
976
+ if (error.code === 'authorization_denied')
977
+ return { status: 'denied', reason: error.code };
978
+ return { status: 'unavailable', reason: error.code };
979
+ }
980
+ return { status: 'unavailable', reason: 'provider_unavailable' };
981
+ }
982
+ function mapProviderError(error, fallback, notFoundCode = 'source_not_found') {
983
+ if (error instanceof MeetingMinutesLineageError)
984
+ return error;
985
+ const code = typeof error === 'object' && error !== null && 'code' in error ? String(error.code) : '';
986
+ if (code === 'authorization_denied' || code === 'forbidden' || code === 'permission_denied')
987
+ return new MeetingMinutesLineageError('authorization_denied', fallback);
988
+ if (code === 'not_found' || code === 'source_not_found' || code === 'task_not_found')
989
+ return new MeetingMinutesLineageError(notFoundCode, fallback);
990
+ if (code === 'integrity_mismatch' || code === 'revision_conflict' || code === 'readback_mismatch')
991
+ return new MeetingMinutesLineageError(code, fallback);
992
+ return new MeetingMinutesLineageError(notFoundCode === 'target_unavailable' ? 'target_unavailable' : 'source_unavailable', fallback);
993
+ }
994
+ /**
995
+ * Provider calls use the candidate identity as their idempotency boundary.
996
+ * The request key belongs to the HTTP/transport attempt and may change when a
997
+ * caller retries after a lost response. Reusing the candidate key lets a
998
+ * provider recover the same canonical target instead of creating a second
999
+ * judgment or Task during that retry window.
1000
+ */
1001
+ function providerIdempotencyKey(kind, candidate) {
1002
+ return `meeting-minutes:${kind}:${candidate.id}`;
1003
+ }
1004
+ function requireId(value, label) {
1005
+ return requireString(value, label, true);
1006
+ }
1007
+ function requireString(value, label, trim = false) {
1008
+ if (typeof value !== 'string')
1009
+ throw new MeetingMinutesLineageError('invalid_input', `${label} must be a string`);
1010
+ const result = trim ? value.normalize('NFKC').trim() : value;
1011
+ if (!result)
1012
+ throw new MeetingMinutesLineageError('invalid_input', `${label} is required`);
1013
+ if (/^[\u0000-\u001f\u007f]/u.test(result))
1014
+ throw new MeetingMinutesLineageError('invalid_input', `${label} contains control characters`);
1015
+ return result;
1016
+ }
1017
+ function requireDigest(value, label) {
1018
+ const result = requireString(value, label, true);
1019
+ if (!/^[a-z0-9][a-z0-9._:-]{3,}$/iu.test(result))
1020
+ throw new MeetingMinutesLineageError('invalid_input', `${label} must be a stable digest`);
1021
+ return result;
1022
+ }
1023
+ function requireEnum(value, allowed, label) {
1024
+ if (typeof value !== 'string' || !allowed.includes(value))
1025
+ throw new MeetingMinutesLineageError('invalid_input', `${label} is invalid`);
1026
+ return value;
1027
+ }
1028
+ function isNonEmptyString(value) {
1029
+ return typeof value === 'string' && value.trim().length > 0;
1030
+ }