@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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +98 -0
  3. package/SPEC.md +234 -0
  4. package/dist/__tests__/cast-order.test.d.ts +2 -0
  5. package/dist/__tests__/cast-order.test.d.ts.map +1 -0
  6. package/dist/__tests__/cast-order.test.js +434 -0
  7. package/dist/__tests__/cast-order.test.js.map +1 -0
  8. package/dist/__tests__/evaluate-verdict.test.d.ts +2 -0
  9. package/dist/__tests__/evaluate-verdict.test.d.ts.map +1 -0
  10. package/dist/__tests__/evaluate-verdict.test.js +228 -0
  11. package/dist/__tests__/evaluate-verdict.test.js.map +1 -0
  12. package/dist/__tests__/fixtures.d.ts +63 -0
  13. package/dist/__tests__/fixtures.d.ts.map +1 -0
  14. package/dist/__tests__/fixtures.js +150 -0
  15. package/dist/__tests__/fixtures.js.map +1 -0
  16. package/dist/__tests__/logic-rule.test.d.ts +2 -0
  17. package/dist/__tests__/logic-rule.test.d.ts.map +1 -0
  18. package/dist/__tests__/logic-rule.test.js +190 -0
  19. package/dist/__tests__/logic-rule.test.js.map +1 -0
  20. package/dist/cast.d.ts +29 -0
  21. package/dist/cast.d.ts.map +1 -0
  22. package/dist/cast.js +85 -0
  23. package/dist/cast.js.map +1 -0
  24. package/dist/cli.d.ts +3 -0
  25. package/dist/cli.d.ts.map +1 -0
  26. package/dist/cli.js +130 -0
  27. package/dist/cli.js.map +1 -0
  28. package/dist/evaluate.d.ts +30 -0
  29. package/dist/evaluate.d.ts.map +1 -0
  30. package/dist/evaluate.js +177 -0
  31. package/dist/evaluate.js.map +1 -0
  32. package/dist/index.d.ts +22 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +9 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/logic.d.ts +16 -0
  37. package/dist/logic.d.ts.map +1 -0
  38. package/dist/logic.js +29 -0
  39. package/dist/logic.js.map +1 -0
  40. package/dist/order.d.ts +50 -0
  41. package/dist/order.d.ts.map +1 -0
  42. package/dist/order.js +290 -0
  43. package/dist/order.js.map +1 -0
  44. package/dist/rule.d.ts +40 -0
  45. package/dist/rule.d.ts.map +1 -0
  46. package/dist/rule.js +324 -0
  47. package/dist/rule.js.map +1 -0
  48. package/dist/types.d.ts +205 -0
  49. package/dist/types.d.ts.map +1 -0
  50. package/dist/types.js +16 -0
  51. package/dist/types.js.map +1 -0
  52. package/dist/verdict.d.ts +14 -0
  53. package/dist/verdict.d.ts.map +1 -0
  54. package/dist/verdict.js +146 -0
  55. package/dist/verdict.js.map +1 -0
  56. package/package.json +42 -0
  57. package/src/__tests__/cast-order.test.ts +473 -0
  58. package/src/__tests__/evaluate-verdict.test.ts +266 -0
  59. package/src/__tests__/fixtures.ts +206 -0
  60. package/src/__tests__/logic-rule.test.ts +221 -0
  61. package/src/cast.ts +120 -0
  62. package/src/cli.ts +143 -0
  63. package/src/evaluate.ts +238 -0
  64. package/src/index.ts +38 -0
  65. package/src/logic.ts +39 -0
  66. package/src/order.ts +380 -0
  67. package/src/rule.ts +340 -0
  68. package/src/types.ts +224 -0
  69. package/src/verdict.ts +159 -0
package/src/rule.ts ADDED
@@ -0,0 +1,340 @@
1
+ // Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
2
+
3
+ /**
4
+ * Rule parsing and validation for bitgraph-player/1.
5
+ *
6
+ * Strict by design: unknown fields are rejected at every level, because a
7
+ * silently ignored field in a rule is a claim the author believes is being
8
+ * enforced and is not. `requires.ordering` is mandatory — a rule that does
9
+ * not declare its trust floor does not parse.
10
+ *
11
+ * One deliberate looseness: a claim may reference a role the cast does not
12
+ * declare. That is NOT a parse error; it evaluates to UNDETERMINED. The
13
+ * rule file stays parseable so the verdict can say exactly which reference
14
+ * was undeclared.
15
+ */
16
+
17
+ import type { CastEntry, CastPin, Claim, EvidenceTier, Rule } from "./types.js";
18
+
19
+ export class RuleError extends Error {
20
+ readonly issues: readonly string[];
21
+ constructor(issues: readonly string[]) {
22
+ super(`invalid rule: ${issues.join("; ")}`);
23
+ this.name = "RuleError";
24
+ this.issues = issues;
25
+ }
26
+ }
27
+
28
+ const HEX64 = /^[0-9a-fA-F]{64}$/;
29
+ const B64_STD = /^[A-Za-z0-9+/]{43}=$/;
30
+ const B64_URL = /^[A-Za-z0-9_-]{43}=?$/;
31
+
32
+ /**
33
+ * Normalize any accepted digest spelling to the standard-base64 form used
34
+ * by bitgraph/1 proofs (artifact.digestB64). Returns undefined when the
35
+ * input is not a CANONICAL spelling of a 32-byte SHA-256: base64 forms
36
+ * must round-trip byte-exactly, so spellings with nonzero trailing
37
+ * padding bits (which Node's lenient decoder silently reinterprets) are
38
+ * rejected rather than collapsed into different bytes than written.
39
+ */
40
+ export function normalizeDigest(input: string): string | undefined {
41
+ let s = input.trim();
42
+ if (s.toLowerCase().startsWith("sha256:")) s = s.slice("sha256:".length);
43
+ if (HEX64.test(s)) {
44
+ return Buffer.from(s, "hex").toString("base64");
45
+ }
46
+ if (B64_STD.test(s)) {
47
+ const bytes = Buffer.from(s, "base64");
48
+ if (bytes.length === 32 && bytes.toString("base64") === s) return s;
49
+ return undefined;
50
+ }
51
+ if (B64_URL.test(s)) {
52
+ const bytes = Buffer.from(s, "base64url");
53
+ if (bytes.length === 32 && bytes.toString("base64url") === s.replace(/=+$/, "")) {
54
+ return bytes.toString("base64");
55
+ }
56
+ return undefined;
57
+ }
58
+ return undefined;
59
+ }
60
+
61
+ /**
62
+ * Tolerant decode of a digest string observed in a bundle proof, for
63
+ * byte-level matching only (a hostile file may spell its digest any way
64
+ * it likes; matching by bytes keeps "only broken recordings exist" from
65
+ * masquerading as "no recordings exist").
66
+ */
67
+ export function decodeDigestBytes(input: string): Buffer | undefined {
68
+ const s = input.trim();
69
+ if (HEX64.test(s)) return Buffer.from(s, "hex");
70
+ if (/^[A-Za-z0-9+/]+=*$/.test(s)) {
71
+ const bytes = Buffer.from(s, "base64");
72
+ return bytes.length === 32 ? bytes : undefined;
73
+ }
74
+ if (/^[A-Za-z0-9_-]+=*$/.test(s)) {
75
+ const bytes = Buffer.from(s, "base64url");
76
+ return bytes.length === 32 ? bytes : undefined;
77
+ }
78
+ return undefined;
79
+ }
80
+
81
+ function isPlainObject(v: unknown): v is Record<string, unknown> {
82
+ return typeof v === "object" && v !== null && !Array.isArray(v);
83
+ }
84
+
85
+ function rejectUnknownKeys(
86
+ obj: Record<string, unknown>,
87
+ allowed: readonly string[],
88
+ where: string,
89
+ issues: string[]
90
+ ): void {
91
+ for (const key of Object.keys(obj)) {
92
+ if (!allowed.includes(key)) issues.push(`${where}: unknown field "${key}"`);
93
+ }
94
+ }
95
+
96
+ function parsePin(v: unknown, where: string, issues: string[]): CastPin | undefined {
97
+ if (!isPlainObject(v)) {
98
+ issues.push(`${where}: "at" must be an object`);
99
+ return undefined;
100
+ }
101
+ const hasProofHash = "proofHash" in v;
102
+ const hasPosition = "epochId" in v || "counter" in v;
103
+ if (hasProofHash && hasPosition) {
104
+ issues.push(`${where}: "at" must be {proofHash} or {epochId, counter}, not both`);
105
+ return undefined;
106
+ }
107
+ if (hasProofHash) {
108
+ rejectUnknownKeys(v, ["proofHash"], where, issues);
109
+ if (typeof v["proofHash"] !== "string" || v["proofHash"].length === 0) {
110
+ issues.push(`${where}: "at.proofHash" must be a non-empty string`);
111
+ return undefined;
112
+ }
113
+ return { proofHash: v["proofHash"] };
114
+ }
115
+ if (hasPosition) {
116
+ rejectUnknownKeys(v, ["epochId", "counter"], where, issues);
117
+ const epochId = v["epochId"];
118
+ const counter = v["counter"];
119
+ if (typeof epochId !== "string" || epochId.length === 0) {
120
+ issues.push(`${where}: "at.epochId" must be a non-empty string`);
121
+ return undefined;
122
+ }
123
+ if (typeof counter !== "string" || !/^\d+$/.test(counter)) {
124
+ issues.push(`${where}: "at.counter" must be a decimal string`);
125
+ return undefined;
126
+ }
127
+ return { epochId, counter };
128
+ }
129
+ issues.push(`${where}: "at" must be {proofHash} or {epochId, counter}`);
130
+ return undefined;
131
+ }
132
+
133
+ function parseCastEntry(role: string, v: unknown, issues: string[]): CastEntry | undefined {
134
+ const where = `cast.${role}`;
135
+ if (!isPlainObject(v)) {
136
+ issues.push(`${where}: must be an object`);
137
+ return undefined;
138
+ }
139
+ rejectUnknownKeys(v, ["digest", "means", "at", "signedBy", "optional"], where, issues);
140
+ const digest = v["digest"];
141
+ if (typeof digest !== "string") {
142
+ issues.push(`${where}: "digest" is required and must be a string`);
143
+ return undefined;
144
+ }
145
+ if (normalizeDigest(digest) === undefined) {
146
+ issues.push(`${where}: "digest" is not a well-formed 32-byte SHA-256 in any accepted spelling`);
147
+ return undefined;
148
+ }
149
+ const entry: CastEntry = { digest };
150
+ if ("means" in v) {
151
+ if (typeof v["means"] !== "string") {
152
+ issues.push(`${where}: "means" must be a string`);
153
+ } else {
154
+ entry.means = v["means"];
155
+ }
156
+ }
157
+ if ("at" in v) {
158
+ const pin = parsePin(v["at"], where, issues);
159
+ if (pin !== undefined) entry.at = pin;
160
+ }
161
+ if ("signedBy" in v) {
162
+ entry.signedBy = v["signedBy"];
163
+ }
164
+ if ("optional" in v) {
165
+ if (typeof v["optional"] !== "boolean") {
166
+ issues.push(`${where}: "optional" must be a boolean`);
167
+ } else {
168
+ entry.optional = v["optional"];
169
+ }
170
+ }
171
+ return entry;
172
+ }
173
+
174
+ function parseClaim(v: unknown, where: string, issues: string[], depth: number): Claim | undefined {
175
+ if (depth > 32) {
176
+ issues.push(`${where}: claim nesting exceeds the maximum depth of 32`);
177
+ return undefined;
178
+ }
179
+ if (!isPlainObject(v)) {
180
+ issues.push(`${where}: must be an object`);
181
+ return undefined;
182
+ }
183
+ const keys = Object.keys(v);
184
+ if (keys.length !== 1) {
185
+ issues.push(`${where}: a claim must have exactly one operator, got [${keys.join(", ")}]`);
186
+ return undefined;
187
+ }
188
+ const op = keys[0];
189
+ const body = v[op as keyof typeof v];
190
+ switch (op) {
191
+ case "exists": {
192
+ if (typeof body !== "string" || body.length === 0) {
193
+ issues.push(`${where}.exists: must be a role name`);
194
+ return undefined;
195
+ }
196
+ return { exists: body };
197
+ }
198
+ case "before":
199
+ case "after": {
200
+ if (!Array.isArray(body) || body.length !== 2 || body.some((r) => typeof r !== "string")) {
201
+ issues.push(`${where}.${op}: must be a two-element array of role names`);
202
+ return undefined;
203
+ }
204
+ const pair = body as [string, string];
205
+ return op === "before" ? { before: pair } : { after: pair };
206
+ }
207
+ case "between": {
208
+ if (!Array.isArray(body) || body.length !== 3 || body.some((r) => typeof r !== "string")) {
209
+ issues.push(`${where}.between: must be [subject, lowerRole, upperRole]`);
210
+ return undefined;
211
+ }
212
+ return { between: body as [string, string, string] };
213
+ }
214
+ case "all":
215
+ case "any": {
216
+ if (!Array.isArray(body) || body.length === 0) {
217
+ issues.push(`${where}.${op}: must be a non-empty array of claims`);
218
+ return undefined;
219
+ }
220
+ const parsed: Claim[] = [];
221
+ let ok = true;
222
+ body.forEach((child, i) => {
223
+ const c = parseClaim(child, `${where}.${op}[${i}]`, issues, depth + 1);
224
+ if (c === undefined) ok = false;
225
+ else parsed.push(c);
226
+ });
227
+ if (!ok) return undefined;
228
+ return op === "all" ? { all: parsed } : { any: parsed };
229
+ }
230
+ case "not": {
231
+ const inner = parseClaim(body, `${where}.not`, issues, depth + 1);
232
+ if (inner === undefined) return undefined;
233
+ return { not: inner };
234
+ }
235
+ default:
236
+ issues.push(`${where}: unknown operator "${op}"`);
237
+ return undefined;
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Parse and validate a rule from raw JSON text. Throws RuleError with every
243
+ * issue found, not just the first.
244
+ */
245
+ export function parseRule(jsonText: string): Rule {
246
+ let raw: unknown;
247
+ try {
248
+ raw = JSON.parse(jsonText);
249
+ } catch (err) {
250
+ throw new RuleError([`not valid JSON: ${(err as Error).message}`]);
251
+ }
252
+ const issues: string[] = [];
253
+ if (!isPlainObject(raw)) throw new RuleError(["rule file must be a JSON object"]);
254
+
255
+ rejectUnknownKeys(raw, ["rule", "id", "cast", "world", "requires", "claim", "then"], "rule", issues);
256
+
257
+ if (raw["rule"] !== "bitgraph-player/1") {
258
+ issues.push(`"rule" must be exactly "bitgraph-player/1"`);
259
+ }
260
+ if (typeof raw["id"] !== "string" || raw["id"].length === 0) {
261
+ issues.push(`"id" is required and must be a non-empty string`);
262
+ }
263
+ if (raw["world"] !== "closed") {
264
+ issues.push(`"world" is required and must be exactly "closed" (the only defined value)`);
265
+ }
266
+
267
+ // The trust floor is mandatory. No default: a rule that does not state
268
+ // what evidence it accepts does not parse.
269
+ let ordering: EvidenceTier | undefined;
270
+ if (!isPlainObject(raw["requires"])) {
271
+ issues.push(`"requires" is required: a rule must declare its trust floor via requires.ordering`);
272
+ } else {
273
+ rejectUnknownKeys(raw["requires"], ["ordering"], "requires", issues);
274
+ const o = raw["requires"]["ordering"];
275
+ if (o === "hash-linked" || o === "assumption-dependent") {
276
+ ordering = o;
277
+ } else {
278
+ issues.push(`requires.ordering must be "hash-linked" or "assumption-dependent"`);
279
+ }
280
+ }
281
+
282
+ // Null prototype: a role named "__proto__" must become an ordinary own
283
+ // key, not a prototype assignment that silently drops the role.
284
+ const cast: Record<string, CastEntry> = Object.create(null) as Record<string, CastEntry>;
285
+ if (!isPlainObject(raw["cast"]) || Object.keys(raw["cast"]).length === 0) {
286
+ issues.push(`"cast" is required and must declare at least one role`);
287
+ } else {
288
+ for (const [role, entry] of Object.entries(raw["cast"])) {
289
+ if (!/^[A-Za-z0-9_.-]+$/.test(role)) {
290
+ issues.push(`cast: role name "${role}" must match [A-Za-z0-9_.-]+`);
291
+ continue;
292
+ }
293
+ // Pure-integer names are re-ordered ahead of string keys by JSON
294
+ // object semantics in some languages, breaking the declaration-order
295
+ // guarantee a verdict depends on. Forbidden by the grammar.
296
+ if (/^[0-9]+$/.test(role)) {
297
+ issues.push(`cast: role name "${role}" must contain at least one non-digit character`);
298
+ continue;
299
+ }
300
+ const parsed = parseCastEntry(role, entry, issues);
301
+ if (parsed !== undefined) cast[role] = parsed;
302
+ }
303
+ }
304
+
305
+ let claim: Claim | undefined;
306
+ if (!("claim" in raw)) {
307
+ issues.push(`"claim" is required`);
308
+ } else {
309
+ claim = parseClaim(raw["claim"], "claim", issues, 0);
310
+ }
311
+
312
+ let then: { label: string } | undefined;
313
+ if ("then" in raw) {
314
+ if (!isPlainObject(raw["then"])) {
315
+ issues.push(`"then" must be an object`);
316
+ } else {
317
+ // A label and nothing else. The first request for a field here that
318
+ // can cause an action is a request to make Player an enforcer.
319
+ rejectUnknownKeys(raw["then"], ["label"], "then", issues);
320
+ if (typeof raw["then"]["label"] !== "string" || raw["then"]["label"].length === 0) {
321
+ issues.push(`then.label must be a non-empty string`);
322
+ } else {
323
+ then = { label: raw["then"]["label"] };
324
+ }
325
+ }
326
+ }
327
+
328
+ if (issues.length > 0) throw new RuleError(issues);
329
+
330
+ const rule: Rule = {
331
+ rule: "bitgraph-player/1",
332
+ id: raw["id"] as string,
333
+ cast,
334
+ world: "closed",
335
+ requires: { ordering: ordering as EvidenceTier },
336
+ claim: claim as Claim,
337
+ };
338
+ if (then !== undefined) rule.then = then;
339
+ return rule;
340
+ }
package/src/types.ts ADDED
@@ -0,0 +1,224 @@
1
+ // Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
2
+
3
+ /**
4
+ * @mikeargento/bitgraph-player
5
+ *
6
+ * Shared vocabulary. The normative semantics live in SPEC.md; the comments
7
+ * here restate them only where a reader of the types needs the contract.
8
+ *
9
+ * Player is a pure function over an AuditResult:
10
+ *
11
+ * evaluate(rule, runAudit(bundle)) -> verdict
12
+ *
13
+ * Three-valued by design. TRUE and FALSE are claims BitGraph evidence
14
+ * supports; UNDETERMINED is the honest answer everywhere the evidence does
15
+ * not decide. A boolean evaluator would launder undecidable into FALSE.
16
+ */
17
+
18
+ import type { ObservedProof } from "@mikeargento/bitgraph-audit";
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Three-valued logic
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export type ThreeValued = "TRUE" | "FALSE" | "UNDETERMINED";
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Ordering evidence
28
+ // ---------------------------------------------------------------------------
29
+
30
+ /**
31
+ * Every ordering answer names what it rests on:
32
+ *
33
+ * "chain-link" same partition, both proofs in one verified prevB64
34
+ * component; commit-counter order within a hash-linked
35
+ * structure.
36
+ * "counter-order" same partition, different components; commit-counter
37
+ * values only, which relies on the authority's counter
38
+ * discipline.
39
+ * "epoch-lineage" different epochs related by hard epochLink succession
40
+ * (cryptographic hand-off; transitive pairs from audit).
41
+ * "anchor-bounds" different epochs related through Ethereum anchor
42
+ * bounds. The not-after side always rests on the
43
+ * anchor-freshness assumption.
44
+ */
45
+ export type OrderBasis = "chain-link" | "counter-order" | "epoch-lineage" | "anchor-bounds";
46
+
47
+ /**
48
+ * The two honest tiers. "hash-linked" rests on hash links alone;
49
+ * "assumption-dependent" additionally rests on counter discipline or
50
+ * anchor freshness. A rule declares the floor it will accept.
51
+ */
52
+ export type EvidenceTier = "hash-linked" | "assumption-dependent";
53
+
54
+ export function basisTier(basis: OrderBasis): EvidenceTier {
55
+ switch (basis) {
56
+ case "chain-link":
57
+ case "epoch-lineage":
58
+ return "hash-linked";
59
+ case "counter-order":
60
+ case "anchor-bounds":
61
+ return "assumption-dependent";
62
+ }
63
+ }
64
+
65
+ /** True when `tier` satisfies a declared floor. */
66
+ export function meetsFloor(tier: EvidenceTier, floor: EvidenceTier): boolean {
67
+ return floor === "assumption-dependent" || tier === "hash-linked";
68
+ }
69
+
70
+ /** Result of comparing two resolved recordings. */
71
+ export interface OrderResult {
72
+ /** "same" when both roles resolved to the identical recording. */
73
+ relation: "before" | "after" | "same" | "unordered";
74
+ /** Present exactly when relation is "before" or "after". */
75
+ basis?: OrderBasis;
76
+ /**
77
+ * Present exactly when relation is "before" or "after": the tier this
78
+ * specific answer rests on. Usually basisTier(basis), but epoch-lineage
79
+ * answers whose predecessor-side coverage rests on counter discipline
80
+ * carry "assumption-dependent" despite the hash-linked basis family.
81
+ * The evidence floor gates THIS field.
82
+ */
83
+ tier?: EvidenceTier;
84
+ /** True when the answer rests on an assumption (counter discipline or anchor freshness). */
85
+ assumptionDependent: boolean;
86
+ /** True when the evidence additionally rests on counter-order rather than a chain-link path. */
87
+ weaker: boolean;
88
+ /** Plain-language statement of exactly what was compared and why it answered this way. */
89
+ detail: string;
90
+ }
91
+
92
+ // ---------------------------------------------------------------------------
93
+ // Rule file (bitgraph-player/1)
94
+ // ---------------------------------------------------------------------------
95
+
96
+ /**
97
+ * Pin selecting one occurrence when the same bits hold several causal
98
+ * positions. Exactly one of the two forms.
99
+ */
100
+ export type CastPin =
101
+ | { proofHash: string }
102
+ | { epochId: string; counter: string };
103
+
104
+ /**
105
+ * One cast role. Everything here is DECLARED: taken on the rule author's
106
+ * word, surfaced in the verdict's `declared` block, never derived.
107
+ */
108
+ export interface CastEntry {
109
+ /**
110
+ * Artifact digest identifying the bits. Accepted forms: "sha256:<64 hex>",
111
+ * bare 64-char hex, standard base64, or base64url of the 32-byte digest.
112
+ * Normalized internally to the proof's base64 form.
113
+ */
114
+ digest: string;
115
+ /** What the digest means as a business object. Declared, echoed, never interpreted. */
116
+ means?: string;
117
+ /** Selects one recording when the digest holds several causal positions. */
118
+ at?: CastPin;
119
+ /**
120
+ * External identity evidence (e.g. a C2PA manifest reference). Player
121
+ * echoes it into `declared` verbatim with verifiedHere: false. SIGNED_BY
122
+ * is not a BitGraph primitive and is never derived here.
123
+ */
124
+ signedBy?: unknown;
125
+ /** When true, absence from the bundle is a definite fact under the closed world, not an error. */
126
+ optional?: boolean;
127
+ }
128
+
129
+ export type Claim =
130
+ | { exists: string }
131
+ | { before: [string, string] }
132
+ | { after: [string, string] }
133
+ | { between: [string, string, string] }
134
+ | { all: Claim[] }
135
+ | { any: Claim[] }
136
+ | { not: Claim };
137
+
138
+ export interface Rule {
139
+ rule: "bitgraph-player/1";
140
+ id: string;
141
+ cast: Record<string, CastEntry>;
142
+ /** The only defined value. Absence is only ever asserted within the declared cast. */
143
+ world: "closed";
144
+ /**
145
+ * Mandatory. A rule that does not state its trust floor does not parse:
146
+ * the rule carries its own security policy.
147
+ */
148
+ requires: { ordering: EvidenceTier };
149
+ claim: Claim;
150
+ /**
151
+ * A label and nothing else. No field in this format is capable of
152
+ * causing an action; Player decides, it does not enforce.
153
+ */
154
+ then?: { label: string };
155
+ }
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // Cast resolution
159
+ // ---------------------------------------------------------------------------
160
+
161
+ /**
162
+ * A digest identifies bits; a recording identifies an occurrence of those
163
+ * bits at a causal position. A cast role resolves to a RECORDING. Two or
164
+ * more matches with no pin is UNDETERMINED, never a silent pick.
165
+ */
166
+ export type Resolution =
167
+ | {
168
+ kind: "resolved";
169
+ role: string;
170
+ proof: ObservedProof;
171
+ /** Verification tier the resolution rests on ("full" | "integrity"). */
172
+ verificationTier: string;
173
+ /** How many verified recordings of this digest the bundle holds. */
174
+ matchCount: number;
175
+ }
176
+ | { kind: "absent"; role: string; optional: boolean }
177
+ | { kind: "ambiguous"; role: string; matchCount: number; candidates: string[] }
178
+ | { kind: "invalid"; role: string; reason: string };
179
+
180
+ // ---------------------------------------------------------------------------
181
+ // Verdict (bitgraph-player-verdict/1)
182
+ // ---------------------------------------------------------------------------
183
+
184
+ /** One evaluated sub-claim, in evaluation order. Everything BitGraph established. */
185
+ export interface DerivedStep {
186
+ claim: string;
187
+ result: ThreeValued;
188
+ basis?: OrderBasis;
189
+ evidenceTier?: EvidenceTier;
190
+ because: Record<string, unknown>;
191
+ }
192
+
193
+ /** One assertion Player took on somebody's word. */
194
+ export interface DeclaredEntry {
195
+ assertion: "signedBy" | "means" | "closed-world" | "pinned-occurrence";
196
+ role?: string;
197
+ verifiedHere: false;
198
+ [k: string]: unknown;
199
+ }
200
+
201
+ export interface Verdict {
202
+ verdict: "bitgraph-player-verdict/1";
203
+ result: ThreeValued;
204
+ rule: { id: string; sha256: string };
205
+ then?: { label: string };
206
+ /** Weakest evidence tier any contributing ordering answer rested on. Absent when no ordering was used. */
207
+ weakestEvidence?: EvidenceTier;
208
+ cast: Record<
209
+ string,
210
+ {
211
+ digestB64: string;
212
+ resolution: string;
213
+ proofHash?: string;
214
+ epochId?: string;
215
+ chainId?: string;
216
+ counter?: string;
217
+ slotCounter?: string;
218
+ }
219
+ >;
220
+ derived: DerivedStep[];
221
+ declared: DeclaredEntry[];
222
+ evaluator: { name: string; version: string };
223
+ network: "none";
224
+ }