@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/cast.ts ADDED
@@ -0,0 +1,120 @@
1
+ // Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
2
+
3
+ /**
4
+ * Cast resolution: digest -> recording.
5
+ *
6
+ * A digest identifies bits; a BitGraph recording identifies an OCCURRENCE
7
+ * of those bits at a causal position. The same bits can hold many
8
+ * positions by design (the ledger's by-digest index is one entry per
9
+ * causal position), so a role must resolve to exactly one recording:
10
+ *
11
+ * exactly 1 verified match -> resolved
12
+ * 0 matches, optional -> absent (a definite fact under the closed world)
13
+ * 0 matches, required -> absent (evaluates to UNDETERMINED)
14
+ * 2+ matches, no pin -> ambiguous (UNDETERMINED, never a silent pick)
15
+ * 2+ matches, pin selects exactly 1 -> resolved
16
+ *
17
+ * Only proofs whose canonical verification passed (status "verified", at
18
+ * either tier) count as recordings. A file shaped like a proof that fails
19
+ * verification is noise, not evidence — but if matches exist and ALL fail
20
+ * verification, that is surfaced as invalid rather than silently treated
21
+ * as absence, because "the bundle contains only broken recordings of this
22
+ * digest" and "the bundle contains no recordings of this digest" are
23
+ * different situations and only one of them supports a closed-world
24
+ * absence claim.
25
+ */
26
+
27
+ import type { AuditResult, ObservedProof } from "@mikeargento/bitgraph-audit";
28
+ import { decodeDigestBytes, normalizeDigest } from "./rule.js";
29
+ import type { CastEntry, Resolution } from "./types.js";
30
+
31
+ function matchesPin(proof: ObservedProof, pin: NonNullable<CastEntry["at"]>): boolean {
32
+ if ("proofHash" in pin) return proof.proofHash === pin.proofHash;
33
+ return proof.epochId === pin.epochId && proof.counter === pin.counter;
34
+ }
35
+
36
+ /**
37
+ * A recording counts when its canonical verification PASSED at either
38
+ * tier: "verified" is the full-tier pass (artifact bytes present and
39
+ * matching), "artifact-unavailable" is the integrity-tier pass (the proof
40
+ * verifies bytes-free; the digest is inside the signed body either way).
41
+ */
42
+ function verificationPassed(proof: ObservedProof): boolean {
43
+ const status = proof.verification?.status;
44
+ return status === "verified" || status === "artifact-unavailable";
45
+ }
46
+
47
+ export function resolveRole(role: string, entry: CastEntry, audit: AuditResult): Resolution {
48
+ const digestB64 = normalizeDigest(entry.digest);
49
+ if (digestB64 === undefined) {
50
+ // parseRule already rejects malformed digests; this guards direct API use.
51
+ return { kind: "invalid", role, reason: "digest is not a well-formed 32-byte SHA-256" };
52
+ }
53
+ const targetBytes = decodeDigestBytes(digestB64) as Buffer;
54
+
55
+ const anyMatch: ObservedProof[] = [];
56
+ const verified: ObservedProof[] = [];
57
+ for (const proof of audit.ingest.proofs) {
58
+ // Byte-level match: a hostile file may spell its digest any way it
59
+ // likes, and "only broken recordings exist" must not masquerade as
60
+ // "no recordings exist".
61
+ const proofDigest = decodeDigestBytes(proof.proof.artifact.digestB64);
62
+ if (proofDigest === undefined || !proofDigest.equals(targetBytes)) continue;
63
+ anyMatch.push(proof);
64
+ if (verificationPassed(proof)) verified.push(proof);
65
+ }
66
+
67
+ if (verified.length === 0) {
68
+ if (anyMatch.length > 0) {
69
+ return {
70
+ kind: "invalid",
71
+ role,
72
+ reason: `${anyMatch.length} recording(s) of this digest are present but none passed verification`,
73
+ };
74
+ }
75
+ return { kind: "absent", role, optional: entry.optional === true };
76
+ }
77
+
78
+ let candidates = verified;
79
+ if (entry.at !== undefined) {
80
+ const pin = entry.at;
81
+ candidates = verified.filter((p) => matchesPin(p, pin));
82
+ if (candidates.length === 0) {
83
+ return {
84
+ kind: "invalid",
85
+ role,
86
+ reason: `"at" pin matches none of the ${verified.length} verified recording(s) of this digest`,
87
+ };
88
+ }
89
+ }
90
+
91
+ if (candidates.length > 1) {
92
+ return {
93
+ kind: "ambiguous",
94
+ role,
95
+ matchCount: candidates.length,
96
+ candidates: candidates.map((p) => p.proofHash).sort(),
97
+ };
98
+ }
99
+
100
+ const proof = candidates[0] as ObservedProof;
101
+ return {
102
+ kind: "resolved",
103
+ role,
104
+ proof,
105
+ verificationTier: proof.verification?.tier ?? "integrity",
106
+ matchCount: verified.length,
107
+ };
108
+ }
109
+
110
+ /** Resolve every declared role. Deterministic: iterates the cast in declaration order. */
111
+ export function resolveCast(
112
+ cast: Record<string, CastEntry>,
113
+ audit: AuditResult
114
+ ): Map<string, Resolution> {
115
+ const out = new Map<string, Resolution>();
116
+ for (const [role, entry] of Object.entries(cast)) {
117
+ out.set(role, resolveRole(role, entry, audit));
118
+ }
119
+ return out;
120
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ // Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
3
+
4
+ /**
5
+ * bitgraph-play <rule.json> <bundle> [--out <file>] [--summary]
6
+ *
7
+ * Evaluates a bitgraph-player/1 rule against a proof bundle (directory,
8
+ * .tar, .tar.gz, or .tgz) and writes the verdict JSON to stdout (or
9
+ * --out). Offline: the audit pipeline and the evaluator make no network
10
+ * requests of any kind.
11
+ *
12
+ * Exit codes: 0 TRUE, 1 FALSE, 2 UNDETERMINED, 3 error.
13
+ * Diagnostics go to stderr; stdout carries verdict bytes only.
14
+ *
15
+ * The process exits by setting process.exitCode and returning, never by
16
+ * process.exit(): exiting early would truncate a verdict still draining
17
+ * to a pipe.
18
+ */
19
+
20
+ import { createHash } from "node:crypto";
21
+ import { readFileSync, writeFileSync } from "node:fs";
22
+ import { runAudit } from "@mikeargento/bitgraph-audit";
23
+ import type { AuditResult } from "@mikeargento/bitgraph-audit";
24
+ import { resolveCast } from "./cast.js";
25
+ import { evaluate } from "./evaluate.js";
26
+ import { parseRule, RuleError } from "./rule.js";
27
+ import { buildVerdict, serializeVerdict } from "./verdict.js";
28
+
29
+ function usage(): number {
30
+ process.stderr.write(
31
+ "usage: bitgraph-play <rule.json> <bundle> [--out <file>] [--summary]\n" +
32
+ " exit codes: 0 TRUE, 1 FALSE, 2 UNDETERMINED, 3 error\n"
33
+ );
34
+ return 3;
35
+ }
36
+
37
+ function printSummary(audit: AuditResult): void {
38
+ const lines: string[] = [];
39
+ const c = audit.ingest.counts;
40
+ lines.push(
41
+ `bundle: ${c.observed} observed proof(s), ${c.artifacts} artifact(s), ${c.witnesses} witness file(s)`
42
+ );
43
+ for (const partition of audit.reconstruction.partitions) {
44
+ const key = partition.key;
45
+ lines.push(
46
+ `partition epoch=${key.epochId ?? "(none)"} chain=${key.chainId} key=${key.publicKeyB64.slice(0, 12)}…: ` +
47
+ `${partition.memberProofHashes.length} proof(s) in ${partition.components.length} component(s)`
48
+ );
49
+ }
50
+ const rel = audit.reconstruction.epochRelationships;
51
+ lines.push(
52
+ `epochs: ${rel.epochs.length} observed, ${rel.orderedPairs.length} lineage-ordered pair(s), ` +
53
+ `${audit.temporal.anchorOrderedPairs.length} anchor-ordered pair(s)`
54
+ );
55
+ lines.push(
56
+ `unchained: ${audit.reconstruction.unchainedProofHashes.length}, unpartitioned: ${audit.reconstruction.unpartitionedProofHashes.length}`
57
+ );
58
+ process.stderr.write(lines.join("\n") + "\n");
59
+ }
60
+
61
+ async function main(): Promise<number> {
62
+ const args = process.argv.slice(2);
63
+ const positional: string[] = [];
64
+ let outFile: string | undefined;
65
+ let summary = false;
66
+ for (let i = 0; i < args.length; i++) {
67
+ const arg = args[i] as string;
68
+ if (arg === "--out") {
69
+ const next = args[++i];
70
+ if (next === undefined || next.startsWith("-")) return usage();
71
+ outFile = next;
72
+ } else if (arg === "--summary") {
73
+ summary = true;
74
+ } else if (arg.startsWith("-")) {
75
+ return usage();
76
+ } else {
77
+ positional.push(arg);
78
+ }
79
+ }
80
+ if (positional.length !== 2) return usage();
81
+ const [rulePath, bundlePath] = positional as [string, string];
82
+
83
+ let ruleBytes: Buffer;
84
+ try {
85
+ ruleBytes = readFileSync(rulePath);
86
+ } catch (err) {
87
+ process.stderr.write(`error: cannot read rule file: ${(err as Error).message}\n`);
88
+ return 3;
89
+ }
90
+ const ruleSha256Hex = createHash("sha256").update(ruleBytes).digest("hex");
91
+
92
+ let rule;
93
+ try {
94
+ rule = parseRule(ruleBytes.toString("utf8"));
95
+ } catch (err) {
96
+ if (err instanceof RuleError) {
97
+ process.stderr.write("error: invalid rule\n");
98
+ for (const issue of err.issues) process.stderr.write(` - ${issue}\n`);
99
+ } else {
100
+ process.stderr.write(`error: ${(err as Error).message}\n`);
101
+ }
102
+ return 3;
103
+ }
104
+
105
+ let audit: AuditResult;
106
+ try {
107
+ audit = await runAudit(bundlePath);
108
+ } catch (err) {
109
+ process.stderr.write(`error: bundle audit failed: ${(err as Error).message}\n`);
110
+ return 3;
111
+ }
112
+
113
+ const resolutions = resolveCast(rule.cast, audit);
114
+ const evaluation = evaluate(rule, resolutions, audit);
115
+ const verdict = buildVerdict(rule, ruleSha256Hex, resolutions, evaluation, audit);
116
+ const bytes = serializeVerdict(verdict);
117
+
118
+ if (outFile !== undefined) {
119
+ writeFileSync(outFile, bytes);
120
+ } else {
121
+ process.stdout.write(bytes);
122
+ }
123
+ if (summary) printSummary(audit);
124
+
125
+ switch (evaluation.result) {
126
+ case "TRUE":
127
+ return 0;
128
+ case "FALSE":
129
+ return 1;
130
+ case "UNDETERMINED":
131
+ return 2;
132
+ }
133
+ }
134
+
135
+ main().then(
136
+ (code) => {
137
+ process.exitCode = code;
138
+ },
139
+ (err: unknown) => {
140
+ process.stderr.write(`error: ${(err as Error).message}\n`);
141
+ process.exitCode = 3;
142
+ }
143
+ );
@@ -0,0 +1,238 @@
1
+ // Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
2
+
3
+ /**
4
+ * The evaluator: walks the claim tree over resolved recordings and the
5
+ * ordering adapter, under strong Kleene semantics.
6
+ *
7
+ * Deliberate properties:
8
+ *
9
+ * - FULL WALK, no short-circuit. Every sub-claim is evaluated and recorded
10
+ * so the verdict is a complete replayable trace, not just an outcome.
11
+ *
12
+ * - CLOSED WORLD, scoped to the cast. An absent OPTIONAL role is a
13
+ * definite absence within the declared universe: positive claims about
14
+ * it are FALSE, so negatives over it hold. An absent REQUIRED role, an
15
+ * ambiguous role, and a role the cast never declared are UNDETERMINED —
16
+ * the evidence does not decide, and saying FALSE would be a lie.
17
+ *
18
+ * - THE FLOOR GATES EVIDENCE, NOT POLARITY. When an ordering answer rests
19
+ * on a tier below requires.ordering, the answer is UNDETERMINED whether
20
+ * it would have been TRUE or FALSE: the rule author said not to trust
21
+ * that evidence in either direction.
22
+ */
23
+
24
+ import type { AuditResult, ObservedProof } from "@mikeargento/bitgraph-audit";
25
+ import { kleeneAll, kleeneAny, kleeneNot } from "./logic.js";
26
+ import { compare } from "./order.js";
27
+ import { basisTier, meetsFloor } from "./types.js";
28
+ import type {
29
+ Claim,
30
+ DerivedStep,
31
+ EvidenceTier,
32
+ Resolution,
33
+ Rule,
34
+ ThreeValued,
35
+ } from "./types.js";
36
+
37
+ export interface Evaluation {
38
+ result: ThreeValued;
39
+ steps: DerivedStep[];
40
+ /** Weakest tier among ordering answers that actually decided a step. */
41
+ weakestEvidence?: EvidenceTier;
42
+ }
43
+
44
+ interface Ctx {
45
+ rule: Rule;
46
+ resolutions: Map<string, Resolution>;
47
+ audit: AuditResult;
48
+ steps: DerivedStep[];
49
+ usedTiers: Set<EvidenceTier>;
50
+ }
51
+
52
+ function positionOf(proof: ObservedProof): Record<string, unknown> {
53
+ const out: Record<string, unknown> = { proofHash: proof.proofHash };
54
+ if (proof.epochId !== undefined) out["epochId"] = proof.epochId;
55
+ out["chainId"] = proof.chainId;
56
+ if (proof.counter !== undefined) out["counter"] = proof.counter;
57
+ if (proof.slotCounter !== undefined) out["slotCounter"] = proof.slotCounter;
58
+ return out;
59
+ }
60
+
61
+ /**
62
+ * Classify a role for predicate purposes.
63
+ * resolved -> use the recording
64
+ * definitely-absent -> optional and absent: closed-world FALSE material
65
+ * undetermined -> everything else, with the reason
66
+ */
67
+ function classify(
68
+ ctx: Ctx,
69
+ role: string
70
+ ):
71
+ | { kind: "resolved"; proof: ObservedProof }
72
+ | { kind: "definitely-absent" }
73
+ | { kind: "undetermined"; reason: string } {
74
+ const res = ctx.resolutions.get(role);
75
+ if (res === undefined) {
76
+ return { kind: "undetermined", reason: `role "${role}" is not declared in the cast` };
77
+ }
78
+ switch (res.kind) {
79
+ case "resolved":
80
+ return { kind: "resolved", proof: res.proof };
81
+ case "absent":
82
+ return res.optional
83
+ ? { kind: "definitely-absent" }
84
+ : {
85
+ kind: "undetermined",
86
+ reason: `required role "${role}" has no verified recording in the bundle`,
87
+ };
88
+ case "ambiguous":
89
+ return {
90
+ kind: "undetermined",
91
+ reason: `role "${role}" matches ${res.matchCount} recordings and no "at" pin selects one`,
92
+ };
93
+ case "invalid":
94
+ return { kind: "undetermined", reason: `role "${role}": ${res.reason}` };
95
+ }
96
+ }
97
+
98
+ function record(ctx: Ctx, step: DerivedStep): ThreeValued {
99
+ ctx.steps.push(step);
100
+ return step.result;
101
+ }
102
+
103
+ function evalExists(ctx: Ctx, role: string): ThreeValued {
104
+ const c = classify(ctx, role);
105
+ const claim = `exists(${role})`;
106
+ if (c.kind === "resolved") {
107
+ return record(ctx, {
108
+ claim,
109
+ result: "TRUE",
110
+ because: { recording: positionOf(c.proof) },
111
+ });
112
+ }
113
+ if (c.kind === "definitely-absent") {
114
+ return record(ctx, {
115
+ claim,
116
+ result: "FALSE",
117
+ because: {
118
+ reason: `no verified recording of the declared digest is in the bundle; definite absence within the declared closed world`,
119
+ },
120
+ });
121
+ }
122
+ return record(ctx, { claim, result: "UNDETERMINED", because: { reason: c.reason } });
123
+ }
124
+
125
+ /** Shared body of before/after: asks whether `x` precedes `y`. */
126
+ function evalPrecedes(ctx: Ctx, claim: string, x: string, y: string): ThreeValued {
127
+ const cx = classify(ctx, x);
128
+ const cy = classify(ctx, y);
129
+
130
+ for (const [role, c] of [
131
+ [x, cx],
132
+ [y, cy],
133
+ ] as const) {
134
+ if (c.kind === "undetermined") {
135
+ return record(ctx, {
136
+ claim,
137
+ result: "UNDETERMINED",
138
+ because: { reason: c.reason, role },
139
+ });
140
+ }
141
+ }
142
+ if (cx.kind === "definitely-absent" || cy.kind === "definitely-absent") {
143
+ const absent = cx.kind === "definitely-absent" ? x : y;
144
+ return record(ctx, {
145
+ claim,
146
+ result: "FALSE",
147
+ because: {
148
+ reason: `role "${absent}" is definitely absent within the declared closed world; no ordering claim about it can hold`,
149
+ },
150
+ });
151
+ }
152
+
153
+ const a = (cx as { kind: "resolved"; proof: ObservedProof }).proof;
154
+ const b = (cy as { kind: "resolved"; proof: ObservedProof }).proof;
155
+ const order = compare(a, b, ctx.audit);
156
+ const because: Record<string, unknown> = {
157
+ a: positionOf(a),
158
+ b: positionOf(b),
159
+ relation: order.relation,
160
+ detail: order.detail,
161
+ };
162
+
163
+ if (order.relation === "same") {
164
+ return record(ctx, { claim, result: "FALSE", because });
165
+ }
166
+ if (order.relation === "unordered") {
167
+ return record(ctx, { claim, result: "UNDETERMINED", because });
168
+ }
169
+
170
+ const basis = order.basis as NonNullable<typeof order.basis>;
171
+ // The tier the specific answer rests on — carried by the adapter, not
172
+ // recomputed from the basis family (lineage coverage can downgrade it).
173
+ const tier = order.tier ?? basisTier(basis);
174
+ because["weaker"] = order.weaker;
175
+ because["assumptionDependent"] = order.assumptionDependent;
176
+
177
+ if (!meetsFloor(tier, ctx.rule.requires.ordering)) {
178
+ because["floor"] = ctx.rule.requires.ordering;
179
+ return record(ctx, {
180
+ claim,
181
+ result: "UNDETERMINED",
182
+ basis,
183
+ evidenceTier: tier,
184
+ because: {
185
+ ...because,
186
+ reason: `ordering evidence ("${basis}", tier "${tier}") is below the rule's declared floor ("${ctx.rule.requires.ordering}")`,
187
+ },
188
+ });
189
+ }
190
+
191
+ ctx.usedTiers.add(tier);
192
+ return record(ctx, {
193
+ claim,
194
+ result: order.relation === "before" ? "TRUE" : "FALSE",
195
+ basis,
196
+ evidenceTier: tier,
197
+ because,
198
+ });
199
+ }
200
+
201
+ function evalClaim(ctx: Ctx, claim: Claim): ThreeValued {
202
+ if ("exists" in claim) return evalExists(ctx, claim.exists);
203
+ if ("before" in claim) {
204
+ const [x, y] = claim.before;
205
+ return evalPrecedes(ctx, `before(${x}, ${y})`, x, y);
206
+ }
207
+ if ("after" in claim) {
208
+ const [x, y] = claim.after;
209
+ // after(x, y) holds exactly when y precedes x.
210
+ return evalPrecedes(ctx, `after(${x}, ${y})`, y, x);
211
+ }
212
+ if ("between" in claim) {
213
+ const [subject, lower, upper] = claim.between;
214
+ // between(x, a, b) := after(x, a) AND before(x, b), recorded as two steps.
215
+ const afterPart = evalPrecedes(ctx, `between(${subject}, ${lower}, ${upper}): after(${subject}, ${lower})`, lower, subject);
216
+ const beforePart = evalPrecedes(ctx, `between(${subject}, ${lower}, ${upper}): before(${subject}, ${upper})`, subject, upper);
217
+ return kleeneAll([afterPart, beforePart]);
218
+ }
219
+ if ("all" in claim) return kleeneAll(claim.all.map((c) => evalClaim(ctx, c)));
220
+ if ("any" in claim) return kleeneAny(claim.any.map((c) => evalClaim(ctx, c)));
221
+ return kleeneNot(evalClaim(ctx, claim.not));
222
+ }
223
+
224
+ export function evaluate(
225
+ rule: Rule,
226
+ resolutions: Map<string, Resolution>,
227
+ audit: AuditResult
228
+ ): Evaluation {
229
+ const ctx: Ctx = { rule, resolutions, audit, steps: [], usedTiers: new Set() };
230
+ const result = evalClaim(ctx, rule.claim);
231
+ const evaluation: Evaluation = { result, steps: ctx.steps };
232
+ if (ctx.usedTiers.has("assumption-dependent")) {
233
+ evaluation.weakestEvidence = "assumption-dependent";
234
+ } else if (ctx.usedTiers.has("hash-linked")) {
235
+ evaluation.weakestEvidence = "hash-linked";
236
+ }
237
+ return evaluation;
238
+ }
package/src/index.ts ADDED
@@ -0,0 +1,38 @@
1
+ // Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
2
+
3
+ /**
4
+ * @mikeargento/bitgraph-player
5
+ *
6
+ * Deterministic evaluation of causal rules over BitGraph proof bundles.
7
+ *
8
+ * BitGraph records. Player executes.
9
+ *
10
+ * Player is a pure function over `runAudit()`'s output: same rule, same
11
+ * bundle, same verdict, on anyone's machine, with no network access at
12
+ * evaluation time. It decides; it does not enforce — no field in the rule
13
+ * format is capable of causing an action.
14
+ */
15
+
16
+ export type {
17
+ CastEntry,
18
+ CastPin,
19
+ Claim,
20
+ DeclaredEntry,
21
+ DerivedStep,
22
+ EvidenceTier,
23
+ OrderBasis,
24
+ OrderResult,
25
+ Resolution,
26
+ Rule,
27
+ ThreeValued,
28
+ Verdict,
29
+ } from "./types.js";
30
+ export { basisTier, meetsFloor } from "./types.js";
31
+
32
+ export { parseRule, normalizeDigest, RuleError } from "./rule.js";
33
+ export { resolveCast, resolveRole } from "./cast.js";
34
+ export { compare } from "./order.js";
35
+ export { kleeneAll, kleeneAny, kleeneNot } from "./logic.js";
36
+ export { evaluate } from "./evaluate.js";
37
+ export type { Evaluation } from "./evaluate.js";
38
+ export { buildVerdict, serializeVerdict, playerVersion } from "./verdict.js";
package/src/logic.ts ADDED
@@ -0,0 +1,39 @@
1
+ // Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
2
+
3
+ /**
4
+ * Strong Kleene three-valued connectives.
5
+ *
6
+ * all: FALSE if any FALSE; else UNDETERMINED if any UNDETERMINED; else TRUE
7
+ * any: TRUE if any TRUE; else UNDETERMINED if any UNDETERMINED; else FALSE
8
+ * not: TRUE <-> FALSE; UNDETERMINED stays UNDETERMINED
9
+ *
10
+ * UNDETERMINED is not a nuisance value: it is the honest answer wherever
11
+ * the evidence does not decide, and these tables are what keep it from
12
+ * being laundered into FALSE by composition.
13
+ */
14
+
15
+ import type { ThreeValued } from "./types.js";
16
+
17
+ export function kleeneNot(v: ThreeValued): ThreeValued {
18
+ if (v === "TRUE") return "FALSE";
19
+ if (v === "FALSE") return "TRUE";
20
+ return "UNDETERMINED";
21
+ }
22
+
23
+ export function kleeneAll(values: readonly ThreeValued[]): ThreeValued {
24
+ let sawUndetermined = false;
25
+ for (const v of values) {
26
+ if (v === "FALSE") return "FALSE";
27
+ if (v === "UNDETERMINED") sawUndetermined = true;
28
+ }
29
+ return sawUndetermined ? "UNDETERMINED" : "TRUE";
30
+ }
31
+
32
+ export function kleeneAny(values: readonly ThreeValued[]): ThreeValued {
33
+ let sawUndetermined = false;
34
+ for (const v of values) {
35
+ if (v === "TRUE") return "TRUE";
36
+ if (v === "UNDETERMINED") sawUndetermined = true;
37
+ }
38
+ return sawUndetermined ? "UNDETERMINED" : "FALSE";
39
+ }