@mikeargento/bitgraph-player 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +98 -0
- package/SPEC.md +234 -0
- package/dist/__tests__/cast-order.test.d.ts +2 -0
- package/dist/__tests__/cast-order.test.d.ts.map +1 -0
- package/dist/__tests__/cast-order.test.js +434 -0
- package/dist/__tests__/cast-order.test.js.map +1 -0
- package/dist/__tests__/evaluate-verdict.test.d.ts +2 -0
- package/dist/__tests__/evaluate-verdict.test.d.ts.map +1 -0
- package/dist/__tests__/evaluate-verdict.test.js +228 -0
- package/dist/__tests__/evaluate-verdict.test.js.map +1 -0
- package/dist/__tests__/fixtures.d.ts +63 -0
- package/dist/__tests__/fixtures.d.ts.map +1 -0
- package/dist/__tests__/fixtures.js +150 -0
- package/dist/__tests__/fixtures.js.map +1 -0
- package/dist/__tests__/logic-rule.test.d.ts +2 -0
- package/dist/__tests__/logic-rule.test.d.ts.map +1 -0
- package/dist/__tests__/logic-rule.test.js +190 -0
- package/dist/__tests__/logic-rule.test.js.map +1 -0
- package/dist/cast.d.ts +29 -0
- package/dist/cast.d.ts.map +1 -0
- package/dist/cast.js +85 -0
- package/dist/cast.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +130 -0
- package/dist/cli.js.map +1 -0
- package/dist/evaluate.d.ts +30 -0
- package/dist/evaluate.d.ts.map +1 -0
- package/dist/evaluate.js +177 -0
- package/dist/evaluate.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/logic.d.ts +16 -0
- package/dist/logic.d.ts.map +1 -0
- package/dist/logic.js +29 -0
- package/dist/logic.js.map +1 -0
- package/dist/order.d.ts +50 -0
- package/dist/order.d.ts.map +1 -0
- package/dist/order.js +290 -0
- package/dist/order.js.map +1 -0
- package/dist/rule.d.ts +40 -0
- package/dist/rule.d.ts.map +1 -0
- package/dist/rule.js +324 -0
- package/dist/rule.js.map +1 -0
- package/dist/types.d.ts +205 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/dist/verdict.d.ts +14 -0
- package/dist/verdict.d.ts.map +1 -0
- package/dist/verdict.js +146 -0
- package/dist/verdict.js.map +1 -0
- package/package.json +42 -0
- package/src/__tests__/cast-order.test.ts +473 -0
- package/src/__tests__/evaluate-verdict.test.ts +266 -0
- package/src/__tests__/fixtures.ts +206 -0
- package/src/__tests__/logic-rule.test.ts +221 -0
- package/src/cast.ts +120 -0
- package/src/cli.ts +143 -0
- package/src/evaluate.ts +238 -0
- package/src/index.ts +38 -0
- package/src/logic.ts +39 -0
- package/src/order.ts +380 -0
- package/src/rule.ts +340 -0
- package/src/types.ts +224 -0
- package/src/verdict.ts +159 -0
package/src/order.ts
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The ordering adapter. Player does not reimplement causal reconstruction —
|
|
5
|
+
* `bitgraph-audit` already computes it — this module carries audit's
|
|
6
|
+
* answers into a single compare(a, b) without flattening their evidence
|
|
7
|
+
* classes, and without asserting anything the specific pair of recordings
|
|
8
|
+
* is not actually covered by.
|
|
9
|
+
*
|
|
10
|
+
* An artifact's position is its COMMIT position. At the slot the digest is
|
|
11
|
+
* not bound yet, so the slot cannot be the position of the artifact.
|
|
12
|
+
*
|
|
13
|
+
* Within a partition (signer key, epochId, chainId):
|
|
14
|
+
* - "chain-link": a DIRECTED prevB64 path connects the two recordings
|
|
15
|
+
* through observed proofs. The path itself decides the direction; a
|
|
16
|
+
* path contradicting the counters is an anomaly and answers nothing.
|
|
17
|
+
* Undirected co-membership in a component is NOT chain-link evidence:
|
|
18
|
+
* two fork branches share a component without any path between them.
|
|
19
|
+
* - "counter-order": no path, but both commit counters parse under the
|
|
20
|
+
* canonical strict-decimal grammar. Rests on counter discipline.
|
|
21
|
+
*
|
|
22
|
+
* Across epochs:
|
|
23
|
+
* - "epoch-lineage": a chain of hard epochLink edges whose observed
|
|
24
|
+
* predecessor/via proofs cover THIS pair. Successor-side coverage is
|
|
25
|
+
* by signer key: a proof signed by the successor key cannot predate
|
|
26
|
+
* that key's creation at epoch init, which the link proves happened
|
|
27
|
+
* after the referenced predecessor existed. Predecessor-side coverage
|
|
28
|
+
* is a directed path to (or identity with) the referenced predecessor
|
|
29
|
+
* — hash-linked — or a strictly smaller parseable counter, which
|
|
30
|
+
* downgrades the whole answer to the assumption-dependent tier.
|
|
31
|
+
* - "anchor-bounds": strict block separation between verified segment
|
|
32
|
+
* bounds; always assumption-dependent (anchor freshness). If BOTH
|
|
33
|
+
* directions satisfy strict separation the evidence is contradictory
|
|
34
|
+
* and answers nothing.
|
|
35
|
+
* Epoch pairs supported by neither are concurrent-or-unordered, and the
|
|
36
|
+
* honest answer is "unordered", never a coin flip.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import type {
|
|
40
|
+
AuditResult,
|
|
41
|
+
ChainPartition,
|
|
42
|
+
ObservedProof,
|
|
43
|
+
SegmentBound,
|
|
44
|
+
TemporalSegment,
|
|
45
|
+
} from "@mikeargento/bitgraph-audit";
|
|
46
|
+
import type { EvidenceTier, OrderResult } from "./types.js";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Canonical strict-decimal counter grammar, mirroring audit's own
|
|
50
|
+
* parseCounter. Every other spelling BigInt would accept ("", "0x10",
|
|
51
|
+
* "-3", " 7 ") is NO counter evidence, exactly as audit treats it.
|
|
52
|
+
*/
|
|
53
|
+
export function parseCounter(value: string | undefined): bigint | undefined {
|
|
54
|
+
if (value === undefined || value.length === 0 || !/^[0-9]+$/.test(value)) return undefined;
|
|
55
|
+
return BigInt(value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function unordered(detail: string): OrderResult {
|
|
59
|
+
return { relation: "unordered", assumptionDependent: false, weaker: false, detail };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function partitionOf(audit: AuditResult, proofHash: string): ChainPartition | undefined {
|
|
63
|
+
return audit.reconstruction.partitions.find((p) => p.memberProofHashes.includes(proofHash));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function segmentOf(audit: AuditResult, proofHash: string): TemporalSegment | undefined {
|
|
67
|
+
return audit.temporal.segments.find((s) => s.memberProofHashes.includes(proofHash));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function proofsByHash(audit: AuditResult): Map<string, ObservedProof> {
|
|
71
|
+
return new Map(audit.ingest.proofs.map((p) => [p.proofHash, p]));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* True when `ancestor` lies on the directed prevB64 chain above
|
|
76
|
+
* `descendant`, walking only proofs observed in the bundle and confined to
|
|
77
|
+
* one partition. Identity does not count.
|
|
78
|
+
*/
|
|
79
|
+
function isDirectedAncestor(
|
|
80
|
+
ancestor: ObservedProof,
|
|
81
|
+
descendant: ObservedProof,
|
|
82
|
+
partition: ChainPartition,
|
|
83
|
+
byChainHash: Map<string, ObservedProof>
|
|
84
|
+
): boolean {
|
|
85
|
+
const members = new Set(partition.memberProofHashes);
|
|
86
|
+
const visited = new Set<string>();
|
|
87
|
+
let current: ObservedProof | undefined = descendant;
|
|
88
|
+
while (current !== undefined) {
|
|
89
|
+
const prev = current.prevB64;
|
|
90
|
+
if (prev === undefined) return false;
|
|
91
|
+
const parent = byChainHash.get(prev);
|
|
92
|
+
if (parent === undefined || !members.has(parent.proofHash)) return false;
|
|
93
|
+
if (visited.has(parent.proofHash)) return false; // cycle guard
|
|
94
|
+
visited.add(parent.proofHash);
|
|
95
|
+
if (parent.proofHash === ancestor.proofHash) return true;
|
|
96
|
+
current = parent;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function chainHashIndex(audit: AuditResult, partition: ChainPartition): Map<string, ObservedProof> {
|
|
102
|
+
const byHash = proofsByHash(audit);
|
|
103
|
+
const index = new Map<string, ObservedProof>();
|
|
104
|
+
for (const memberHash of partition.memberProofHashes) {
|
|
105
|
+
const proof = byHash.get(memberHash);
|
|
106
|
+
if (proof !== undefined) index.set(proof.chainHash, proof);
|
|
107
|
+
}
|
|
108
|
+
return index;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Epoch lineage at recording granularity
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Try to prove `a` precedes `b` through a chain of hard epochLink edges.
|
|
117
|
+
*
|
|
118
|
+
* A usable first edge references an observed predecessor proof P in a's
|
|
119
|
+
* own partition, with a covering P (identity, directed path, or strictly
|
|
120
|
+
* smaller parseable counter — the last downgrades the tier). Every
|
|
121
|
+
* subsequent edge must be anchored in the partition the previous edge's
|
|
122
|
+
* via proof belongs to; reaching b's partition completes the proof, since
|
|
123
|
+
* any proof of that partition postdates its key's creation at init.
|
|
124
|
+
*/
|
|
125
|
+
function lineagePrecedes(
|
|
126
|
+
a: ObservedProof,
|
|
127
|
+
partA: ChainPartition,
|
|
128
|
+
b: ObservedProof,
|
|
129
|
+
partB: ChainPartition,
|
|
130
|
+
audit: AuditResult
|
|
131
|
+
): { tier: EvidenceTier } | undefined {
|
|
132
|
+
const byHash = proofsByHash(audit);
|
|
133
|
+
const edges = audit.reconstruction.epochRelationships.edges.filter(
|
|
134
|
+
(e) => e.hardEdge && e.predecessorProofHash !== undefined
|
|
135
|
+
);
|
|
136
|
+
const partitionKeyOf = (p: ChainPartition): string => JSON.stringify(p.key);
|
|
137
|
+
const partitionByProofHash = (hash: string): ChainPartition | undefined =>
|
|
138
|
+
partitionOf(audit, hash);
|
|
139
|
+
|
|
140
|
+
// First hops out of a's partition, each with the tier its coverage rests on.
|
|
141
|
+
interface State {
|
|
142
|
+
partitionKey: string;
|
|
143
|
+
tier: EvidenceTier;
|
|
144
|
+
}
|
|
145
|
+
const queue: State[] = [];
|
|
146
|
+
const chainIndexA = chainHashIndex(audit, partA);
|
|
147
|
+
const ca = parseCounter(a.counter);
|
|
148
|
+
for (const edge of edges) {
|
|
149
|
+
const pred = byHash.get(edge.predecessorProofHash as string);
|
|
150
|
+
if (pred === undefined) continue;
|
|
151
|
+
const predPartition = partitionByProofHash(pred.proofHash);
|
|
152
|
+
if (predPartition !== partA) continue;
|
|
153
|
+
let hopTier: EvidenceTier | undefined;
|
|
154
|
+
if (pred.proofHash === a.proofHash || isDirectedAncestor(a, pred, partA, chainIndexA)) {
|
|
155
|
+
hopTier = "hash-linked";
|
|
156
|
+
} else {
|
|
157
|
+
const cp = parseCounter(pred.counter);
|
|
158
|
+
if (ca !== undefined && cp !== undefined && ca < cp) hopTier = "assumption-dependent";
|
|
159
|
+
}
|
|
160
|
+
if (hopTier === undefined) continue;
|
|
161
|
+
const viaPartition = partitionByProofHash(edge.viaProofHash);
|
|
162
|
+
if (viaPartition === undefined) continue;
|
|
163
|
+
queue.push({ partitionKey: partitionKeyOf(viaPartition), tier: hopTier });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// BFS over subsequent hops. Later hops are free of counter assumptions:
|
|
167
|
+
// the next edge's predecessor proof is signed by the current partition's
|
|
168
|
+
// key, so it postdates that key's creation.
|
|
169
|
+
const bestTierByPartition = new Map<string, EvidenceTier>();
|
|
170
|
+
const targetKey = partitionKeyOf(partB);
|
|
171
|
+
let best: EvidenceTier | undefined;
|
|
172
|
+
while (queue.length > 0) {
|
|
173
|
+
const state = queue.shift() as State;
|
|
174
|
+
const seen = bestTierByPartition.get(state.partitionKey);
|
|
175
|
+
if (seen === "hash-linked" || seen === state.tier) continue;
|
|
176
|
+
if (seen === undefined || state.tier === "hash-linked") {
|
|
177
|
+
bestTierByPartition.set(state.partitionKey, state.tier);
|
|
178
|
+
}
|
|
179
|
+
if (state.partitionKey === targetKey) {
|
|
180
|
+
if (best === undefined || state.tier === "hash-linked") best = state.tier;
|
|
181
|
+
if (best === "hash-linked") break;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
for (const edge of edges) {
|
|
185
|
+
const pred = byHash.get(edge.predecessorProofHash as string);
|
|
186
|
+
if (pred === undefined) continue;
|
|
187
|
+
const predPartition = partitionByProofHash(pred.proofHash);
|
|
188
|
+
if (predPartition === undefined || partitionKeyOf(predPartition) !== state.partitionKey) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const viaPartition = partitionByProofHash(edge.viaProofHash);
|
|
192
|
+
if (viaPartition === undefined) continue;
|
|
193
|
+
queue.push({ partitionKey: partitionKeyOf(viaPartition), tier: state.tier });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return best === undefined ? undefined : { tier: best };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// Anchor bounds
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Strict precedence between two one-sided anchor bounds. Equality proves
|
|
205
|
+
* nothing: A precedes the anchor COMMIT that consumed block N, B follows
|
|
206
|
+
* the MINING of block N, and the gap between mining and commit is exactly
|
|
207
|
+
* where the two could swap.
|
|
208
|
+
*/
|
|
209
|
+
function boundsStrictlyOrdered(upper: SegmentBound, lower: SegmentBound): boolean {
|
|
210
|
+
if (upper.blockNumber !== undefined && lower.blockNumber !== undefined) {
|
|
211
|
+
return BigInt(upper.blockNumber) < BigInt(lower.blockNumber);
|
|
212
|
+
}
|
|
213
|
+
// Verified witness timestamps; Ethereum block timestamps are strictly
|
|
214
|
+
// increasing, so strict timestamp inequality is strict block inequality.
|
|
215
|
+
return upper.timestamp < lower.timestamp;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function anchorPrecedes(
|
|
219
|
+
firstSegment: TemporalSegment,
|
|
220
|
+
secondSegment: TemporalSegment
|
|
221
|
+
): { weaker: boolean; upper: SegmentBound; lower: SegmentBound } | undefined {
|
|
222
|
+
let best: { weaker: boolean; upper: SegmentBound; lower: SegmentBound } | undefined;
|
|
223
|
+
for (const upper of firstSegment.upperBounds) {
|
|
224
|
+
for (const lower of secondSegment.lowerBounds) {
|
|
225
|
+
if (!boundsStrictlyOrdered(upper, lower)) continue;
|
|
226
|
+
const weaker = upper.weaker || lower.weaker;
|
|
227
|
+
if (best === undefined || (best.weaker && !weaker)) {
|
|
228
|
+
best = { weaker, upper, lower };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return best;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function describeBound(bound: SegmentBound): string {
|
|
236
|
+
return bound.blockNumber !== undefined ? `block ${bound.blockNumber}` : `t=${bound.timestamp}`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// compare
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Compare two resolved recordings. Pure over the AuditResult; no network,
|
|
245
|
+
* no clock. The returned tier is what the answer actually rests on and is
|
|
246
|
+
* what the evidence floor gates.
|
|
247
|
+
*/
|
|
248
|
+
export function compare(a: ObservedProof, b: ObservedProof, audit: AuditResult): OrderResult {
|
|
249
|
+
if (a.proofHash === b.proofHash) {
|
|
250
|
+
return {
|
|
251
|
+
relation: "same",
|
|
252
|
+
assumptionDependent: false,
|
|
253
|
+
weaker: false,
|
|
254
|
+
detail: "both roles resolved to the identical recording",
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const partA = partitionOf(audit, a.proofHash);
|
|
259
|
+
const partB = partitionOf(audit, b.proofHash);
|
|
260
|
+
|
|
261
|
+
if (partA === undefined || partB === undefined) {
|
|
262
|
+
return unordered(
|
|
263
|
+
"at least one recording is observed-but-unchained (no counter or epochId): it holds no causal position to compare"
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// -------------------------------------------------------------------
|
|
268
|
+
// Same partition: a directed prevB64 path decides; counters corroborate
|
|
269
|
+
// or, absent a path, carry the answer at the weaker tier.
|
|
270
|
+
// -------------------------------------------------------------------
|
|
271
|
+
if (partA === partB) {
|
|
272
|
+
const chainIndex = chainHashIndex(audit, partA);
|
|
273
|
+
const aBeforeB = isDirectedAncestor(a, b, partA, chainIndex);
|
|
274
|
+
const bBeforeA = isDirectedAncestor(b, a, partA, chainIndex);
|
|
275
|
+
const ca = parseCounter(a.counter);
|
|
276
|
+
const cb = parseCounter(b.counter);
|
|
277
|
+
|
|
278
|
+
if (aBeforeB && bBeforeA) {
|
|
279
|
+
return unordered("prevB64 links form a cycle between these recordings: an anomaly, not an order");
|
|
280
|
+
}
|
|
281
|
+
if (aBeforeB || bBeforeA) {
|
|
282
|
+
const relation = aBeforeB ? ("before" as const) : ("after" as const);
|
|
283
|
+
// A verified path contradicted by the counters is an anomaly:
|
|
284
|
+
// both cannot be honest, so neither answers.
|
|
285
|
+
if (ca !== undefined && cb !== undefined && (aBeforeB ? ca >= cb : cb >= ca)) {
|
|
286
|
+
return unordered(
|
|
287
|
+
"a verified prevB64 path contradicts the commit counters: an anomaly, not an order"
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
relation,
|
|
292
|
+
basis: "chain-link",
|
|
293
|
+
tier: "hash-linked",
|
|
294
|
+
assumptionDependent: false,
|
|
295
|
+
weaker: false,
|
|
296
|
+
detail: `a directed prevB64 path through observed proofs connects the recordings; the path decides the direction`,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (ca === undefined || cb === undefined) {
|
|
301
|
+
return unordered(
|
|
302
|
+
"no prevB64 path connects the recordings and at least one carries no parseable strict-decimal commit counter"
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
if (ca === cb) {
|
|
306
|
+
return unordered(
|
|
307
|
+
"two distinct recordings claim the same commit counter in one partition: an anomaly, not an order"
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
relation: ca < cb ? "before" : "after",
|
|
312
|
+
basis: "counter-order",
|
|
313
|
+
tier: "assumption-dependent",
|
|
314
|
+
assumptionDependent: true,
|
|
315
|
+
weaker: true,
|
|
316
|
+
detail: `no prevB64 path in the bundle connects the recordings; commit counters ${a.counter} vs ${b.counter} rest on the authority's counter discipline`,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// -------------------------------------------------------------------
|
|
321
|
+
// Different partitions. Counters are never comparable across
|
|
322
|
+
// partitions; order can only come through covered epoch lineage or
|
|
323
|
+
// anchor bounds.
|
|
324
|
+
// -------------------------------------------------------------------
|
|
325
|
+
if (a.epochId !== undefined && b.epochId !== undefined && a.epochId === b.epochId) {
|
|
326
|
+
return unordered(
|
|
327
|
+
"same epochId under a different signer key or chain: counters are not comparable and no cross-partition evidence orders them"
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const lineageForward = lineagePrecedes(a, partA, b, partB, audit);
|
|
332
|
+
const lineageBackward = lineagePrecedes(b, partB, a, partA, audit);
|
|
333
|
+
if (lineageForward !== undefined && lineageBackward !== undefined) {
|
|
334
|
+
return unordered(
|
|
335
|
+
"epochLink lineage evidence runs in both directions between these recordings: contradictory, an anomaly, not an order"
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
const lineage = lineageForward ?? lineageBackward;
|
|
339
|
+
if (lineage !== undefined) {
|
|
340
|
+
const relation = lineageForward !== undefined ? ("before" as const) : ("after" as const);
|
|
341
|
+
const assumptionDependent = lineage.tier === "assumption-dependent";
|
|
342
|
+
return {
|
|
343
|
+
relation,
|
|
344
|
+
basis: "epoch-lineage",
|
|
345
|
+
tier: lineage.tier,
|
|
346
|
+
assumptionDependent,
|
|
347
|
+
weaker: assumptionDependent,
|
|
348
|
+
detail: assumptionDependent
|
|
349
|
+
? "hard epochLink succession covers this pair, with the predecessor-side coverage resting on counter discipline"
|
|
350
|
+
: "hard epochLink succession covers this pair: the earlier recording precedes the referenced predecessor by hash links, and the later one is signed by a key created after it",
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const segA = segmentOf(audit, a.proofHash);
|
|
355
|
+
const segB = segmentOf(audit, b.proofHash);
|
|
356
|
+
if (segA !== undefined && segB !== undefined) {
|
|
357
|
+
const forward = anchorPrecedes(segA, segB);
|
|
358
|
+
const backward = anchorPrecedes(segB, segA);
|
|
359
|
+
if (forward !== undefined && backward !== undefined) {
|
|
360
|
+
return unordered(
|
|
361
|
+
"anchor bounds satisfy strict precedence in both directions: contradictory evidence, an anomaly, not an order"
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
const hit = forward ?? backward;
|
|
365
|
+
if (hit !== undefined) {
|
|
366
|
+
return {
|
|
367
|
+
relation: forward !== undefined ? "before" : "after",
|
|
368
|
+
basis: "anchor-bounds",
|
|
369
|
+
tier: "assumption-dependent",
|
|
370
|
+
assumptionDependent: true,
|
|
371
|
+
weaker: hit.weaker,
|
|
372
|
+
detail: `not-after anchor ${describeBound(hit.upper)} strictly precedes not-before anchor ${describeBound(hit.lower)}; rests on the anchor-freshness assumption`,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return unordered(
|
|
378
|
+
`epochs ${a.epochId ?? "(none)"} and ${b.epochId ?? "(none)"} are concurrent-or-unordered for this pair: no covering lineage and no strict anchor-bound separation`
|
|
379
|
+
);
|
|
380
|
+
}
|