@agentplat/trust 0.3.0-alpha.4

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 (63) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +14 -0
  3. package/dist/canonical.d.ts +20 -0
  4. package/dist/canonical.d.ts.map +1 -0
  5. package/dist/canonical.js +173 -0
  6. package/dist/canonical.js.map +1 -0
  7. package/dist/causal.d.ts +7 -0
  8. package/dist/causal.d.ts.map +1 -0
  9. package/dist/causal.js +155 -0
  10. package/dist/causal.js.map +1 -0
  11. package/dist/eligibility.d.ts +8 -0
  12. package/dist/eligibility.d.ts.map +1 -0
  13. package/dist/eligibility.js +325 -0
  14. package/dist/eligibility.js.map +1 -0
  15. package/dist/evidence.d.ts +20 -0
  16. package/dist/evidence.d.ts.map +1 -0
  17. package/dist/evidence.js +424 -0
  18. package/dist/evidence.js.map +1 -0
  19. package/dist/fusion.d.ts +8 -0
  20. package/dist/fusion.d.ts.map +1 -0
  21. package/dist/fusion.js +1278 -0
  22. package/dist/fusion.js.map +1 -0
  23. package/dist/index.d.ts +13 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +13 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/lifecycle.d.ts +41 -0
  28. package/dist/lifecycle.d.ts.map +1 -0
  29. package/dist/lifecycle.js +1369 -0
  30. package/dist/lifecycle.js.map +1 -0
  31. package/dist/mesh-records.d.ts +36 -0
  32. package/dist/mesh-records.d.ts.map +1 -0
  33. package/dist/mesh-records.js +133 -0
  34. package/dist/mesh-records.js.map +1 -0
  35. package/dist/policy.d.ts +9 -0
  36. package/dist/policy.d.ts.map +1 -0
  37. package/dist/policy.js +598 -0
  38. package/dist/policy.js.map +1 -0
  39. package/dist/profile.d.ts +21 -0
  40. package/dist/profile.d.ts.map +1 -0
  41. package/dist/profile.js +253 -0
  42. package/dist/profile.js.map +1 -0
  43. package/dist/quarantine.d.ts +53 -0
  44. package/dist/quarantine.d.ts.map +1 -0
  45. package/dist/quarantine.js +742 -0
  46. package/dist/quarantine.js.map +1 -0
  47. package/dist/sha256.d.ts +2 -0
  48. package/dist/sha256.d.ts.map +1 -0
  49. package/dist/sha256.js +66 -0
  50. package/dist/sha256.js.map +1 -0
  51. package/dist/state.d.ts +18 -0
  52. package/dist/state.d.ts.map +1 -0
  53. package/dist/state.js +1015 -0
  54. package/dist/state.js.map +1 -0
  55. package/dist/types.d.ts +862 -0
  56. package/dist/types.d.ts.map +1 -0
  57. package/dist/types.js +60 -0
  58. package/dist/types.js.map +1 -0
  59. package/dist/validation.d.ts +18 -0
  60. package/dist/validation.d.ts.map +1 -0
  61. package/dist/validation.js +223 -0
  62. package/dist/validation.js.map +1 -0
  63. package/package.json +43 -0
@@ -0,0 +1,742 @@
1
+ import { deepFreeze, digestTrustJsonV1, TrustValidationError, } from "./canonical.js";
2
+ import { digestScopeV1, digestSubjectV1 } from "./evidence.js";
3
+ import { validateEvidenceFusionDecisionV1 } from "./fusion.js";
4
+ import { digestEvidenceFusionPolicyV1, validateEvidenceFusionPolicyV1, } from "./policy.js";
5
+ import { validateTrustProfileV1 } from "./profile.js";
6
+ import { assertExactKeys, assertIdentifier, assertSafeInteger, assertTrustDigest, validateReasonCodeV1, } from "./validation.js";
7
+ const compare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
8
+ const recordKeys = [
9
+ "schemaVersion",
10
+ "quarantineId",
11
+ "quarantineKey",
12
+ "revision",
13
+ "previousRecordId",
14
+ "tenantId",
15
+ "subjectDigest",
16
+ "scopeDigest",
17
+ "dimensionId",
18
+ "policyDigest",
19
+ "fusionDecisionId",
20
+ "activationEvidenceIds",
21
+ "activationEvidenceSetDigest",
22
+ "activationDependencyGroupIds",
23
+ "reasonCodes",
24
+ "activatedAtLogicalMs",
25
+ "reviewAfterLogicalMs",
26
+ "status",
27
+ "recoveredAtLogicalMs",
28
+ "recoveryDecisionId",
29
+ ];
30
+ const recoveryKeys = [
31
+ "schemaVersion",
32
+ "recoveryDecisionId",
33
+ "recoveryDecisionDigest",
34
+ "quarantineId",
35
+ "quarantineKey",
36
+ "policyDigest",
37
+ "fusionDecisionId",
38
+ "evaluatedAtLogicalMs",
39
+ "recoveryEvidenceIds",
40
+ "recoveryEvidenceSetDigest",
41
+ "recoveryClaimSourceDependencyGroupIds",
42
+ "effectiveRecoveryWeightBasisPoints",
43
+ "scoreBasisPoints",
44
+ "uncertaintyBasisPoints",
45
+ "disposition",
46
+ "reasonCodes",
47
+ ];
48
+ const tupleKinds = ["claim", "attestation", "content_resolution"];
49
+ function assertContentId(value, prefix, label) {
50
+ assertIdentifier(value, label);
51
+ if (!new RegExp(`^${prefix}:[0-9a-f]{64}$`, "u").test(value))
52
+ throw new TrustValidationError(`${label} is not content-bound`);
53
+ }
54
+ function validateSortedIdentifiers(value, label, nonEmpty = false) {
55
+ if (!Array.isArray(value) || (nonEmpty && value.length === 0))
56
+ throw new TrustValidationError(`${label} is invalid`);
57
+ for (const item of value)
58
+ assertIdentifier(item, label);
59
+ if (value.some((item, index) => index > 0 && compare(value[index - 1], item) >= 0))
60
+ throw new TrustValidationError(`${label} must be canonically ordered and unique`);
61
+ return deepFreeze(structuredClone(value));
62
+ }
63
+ function validateSortedReasons(value, label) {
64
+ const reasons = validateSortedIdentifiers(value, label);
65
+ for (const reason of reasons)
66
+ validateReasonCodeV1(reason);
67
+ return reasons;
68
+ }
69
+ function tupleKey(value) {
70
+ return `${value.kind}\u0000${value.id}\u0000${value.digest}`;
71
+ }
72
+ export function validateQuarantineEvidenceTuplesV1(value, options = {}) {
73
+ if (!Array.isArray(value) ||
74
+ (options.nonEmpty === true && value.length === 0))
75
+ throw new TrustValidationError("quarantine evidence tuples are invalid");
76
+ const tuples = value.map((candidate) => {
77
+ assertExactKeys(candidate, ["kind", "id", "digest"], "evidence tuple");
78
+ const tuple = candidate;
79
+ if (!tupleKinds.includes(tuple.kind))
80
+ throw new TrustValidationError("evidence tuple kind is invalid");
81
+ assertTrustDigest(tuple.digest, "evidence tuple digest");
82
+ const prefix = tuple.kind === "content_resolution" ? "content-resolution" : tuple.kind;
83
+ assertContentId(tuple.id, prefix, "evidence tuple ID");
84
+ if (tuple.id !== `${prefix}:${tuple.digest}`)
85
+ throw new TrustValidationError("evidence tuple binding is invalid");
86
+ return tuple;
87
+ });
88
+ if (tuples.some((tuple, index) => index > 0 && compare(tupleKey(tuples[index - 1]), tupleKey(tuple)) >= 0))
89
+ throw new TrustValidationError("evidence tuples must be canonically ordered and unique");
90
+ return deepFreeze(structuredClone(tuples));
91
+ }
92
+ export function digestTrustQuarantineKeyV1(value) {
93
+ assertExactKeys(value, ["tenantId", "subjectDigest", "scopeDigest", "dimensionId", "policyDigest"], "quarantine key");
94
+ assertIdentifier(value.tenantId, "tenantId");
95
+ assertIdentifier(value.dimensionId, "dimensionId");
96
+ for (const key of ["subjectDigest", "scopeDigest", "policyDigest"])
97
+ assertTrustDigest(value[key], key);
98
+ return digestTrustJsonV1("quarantine-key", value);
99
+ }
100
+ export function digestQuarantineEvidenceSetV1(value, domain = "quarantine-evidence-set") {
101
+ const tuples = validateQuarantineEvidenceTuplesV1(value, {
102
+ nonEmpty: domain === "quarantine-evidence-set",
103
+ });
104
+ return digestTrustJsonV1(domain, tuples);
105
+ }
106
+ export const digestTrustQuarantineEvidenceSetV1 = (value) => digestQuarantineEvidenceSetV1(value, "quarantine-evidence-set");
107
+ export const digestTrustRecoveryEvidenceSetV1 = (value) => digestQuarantineEvidenceSetV1(value, "recovery-evidence-set");
108
+ function quarantineRecordBody(value) {
109
+ const { quarantineId: _id, ...body } = value;
110
+ return body;
111
+ }
112
+ export function digestQuarantineRecordV1(value) {
113
+ return digestTrustJsonV1("quarantine-record", quarantineRecordBody(value));
114
+ }
115
+ export function validateQuarantineRecordV1(value) {
116
+ assertExactKeys(value, recordKeys, "quarantine record");
117
+ const record = value;
118
+ if (record.schemaVersion !== 1)
119
+ throw new TrustValidationError("quarantine schema is invalid");
120
+ assertContentId(record.quarantineId, "quarantine-record", "quarantineId");
121
+ if (record.previousRecordId !== null)
122
+ assertContentId(record.previousRecordId, "quarantine-record", "previousRecordId");
123
+ assertIdentifier(record.tenantId, "tenantId");
124
+ assertIdentifier(record.dimensionId, "dimensionId");
125
+ assertContentId(record.fusionDecisionId, "fusion-decision", "fusionDecisionId");
126
+ for (const key of [
127
+ "quarantineKey",
128
+ "subjectDigest",
129
+ "scopeDigest",
130
+ "policyDigest",
131
+ "activationEvidenceSetDigest",
132
+ ])
133
+ assertTrustDigest(record[key], key);
134
+ if (record.quarantineKey !==
135
+ digestTrustQuarantineKeyV1({
136
+ tenantId: record.tenantId,
137
+ subjectDigest: record.subjectDigest,
138
+ scopeDigest: record.scopeDigest,
139
+ dimensionId: record.dimensionId,
140
+ policyDigest: record.policyDigest,
141
+ }))
142
+ throw new TrustValidationError("quarantine key binding is invalid");
143
+ assertSafeInteger(record.revision, "revision", 1);
144
+ if ((record.revision === 1) !== (record.previousRecordId === null))
145
+ throw new TrustValidationError("quarantine lineage is invalid");
146
+ validateSortedIdentifiers(record.activationEvidenceIds, "activation evidence", true);
147
+ validateSortedIdentifiers(record.activationDependencyGroupIds, "activation groups", true);
148
+ const reasons = validateSortedReasons(record.reasonCodes, "quarantine reasons");
149
+ assertSafeInteger(record.activatedAtLogicalMs, "activatedAtLogicalMs");
150
+ assertSafeInteger(record.reviewAfterLogicalMs, "reviewAfterLogicalMs");
151
+ if (record.reviewAfterLogicalMs <= record.activatedAtLogicalMs)
152
+ throw new TrustValidationError("quarantine review time is invalid");
153
+ if (!(record.status === "active" ||
154
+ record.status === "review_required" ||
155
+ record.status === "recovered"))
156
+ throw new TrustValidationError("quarantine status is invalid");
157
+ const expectedReason = {
158
+ active: "quarantine_activated",
159
+ review_required: "quarantine_review_required",
160
+ recovered: "quarantine_recovered",
161
+ }[record.status];
162
+ if (reasons.length !== 1 || reasons[0] !== expectedReason)
163
+ throw new TrustValidationError("quarantine status reason is invalid");
164
+ if (record.status === "recovered") {
165
+ assertSafeInteger(record.recoveredAtLogicalMs, "recoveredAtLogicalMs");
166
+ assertContentId(record.recoveryDecisionId, "recovery-decision", "recoveryDecisionId");
167
+ if (record.recoveredAtLogicalMs < record.reviewAfterLogicalMs)
168
+ throw new TrustValidationError("recovery precedes review");
169
+ }
170
+ else if (record.recoveredAtLogicalMs !== null ||
171
+ record.recoveryDecisionId !== null)
172
+ throw new TrustValidationError("quarantine recovery fields are invalid");
173
+ if (record.quarantineId !==
174
+ `quarantine-record:${digestQuarantineRecordV1(record)}`)
175
+ throw new TrustValidationError("quarantine digest is invalid");
176
+ return deepFreeze(structuredClone(record));
177
+ }
178
+ export function createQuarantineRecordV1(input) {
179
+ const digest = digestTrustJsonV1("quarantine-record", input);
180
+ return validateQuarantineRecordV1({
181
+ ...input,
182
+ quarantineId: `quarantine-record:${digest}`,
183
+ });
184
+ }
185
+ export function createActiveQuarantineRecordV1(input) {
186
+ assertExactKeys(input, [
187
+ "revision",
188
+ "previousRecordId",
189
+ "tenantId",
190
+ "subjectDigest",
191
+ "scopeDigest",
192
+ "dimensionId",
193
+ "policyDigest",
194
+ "fusionDecisionId",
195
+ "activationEvidence",
196
+ "activationDependencyGroupIds",
197
+ "activatedAtLogicalMs",
198
+ "reviewIntervalMs",
199
+ ], "active quarantine input");
200
+ assertSafeInteger(input.activatedAtLogicalMs, "activatedAtLogicalMs");
201
+ assertSafeInteger(input.reviewIntervalMs, "reviewIntervalMs", 1);
202
+ if (input.activatedAtLogicalMs >
203
+ Number.MAX_SAFE_INTEGER - input.reviewIntervalMs)
204
+ throw new TrustValidationError("quarantine review time overflows");
205
+ const evidence = validateQuarantineEvidenceTuplesV1(input.activationEvidence, { nonEmpty: true });
206
+ const activationEvidenceIds = [...evidence]
207
+ .map((item) => item.id)
208
+ .sort(compare);
209
+ validateSortedIdentifiers(activationEvidenceIds, "activation evidence IDs", true);
210
+ const activationDependencyGroupIds = validateSortedIdentifiers(input.activationDependencyGroupIds, "activation groups", true);
211
+ return createQuarantineRecordV1({
212
+ schemaVersion: 1,
213
+ quarantineKey: digestTrustQuarantineKeyV1({
214
+ tenantId: input.tenantId,
215
+ subjectDigest: input.subjectDigest,
216
+ scopeDigest: input.scopeDigest,
217
+ dimensionId: input.dimensionId,
218
+ policyDigest: input.policyDigest,
219
+ }),
220
+ revision: input.revision,
221
+ previousRecordId: input.previousRecordId,
222
+ tenantId: input.tenantId,
223
+ subjectDigest: input.subjectDigest,
224
+ scopeDigest: input.scopeDigest,
225
+ dimensionId: input.dimensionId,
226
+ policyDigest: input.policyDigest,
227
+ fusionDecisionId: input.fusionDecisionId,
228
+ activationEvidenceIds,
229
+ activationEvidenceSetDigest: digestTrustQuarantineEvidenceSetV1(evidence),
230
+ activationDependencyGroupIds,
231
+ reasonCodes: ["quarantine_activated"],
232
+ activatedAtLogicalMs: input.activatedAtLogicalMs,
233
+ reviewAfterLogicalMs: input.activatedAtLogicalMs + input.reviewIntervalMs,
234
+ status: "active",
235
+ recoveredAtLogicalMs: null,
236
+ recoveryDecisionId: null,
237
+ });
238
+ }
239
+ function validateProjectionBinding(state, profileValue, decisionValue, policyValue) {
240
+ const profile = validateTrustProfileV1(profileValue);
241
+ const decision = validateEvidenceFusionDecisionV1(decisionValue);
242
+ const policy = validateEvidenceFusionPolicyV1(policyValue, state.limits);
243
+ const policyDigest = digestEvidenceFusionPolicyV1(policy);
244
+ if (decision.policyId !== policy.policyId ||
245
+ decision.policyVersion !== policy.policyVersion ||
246
+ decision.policyDigest !== policyDigest ||
247
+ profile.policyId !== policy.policyId ||
248
+ profile.policyVersion !== policy.policyVersion ||
249
+ profile.policyDigest !== policyDigest ||
250
+ profile.tenantId !== decision.tenantId ||
251
+ profile.subjectDigest !== decision.subjectDigest ||
252
+ profile.scopeDigest !== decision.scopeDigest ||
253
+ profile.fusionDecisionId !== decision.fusionDecisionId ||
254
+ profile.fusionDecisionDigest !== decision.fusionDecisionDigest ||
255
+ profile.inputSetDigest !== decision.inputSetDigest ||
256
+ profile.updatedAtLogicalMs !== decision.evaluatedAtLogicalMs)
257
+ throw new TrustValidationError("quarantine projection binding is invalid");
258
+ return { profile, decision, policy };
259
+ }
260
+ function sortedTuples(values) {
261
+ const byKey = new Map(values.map((value) => [tupleKey(value), value]));
262
+ return validateQuarantineEvidenceTuplesV1([...byKey.values()].sort((left, right) => compare(tupleKey(left), tupleKey(right))));
263
+ }
264
+ function exactClaimEvidence(state, decision, classification, afterLogicalMs) {
265
+ const claim = state.records.find((record) => record.recordKind === "claim" &&
266
+ record.recordId === classification.claimId &&
267
+ record.recordDigest === classification.claimDigest);
268
+ if (!claim ||
269
+ (afterLogicalMs !== null && claim.acceptedAtLogicalMs <= afterLogicalMs))
270
+ return null;
271
+ const tuples = [
272
+ { kind: "claim", id: claim.recordId, digest: claim.recordDigest },
273
+ ];
274
+ for (const attestationId of classification.effectiveSupportingAttestationIds) {
275
+ const attestation = state.records.find((record) => record.recordKind === "attestation" &&
276
+ record.recordId === attestationId &&
277
+ "claimId" in record.record &&
278
+ "disposition" in record.record &&
279
+ record.record.claimId === claim.recordId &&
280
+ record.record.claimDigest === claim.recordDigest &&
281
+ record.record.disposition === "support");
282
+ if (!attestation ||
283
+ (afterLogicalMs !== null &&
284
+ attestation.acceptedAtLogicalMs <= afterLogicalMs))
285
+ return null;
286
+ tuples.push({
287
+ kind: "attestation",
288
+ id: attestation.recordId,
289
+ digest: attestation.recordDigest,
290
+ });
291
+ }
292
+ for (const resolutionId of classification.effectiveContentResolutionIds) {
293
+ const resolution = state.contentResolutions.find((candidate) => candidate.resolutionId === resolutionId &&
294
+ candidate.claimId === claim.recordId &&
295
+ candidate.claimDigest === claim.recordDigest &&
296
+ candidate.result === "verified" &&
297
+ candidate.resolvedAtLogicalMs <= decision.evaluatedAtLogicalMs);
298
+ if (!resolution ||
299
+ (afterLogicalMs !== null &&
300
+ resolution.resolvedAtLogicalMs <= afterLogicalMs))
301
+ return null;
302
+ tuples.push({
303
+ kind: "content_resolution",
304
+ id: resolution.resolutionId,
305
+ digest: resolution.resolutionDigest,
306
+ });
307
+ }
308
+ return sortedTuples(tuples);
309
+ }
310
+ export function deriveQuarantineActivationsV1(state, profileValue, decisionValue, policyValue, acceptedAfterLogicalMs = null) {
311
+ if (acceptedAfterLogicalMs !== null)
312
+ assertSafeInteger(acceptedAfterLogicalMs, "acceptedAfterLogicalMs");
313
+ const { profile, decision, policy } = validateProjectionBinding(state, profileValue, decisionValue, policyValue);
314
+ if (!policy.quarantinePolicy.enabled)
315
+ return deepFreeze([]);
316
+ const projections = [];
317
+ for (const rule of policy.quarantinePolicy.rules) {
318
+ const dimension = profile.dimensions.find((candidate) => candidate.dimensionId === rule.dimensionId);
319
+ if (!dimension ||
320
+ dimension.scoreBasisPoints > rule.activationScoreAtOrBelowBasisPoints)
321
+ continue;
322
+ const groups = new Set();
323
+ const tuples = [];
324
+ let weight = 0;
325
+ for (const classification of decision.claimClassifications) {
326
+ if (classification.dimensionId !== rule.dimensionId ||
327
+ !dimension.includedClaimIds.includes(classification.claimId) ||
328
+ classification.classification !== "supported" ||
329
+ classification.effectiveWeightBasisPoints <= 0 ||
330
+ classification.claimSourceDependencyGroupId === null)
331
+ continue;
332
+ const criterion = policy.criteria.find((candidate) => candidate.criterionId === classification.criterionId);
333
+ const claim = state.records.find((record) => record.recordKind === "claim" &&
334
+ record.recordId === classification.claimId &&
335
+ record.recordDigest === classification.claimDigest);
336
+ if (!criterion?.quarantineEligible ||
337
+ !claim ||
338
+ !("outcome" in claim.record) ||
339
+ claim.record.outcome !== "violated")
340
+ continue;
341
+ const evidence = exactClaimEvidence(state, decision, classification, acceptedAfterLogicalMs);
342
+ if (!evidence)
343
+ continue;
344
+ groups.add(classification.claimSourceDependencyGroupId);
345
+ weight = Math.min(10_000, weight + classification.effectiveWeightBasisPoints);
346
+ tuples.push(...evidence);
347
+ }
348
+ const activationDependencyGroupIds = [...groups].sort(compare);
349
+ if (activationDependencyGroupIds.length <
350
+ rule.minimumNegativeClaimSourceGroups ||
351
+ weight < rule.minimumNegativeWeightBasisPoints)
352
+ continue;
353
+ projections.push({
354
+ dimensionId: rule.dimensionId,
355
+ activationEvidence: sortedTuples(tuples),
356
+ activationDependencyGroupIds,
357
+ effectiveNegativeWeightBasisPoints: weight,
358
+ reviewIntervalMs: rule.reviewIntervalMs,
359
+ });
360
+ }
361
+ return deepFreeze(projections.sort((left, right) => compare(left.dimensionId, right.dimensionId)));
362
+ }
363
+ export function evaluateQuarantineRecoveryV1(state, quarantineValue, profileValue, decisionValue, policyValue, logicalTimeMs) {
364
+ const quarantine = validateQuarantineRecordV1(quarantineValue);
365
+ assertSafeInteger(logicalTimeMs, "logicalTimeMs");
366
+ const { profile, decision, policy } = validateProjectionBinding(state, profileValue, decisionValue, policyValue);
367
+ const exhaustedActive = quarantine.status === "active" &&
368
+ logicalTimeMs >= quarantine.reviewAfterLogicalMs &&
369
+ quarantine.revision >= state.limits.maximumQuarantineRevisionsPerHead;
370
+ if ((quarantine.status !== "review_required" && !exhaustedActive) ||
371
+ quarantine.policyDigest !== profile.policyDigest ||
372
+ quarantine.tenantId !== profile.tenantId ||
373
+ quarantine.subjectDigest !== profile.subjectDigest ||
374
+ quarantine.scopeDigest !== profile.scopeDigest ||
375
+ decision.evaluatedAtLogicalMs !== logicalTimeMs ||
376
+ logicalTimeMs < quarantine.reviewAfterLogicalMs)
377
+ throw new TrustValidationError("quarantine review binding is invalid");
378
+ const rule = policy.recoveryPolicy.rules.find((candidate) => candidate.dimensionId === quarantine.dimensionId);
379
+ const dimension = profile.dimensions.find((candidate) => candidate.dimensionId === quarantine.dimensionId);
380
+ const reasons = new Set();
381
+ const evidence = [];
382
+ const groups = new Set();
383
+ let weight = 0;
384
+ let unresolvedNegative = false;
385
+ let unresolvedRecoveryEvidence = false;
386
+ const blockerReasons = new Set();
387
+ const exactRecord = (recordId, recordDigest) => state.records.find((record) => record.recordId === recordId && record.recordDigest === recordDigest) ?? null;
388
+ const relationDigest = (record) => {
389
+ if ("claimRelationDigest" in record.record)
390
+ return record.record.claimRelationDigest;
391
+ if ("attestationRelationDigest" in record.record)
392
+ return record.record.attestationRelationDigest;
393
+ if ("challengeRelationDigest" in record.record)
394
+ return record.record.challengeRelationDigest;
395
+ return record.record.retractionRelationDigest;
396
+ };
397
+ const conflictedAtDecision = (record) => state.records.filter((candidate) => candidate.recordKind === record.recordKind &&
398
+ candidate.acceptedAtLogicalMs <= decision.evaluatedAtLogicalMs &&
399
+ relationDigest(candidate) === relationDigest(record)).length > 1;
400
+ const claimForRecord = (record) => {
401
+ if (record.recordKind === "claim")
402
+ return record;
403
+ if ((record.recordKind === "attestation" ||
404
+ record.recordKind === "retraction") &&
405
+ "claimId" in record.record &&
406
+ "claimDigest" in record.record)
407
+ return exactRecord(record.record.claimId, record.record.claimDigest);
408
+ if (record.recordKind === "challenge") {
409
+ if (!("targetId" in record.record))
410
+ return null;
411
+ const target = exactRecord(record.record.targetId, record.record.targetDigest);
412
+ if (!target)
413
+ return null;
414
+ if (target.recordKind === "claim")
415
+ return target;
416
+ if (target.recordKind === "attestation" &&
417
+ "claimId" in target.record &&
418
+ "claimDigest" in target.record)
419
+ return exactRecord(target.record.claimId, target.record.claimDigest);
420
+ }
421
+ if (record.recordKind === "retraction" && "targetId" in record.record) {
422
+ const target = exactRecord(record.record.targetId, record.record.targetDigest);
423
+ if (!target)
424
+ return null;
425
+ if (target.recordKind === "claim")
426
+ return target;
427
+ if (target.recordKind === "attestation" &&
428
+ "claimId" in target.record &&
429
+ "claimDigest" in target.record)
430
+ return exactRecord(target.record.claimId, target.record.claimDigest);
431
+ }
432
+ return null;
433
+ };
434
+ const relevantEquivocation = state.records.some((record) => {
435
+ if (!conflictedAtDecision(record) ||
436
+ record.acceptedAtLogicalMs <= quarantine.activatedAtLogicalMs ||
437
+ record.acceptedAtLogicalMs > decision.evaluatedAtLogicalMs ||
438
+ digestScopeV1(record.record.scope) !== quarantine.scopeDigest)
439
+ return false;
440
+ const claim = claimForRecord(record);
441
+ if (!claim ||
442
+ !("criterionId" in claim.record) ||
443
+ !("outcome" in claim.record))
444
+ return false;
445
+ if (digestSubjectV1(claim.record.subject) !== quarantine.subjectDigest)
446
+ return false;
447
+ const criterionId = claim.record.criterionId;
448
+ const criterion = policy.criteria.find((candidate) => candidate.criterionId === criterionId);
449
+ return (criterion?.dimensionId === quarantine.dimensionId &&
450
+ (criterion.quarantineEligible || criterion.recoveryEligible));
451
+ });
452
+ if (relevantEquivocation) {
453
+ unresolvedRecoveryEvidence = true;
454
+ blockerReasons.add("relationship_conflict");
455
+ }
456
+ for (const resolution of decision.challengeResolutions) {
457
+ if (resolution.result === "dismissed" || resolution.result === "sustained")
458
+ continue;
459
+ const blocksRecovery = resolution.challenges.some((reference) => {
460
+ const challenge = exactRecord(reference.challengeId, reference.challengeDigest);
461
+ if (!challenge ||
462
+ challenge.recordKind !== "challenge" ||
463
+ challenge.acceptedAtLogicalMs <= quarantine.activatedAtLogicalMs ||
464
+ challenge.acceptedAtLogicalMs > decision.evaluatedAtLogicalMs ||
465
+ digestScopeV1(challenge.record.scope) !== quarantine.scopeDigest)
466
+ return false;
467
+ const claim = claimForRecord(challenge);
468
+ if (!claim ||
469
+ !("criterionId" in claim.record) ||
470
+ digestSubjectV1(claim.record.subject) !== quarantine.subjectDigest)
471
+ return false;
472
+ const criterionId = claim.record.criterionId;
473
+ const criterion = policy.criteria.find((candidate) => candidate.criterionId === criterionId);
474
+ return (criterion?.dimensionId === quarantine.dimensionId &&
475
+ (criterion.quarantineEligible || criterion.recoveryEligible));
476
+ });
477
+ if (!blocksRecovery)
478
+ continue;
479
+ unresolvedRecoveryEvidence = true;
480
+ blockerReasons.add(resolution.result === "contested"
481
+ ? "challenge_contested"
482
+ : "challenge_unresolved");
483
+ }
484
+ if (rule && dimension) {
485
+ for (const classification of decision.claimClassifications) {
486
+ if (classification.dimensionId !== quarantine.dimensionId)
487
+ continue;
488
+ const criterion = policy.criteria.find((candidate) => candidate.criterionId === classification.criterionId);
489
+ const claim = state.records.find((record) => record.recordKind === "claim" &&
490
+ record.recordId === classification.claimId &&
491
+ record.recordDigest === classification.claimDigest);
492
+ if (!criterion || !claim || !("outcome" in claim.record))
493
+ continue;
494
+ const included = dimension.includedClaimIds.includes(classification.claimId);
495
+ const claimConflicted = conflictedAtDecision(claim);
496
+ const negativeExcludedAsUnresolved = decision.recordExclusions.some((exclusion) => exclusion.recordKind === "claim" &&
497
+ exclusion.recordId === classification.claimId &&
498
+ exclusion.recordDigest === classification.claimDigest &&
499
+ (exclusion.reasonCodes.includes("root_basis_conflict") ||
500
+ exclusion.reasonCodes.includes("challenge_unresolved") ||
501
+ exclusion.reasonCodes.includes("fusion_contested")));
502
+ if (claim.record.outcome === "violated" &&
503
+ criterion.quarantineEligible &&
504
+ claim.acceptedAtLogicalMs > quarantine.activatedAtLogicalMs &&
505
+ (claimConflicted ||
506
+ (classification.classification === "supported" && included) ||
507
+ classification.classification === "contested" ||
508
+ (classification.classification === "unavailable" &&
509
+ (classification.reasonCodes.includes("challenge_unresolved") ||
510
+ classification.reasonCodes.includes("root_basis_conflict"))) ||
511
+ negativeExcludedAsUnresolved))
512
+ unresolvedNegative = true;
513
+ if (claim.record.outcome === "violated" &&
514
+ claimConflicted &&
515
+ claim.acceptedAtLogicalMs > quarantine.activatedAtLogicalMs)
516
+ blockerReasons.add("relationship_conflict");
517
+ const recoveryClaimUnresolved = claim.record.outcome === "satisfied" &&
518
+ criterion.recoveryEligible &&
519
+ claim.acceptedAtLogicalMs > quarantine.activatedAtLogicalMs &&
520
+ (classification.classification === "contested" ||
521
+ (classification.classification === "unavailable" &&
522
+ classification.reasonCodes.includes("challenge_unresolved")) ||
523
+ negativeExcludedAsUnresolved);
524
+ const recoverySupportUnresolved = decision.recordExclusions.some((exclusion) => {
525
+ if (exclusion.recordKind !== "attestation" ||
526
+ !(exclusion.reasonCodes.includes("challenge_unresolved") ||
527
+ exclusion.reasonCodes.includes("dependency_group_conflict")))
528
+ return false;
529
+ const attestation = state.records.find((record) => record.recordKind === "attestation" &&
530
+ record.recordId === exclusion.recordId &&
531
+ record.recordDigest === exclusion.recordDigest &&
532
+ "claimId" in record.record &&
533
+ record.record.claimId === claim.recordId &&
534
+ record.acceptedAtLogicalMs > quarantine.activatedAtLogicalMs);
535
+ if (attestation)
536
+ for (const reason of exclusion.reasonCodes)
537
+ if (reason === "challenge_unresolved" ||
538
+ reason === "dependency_group_conflict")
539
+ blockerReasons.add(reason);
540
+ return attestation !== undefined;
541
+ });
542
+ if (recoveryClaimUnresolved || recoverySupportUnresolved) {
543
+ unresolvedRecoveryEvidence = true;
544
+ if (classification.classification === "contested")
545
+ blockerReasons.add("fusion_contested");
546
+ for (const exclusion of decision.recordExclusions)
547
+ if (exclusion.recordId === classification.claimId &&
548
+ exclusion.recordDigest === classification.claimDigest)
549
+ for (const reason of exclusion.reasonCodes)
550
+ if (reason === "root_basis_conflict" ||
551
+ reason === "challenge_unresolved" ||
552
+ reason === "fusion_contested")
553
+ blockerReasons.add(reason);
554
+ }
555
+ const selfClaim = claim.record.sourceKind === "peer" &&
556
+ claim.record.sourceId === decision.subject.peerId;
557
+ if (claim.record.outcome !== "satisfied" ||
558
+ !criterion.recoveryEligible ||
559
+ selfClaim ||
560
+ !included ||
561
+ classification.classification !== "supported" ||
562
+ classification.effectiveWeightBasisPoints <= 0 ||
563
+ classification.claimSourceDependencyGroupId === null ||
564
+ quarantine.activationDependencyGroupIds.includes(classification.claimSourceDependencyGroupId) ||
565
+ claim.acceptedAtLogicalMs <= quarantine.activatedAtLogicalMs ||
566
+ logicalTimeMs - claim.effectiveAtLogicalMs >
567
+ rule.maximumRecoveryEvidenceAgeMs)
568
+ continue;
569
+ const claimEvidence = exactClaimEvidence(state, decision, classification, quarantine.activatedAtLogicalMs);
570
+ if (!claimEvidence)
571
+ continue;
572
+ groups.add(classification.claimSourceDependencyGroupId);
573
+ weight = Math.min(10_000, weight + classification.effectiveWeightBasisPoints);
574
+ evidence.push(...claimEvidence);
575
+ }
576
+ }
577
+ if (unresolvedNegative)
578
+ blockerReasons.add("challenge_unresolved");
579
+ for (const reason of blockerReasons)
580
+ reasons.add(reason);
581
+ const recoveryEvidence = sortedTuples(evidence);
582
+ const recoveryGroups = [...groups].sort(compare);
583
+ const revisionAvailable = quarantine.revision < state.limits.maximumQuarantineRevisionsPerHead;
584
+ let disposition;
585
+ if (!rule || !dimension || !revisionAvailable) {
586
+ disposition = "unavailable";
587
+ reasons.add("quarantine_recovery_unavailable");
588
+ }
589
+ else if (unresolvedNegative ||
590
+ unresolvedRecoveryEvidence ||
591
+ dimension.scoreBasisPoints < rule.recoveryScoreAtOrAboveBasisPoints ||
592
+ dimension.uncertaintyBasisPoints >
593
+ rule.maximumRecoveryUncertaintyBasisPoints ||
594
+ recoveryGroups.length < rule.minimumRecoveryClaimSourceGroups ||
595
+ weight < rule.minimumRecoveryWeightBasisPoints) {
596
+ disposition = "insufficient";
597
+ reasons.add("quarantine_recovery_insufficient");
598
+ }
599
+ else {
600
+ disposition = "recovered";
601
+ reasons.add("quarantine_recovered");
602
+ }
603
+ return createQuarantineRecoveryDecisionV1({
604
+ schemaVersion: 1,
605
+ quarantineId: quarantine.quarantineId,
606
+ quarantineKey: quarantine.quarantineKey,
607
+ policyDigest: quarantine.policyDigest,
608
+ fusionDecisionId: decision.fusionDecisionId,
609
+ evaluatedAtLogicalMs: logicalTimeMs,
610
+ recoveryEvidenceIds: recoveryEvidence.map((item) => item.id).sort(compare),
611
+ recoveryEvidenceSetDigest: digestTrustRecoveryEvidenceSetV1(recoveryEvidence),
612
+ recoveryClaimSourceDependencyGroupIds: recoveryGroups,
613
+ effectiveRecoveryWeightBasisPoints: weight,
614
+ scoreBasisPoints: dimension?.scoreBasisPoints ?? 0,
615
+ uncertaintyBasisPoints: dimension?.uncertaintyBasisPoints ?? 10_000,
616
+ disposition,
617
+ reasonCodes: [...reasons].sort(compare),
618
+ });
619
+ }
620
+ export function createReviewRequiredQuarantineRecordV1(value, reviewedAtLogicalMs) {
621
+ const current = validateQuarantineRecordV1(value);
622
+ assertSafeInteger(reviewedAtLogicalMs, "reviewedAtLogicalMs");
623
+ if (current.status !== "active" ||
624
+ reviewedAtLogicalMs < current.reviewAfterLogicalMs)
625
+ throw new TrustValidationError("quarantine is not ready for review");
626
+ return createQuarantineRecordV1({
627
+ ...quarantineRecordBody(current),
628
+ revision: current.revision + 1,
629
+ previousRecordId: current.quarantineId,
630
+ reasonCodes: ["quarantine_review_required"],
631
+ status: "review_required",
632
+ });
633
+ }
634
+ function recoveryDecisionBody(value) {
635
+ const { recoveryDecisionId: _id, recoveryDecisionDigest: _digest, ...body } = value;
636
+ return body;
637
+ }
638
+ export function digestQuarantineRecoveryDecisionV1(value) {
639
+ return digestTrustJsonV1("recovery-decision", recoveryDecisionBody(value));
640
+ }
641
+ export function validateQuarantineRecoveryDecisionV1(value) {
642
+ assertExactKeys(value, recoveryKeys, "recovery decision");
643
+ const decision = value;
644
+ if (decision.schemaVersion !== 1)
645
+ throw new TrustValidationError("recovery schema is invalid");
646
+ assertContentId(decision.recoveryDecisionId, "recovery-decision", "recoveryDecisionId");
647
+ assertContentId(decision.quarantineId, "quarantine-record", "quarantineId");
648
+ assertContentId(decision.fusionDecisionId, "fusion-decision", "fusionDecisionId");
649
+ for (const key of [
650
+ "recoveryDecisionDigest",
651
+ "quarantineKey",
652
+ "policyDigest",
653
+ "recoveryEvidenceSetDigest",
654
+ ])
655
+ assertTrustDigest(decision[key], key);
656
+ assertSafeInteger(decision.evaluatedAtLogicalMs, "evaluatedAtLogicalMs");
657
+ validateSortedIdentifiers(decision.recoveryEvidenceIds, "recovery evidence");
658
+ validateSortedIdentifiers(decision.recoveryClaimSourceDependencyGroupIds, "recovery groups");
659
+ for (const key of [
660
+ "effectiveRecoveryWeightBasisPoints",
661
+ "scoreBasisPoints",
662
+ "uncertaintyBasisPoints",
663
+ ]) {
664
+ assertSafeInteger(decision[key], key);
665
+ if (decision[key] > 10_000)
666
+ throw new TrustValidationError("recovery basis points are invalid");
667
+ }
668
+ if (!(decision.disposition === "unavailable" ||
669
+ decision.disposition === "insufficient" ||
670
+ decision.disposition === "recovered"))
671
+ throw new TrustValidationError("recovery disposition is invalid");
672
+ const reasons = validateSortedReasons(decision.reasonCodes, "recovery reasons");
673
+ const primaryReason = {
674
+ unavailable: "quarantine_recovery_unavailable",
675
+ insufficient: "quarantine_recovery_insufficient",
676
+ recovered: "quarantine_recovered",
677
+ }[decision.disposition];
678
+ if (!reasons.includes(primaryReason))
679
+ throw new TrustValidationError("recovery disposition reason is invalid");
680
+ if (decision.disposition === "recovered" &&
681
+ (decision.recoveryEvidenceIds.length === 0 ||
682
+ decision.recoveryClaimSourceDependencyGroupIds.length === 0 ||
683
+ decision.effectiveRecoveryWeightBasisPoints === 0))
684
+ throw new TrustValidationError("recovered decision has no evidence");
685
+ if (decision.recoveryDecisionDigest !==
686
+ digestQuarantineRecoveryDecisionV1(decision) ||
687
+ decision.recoveryDecisionId !==
688
+ `recovery-decision:${decision.recoveryDecisionDigest}`)
689
+ throw new TrustValidationError("recovery digest is invalid");
690
+ return deepFreeze(structuredClone(decision));
691
+ }
692
+ export function createQuarantineRecoveryDecisionV1(input) {
693
+ const digest = digestTrustJsonV1("recovery-decision", input);
694
+ return validateQuarantineRecoveryDecisionV1({
695
+ ...input,
696
+ recoveryDecisionDigest: digest,
697
+ recoveryDecisionId: `recovery-decision:${digest}`,
698
+ });
699
+ }
700
+ export function createRecoveredQuarantineRecordV1(value, decisionValue) {
701
+ const current = validateQuarantineRecordV1(value);
702
+ const decision = validateQuarantineRecoveryDecisionV1(decisionValue);
703
+ if (current.status !== "review_required" ||
704
+ decision.disposition !== "recovered" ||
705
+ decision.quarantineId !== current.quarantineId ||
706
+ decision.quarantineKey !== current.quarantineKey ||
707
+ decision.policyDigest !== current.policyDigest ||
708
+ decision.evaluatedAtLogicalMs < current.reviewAfterLogicalMs)
709
+ throw new TrustValidationError("recovery decision does not bind the current quarantine");
710
+ return createQuarantineRecordV1({
711
+ ...quarantineRecordBody(current),
712
+ revision: current.revision + 1,
713
+ previousRecordId: current.quarantineId,
714
+ fusionDecisionId: decision.fusionDecisionId,
715
+ reasonCodes: ["quarantine_recovered"],
716
+ status: "recovered",
717
+ recoveredAtLogicalMs: decision.evaluatedAtLogicalMs,
718
+ recoveryDecisionId: decision.recoveryDecisionId,
719
+ });
720
+ }
721
+ export function quarantineHeadV1(value) {
722
+ const record = validateQuarantineRecordV1(value);
723
+ return deepFreeze({
724
+ quarantineKey: record.quarantineKey,
725
+ quarantineId: record.quarantineId,
726
+ revision: record.revision,
727
+ status: record.status,
728
+ });
729
+ }
730
+ export function validateQuarantineHeadV1(value) {
731
+ assertExactKeys(value, ["quarantineKey", "quarantineId", "revision", "status"], "quarantine head");
732
+ const head = value;
733
+ assertTrustDigest(head.quarantineKey, "quarantineKey");
734
+ assertContentId(head.quarantineId, "quarantine-record", "quarantineId");
735
+ assertSafeInteger(head.revision, "revision", 1);
736
+ if (!(head.status === "active" ||
737
+ head.status === "review_required" ||
738
+ head.status === "recovered"))
739
+ throw new TrustValidationError("quarantine head status is invalid");
740
+ return deepFreeze(structuredClone(head));
741
+ }
742
+ //# sourceMappingURL=quarantine.js.map