@mikeargento/bitgraph-player 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +32 -18
  3. package/dist/__tests__/base-floor-check.test.d.ts +2 -0
  4. package/dist/__tests__/base-floor-check.test.d.ts.map +1 -0
  5. package/dist/__tests__/base-floor-check.test.js +163 -0
  6. package/dist/__tests__/base-floor-check.test.js.map +1 -0
  7. package/dist/__tests__/cast-order.test.js +1 -1
  8. package/dist/__tests__/cast-order.test.js.map +1 -1
  9. package/dist/__tests__/check.test.js +2 -2
  10. package/dist/__tests__/check.test.js.map +1 -1
  11. package/dist/__tests__/evaluate-verdict.test.js +1 -1
  12. package/dist/__tests__/evaluate-verdict.test.js.map +1 -1
  13. package/dist/__tests__/fixtures.js +1 -1
  14. package/dist/__tests__/fixtures.js.map +1 -1
  15. package/dist/__tests__/fuse-check.test.js +2 -2
  16. package/dist/__tests__/fuse-check.test.js.map +1 -1
  17. package/dist/__tests__/init-play.test.js +1 -1
  18. package/dist/__tests__/init-play.test.js.map +1 -1
  19. package/dist/__tests__/logic-rule.test.js +1 -1
  20. package/dist/__tests__/logic-rule.test.js.map +1 -1
  21. package/dist/__tests__/sig-v2.test.js +1 -1
  22. package/dist/__tests__/sig-v2.test.js.map +1 -1
  23. package/dist/cast.js +1 -1
  24. package/dist/cast.js.map +1 -1
  25. package/dist/check.d.ts +58 -9
  26. package/dist/check.d.ts.map +1 -1
  27. package/dist/check.js +102 -32
  28. package/dist/check.js.map +1 -1
  29. package/dist/cli.js +1 -1
  30. package/dist/cli.js.map +1 -1
  31. package/dist/evaluate.js +1 -1
  32. package/dist/evaluate.js.map +1 -1
  33. package/dist/index.js +1 -1
  34. package/dist/index.js.map +1 -1
  35. package/dist/init.js +1 -1
  36. package/dist/init.js.map +1 -1
  37. package/dist/logic.js +1 -1
  38. package/dist/logic.js.map +1 -1
  39. package/dist/order.d.ts +4 -1
  40. package/dist/order.d.ts.map +1 -1
  41. package/dist/order.js +10 -1
  42. package/dist/order.js.map +1 -1
  43. package/dist/play.js +1 -1
  44. package/dist/play.js.map +1 -1
  45. package/dist/rule.js +1 -1
  46. package/dist/rule.js.map +1 -1
  47. package/dist/sig.js +1 -1
  48. package/dist/sig.js.map +1 -1
  49. package/dist/types.js +1 -1
  50. package/dist/types.js.map +1 -1
  51. package/dist/verdict.d.ts +1 -1
  52. package/dist/verdict.js +2 -2
  53. package/dist/verdict.js.map +1 -1
  54. package/dist-web/verify.html +3 -3
  55. package/package.json +15 -7
  56. package/src/__tests__/base-floor-check.test.ts +187 -0
  57. package/src/__tests__/cast-order.test.ts +1 -1
  58. package/src/__tests__/check.test.ts +2 -2
  59. package/src/__tests__/evaluate-verdict.test.ts +1 -1
  60. package/src/__tests__/fixtures.ts +1 -1
  61. package/src/__tests__/fuse-check.test.ts +2 -2
  62. package/src/__tests__/init-play.test.ts +1 -1
  63. package/src/__tests__/logic-rule.test.ts +1 -1
  64. package/src/__tests__/sig-v2.test.ts +1 -1
  65. package/src/cast.ts +1 -1
  66. package/src/check.ts +167 -40
  67. package/src/cli.ts +1 -1
  68. package/src/evaluate.ts +1 -1
  69. package/src/index.ts +1 -1
  70. package/src/init.ts +1 -1
  71. package/src/logic.ts +1 -1
  72. package/src/order.ts +13 -2
  73. package/src/play.ts +1 -1
  74. package/src/rule.ts +1 -1
  75. package/src/sig.ts +1 -1
  76. package/src/types.ts +1 -1
  77. package/src/verdict.ts +2 -2
package/src/check.ts CHANGED
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
1
+ // Copyright (c) 2024-2026 Argento Computing Inc. Licensed under the MIT License. See LICENSE.
2
2
 
3
3
  /**
4
4
  * `check`: what a bundle establishes about the recordings it holds, said
@@ -70,6 +70,43 @@ import type { ThreeValued } from "./types.js";
70
70
  import { kleeneAll } from "./logic.js";
71
71
  import { PLAYER_VERSION } from "./verdict.js";
72
72
 
73
+ // ---------------------------------------------------------------------------
74
+ // Base floors (enclave v10), read structurally
75
+ // ---------------------------------------------------------------------------
76
+
77
+ /**
78
+ * What an audit that reads Base floors adds (bitgraph-audit after 0.9.0:
79
+ * TemporalAnalysis.signedFloors and floorProblems, and source / chain /
80
+ * timeSource on a segment bound). Read structurally, so the Player builds and
81
+ * behaves exactly as before against an audit without them; every field is
82
+ * optional and absent means an Ethereum anchor bound.
83
+ * TODO(release): bump the bitgraph-audit dependency to the release that
84
+ * carries signedFloors, then these can be the audit's own exported types.
85
+ */
86
+ export interface SignedFloorRecord {
87
+ proofHash: string;
88
+ chain: "base";
89
+ blockNumber: number;
90
+ blockHash: string;
91
+ blockTimestamp: number;
92
+ header: "checked" | "not-carried";
93
+ headerPath?: string;
94
+ bound: "not-before" | "withheld";
95
+ withheldReason?: string;
96
+ }
97
+ export interface FloorProblem {
98
+ code: string;
99
+ proofHash?: string;
100
+ path?: string;
101
+ message: string;
102
+ }
103
+ type FloorAwareBound = SegmentBound & { source?: "signed-floor"; chain?: "base"; timeSource?: "header" | "signed" };
104
+
105
+ function floorView(audit: AuditResult): { floors: SignedFloorRecord[]; problems: FloorProblem[] } {
106
+ const t = audit.temporal as { signedFloors?: SignedFloorRecord[]; floorProblems?: FloorProblem[] };
107
+ return { floors: t.signedFloors ?? [], problems: t.floorProblems ?? [] };
108
+ }
109
+
73
110
  // ---------------------------------------------------------------------------
74
111
  // Declared knowledge: published enclave measurements
75
112
  // ---------------------------------------------------------------------------
@@ -114,7 +151,17 @@ export const KNOWN_ENCLAVE_MEASUREMENTS: ReadonlyArray<{ pcr0: string; label: st
114
151
  {
115
152
  pcr0: "eccfc1c78006f4b74f929c992785575c908a0f60eca08ff638cd6c0842f993f182ebb002457b8ef3e732a6a10805c72b",
116
153
  label: "enclave v8 (reproducible, authenticated anchors and the floor gate)",
117
- period: "2026-09-07 onward",
154
+ period: "2026-09-07 to 2026-09-30",
155
+ },
156
+ {
157
+ pcr0: "934feb8bb6f4f7e2d2f85d902a7d5edd0981f706d9d2385638988ac096a05ea0583c3d00eef2a7947865ec66efc1fcf8",
158
+ label: "enclave v9 (reproducible; the allocation returns the floor anchor, for bitgraph-fuse/2)",
159
+ period: "2026-09-30 to 2026-10-07",
160
+ },
161
+ {
162
+ pcr0: "5a947cc66095adcceefa9e5ece5d1416dfe08c3470df1bcaa5b2bc5267b0480e6cdc172fe077cd06b0afb07614307973",
163
+ label: "enclave v10 (reproducible; the floor is a Base block fixed at allocation, for bitgraph-fuse/3)",
164
+ period: "2026-10-08 onward",
118
165
  },
119
166
  ];
120
167
 
@@ -129,15 +176,24 @@ export interface CheckLine {
129
176
  detail: string;
130
177
  }
131
178
 
132
- /** A verified Ethereum bound on a recording, from a witness in the bundle. */
179
+ /**
180
+ * A verified bound on a recording: an Ethereum anchor's, from a witness in
181
+ * the bundle, or (enclave v10) the Base block a recording signs as its floor,
182
+ * which is a not-before only.
183
+ */
133
184
  export interface CheckBound {
185
+ /** Absent for Ethereum (an anchor); "base" for a Base floor (commit.slotFloor). */
186
+ chain?: "base";
134
187
  blockNumber?: string;
135
188
  blockHash: string;
136
- /** Unix seconds from the verified block header. */
189
+ /** Unix seconds: from the verified block header, or for a Base floor without one, the time the proof signs. */
137
190
  timestamp: number;
191
+ /** The anchor proof, or for a Base floor the recording that signs it. */
138
192
  anchorProofHash: string;
139
- evidence: "chain-link" | "counter-order";
193
+ evidence: "chain-link" | "counter-order" | "signed-floor";
140
194
  weaker: boolean;
195
+ /** Base floors only: "header" checked in the bundle, or "signed" (confirming the block needs a Base lookup). */
196
+ timeSource?: "header" | "signed";
141
197
  }
142
198
 
143
199
  export interface CheckBounds {
@@ -148,18 +204,26 @@ export interface CheckBounds {
148
204
  }
149
205
 
150
206
  /**
151
- * The wall-clock floor of a fused artifact: the last verified anchored
152
- * block preceding its SLOT counter in the same epoch chain. Counter-order
153
- * evidence by construction (no hash path reaches a slot record), reported
154
- * with the same "genuine block" assumption as every other bound.
207
+ * The wall-clock floor of a fused artifact. Ethereum floors (enclave v9 and
208
+ * before): the last verified anchored block preceding its SLOT counter in
209
+ * the same epoch chain, counter-order evidence by construction (no hash path
210
+ * reaches a slot record). Base floors (enclave v10): the block the recording
211
+ * itself signs as commit.slotFloor, which a bitgraph-fuse/3 commitment binds
212
+ * ("signed-floor"). Either is reported with the same "genuine block"
213
+ * assumption as every other bound.
155
214
  */
156
215
  export interface CheckFloor {
216
+ /** Absent for Ethereum; "base" for the recording's own Base floor. */
217
+ chain?: "base";
157
218
  blockNumber?: string;
158
219
  blockHash: string;
159
- /** Unix seconds from the verified block header. */
220
+ /** Unix seconds from the verified block header, or for a Base floor without one, the signed time. */
160
221
  timestamp: number;
222
+ /** The anchor proof, or for a Base floor the recording itself. */
161
223
  anchorProofHash: string;
162
- evidence: "counter-order";
224
+ evidence: "counter-order" | "signed-floor";
225
+ /** Base floors only: "header" checked in the bundle, or "signed" (confirming the block needs a Base lookup). */
226
+ timeSource?: "header" | "signed";
163
227
  }
164
228
 
165
229
  /** What the bundle establishes about a recording marked fused (profile bitgraph-fuse/1, working name). */
@@ -332,6 +396,7 @@ export function buildCheckReport(audit: AuditResult, options?: CheckOptions): Ch
332
396
  const attestationByHash = new Map<string, ProofAttestationRecord>(
333
397
  audit.attestations.records.map((r) => [r.proofHash, r])
334
398
  );
399
+ const floorByHash = new Map<string, SignedFloorRecord>(floorView(audit).floors.map((f) => [f.proofHash, f]));
335
400
  const segmentByHash = new Map<string, TemporalSegment>();
336
401
  for (const segment of audit.temporal.segments) {
337
402
  for (const hash of segment.memberProofHashes) segmentByHash.set(hash, segment);
@@ -368,7 +433,8 @@ export function buildCheckReport(audit: AuditResult, options?: CheckOptions): Ch
368
433
  artifactPathByProof.get(proof.proofHash),
369
434
  webCrypto,
370
435
  options?.fuseEvidence,
371
- anchorByHash
436
+ anchorByHash,
437
+ floorByHash.get(proof.proofHash)
372
438
  )
373
439
  );
374
440
  }
@@ -378,7 +444,7 @@ export function buildCheckReport(audit: AuditResult, options?: CheckOptions): Ch
378
444
  sortByPosition(anchors);
379
445
  const contradictions = collectContradictions(audit);
380
446
  const notes = collectNotes(audit, recordings, anchors, options?.fuseOriginHexes);
381
- const notChecked = collectNotChecked(anchors.length > 0);
447
+ const notChecked = collectNotChecked(anchors.length > 0, floorByHash.size > 0);
382
448
 
383
449
  const allLines: ThreeValued[] = [
384
450
  ...recordings.map((r) => r.result),
@@ -432,7 +498,8 @@ function buildRecording(
432
498
  filePath: string | undefined,
433
499
  webCrypto: boolean,
434
500
  fuseEvidence?: ReadonlyMap<string, FuseVerifyResult>,
435
- anchorByHash?: ReadonlyMap<string, AnchorRecord>
501
+ anchorByHash?: ReadonlyMap<string, AnchorRecord>,
502
+ signedFloor?: SignedFloorRecord
436
503
  ): CheckRecording {
437
504
  const lines: CheckLine[] = [];
438
505
  const v = proof.verification;
@@ -488,12 +555,13 @@ function buildRecording(
488
555
  // fused: the proof's signed attribution marks a fused artifact (profile
489
556
  // bitgraph-fuse/1). The commitment check runs over the fused bytes, or
490
557
  // over the original by reconstruction; its floor is the last verified
491
- // anchor preceding the SLOT, not the commit.
558
+ // anchor preceding the SLOT, not the commit, or for a recording that signs
559
+ // a Base floor (enclave v10, bitgraph-fuse/3) that block itself.
492
560
  const marker = readFuseAttribution(proof.proof);
493
561
  let fused: CheckFused | undefined;
494
562
  if (marker !== null) {
495
- const floor = fusedFloor(segment, anchorByHash, proof.slotCounter, proof.chainId);
496
- const built = fusedLine(marker.placement, marker.originDigest !== undefined ? proof.proof.attribution?.message ?? null : null, fuseEvidence?.get(proof.proofHash), floor, proof);
563
+ const floor = signedFloor !== undefined ? ownBaseFloor(signedFloor) : fusedFloor(segment, anchorByHash, proof.slotCounter, proof.chainId);
564
+ const built = fusedLine(marker.placement, marker.originDigest !== undefined ? proof.proof.attribution?.message ?? null : null, fuseEvidence?.get(proof.proofHash), floor, proof, signedFloor);
497
565
  lines.push(built.line);
498
566
  fused = built.fused;
499
567
  }
@@ -511,7 +579,7 @@ function buildRecording(
511
579
  ...(filePath !== undefined && v?.tier === "full" ? { filePath } : {}),
512
580
  lines,
513
581
  result,
514
- bounds: boundsFor(segment),
582
+ bounds: boundsFor(segment, signedFloor),
515
583
  ...(fused !== undefined ? { fused } : {}),
516
584
  };
517
585
  }
@@ -556,11 +624,29 @@ export function fusedFloor(
556
624
  };
557
625
  }
558
626
 
627
+ /**
628
+ * The fused floor of a recording that signs a Base floor (enclave v10): the
629
+ * block itself, which a bitgraph-fuse/3 commitment binds. Null when the audit
630
+ * withheld it as a bound (the reason is in the floor detail).
631
+ */
632
+ export function ownBaseFloor(floor: SignedFloorRecord): CheckFloor | null {
633
+ if (floor.bound !== "not-before") return null;
634
+ return {
635
+ chain: "base",
636
+ blockNumber: String(floor.blockNumber),
637
+ blockHash: floor.blockHash,
638
+ timestamp: floor.blockTimestamp,
639
+ anchorProofHash: floor.proofHash,
640
+ evidence: "signed-floor",
641
+ timeSource: floor.header === "checked" ? "header" : "signed",
642
+ };
643
+ }
644
+
559
645
  const FLOOR_UNDETERMINED = "floor undetermined: no anchor precedes this slot in its epoch";
560
646
 
561
647
  function floorClause(floor: CheckFloor): string {
562
648
  const block = floor.blockNumber !== undefined ? `#${floor.blockNumber}` : floor.blockHash;
563
- return `anchored block ${block} (${new Date(floor.timestamp * 1000).toISOString()})`;
649
+ return `${floor.chain === "base" ? "Base" : "anchored"} block ${block} (${new Date(floor.timestamp * 1000).toISOString()})`;
564
650
  }
565
651
 
566
652
  function fusedLine(
@@ -568,13 +654,19 @@ function fusedLine(
568
654
  originMessage: string | null,
569
655
  ev: FuseVerifyResult | undefined,
570
656
  floor: CheckFloor | null,
571
- proof: ObservedProof
657
+ proof: ObservedProof,
658
+ signedFloor?: SignedFloorRecord
572
659
  ): { line: CheckLine; fused: CheckFused } {
573
660
  const slot = proof.slotCounter ?? "?";
574
661
  const where = placement !== null ? ` (placement ${placement})` : " (placement undeclared)";
575
- const floorDetail = floor !== null
662
+ const floorDetail = floor !== null && floor.chain === "base"
663
+ ? `assembled after ${floorClause(floor)}, the Base block this recording signs as its floor` +
664
+ (floor.timeSource === "signed" ? "; no header for it is in this bundle, so confirming the block needs a Base lookup" : "")
665
+ : floor !== null
576
666
  ? `assembled after ${floorClause(floor)}, the last verified anchor preceding slot ${slot} in this epoch`
577
- : proof.chainId !== ANCHORED_CHAIN
667
+ : signedFloor !== undefined
668
+ ? `floor undetermined: the Base floor this recording signs is not used as a bound (${signedFloor.withheldReason ?? "withheld"})`
669
+ : proof.chainId !== ANCHORED_CHAIN
578
670
  ? `floor undetermined: the slot is on chain "${proof.chainId}", not the anchored chain`
579
671
  : FLOOR_UNDETERMINED;
580
672
  const span = ev?.span !== undefined && ev.span !== null
@@ -708,9 +800,13 @@ function enclaveLine(attestedPcr0: string | undefined, attestationResult: ThreeV
708
800
  };
709
801
  }
710
802
 
711
- function boundsFor(segment: TemporalSegment | undefined): CheckBounds {
803
+ function boundsFor(segment: TemporalSegment | undefined, signedFloor?: SignedFloorRecord): CheckBounds {
804
+ const withheld =
805
+ signedFloor !== undefined && signedFloor.bound === "withheld"
806
+ ? `; the Base floor it signs is not used as a bound (${signedFloor.withheldReason ?? "withheld"})`
807
+ : "";
712
808
  if (segment === undefined) {
713
- return { status: "unanchored", detail: "no verified Ethereum anchor bounds this recording in this bundle" };
809
+ return { status: "unanchored", detail: `no verified Ethereum anchor bounds this recording in this bundle${withheld}` };
714
810
  }
715
811
  const lower = tightest(segment.lowerBounds, "not-before");
716
812
  const upper = tightest(segment.upperBounds, "not-after");
@@ -726,8 +822,8 @@ function boundsFor(segment: TemporalSegment | undefined): CheckBounds {
726
822
  : "unanchored";
727
823
  const detail =
728
824
  status === "unanchored"
729
- ? "no verified Ethereum anchor bounds this recording in this bundle"
730
- : `recorded ${boundsPhrase(status, notBefore, notAfter)}`;
825
+ ? `no verified Ethereum anchor bounds this recording in this bundle${withheld}`
826
+ : `recorded ${boundsPhrase(status, notBefore, notAfter)}${withheld}`;
731
827
  return {
732
828
  status,
733
829
  ...(notBefore !== undefined ? { notBefore } : {}),
@@ -751,29 +847,43 @@ function boundsPhrase(
751
847
  switch (status) {
752
848
  case "lower-bounded-with-following-anchor":
753
849
  return (
754
- `after Ethereum block ${blockRef(notBefore as CheckBound)} (header verified in this bundle); precedes an anchor that consumed block ${blockRef(notAfter as CheckBound)} (header verified), an anchor-latency assumption rather than an upper bound on this recording` +
850
+ `${afterBlock(notBefore as CheckBound)}; precedes an anchor that consumed block ${blockRef(notAfter as CheckBound)} (header verified), an anchor-latency assumption rather than an upper bound on this recording` +
755
851
  weakerSuffix((notBefore as CheckBound).weaker || (notAfter as CheckBound).weaker)
756
852
  );
757
853
  case "lower-bounded":
758
- return `after Ethereum block ${blockRef(notBefore as CheckBound)} (header verified in this bundle); no verified upper bound here` + weakerSuffix((notBefore as CheckBound).weaker);
854
+ return `${afterBlock(notBefore as CheckBound)}; no verified upper bound here` + weakerSuffix((notBefore as CheckBound).weaker);
759
855
  case "upper-bounded":
760
- return `before Ethereum block ${blockRef(notAfter as CheckBound)} (header verified in this bundle); no verified lower bound here` + weakerSuffix((notAfter as CheckBound).weaker);
856
+ return `with no verified lower bound in this bundle; an anchor that consumed block ${blockRef(notAfter as CheckBound)} followed (header verified), which is not an upper bound` + weakerSuffix((notAfter as CheckBound).weaker);
761
857
  default:
762
858
  return "with no verified Ethereum bound in this bundle";
763
859
  }
764
860
  }
765
861
 
862
+ /**
863
+ * "after Ethereum block N (header verified in this bundle)", or for a Base
864
+ * floor (enclave v10) "after Base block N" with where its time comes from.
865
+ */
866
+ function afterBlock(b: CheckBound): string {
867
+ if (b.chain !== "base") return `after Ethereum block ${blockRef(b)} (header verified in this bundle)`;
868
+ return b.timeSource === "header"
869
+ ? `after Base block ${blockRef(b)} (its floor, header checked in this bundle)`
870
+ : `after Base block ${blockRef(b)} (its floor, at the time the proof signs; confirming the block needs a Base lookup)`;
871
+ }
872
+
766
873
  /**
767
874
  * The tightest bound of a kind: for not-before the LARGEST block number
768
875
  * (latest verified anchor known to precede), for not-after the SMALLEST.
876
+ * Block numbers compare only within one chain: across Ethereum and Base
877
+ * (enclave v10 floors) the block times compare instead.
769
878
  * Ties resolve by preferring chain-link evidence over counter-order.
770
879
  */
771
- function tightest(bounds: SegmentBound[], kind: "not-before" | "not-after"): SegmentBound | undefined {
880
+ function tightest(bounds: FloorAwareBound[], kind: "not-before" | "not-after"): FloorAwareBound | undefined {
772
881
  const ofKind = bounds.filter((b) => b.kind === kind);
773
882
  if (ofKind.length === 0) return undefined;
774
883
  const sorted = [...ofKind].sort((a, b) => {
775
- const an = a.blockNumber !== undefined ? BigInt(a.blockNumber) : BigInt(a.timestamp);
776
- const bn = b.blockNumber !== undefined ? BigInt(b.blockNumber) : BigInt(b.timestamp);
884
+ const sameChain = a.chain === b.chain;
885
+ const an = sameChain && a.blockNumber !== undefined ? BigInt(a.blockNumber) : BigInt(a.timestamp);
886
+ const bn = sameChain && b.blockNumber !== undefined ? BigInt(b.blockNumber) : BigInt(b.timestamp);
777
887
  if (an !== bn) return kind === "not-before" ? (an > bn ? -1 : 1) : an < bn ? -1 : 1;
778
888
  if (a.weaker !== b.weaker) return a.weaker ? 1 : -1;
779
889
  return 0;
@@ -781,14 +891,16 @@ function tightest(bounds: SegmentBound[], kind: "not-before" | "not-after"): Seg
781
891
  return sorted[0];
782
892
  }
783
893
 
784
- function toBound(b: SegmentBound): CheckBound {
894
+ function toBound(b: FloorAwareBound): CheckBound {
785
895
  return {
896
+ ...(b.chain === "base" ? { chain: "base" as const } : {}),
786
897
  ...(b.blockNumber !== undefined ? { blockNumber: b.blockNumber } : {}),
787
898
  blockHash: b.blockHash,
788
899
  timestamp: b.timestamp,
789
900
  anchorProofHash: b.anchorProofHash,
790
901
  evidence: b.evidence,
791
902
  weaker: b.weaker,
903
+ ...(b.timeSource !== undefined ? { timeSource: b.timeSource } : {}),
792
904
  };
793
905
  }
794
906
 
@@ -796,16 +908,20 @@ function toBound(b: SegmentBound): CheckBound {
796
908
  function shortBoundsPhrase(b: CheckBounds): string {
797
909
  switch (b.status) {
798
910
  case "lower-bounded-with-following-anchor":
799
- return `after Ethereum block ${blockRef(b.notBefore as CheckBound)}, then an anchor at block ${blockRef(b.notAfter as CheckBound)}`;
911
+ return `after ${chainWord(b.notBefore as CheckBound)} block ${blockRef(b.notBefore as CheckBound)}, then an anchor at block ${blockRef(b.notAfter as CheckBound)}`;
800
912
  case "lower-bounded":
801
- return `after Ethereum block ${blockRef(b.notBefore as CheckBound)}`;
913
+ return `after ${chainWord(b.notBefore as CheckBound)} block ${blockRef(b.notBefore as CheckBound)}`;
802
914
  case "upper-bounded":
803
- return `before Ethereum block ${blockRef(b.notAfter as CheckBound)}`;
915
+ return `with no lower bound in this bundle, then an anchor at block ${blockRef(b.notAfter as CheckBound)} (not an upper bound)`;
804
916
  default:
805
917
  return "with no Ethereum bound in this bundle";
806
918
  }
807
919
  }
808
920
 
921
+ function chainWord(b: CheckBound): string {
922
+ return b.chain === "base" ? "Base" : "Ethereum";
923
+ }
924
+
809
925
  function blockRef(b: CheckBound): string {
810
926
  return b.blockNumber !== undefined ? b.blockNumber : b.blockHash;
811
927
  }
@@ -910,6 +1026,12 @@ function collectContradictions(audit: AuditResult): CheckLine[] {
910
1026
  push(`${finding.code}: ${finding.message}`);
911
1027
  }
912
1028
  }
1029
+ // A floor problem (enclave v10 floors): a recording that signs two floors,
1030
+ // a malformed or off-schedule Base floor, a floor header that contradicts
1031
+ // the signed floor or matches none. Evidence that contradicts itself.
1032
+ for (const problem of floorView(audit).problems) {
1033
+ push(`${problem.code}: ${problem.message}${problem.path !== undefined ? ` (${problem.path})` : ""}`);
1034
+ }
913
1035
  // An embedded proofHash that does not match the recomputed one: the
914
1036
  // stored proof file was altered after the ledger wrote it, or is not
915
1037
  // what it claims.
@@ -937,7 +1059,7 @@ function collectNotes(audit: AuditResult, recordings: CheckRecording[], anchors:
937
1059
  }
938
1060
  if (missingPositions > 0n) {
939
1061
  notes.push(
940
- `${missingPositions} causal position${missingPositions === 1n ? "" : "s"} between the earliest and latest recording here ${missingPositions === 1n ? "is" : "are"} not in this bundle: normal for an export, which is an excerpt of the chain; a full-epoch audit checks them`
1062
+ `${missingPositions} causal position${missingPositions === 1n ? "" : "s"} between the earliest and latest recording here ${missingPositions === 1n ? "is" : "are"} not in this bundle: normal for an export, which is an excerpt of the chain; the positions between belong to whoever holds those recordings`
941
1063
  );
942
1064
  }
943
1065
  if (predecessorsAbsent > 0) {
@@ -951,7 +1073,7 @@ function collectNotes(audit: AuditResult, recordings: CheckRecording[], anchors:
951
1073
  const originals = audit.ingest.artifacts.filter((a) => a.matchedProofHashes.length === 0 && fuseOriginHexes?.has(a.sha256Hex) === true);
952
1074
  if (originals.length > 0) {
953
1075
  notes.push(
954
- `${originals.length} file${originals.length === 1 ? "" : "s"} in this bundle ${originals.length === 1 ? "is" : "are"} the original of a fused recording: ${originals.slice(0, 5).map((a) => a.paths[0] ?? "(unnamed)").join(", ")}${originals.length > 5 ? ", …" : ""}. The original itself receives only the ceiling (it existed no later than the commit); the fused bytes receive the interval`
1076
+ `${originals.length} file${originals.length === 1 ? "" : "s"} in this bundle ${originals.length === 1 ? "is" : "are"} the original of a fused recording: ${originals.slice(0, 5).map((a) => a.paths[0] ?? "(unnamed)").join(", ")}${originals.length > 5 ? ", …" : ""}. The original carries no floor of its own: the commit that consumed its digest shows it existed by that position, an order, not a time ceiling. The fused bytes carry the floor`
955
1077
  );
956
1078
  }
957
1079
  const unmatched = audit.ingest.artifacts.filter((a) => a.matchedProofHashes.length === 0 && fuseOriginHexes?.has(a.sha256Hex) !== true);
@@ -995,15 +1117,20 @@ function collectNotes(audit: AuditResult, recordings: CheckRecording[], anchors:
995
1117
  return notes;
996
1118
  }
997
1119
 
998
- function collectNotChecked(hasAnchors: boolean): string[] {
1120
+ function collectNotChecked(hasAnchors: boolean, hasBaseFloors = false): string[] {
999
1121
  const out: string[] = [];
1000
1122
  if (hasAnchors) {
1001
1123
  out.push(
1002
1124
  "whether the anchored Ethereum blocks are canonical: their headers are recomputed here, but canonicality needs an Ethereum node or a block explorer"
1003
1125
  );
1004
1126
  }
1127
+ if (hasBaseFloors) {
1128
+ out.push(
1129
+ "whether the Base floor blocks are Base's own: a header in the bundle is checked here against the signed block, but that the block is on Base's chain needs a Base node or a block explorer"
1130
+ );
1131
+ }
1005
1132
  out.push(
1006
- "whether the public ledger holds these exact recordings at these positions: this is an offline check; drop the file on bitgraph.ing to compare against the ledger"
1133
+ "whether the anchors in this bundle are the ones BitGraph published: this is an offline check; the published anchors are listed at bitgraph.ing/ledger"
1007
1134
  );
1008
1135
  out.push("whether an epoch key was later quarantined: that is published outside any bundle");
1009
1136
  return out;
package/src/cli.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
2
+ // Copyright (c) 2024-2026 Argento Computing Inc. Licensed under the MIT License. See LICENSE.
3
3
 
4
4
  /**
5
5
  * bitgraph-play <rule.json> <bundle> [--out <file>] [--summary]
package/src/evaluate.ts CHANGED
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
1
+ // Copyright (c) 2024-2026 Argento Computing Inc. Licensed under the MIT License. See LICENSE.
2
2
 
3
3
  /**
4
4
  * The evaluator: walks the claim tree over resolved recordings and the
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
1
+ // Copyright (c) 2024-2026 Argento Computing Inc. Licensed under the MIT License. See LICENSE.
2
2
 
3
3
  /**
4
4
  * @mikeargento/bitgraph-player
package/src/init.ts CHANGED
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
1
+ // Copyright (c) 2024-2026 Argento Computing Inc. Licensed under the MIT License. See LICENSE.
2
2
 
3
3
  /**
4
4
  * Rule scaffolding: from a list of (filename, digest) pairs to a rule
package/src/logic.ts CHANGED
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
1
+ // Copyright (c) 2024-2026 Argento Computing Inc. Licensed under the MIT License. See LICENSE.
2
2
 
3
3
  /**
4
4
  * Strong Kleene three-valued connectives.
package/src/order.ts CHANGED
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
1
+ // Copyright (c) 2024-2026 Argento Computing Inc. Licensed under the MIT License. See LICENSE.
2
2
 
3
3
  /**
4
4
  * The ordering adapter. Player does not reimplement causal reconstruction —
@@ -31,7 +31,10 @@
31
31
  * - "anchor-bounds": strict block separation between verified segment
32
32
  * bounds; always assumption-dependent (anchor freshness). If BOTH
33
33
  * directions satisfy strict separation the evidence is contradictory
34
- * and answers nothing.
34
+ * and answers nothing. Only ANCHOR bounds take part: a Base floor
35
+ * (enclave v10, a segment bound with source "signed-floor") is a
36
+ * not-before for its own recording and orders nothing across epochs,
37
+ * and its block numbers are Base's, never comparable to Ethereum's.
35
38
  * Epoch pairs supported by neither are concurrent-or-unordered, and the
36
39
  * honest answer is "unordered", never a coin flip.
37
40
  */
@@ -215,13 +218,21 @@ function boundsStrictlyOrdered(upper: SegmentBound, lower: SegmentBound): boolea
215
218
  return upper.timestamp < lower.timestamp;
216
219
  }
217
220
 
221
+ /** A Base floor bound (enclave v10; audit field source "signed-floor"), read structurally. */
222
+ function isFloorBound(b: SegmentBound): boolean {
223
+ return (b as { source?: string }).source !== undefined;
224
+ }
225
+
218
226
  function anchorPrecedes(
219
227
  firstSegment: TemporalSegment,
220
228
  secondSegment: TemporalSegment
221
229
  ): { weaker: boolean; upper: SegmentBound; lower: SegmentBound } | undefined {
222
230
  let best: { weaker: boolean; upper: SegmentBound; lower: SegmentBound } | undefined;
223
231
  for (const upper of firstSegment.upperBounds) {
232
+ if (isFloorBound(upper)) continue;
224
233
  for (const lower of secondSegment.lowerBounds) {
234
+ // Anchor bounds only: a Base floor orders no epoch.
235
+ if (isFloorBound(lower)) continue;
225
236
  if (!boundsStrictlyOrdered(upper, lower)) continue;
226
237
  const weaker = upper.weaker || lower.weaker;
227
238
  if (best === undefined || (best.weaker && !weaker)) {
package/src/play.ts CHANGED
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
1
+ // Copyright (c) 2024-2026 Argento Computing Inc. Licensed under the MIT License. See LICENSE.
2
2
 
3
3
  /**
4
4
  * One-call evaluation: play(rulePath, bundlePath) -> verdict.
package/src/rule.ts CHANGED
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
1
+ // Copyright (c) 2024-2026 Argento Computing Inc. Licensed under the MIT License. See LICENSE.
2
2
 
3
3
  /**
4
4
  * Rule parsing and validation for bitgraph-player/1.
package/src/sig.ts CHANGED
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
1
+ // Copyright (c) 2024-2026 Argento Computing Inc. Licensed under the MIT License. See LICENSE.
2
2
 
3
3
  /**
4
4
  * bitgraph-sig/1: detached signature evidence over a digest.
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
1
+ // Copyright (c) 2024-2026 Argento Computing Inc. Licensed under the MIT License. See LICENSE.
2
2
 
3
3
  /**
4
4
  * @mikeargento/bitgraph-player
package/src/verdict.ts CHANGED
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
1
+ // Copyright (c) 2024-2026 Argento Computing Inc. Licensed under the MIT License. See LICENSE.
2
2
 
3
3
  /**
4
4
  * Verdict construction and serialization.
@@ -31,7 +31,7 @@ import type { DeclaredEntry, Resolution, Rule, Verdict } from "./types.js";
31
31
  * A unit test asserts this equals package.json's version, so the
32
32
  * constant cannot drift silently across releases.
33
33
  */
34
- export const PLAYER_VERSION = "0.14.0";
34
+ export const PLAYER_VERSION = "0.16.0";
35
35
 
36
36
  /** The player package's own version. */
37
37
  export function playerVersion(): string {