speedrunpapi 0.3.2__py3-none-any.whl

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.
@@ -0,0 +1,1598 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import * as fs2 from "node:fs";
5
+ import * as path2 from "node:path";
6
+ import { createHash } from "node:crypto";
7
+
8
+ // ../carrycert-core/src/math.ts
9
+ var BN128_PRIME = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
10
+ function arrangementNumber(k) {
11
+ let v = 1n;
12
+ for (let i = 1; i <= k; i++) v = BigInt(i) * v + 1n;
13
+ return v;
14
+ }
15
+ function cascadeFunctionCount(radix, stages) {
16
+ if (radix < 2n || stages < 0) throw new Error("radix>=2, stages>=0");
17
+ const t = Math.min(stages, Number(radix - 1n) > 1e9 ? stages : Number(radix - 1n));
18
+ const tt = Math.min(stages, Number(radix > 10n ** 9n ? 10n ** 9n : radix - 1n));
19
+ const tEff = stages;
20
+ const A = arrangementNumber(tEff);
21
+ return (radix - 1n) * A + 1n;
22
+ }
23
+ function cascadeMinBits(radix, stages) {
24
+ const n = cascadeFunctionCount(radix, stages);
25
+ return n === 0n ? 0 : n.toString(2).length;
26
+ }
27
+ function twoStageCount(radix) {
28
+ return 5n * radix - 4n;
29
+ }
30
+ function frontierBound(m) {
31
+ return m + 1;
32
+ }
33
+ function frontierSolve(width, offsets, masks, targets) {
34
+ const m = offsets.length;
35
+ const limit = 1 << width;
36
+ const mm = masks ?? Array(m).fill(0);
37
+ const tt = targets ?? Array(m).fill(0);
38
+ let thresholds = Array(m).fill(1);
39
+ let order = [...Array(m).keys()];
40
+ let boundaries = [0, 1];
41
+ let switches = Array(m).fill(0);
42
+ let counts = [1n];
43
+ let attempted = 0;
44
+ let maxStates = 1;
45
+ const layer = (w, th, ord) => {
46
+ const lim = 1 << w;
47
+ const b = [0];
48
+ const sw = Array(th.length).fill(0);
49
+ for (const j of ord) {
50
+ const t = th[j];
51
+ if (t < lim && t !== b[b.length - 1]) b.push(t);
52
+ sw[j] = t < lim ? b.length - 1 : -1;
53
+ }
54
+ for (let j = 0; j < th.length; j++) if (th[j] === lim) sw[j] = b.length;
55
+ b.push(lim);
56
+ return { boundaries: b, switches: sw, states: b.length - 1 };
57
+ };
58
+ let current = layer(0, thresholds, order);
59
+ for (let i = 0; i < width; i++) {
60
+ const span = 1 << i;
61
+ const bits = offsets.map((a) => a >> i & 1);
62
+ order = [...order.filter((j) => bits[j]), ...order.filter((j) => !bits[j])];
63
+ thresholds = thresholds.map((t, j) => t + (1 - bits[j]) * span);
64
+ const following = layer(i + 1, thresholds, order);
65
+ const nextCounts = Array(following.states).fill(0n);
66
+ maxStates = Math.max(maxStates, current.states, following.states);
67
+ const successors = (bit) => {
68
+ const target = [];
69
+ let r = 0;
70
+ const shift = bit << current.boundaries.length;
71
+ const sh = bit << i;
72
+ for (let idx = 0; idx < current.states; idx++) {
73
+ const v = current.boundaries[idx] + sh;
74
+ while (r + 1 < following.states && following.boundaries[r + 1] <= v) r++;
75
+ target.push(r);
76
+ }
77
+ return target;
78
+ };
79
+ for (const b of [0, 1]) {
80
+ const dest = successors(b);
81
+ let lo = 0, hi = current.states - 1;
82
+ for (let j = 0; j < m; j++) {
83
+ if (mm[j] >> i & 1) {
84
+ const required = b ^ bits[j] ^ tt[j] >> i & 1;
85
+ if (required) lo = Math.max(lo, current.switches[j]);
86
+ else hi = Math.min(hi, current.switches[j] - 1);
87
+ }
88
+ }
89
+ for (let r = 0; r < current.states; r++) {
90
+ attempted++;
91
+ if (!counts[r] || r < lo || r > hi) continue;
92
+ nextCounts[dest[r]] += counts[r];
93
+ }
94
+ }
95
+ counts = nextCounts;
96
+ current = following;
97
+ }
98
+ const count = counts.reduce((a, b) => a + b, 0n);
99
+ return { count, attempted, maxStates };
100
+ }
101
+
102
+ // ../carrycert-core/src/analyze.ts
103
+ function modsumUnique(c) {
104
+ if (c.equation && c.sum_bitified && c.carry_bitified) {
105
+ return { unique: true, reason: "joint Num2Bits-style (sum+carry bits) uniquely determines (sum,carry)" };
106
+ }
107
+ if (c.equation && c.sum_bitified && !c.carry_bitified) {
108
+ return { unique: false, reason: "sum bits without carry bit: field alias carry=(a+b)*inv(2^n)" };
109
+ }
110
+ if (c.equation && !c.sum_bitified) {
111
+ return { unique: false, reason: "equation alone leaves free rank-1 variable over F_p" };
112
+ }
113
+ return { unique: false, reason: "insufficient constraints" };
114
+ }
115
+ function modsumCex(n, a = 3, b = 5) {
116
+ const MOD = 1 << n;
117
+ const sum0 = (a + b) % MOD;
118
+ const carry0 = Math.floor((a + b) / MOD);
119
+ const sum1 = ((BigInt(sum0) - BigInt(MOD) + BN128_PRIME) % BN128_PRIME).toString();
120
+ const carry1 = ((BigInt(carry0) + 1n) % BN128_PRIME).toString();
121
+ return {
122
+ inputs: { a, b, n },
123
+ witness_a: { sum: sum0, carry: carry0 },
124
+ witness_b: { sum: sum1, carry: carry1 },
125
+ equation: "a+b = sum + carry*2^n (mod p)",
126
+ prime: BN128_PRIME.toString()
127
+ };
128
+ }
129
+ function sumBitsCarryFreeCex(n, a = 0, b = 1) {
130
+ const MOD = 1 << n;
131
+ const inv = (() => {
132
+ return BigInt(MOD) === 0n ? 0n : modInv(BigInt(MOD), BN128_PRIME);
133
+ })();
134
+ const sum0 = (a + b) % MOD;
135
+ const carry0 = Math.floor((a + b) / MOD);
136
+ const sum1 = 0;
137
+ const carry1 = BigInt(a + b) % BN128_PRIME * inv % BN128_PRIME;
138
+ return {
139
+ inputs: { a, b, n },
140
+ witness_a: { sum: sum0, carry: carry0 },
141
+ witness_b: { sum: sum1, carry: carry1.toString() },
142
+ equation: "a+b = sum + carry*2^n (mod p), sum in [0,2^n)",
143
+ prime: BN128_PRIME.toString()
144
+ };
145
+ }
146
+ function modInv(a, m) {
147
+ let [old_r, r] = [a % m, m];
148
+ let [old_s, s] = [1n, 0n];
149
+ while (r !== 0n) {
150
+ const q = old_r / r;
151
+ [old_r, r] = [r, old_r - q * r];
152
+ [old_s, s] = [s, old_s - q * s];
153
+ }
154
+ return (old_s % m + m) % m;
155
+ }
156
+ function limbCex(n, k) {
157
+ const MOD = 1 << n;
158
+ const a = Array(k).fill(0);
159
+ const b = Array(k).fill(0);
160
+ const out = Array(k + 1).fill(0);
161
+ const carries = Array(k).fill(0);
162
+ out[0] = MOD;
163
+ carries[0] = Number(BN128_PRIME - 1n);
164
+ for (let i = 1; i < k; i++) {
165
+ carries[i] = 0;
166
+ out[i] = carries[i - 1];
167
+ }
168
+ out[k] = carries[k - 1];
169
+ return {
170
+ inputs: { a, b, n, k },
171
+ witness_a: { out: Array(k + 1).fill(0), carry: Array(k).fill(0) },
172
+ witness_b: { out, carry: carries },
173
+ prime: BN128_PRIME.toString()
174
+ };
175
+ }
176
+ function withDomain(gadget, r) {
177
+ if (!r.domain && gadget.domain) r.domain = gadget.domain;
178
+ return r;
179
+ }
180
+ function analyze(gadget) {
181
+ const t0 = performance.now();
182
+ const { family, constraints: c, params, id } = gadget;
183
+ const finish = (r) => withDomain(gadget, r);
184
+ if (family === "circom_function") {
185
+ const primitive = String(params.primitive ?? "");
186
+ const expr = primitive === "and" ? "a*b" : primitive === "mux1" ? "(b-a)*s+a" : null;
187
+ if (!expr || !c.equation) {
188
+ return finish({
189
+ id,
190
+ verdict: "Unknown",
191
+ time_ms: performance.now() - t0,
192
+ method: "circom_direct_incomplete",
193
+ theorem: "n/a",
194
+ reason: "Incomplete field assignment"
195
+ });
196
+ }
197
+ return finish({
198
+ id,
199
+ verdict: "Certified",
200
+ witness_count: 1,
201
+ time_ms: performance.now() - t0,
202
+ method: "circom_direct_arithmetic",
203
+ theorem: "A constrained field assignment fixes its output for each input tuple",
204
+ certificate: {
205
+ type: "functional_assignment",
206
+ primitive,
207
+ expression: expr,
208
+ field: "BN254",
209
+ input_domain: "field elements",
210
+ scope: "The main template contains only the declared inputs and one constrained output; every input tuple has one output."
211
+ }
212
+ });
213
+ }
214
+ if (family === "circom_comparison") {
215
+ const n = Number(params.n ?? 0);
216
+ const bits = Number(params.range_bits ?? 0);
217
+ if (n !== 4 || bits !== 5 || !c.equation || !c.sum_bitified || !c.carry_bitified) {
218
+ return finish({
219
+ id,
220
+ verdict: "Unknown",
221
+ time_ms: performance.now() - t0,
222
+ method: "circom_comparison_incomplete",
223
+ theorem: "n/a",
224
+ reason: "Missing exact bit decomposition"
225
+ });
226
+ }
227
+ return finish({
228
+ id,
229
+ verdict: "Certified",
230
+ witness_count: 1,
231
+ time_ms: performance.now() - t0,
232
+ method: "circom_bitified_comparison",
233
+ theorem: "Unique bit decomposition for 2^5 < p, followed by a constrained output",
234
+ certificate: {
235
+ type: "bit_decomposition",
236
+ n: 4,
237
+ range_bits: 5,
238
+ field: "BN254",
239
+ equation: "diff=a+16-b; diff=sum(bit[i]*2^i); out=1-bit[4]",
240
+ scope: "At most one witness for fixed field inputs when the five-bit decomposition is satisfiable; usual less-than semantics for inputs 0..15."
241
+ }
242
+ });
243
+ }
244
+ if (family === "modsum") {
245
+ const n = Number(params.n ?? 4);
246
+ const { unique, reason } = modsumUnique(c);
247
+ const dt = performance.now() - t0;
248
+ if (unique) {
249
+ const B = 1n << BigInt(n);
250
+ return finish({
251
+ id,
252
+ verdict: "Certified",
253
+ witness_count: 1,
254
+ time_ms: dt,
255
+ method: "modsum_uniqueness_lemma",
256
+ theorem: "two-stage-addition + Num2Bits soundness",
257
+ certificate: {
258
+ type: "uniqueness_lemma",
259
+ family: "modsum",
260
+ n,
261
+ lemma: reason,
262
+ two_stage_count: twoStageCount(B).toString(),
263
+ theorem_ref: "DOI 10.5281/zenodo.23698617"
264
+ }
265
+ });
266
+ }
267
+ const cex = c.sum_bitified && !c.carry_bitified ? sumBitsCarryFreeCex(n) : modsumCex(n);
268
+ return finish({
269
+ id,
270
+ verdict: "Broken",
271
+ time_ms: dt,
272
+ method: c.sum_bitified ? "modsum_sum_bits_carry_free" : "modsum_equation_rank",
273
+ theorem: "field alias / free variable over F_p",
274
+ counterexample: cex,
275
+ reason
276
+ });
277
+ }
278
+ if (family === "modsum3" || family === "full_adder") {
279
+ const dt = performance.now() - t0;
280
+ const ok = c.sum_bitified && c.carry_bitified && (family === "full_adder" || c.incoming_bitified || true);
281
+ if (family === "full_adder" && c.sum_bitified && c.carry_bitified) {
282
+ return finish({
283
+ id,
284
+ verdict: "Certified",
285
+ witness_count: 1,
286
+ time_ms: dt,
287
+ method: "full_adder_num2bits",
288
+ theorem: "two-stage-addition",
289
+ certificate: { type: "uniqueness_lemma", family, safety: c.input_bitified ? "strong" : "weak" }
290
+ });
291
+ }
292
+ if (family === "modsum3" && c.sum_bitified && c.carry_bitified) {
293
+ const n = Number(params.n ?? 4);
294
+ const B = 1n << BigInt(n);
295
+ return finish({
296
+ id,
297
+ verdict: "Certified",
298
+ witness_count: 1,
299
+ time_ms: dt,
300
+ method: "modsum3_bitified",
301
+ theorem: "cascaded-sum-carry",
302
+ certificate: { cascade_count: cascadeFunctionCount(B, 2).toString(), theorem_ref: "DOI 10.5281/zenodo.23700193" }
303
+ });
304
+ }
305
+ return finish({ id, verdict: "Broken", time_ms: dt, method: "open", theorem: "n/a", counterexample: modsumCex(Number(params.n ?? 1)), reason: "missing bitification" });
306
+ }
307
+ if (family === "limb_add" || family === "binsum") {
308
+ const n = Number(params.n ?? 4);
309
+ const k = Number(params.k ?? params.ops ?? 2);
310
+ const dt = performance.now() - t0;
311
+ if (c.equation && c.sum_bitified && c.carry_bitified) {
312
+ const B = 1n << BigInt(n);
313
+ const stages = Math.max(1, k - 1);
314
+ return finish({
315
+ id,
316
+ verdict: "Certified",
317
+ witness_count: 1,
318
+ time_ms: dt,
319
+ method: "limb_add_bitified",
320
+ theorem: "cascaded-sum-carry + two-stage-addition",
321
+ certificate: {
322
+ n,
323
+ k,
324
+ two_stage_count: twoStageCount(B).toString(),
325
+ cascade_count: cascadeFunctionCount(B, stages).toString(),
326
+ cascade_bits: cascadeMinBits(B, stages),
327
+ theorem_ref: ["DOI 10.5281/zenodo.23698617", "DOI 10.5281/zenodo.23700193"]
328
+ }
329
+ });
330
+ }
331
+ return finish({
332
+ id,
333
+ verdict: "Broken",
334
+ time_ms: dt,
335
+ method: "limb_add_equation_only",
336
+ theorem: "cascaded-sum-carry (negative)",
337
+ counterexample: limbCex(n, k),
338
+ reason: "equation-only limb chain"
339
+ });
340
+ }
341
+ if (family === "shared_input") {
342
+ const width = Number(params.width ?? 8);
343
+ const offsets = params.offsets ?? [13, 45, 93];
344
+ const sample = frontierSolve(width, offsets);
345
+ const dt = performance.now() - t0;
346
+ if (c.sum_bitified && c.carry_bitified && c.input_bitified) {
347
+ return finish({
348
+ id,
349
+ verdict: "Certified",
350
+ witness_count: 1,
351
+ time_ms: dt,
352
+ method: "shared_input_frontier",
353
+ theorem: "shared-input-carry-frontiers DOI 10.5281/zenodo.23702847",
354
+ certificate: {
355
+ frontier_bound: frontierBound(offsets.length),
356
+ sample_count: sample.count.toString(),
357
+ max_states: sample.maxStates,
358
+ offsets
359
+ }
360
+ });
361
+ }
362
+ const MOD = 1 << width;
363
+ return finish({
364
+ id,
365
+ verdict: "Broken",
366
+ time_ms: dt,
367
+ method: "shared_input_equation_only",
368
+ theorem: "shared-input-carry-frontiers (negative)",
369
+ counterexample: {
370
+ inputs: { x: 0, offsets, width },
371
+ witness_a: { y: offsets.map((o) => o % MOD), c: offsets.map((o) => Math.floor(o / MOD)) },
372
+ witness_b: {
373
+ y: [(offsets[0] + MOD) % Number(BN128_PRIME > BigInt(Number.MAX_SAFE_INTEGER) ? MOD * 2 : Number(BN128_PRIME)), ...offsets.slice(1).map((o) => o % MOD)],
374
+ c: [Number(BN128_PRIME - 1n), ...offsets.slice(1).map((o) => Math.floor(o / MOD))]
375
+ },
376
+ frontier_bound: frontierBound(offsets.length)
377
+ }
378
+ });
379
+ }
380
+ if (family === "cascade") {
381
+ const n = Number(params.n ?? 4);
382
+ const stages = Number(params.stages ?? 3);
383
+ const B = 1n << BigInt(n);
384
+ const dt = performance.now() - t0;
385
+ if (c.sum_bitified && c.carry_bitified && c.incoming_bitified) {
386
+ return finish({
387
+ id,
388
+ verdict: "Certified",
389
+ witness_count: 1,
390
+ time_ms: dt,
391
+ method: "cascade_bitified",
392
+ theorem: "cascaded-sum-carry",
393
+ certificate: {
394
+ exact_function_count: cascadeFunctionCount(B, stages).toString(),
395
+ minimum_label_bits: cascadeMinBits(B, stages),
396
+ theorem_ref: "DOI 10.5281/zenodo.23700193"
397
+ }
398
+ });
399
+ }
400
+ return finish({
401
+ id,
402
+ verdict: "Broken",
403
+ time_ms: dt,
404
+ method: "cascade_equation_only",
405
+ theorem: "cascaded-sum-carry (negative)",
406
+ counterexample: limbCex(n, stages)
407
+ });
408
+ }
409
+ if (family === "bigint_mod") {
410
+ const n = Number(params.n ?? 3);
411
+ const k = Number(params.k ?? 2);
412
+ const dt = performance.now() - t0;
413
+ if (c.sum_bitified && c.carry_bitified) {
414
+ const B = 1n << BigInt(n);
415
+ return finish({
416
+ id,
417
+ verdict: "Certified",
418
+ witness_count: 1,
419
+ time_ms: dt,
420
+ method: "bigint_mod_range_checked",
421
+ theorem: "limb range checks + reconstruction (circom-bigint BigMod pattern)",
422
+ certificate: {
423
+ n,
424
+ k,
425
+ note: "Certifies uniqueness under the standard BigMod constraint pattern: Num2Bits on div/mod limbs, BigMult/BigAdd reconstruction equality, and mod < b. Subgadget carry structure uses two-stage/cascade counts.",
426
+ two_stage_count: twoStageCount(B).toString(),
427
+ cascade_count: cascadeFunctionCount(B, Math.max(1, k)).toString(),
428
+ theorem_ref: ["DOI 10.5281/zenodo.23698617", "DOI 10.5281/zenodo.23700193"],
429
+ scope: "weak_safety_outputs_unique_given_public_inputs_under_stated_pattern"
430
+ }
431
+ });
432
+ }
433
+ return finish({
434
+ id,
435
+ verdict: "Unknown",
436
+ time_ms: dt,
437
+ method: "bigint_mod_incomplete",
438
+ theorem: "n/a",
439
+ reason: "BigMod without detected limb range checks; refusing to guess"
440
+ });
441
+ }
442
+ if (family === "arx_sha256") {
443
+ const dt = performance.now() - t0;
444
+ if (c.sum_bitified && c.input_bitified) {
445
+ return finish({
446
+ id,
447
+ verdict: "Certified",
448
+ witness_count: 1,
449
+ time_ms: dt,
450
+ method: "arx_sha256_binsum_bitpath",
451
+ theorem: "circomlib Sha256compression uses bit signals + BinSum; shared-input/cascade bounds apply to add layers",
452
+ certificate: {
453
+ type: "compositional",
454
+ subgadgets: ["BinSum(32,2)", "BinSum(32,5)", "SigmaPlus", "T1", "T2"],
455
+ frontier_note: "T1 adds the same derived word into multiple running states \u2014 shared-input frontier DOI 10.5281/zenodo.23702847",
456
+ theorem_ref: [
457
+ "DOI 10.5281/zenodo.23702847",
458
+ "DOI 10.5281/zenodo.23697726",
459
+ "DOI 10.5281/zenodo.23697063"
460
+ ],
461
+ scope: "Certified for circomlib-style bit-level SHA-256 compression where all wires are bits and sums use BinSum. Not a general ARX soundness proof for arbitrary encodings.",
462
+ honesty: "Full differential/fiber analysis via affine-carry-inversion is available for \u03C3-shaped maps but not yet auto-composed for every custom SHA circuit."
463
+ }
464
+ });
465
+ }
466
+ return finish({
467
+ id,
468
+ verdict: "Unknown",
469
+ time_ms: dt,
470
+ method: "arx_sha256_unclassified",
471
+ theorem: "affine-carry-inversion / tap-span available but pattern incomplete",
472
+ reason: "SHA-256-like circuit without detected bit-level BinSum pattern"
473
+ });
474
+ }
475
+ if (family === "fixed_mac") {
476
+ const bits = Number(params.bits ?? 8);
477
+ const accBits = Number(params.acc_bits ?? 32);
478
+ const mode = String(params.overflow ?? "wrap");
479
+ const dt = performance.now() - t0;
480
+ if (c.sum_bitified && c.carry_bitified) {
481
+ const B = 1n << BigInt(accBits);
482
+ return finish({
483
+ id,
484
+ verdict: "Certified",
485
+ witness_count: 1,
486
+ time_ms: dt,
487
+ domain: gadget.domain,
488
+ method: "fixed_mac_range_checked",
489
+ theorem: "cascaded-sum-carry / two-stage-addition applied to fixed-point accumulator",
490
+ certificate: {
491
+ bits,
492
+ acc_bits: accBits,
493
+ overflow: mode,
494
+ two_stage_count: twoStageCount(1n << BigInt(Math.min(accBits, 32))).toString(),
495
+ theorem_ref: ["DOI 10.5281/zenodo.23698617", "DOI 10.5281/zenodo.23700193"],
496
+ scope: "Unique accumulator update given inputs when product and acc residue are range-checked (or jointly bitified)."
497
+ }
498
+ });
499
+ }
500
+ const n = Math.min(bits, 16);
501
+ return finish({
502
+ id,
503
+ verdict: "Broken",
504
+ time_ms: dt,
505
+ domain: gadget.domain,
506
+ method: "fixed_mac_equation_only",
507
+ theorem: "field/int alias of accumulator residue",
508
+ counterexample: modsumCex(n, 3, 5),
509
+ reason: "accumulator equation without range checks admits multiple (residue, overflow) pairs"
510
+ });
511
+ }
512
+ if (family === "hw_adder") {
513
+ const width = Number(params.width ?? 8);
514
+ const style = String(params.style ?? "ripple");
515
+ const dt = performance.now() - t0;
516
+ if (c.sum_bitified && c.carry_bitified && c.input_bitified) {
517
+ const B = 1n << BigInt(width);
518
+ return finish({
519
+ id,
520
+ verdict: "Certified",
521
+ witness_count: 1,
522
+ time_ms: dt,
523
+ domain: gadget.domain,
524
+ method: "hw_adder_bit_carry_chain",
525
+ theorem: "cascaded-sum-carry exact state; ripple/CLA with bit I/O is unique",
526
+ certificate: {
527
+ width,
528
+ style,
529
+ cascade_count: cascadeFunctionCount(2n, width).toString(),
530
+ cascade_bits: cascadeMinBits(2n, width),
531
+ theorem_ref: "DOI 10.5281/zenodo.23700193",
532
+ scope: "Bit-level adder netlist with full carry chain: (sum, cout) unique given (a,b,cin)."
533
+ }
534
+ });
535
+ }
536
+ if (c.equation && !c.carry_bitified) {
537
+ return finish({
538
+ id,
539
+ verdict: "Broken",
540
+ time_ms: dt,
541
+ domain: gadget.domain,
542
+ method: "hw_adder_missing_carry_bits",
543
+ theorem: "carry frontier incomplete",
544
+ counterexample: modsumCex(Math.min(width, 8), 1, 1),
545
+ reason: "adder equation without bit-constrained carries"
546
+ });
547
+ }
548
+ return finish({
549
+ id,
550
+ verdict: "Unknown",
551
+ time_ms: dt,
552
+ domain: gadget.domain,
553
+ method: "hw_adder_partial",
554
+ theorem: "n/a",
555
+ reason: "hardware adder pattern incomplete (need bit I/O + carry chain flags)"
556
+ });
557
+ }
558
+ return finish({
559
+ id,
560
+ verdict: "Unknown",
561
+ time_ms: performance.now() - t0,
562
+ method: "no_analyzer",
563
+ theorem: "n/a",
564
+ reason: `unsupported family ${family}`
565
+ });
566
+ }
567
+
568
+ // ../carrycert-core/src/circom-adapter.ts
569
+ function stripComments(src) {
570
+ return src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
571
+ }
572
+ function detectMain(src) {
573
+ const m = src.match(/component\s+main(?:\s*\{[^}]*\})?\s*=\s*([A-Za-z_][A-Za-z0-9_]*)\s*(?:\(([^)]*)\))?\s*;/);
574
+ if (!m) return null;
575
+ const args = (m[2] ?? "").split(",").map((s) => s.trim()).filter(Boolean).map((s) => Number(s)).filter((n) => !Number.isNaN(n));
576
+ return { template: m[1], args };
577
+ }
578
+ function extractTemplateBody(src, name) {
579
+ const re = new RegExp(String.raw`template\s+${name}\s*(?:\([^)]*\))?\s*\{`);
580
+ const m = re.exec(src);
581
+ if (!m) return null;
582
+ let i = m.index + m[0].length;
583
+ let depth = 1;
584
+ const start = i;
585
+ while (i < src.length && depth > 0) {
586
+ if (src[i] === "{") depth++;
587
+ else if (src[i] === "}") depth--;
588
+ i++;
589
+ }
590
+ return src.slice(start, i - 1);
591
+ }
592
+ function num2BitsOnSumOnly(body) {
593
+ const sumBits = /Num2Bits\s*\([^)]*\)[\s\S]{0,120}?\.in\s*<==\s*sum\b/.test(body) || /n2b\.in\s*<==\s*sum\b/.test(body);
594
+ const carryFromBits = /carry\s*<==\s*[^;]*n2b\.out/.test(body) || /carry\s*<==\s*[^;]*\.out\[/.test(body);
595
+ const carryFree = /carry\s*<--/.test(body);
596
+ return sumBits && (carryFree || !carryFromBits);
597
+ }
598
+ function jointNum2BitsOnSum(body) {
599
+ return /Num2Bits\s*\(\s*n\s*\+\s*[12]\s*\)/.test(body) && /\.in\s*<==\s*a\s*\+\s*b/.test(body) && /carry\s*<==/.test(body);
600
+ }
601
+ function hasNum2Bits(src) {
602
+ return /Num2Bits\s*\(/.test(src);
603
+ }
604
+ function hasBitConstraint(src) {
605
+ return /\*\s*\(\s*[A-Za-z0-9_\.]+\s*-\s*1\s*\)\s*===\s*0/.test(src) || /Num2Bits\s*\(\s*1\s*\)/.test(src);
606
+ }
607
+ function hasEquationAssign(src) {
608
+ return /<--|===/.test(src);
609
+ }
610
+ function usesBinSum(src) {
611
+ return /BinSum\s*\(/.test(src);
612
+ }
613
+ function usesModSum(src) {
614
+ return /ModSum(?:Three|Four)?\s*\(/.test(src) || /template\s+ModSum/.test(src);
615
+ }
616
+ function usesBigAdd(src) {
617
+ return /BigAdd\s*\(/.test(src) || /template\s+BigAdd/.test(src);
618
+ }
619
+ function usesBigMod(src) {
620
+ return /BigMod\s*\(/.test(src) || /template\s+BigMod/.test(src);
621
+ }
622
+ function looksSha256(src) {
623
+ return /Sha256compression|SigmaPlus|BigSigma|SmallSigma|template\s+T1\b/.test(src);
624
+ }
625
+ function defaultConstraints(partial) {
626
+ return {
627
+ equation: partial.equation ?? true,
628
+ sum_bitified: partial.sum_bitified ?? false,
629
+ carry_bitified: partial.carry_bitified ?? false,
630
+ input_bitified: partial.input_bitified ?? false,
631
+ incoming_bitified: partial.incoming_bitified ?? false
632
+ };
633
+ }
634
+ function classifyCircomSource(src, id = "circuit") {
635
+ const clean = stripComments(src);
636
+ const main2 = detectMain(clean);
637
+ const tpl = main2?.template ?? "";
638
+ const args = main2?.args ?? [];
639
+ const body = tpl && extractTemplateBody(clean, tpl) || clean;
640
+ if (!/\binclude\s*["']/.test(clean) && tpl && extractTemplateBody(clean, tpl)) {
641
+ const compact = body.replace(/\s+/g, "");
642
+ const direct = /* @__PURE__ */ new Map([
643
+ ["signalinputa;signalinputb;signaloutputout;out<==a*b;", "and"],
644
+ ["signalinputa;signalinputb;signalinputs;signaloutputout;out<==(b-a)*s+a;", "mux1"]
645
+ ]);
646
+ const primitive = direct.get(compact);
647
+ if (primitive) {
648
+ return {
649
+ id,
650
+ domain: "circom",
651
+ family: "circom_function",
652
+ params: { primitive },
653
+ constraints: defaultConstraints({ equation: true }),
654
+ source: "circom",
655
+ notes: "Single field output bound by a complete arithmetic assignment"
656
+ };
657
+ }
658
+ const compare4 = "signalinputa;signalinputb;signaloutputout;signaldiff;signalbit[5];diff<==a+16-b;varlc=0;vare2=1;for(vari=0;i<5;i++){bit[i]<--(diff>>i)&1;bit[i]*(bit[i]-1)===0;lc+=bit[i]*e2;e2=e2+e2;}lc===diff;out<==1-bit[4];";
659
+ if (compact === compare4) {
660
+ return {
661
+ id,
662
+ domain: "circom",
663
+ family: "circom_comparison",
664
+ params: { n: 4, range_bits: 5 },
665
+ constraints: defaultConstraints({
666
+ equation: true,
667
+ sum_bitified: true,
668
+ carry_bitified: true
669
+ }),
670
+ source: "circom",
671
+ notes: "Five checked bits give a unique residue and comparison output"
672
+ };
673
+ }
674
+ }
675
+ if (/^ModSumCarryFree$/i.test(tpl)) {
676
+ const n = args[0] ?? 4;
677
+ return { id, family: "modsum", params: { n }, constraints: defaultConstraints({ equation: true, sum_bitified: true, carry_bitified: false }), source: "circom", notes: "sum Num2Bits only" };
678
+ }
679
+ if (/^ModSumBuggy$/i.test(tpl)) {
680
+ const n = args[0] ?? 4;
681
+ return { id, family: "modsum", params: { n }, constraints: defaultConstraints({ equation: true }), source: "circom", notes: "equation-only" };
682
+ }
683
+ if (/^ModSumThreeSafe$/i.test(tpl) || /^ModSumThree$/i.test(tpl)) {
684
+ const n = args[0] ?? 4;
685
+ return { id, family: "modsum3", params: { n }, constraints: defaultConstraints({ equation: true, sum_bitified: true, carry_bitified: true, incoming_bitified: true }), source: "circom" };
686
+ }
687
+ if (/^ModSum$/i.test(tpl) || /^ModSumSafe$/i.test(tpl)) {
688
+ const n = args[0] ?? 4;
689
+ if (num2BitsOnSumOnly(body)) {
690
+ return { id, family: "modsum", params: { n }, constraints: defaultConstraints({ equation: true, sum_bitified: true, carry_bitified: false }), source: "circom" };
691
+ }
692
+ if (/sum\s*<--/.test(body) && !/Num2Bits\s*\(/.test(body)) {
693
+ return { id, family: "modsum", params: { n }, constraints: defaultConstraints({ equation: true }), source: "circom", notes: "equation-only ModSum" };
694
+ }
695
+ const joint = jointNum2BitsOnSum(body) || /Num2Bits\s*\(\s*n\s*\+\s*1\s*\)/.test(body);
696
+ return { id, family: "modsum", params: { n }, constraints: defaultConstraints({ equation: true, sum_bitified: joint, carry_bitified: joint }), source: "circom" };
697
+ }
698
+ if (/^FullAdder$/i.test(tpl)) {
699
+ return {
700
+ id,
701
+ family: "full_adder",
702
+ params: { n: 1 },
703
+ constraints: defaultConstraints({
704
+ equation: true,
705
+ sum_bitified: hasNum2Bits(clean),
706
+ carry_bitified: hasNum2Bits(clean),
707
+ input_bitified: hasBitConstraint(clean),
708
+ incoming_bitified: hasBitConstraint(clean)
709
+ }),
710
+ source: "circom"
711
+ };
712
+ }
713
+ if (/FullAdderLoose/i.test(tpl)) {
714
+ return {
715
+ id,
716
+ family: "full_adder",
717
+ params: { n: 1 },
718
+ constraints: defaultConstraints({ equation: true, sum_bitified: true, carry_bitified: true, input_bitified: false }),
719
+ source: "circom"
720
+ };
721
+ }
722
+ if (/BinSum/i.test(tpl)) {
723
+ return {
724
+ id,
725
+ family: "binsum",
726
+ params: { n: args[0] ?? 2, ops: args[1] ?? 2 },
727
+ constraints: defaultConstraints({ equation: true, sum_bitified: true, carry_bitified: true, input_bitified: true }),
728
+ source: "circom"
729
+ };
730
+ }
731
+ if (/BigAdd/i.test(tpl)) {
732
+ return {
733
+ id,
734
+ family: "limb_add",
735
+ params: { n: args[0] ?? 1, k: args[1] ?? 2 },
736
+ constraints: defaultConstraints({ equation: true, sum_bitified: true, carry_bitified: true }),
737
+ source: "circom",
738
+ notes: "BigAdd via ModSum/ModSumThree"
739
+ };
740
+ }
741
+ if (/BigMod/i.test(tpl)) {
742
+ return {
743
+ id,
744
+ family: "bigint_mod",
745
+ params: { n: args[0] ?? 3, k: args[1] ?? 2 },
746
+ constraints: defaultConstraints({
747
+ equation: true,
748
+ sum_bitified: hasNum2Bits(clean),
749
+ carry_bitified: hasNum2Bits(clean)
750
+ }),
751
+ source: "circom",
752
+ notes: "BigMod with Num2Bits range checks + reconstruction"
753
+ };
754
+ }
755
+ if (/Sha256compression|Sha256/i.test(tpl) || looksSha256(clean)) {
756
+ return {
757
+ id,
758
+ family: "arx_sha256",
759
+ params: { rounds: 64 },
760
+ constraints: defaultConstraints({
761
+ equation: true,
762
+ sum_bitified: usesBinSum(clean) || hasNum2Bits(clean),
763
+ carry_bitified: usesBinSum(clean),
764
+ input_bitified: true
765
+ // circomlib uses bit wires
766
+ }),
767
+ source: "circom",
768
+ notes: "SHA-256 compression / ARX"
769
+ };
770
+ }
771
+ if (/U4AddSafe|LimbAddSafe/i.test(tpl)) {
772
+ const n = /U4/.test(tpl) ? 4 : args[0] ?? 4;
773
+ const k = /U4AddSafe/.test(tpl) ? args[0] ?? 8 : args[1] ?? args[0] ?? 2;
774
+ return {
775
+ id,
776
+ family: "limb_add",
777
+ params: { n, k },
778
+ constraints: defaultConstraints({ equation: true, sum_bitified: true, carry_bitified: true }),
779
+ source: "circom"
780
+ };
781
+ }
782
+ if (/U4AddBuggy|LimbAddBuggy/i.test(tpl)) {
783
+ const n = /U4/.test(tpl) ? 4 : args[0] ?? 4;
784
+ const k = /U4AddBuggy/.test(tpl) ? args[0] ?? 8 : args[1] ?? 2;
785
+ return {
786
+ id,
787
+ family: "limb_add",
788
+ params: { n, k },
789
+ constraints: defaultConstraints({ equation: true, sum_bitified: false, carry_bitified: false }),
790
+ source: "circom"
791
+ };
792
+ }
793
+ if (/SharedInputParallel/i.test(tpl)) {
794
+ return {
795
+ id,
796
+ family: "shared_input",
797
+ params: { width: args[0] ?? 8, offsets: [13, 45, 93] },
798
+ constraints: defaultConstraints({ equation: true, sum_bitified: true, carry_bitified: true, input_bitified: true }),
799
+ source: "circom"
800
+ };
801
+ }
802
+ if (/SharedInputBuggy/i.test(tpl)) {
803
+ return {
804
+ id,
805
+ family: "shared_input",
806
+ params: { width: args[0] ?? 8, offsets: [13, 45, 93] },
807
+ constraints: defaultConstraints({ equation: true }),
808
+ source: "circom"
809
+ };
810
+ }
811
+ if (/^Cascade3$/i.test(tpl)) {
812
+ return {
813
+ id,
814
+ family: "cascade",
815
+ params: { n: args[0] ?? 4, stages: 3 },
816
+ constraints: defaultConstraints({ equation: true, sum_bitified: true, carry_bitified: true, incoming_bitified: true }),
817
+ source: "circom"
818
+ };
819
+ }
820
+ if (/Cascade3Buggy/i.test(tpl)) {
821
+ return {
822
+ id,
823
+ family: "cascade",
824
+ params: { n: args[0] ?? 4, stages: 3 },
825
+ constraints: defaultConstraints({ equation: true }),
826
+ source: "circom"
827
+ };
828
+ }
829
+ if (usesBigMod(clean)) {
830
+ return {
831
+ id,
832
+ family: "bigint_mod",
833
+ params: { n: args[0] ?? 3, k: args[1] ?? 2 },
834
+ constraints: defaultConstraints({ equation: true, sum_bitified: hasNum2Bits(clean), carry_bitified: hasNum2Bits(clean) }),
835
+ source: "circom"
836
+ };
837
+ }
838
+ if (looksSha256(clean)) {
839
+ return {
840
+ id,
841
+ family: "arx_sha256",
842
+ params: { rounds: 64 },
843
+ constraints: defaultConstraints({
844
+ equation: true,
845
+ sum_bitified: usesBinSum(clean),
846
+ carry_bitified: usesBinSum(clean),
847
+ input_bitified: true
848
+ }),
849
+ source: "circom"
850
+ };
851
+ }
852
+ if (usesBigAdd(clean) || usesModSum(clean)) {
853
+ return {
854
+ id,
855
+ family: "limb_add",
856
+ params: { n: args[0] ?? 4, k: args[1] ?? 2 },
857
+ constraints: defaultConstraints({
858
+ equation: true,
859
+ sum_bitified: hasNum2Bits(clean),
860
+ carry_bitified: hasNum2Bits(clean)
861
+ }),
862
+ source: "circom"
863
+ };
864
+ }
865
+ if (usesBinSum(clean)) {
866
+ return {
867
+ id,
868
+ family: "binsum",
869
+ params: { n: args[0] ?? 32, ops: args[1] ?? 2 },
870
+ constraints: defaultConstraints({ equation: true, sum_bitified: true, carry_bitified: true, input_bitified: true }),
871
+ source: "circom"
872
+ };
873
+ }
874
+ if (/<--/.test(clean) && !hasNum2Bits(clean) && /\+\s*.*===/.test(clean)) {
875
+ return {
876
+ id,
877
+ family: "modsum",
878
+ params: { n: 4 },
879
+ constraints: defaultConstraints({ equation: true }),
880
+ source: "circom",
881
+ notes: "heuristic: assignments without Num2Bits"
882
+ };
883
+ }
884
+ return {
885
+ id,
886
+ family: "unknown",
887
+ params: {},
888
+ constraints: defaultConstraints({ equation: hasEquationAssign(clean) }),
889
+ source: "circom",
890
+ notes: `unclassified template=${tpl || "(none)"}`
891
+ };
892
+ }
893
+
894
+ // ../carrycert-core/src/circom-fs.ts
895
+ import * as fs from "node:fs";
896
+ import * as path from "node:path";
897
+ function readWithIncludes(entry, depth = 0, maxDepth = 6, seen = /* @__PURE__ */ new Set()) {
898
+ const abs = path.resolve(entry);
899
+ if (seen.has(abs) || depth > maxDepth) return "";
900
+ seen.add(abs);
901
+ if (!fs.existsSync(abs)) return "";
902
+ let text = fs.readFileSync(abs, "utf8");
903
+ const dir = path.dirname(abs);
904
+ const includeRe = /include\s+"([^"]+)"\s*;/g;
905
+ let m;
906
+ const parts = [text];
907
+ while (m = includeRe.exec(text)) {
908
+ const inc = path.resolve(dir, m[1]);
909
+ parts.push(readWithIncludes(inc, depth + 1, maxDepth, seen));
910
+ }
911
+ return parts.join("\n");
912
+ }
913
+ function classifyCircomFile(filePath, opts = {}) {
914
+ const follow = opts.followIncludes !== false;
915
+ const src = follow ? readWithIncludes(filePath, 0, opts.maxIncludeDepth ?? 8) : fs.readFileSync(filePath, "utf8");
916
+ const id = opts.id ?? path.basename(filePath, path.extname(filePath));
917
+ return classifyCircomSource(src, id);
918
+ }
919
+ function scanCircomFile(filePath, opts = {}) {
920
+ return analyze(classifyCircomFile(filePath, opts));
921
+ }
922
+
923
+ // ../carrycert-core/src/checker.ts
924
+ function checkResult(r) {
925
+ if (r.verdict === "Broken") {
926
+ const cex = r.counterexample ?? {};
927
+ const wa = cex.witness_a;
928
+ const wb = cex.witness_b;
929
+ if (wa?.sum !== void 0 && wb?.sum !== void 0) {
930
+ const n = cex.inputs.n;
931
+ const MOD = 1n << BigInt(n);
932
+ const a = BigInt(cex.inputs.a);
933
+ const b = BigInt(cex.inputs.b);
934
+ const lhs = (a + b) % BN128_PRIME;
935
+ const okA = (BigInt(String(wa.sum)) + BigInt(String(wa.carry)) * MOD) % BN128_PRIME === lhs;
936
+ const okB = (BigInt(String(wb.sum)) + BigInt(String(wb.carry)) * MOD) % BN128_PRIME === lhs;
937
+ const differ = !(String(wa.sum) === String(wb.sum) && String(wa.carry) === String(wb.carry));
938
+ return { ok: okA && okB && differ, kind: "modsum_cex" };
939
+ }
940
+ if (wb?.out) {
941
+ return { ok: Array.isArray(wb.out) && wb.out.some((x) => x !== 0), kind: "limb_cex" };
942
+ }
943
+ if (wa?.y) {
944
+ return { ok: JSON.stringify(wa) !== JSON.stringify(wb), kind: "shared_cex" };
945
+ }
946
+ return { ok: false, kind: "unrecognized_cex" };
947
+ }
948
+ if (r.verdict === "Certified") {
949
+ if (r.method === "r1cs_triangular_uniqueness")
950
+ return { ok: false, kind: "r1cs_source_recheck_required" };
951
+ if (r.method === "circom_direct_arithmetic") {
952
+ const c = r.certificate;
953
+ const expected = c?.primitive === "and" ? "a*b" : c?.primitive === "mux1" ? "(b-a)*s+a" : null;
954
+ return { ok: c?.type === "functional_assignment" && c.expression === expected && r.witness_count === 1, kind: "functional_assignment" };
955
+ }
956
+ if (r.method === "circom_bitified_comparison") {
957
+ const c = r.certificate;
958
+ return { ok: c?.type === "bit_decomposition" && c.n === 4 && c.range_bits === 5 && r.witness_count === 1, kind: "bit_decomposition" };
959
+ }
960
+ return { ok: !!r.certificate, kind: "certificate" };
961
+ }
962
+ return { ok: true, kind: "unknown_skip" };
963
+ }
964
+
965
+ // ../carrycert-core/src/adapters/verilog.ts
966
+ function classifyVerilogSource(src, id = "hw_circuit") {
967
+ const clean = src.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
968
+ const widthM = clean.match(/\[\s*(\d+)\s*:\s*0\s*\]/);
969
+ const width = widthM ? Number(widthM[1]) + 1 : 8;
970
+ const hasFullAdder = /full_adder|FA\b|module\s+fa\b/i.test(clean);
971
+ const hasRipple = /ripple|rca\b|generate.*full_adder|cout\s*\(\s*c\s*\[/i.test(clean) || hasFullAdder;
972
+ const bitIO = /input\s+\[\s*\d+\s*:\s*0\s*\]\s*a/.test(clean) && /output\s+\[\s*\d+\s*:\s*0\s*\]\s*sum/.test(clean);
973
+ const carryChain = /cout|carry/.test(clean);
974
+ const bareAssign = /assign\s+sum\s*=\s*a\s*\+\s*b/.test(clean) && !carryChain;
975
+ if (bareAssign) {
976
+ return {
977
+ id,
978
+ family: "hw_adder",
979
+ domain: "verilog",
980
+ params: { width, style: "bare_assign" },
981
+ constraints: { equation: true, sum_bitified: false, carry_bitified: false, input_bitified: bitIO, incoming_bitified: false },
982
+ notes: "bare a+b assign without explicit carry chain"
983
+ };
984
+ }
985
+ if (hasRipple || hasFullAdder || bitIO && carryChain) {
986
+ return {
987
+ id,
988
+ family: "hw_adder",
989
+ domain: "verilog",
990
+ params: { width, style: hasRipple ? "ripple" : "adder" },
991
+ constraints: {
992
+ equation: true,
993
+ sum_bitified: true,
994
+ carry_bitified: carryChain,
995
+ input_bitified: true,
996
+ incoming_bitified: /cin/.test(clean)
997
+ },
998
+ notes: "structural bit-level adder"
999
+ };
1000
+ }
1001
+ return {
1002
+ id,
1003
+ family: "unknown",
1004
+ domain: "verilog",
1005
+ params: { width },
1006
+ constraints: { equation: false, sum_bitified: false, carry_bitified: false, input_bitified: false, incoming_bitified: false },
1007
+ notes: "unrecognized verilog"
1008
+ };
1009
+ }
1010
+
1011
+ // ../carrycert-core/src/adapters/int8.ts
1012
+ function classifyInt8Kernel(k) {
1013
+ const range = k.range_checked !== false;
1014
+ return {
1015
+ id: k.id,
1016
+ family: "fixed_mac",
1017
+ domain: "int8",
1018
+ params: {
1019
+ bits: k.bits ?? 8,
1020
+ acc_bits: k.acc_bits ?? 32,
1021
+ overflow: k.overflow ?? "wrap"
1022
+ },
1023
+ constraints: {
1024
+ equation: true,
1025
+ sum_bitified: range,
1026
+ carry_bitified: range,
1027
+ input_bitified: range,
1028
+ incoming_bitified: false,
1029
+ extra: { overflow: k.overflow ?? "wrap" }
1030
+ },
1031
+ notes: k.notes
1032
+ };
1033
+ }
1034
+ function classifyInt8Source(src, id = "int8_kernel") {
1035
+ const s = src.toLowerCase();
1036
+ const bitsM = s.match(/int(\d+)/);
1037
+ const accM = s.match(/acc(\d+)/);
1038
+ const range = /norange|no.?range|unchecked/.test(s) ? false : !/unchecked/.test(s);
1039
+ const overflow = /sat/.test(s) ? "saturate" : "wrap";
1040
+ return classifyInt8Kernel({
1041
+ id,
1042
+ bits: bitsM ? Number(bitsM[1]) : 8,
1043
+ acc_bits: accM ? Number(accM[1]) : 32,
1044
+ overflow,
1045
+ range_checked: range && !/norange/.test(s)
1046
+ });
1047
+ }
1048
+
1049
+ // ../carrycert-core/src/adapters/onnx.ts
1050
+ function classifyOnnxKernel(k) {
1051
+ const ir = classifyInt8Kernel({
1052
+ id: k.id,
1053
+ bits: k.bits ?? 8,
1054
+ acc_bits: k.acc_bits ?? 32,
1055
+ overflow: k.overflow ?? "wrap",
1056
+ range_checked: k.range_checked !== false,
1057
+ notes: k.notes ?? `onnx ${k.op ?? "mac"}`
1058
+ });
1059
+ ir.domain = "onnx";
1060
+ ir.params = { ...ir.params, op: k.op ?? "mac" };
1061
+ return ir;
1062
+ }
1063
+ function classifyOnnxSource(src, id = "onnx_kernel") {
1064
+ const s = src.toLowerCase();
1065
+ const bitsM = s.match(/int(\d+)/);
1066
+ const accM = s.match(/acc(\d+)/);
1067
+ const op = /conv/.test(s) ? "conv" : /matmul|gemm/.test(s) ? "matmul" : "mac";
1068
+ const norange = /norange|unchecked|no.?quant/.test(s);
1069
+ const overflow = /sat/.test(s) ? "saturate" : "wrap";
1070
+ return classifyOnnxKernel({
1071
+ id,
1072
+ op,
1073
+ bits: bitsM ? Number(bitsM[1]) : 8,
1074
+ acc_bits: accM ? Number(accM[1]) : 32,
1075
+ range_checked: !norange,
1076
+ overflow,
1077
+ notes: src.trim().slice(0, 120)
1078
+ });
1079
+ }
1080
+
1081
+ // ../carrycert-core/src/adapters/carry-graph.ts
1082
+ function unsupported(id, domain, reason) {
1083
+ return {
1084
+ id,
1085
+ domain,
1086
+ family: "unknown",
1087
+ params: {},
1088
+ constraints: {
1089
+ equation: false,
1090
+ sum_bitified: false,
1091
+ carry_bitified: false,
1092
+ input_bitified: false,
1093
+ incoming_bitified: false
1094
+ },
1095
+ notes: reason
1096
+ };
1097
+ }
1098
+ function classifyCarryGraph(graph, id = "carry_graph") {
1099
+ const n = graph?.limb_bits;
1100
+ const k = graph?.limbs;
1101
+ const domain = graph?.domain || "unknown";
1102
+ if (graph?.format !== "carry-graph-v1" || !["bitvm", "zkvm", "r1cs"].includes(domain) || graph.op !== "add" || !Number.isInteger(n) || n < 1 || n > 32 || !Number.isInteger(k) || k < 1 || k > 64 || !Array.isArray(graph.equations) || !Array.isArray(graph.ranges) || graph.equations.length !== k || graph.ranges.length !== 4 * k)
1103
+ return unsupported(id, domain, "Carry graph requires complete bounded addition constraints");
1104
+ const radix = (1n << BigInt(n)).toString();
1105
+ const requiredEquations = Array.from(
1106
+ { length: k },
1107
+ (_, i) => `a[${i}]+b[${i}]${i ? `+carry[${i - 1}]` : ""}=sum[${i}]+${radix}*carry[${i}]`
1108
+ );
1109
+ const requiredRanges = Array.from({ length: k }, (_, i) => [
1110
+ `a[${i}]:${n}`,
1111
+ `b[${i}]:${n}`,
1112
+ `sum[${i}]:${n}`,
1113
+ `carry[${i}]:1`
1114
+ ]).flat();
1115
+ const normalize = (x) => typeof x === "string" ? x.replace(/\s+/g, "") : "";
1116
+ const eqs = graph.equations.map(normalize);
1117
+ const ranges = graph.ranges.map(normalize);
1118
+ if (new Set(eqs).size !== k || new Set(ranges).size !== 4 * k || requiredEquations.some((e) => !eqs.includes(e)) || requiredRanges.some((r) => !ranges.includes(r)))
1119
+ return unsupported(id, domain, "Unrecognized or incomplete equations/ranges; source contract undecided");
1120
+ return {
1121
+ id,
1122
+ domain,
1123
+ family: "limb_add",
1124
+ params: { n, k, style: "bounded_carry_graph_v1" },
1125
+ constraints: {
1126
+ equation: true,
1127
+ sum_bitified: true,
1128
+ carry_bitified: true,
1129
+ input_bitified: true,
1130
+ incoming_bitified: false,
1131
+ extra: { graph_format: "carry-graph-v1", equations: k, ranges: 4 * k }
1132
+ },
1133
+ notes: "Exact carry-graph constraints. Scope: mathematical graph; source frontend binding required."
1134
+ };
1135
+ }
1136
+
1137
+ // ../carrycert-core/src/scan.ts
1138
+ function unknown(id, domain, detail) {
1139
+ return {
1140
+ id,
1141
+ domain,
1142
+ family: "unknown",
1143
+ params: {},
1144
+ constraints: {
1145
+ equation: false,
1146
+ sum_bitified: false,
1147
+ carry_bitified: false,
1148
+ input_bitified: false,
1149
+ incoming_bitified: false
1150
+ },
1151
+ notes: detail
1152
+ };
1153
+ }
1154
+ function classifySource(text, options = {}) {
1155
+ const id = options.id || options.filename?.replace(/^.*[/\\]/, "").replace(/\.[^.]+$/, "") || "pasted";
1156
+ const fmt = options.format || options.filename?.toLowerCase().split(".").pop();
1157
+ if (text.trimStart().startsWith("{")) {
1158
+ try {
1159
+ const spec = JSON.parse(text);
1160
+ if (spec?.format === "carry-graph-v1") return classifyCarryGraph(spec, id);
1161
+ if (spec?.format === "bitvm-limb-v1" || spec?.format === "zkvm-limb-v1")
1162
+ return unknown(
1163
+ id,
1164
+ spec.format.startsWith("bitvm") ? "bitvm" : "zkvm",
1165
+ "Legacy declaration alone does not bind carry constraints. Supply a carry-graph-v1."
1166
+ );
1167
+ return unknown(id, "declaration", "Unrecognized contract format");
1168
+ } catch {
1169
+ return unknown(id, "declaration", "Invalid JSON state contract");
1170
+ }
1171
+ }
1172
+ if (fmt === "r1cs")
1173
+ return unknown(id, "r1cs", "Binary R1CS needs a decoded constraint graph");
1174
+ if (fmt === "circom" || /pragma\s+circom|template\s+\w+/.test(text))
1175
+ return classifyCircomSource(text, id);
1176
+ if (fmt === "verilog" || fmt === "v" || fmt === "sv" || /module\s+\w+/.test(text))
1177
+ return classifyVerilogSource(text, id);
1178
+ if (fmt === "int8" || fmt === "mac" || /mac\s+int|int8|acc\d+|range_checked/i.test(text))
1179
+ return classifyInt8Source(text, id);
1180
+ if (fmt === "bitvm" || fmt === "script" || /bitvm|script\s+limb|op_add/i.test(text))
1181
+ return unknown(id, "bitvm", "Raw Bitcoin Script/BitVM requires a constraint-graph frontend");
1182
+ if (fmt === "zkvm" || /zkvm|alu\s+limb/i.test(text))
1183
+ return unknown(id, "zkvm", "Raw zkVM trace requires a constraint-graph frontend");
1184
+ if (fmt === "onnx" || /onnx|quantizelinear/i.test(text))
1185
+ return classifyOnnxSource(text, id);
1186
+ return unknown(id, "pasted", "No recognized source adapter");
1187
+ }
1188
+ function scanPasted(source, options = {}) {
1189
+ const ir = classifySource(String(source || ""), options);
1190
+ const result = analyze(ir);
1191
+ if (result.method === "arx_sha256_binsum_bitpath" && !options.trusted_sha256) {
1192
+ return {
1193
+ id: result.id,
1194
+ domain: result.domain,
1195
+ verdict: "Unknown",
1196
+ method: "sha256_source_binding_required",
1197
+ theorem: result.theorem,
1198
+ time_ms: result.time_ms,
1199
+ reason: "SHA-256 source requires a pinned dependency graph and digest",
1200
+ source_note: ir.notes || null,
1201
+ check: { ok: true, kind: "unknown_skip" }
1202
+ };
1203
+ }
1204
+ return {
1205
+ ...result,
1206
+ source_note: ir.notes || null,
1207
+ check: checkResult(result)
1208
+ };
1209
+ }
1210
+
1211
+ // ../carrycert-core/src/adapters/r1cs-binary.ts
1212
+ var MAX_SIZE = 8 * 1024 * 1024;
1213
+ var MAX_WIRES = 65536;
1214
+ var MAX_CONSTRAINTS = 2e5;
1215
+ var MAX_TERMS = 4e5;
1216
+ var Reader = class {
1217
+ constructor(b, start = 0, end = b.length) {
1218
+ this.b = b;
1219
+ this.end = end;
1220
+ this.p = start;
1221
+ }
1222
+ p;
1223
+ require(n) {
1224
+ if (!Number.isSafeInteger(n) || n < 0 || this.p + n > this.end)
1225
+ throw new Error("Truncated or out-of-bounds R1CS section");
1226
+ }
1227
+ u32() {
1228
+ this.require(4);
1229
+ const v = new DataView(this.b.buffer, this.b.byteOffset + this.p, 4).getUint32(0, true);
1230
+ this.p += 4;
1231
+ return v;
1232
+ }
1233
+ u64() {
1234
+ this.require(8);
1235
+ const v = new DataView(this.b.buffer, this.b.byteOffset + this.p, 8).getBigUint64(0, true);
1236
+ this.p += 8;
1237
+ return v;
1238
+ }
1239
+ field(n) {
1240
+ this.require(n);
1241
+ let v = 0n;
1242
+ for (let i = n - 1; i >= 0; i--) v = (v << 8n) + BigInt(this.b[this.p + i]);
1243
+ this.p += n;
1244
+ return v;
1245
+ }
1246
+ done() {
1247
+ if (this.p !== this.end) throw new Error("Trailing bytes in R1CS section");
1248
+ }
1249
+ };
1250
+ function linear(r, nWires, fieldSize) {
1251
+ const count = r.u32();
1252
+ if (count > nWires || count > MAX_TERMS || count > Math.floor((r.end - r.p) / (fieldSize + 4)))
1253
+ throw new Error("Invalid R1CS linear-combination length");
1254
+ const result = [];
1255
+ let prior = -1;
1256
+ for (let i = 0; i < count; i++) {
1257
+ const wire = r.u32();
1258
+ const coeff = r.field(fieldSize);
1259
+ if (wire >= nWires || wire <= prior || coeff === 0n || coeff >= BN128_PRIME)
1260
+ throw new Error("Noncanonical R1CS term");
1261
+ prior = wire;
1262
+ result.push({ wire, coeff });
1263
+ }
1264
+ return result;
1265
+ }
1266
+ function parseR1CSBinary(bytes) {
1267
+ if (bytes.byteLength > MAX_SIZE) throw new Error("R1CS exceeds 8 MiB parser limit");
1268
+ const r = new Reader(bytes);
1269
+ r.require(12);
1270
+ if (String.fromCharCode(...bytes.subarray(0, 4)) !== "r1cs")
1271
+ throw new Error("Invalid R1CS magic");
1272
+ r.p = 4;
1273
+ if (r.u32() !== 1) throw new Error("Unsupported R1CS version");
1274
+ const count = r.u32();
1275
+ if (count < 2 || count > 32) throw new Error("Invalid R1CS section count");
1276
+ const sections = /* @__PURE__ */ new Map();
1277
+ for (let i = 0; i < count; i++) {
1278
+ const kind = r.u32(), size = r.u64();
1279
+ if (size > BigInt(r.end - r.p)) throw new Error("Invalid R1CS section length");
1280
+ const n = Number(size);
1281
+ if (sections.has(kind)) throw new Error("Duplicate R1CS section");
1282
+ sections.set(kind, new Reader(bytes, r.p, r.p + n));
1283
+ r.p += n;
1284
+ }
1285
+ r.done();
1286
+ if (sections.has(4) || sections.has(5)) throw new Error("R1CS custom gates are outside this parser scope");
1287
+ const h = sections.get(1), d = sections.get(2), w = sections.get(3);
1288
+ if (!h || !d || !w) throw new Error("Missing required R1CS section");
1289
+ const fieldSize = h.u32();
1290
+ if (fieldSize !== 32 || h.field(fieldSize) !== BN128_PRIME)
1291
+ throw new Error("Unsupported R1CS prime field");
1292
+ const nWires = h.u32(), nPubOut = h.u32(), nPubIn = h.u32(), nPrvIn = h.u32();
1293
+ const labelCountBig = h.u64(), nConstraints = h.u32();
1294
+ h.done();
1295
+ if (!nWires || nWires > MAX_WIRES || nConstraints > MAX_CONSTRAINTS || nPubOut + nPubIn + nPrvIn + 1 > nWires || labelCountBig > BigInt(Number.MAX_SAFE_INTEGER))
1296
+ throw new Error("Invalid R1CS dimensions");
1297
+ if (w.end - w.p !== nWires * 8) throw new Error("Invalid R1CS label map size");
1298
+ const labels = /* @__PURE__ */ new Set();
1299
+ for (let i = 0; i < nWires; i++) {
1300
+ const label = w.u64();
1301
+ if (label >= labelCountBig || labels.has(label) || i === 0 && label !== 0n)
1302
+ throw new Error("Invalid R1CS wire-to-label map");
1303
+ labels.add(label);
1304
+ }
1305
+ w.done();
1306
+ const constraints = [];
1307
+ let totalTerms = 0;
1308
+ for (let i = 0; i < nConstraints; i++) {
1309
+ const A = linear(d, nWires, fieldSize);
1310
+ const B = linear(d, nWires, fieldSize);
1311
+ const C = linear(d, nWires, fieldSize);
1312
+ totalTerms += A.length + B.length + C.length;
1313
+ if (totalTerms > MAX_TERMS) throw new Error("Too many R1CS terms");
1314
+ constraints.push({ A, B, C });
1315
+ }
1316
+ d.done();
1317
+ return {
1318
+ prime: BN128_PRIME,
1319
+ nWires,
1320
+ nPubOut,
1321
+ nPubIn,
1322
+ nPrvIn,
1323
+ nConstraints,
1324
+ constraints,
1325
+ labelCount: Number(labelCountBig)
1326
+ };
1327
+ }
1328
+ var mod = (n) => (n % BN128_PRIME + BN128_PRIME) % BN128_PRIME;
1329
+ function unknownTerms(lc, known) {
1330
+ return lc.filter((t) => !known.has(t.wire));
1331
+ }
1332
+ function fixedConstant(lc) {
1333
+ return lc.every((t) => t.wire === 0) ? mod(lc.reduce((v, t) => v + t.coeff, 0n)) : null;
1334
+ }
1335
+ function proveR1CSUniqueness(parsed) {
1336
+ const known = /* @__PURE__ */ new Set([0]);
1337
+ const firstInput = 1 + parsed.nPubOut;
1338
+ const inputEnd = firstInput + parsed.nPubIn + parsed.nPrvIn;
1339
+ for (let j = firstInput; j < inputEnd; j++) known.add(j);
1340
+ const pivots = [];
1341
+ let changed = true;
1342
+ while (changed) {
1343
+ changed = false;
1344
+ for (let i = 0; i < parsed.constraints.length; i++) {
1345
+ const { A, B, C } = parsed.constraints[i];
1346
+ const a = unknownTerms(A, known), b = unknownTerms(B, known), c = unknownTerms(C, known);
1347
+ let candidate = null;
1348
+ if (!a.length && !b.length && c.length === 1) candidate = c[0].wire;
1349
+ else {
1350
+ const ca = fixedConstant(A), cb = fixedConstant(B);
1351
+ if (ca !== null && a.length === 0 && ca !== 0n) {
1352
+ const coeffs = /* @__PURE__ */ new Map();
1353
+ for (const t of b) coeffs.set(t.wire, mod((coeffs.get(t.wire) || 0n) + ca * t.coeff));
1354
+ for (const t of c) coeffs.set(t.wire, mod((coeffs.get(t.wire) || 0n) - t.coeff));
1355
+ const nonzero = [...coeffs].filter(([, v]) => v !== 0n);
1356
+ if (nonzero.length === 1) candidate = nonzero[0][0];
1357
+ } else if (cb !== null && b.length === 0 && cb !== 0n) {
1358
+ const coeffs = /* @__PURE__ */ new Map();
1359
+ for (const t of a) coeffs.set(t.wire, mod((coeffs.get(t.wire) || 0n) + cb * t.coeff));
1360
+ for (const t of c) coeffs.set(t.wire, mod((coeffs.get(t.wire) || 0n) - t.coeff));
1361
+ const nonzero = [...coeffs].filter(([, v]) => v !== 0n);
1362
+ if (nonzero.length === 1) candidate = nonzero[0][0];
1363
+ }
1364
+ }
1365
+ if (candidate !== null && !known.has(candidate)) {
1366
+ known.add(candidate);
1367
+ pivots.push({ constraint: i, wire: candidate });
1368
+ changed = true;
1369
+ }
1370
+ }
1371
+ }
1372
+ const free = Array.from({ length: parsed.nWires }, (_, i) => i).filter((i) => !known.has(i));
1373
+ return { complete: !free.length, pivots, free };
1374
+ }
1375
+ function scanR1CSBinary(bytes, id = "binary_r1cs") {
1376
+ const t0 = performance.now();
1377
+ try {
1378
+ const parsed = parseR1CSBinary(bytes);
1379
+ const proof = proveR1CSUniqueness(parsed);
1380
+ if (!proof.complete) return {
1381
+ id,
1382
+ domain: "r1cs",
1383
+ verdict: "Unknown",
1384
+ method: "r1cs_constraints_undecided",
1385
+ theorem: "n/a",
1386
+ time_ms: performance.now() - t0,
1387
+ reason: proof.free.length + " unbound wires",
1388
+ source_note: "R1CS parsed; remaining wire uniqueness not established",
1389
+ check: { ok: true, kind: "unknown_skip" }
1390
+ };
1391
+ const certificate = {
1392
+ type: "r1cs_triangular_uniqueness",
1393
+ field: "BN254",
1394
+ n_wires: parsed.nWires,
1395
+ n_constraints: parsed.nConstraints,
1396
+ public_inputs: parsed.nPubIn,
1397
+ private_inputs: parsed.nPrvIn,
1398
+ public_outputs: parsed.nPubOut,
1399
+ pivots: proof.pivots,
1400
+ scope: "At most one full wire assignment for each fixed public AND private input tuple; existence not asserted"
1401
+ };
1402
+ return {
1403
+ id,
1404
+ domain: "r1cs",
1405
+ verdict: "Certified",
1406
+ witness_count: "\u22641",
1407
+ method: "r1cs_triangular_uniqueness",
1408
+ theorem: "nonzero constant pivots determine all non-input wires",
1409
+ time_ms: performance.now() - t0,
1410
+ certificate,
1411
+ source_note: certificate.scope,
1412
+ check: { ok: true, kind: "r1cs_triangular_uniqueness" }
1413
+ };
1414
+ } catch (err) {
1415
+ return {
1416
+ id,
1417
+ domain: "r1cs",
1418
+ verdict: "Unknown",
1419
+ method: "r1cs_parse_rejected",
1420
+ theorem: "n/a",
1421
+ time_ms: performance.now() - t0,
1422
+ reason: err instanceof Error ? err.message : String(err),
1423
+ source_note: "Binary R1CS format rejected",
1424
+ check: { ok: true, kind: "unknown_skip" }
1425
+ };
1426
+ }
1427
+ }
1428
+
1429
+ // src/cli.ts
1430
+ function usage() {
1431
+ console.log(`\u1455. \u2014 exact arithmetic circuit verification by state contraction
1432
+
1433
+ Usage:
1434
+ speedrunpapi scan <file> [--json] [--sarif out.sarif] [--api <url>] [--key <key>]
1435
+ speedrunpapi classify <file>
1436
+ speedrunpapi verify <result.json> [--source circuit.r1cs]
1437
+ speedrunpapi claim <order> <confirmation> <email>
1438
+ speedrunpapi usage [--key <key>]
1439
+ speedrunpapi version
1440
+
1441
+ File types: .r1cs (binary) | .circom | .v/.sv | .txt/.mac (int8) | .bitvm/.script | .zkvm | .json (carry graph) | .onnx.txt
1442
+ Compute is local. Optional source-bound API signature uses one certificate credit.
1443
+ `);
1444
+ }
1445
+ function scanAny(file) {
1446
+ const abs = path2.resolve(file);
1447
+ const id = path2.basename(file).replace(/\.[^.]+$/, "");
1448
+ if (path2.extname(file).toLowerCase() === ".r1cs")
1449
+ return scanR1CSBinary(new Uint8Array(fs2.readFileSync(abs)), id);
1450
+ if (path2.extname(file).toLowerCase() === ".circom")
1451
+ return scanCircomFile(abs, { followIncludes: true });
1452
+ return scanPasted(fs2.readFileSync(abs, "utf8"), { id, filename: file });
1453
+ }
1454
+ function classifyAny(file) {
1455
+ const abs = path2.resolve(file);
1456
+ const id = path2.basename(file).replace(/\.[^.]+$/, "");
1457
+ if (path2.extname(file).toLowerCase() === ".r1cs")
1458
+ return scanR1CSBinary(new Uint8Array(fs2.readFileSync(abs)), id);
1459
+ if (path2.extname(file).toLowerCase() === ".circom")
1460
+ return classifyCircomFile(abs, { followIncludes: true });
1461
+ return classifySource(fs2.readFileSync(abs, "utf8"), { id, filename: file });
1462
+ }
1463
+ async function maybeRegister(api, key, result, file) {
1464
+ const bytes = fs2.readFileSync(path2.resolve(file));
1465
+ const binary = path2.extname(file).toLowerCase() === ".r1cs";
1466
+ const body = {
1467
+ ...binary ? { source_base64: bytes.toString("base64"), format: "r1cs" } : { source: bytes.toString("utf8") },
1468
+ filename: path2.basename(file),
1469
+ result: result.verdict,
1470
+ family: result.certificate?.family || result.method || "unknown",
1471
+ domain: result.domain || "circom",
1472
+ scope: result.certificate?.lemma || result.theorem || result.method,
1473
+ gadget_id: result.id,
1474
+ checksum: result.source_checksum,
1475
+ public: process.env.COLONY_CERT_PUBLIC === "true"
1476
+ };
1477
+ const res = await fetch(`${api.replace(/\/$/, "")}/v1/cert`, {
1478
+ method: "POST",
1479
+ headers: { "content-type": "application/json", authorization: `Bearer ${key}` },
1480
+ body: JSON.stringify(body)
1481
+ });
1482
+ if (!res.ok) throw new Error(`API ${res.status}: ${await res.text()}`);
1483
+ return res.json();
1484
+ }
1485
+ function toSarif(file, result) {
1486
+ return {
1487
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
1488
+ version: "2.1.0",
1489
+ runs: [{
1490
+ tool: { driver: {
1491
+ name: "exactlab",
1492
+ version: "0.1.0",
1493
+ rules: [{ id: "exact-broken", shortDescription: { text: "Multiple witnesses / underconstrained" } }]
1494
+ } },
1495
+ results: result.verdict === "Broken" ? [{
1496
+ ruleId: "exact-broken",
1497
+ level: "error",
1498
+ message: { text: `${result.id}: ${result.reason ?? result.method}` },
1499
+ locations: [{ physicalLocation: { artifactLocation: { uri: file } } }]
1500
+ }] : []
1501
+ }]
1502
+ };
1503
+ }
1504
+ async function main() {
1505
+ const args = process.argv.slice(2);
1506
+ const cmd = args[0];
1507
+ if (!cmd || cmd === "-h" || cmd === "--help") return usage();
1508
+ if (cmd === "version") return void console.log("\u1455. speedrunpapi 0.3.2");
1509
+ if (cmd === "claim") {
1510
+ if (!args[1] || !args[2] || !args[3]) return usage();
1511
+ const order = args[1].startsWith("#") ? args[1] : "#" + args[1];
1512
+ const api = (process.env.COLONY_API || "https://api.colony.technology").replace(/\/$/, "");
1513
+ const res = await fetch(api + "/v1/claim", {
1514
+ method: "POST",
1515
+ headers: { "content-type": "application/json" },
1516
+ body: JSON.stringify({ order_number: order, confirmation_number: args[2], email: args[3] })
1517
+ });
1518
+ const body = await res.json();
1519
+ if (!res.ok) throw new Error("Claim " + res.status + ": " + (body.error || "failed"));
1520
+ console.log(JSON.stringify({ api_key: body.api_key, balance: body.balance, order }, null, 2));
1521
+ return;
1522
+ }
1523
+ if (cmd === "usage") {
1524
+ const idx = args.indexOf("--key");
1525
+ const token = idx >= 0 ? args[idx + 1] : process.env.COLONY_API_KEY || process.env.EXACTLAB_API_KEY;
1526
+ if (!token) throw new Error("Set COLONY_API_KEY or use --key");
1527
+ const api = (process.env.COLONY_API || "https://api.colony.technology").replace(/\/$/, "");
1528
+ const res = await fetch(api + "/v1/usage", { headers: { authorization: "Bearer " + token } });
1529
+ const body = await res.json();
1530
+ if (!res.ok) throw new Error("Usage " + res.status + ": " + (body.error || "failed"));
1531
+ console.log(JSON.stringify(body, null, 2));
1532
+ return;
1533
+ }
1534
+ if (cmd === "classify") {
1535
+ console.log(JSON.stringify(classifyAny(args[1]), null, 2));
1536
+ return;
1537
+ }
1538
+ if (cmd === "verify") {
1539
+ const results = JSON.parse(fs2.readFileSync(args[1], "utf8"));
1540
+ const arr = Array.isArray(results) ? results : [results];
1541
+ let bad = 0;
1542
+ for (const x of arr) {
1543
+ let c = checkResult(x);
1544
+ if (x.method === "r1cs_triangular_uniqueness") {
1545
+ const sourceIdx = args.indexOf("--source");
1546
+ const sourcePath = sourceIdx >= 0 ? args[sourceIdx + 1] : void 0;
1547
+ if (sourcePath && path2.extname(sourcePath).toLowerCase() === ".r1cs") {
1548
+ const bytes = fs2.readFileSync(path2.resolve(sourcePath));
1549
+ const hash = "sha256:" + createHash("sha256").update(bytes).digest("hex");
1550
+ const fresh = scanR1CSBinary(new Uint8Array(bytes), x.id);
1551
+ c = {
1552
+ ok: x.source_checksum === hash && fresh.verdict === "Certified" && fresh.method === x.method && x.witness_count === "\u22641" && JSON.stringify(fresh.certificate) === JSON.stringify(x.certificate),
1553
+ kind: "r1cs_source_recheck"
1554
+ };
1555
+ }
1556
+ }
1557
+ console.log(`${c.ok ? "PASS" : "FAIL"} ${x.id} ${x.verdict} ${c.kind}`);
1558
+ if (!c.ok) bad++;
1559
+ }
1560
+ process.exit(bad ? 1 : 0);
1561
+ }
1562
+ if (cmd === "scan") {
1563
+ const file = args[1];
1564
+ if (!file) return usage();
1565
+ const result = scanAny(file);
1566
+ result.source_checksum = "sha256:" + createHash("sha256").update(fs2.readFileSync(path2.resolve(file))).digest("hex");
1567
+ const sarifIdx = args.indexOf("--sarif");
1568
+ if (sarifIdx >= 0) fs2.writeFileSync(args[sarifIdx + 1], JSON.stringify(toSarif(file, result), null, 2));
1569
+ const apiIdx = args.indexOf("--api");
1570
+ const keyIdx = args.indexOf("--key");
1571
+ const api = apiIdx >= 0 ? args[apiIdx + 1] : process.env.EXACTLAB_API || process.env.COLONY_API || "https://api.colony.technology";
1572
+ const key = keyIdx >= 0 ? args[keyIdx + 1] : process.env.EXACTLAB_API_KEY || process.env.COLONY_API_KEY;
1573
+ if (key && result.verdict !== "Unknown") {
1574
+ const reg = await maybeRegister(api, key, result, file);
1575
+ result.certificate_id = reg.certificate?.id || reg.id;
1576
+ result.mark = reg.certificate?.mark;
1577
+ result.signed = Boolean(reg.certificate?.signature);
1578
+ result.remaining = reg.remaining;
1579
+ }
1580
+ if (args.includes("--json")) console.log(JSON.stringify(result, null, 2));
1581
+ else {
1582
+ console.log(`${result.verdict} ${result.id}`);
1583
+ console.log(` method: ${result.method}`);
1584
+ console.log(` theorem: ${result.theorem}`);
1585
+ console.log(` domain: ${result.domain ?? "circom/ir"}`);
1586
+ console.log(` time: ${result.time_ms.toFixed(3)} ms (client-side)`);
1587
+ if (result.reason) console.log(` reason: ${result.reason}`);
1588
+ if (result.certificate_id) console.log(` cert: ${result.certificate_id}${result.mark ? ` ${result.mark}` : ""}`);
1589
+ }
1590
+ process.exit(result.verdict === "Broken" ? 2 : result.verdict === "Unknown" ? 3 : 0);
1591
+ }
1592
+ usage();
1593
+ process.exit(1);
1594
+ }
1595
+ main().catch((e) => {
1596
+ console.error(e);
1597
+ process.exit(1);
1598
+ });