@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.
- package/LICENSE +202 -0
- package/README.md +14 -0
- package/dist/canonical.d.ts +20 -0
- package/dist/canonical.d.ts.map +1 -0
- package/dist/canonical.js +173 -0
- package/dist/canonical.js.map +1 -0
- package/dist/causal.d.ts +7 -0
- package/dist/causal.d.ts.map +1 -0
- package/dist/causal.js +155 -0
- package/dist/causal.js.map +1 -0
- package/dist/eligibility.d.ts +8 -0
- package/dist/eligibility.d.ts.map +1 -0
- package/dist/eligibility.js +325 -0
- package/dist/eligibility.js.map +1 -0
- package/dist/evidence.d.ts +20 -0
- package/dist/evidence.d.ts.map +1 -0
- package/dist/evidence.js +424 -0
- package/dist/evidence.js.map +1 -0
- package/dist/fusion.d.ts +8 -0
- package/dist/fusion.d.ts.map +1 -0
- package/dist/fusion.js +1278 -0
- package/dist/fusion.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/lifecycle.d.ts +41 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +1369 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/mesh-records.d.ts +36 -0
- package/dist/mesh-records.d.ts.map +1 -0
- package/dist/mesh-records.js +133 -0
- package/dist/mesh-records.js.map +1 -0
- package/dist/policy.d.ts +9 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +598 -0
- package/dist/policy.js.map +1 -0
- package/dist/profile.d.ts +21 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +253 -0
- package/dist/profile.js.map +1 -0
- package/dist/quarantine.d.ts +53 -0
- package/dist/quarantine.d.ts.map +1 -0
- package/dist/quarantine.js +742 -0
- package/dist/quarantine.js.map +1 -0
- package/dist/sha256.d.ts +2 -0
- package/dist/sha256.d.ts.map +1 -0
- package/dist/sha256.js +66 -0
- package/dist/sha256.js.map +1 -0
- package/dist/state.d.ts +18 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +1015 -0
- package/dist/state.js.map +1 -0
- package/dist/types.d.ts +862 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +60 -0
- package/dist/types.js.map +1 -0
- package/dist/validation.d.ts +18 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +223 -0
- package/dist/validation.js.map +1 -0
- package/package.json +43 -0
package/dist/fusion.js
ADDED
|
@@ -0,0 +1,1278 @@
|
|
|
1
|
+
import { deepFreeze, digestTrustJsonV1, TrustValidationError, } from "./canonical.js";
|
|
2
|
+
import { digestRootBasisV1, digestScopeV1, digestSubjectV1, } from "./evidence.js";
|
|
3
|
+
import { digestEvidenceFusionPolicyV1 } from "./policy.js";
|
|
4
|
+
import { causalAuthorizationReferenceKeyV1 } from "./causal.js";
|
|
5
|
+
import { assertExactKeys, assertIdentifier, assertSafeInteger, assertTrustDigest, validateEvidenceScopeV1, validateReasonCodeV1, validateTrustSubjectV1, } from "./validation.js";
|
|
6
|
+
const compare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
7
|
+
const FUSION_PROJECTION_JSON_LIMITS_V1 = Object.freeze({
|
|
8
|
+
maximumBytes: 16_777_216,
|
|
9
|
+
maximumDepth: 64,
|
|
10
|
+
maximumNodes: 100_000,
|
|
11
|
+
maximumKeysPerObject: 256,
|
|
12
|
+
maximumItemsPerArray: 4_096,
|
|
13
|
+
});
|
|
14
|
+
const MAXIMUM_FUSION_RECORD_ITEMS_V1 = 1_024;
|
|
15
|
+
const MAXIMUM_FUSION_GROUP_ALLOCATIONS_V1 = 4_096;
|
|
16
|
+
const MAXIMUM_FUSION_DIMENSIONS_V1 = 16;
|
|
17
|
+
const MAXIMUM_FUSION_DEPENDENCY_GROUPS_V1 = 64;
|
|
18
|
+
const MAXIMUM_FUSION_CONTENT_RESOLUTIONS_V1 = 4_096;
|
|
19
|
+
const MAXIMUM_FUSION_REASON_CODES_V1 = 256;
|
|
20
|
+
const sorted = (values, key) => [...values].sort((a, b) => compare(key(a), key(b)));
|
|
21
|
+
const uniqueSorted = (values, label) => {
|
|
22
|
+
const out = [...values].sort(compare);
|
|
23
|
+
if (out.some((value, index) => index && value === out[index - 1]))
|
|
24
|
+
throw new TrustValidationError(`${label} must be sorted and unique`);
|
|
25
|
+
return out;
|
|
26
|
+
};
|
|
27
|
+
const assertSortedUnique = (values, label, maximumItems = MAXIMUM_FUSION_GROUP_ALLOCATIONS_V1) => {
|
|
28
|
+
if (!Array.isArray(values))
|
|
29
|
+
throw new TrustValidationError(`${label} is invalid`);
|
|
30
|
+
if (values.length > maximumItems)
|
|
31
|
+
throw new TrustValidationError(`${label} capacity exceeded`);
|
|
32
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
33
|
+
if (typeof values[index] !== "string" ||
|
|
34
|
+
(index > 0 &&
|
|
35
|
+
compare(values[index - 1], values[index]) >= 0))
|
|
36
|
+
throw new TrustValidationError(`${label} must be sorted and unique`);
|
|
37
|
+
}
|
|
38
|
+
return values;
|
|
39
|
+
};
|
|
40
|
+
const assertCanonicalItems = (values, label, key) => {
|
|
41
|
+
for (let index = 1; index < values.length; index += 1)
|
|
42
|
+
if (compare(key(values[index - 1]), key(values[index])) >= 0)
|
|
43
|
+
throw new TrustValidationError(`${label} must be sorted and unique`);
|
|
44
|
+
};
|
|
45
|
+
const assertBasisPoints = (value, label) => {
|
|
46
|
+
assertSafeInteger(value, label);
|
|
47
|
+
if (value < 0 || value > 10_000)
|
|
48
|
+
throw new TrustValidationError(`${label} is outside basis-point range`);
|
|
49
|
+
return value;
|
|
50
|
+
};
|
|
51
|
+
const add = (a, b) => {
|
|
52
|
+
const value = a + b;
|
|
53
|
+
if (!Number.isSafeInteger(value))
|
|
54
|
+
throw new TrustValidationError("fusion arithmetic overflow");
|
|
55
|
+
return value;
|
|
56
|
+
};
|
|
57
|
+
const product = (a, b) => {
|
|
58
|
+
const value = a * b;
|
|
59
|
+
if (!Number.isSafeInteger(value))
|
|
60
|
+
throw new TrustValidationError("fusion arithmetic overflow");
|
|
61
|
+
return value;
|
|
62
|
+
};
|
|
63
|
+
const clamp = (value) => Math.max(0, Math.min(10_000, value));
|
|
64
|
+
const kindOf = (record) => record.recordKind;
|
|
65
|
+
const target = (record) => {
|
|
66
|
+
if (record.recordKind === "attestation")
|
|
67
|
+
return {
|
|
68
|
+
id: record.record.claimId,
|
|
69
|
+
digest: record.record.claimDigest,
|
|
70
|
+
kind: "claim",
|
|
71
|
+
};
|
|
72
|
+
if (record.recordKind === "challenge" || record.recordKind === "retraction")
|
|
73
|
+
return {
|
|
74
|
+
id: record.record.targetId,
|
|
75
|
+
digest: record.record.targetDigest,
|
|
76
|
+
kind: record.record
|
|
77
|
+
.targetKind,
|
|
78
|
+
};
|
|
79
|
+
return null;
|
|
80
|
+
};
|
|
81
|
+
const sourceKey = (record) => `${record.record.sourceKind}\u0000${record.record.sourceId}`;
|
|
82
|
+
const recordKey = (record) => `${record.recordId}\u0000${record.recordDigest}`;
|
|
83
|
+
const isActive = (record) => record.status === "active";
|
|
84
|
+
const isClaim = (record) => record.recordKind === "claim";
|
|
85
|
+
function exactScope(record, scopeDigest) {
|
|
86
|
+
return digestScopeV1(record.record.scope) === scopeDigest;
|
|
87
|
+
}
|
|
88
|
+
function selectedBinding(state, digest, policyDigest, logicalTimeMs) {
|
|
89
|
+
const binding = state.dependencyBindings.find((item) => item.bindingDigest === digest);
|
|
90
|
+
if (!binding ||
|
|
91
|
+
binding.policyDigest !== policyDigest ||
|
|
92
|
+
!deriveApplicableBindingDigests(state, policyDigest, logicalTimeMs).includes(binding.bindingDigest) ||
|
|
93
|
+
binding.validFromLogicalMs > logicalTimeMs ||
|
|
94
|
+
(binding.validUntilLogicalMs !== null &&
|
|
95
|
+
logicalTimeMs >= binding.validUntilLogicalMs))
|
|
96
|
+
throw new TrustValidationError("fusion dependency binding is unavailable");
|
|
97
|
+
return binding;
|
|
98
|
+
}
|
|
99
|
+
export function deriveApplicableBindingDigests(state, policyDigest, logicalTimeMs) {
|
|
100
|
+
const histories = new Map();
|
|
101
|
+
for (const binding of state.dependencyBindings) {
|
|
102
|
+
const key = `${binding.bindingKind}\u0000${binding.bindingName}`;
|
|
103
|
+
const history = histories.get(key) ?? [];
|
|
104
|
+
history.push(binding);
|
|
105
|
+
histories.set(key, history);
|
|
106
|
+
}
|
|
107
|
+
const heads = [];
|
|
108
|
+
for (const history of histories.values()) {
|
|
109
|
+
const visible = history.filter((binding) => binding.registeredAtLogicalMs <= logicalTimeMs);
|
|
110
|
+
if (!visible.some((binding) => binding.policyDigest === policyDigest))
|
|
111
|
+
continue;
|
|
112
|
+
const head = visible.sort((left, right) => right.bindingVersion - left.bindingVersion)[0];
|
|
113
|
+
if (!head ||
|
|
114
|
+
head.policyDigest !== policyDigest ||
|
|
115
|
+
head.validFromLogicalMs > logicalTimeMs ||
|
|
116
|
+
(head.validUntilLogicalMs !== null &&
|
|
117
|
+
logicalTimeMs >= head.validUntilLogicalMs))
|
|
118
|
+
throw new TrustValidationError("fusion dependency binding head is unavailable");
|
|
119
|
+
heads.push(head);
|
|
120
|
+
}
|
|
121
|
+
return uniqueSorted(heads.map((binding) => binding.bindingDigest), "applicable dependency bindings");
|
|
122
|
+
}
|
|
123
|
+
function reasonExclusion(record, reason) {
|
|
124
|
+
return {
|
|
125
|
+
recordKind: kindOf(record),
|
|
126
|
+
recordId: record.recordId,
|
|
127
|
+
recordDigest: record.recordDigest,
|
|
128
|
+
reasonCodes: [reason],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function sourceBinding(policy, record, role, time) {
|
|
132
|
+
const binding = policy.sourceBindings.find((item) => item.sourceId === record.record.sourceId &&
|
|
133
|
+
item.sourceKind === record.record.sourceKind);
|
|
134
|
+
if (!binding ||
|
|
135
|
+
!binding.roles.includes(role) ||
|
|
136
|
+
binding.validFromLogicalMs > time ||
|
|
137
|
+
time >= binding.validUntilLogicalMs)
|
|
138
|
+
return null;
|
|
139
|
+
return binding;
|
|
140
|
+
}
|
|
141
|
+
function usableContentResolutions(state, claim, resolverDigest, logicalTimeMs) {
|
|
142
|
+
const content = claim.record.content;
|
|
143
|
+
if (!content || content.kind !== "reference" || !resolverDigest)
|
|
144
|
+
return [];
|
|
145
|
+
return state.contentResolutions
|
|
146
|
+
.filter((resolution) => resolution.result === "verified" &&
|
|
147
|
+
resolution.resolvedAtLogicalMs <= logicalTimeMs &&
|
|
148
|
+
resolution.resolverBindingDigest === resolverDigest &&
|
|
149
|
+
resolution.claimId === claim.recordId &&
|
|
150
|
+
resolution.claimDigest === claim.recordDigest &&
|
|
151
|
+
resolution.scopeDigest === digestScopeV1(claim.record.scope) &&
|
|
152
|
+
resolution.referenceId === content.reference.referenceId &&
|
|
153
|
+
resolution.referenceDigest === content.reference.referenceDigest &&
|
|
154
|
+
resolution.contentDigest === content.contentDigest &&
|
|
155
|
+
resolution.mediaType === content.mediaType &&
|
|
156
|
+
resolution.encodedBytes === content.encodedBytes &&
|
|
157
|
+
!state.contentInvalidations.some((invalidation) => invalidation.invalidatedAtLogicalMs <= logicalTimeMs &&
|
|
158
|
+
invalidation.resolutionId === resolution.resolutionId &&
|
|
159
|
+
invalidation.resolutionDigest === resolution.resolutionDigest &&
|
|
160
|
+
invalidation.resolverBindingDigest === resolverDigest))
|
|
161
|
+
.sort((left, right) => compare(left.resolutionId, right.resolutionId));
|
|
162
|
+
}
|
|
163
|
+
function contentUsable(state, claim, resolverDigest, logicalTimeMs) {
|
|
164
|
+
const content = claim.record.content;
|
|
165
|
+
return (content === null ||
|
|
166
|
+
content?.kind === "inline_summary" ||
|
|
167
|
+
usableContentResolutions(state, claim, resolverDigest, logicalTimeMs)
|
|
168
|
+
.length > 0);
|
|
169
|
+
}
|
|
170
|
+
function recordReferenceKeys(record) {
|
|
171
|
+
const references = record.record.basisReferences ?? [];
|
|
172
|
+
return references.map(causalAuthorizationReferenceKeyV1);
|
|
173
|
+
}
|
|
174
|
+
function authorizationMatches(state, record, policyDigest, criterionId, scopeDigest, subjectDigest, allowedRelations, targetRecord, logicalTimeMs, applicableBindingDigests) {
|
|
175
|
+
const recordReferences = recordReferenceKeys(record);
|
|
176
|
+
return (sorted(state.causalAuthorizations.filter((authorization) => authorization.authorizedAtLogicalMs <= logicalTimeMs &&
|
|
177
|
+
applicableBindingDigests.has(authorization.authorityBindingDigest) &&
|
|
178
|
+
authorization.recordId === record.recordId &&
|
|
179
|
+
authorization.recordDigest === record.recordDigest &&
|
|
180
|
+
authorization.recordKind === record.recordKind &&
|
|
181
|
+
authorization.policyDigest === policyDigest &&
|
|
182
|
+
authorization.criterionId === criterionId &&
|
|
183
|
+
authorization.scopeDigest === scopeDigest &&
|
|
184
|
+
authorization.subjectDigest === subjectDigest &&
|
|
185
|
+
allowedRelations.includes(authorization.sourceRelation) &&
|
|
186
|
+
authorization.targetRecordId === (targetRecord?.recordId ?? null) &&
|
|
187
|
+
authorization.targetRecordDigest ===
|
|
188
|
+
(targetRecord?.recordDigest ?? null) &&
|
|
189
|
+
authorization.bases.length === recordReferences.length &&
|
|
190
|
+
authorization.bases.every((basis, index) => basis.trustedEffectiveAtLogicalMs <=
|
|
191
|
+
authorization.authorizedAtLogicalMs &&
|
|
192
|
+
causalAuthorizationReferenceKeyV1(basis) ===
|
|
193
|
+
recordReferences[index] &&
|
|
194
|
+
(basis.kind !== "evidence" ||
|
|
195
|
+
state.records.some((item) => item.recordId === basis.referenceId &&
|
|
196
|
+
item.recordDigest === basis.referenceDigest &&
|
|
197
|
+
item.recordKind === basis.referenceType &&
|
|
198
|
+
digestScopeV1(item.record.scope) === scopeDigest &&
|
|
199
|
+
basis.resolvedDigest === item.recordDigest &&
|
|
200
|
+
basis.trustedEffectiveAtLogicalMs ===
|
|
201
|
+
item.effectiveAtLogicalMs)))), (authorization) => authorization.authorizationDigest)[0] ?? null);
|
|
202
|
+
}
|
|
203
|
+
function effectiveRootBasisDigest(state, record, policyDigest, logicalTimeMs, recordByKey, applicableBindingDigests) {
|
|
204
|
+
const visit = (candidate, seen) => {
|
|
205
|
+
const key = recordKey(candidate);
|
|
206
|
+
if (seen.has(key))
|
|
207
|
+
return null;
|
|
208
|
+
const authorization = sorted(state.causalAuthorizations.filter((item) => item.authorizedAtLogicalMs <= logicalTimeMs &&
|
|
209
|
+
applicableBindingDigests.has(item.authorityBindingDigest) &&
|
|
210
|
+
item.policyDigest === policyDigest &&
|
|
211
|
+
item.recordId === candidate.recordId &&
|
|
212
|
+
item.recordDigest === candidate.recordDigest &&
|
|
213
|
+
item.bases.length === recordReferenceKeys(candidate).length &&
|
|
214
|
+
item.bases.every((basis, index) => basis.trustedEffectiveAtLogicalMs <= item.authorizedAtLogicalMs &&
|
|
215
|
+
causalAuthorizationReferenceKeyV1(basis) ===
|
|
216
|
+
recordReferenceKeys(candidate)[index])), (item) => item.authorizationDigest)[0];
|
|
217
|
+
if (!authorization)
|
|
218
|
+
return null;
|
|
219
|
+
const terminal = authorization.bases.filter((basis) => basis.kind !== "evidence");
|
|
220
|
+
const nested = [
|
|
221
|
+
...terminal,
|
|
222
|
+
];
|
|
223
|
+
for (const basis of authorization.bases.filter((item) => item.kind === "evidence")) {
|
|
224
|
+
const referenced = recordByKey.get(`${basis.referenceId}\u0000${basis.referenceDigest}`);
|
|
225
|
+
if (!referenced ||
|
|
226
|
+
!isActive(referenced) ||
|
|
227
|
+
referenced.recordKind !== basis.referenceType ||
|
|
228
|
+
digestScopeV1(referenced.record.scope) !==
|
|
229
|
+
digestScopeV1(candidate.record.scope) ||
|
|
230
|
+
basis.resolvedDigest !== referenced.recordDigest ||
|
|
231
|
+
basis.trustedEffectiveAtLogicalMs !== referenced.effectiveAtLogicalMs)
|
|
232
|
+
return null;
|
|
233
|
+
const roots = visit(referenced, new Set([...seen, key]));
|
|
234
|
+
if (!roots)
|
|
235
|
+
return null;
|
|
236
|
+
nested.push(...roots);
|
|
237
|
+
}
|
|
238
|
+
return nested;
|
|
239
|
+
};
|
|
240
|
+
const roots = visit(record, new Set());
|
|
241
|
+
if (!roots || roots.length === 0)
|
|
242
|
+
return null;
|
|
243
|
+
const unique = new Map(roots.map((root) => {
|
|
244
|
+
const reference = {
|
|
245
|
+
schemaVersion: 1,
|
|
246
|
+
kind: root.kind,
|
|
247
|
+
referenceType: root.referenceType,
|
|
248
|
+
referenceId: root.referenceId,
|
|
249
|
+
referenceDigest: root.referenceDigest,
|
|
250
|
+
};
|
|
251
|
+
return [causalAuthorizationReferenceKeyV1(reference), reference];
|
|
252
|
+
}));
|
|
253
|
+
return digestRootBasisV1(sorted([...unique.values()], causalAuthorizationReferenceKeyV1));
|
|
254
|
+
}
|
|
255
|
+
function authorizationBasisCutoff(record, allowed, authorization) {
|
|
256
|
+
const refs = record.record
|
|
257
|
+
.basisReferences;
|
|
258
|
+
const counts = new Map();
|
|
259
|
+
let cutoff = record.effectiveAtLogicalMs;
|
|
260
|
+
for (const reference of refs) {
|
|
261
|
+
counts.set(`${reference.kind}\u0000${reference.referenceType}`, (counts.get(`${reference.kind}\u0000${reference.referenceType}`) ?? 0) +
|
|
262
|
+
1);
|
|
263
|
+
const certified = authorization.bases.find((basis) => causalAuthorizationReferenceKeyV1(basis) ===
|
|
264
|
+
causalAuthorizationReferenceKeyV1(reference));
|
|
265
|
+
if (!certified)
|
|
266
|
+
return null;
|
|
267
|
+
cutoff = Math.max(cutoff, certified.trustedEffectiveAtLogicalMs);
|
|
268
|
+
}
|
|
269
|
+
for (const rule of allowed.allowedBasisReferences) {
|
|
270
|
+
const count = counts.get(`${rule.kind}\u0000${rule.referenceType}`) ?? 0;
|
|
271
|
+
if (count < rule.minimumCount || count > rule.maximumCount)
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
for (const key of counts.keys())
|
|
275
|
+
if (!allowed.allowedBasisReferences.some((rule) => `${rule.kind}\u0000${rule.referenceType}` === key))
|
|
276
|
+
return null;
|
|
277
|
+
return cutoff;
|
|
278
|
+
}
|
|
279
|
+
function validateRequest(value) {
|
|
280
|
+
assertExactKeys(value, [
|
|
281
|
+
"tenantId",
|
|
282
|
+
"subject",
|
|
283
|
+
"scope",
|
|
284
|
+
"policyId",
|
|
285
|
+
"policyVersion",
|
|
286
|
+
"policyDigest",
|
|
287
|
+
"dependencyBindingDigests",
|
|
288
|
+
], "fusion request");
|
|
289
|
+
const request = value;
|
|
290
|
+
assertIdentifier(request.tenantId, "tenantId");
|
|
291
|
+
const subject = validateTrustSubjectV1(request.subject);
|
|
292
|
+
const scope = validateEvidenceScopeV1(request.scope);
|
|
293
|
+
if (scope.tenantId !== request.tenantId)
|
|
294
|
+
throw new TrustValidationError("fusion tenant does not match scope");
|
|
295
|
+
assertIdentifier(request.policyId, "policyId");
|
|
296
|
+
assertSafeInteger(request.policyVersion, "policyVersion", 1);
|
|
297
|
+
assertTrustDigest(request.policyDigest, "policyDigest");
|
|
298
|
+
if (!Array.isArray(request.dependencyBindingDigests))
|
|
299
|
+
throw new TrustValidationError("dependencyBindingDigests is invalid");
|
|
300
|
+
const dependencyBindingDigests = request.dependencyBindingDigests.map((item) => {
|
|
301
|
+
assertTrustDigest(item, "dependencyBindingDigest");
|
|
302
|
+
return item;
|
|
303
|
+
});
|
|
304
|
+
uniqueSorted(dependencyBindingDigests, "dependencyBindingDigests");
|
|
305
|
+
return deepFreeze({
|
|
306
|
+
...request,
|
|
307
|
+
subject,
|
|
308
|
+
scope,
|
|
309
|
+
dependencyBindingDigests,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
function decisionBody(decision) {
|
|
313
|
+
const { fusionDecisionId: _id, fusionDecisionDigest: _digest, ...body } = decision;
|
|
314
|
+
return body;
|
|
315
|
+
}
|
|
316
|
+
export function digestEvidenceFusionDecisionV1(decision) {
|
|
317
|
+
return digestTrustJsonV1("fusion-decision", decisionBody(decision), FUSION_PROJECTION_JSON_LIMITS_V1);
|
|
318
|
+
}
|
|
319
|
+
function challengeBody(value) {
|
|
320
|
+
const { challengeResolutionId: _id, ...body } = value;
|
|
321
|
+
return body;
|
|
322
|
+
}
|
|
323
|
+
function digestChallengeResolution(value) {
|
|
324
|
+
return digestTrustJsonV1("challenge-resolution", challengeBody(value));
|
|
325
|
+
}
|
|
326
|
+
function simpleDecisionValidation(value) {
|
|
327
|
+
assertExactKeys(value, [
|
|
328
|
+
"schemaVersion",
|
|
329
|
+
"fusionDecisionId",
|
|
330
|
+
"fusionDecisionDigest",
|
|
331
|
+
"tenantId",
|
|
332
|
+
"subject",
|
|
333
|
+
"subjectDigest",
|
|
334
|
+
"scope",
|
|
335
|
+
"scopeDigest",
|
|
336
|
+
"policyId",
|
|
337
|
+
"policyVersion",
|
|
338
|
+
"policyDigest",
|
|
339
|
+
"evaluatedAtLogicalMs",
|
|
340
|
+
"inputSetDigest",
|
|
341
|
+
"consideredRecordIds",
|
|
342
|
+
"includedRecordIds",
|
|
343
|
+
"recordExclusions",
|
|
344
|
+
"claimClassifications",
|
|
345
|
+
"challengeResolutions",
|
|
346
|
+
"groupAllocations",
|
|
347
|
+
"dimensions",
|
|
348
|
+
"previousProfileDigest",
|
|
349
|
+
"reasonCodes",
|
|
350
|
+
], "fusion decision");
|
|
351
|
+
const decision = value;
|
|
352
|
+
if (decision.schemaVersion !== 1)
|
|
353
|
+
throw new TrustValidationError("fusion decision schema is invalid");
|
|
354
|
+
assertIdentifier(decision.tenantId, "tenantId");
|
|
355
|
+
assertIdentifier(decision.policyId, "policyId");
|
|
356
|
+
assertSafeInteger(decision.policyVersion, "policyVersion", 1);
|
|
357
|
+
assertSafeInteger(decision.evaluatedAtLogicalMs, "evaluatedAtLogicalMs");
|
|
358
|
+
validateTrustSubjectV1(decision.subject);
|
|
359
|
+
validateEvidenceScopeV1(decision.scope);
|
|
360
|
+
if (decision.scope.tenantId !== decision.tenantId ||
|
|
361
|
+
digestSubjectV1(decision.subject) !== decision.subjectDigest ||
|
|
362
|
+
digestScopeV1(decision.scope) !== decision.scopeDigest)
|
|
363
|
+
throw new TrustValidationError("fusion decision subject or scope binding is invalid");
|
|
364
|
+
for (const digest of [
|
|
365
|
+
decision.fusionDecisionDigest,
|
|
366
|
+
decision.policyDigest,
|
|
367
|
+
decision.inputSetDigest,
|
|
368
|
+
])
|
|
369
|
+
assertTrustDigest(digest, "fusion digest");
|
|
370
|
+
if (decision.previousProfileDigest !== null)
|
|
371
|
+
assertTrustDigest(decision.previousProfileDigest, "previousProfileDigest");
|
|
372
|
+
assertSortedUnique(decision.consideredRecordIds, "fusion considered records", MAXIMUM_FUSION_RECORD_ITEMS_V1);
|
|
373
|
+
assertSortedUnique(decision.includedRecordIds, "fusion included records", MAXIMUM_FUSION_RECORD_ITEMS_V1);
|
|
374
|
+
assertSortedUnique(decision.reasonCodes, "fusion decision reasons", MAXIMUM_FUSION_REASON_CODES_V1);
|
|
375
|
+
for (const reason of decision.reasonCodes)
|
|
376
|
+
validateReasonCodeV1(reason);
|
|
377
|
+
if (!Array.isArray(decision.recordExclusions) ||
|
|
378
|
+
!Array.isArray(decision.claimClassifications) ||
|
|
379
|
+
!Array.isArray(decision.challengeResolutions) ||
|
|
380
|
+
!Array.isArray(decision.groupAllocations) ||
|
|
381
|
+
!Array.isArray(decision.dimensions))
|
|
382
|
+
throw new TrustValidationError("fusion decision nested arrays are invalid");
|
|
383
|
+
if (decision.recordExclusions.length > MAXIMUM_FUSION_RECORD_ITEMS_V1 ||
|
|
384
|
+
decision.claimClassifications.length > MAXIMUM_FUSION_RECORD_ITEMS_V1 ||
|
|
385
|
+
decision.challengeResolutions.length > MAXIMUM_FUSION_RECORD_ITEMS_V1 ||
|
|
386
|
+
decision.groupAllocations.length > MAXIMUM_FUSION_GROUP_ALLOCATIONS_V1 ||
|
|
387
|
+
decision.dimensions.length > MAXIMUM_FUSION_DIMENSIONS_V1)
|
|
388
|
+
throw new TrustValidationError("fusion decision nested capacity exceeded");
|
|
389
|
+
for (const allocation of decision.groupAllocations) {
|
|
390
|
+
assertExactKeys(allocation, [
|
|
391
|
+
"stage",
|
|
392
|
+
"dimensionId",
|
|
393
|
+
"criterionId",
|
|
394
|
+
"claimId",
|
|
395
|
+
"dependencyGroupId",
|
|
396
|
+
"candidateRecordIds",
|
|
397
|
+
"capBasisPoints",
|
|
398
|
+
"allocatedWeightBasisPoints",
|
|
399
|
+
], "fusion allocation");
|
|
400
|
+
assertSortedUnique(allocation.candidateRecordIds, "fusion allocation candidates", MAXIMUM_FUSION_RECORD_ITEMS_V1);
|
|
401
|
+
}
|
|
402
|
+
assertCanonicalItems(decision.recordExclusions, "fusion exclusions", (item) => `${item.recordDigest}\u0000${item.recordId}`);
|
|
403
|
+
assertCanonicalItems(decision.claimClassifications, "fusion classifications", (item) => item.claimDigest);
|
|
404
|
+
assertCanonicalItems(decision.challengeResolutions, "fusion challenge resolutions", (item) => item.challengeResolutionId);
|
|
405
|
+
assertCanonicalItems(decision.groupAllocations, "fusion group allocations", (item) => `${item.stage}\u0000${item.dimensionId ?? ""}\u0000${item.criterionId ?? ""}\u0000${item.claimId ?? ""}\u0000${item.dependencyGroupId}\u0000${item.candidateRecordIds.join("\u0000")}`);
|
|
406
|
+
assertCanonicalItems(decision.dimensions, "fusion dimensions", (item) => item.dimensionId);
|
|
407
|
+
for (const exclusion of decision.recordExclusions) {
|
|
408
|
+
assertExactKeys(exclusion, ["recordKind", "recordId", "recordDigest", "reasonCodes"], "fusion exclusion");
|
|
409
|
+
if (!["claim", "attestation", "challenge", "retraction"].includes(exclusion.recordKind))
|
|
410
|
+
throw new TrustValidationError("fusion exclusion kind is invalid");
|
|
411
|
+
assertIdentifier(exclusion.recordId, "fusion exclusion recordId");
|
|
412
|
+
assertTrustDigest(exclusion.recordDigest, "fusion exclusion recordDigest");
|
|
413
|
+
for (const reason of assertSortedUnique(exclusion.reasonCodes, "fusion exclusion reasons", MAXIMUM_FUSION_REASON_CODES_V1))
|
|
414
|
+
validateReasonCodeV1(reason);
|
|
415
|
+
}
|
|
416
|
+
for (const classification of decision.claimClassifications) {
|
|
417
|
+
assertExactKeys(classification, [
|
|
418
|
+
"claimId",
|
|
419
|
+
"claimDigest",
|
|
420
|
+
"criterionId",
|
|
421
|
+
"dimensionId",
|
|
422
|
+
"classification",
|
|
423
|
+
"mappedValueBasisPoints",
|
|
424
|
+
"supportGroupIds",
|
|
425
|
+
"supportWeightBasisPoints",
|
|
426
|
+
"contradictionGroupIds",
|
|
427
|
+
"contradictionWeightBasisPoints",
|
|
428
|
+
"rawWeightBasisPoints",
|
|
429
|
+
"retainedWeightBasisPoints",
|
|
430
|
+
"effectiveWeightBasisPoints",
|
|
431
|
+
"claimSourceDependencyGroupId",
|
|
432
|
+
"effectiveSupportingAttestationIds",
|
|
433
|
+
"effectiveContentResolutionIds",
|
|
434
|
+
"reasonCodes",
|
|
435
|
+
], "fusion classification");
|
|
436
|
+
assertIdentifier(classification.claimId, "fusion claimId");
|
|
437
|
+
assertTrustDigest(classification.claimDigest, "fusion claimDigest");
|
|
438
|
+
assertIdentifier(classification.criterionId, "fusion criterionId");
|
|
439
|
+
assertIdentifier(classification.dimensionId, "fusion dimensionId");
|
|
440
|
+
if (![
|
|
441
|
+
"supported",
|
|
442
|
+
"contradicted",
|
|
443
|
+
"contested",
|
|
444
|
+
"inconclusive",
|
|
445
|
+
"unavailable",
|
|
446
|
+
].includes(classification.classification))
|
|
447
|
+
throw new TrustValidationError("fusion classification kind is invalid");
|
|
448
|
+
if (classification.mappedValueBasisPoints !== null)
|
|
449
|
+
assertBasisPoints(classification.mappedValueBasisPoints, "fusion mappedValueBasisPoints");
|
|
450
|
+
assertSortedUnique(classification.supportGroupIds, "fusion support groups", MAXIMUM_FUSION_DEPENDENCY_GROUPS_V1);
|
|
451
|
+
assertBasisPoints(classification.supportWeightBasisPoints, "fusion supportWeightBasisPoints");
|
|
452
|
+
assertSortedUnique(classification.contradictionGroupIds, "fusion contradiction groups", MAXIMUM_FUSION_DEPENDENCY_GROUPS_V1);
|
|
453
|
+
assertBasisPoints(classification.contradictionWeightBasisPoints, "fusion contradictionWeightBasisPoints");
|
|
454
|
+
assertBasisPoints(classification.rawWeightBasisPoints, "fusion rawWeightBasisPoints");
|
|
455
|
+
assertBasisPoints(classification.retainedWeightBasisPoints, "fusion retainedWeightBasisPoints");
|
|
456
|
+
assertBasisPoints(classification.effectiveWeightBasisPoints, "fusion effectiveWeightBasisPoints");
|
|
457
|
+
if (classification.claimSourceDependencyGroupId !== null)
|
|
458
|
+
assertIdentifier(classification.claimSourceDependencyGroupId, "fusion claimSourceDependencyGroupId");
|
|
459
|
+
assertSortedUnique(classification.effectiveSupportingAttestationIds, "fusion effective supporting attestations", MAXIMUM_FUSION_RECORD_ITEMS_V1);
|
|
460
|
+
assertSortedUnique(classification.effectiveContentResolutionIds, "fusion effective content resolutions", MAXIMUM_FUSION_CONTENT_RESOLUTIONS_V1);
|
|
461
|
+
for (const reason of assertSortedUnique(classification.reasonCodes, "fusion classification reasons", MAXIMUM_FUSION_REASON_CODES_V1))
|
|
462
|
+
validateReasonCodeV1(reason);
|
|
463
|
+
}
|
|
464
|
+
for (const resolution of decision.challengeResolutions) {
|
|
465
|
+
assertExactKeys(resolution, [
|
|
466
|
+
"schemaVersion",
|
|
467
|
+
"challengeResolutionId",
|
|
468
|
+
"challenges",
|
|
469
|
+
"targetId",
|
|
470
|
+
"targetDigest",
|
|
471
|
+
"challengerDependencyGroupId",
|
|
472
|
+
"basisCutoffLogicalMs",
|
|
473
|
+
"policyDigest",
|
|
474
|
+
"evaluatedAtLogicalMs",
|
|
475
|
+
"result",
|
|
476
|
+
"corroboratingGroupIds",
|
|
477
|
+
"corroboratingWeightBasisPoints",
|
|
478
|
+
"opposingGroupIds",
|
|
479
|
+
"opposingWeightBasisPoints",
|
|
480
|
+
"consideredAttestationIds",
|
|
481
|
+
"reasonCodes",
|
|
482
|
+
], "challenge resolution");
|
|
483
|
+
if (resolution.schemaVersion !== 1)
|
|
484
|
+
throw new TrustValidationError("challenge resolution schema is invalid");
|
|
485
|
+
assertIdentifier(resolution.challengeResolutionId, "challengeResolutionId");
|
|
486
|
+
assertIdentifier(resolution.targetId, "challenge targetId");
|
|
487
|
+
assertTrustDigest(resolution.targetDigest, "challenge targetDigest");
|
|
488
|
+
assertIdentifier(resolution.challengerDependencyGroupId, "challengerDependencyGroupId");
|
|
489
|
+
assertSafeInteger(resolution.basisCutoffLogicalMs, "challenge basisCutoffLogicalMs");
|
|
490
|
+
assertTrustDigest(resolution.policyDigest, "challenge policyDigest");
|
|
491
|
+
assertSafeInteger(resolution.evaluatedAtLogicalMs, "challenge evaluatedAtLogicalMs");
|
|
492
|
+
if (!["unresolved", "dismissed", "sustained", "contested"].includes(resolution.result))
|
|
493
|
+
throw new TrustValidationError("challenge result is invalid");
|
|
494
|
+
if (!Array.isArray(resolution.challenges))
|
|
495
|
+
throw new TrustValidationError("challenge entries are invalid");
|
|
496
|
+
if (resolution.challenges.length > MAXIMUM_FUSION_RECORD_ITEMS_V1)
|
|
497
|
+
throw new TrustValidationError("challenge entry capacity exceeded");
|
|
498
|
+
assertCanonicalItems(resolution.challenges, "challenge entries", (item) => `${item.challengeId}\u0000${item.challengeDigest}`);
|
|
499
|
+
for (const challenge of resolution.challenges) {
|
|
500
|
+
assertExactKeys(challenge, ["challengeId", "challengeDigest", "basisCutoffLogicalMs"], "challenge entry");
|
|
501
|
+
assertIdentifier(challenge.challengeId, "challengeId");
|
|
502
|
+
assertTrustDigest(challenge.challengeDigest, "challengeDigest");
|
|
503
|
+
assertSafeInteger(challenge.basisCutoffLogicalMs, "challenge cutoff");
|
|
504
|
+
}
|
|
505
|
+
assertSortedUnique(resolution.corroboratingGroupIds, "corroborating groups", MAXIMUM_FUSION_DEPENDENCY_GROUPS_V1);
|
|
506
|
+
assertBasisPoints(resolution.corroboratingWeightBasisPoints, "corroboratingWeightBasisPoints");
|
|
507
|
+
assertSortedUnique(resolution.opposingGroupIds, "opposing groups", MAXIMUM_FUSION_DEPENDENCY_GROUPS_V1);
|
|
508
|
+
assertBasisPoints(resolution.opposingWeightBasisPoints, "opposingWeightBasisPoints");
|
|
509
|
+
assertSortedUnique(resolution.consideredAttestationIds, "considered attestations", MAXIMUM_FUSION_RECORD_ITEMS_V1);
|
|
510
|
+
for (const reason of assertSortedUnique(resolution.reasonCodes, "challenge reasons", MAXIMUM_FUSION_REASON_CODES_V1))
|
|
511
|
+
validateReasonCodeV1(reason);
|
|
512
|
+
if (resolution.challengeResolutionId !==
|
|
513
|
+
`challenge-resolution:${digestChallengeResolution(resolution)}`)
|
|
514
|
+
throw new TrustValidationError("challenge resolution digest is invalid");
|
|
515
|
+
}
|
|
516
|
+
for (const allocation of decision.groupAllocations) {
|
|
517
|
+
if (!["attestation", "challenge_resolution", "profile"].includes(allocation.stage))
|
|
518
|
+
throw new TrustValidationError("fusion allocation stage is invalid");
|
|
519
|
+
for (const [name, item] of [
|
|
520
|
+
["dimensionId", allocation.dimensionId],
|
|
521
|
+
["criterionId", allocation.criterionId],
|
|
522
|
+
["claimId", allocation.claimId],
|
|
523
|
+
]) {
|
|
524
|
+
if (item !== null)
|
|
525
|
+
assertIdentifier(item, `fusion allocation ${name}`);
|
|
526
|
+
}
|
|
527
|
+
assertIdentifier(allocation.dependencyGroupId, "fusion allocation dependencyGroupId");
|
|
528
|
+
assertBasisPoints(allocation.capBasisPoints, "fusion allocation cap");
|
|
529
|
+
assertBasisPoints(allocation.allocatedWeightBasisPoints, "fusion allocation allocated");
|
|
530
|
+
if (allocation.allocatedWeightBasisPoints >
|
|
531
|
+
allocation.capBasisPoints)
|
|
532
|
+
throw new TrustValidationError("fusion allocation exceeds cap");
|
|
533
|
+
}
|
|
534
|
+
for (const dimension of decision.dimensions) {
|
|
535
|
+
assertExactKeys(dimension, [
|
|
536
|
+
"dimensionId",
|
|
537
|
+
"scoreBasisPoints",
|
|
538
|
+
"uncertaintyBasisPoints",
|
|
539
|
+
"effectiveWeightBasisPoints",
|
|
540
|
+
"coverageBasisPoints",
|
|
541
|
+
"ageUncertaintyBasisPoints",
|
|
542
|
+
"contradictionPressureBasisPoints",
|
|
543
|
+
"includedClaimIds",
|
|
544
|
+
"excludedClaimIds",
|
|
545
|
+
"claimSourceDependencyGroupIds",
|
|
546
|
+
"latestQualifyingEffectiveAtLogicalMs",
|
|
547
|
+
], "fusion dimension");
|
|
548
|
+
assertIdentifier(dimension.dimensionId, "fusion dimensionId");
|
|
549
|
+
for (const [name, item] of [
|
|
550
|
+
["scoreBasisPoints", dimension.scoreBasisPoints],
|
|
551
|
+
["uncertaintyBasisPoints", dimension.uncertaintyBasisPoints],
|
|
552
|
+
["coverageBasisPoints", dimension.coverageBasisPoints],
|
|
553
|
+
["ageUncertaintyBasisPoints", dimension.ageUncertaintyBasisPoints],
|
|
554
|
+
[
|
|
555
|
+
"contradictionPressureBasisPoints",
|
|
556
|
+
dimension.contradictionPressureBasisPoints,
|
|
557
|
+
],
|
|
558
|
+
])
|
|
559
|
+
assertBasisPoints(item, `fusion dimension ${name}`);
|
|
560
|
+
assertSafeInteger(dimension.effectiveWeightBasisPoints, "fusion dimension effectiveWeightBasisPoints");
|
|
561
|
+
if (dimension.effectiveWeightBasisPoints < 0)
|
|
562
|
+
throw new TrustValidationError("fusion dimension effectiveWeightBasisPoints is invalid");
|
|
563
|
+
assertSortedUnique(dimension.includedClaimIds, "fusion dimension included claims", MAXIMUM_FUSION_RECORD_ITEMS_V1);
|
|
564
|
+
assertSortedUnique(dimension.excludedClaimIds, "fusion dimension excluded claims", MAXIMUM_FUSION_RECORD_ITEMS_V1);
|
|
565
|
+
assertSortedUnique(dimension.claimSourceDependencyGroupIds, "fusion dimension source groups", MAXIMUM_FUSION_DEPENDENCY_GROUPS_V1);
|
|
566
|
+
if (dimension.latestQualifyingEffectiveAtLogicalMs !== null)
|
|
567
|
+
assertSafeInteger(dimension.latestQualifyingEffectiveAtLogicalMs, "fusion dimension latest effective time");
|
|
568
|
+
}
|
|
569
|
+
if (decision.fusionDecisionDigest !==
|
|
570
|
+
digestEvidenceFusionDecisionV1(decision) ||
|
|
571
|
+
decision.fusionDecisionId !==
|
|
572
|
+
`fusion-decision:${decision.fusionDecisionDigest}`)
|
|
573
|
+
throw new TrustValidationError("fusion decision digest is invalid");
|
|
574
|
+
return deepFreeze(structuredClone(decision));
|
|
575
|
+
}
|
|
576
|
+
export const validateEvidenceFusionDecisionV1 = simpleDecisionValidation;
|
|
577
|
+
export function evaluateEvidenceFusionV1(state, requestValue, logicalTimeMs) {
|
|
578
|
+
const request = validateRequest(requestValue);
|
|
579
|
+
assertSafeInteger(logicalTimeMs, "logicalTimeMs");
|
|
580
|
+
if (logicalTimeMs < state.logicalTimeHighWaterMs)
|
|
581
|
+
throw new TrustValidationError("logical time rollback");
|
|
582
|
+
const policy = state.policies.find((item) => item.policyId === request.policyId &&
|
|
583
|
+
item.policyVersion === request.policyVersion &&
|
|
584
|
+
digestEvidenceFusionPolicyV1(item) === request.policyDigest);
|
|
585
|
+
if (!policy)
|
|
586
|
+
throw new TrustValidationError("fusion policy is unavailable");
|
|
587
|
+
const policyHead = state.policyHeads.find((head) => head.policyId === request.policyId);
|
|
588
|
+
if (!policyHead ||
|
|
589
|
+
policyHead.policyVersion !== request.policyVersion ||
|
|
590
|
+
policyHead.policyDigest !== request.policyDigest)
|
|
591
|
+
throw new TrustValidationError("fusion policy is not the current head");
|
|
592
|
+
const expectedBindingDigests = deriveApplicableBindingDigests(state, request.policyDigest, logicalTimeMs);
|
|
593
|
+
if (expectedBindingDigests.length !== request.dependencyBindingDigests.length ||
|
|
594
|
+
expectedBindingDigests.some((digest, index) => digest !== request.dependencyBindingDigests[index]))
|
|
595
|
+
throw new TrustValidationError("fusion dependency bindings are not the current exact set");
|
|
596
|
+
const bindings = request.dependencyBindingDigests.map((digest) => selectedBinding(state, digest, request.policyDigest, logicalTimeMs));
|
|
597
|
+
const applicableBindingDigests = new Set(request.dependencyBindingDigests);
|
|
598
|
+
const resolverDigest = bindings.find((binding) => binding.bindingKind === "content_resolver")
|
|
599
|
+
?.bindingDigest ?? null;
|
|
600
|
+
const scopeDigest = digestScopeV1(request.scope);
|
|
601
|
+
const subjectDigest = digestSubjectV1(request.subject);
|
|
602
|
+
const exclusions = [];
|
|
603
|
+
const allocations = [];
|
|
604
|
+
const allScopeRecords = sorted(state.records.filter((record) => record.acceptedAtLogicalMs <= logicalTimeMs &&
|
|
605
|
+
exactScope(record, scopeDigest)), (record) => record.recordDigest);
|
|
606
|
+
if (allScopeRecords.length > policy.limits.maximumConsideredRecordsPerFusion)
|
|
607
|
+
throw new TrustValidationError("fusion considered-record capacity exceeded");
|
|
608
|
+
const recordByKey = new Map(allScopeRecords.map((record) => [recordKey(record), record]));
|
|
609
|
+
const criteria = new Map(policy.criteria.map((criterion) => [criterion.criterionId, criterion]));
|
|
610
|
+
const groups = new Map(policy.dependencyGroups.map((group) => [group.dependencyGroupId, group]));
|
|
611
|
+
const works = [];
|
|
612
|
+
for (const claim of allScopeRecords.filter((record) => isClaim(record) &&
|
|
613
|
+
digestSubjectV1(record.record.subject) === subjectDigest)) {
|
|
614
|
+
const criterion = criteria.get(claim.record.criterionId);
|
|
615
|
+
if (!criterion) {
|
|
616
|
+
exclusions.push(reasonExclusion(claim, "source_not_effective"));
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
const claimBinding = sourceBinding(policy, claim, "claim", logicalTimeMs);
|
|
620
|
+
const reasons = new Set();
|
|
621
|
+
if (!["active", "challenged"].includes(claim.status))
|
|
622
|
+
reasons.add("evidence_unavailable");
|
|
623
|
+
if (!claimBinding)
|
|
624
|
+
reasons.add("source_not_effective");
|
|
625
|
+
const claimAuthorization = authorizationMatches(state, claim, request.policyDigest, criterion.criterionId, scopeDigest, subjectDigest, criterion.claimAuthority.allowedSourceRelations, null, logicalTimeMs, applicableBindingDigests);
|
|
626
|
+
if (!claimAuthorization ||
|
|
627
|
+
authorizationBasisCutoff(claim, criterion.claimAuthority, claimAuthorization) === null)
|
|
628
|
+
reasons.add("claim_subject_authority_invalid");
|
|
629
|
+
const rootDigest = effectiveRootBasisDigest(state, claim, request.policyDigest, logicalTimeMs, recordByKey, applicableBindingDigests);
|
|
630
|
+
if (!rootDigest)
|
|
631
|
+
reasons.add("source_not_effective");
|
|
632
|
+
if (logicalTimeMs - claim.effectiveAtLogicalMs > criterion.maximumAgeMs)
|
|
633
|
+
reasons.add("evidence_stale");
|
|
634
|
+
if (criterion.contentRequired &&
|
|
635
|
+
!contentUsable(state, claim, resolverDigest, logicalTimeMs))
|
|
636
|
+
reasons.add("content_unavailable");
|
|
637
|
+
const attestations = [];
|
|
638
|
+
if (reasons.size === 0 || claim.status === "challenged") {
|
|
639
|
+
for (const attestation of allScopeRecords.filter((record) => record.recordKind === "attestation" &&
|
|
640
|
+
(record.status === "active" || record.status === "challenged"))) {
|
|
641
|
+
const related = target(attestation);
|
|
642
|
+
if (!related ||
|
|
643
|
+
related.id !== claim.recordId ||
|
|
644
|
+
related.digest !== claim.recordDigest)
|
|
645
|
+
continue;
|
|
646
|
+
const binding = sourceBinding(policy, attestation, "attest", logicalTimeMs);
|
|
647
|
+
if (!binding) {
|
|
648
|
+
exclusions.push(reasonExclusion(attestation, "source_not_effective"));
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
const sameSource = sourceKey(attestation) === sourceKey(claim);
|
|
652
|
+
if (sameSource && !criterion.allowClaimSourceAttestation) {
|
|
653
|
+
exclusions.push(reasonExclusion(attestation, "source_not_effective"));
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
const confidence = attestation.record.confidenceBasisPoints;
|
|
657
|
+
attestations.push({
|
|
658
|
+
record: attestation,
|
|
659
|
+
groupId: binding.dependencyGroupId,
|
|
660
|
+
sourceWeight: Math.floor(product(binding.maximumWeightBasisPoints, confidence) / 10_000),
|
|
661
|
+
countsForThreshold: !sameSource,
|
|
662
|
+
disposition: attestation.record.disposition,
|
|
663
|
+
allocated: 0,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
works.push({
|
|
668
|
+
claim,
|
|
669
|
+
criterion,
|
|
670
|
+
sourceGroupId: claimBinding?.dependencyGroupId ?? null,
|
|
671
|
+
effectiveRootBasisDigest: rootDigest,
|
|
672
|
+
candidates: attestations,
|
|
673
|
+
reasons,
|
|
674
|
+
classification: null,
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
const candidateByClaim = new Map(works.map((work) => [work.claim.recordDigest, work]));
|
|
678
|
+
const allocateAttestationCandidates = (candidates) => {
|
|
679
|
+
const byGroup = new Map();
|
|
680
|
+
for (const candidate of candidates)
|
|
681
|
+
(byGroup.get(candidate.groupId) ??
|
|
682
|
+
byGroup.set(candidate.groupId, []).get(candidate.groupId)).push(candidate);
|
|
683
|
+
return sorted([...byGroup.entries()], ([groupId]) => groupId).map(([groupId, groupCandidates]) => {
|
|
684
|
+
const disposition = groupCandidates.some((candidate) => candidate.disposition === "contradict" &&
|
|
685
|
+
candidate.sourceWeight > 0)
|
|
686
|
+
? "contradict"
|
|
687
|
+
: groupCandidates.some((candidate) => candidate.disposition === "support" &&
|
|
688
|
+
candidate.sourceWeight > 0)
|
|
689
|
+
? "support"
|
|
690
|
+
: "inconclusive";
|
|
691
|
+
const eligible = groupCandidates
|
|
692
|
+
.filter((candidate) => candidate.disposition === disposition)
|
|
693
|
+
.sort((a, b) => b.record.acceptedAtLogicalMs - a.record.acceptedAtLogicalMs ||
|
|
694
|
+
compare(a.record.recordDigest, b.record.recordDigest));
|
|
695
|
+
const cap = groups.get(groupId)?.maximumAttestationWeightPerClaimBasisPoints ?? 0;
|
|
696
|
+
let remaining = cap;
|
|
697
|
+
const allocatedByRecordDigest = new Map();
|
|
698
|
+
for (const candidate of eligible) {
|
|
699
|
+
const allocated = Math.min(candidate.sourceWeight, remaining);
|
|
700
|
+
allocatedByRecordDigest.set(candidate.record.recordDigest, allocated);
|
|
701
|
+
remaining -= allocated;
|
|
702
|
+
}
|
|
703
|
+
return {
|
|
704
|
+
groupId,
|
|
705
|
+
disposition,
|
|
706
|
+
candidates: eligible,
|
|
707
|
+
allocatedByRecordDigest,
|
|
708
|
+
capBasisPoints: cap,
|
|
709
|
+
allocatedWeightBasisPoints: cap - remaining,
|
|
710
|
+
};
|
|
711
|
+
});
|
|
712
|
+
};
|
|
713
|
+
const resolutions = [];
|
|
714
|
+
const diagnosticByRecordDigest = new Map(state.diagnostics.map((item) => [item.recordDigest, item.reasonCode]));
|
|
715
|
+
const byTarget = new Map();
|
|
716
|
+
for (const challenge of allScopeRecords.filter((record) => record.recordKind === "challenge" &&
|
|
717
|
+
(isActive(record) ||
|
|
718
|
+
(record.status === "unavailable" &&
|
|
719
|
+
diagnosticByRecordDigest.get(record.recordDigest) ===
|
|
720
|
+
"challenge_basis_unavailable")))) {
|
|
721
|
+
const relation = target(challenge);
|
|
722
|
+
if (!relation)
|
|
723
|
+
continue;
|
|
724
|
+
const targetRecord = recordByKey.get(`${relation.id}\u0000${relation.digest}`);
|
|
725
|
+
if (!targetRecord)
|
|
726
|
+
continue;
|
|
727
|
+
const claim = targetRecord.recordKind === "claim"
|
|
728
|
+
? targetRecord
|
|
729
|
+
: recordByKey.get(`${targetRecord.record.claimId}\u0000${targetRecord.record.claimDigest}`);
|
|
730
|
+
const work = claim ? candidateByClaim.get(claim.recordDigest) : undefined;
|
|
731
|
+
if (!claim || !work)
|
|
732
|
+
continue;
|
|
733
|
+
const binding = sourceBinding(policy, challenge, "challenge", logicalTimeMs);
|
|
734
|
+
const authorization = authorizationMatches(state, challenge, request.policyDigest, work.criterion.criterionId, scopeDigest, subjectDigest, work.criterion.challengeAuthority.allowedSourceRelations, targetRecord, logicalTimeMs, applicableBindingDigests);
|
|
735
|
+
const cutoff = authorization
|
|
736
|
+
? authorizationBasisCutoff(challenge, work.criterion.challengeAuthority, authorization)
|
|
737
|
+
: null;
|
|
738
|
+
if (!binding || cutoff === null) {
|
|
739
|
+
exclusions.push(reasonExclusion(challenge, "challenge_basis_unavailable"));
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
const key = `${relation.kind}\u0000${relation.id}\u0000${relation.digest}\u0000${binding.dependencyGroupId}`;
|
|
743
|
+
const value = {
|
|
744
|
+
challenge,
|
|
745
|
+
groupId: binding.dependencyGroupId,
|
|
746
|
+
cutoff,
|
|
747
|
+
target: targetRecord,
|
|
748
|
+
work,
|
|
749
|
+
};
|
|
750
|
+
const list = byTarget.get(key) ?? [];
|
|
751
|
+
list.push(value);
|
|
752
|
+
byTarget.set(key, list);
|
|
753
|
+
}
|
|
754
|
+
const targetGroups = new Map();
|
|
755
|
+
for (const [key] of byTarget) {
|
|
756
|
+
const targetKey = key.split("\u0000").slice(0, 3).join("\u0000");
|
|
757
|
+
const values = targetGroups.get(targetKey) ?? [];
|
|
758
|
+
values.push(key);
|
|
759
|
+
targetGroups.set(targetKey, values);
|
|
760
|
+
}
|
|
761
|
+
const groupsByKey = new Map();
|
|
762
|
+
for (const [key, entries] of sorted([...byTarget.entries()], ([entry]) => entry)) {
|
|
763
|
+
const [targetKind, targetId, targetDigest, challengerGroupId] = key.split("\u0000");
|
|
764
|
+
const work = entries[0].work;
|
|
765
|
+
const targetRecord = entries[0].target;
|
|
766
|
+
const targetKey = `${targetKind}\u0000${targetId}\u0000${targetDigest}`;
|
|
767
|
+
const dependencies = new Set();
|
|
768
|
+
for (const entry of entries) {
|
|
769
|
+
const references = entry.challenge.record.basisReferences;
|
|
770
|
+
for (const reference of references) {
|
|
771
|
+
if (reference.kind !== "evidence")
|
|
772
|
+
continue;
|
|
773
|
+
if (reference.referenceType !== "claim" &&
|
|
774
|
+
reference.referenceType !== "attestation")
|
|
775
|
+
continue;
|
|
776
|
+
for (const dependency of targetGroups.get(`${reference.referenceType}\u0000${reference.referenceId}\u0000${reference.referenceDigest}`) ?? [])
|
|
777
|
+
dependencies.add(dependency);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
const cutoff = Math.min(...entries.map((entry) => entry.cutoff));
|
|
781
|
+
const targetAuthorGroup = targetKind === "attestation"
|
|
782
|
+
? (sourceBinding(policy, targetRecord, "attest", logicalTimeMs)
|
|
783
|
+
?.dependencyGroupId ?? null)
|
|
784
|
+
: work.sourceGroupId;
|
|
785
|
+
const candidates = work.candidates.filter((candidate) => candidate.record.acceptedAtLogicalMs > cutoff &&
|
|
786
|
+
candidate.groupId !== challengerGroupId &&
|
|
787
|
+
candidate.groupId !== targetAuthorGroup &&
|
|
788
|
+
candidate.countsForThreshold &&
|
|
789
|
+
(targetKind !== "attestation" ||
|
|
790
|
+
candidate.record.recordId !== targetId));
|
|
791
|
+
for (const candidate of candidates)
|
|
792
|
+
for (const dependency of targetGroups.get(`attestation\u0000${candidate.record.recordId}\u0000${candidate.record.recordDigest}`) ?? [])
|
|
793
|
+
dependencies.add(dependency);
|
|
794
|
+
groupsByKey.set(key, {
|
|
795
|
+
key,
|
|
796
|
+
entries,
|
|
797
|
+
targetKind: targetKind,
|
|
798
|
+
targetId,
|
|
799
|
+
targetDigest,
|
|
800
|
+
challengerGroupId,
|
|
801
|
+
dependencies: uniqueSorted([...dependencies], "challenge dependencies"),
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
const settled = new Map();
|
|
805
|
+
const attestationAvailable = (record) => {
|
|
806
|
+
if (record.status !== "active" && record.status !== "challenged")
|
|
807
|
+
return false;
|
|
808
|
+
const keys = targetGroups.get(`attestation\u0000${record.recordId}\u0000${record.recordDigest}`) ?? [];
|
|
809
|
+
if (keys.length === 0)
|
|
810
|
+
return true;
|
|
811
|
+
return keys.every((key) => settled.get(key)?.result === "dismissed");
|
|
812
|
+
};
|
|
813
|
+
const resolveGroup = (group, forcedUnresolved = false) => {
|
|
814
|
+
const entries = group.entries;
|
|
815
|
+
const work = entries[0].work;
|
|
816
|
+
const targetRecord = entries[0].target;
|
|
817
|
+
const cutoff = Math.min(...entries.map((entry) => entry.cutoff));
|
|
818
|
+
const targetAuthorGroup = group.targetKind === "attestation"
|
|
819
|
+
? (sourceBinding(policy, targetRecord, "attest", logicalTimeMs)
|
|
820
|
+
?.dependencyGroupId ?? null)
|
|
821
|
+
: work.sourceGroupId;
|
|
822
|
+
const candidates = work.candidates.filter((candidate) => candidate.record.acceptedAtLogicalMs > cutoff &&
|
|
823
|
+
candidate.groupId !== group.challengerGroupId &&
|
|
824
|
+
candidate.groupId !== targetAuthorGroup &&
|
|
825
|
+
candidate.countsForThreshold &&
|
|
826
|
+
(group.targetKind !== "attestation" ||
|
|
827
|
+
candidate.record.recordId !== group.targetId) &&
|
|
828
|
+
attestationAvailable(candidate.record));
|
|
829
|
+
const supportForClaim = group.targetKind === "claim";
|
|
830
|
+
const groupsFor = new Map();
|
|
831
|
+
for (const groupAllocation of allocateAttestationCandidates(candidates)) {
|
|
832
|
+
const entry = { support: 0, contradict: 0, ids: [] };
|
|
833
|
+
for (const candidate of groupAllocation.candidates) {
|
|
834
|
+
const allocated = groupAllocation.allocatedByRecordDigest.get(candidate.record.recordDigest) ?? 0;
|
|
835
|
+
entry.ids.push(candidate.record.recordId);
|
|
836
|
+
if (candidate.disposition === "support")
|
|
837
|
+
entry.support = add(entry.support, allocated);
|
|
838
|
+
if (candidate.disposition === "contradict")
|
|
839
|
+
entry.contradict = add(entry.contradict, allocated);
|
|
840
|
+
}
|
|
841
|
+
groupsFor.set(groupAllocation.groupId, entry);
|
|
842
|
+
}
|
|
843
|
+
const targetDisposition = group.targetKind === "attestation"
|
|
844
|
+
? targetRecord.record.disposition
|
|
845
|
+
: null;
|
|
846
|
+
const corroborating = [...groupsFor.entries()].filter(([, value]) => supportForClaim
|
|
847
|
+
? value.support > 0
|
|
848
|
+
: targetDisposition === "support"
|
|
849
|
+
? value.support > 0
|
|
850
|
+
: targetDisposition === "contradict"
|
|
851
|
+
? value.contradict > 0
|
|
852
|
+
: false);
|
|
853
|
+
const opposing = [...groupsFor.entries()].filter(([, value]) => supportForClaim
|
|
854
|
+
? value.contradict > 0
|
|
855
|
+
: targetDisposition === "support"
|
|
856
|
+
? value.contradict > 0
|
|
857
|
+
: targetDisposition === "contradict"
|
|
858
|
+
? value.support > 0
|
|
859
|
+
: false);
|
|
860
|
+
const corroboratingWeight = corroborating.reduce((sum, [, value]) => clamp(add(sum, supportForClaim || targetDisposition === "support"
|
|
861
|
+
? value.support
|
|
862
|
+
: value.contradict)), 0);
|
|
863
|
+
const opposingWeight = opposing.reduce((sum, [, value]) => clamp(add(sum, supportForClaim || targetDisposition === "support"
|
|
864
|
+
? value.contradict
|
|
865
|
+
: value.support)), 0);
|
|
866
|
+
const rule = work.criterion.challengeResolution;
|
|
867
|
+
const targetUnavailable = !["active", "challenged"].includes(targetRecord.status) ||
|
|
868
|
+
(group.targetKind === "claim"
|
|
869
|
+
? work.reasons.size > 0
|
|
870
|
+
: !work.candidates.some((candidate) => candidate.record.recordId === targetRecord.recordId &&
|
|
871
|
+
candidate.record.recordDigest === targetRecord.recordDigest));
|
|
872
|
+
const result = forcedUnresolved
|
|
873
|
+
? "unresolved"
|
|
874
|
+
: targetUnavailable
|
|
875
|
+
? "sustained"
|
|
876
|
+
: corroborating.length >= rule.minimumCorroboratingGroups &&
|
|
877
|
+
corroboratingWeight >= rule.minimumCorroboratingWeightBasisPoints &&
|
|
878
|
+
opposing.length >= rule.minimumOpposingGroups &&
|
|
879
|
+
opposingWeight >= rule.minimumOpposingWeightBasisPoints
|
|
880
|
+
? "contested"
|
|
881
|
+
: opposing.length >= rule.minimumOpposingGroups &&
|
|
882
|
+
opposingWeight >= rule.minimumOpposingWeightBasisPoints
|
|
883
|
+
? "sustained"
|
|
884
|
+
: corroborating.length >= rule.minimumCorroboratingGroups &&
|
|
885
|
+
corroboratingWeight >=
|
|
886
|
+
rule.minimumCorroboratingWeightBasisPoints
|
|
887
|
+
? "dismissed"
|
|
888
|
+
: "unresolved";
|
|
889
|
+
const bare = {
|
|
890
|
+
schemaVersion: 1,
|
|
891
|
+
challenges: sorted(entries, (entry) => `${entry.challenge.recordId}\u0000${entry.challenge.recordDigest}`).map((entry) => ({
|
|
892
|
+
challengeId: entry.challenge.recordId,
|
|
893
|
+
challengeDigest: entry.challenge.recordDigest,
|
|
894
|
+
basisCutoffLogicalMs: entry.cutoff,
|
|
895
|
+
})),
|
|
896
|
+
targetId: group.targetId,
|
|
897
|
+
targetDigest: group.targetDigest,
|
|
898
|
+
challengerDependencyGroupId: group.challengerGroupId,
|
|
899
|
+
basisCutoffLogicalMs: cutoff,
|
|
900
|
+
policyDigest: request.policyDigest,
|
|
901
|
+
evaluatedAtLogicalMs: logicalTimeMs,
|
|
902
|
+
result,
|
|
903
|
+
corroboratingGroupIds: uniqueSorted(corroborating.map(([id]) => id), "corroborating groups"),
|
|
904
|
+
corroboratingWeightBasisPoints: corroboratingWeight,
|
|
905
|
+
opposingGroupIds: uniqueSorted(opposing.map(([id]) => id), "opposing groups"),
|
|
906
|
+
opposingWeightBasisPoints: opposingWeight,
|
|
907
|
+
consideredAttestationIds: uniqueSorted(candidates.map((candidate) => candidate.record.recordId), "considered attestations"),
|
|
908
|
+
reasonCodes: (forcedUnresolved
|
|
909
|
+
? ["challenge_basis_unavailable", "challenge_unresolved"]
|
|
910
|
+
: [
|
|
911
|
+
result === "dismissed"
|
|
912
|
+
? "challenge_dismissed"
|
|
913
|
+
: result === "sustained"
|
|
914
|
+
? "challenge_sustained"
|
|
915
|
+
: result === "contested"
|
|
916
|
+
? "challenge_contested"
|
|
917
|
+
: "challenge_unresolved",
|
|
918
|
+
]),
|
|
919
|
+
};
|
|
920
|
+
const challengeResolutionId = `challenge-resolution:${digestChallengeResolution({ ...bare, challengeResolutionId: "" })}`;
|
|
921
|
+
return { ...bare, challengeResolutionId };
|
|
922
|
+
};
|
|
923
|
+
while (true) {
|
|
924
|
+
const ready = sorted([...groupsByKey.values()].filter((group) => !settled.has(group.key) &&
|
|
925
|
+
group.dependencies.every((dependency) => settled.has(dependency))), (group) => group.key);
|
|
926
|
+
if (ready.length === 0)
|
|
927
|
+
break;
|
|
928
|
+
for (const group of ready)
|
|
929
|
+
settled.set(group.key, resolveGroup(group, group.dependencies.some((dependency) => settled.get(dependency)?.result !== "dismissed")));
|
|
930
|
+
}
|
|
931
|
+
for (const group of sorted([...groupsByKey.values()].filter((group) => !settled.has(group.key)), (group) => group.key))
|
|
932
|
+
settled.set(group.key, resolveGroup(group, true));
|
|
933
|
+
const blockingClaims = new Set();
|
|
934
|
+
const blockingAttestations = new Set();
|
|
935
|
+
for (const group of sorted([...groupsByKey.values()], (group) => group.key)) {
|
|
936
|
+
const resolution = settled.get(group.key);
|
|
937
|
+
const work = group.entries[0].work;
|
|
938
|
+
resolutions.push(resolution);
|
|
939
|
+
allocations.push({
|
|
940
|
+
stage: "challenge_resolution",
|
|
941
|
+
dimensionId: work.criterion.dimensionId,
|
|
942
|
+
criterionId: work.criterion.criterionId,
|
|
943
|
+
claimId: work.claim.recordId,
|
|
944
|
+
dependencyGroupId: group.challengerGroupId,
|
|
945
|
+
candidateRecordIds: resolution.consideredAttestationIds,
|
|
946
|
+
capBasisPoints: 10_000,
|
|
947
|
+
allocatedWeightBasisPoints: Math.max(resolution.corroboratingWeightBasisPoints, resolution.opposingWeightBasisPoints),
|
|
948
|
+
});
|
|
949
|
+
if (group.targetKind === "claim" && resolution.result !== "dismissed")
|
|
950
|
+
blockingClaims.add(group.targetDigest);
|
|
951
|
+
if (group.targetKind === "attestation" && resolution.result !== "dismissed")
|
|
952
|
+
blockingAttestations.add(group.targetDigest);
|
|
953
|
+
}
|
|
954
|
+
for (const work of works) {
|
|
955
|
+
for (const candidate of work.candidates)
|
|
956
|
+
candidate.allocated = 0;
|
|
957
|
+
const availableCandidates = work.candidates.filter((candidate) => {
|
|
958
|
+
if (!blockingAttestations.has(candidate.record.recordDigest))
|
|
959
|
+
return true;
|
|
960
|
+
exclusions.push(reasonExclusion(candidate.record, "challenge_unresolved"));
|
|
961
|
+
return false;
|
|
962
|
+
});
|
|
963
|
+
const groupAllocations = allocateAttestationCandidates(availableCandidates);
|
|
964
|
+
const dispositionByGroup = new Map(groupAllocations.map((allocation) => [
|
|
965
|
+
allocation.groupId,
|
|
966
|
+
allocation.disposition,
|
|
967
|
+
]));
|
|
968
|
+
for (const candidate of availableCandidates) {
|
|
969
|
+
if (candidate.sourceWeight > 0 &&
|
|
970
|
+
candidate.disposition !== dispositionByGroup.get(candidate.groupId))
|
|
971
|
+
exclusions.push(reasonExclusion(candidate.record, "dependency_group_conflict"));
|
|
972
|
+
}
|
|
973
|
+
for (const allocation of groupAllocations) {
|
|
974
|
+
for (const candidate of allocation.candidates)
|
|
975
|
+
candidate.allocated =
|
|
976
|
+
allocation.allocatedByRecordDigest.get(candidate.record.recordDigest) ?? 0;
|
|
977
|
+
allocations.push({
|
|
978
|
+
stage: "attestation",
|
|
979
|
+
dimensionId: work.criterion.dimensionId,
|
|
980
|
+
criterionId: work.criterion.criterionId,
|
|
981
|
+
claimId: work.claim.recordId,
|
|
982
|
+
dependencyGroupId: allocation.groupId,
|
|
983
|
+
candidateRecordIds: uniqueSorted(allocation.candidates.map((candidate) => candidate.record.recordId), "attestation candidates"),
|
|
984
|
+
capBasisPoints: allocation.capBasisPoints,
|
|
985
|
+
allocatedWeightBasisPoints: allocation.allocatedWeightBasisPoints,
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
const supported = [];
|
|
990
|
+
for (const work of works) {
|
|
991
|
+
const supportGroups = new Set(), contradictionGroups = new Set();
|
|
992
|
+
let supportWeight = 0, contradictionWeight = 0;
|
|
993
|
+
for (const candidate of work.candidates) {
|
|
994
|
+
if (candidate.allocated === 0)
|
|
995
|
+
continue;
|
|
996
|
+
if (candidate.disposition === "support") {
|
|
997
|
+
supportWeight = clamp(add(supportWeight, candidate.allocated));
|
|
998
|
+
if (candidate.countsForThreshold)
|
|
999
|
+
supportGroups.add(candidate.groupId);
|
|
1000
|
+
}
|
|
1001
|
+
else if (candidate.disposition === "contradict") {
|
|
1002
|
+
contradictionWeight = clamp(add(contradictionWeight, candidate.allocated));
|
|
1003
|
+
if (candidate.countsForThreshold)
|
|
1004
|
+
contradictionGroups.add(candidate.groupId);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
let classification = "inconclusive";
|
|
1008
|
+
if (work.reasons.size || blockingClaims.has(work.claim.recordDigest))
|
|
1009
|
+
classification = "unavailable";
|
|
1010
|
+
else if (supportGroups.size >= work.criterion.minimumSupportGroups &&
|
|
1011
|
+
supportWeight >= work.criterion.minimumSupportWeightBasisPoints &&
|
|
1012
|
+
contradictionGroups.size >= work.criterion.minimumContradictionGroups &&
|
|
1013
|
+
contradictionWeight >=
|
|
1014
|
+
work.criterion.minimumContradictionWeightBasisPoints)
|
|
1015
|
+
classification = "contested";
|
|
1016
|
+
else if (supportGroups.size >= work.criterion.minimumSupportGroups &&
|
|
1017
|
+
supportWeight >= work.criterion.minimumSupportWeightBasisPoints)
|
|
1018
|
+
classification = "supported";
|
|
1019
|
+
else if (contradictionGroups.size >= work.criterion.minimumContradictionGroups &&
|
|
1020
|
+
contradictionWeight >=
|
|
1021
|
+
work.criterion.minimumContradictionWeightBasisPoints)
|
|
1022
|
+
classification = "contradicted";
|
|
1023
|
+
const mappedValue = classification === "supported"
|
|
1024
|
+
? work.claim.record.outcome === "satisfied"
|
|
1025
|
+
? work.criterion.satisfiedValueBasisPoints
|
|
1026
|
+
: work.claim.record.outcome === "violated"
|
|
1027
|
+
? work.criterion.violatedValueBasisPoints
|
|
1028
|
+
: work.criterion.inconclusiveValueBasisPoints
|
|
1029
|
+
: null;
|
|
1030
|
+
const rawWeight = classification === "supported"
|
|
1031
|
+
? Math.min(work.criterion.maximumClaimWeightBasisPoints, work.criterion.baseWeightBasisPoints, supportWeight)
|
|
1032
|
+
: 0;
|
|
1033
|
+
const dimension = policy.dimensions.find((item) => item.dimensionId === work.criterion.dimensionId);
|
|
1034
|
+
const intervals = Math.floor((logicalTimeMs - work.claim.effectiveAtLogicalMs) /
|
|
1035
|
+
dimension.decayIntervalMs);
|
|
1036
|
+
const retention = Math.max(dimension.minimumRetainedWeightBasisPoints, 10_000 - product(intervals, dimension.decayBasisPointsPerInterval));
|
|
1037
|
+
const effectiveWeight = rawWeight
|
|
1038
|
+
? Math.floor(product(rawWeight, retention) / 10_000)
|
|
1039
|
+
: 0;
|
|
1040
|
+
work.classification = {
|
|
1041
|
+
claimId: work.claim.recordId,
|
|
1042
|
+
claimDigest: work.claim.recordDigest,
|
|
1043
|
+
criterionId: work.criterion.criterionId,
|
|
1044
|
+
dimensionId: work.criterion.dimensionId,
|
|
1045
|
+
classification,
|
|
1046
|
+
mappedValueBasisPoints: mappedValue,
|
|
1047
|
+
supportGroupIds: uniqueSorted([...supportGroups], "support groups"),
|
|
1048
|
+
supportWeightBasisPoints: supportWeight,
|
|
1049
|
+
contradictionGroupIds: uniqueSorted([...contradictionGroups], "contradiction groups"),
|
|
1050
|
+
contradictionWeightBasisPoints: contradictionWeight,
|
|
1051
|
+
rawWeightBasisPoints: rawWeight,
|
|
1052
|
+
retainedWeightBasisPoints: retention,
|
|
1053
|
+
effectiveWeightBasisPoints: effectiveWeight,
|
|
1054
|
+
claimSourceDependencyGroupId: work.sourceGroupId,
|
|
1055
|
+
effectiveSupportingAttestationIds: uniqueSorted(work.candidates
|
|
1056
|
+
.filter((candidate) => candidate.allocated > 0 && candidate.disposition === "support")
|
|
1057
|
+
.map((candidate) => candidate.record.recordId), "effective supporting attestations"),
|
|
1058
|
+
effectiveContentResolutionIds: uniqueSorted(usableContentResolutions(state, work.claim, resolverDigest, logicalTimeMs).map((resolution) => resolution.resolutionId), "effective content resolutions"),
|
|
1059
|
+
reasonCodes: uniqueSorted([
|
|
1060
|
+
...work.reasons,
|
|
1061
|
+
...(classification === "unavailable" &&
|
|
1062
|
+
blockingClaims.has(work.claim.recordDigest)
|
|
1063
|
+
? ["challenge_unresolved"]
|
|
1064
|
+
: []),
|
|
1065
|
+
], "claim reasons"),
|
|
1066
|
+
};
|
|
1067
|
+
if (classification === "supported" &&
|
|
1068
|
+
mappedValue !== null &&
|
|
1069
|
+
effectiveWeight > 0 &&
|
|
1070
|
+
work.sourceGroupId !== null)
|
|
1071
|
+
supported.push(work);
|
|
1072
|
+
else
|
|
1073
|
+
exclusions.push(reasonExclusion(work.claim, classification === "contested"
|
|
1074
|
+
? "fusion_contested"
|
|
1075
|
+
: classification === "unavailable"
|
|
1076
|
+
? "evidence_unavailable"
|
|
1077
|
+
: "support_threshold_missing"));
|
|
1078
|
+
}
|
|
1079
|
+
const retained = [];
|
|
1080
|
+
const rootGroups = new Map();
|
|
1081
|
+
const rootConflictByDimension = new Map();
|
|
1082
|
+
for (const work of supported) {
|
|
1083
|
+
if (work.effectiveRootBasisDigest === null) {
|
|
1084
|
+
exclusions.push(reasonExclusion(work.claim, "source_not_effective"));
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
1087
|
+
const key = `${work.criterion.dimensionId}\u0000${work.effectiveRootBasisDigest}`;
|
|
1088
|
+
(rootGroups.get(key) ?? rootGroups.set(key, []).get(key)).push(work);
|
|
1089
|
+
}
|
|
1090
|
+
for (const values of rootGroups.values()) {
|
|
1091
|
+
if (new Set(values.map((work) => work.classification.mappedValueBasisPoints))
|
|
1092
|
+
.size !== 1) {
|
|
1093
|
+
rootConflictByDimension.set(values[0].criterion.dimensionId, add(rootConflictByDimension.get(values[0].criterion.dimensionId) ?? 0, 1));
|
|
1094
|
+
for (const work of values)
|
|
1095
|
+
exclusions.push(reasonExclusion(work.claim, "root_basis_conflict"));
|
|
1096
|
+
continue;
|
|
1097
|
+
}
|
|
1098
|
+
retained.push([...values].sort((a, b) => b.classification.effectiveWeightBasisPoints -
|
|
1099
|
+
a.classification.effectiveWeightBasisPoints ||
|
|
1100
|
+
b.claim.effectiveAtLogicalMs - a.claim.effectiveAtLogicalMs ||
|
|
1101
|
+
compare(a.claim.recordDigest, b.claim.recordDigest))[0]);
|
|
1102
|
+
}
|
|
1103
|
+
const dimensions = [];
|
|
1104
|
+
for (const dimension of policy.dimensions) {
|
|
1105
|
+
const accepted = retained
|
|
1106
|
+
.filter((work) => work.criterion.dimensionId === dimension.dimensionId)
|
|
1107
|
+
.sort((a, b) => b.claim.effectiveAtLogicalMs - a.claim.effectiveAtLogicalMs ||
|
|
1108
|
+
compare(a.claim.recordDigest, b.claim.recordDigest));
|
|
1109
|
+
const capRemaining = new Map();
|
|
1110
|
+
for (const work of accepted) {
|
|
1111
|
+
const group = groups.get(work.sourceGroupId);
|
|
1112
|
+
const key = `${work.criterion.criterionId}\u0000${work.sourceGroupId}`;
|
|
1113
|
+
const cap = Math.min(work.criterion.maximumSourceGroupContributionWeightBasisPoints, group.maximumProfileWeightPerDimensionCriterionBasisPoints);
|
|
1114
|
+
const remaining = capRemaining.get(key) ?? cap;
|
|
1115
|
+
const allocated = Math.min(work.classification.effectiveWeightBasisPoints, remaining);
|
|
1116
|
+
if (allocated < work.classification.effectiveWeightBasisPoints)
|
|
1117
|
+
exclusions.push(reasonExclusion(work.claim, "dependency_group_cap_exhausted"));
|
|
1118
|
+
capRemaining.set(key, remaining - allocated);
|
|
1119
|
+
work.classification = {
|
|
1120
|
+
...work.classification,
|
|
1121
|
+
effectiveWeightBasisPoints: allocated,
|
|
1122
|
+
};
|
|
1123
|
+
allocations.push({
|
|
1124
|
+
stage: "profile",
|
|
1125
|
+
dimensionId: dimension.dimensionId,
|
|
1126
|
+
criterionId: work.criterion.criterionId,
|
|
1127
|
+
claimId: work.claim.recordId,
|
|
1128
|
+
dependencyGroupId: work.sourceGroupId,
|
|
1129
|
+
candidateRecordIds: [work.claim.recordId],
|
|
1130
|
+
capBasisPoints: cap,
|
|
1131
|
+
allocatedWeightBasisPoints: allocated,
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
const effective = accepted.reduce((sum, work) => add(sum, work.classification.effectiveWeightBasisPoints), 0);
|
|
1135
|
+
const qualifying = accepted.filter((work) => work.classification.effectiveWeightBasisPoints > 0);
|
|
1136
|
+
const numerator = accepted.reduce((sum, work) => add(sum, product(work.classification.mappedValueBasisPoints, work.classification.effectiveWeightBasisPoints)), product(dimension.priorScoreBasisPoints, dimension.priorWeightBasisPoints));
|
|
1137
|
+
const denominator = add(dimension.priorWeightBasisPoints, effective);
|
|
1138
|
+
if (denominator <= 0)
|
|
1139
|
+
throw new TrustValidationError("fusion denominator is invalid");
|
|
1140
|
+
const score = Math.floor(numerator / denominator);
|
|
1141
|
+
const coverage = Math.min(10_000, Math.floor(product(effective, 10_000) / dimension.coverageTargetBasisPoints));
|
|
1142
|
+
const ageUncertainty = qualifying.length
|
|
1143
|
+
? Math.min(10_000, Math.max(...qualifying.map((work) => Math.min(10_000, product(Math.floor((logicalTimeMs - work.claim.effectiveAtLogicalMs) /
|
|
1144
|
+
dimension.decayIntervalMs), dimension.uncertaintyGrowthBasisPointsPerInterval)))))
|
|
1145
|
+
: 0;
|
|
1146
|
+
const contestedCount = works.filter((work) => work.criterion.dimensionId === dimension.dimensionId &&
|
|
1147
|
+
(work.classification.classification === "contested" ||
|
|
1148
|
+
(work.classification.classification === "unavailable" &&
|
|
1149
|
+
work.claim.status === "challenged"))).length + (rootConflictByDimension.get(dimension.dimensionId) ?? 0);
|
|
1150
|
+
const pressure = Math.min(dimension.maximumContradictionUncertaintyBasisPoints, product(contestedCount, dimension.contradictionUncertaintyBasisPointsPerClaim));
|
|
1151
|
+
dimensions.push({
|
|
1152
|
+
dimensionId: dimension.dimensionId,
|
|
1153
|
+
scoreBasisPoints: clamp(score),
|
|
1154
|
+
uncertaintyBasisPoints: Math.max(dimension.minimumUncertaintyBasisPoints, 10_000 - coverage, ageUncertainty, pressure),
|
|
1155
|
+
effectiveWeightBasisPoints: effective,
|
|
1156
|
+
coverageBasisPoints: coverage,
|
|
1157
|
+
ageUncertaintyBasisPoints: ageUncertainty,
|
|
1158
|
+
contradictionPressureBasisPoints: pressure,
|
|
1159
|
+
includedClaimIds: uniqueSorted(qualifying.map((work) => work.claim.recordId), "included claims"),
|
|
1160
|
+
excludedClaimIds: uniqueSorted(works
|
|
1161
|
+
.filter((work) => work.criterion.dimensionId === dimension.dimensionId &&
|
|
1162
|
+
!qualifying.includes(work))
|
|
1163
|
+
.map((work) => work.claim.recordId), "excluded claims"),
|
|
1164
|
+
claimSourceDependencyGroupIds: uniqueSorted(qualifying.map((work) => work.sourceGroupId), "claim source groups"),
|
|
1165
|
+
latestQualifyingEffectiveAtLogicalMs: qualifying.length
|
|
1166
|
+
? Math.max(...qualifying.map((work) => work.claim.effectiveAtLogicalMs))
|
|
1167
|
+
: null,
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
const consideredRecordIds = uniqueSorted(allScopeRecords.map((record) => record.recordId), "considered records");
|
|
1171
|
+
const inputSetDigest = digestTrustJsonV1("fusion-input", {
|
|
1172
|
+
subjectDigest,
|
|
1173
|
+
scopeDigest,
|
|
1174
|
+
policyDigest: request.policyDigest,
|
|
1175
|
+
evaluatedAtLogicalMs: logicalTimeMs,
|
|
1176
|
+
records: allScopeRecords.map((record) => ({
|
|
1177
|
+
recordKind: record.recordKind,
|
|
1178
|
+
recordId: record.recordId,
|
|
1179
|
+
recordDigest: record.recordDigest,
|
|
1180
|
+
status: record.status,
|
|
1181
|
+
originBindingDigest: record.originBindingDigest,
|
|
1182
|
+
originVerifierBindingDigest: record.originVerifierBindingDigest,
|
|
1183
|
+
originProofDigest: record.originProofDigest,
|
|
1184
|
+
acceptedAtLogicalMs: record.acceptedAtLogicalMs,
|
|
1185
|
+
effectiveAtLogicalMs: record.effectiveAtLogicalMs,
|
|
1186
|
+
})),
|
|
1187
|
+
contentResolutions: state.contentResolutions
|
|
1188
|
+
.filter((item) => item.resolvedAtLogicalMs <= logicalTimeMs)
|
|
1189
|
+
.map((item) => ({
|
|
1190
|
+
resolutionId: item.resolutionId,
|
|
1191
|
+
resolutionDigest: item.resolutionDigest,
|
|
1192
|
+
resolvedAtLogicalMs: item.resolvedAtLogicalMs,
|
|
1193
|
+
})),
|
|
1194
|
+
contentInvalidations: state.contentInvalidations
|
|
1195
|
+
.filter((item) => item.invalidatedAtLogicalMs <= logicalTimeMs)
|
|
1196
|
+
.map((item) => ({
|
|
1197
|
+
invalidationId: item.invalidationId,
|
|
1198
|
+
resolutionId: item.resolutionId,
|
|
1199
|
+
resolutionDigest: item.resolutionDigest,
|
|
1200
|
+
resolverBindingDigest: item.resolverBindingDigest,
|
|
1201
|
+
invalidatedAtLogicalMs: item.invalidatedAtLogicalMs,
|
|
1202
|
+
})),
|
|
1203
|
+
causalAuthorizations: state.causalAuthorizations
|
|
1204
|
+
.filter((item) => item.authorizedAtLogicalMs <= logicalTimeMs)
|
|
1205
|
+
.map((item) => ({
|
|
1206
|
+
authorizationId: item.authorizationId,
|
|
1207
|
+
authorizationDigest: item.authorizationDigest,
|
|
1208
|
+
recordId: item.recordId,
|
|
1209
|
+
recordDigest: item.recordDigest,
|
|
1210
|
+
authorityBindingDigest: item.authorityBindingDigest,
|
|
1211
|
+
authorizedAtLogicalMs: item.authorizedAtLogicalMs,
|
|
1212
|
+
})),
|
|
1213
|
+
dependencyBindingDigests: request.dependencyBindingDigests,
|
|
1214
|
+
}, FUSION_PROJECTION_JSON_LIMITS_V1);
|
|
1215
|
+
const exclusionGroups = new Map();
|
|
1216
|
+
for (const exclusion of exclusions) {
|
|
1217
|
+
const key = `${exclusion.recordDigest}\u0000${exclusion.recordId}`;
|
|
1218
|
+
const prior = exclusionGroups.get(key);
|
|
1219
|
+
exclusionGroups.set(key, {
|
|
1220
|
+
...exclusion,
|
|
1221
|
+
reasonCodes: uniqueSorted([...(prior?.reasonCodes ?? []), ...exclusion.reasonCodes], "fusion exclusion reasons"),
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
const normalizedExclusions = sorted([...exclusionGroups.values()], (item) => `${item.recordDigest}\u0000${item.recordId}`);
|
|
1225
|
+
const profileKey = digestTrustJsonV1("profile-key", {
|
|
1226
|
+
tenantId: request.tenantId,
|
|
1227
|
+
scopeDigest,
|
|
1228
|
+
subjectDigest,
|
|
1229
|
+
policyDigest: request.policyDigest,
|
|
1230
|
+
});
|
|
1231
|
+
const previousProfileDigest = state.profileHeads.find((head) => head.profileKey === profileKey)
|
|
1232
|
+
?.profileDigest ?? null;
|
|
1233
|
+
const bare = {
|
|
1234
|
+
schemaVersion: 1,
|
|
1235
|
+
tenantId: request.tenantId,
|
|
1236
|
+
subject: request.subject,
|
|
1237
|
+
subjectDigest,
|
|
1238
|
+
scope: request.scope,
|
|
1239
|
+
scopeDigest,
|
|
1240
|
+
policyId: request.policyId,
|
|
1241
|
+
policyVersion: request.policyVersion,
|
|
1242
|
+
policyDigest: request.policyDigest,
|
|
1243
|
+
evaluatedAtLogicalMs: logicalTimeMs,
|
|
1244
|
+
inputSetDigest,
|
|
1245
|
+
consideredRecordIds,
|
|
1246
|
+
includedRecordIds: uniqueSorted([
|
|
1247
|
+
...new Set([
|
|
1248
|
+
...retained.map((work) => work.claim.recordId),
|
|
1249
|
+
...works.flatMap((work) => work.candidates
|
|
1250
|
+
.filter((candidate) => candidate.allocated > 0)
|
|
1251
|
+
.map((candidate) => candidate.record.recordId)),
|
|
1252
|
+
...resolutions.flatMap((resolution) => [
|
|
1253
|
+
...resolution.challenges.map((challenge) => challenge.challengeId),
|
|
1254
|
+
...resolution.consideredAttestationIds,
|
|
1255
|
+
]),
|
|
1256
|
+
]),
|
|
1257
|
+
], "included records"),
|
|
1258
|
+
recordExclusions: normalizedExclusions,
|
|
1259
|
+
claimClassifications: sorted(works.map((work) => work.classification), (item) => item.claimDigest),
|
|
1260
|
+
challengeResolutions: sorted(resolutions, (item) => item.challengeResolutionId),
|
|
1261
|
+
groupAllocations: sorted(allocations, (item) => `${item.stage}\u0000${item.dimensionId ?? ""}\u0000${item.criterionId ?? ""}\u0000${item.claimId ?? ""}\u0000${item.dependencyGroupId}\u0000${item.candidateRecordIds.join("\u0000")}`),
|
|
1262
|
+
dimensions: sorted(dimensions, (item) => item.dimensionId),
|
|
1263
|
+
previousProfileDigest,
|
|
1264
|
+
reasonCodes: uniqueSorted([
|
|
1265
|
+
...new Set([
|
|
1266
|
+
...normalizedExclusions.flatMap((item) => item.reasonCodes),
|
|
1267
|
+
...resolutions.flatMap((item) => item.reasonCodes),
|
|
1268
|
+
]),
|
|
1269
|
+
], "fusion reasons"),
|
|
1270
|
+
};
|
|
1271
|
+
const digest = digestTrustJsonV1("fusion-decision", bare, FUSION_PROJECTION_JSON_LIMITS_V1);
|
|
1272
|
+
return validateEvidenceFusionDecisionV1({
|
|
1273
|
+
...bare,
|
|
1274
|
+
fusionDecisionDigest: digest,
|
|
1275
|
+
fusionDecisionId: `fusion-decision:${digest}`,
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
//# sourceMappingURL=fusion.js.map
|