alkahest-cas 3.12.0

High-performance computer algebra kernel: symbolic expressions, polynomials, Gröbner bases, JIT, and Arb ball arithmetic.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
//! Central registry of all stable diagnostic codes for alkahest-core.
//!
//! Every code returned by an `AlkahestError::code()` implementation must appear
//! in `REGISTRY`.  Tests below assert no duplicates and ascending order within
//! each prefix.

/// Root cause of an error — informs remediation style, not type dispatch.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Cause {
    UserInput,
    Domain,
    Unsupported,
    Resource,
    Internal,
}

pub struct ErrorSpec {
    pub code: &'static str,
    pub class: &'static str,
    pub cause: Cause,
    pub remediation: Option<&'static str>,
}

pub const REGISTRY: &[ErrorSpec] = &[
    // E-POLY — ConversionError
    ErrorSpec { code: "E-POLY-001", class: "ConversionError", cause: Cause::UserInput,   remediation: Some("remove the unsupported symbol, or declare it as a parameter") },
    ErrorSpec { code: "E-POLY-002", class: "ConversionError", cause: Cause::UserInput,   remediation: Some("all coefficients must be rational integers; rationalize or substitute") },
    ErrorSpec { code: "E-POLY-003", class: "ConversionError", cause: Cause::UserInput,   remediation: Some("only non-negative integer exponents are supported in poly_normal") },
    ErrorSpec { code: "E-POLY-004", class: "ConversionError", cause: Cause::UserInput,   remediation: Some("reduce the degree (e.g. substitute t = x^k), or keep the expression symbolic") },
    ErrorSpec { code: "E-POLY-005", class: "ConversionError", cause: Cause::UserInput,   remediation: Some("substitute a concrete integer for the exponent before calling poly_normal") },
    ErrorSpec { code: "E-POLY-006", class: "ConversionError", cause: Cause::Unsupported, remediation: Some("only polynomial expressions are supported; remove transcendental functions") },
    ErrorSpec { code: "E-POLY-007", class: "ConversionError", cause: Cause::Domain,      remediation: Some("ensure the denominator is non-zero before converting") },
    ErrorSpec { code: "E-POLY-008", class: "FactorError", cause: Cause::UserInput,   remediation: Some("factorization is only defined for non-zero polynomials") },
    ErrorSpec { code: "E-POLY-009", class: "FactorError", cause: Cause::UserInput,   remediation: Some("use a prime modulus p ≥ 2 that fits in a machine word (FLINT nmod); ℤ/nℤ for composite n is not a field") },
    ErrorSpec { code: "E-POLY-010", class: "FactorError", cause: Cause::Internal,    remediation: Some("report the polynomial as a minimal failing example") },
    // E-DIFF — DiffError (symbolic + forward-mode)
    ErrorSpec { code: "E-DIFF-001", class: "DiffError", cause: Cause::Unsupported, remediation: Some("register the function in PrimitiveRegistry, or use diff_forward with a custom rule") },
    ErrorSpec { code: "E-DIFF-002", class: "DiffError", cause: Cause::UserInput,   remediation: Some("symbolic exponents require the chain rule; use diff_forward for non-integer powers") },
    ErrorSpec { code: "E-DIFF-003", class: "DiffError", cause: Cause::Unsupported, remediation: Some("register the function in PrimitiveRegistry with diff_forward implemented") },
    ErrorSpec { code: "E-DIFF-004", class: "DiffError", cause: Cause::UserInput,   remediation: Some("substitute concrete values first; diff_forward requires integer exponents") },
    // E-SIMPLIFY — AssumptionError
    ErrorSpec { code: "E-SIMPLIFY-001", class: "AssumptionError", cause: Cause::Domain, remediation: Some("remove the conflicting refinement or create a separate AssumptionContext") },
    // E-SERIES — SeriesError (V2-15 truncated expansions)
    ErrorSpec { code: "E-SERIES-001", class: "SeriesError", cause: Cause::Unsupported, remediation: Some("ensure all functions are registered primitives with differentiation rules") },
    ErrorSpec { code: "E-SERIES-002", class: "SeriesError", cause: Cause::UserInput,   remediation: Some("pass order >= 1 (exclusive truncation degree in x)") },
    // A `series` call that ran past its work ceiling (or an active budget) before
    // reaching the requested order. Refusing is the point: coefficients are formed by
    // repeated differentiation without re-simplifying, so a nested radical grows by a
    // constant factor per coefficient, and returning the prefix under the requested
    // `O(h^order)` label would understate the remainder rather than admit the miss.
    // Carried out of band on `SeriesError::InvalidOrder` (exhaustive public enum) —
    // see `calculus::series::take_series_refusal`.
    ErrorSpec { code: "E-SERIES-003", class: "SeriesRefusal", cause: Cause::Resource,  remediation: Some("ask for a lower order, raise the budget, or rewrite the expression so its repeated derivatives close") },
    // A coefficient came out as an indeterminate form rather than a number: `0/0` for a
    // removable singularity the engine could not cancel, `1/0` for a branch point,
    // `log(0)`, `exp(0^-1)` for an essential one. Returning the series anyway is the
    // silent-wrong-answer shape this registry exists to prevent — it reports success,
    // cannot be evaluated, and turns into NaN the moment anyone tries. Carried out of
    // band on `SeriesError::InvalidOrder` alongside E-SERIES-003; see
    // `calculus::series::take_series_refusal` and `SeriesRefusal::cause`.
    ErrorSpec { code: "E-SERIES-004", class: "SeriesRefusal", cause: Cause::Domain,    remediation: Some("the expansion point is a branch point, an essential singularity, or a removable one this engine cannot cancel: cancel the singular factor by hand, expand about a nearby regular point, or use `limit` for the single value you need") },
    // `calculus::puiseux` — the fractional-exponent expansion `series` refuses with
    // E-SERIES-004. Same subsystem, same Python exception class: this is the series
    // engine widened, not a new one, and reusing the prefix keeps a caller's
    // `except SeriesError` covering both.
    //
    // 005: the expression has no Puiseux expansion at the point at all — a logarithm
    // of something vanishing (`√x·log x` needs a Puiseux-log/transseries type that
    // does not exist here), an essential singularity, or a ramification index finer
    // than the returned expansion could be *checked* at.
    ErrorSpec { code: "E-SERIES-005", class: "PuiseuxError", cause: Cause::Domain,      remediation: Some("a logarithm of a vanishing quantity (`sqrt(x)*log x`) needs a Puiseux-log/transseries type this engine does not have, and an essential singularity has no truncation exponent at all: factor the log term out by hand, or expand about a nearby regular point. A ramification index past the verifiable range is the third reading — a Python `Fraction` exponent reaches the kernel through `f64` and is not the fraction you wrote; build it with `pow_expr(pool.rational(p, q))`") },
    // 006: an expansion was computed and then **withheld** because the verifier could
    // not confirm it. Distinct from 005 on purpose — 005 says "there is nothing to
    // return", 006 says "there was something and it is not trustworthy". Returning it
    // with a caveat is not an option: a caller cannot tell a checked expansion from an
    // unchecked one once it is in their hands.
    ErrorSpec { code: "E-SERIES-006", class: "PuiseuxError", cause: Cause::Unsupported, remediation: Some("ask for a lower order, or expand about a point where the function is real and its heads have numeric kernels; an unverified expansion is withheld rather than returned") },
    // E-INT — IntegrationError
    ErrorSpec { code: "E-INT-001", class: "IntegrationError", cause: Cause::Unsupported, remediation: Some("use a numeric integrator for arbitrary functions") },
    ErrorSpec { code: "E-INT-002", class: "IntegrationError", cause: Cause::Domain,      remediation: None },
    ErrorSpec { code: "E-INT-003", class: "IntegrationError", cause: Cause::Unsupported, remediation: Some("v1.1 supports sqrt(P(x)) only; higher-degree radicals planned for v2.0") },
    ErrorSpec { code: "E-INT-004", class: "IntegrationError", cause: Cause::Domain,      remediation: Some("no elementary antiderivative exists; use a numeric integrator or elliptic-integral library") },
    // E-MAT — MatrixError
    ErrorSpec { code: "E-MAT-001", class: "MatrixError", cause: Cause::UserInput, remediation: Some("check that row/column counts agree") },
    ErrorSpec { code: "E-MAT-002", class: "MatrixError", cause: Cause::UserInput, remediation: Some("use pseudo-inverse for rectangular matrices") },
    ErrorSpec { code: "E-MAT-003", class: "MatrixError", cause: Cause::Domain,    remediation: Some("check for linear dependence in the rows/columns") },
    ErrorSpec { code: "E-MAT-004", class: "MatrixError", cause: Cause::Unsupported, remediation: Some("rewrite the entries into a form whose determinant's vanishing is decidable, or substitute concrete values") },
    // V2-17 — EigenError
    ErrorSpec { code: "E-EIGEN-001", class: "EigenError", cause: Cause::UserInput,   remediation: Some("pass a square n×n matrix") },
    ErrorSpec { code: "E-EIGEN-002", class: "EigenError", cause: Cause::UserInput,   remediation: Some("ensure det(λI−M) is a ℤ-polynomial in the fresh λ variable") },
    ErrorSpec { code: "E-EIGEN-003", class: "EigenError", cause: Cause::Internal,    remediation: Some("report the polynomial as a minimal failing example") },
    ErrorSpec { code: "E-EIGEN-004", class: "EigenError", cause: Cause::Unsupported, remediation: Some("irreducible characteristic factors of degree > 2 require a future algebraic-number extension") },
    ErrorSpec { code: "E-EIGEN-005", class: "EigenError", cause: Cause::Domain,    remediation: Some("the matrix is defective or the eigenbasis is incomplete") },
    ErrorSpec { code: "E-EIGEN-006", class: "EigenError", cause: Cause::Unsupported, remediation: Some("nullspace elimination failed; try a purely rational or ℚ(i) spectrum") },
    ErrorSpec { code: "E-EIGEN-007", class: "EigenError", cause: Cause::Domain,    remediation: Some("eigenvector matrix is singular; check multiplicities") },
    ErrorSpec { code: "E-EIGEN-008", class: "SpectrumRefusal", cause: Cause::Domain,    remediation: Some("substitute concrete numbers for any symbolic entries, or use real_roots / numeric eigenvalues for this matrix") },
    // E-LINALG — LinearAlgebraError (symbolic LA coverage)
    ErrorSpec { code: "E-LINALG-001", class: "LinearAlgebraError", cause: Cause::UserInput,   remediation: Some("pass a square n×n matrix") },
    ErrorSpec { code: "E-LINALG-002", class: "LinearAlgebraError", cause: Cause::Unsupported, remediation: Some("nullspace elimination failed; try rational entries") },
    ErrorSpec { code: "E-LINALG-003", class: "LinearAlgebraError", cause: Cause::Domain,      remediation: Some("Cholesky requires symmetric positive definite input") },
    ErrorSpec { code: "E-LINALG-004", class: "LinearAlgebraError", cause: Cause::UserInput,   remediation: Some("ensure det(λI−M) is a polynomial in λ") },
    ErrorSpec { code: "E-LINALG-005", class: "LinearAlgebraError", cause: Cause::Internal,    remediation: None },
    ErrorSpec { code: "E-LINALG-006", class: "LinearAlgebraError", cause: Cause::Unsupported, remediation: Some("irreducible factor of degree > 2 in minimal polynomial") },
    ErrorSpec { code: "E-LINALG-007", class: "LinearAlgebraError", cause: Cause::Unsupported, remediation: Some("use rational entries for Smith-based decompositions") },
    ErrorSpec { code: "E-LINALG-008", class: "LinearAlgebraError", cause: Cause::Domain,      remediation: Some("similarity transform matrix is singular") },
    ErrorSpec { code: "E-LINALG-009", class: "LinearAlgebraError", cause: Cause::UserInput,   remediation: Some("matrix entries must be rational constants") },
    // Zero-testing over a transcendental extension is not decidable in general, so
    // elimination can reach an entry it can neither prove zero nor prove non-zero.
    // Refusing is the point: treating "unknown" as "non-zero" is what produced a
    // confident wrong rank (and a false inconsistency signature) before this code existed.
    // Raised through `EigenError` too (`eigenvects`), which shares the elimination it
    // refuses in; the class below names where the code is defined, not every route.
    ErrorSpec { code: "E-LINALG-010", class: "LinearAlgebraError", cause: Cause::Unsupported, remediation: Some("rewrite the entry into a form whose vanishing is decidable, or substitute concrete values for the parameters") },
    // `matrix_exponential` refusing rather than returning an e^A it cannot stand behind:
    // an eigenvalue list nothing confirmed is the spectrum, an eigenvalue gap that is
    // neither provably zero nor settled non-zero (the confluent and generic branches
    // disagree and only one is right), or a candidate that failed the standing
    // scaling-and-squaring check. Raised through `LinearAlgebraError::UnsupportedField`;
    // the class below names where the code is defined, not the carrier variant.
    ErrorSpec { code: "E-LINALG-011", class: "MatrixExpRefusal", cause: Cause::Unsupported, remediation: Some("substitute concrete numbers for any symbolic entries, or state the assumption that makes the eigenvalue gap non-zero") },
    // E-ODE — OdeError
    ErrorSpec { code: "E-ODE-001", class: "OdeError", cause: Cause::UserInput,   remediation: Some("number of state variables must equal number of RHS expressions") },
    ErrorSpec { code: "E-ODE-002", class: "OdeError", cause: Cause::UserInput,   remediation: Some("use lower_to_first_order() before passing to a solver") },
    ErrorSpec { code: "E-ODE-003", class: "OdeError", cause: Cause::Unsupported, remediation: Some("check differentiability of all functions in the system") },
    ErrorSpec { code: "E-ODE-010", class: "DsolveError", cause: Cause::Unsupported, remediation: None },
    ErrorSpec { code: "E-ODE-011", class: "DsolveError", cause: Cause::UserInput, remediation: None },
    ErrorSpec { code: "E-ODE-012", class: "DsolveError", cause: Cause::UserInput, remediation: None },
    ErrorSpec { code: "E-ODE-013", class: "DsolveError", cause: Cause::Unsupported, remediation: Some("the ODE class was recognised but the quadrature it needs is not elementary for this integrator") },
    ErrorSpec { code: "E-ODE-014", class: "DsolveError", cause: Cause::Unsupported, remediation: Some("supply a particular solution; no general-Riccati closed form is attempted") },
    ErrorSpec { code: "E-ODE-020", class: "NumericOdeError", cause: Cause::UserInput, remediation: None },
    ErrorSpec { code: "E-ODE-021", class: "NumericOdeError", cause: Cause::Resource, remediation: None },
    ErrorSpec { code: "E-ODE-022", class: "NumericOdeError", cause: Cause::Resource, remediation: None },
    ErrorSpec { code: "E-ODE-023", class: "NumericOdeError", cause: Cause::UserInput, remediation: None },
    ErrorSpec { code: "E-ODE-024", class: "NumericOdeError", cause: Cause::Unsupported, remediation: None },
    ErrorSpec { code: "E-ODE-025", class: "NumericOdeError", cause: Cause::UserInput, remediation: None },
    ErrorSpec { code: "E-ODE-026", class: "NumericOdeError", cause: Cause::UserInput, remediation: None },
    ErrorSpec { code: "E-ODE-030", class: "DsolveSystemError", cause: Cause::UserInput,   remediation: Some("each right-hand side must be affine in the state variables") },
    ErrorSpec { code: "E-ODE-031", class: "DsolveSystemError", cause: Cause::Unsupported, remediation: Some("y' = A(t)y needs a Magnus/Peano series, not e^{At}") },
    ErrorSpec { code: "E-ODE-032", class: "DsolveSystemError", cause: Cause::Unsupported, remediation: Some("substitute concrete values for the symbolic entries of A, or use a 2x2/3x3 or triangular system") },
    ErrorSpec { code: "E-ODE-033", class: "DsolveSystemError", cause: Cause::Unsupported, remediation: Some("the forcing term has no elementary antiderivative against the fundamental matrix") },
    ErrorSpec { code: "E-ODE-034", class: "DsolveSystemError", cause: Cause::UserInput,   remediation: None },
    // E-ODE-040..045 — `ode::series_solve::SeriesError` (Frobenius / power-series
    // solutions).  These sat on 020..025 until 3.10, colliding head-on with the
    // `NumericOdeError` block above: `E-ODE-021` was simultaneously "the adaptive
    // step size fell below the floor" and "the point is irregular singular", which
    // is precisely the branch a caller is told these codes exist to support.  The
    // series side moved because it had never surfaced — `series_solve` raised an
    // uncoded `ValueError` — so no caller could be reading it.
    //
    // 041/042 are declines about *this* implementation's reach; 040 and 043 are
    // facts about the equation; 044 is the withheld-answer code (a candidate
    // series that failed the exact-residual gate is never returned with a caveat).
    ErrorSpec { code: "E-ODE-040", class: "SeriesOdeError", cause: Cause::Unsupported, remediation: Some("ensure p, q, r are analytic at x0 with rational Taylor coefficients") },
    ErrorSpec { code: "E-ODE-041", class: "SeriesOdeError", cause: Cause::Domain,      remediation: Some("no Frobenius series exists at an irregular singular point; expand about an ordinary or regular-singular point instead") },
    ErrorSpec { code: "E-ODE-042", class: "SeriesOdeError", cause: Cause::Unsupported, remediation: Some("the indicial equation has irrational roots, outside the rational-recurrence path this solver uses") },
    ErrorSpec { code: "E-ODE-043", class: "SeriesOdeError", cause: Cause::UserInput,   remediation: Some("supply a non-zero coefficient for y''; p = 0 is not a second-order ODE") },
    ErrorSpec { code: "E-ODE-044", class: "SeriesOdeError", cause: Cause::Internal,    remediation: Some("the candidate series did not satisfy the ODE exactly, so it is withheld rather than returned; report the equation as a minimal failing example") },
    ErrorSpec { code: "E-ODE-045", class: "SeriesOdeError", cause: Cause::Unsupported, remediation: Some("the logarithmic second solution is intractable for this equation; the first solution is available on its own") },
    // E-DAE — DaeError
    ErrorSpec { code: "E-DAE-001", class: "DaeError", cause: Cause::Unsupported, remediation: Some("ensure all functions are differentiable before calling pantelides()") },
    ErrorSpec { code: "E-DAE-002", class: "DaeError", cause: Cause::UserInput,   remediation: Some("DAE index exceeds depth-10 limit; reformulate the model") },
    ErrorSpec { code: "E-DAE-003", class: "DaeError", cause: Cause::UserInput,   remediation: Some("check constraint count against variable count") },
    // E-DIFFALG — DiffAlgError (V2-13 differential algebra / Rosenfeld–Gröbner)
    ErrorSpec { code: "E-DIFFALG-001", class: "DiffAlgError", cause: Cause::Unsupported, remediation: Some("ensure the DAE is polynomial in its state and derivative symbols") },
    ErrorSpec { code: "E-DIFFALG-002", class: "DiffAlgError", cause: Cause::UserInput,   remediation: Some("declare all jet variables; remove transcendental functions") },
    ErrorSpec { code: "E-DIFFALG-003", class: "DiffAlgError", cause: Cause::UserInput, remediation: None },
    ErrorSpec { code: "E-SOLVE-001", class: "SolverError", cause: Cause::UserInput,   remediation: Some("ensure all equations are polynomial or rational in the declared variables") },
    ErrorSpec { code: "E-SOLVE-002", class: "SolverError", cause: Cause::Unsupported, remediation: Some("only degree ≤ 2 univariate solving is implemented; Gröbner basis is still returned") },
    ErrorSpec { code: "E-SOLVE-003", class: "SolverError", cause: Cause::UserInput,   remediation: Some("provide one equation per variable") },
    // `triangularize` extracts one polynomial per main variable, so two basis
    // generators sharing a main variable lose one of them and the chain describes
    // a larger variety than the input system.  Splitting on the initials
    // (Lazard–Kalkbrener) is what would decompose those ideals; refusing is the
    // point until it exists.  Travels inside `SolverError::NotPolynomial` — see
    // `solver::regular_chains::TriangularizeRefusal`.
    ErrorSpec { code: "E-SOLVE-004", class: "TriangularizeRefusal", cause: Cause::Unsupported, remediation: Some("this ideal needs a splitting triangular decomposition (Lazard–Kalkbrener on the initials), which is not implemented; use GroebnerBasis::compute or primary_decomposition instead") },
    // A rational equation whose denominator is identically zero (`1/(x − x)`)
    // denotes no function, so it has no solution set.  Clearing it would
    // multiply through by zero and turn every point into a "solution".  Like
    // `E-SOLVE-004`, it travels inside `SolverError::NotPolynomial` — see
    // `solver::UndefinedEquation`.
    ErrorSpec { code: "E-SOLVE-005", class: "UndefinedEquation", cause: Cause::UserInput,   remediation: Some("a denominator simplifies to zero, so the equation denotes no function; check the equation for a subtraction that cancels") },
    ErrorSpec { code: "E-SOLVE-010", class: "SolverError", cause: Cause::Resource,    remediation: Some("check GPU availability; pass device_id=None to fall back to CPU") },
    ErrorSpec { code: "E-SOLVE-011", class: "SolverError", cause: Cause::Resource,    remediation: Some("CRT reconstruction failed; try adding more equations or use CPU path") },
    // E-IDEAL — PrimaryDecompositionError (ideal/primary.rs) and IdealRefusal
    ErrorSpec { code: "E-IDEAL-001", class: "PrimaryDecompositionError", cause: Cause::UserInput,   remediation: Some("pass at least one generator") },
    ErrorSpec { code: "E-IDEAL-002", class: "PrimaryDecompositionError", cause: Cause::UserInput,   remediation: Some("all generators must be polynomials in the same variable list") },
    ErrorSpec { code: "E-IDEAL-003", class: "PrimaryDecompositionError", cause: Cause::Resource,    remediation: Some("the saturation split recursed past its depth limit; simplify the generating set") },
    ErrorSpec { code: "E-IDEAL-004", class: "PrimaryDecompositionError", cause: Cause::Internal,    remediation: Some("report the generating set as a minimal failing example") },
    // √I over an arbitrary ideal needs Gianni–Trager–Zacharias (or a
    // characteristic-set method).  Only monomial, principal and zero-dimensional
    // ideals are certified; outside those, returning the input unchanged would be
    // asserting √I = I with no justification, so the routine refuses instead.
    ErrorSpec { code: "E-IDEAL-005", class: "IdealRefusal", cause: Cause::Unsupported, remediation: Some("radical is certified for monomial, principal and zero-dimensional ideals; intersect the associated primes of a primary decomposition if one is available") },
    ErrorSpec { code: "E-IDEAL-006", class: "IdealRefusal", cause: Cause::Unsupported, remediation: Some("primary decomposition is certified for monomial and principal ideals, for saturation/CRT splits of them, and for shape-position zero-dimensional ideals; no general algorithm is implemented") },
    // E-HOMOTOPY — HomotopyError (V2-14 numerical algebraic geometry)
    ErrorSpec { code: "E-HOMOTOPY-002", class: "HomotopyError", cause: Cause::Unsupported, remediation: Some("raise HomotopyOpts.max_bezout_paths or use mixed-volume continuation for deficient systems") },
    ErrorSpec { code: "E-HOMOTOPY-003", class: "HomotopyError", cause: Cause::Resource,    remediation: Some("try HomotopyOpts.gamma_angle_seed or rescale equations") },
    ErrorSpec { code: "E-HOMOTOPY-004", class: "HomotopyError", cause: Cause::Resource,    remediation: Some("adjust predictor step or increase max_tracker_steps") },
    // E-JIT — JitError
    ErrorSpec { code: "E-JIT-001", class: "JitError", cause: Cause::Unsupported, remediation: Some("use eval_expr or simplify the expression before JIT") },
    ErrorSpec { code: "E-JIT-002", class: "JitError", cause: Cause::Resource,    remediation: Some("check LLVM 21 installation; run with RUST_LOG=debug for details") },
    ErrorSpec { code: "E-JIT-003", class: "JitError", cause: Cause::Resource,    remediation: Some("ensure LLVM_SYS_211_PREFIX is set correctly") },
    ErrorSpec { code: "E-JIT-004", class: "JitError", cause: Cause::UserInput, remediation: None },
    // The expression has a free symbol the input list does not bind, so the compiled
    // function would be NaN everywhere. Carried out of band on `JitError::UnsupportedNode`
    // (exhaustive public enum) — see `jit::take_unbound_symbols`.
    ErrorSpec { code: "E-JIT-005", class: "UnboundSymbolsError", cause: Cause::UserInput, remediation: Some("add every free symbol of the expression to the input list, or substitute a value for each parameter before compiling") },
    // E-CAD — CadError (V2-9 QE / cylindrical decomposition)
    ErrorSpec {
        code: "E-CAD-001",
        class: "CadError",
        cause: Cause::Unsupported,
        // Two distinct causes now share this code, and the remediation has to
        // cover both: outside the supported fragment (≤ 2 variables, ≤ 2
        // quantifiers, polynomial atoms), or inside it but undecidable by the
        // sample points available — a non-strict atom whose only solutions sit
        // at an irrational boundary. The second is a *refusal to guess*: before
        // 3.8 that case silently answered, which produced false universals.
        remediation: Some(
            "keep to polynomial atoms in at most two real variables with at most two quantifiers; if the sentence is already in that fragment, its solutions may lie only at an irrational boundary point, which cannot be tested exactly — substitute concrete values, use a strict inequality, or hand it to an SMT solver via alkahest.smt",
        ),
    },
    // E-CUDA — CudaError
    ErrorSpec { code: "E-CUDA-001", class: "CudaError", cause: Cause::Resource,    remediation: Some("rebuild LLVM with nvptx64 in LLVM_TARGETS_TO_BUILD") },
    ErrorSpec { code: "E-CUDA-002", class: "CudaError", cause: Cause::Unsupported, remediation: Some("inspect PTX; verify every primitive has CUDA lowering") },
    ErrorSpec { code: "E-CUDA-003", class: "CudaError", cause: Cause::Resource,    remediation: Some("run nvidia-smi; retry with CUDA_LAUNCH_BLOCKING=1") },
    ErrorSpec { code: "E-CUDA-004", class: "CudaError", cause: Cause::Unsupported, remediation: Some("V1.0 stub; track feature request") },
    ErrorSpec { code: "E-CUDA-005", class: "CudaError", cause: Cause::Resource,    remediation: Some("install nvidia-cuda-toolkit or set ALKAHEST_LIBDEVICE_PATH") },
    ErrorSpec { code: "E-CUDA-006", class: "CudaError", cause: Cause::Resource,    remediation: Some("check grid/block dimensions; rerun with compute-sanitizer") },
    // E-POOL — ExprPool construction (E-POOL-001, cross-pool arguments, is raised
    // only by the Python bindings)
    ErrorSpec { code: "E-POOL-002", class: "PoolError", cause: Cause::UserInput, remediation: Some("a built-in function name has a fixed number of arguments; use a different name for a user-defined function of another arity") },
    // E-IO — IoError (formerly PoolPersistError with E-POOL-* codes)
    ErrorSpec { code: "E-IO-001", class: "IoError", cause: Cause::Resource,  remediation: None },
    ErrorSpec { code: "E-IO-002", class: "IoError", cause: Cause::UserInput, remediation: Some("file is not an alkahest pool; check the path or regenerate with ExprPool::checkpoint()") },
    ErrorSpec { code: "E-IO-003", class: "IoError", cause: Cause::UserInput, remediation: Some("run `alkahest migrate-pool` to upgrade the file, or regenerate from source") },
    ErrorSpec { code: "E-IO-004", class: "IoError", cause: Cause::Resource,  remediation: Some("file was truncated (likely a crash during checkpoint); rerun from source and checkpoint again") },
    ErrorSpec { code: "E-IO-005", class: "IoError", cause: Cause::UserInput, remediation: None },
    ErrorSpec { code: "E-IO-006", class: "IoError", cause: Cause::UserInput, remediation: None },
    ErrorSpec { code: "E-IO-007", class: "IoError", cause: Cause::UserInput, remediation: None },
    ErrorSpec { code: "E-IO-008", class: "IoError", cause: Cause::UserInput, remediation: None },
    ErrorSpec { code: "E-IO-009", class: "IoError", cause: Cause::UserInput, remediation: None },
    // E-MOD — ModularError (V2-1 Modular/CRT framework)
    ErrorSpec { code: "E-MOD-001", class: "ModularError", cause: Cause::UserInput,   remediation: Some("use a prime modulus p ≥ 2, e.g. 101, 1009, 32749") },
    ErrorSpec { code: "E-MOD-002", class: "ModularError", cause: Cause::UserInput,   remediation: Some("ensure all images share the same variable ordering and modulus") },
    ErrorSpec { code: "E-MOD-003", class: "ModularError", cause: Cause::UserInput,   remediation: Some("provide at least one (MultiPolyFp, prime) pair") },
    ErrorSpec { code: "E-MOD-004", class: "ModularError", cause: Cause::Unsupported, remediation: Some("provide more modular images so the prime product M exceeds 2 * max_coeff²") },
    // E-LAT — LatticeError (V2-6 LLL)
    ErrorSpec { code: "E-LAT-001", class: "LatticeError", cause: Cause::UserInput,   remediation: Some("pass a non-empty matrix of integer rows, all of equal length") },
    ErrorSpec { code: "E-LAT-002", class: "LatticeError", cause: Cause::UserInput,   remediation: Some("every row must lie in ℤ^m for fixed ambient dimension m") },
    ErrorSpec { code: "E-LAT-003", class: "LatticeError", cause: Cause::UserInput,   remediation: Some("pick δ strictly between ¼ and 1; the default δ = ¾ is standard") },
    ErrorSpec { code: "E-LAT-004", class: "LatticeError", cause: Cause::Unsupported, remediation: Some("check for rank deficiency; try a smaller basis or report a minimal reproducer") },
    ErrorSpec { code: "E-LAT-005", class: "LatticeGeometryError", cause: Cause::UserInput,   remediation: Some("a Gram matrix has one row and one column per basis vector") },
    ErrorSpec { code: "E-LAT-006", class: "LatticeGeometryError", cause: Cause::UserInput,   remediation: Some("supply the full symmetric matrix G[i][j] = <b_i, b_j>") },
    ErrorSpec { code: "E-LAT-007", class: "LatticeGeometryError", cause: Cause::UserInput,   remediation: Some("basis rows must be linearly independent and a Gram matrix positive definite") },
    ErrorSpec { code: "E-LAT-008", class: "LatticeGeometryError", cause: Cause::Unsupported, remediation: Some("exact SVP/CVP is exponential in the rank; project to a sublattice or use LLL instead of enumeration") },
    ErrorSpec { code: "E-LAT-009", class: "LatticeGeometryError", cause: Cause::Resource,    remediation: Some("raise the enumeration node budget, lower the norm bound, or reduce the basis first") },
    ErrorSpec { code: "E-LAT-010", class: "LatticeGeometryError", cause: Cause::Unsupported, remediation: Some("scale the lattice to an integral Gram matrix, or ask for the minimum rather than a theta series") },
    ErrorSpec { code: "E-LAT-011", class: "LatticeGeometryError", cause: Cause::UserInput,   remediation: Some("supply a vector with one entry per ambient coordinate") },
    ErrorSpec { code: "E-LAT-012", class: "LatticeGeometryError", cause: Cause::UserInput,   remediation: Some("check the documented parameter range for this lattice constructor") },
    ErrorSpec { code: "E-LAT-013", class: "LatticeGeometryError", cause: Cause::UserInput,   remediation: Some("build the lattice from a basis if you need ambient coordinates") },
    ErrorSpec { code: "E-LAT-014", class: "LatticeGeometryError", cause: Cause::Internal,    remediation: Some("this is a bug: please report it with the lattice that produced it") },
    // E-LOGIC — first-order formulas (V3-3)
    ErrorSpec { code: "E-LOGIC-001", class: "LogicError", cause: Cause::UserInput, remediation: Some("pass a predicate or quantified Expr; use pool.gt/… or And/Or/Not") },
    // E-PSLQ — PslqError (V2-6 augmented-lattice relation heuristic)
    ErrorSpec { code: "E-PSLQ-001", class: "PslqError", cause: Cause::UserInput,   remediation: Some("pass at least two constants that might admit a linear dependence") },
    ErrorSpec { code: "E-PSLQ-002", class: "PslqError", cause: Cause::UserInput,   remediation: Some("literals must not truncate to zero — use higher precision or decimal strings") },
    ErrorSpec { code: "E-PSLQ-003", class: "PslqError", cause: Cause::UserInput,   remediation: Some("allocate at least 64 MPFR bits; ≈664 bits ≈ 200 decimal digits") },
    // E-RSOLVE — RsolveError (V2-18 difference equations / rsolve)
    ErrorSpec { code: "E-RSOLVE-001", class: "RsolveError", cause: Cause::UserInput,   remediation: Some("write the recurrence as a sum of pool.func(seq, [n ± integer]) shifts plus a polynomial in n, then call rsolve(equation, n, seq_name)") },
    ErrorSpec { code: "E-RSOLVE-002", class: "RsolveError", cause: Cause::UserInput,   remediation: Some("each addend may contain at most one sequence application; avoid products like n*f(n)") },
    ErrorSpec { code: "E-RSOLVE-003", class: "RsolveError", cause: Cause::UserInput,   remediation: Some("clear denominators; the rhs must be a polynomial in the recurrence index") },
    ErrorSpec { code: "E-RSOLVE-004", class: "RsolveError", cause: Cause::Unsupported, remediation: Some("order > 2 non-homogeneous systems and some characteristic factorizations are not implemented yet") },
    ErrorSpec { code: "E-RSOLVE-005", class: "RsolveError", cause: Cause::UserInput,   remediation: Some("pass exactly order-many initial samples as a dict n → Expr value") },
    // E-DIOPH — DiophantineError (V2-19 linear / Pell / sum-of-squares)
    ErrorSpec { code: "E-DIOPH-001", class: "DiophantineError", cause: Cause::UserInput,   remediation: Some("pass one polynomial equation = 0 in two integer symbols") },
    ErrorSpec { code: "E-DIOPH-002", class: "DiophantineError", cause: Cause::UserInput,   remediation: Some("clear denominators; coefficients must be rational integers") },
    ErrorSpec { code: "E-DIOPH-003", class: "DiophantineError", cause: Cause::Unsupported, remediation: Some("supported: linear 2-variable, x²+y²=n, x²−D·y²=N including unit Pell (no xy term); very large integers may be unsupported") },
    ErrorSpec { code: "E-DIOPH-004", class: "DiophantineError", cause: Cause::Domain,      remediation: Some("check gcd divisibility (linear) or solvability over ℤ (quadratic)") },
    // E-PROD — ProductError (V2-22 symbolic discrete products)
    ErrorSpec { code: "E-PROD-001", class: "ProductError", cause: Cause::Unsupported, remediation: Some("reduce the term to ℚ(k) with only ℤ-linear factors in k (no extra symbols in k)") },
    ErrorSpec { code: "E-PROD-002", class: "ProductError", cause: Cause::Internal,    remediation: Some("report the polynomial as a minimal failing example") },
    ErrorSpec { code: "E-PROD-003", class: "ProductError", cause: Cause::Unsupported, remediation: Some("ℤ-irreducible factors of degree ≥ 2 in k require a Gamma extension beyond this path") },
    ErrorSpec { code: "E-PROD-004", class: "ProductError", cause: Cause::UserInput,   remediation: Some("check that lo/hi share the ExprPool with the summation index symbol") },
    // E-NT — NumberTheoryError (V3-1)
    ErrorSpec { code: "E-NT-001", class: "NumberTheoryError", cause: Cause::UserInput, remediation: Some("pass decimal strings parsable into fmpz") },
    ErrorSpec { code: "E-NT-002", class: "NumberTheoryError", cause: Cause::Domain, remediation: Some("check positivity / parity constraints on arguments") },
    ErrorSpec { code: "E-NT-003", class: "NumberTheoryError", cause: Cause::Domain, remediation: Some("adjust residue, base, or root degree until a modular solution exists") },
    ErrorSpec { code: "E-NT-004", class: "NumberTheoryError", cause: Cause::Domain, remediation: Some("use prime moduli for discrete_log/nthroot_mod as documented") },
    ErrorSpec { code: "E-NT-005", class: "NumberTheoryError", cause: Cause::Unsupported, remediation: Some("use quadratic roots or gcd(k,p−1)=1; general radicals require more machinery") },
    ErrorSpec { code: "E-NT-006", class: "ArithmeticError", cause: Cause::Resource, remediation: Some("the arithmetic-function argument is past this crate's work cap; p(n), B_n, E_n and H_n all grow superpolynomially in the number of digits, so the cap is a refusal to sit in FLINT for an unbounded time rather than a statement that the value does not exist") },
    // E-PARSE — expression parser (V2-21)
    ErrorSpec { code: "E-PARSE-001", class: "ParseError", cause: Cause::UserInput,   remediation: Some("only ASCII arithmetic expressions are supported") },
    ErrorSpec { code: "E-PARSE-002", class: "ParseError", cause: Cause::UserInput,   remediation: Some("check parentheses and operator placement") },
    ErrorSpec { code: "E-PARSE-003", class: "ParseError", cause: Cause::UserInput,   remediation: Some("use a known function: sin, cos, tan, sinh, cosh, tanh, asin, acos, atan, atan2, exp, log, sqrt, abs, sign, floor, ceil, round, erf, erfc, gamma, lambert_w, digamma, bessel_j0, bessel_j1") },
    ErrorSpec { code: "E-PARSE-004", class: "ParseError", cause: Cause::Resource,    remediation: Some("flatten the expression — deeply nested parentheses, prefix signs or function calls exceed the parser's recursion budget") },
    ErrorSpec { code: "E-EVAL-001", class: "EvalError", cause: Cause::UserInput,   remediation: Some("bind every free symbol before evaluation") },
    ErrorSpec { code: "E-EVAL-002", class: "EvalError", cause: Cause::UserInput,   remediation: Some("use mode='f64' or 'complex' for float literals") },
    ErrorSpec { code: "E-EVAL-003", class: "EvalError", cause: Cause::UserInput,   remediation: Some("only integer exponents are supported in exact mode") },
    ErrorSpec { code: "E-EVAL-004", class: "EvalError", cause: Cause::Domain,      remediation: Some("0 to a negative power is undefined") },
    ErrorSpec { code: "E-EVAL-005", class: "EvalError", cause: Cause::Unsupported, remediation: Some("register the function or use a supported evaluation mode") },
    ErrorSpec { code: "E-EVAL-006", class: "EvalError", cause: Cause::Unsupported, remediation: Some("this expression form is not evaluable in the requested mode") },
    ErrorSpec { code: "E-EVAL-007", class: "EvalError", cause: Cause::UserInput,   remediation: Some("check predicate arity") },
    ErrorSpec { code: "E-EVAL-008", class: "EvalError", cause: Cause::Domain,      remediation: Some("predicate truth value is not uniform over the input domain") },
    ErrorSpec { code: "E-EVAL-009", class: "EvalError", cause: Cause::Domain,      remediation: Some("result is not finite") },
    ErrorSpec { code: "E-EVAL-010", class: "EvalError", cause: Cause::Domain,      remediation: Some("interval evaluation failed or branch is indeterminate") },
    ErrorSpec { code: "E-EVAL-011", class: "EvalError", cause: Cause::Domain,      remediation: Some("principal Arg/log branch cut — expression stays unevaluated at this point") },
    ErrorSpec { code: "E-EVAL-012", class: "EvalError", cause: Cause::Resource,    remediation: Some("the exponent is an exact integer whose power the requested representation cannot hold — reduce the exponent, or evaluate a base of 0 or ±1 where the answer is the parity alone") },
    ErrorSpec { code: "E-RESIDUE-001", class: "ResidueError", cause: Cause::UserInput,   remediation: Some("input must be a rational function of the variable over ℚ") },
    ErrorSpec { code: "E-RESIDUE-002", class: "ResidueError", cause: Cause::Domain,      remediation: Some("denominator must be non-zero") },
    ErrorSpec { code: "E-RESIDUE-003", class: "ResidueError", cause: Cause::Unsupported, remediation: Some("pole order exceeds supported bound; essential singularities are out of scope") },
    ErrorSpec { code: "E-RESIDUE-004", class: "ResidueError", cause: Cause::Domain,      remediation: Some("division by zero during Laurent coefficient extraction") },
    // E-BUDGET — BudgetError (P1 search plumbing item 4: budgets/cancellation/determinism)
    ErrorSpec { code: "E-BUDGET-001", class: "BudgetError", cause: Cause::Resource, remediation: Some("raise Budget(wall_ms=...), or accept a heuristic/numeric result for this candidate instead of an exact one") },
    // Also the code for a *growth* refusal: one step asking `budget::check_growth`
    // to spend more units at once than `max_steps` (or, with no budget active,
    // `budget::DEFAULT_MAX_GROWTH_UNITS`) allows. Same currency, same remedy — and
    // the reason it needs no code of its own.
    ErrorSpec { code: "E-BUDGET-002", class: "BudgetError", cause: Cause::Resource, remediation: Some("raise Budget(max_steps=...), or accept a partial/heuristic result for this candidate instead of an exact one") },
    ErrorSpec { code: "E-BUDGET-003", class: "BudgetError", cause: Cause::Resource, remediation: Some("call alkahest.clear_cancel() (Python) or budget::clear_cancel() (Rust) before starting the next candidate") },
    // E-BUDGET-004/005 — BudgetTrip. Carried on `BudgetTrip`, not `BudgetError`:
    // the latter is an exhaustive public enum, so a `Memory` variant would be a
    // major semver break. 005 fires with no budget active — it is the guard that
    // turns GMP's `abort()` under a `ulimit -v` into a catchable refusal.
    ErrorSpec { code: "E-BUDGET-004", class: "BudgetError", cause: Cause::Resource, remediation: Some("raise Budget(max_bytes=...), or ask for a smaller problem (fewer unknowns, a lower order/degree) — the exact coefficients, not the shape of the system, are what grew") },
    ErrorSpec { code: "E-BUDGET-005", class: "BudgetError", cause: Cause::Resource, remediation: Some("raise the process address-space limit (ulimit -v, or the container/cgroup memory limit), or ask for a smaller problem — this refusal replaces the uncatchable abort that would otherwise follow") },
    // E-BUDGET-006 — BudgetTrip::Oversized: a single result larger than physical
    // memory, or than GMP can represent as one integer. Fires with no budget.
    ErrorSpec { code: "E-BUDGET-006", class: "BudgetError", cause: Cause::Resource, remediation: Some("ask for a smaller result (a smaller exponent, degree or shape) — no limit setting can make this one fit; this refusal replaces the uncatchable abort that would otherwise follow") },
    // E-DEPTH — DepthLimitError (expression nesting ceiling; see kernel::depth).
    // Resource, not UserInput: the expression is well-formed, we decline to
    // recurse over it because a native stack overflow would kill the process.
    ErrorSpec { code: "E-DEPTH-001", class: "DepthLimitError", cause: Cause::Resource, remediation: Some("rebuild the expression with less nesting (a balanced n-ary Add is shallow where a chain of binary ones is not), or process it in smaller pieces") },
    // E-DOMAIN — reserved; DomainError is Python-only pending Rust implementation
    // E-SOS — SosError (P1 item 8: positivity certificates / Positivstellensatz)
    ErrorSpec { code: "E-SOS-001", class: "SosError", cause: Cause::UserInput,   remediation: Some("positivity certificates are for polynomials in the listed variables; expand or clear denominators first, and pass every symbol that occurs as a variable") },
    ErrorSpec { code: "E-SOS-002", class: "SosError", cause: Cause::Unsupported, remediation: Some("record this as unknown, not as a closed branch: read the 'what the search actually did' trace in the message first — a line marked NOT SEARCHED is a size ceiling that fired, not a search that came up empty — then raise basis_degree (unconstrained) or level (constrained); this is not a proof that no SOS decomposition exists, and still less that the inequality is false — alkahest.decide is the complete (and far more expensive) fallback") },
    ErrorSpec { code: "E-SOS-003", class: "SosError", cause: Cause::UserInput,   remediation: Some("the witness point in the message satisfies the constraints and makes the target negative; the claim is false as stated") },
    ErrorSpec { code: "E-SOS-004", class: "SosError", cause: Cause::UserInput,   remediation: Some("pass at least one variable, and keep basis_degree/level within the supported range") },
    ErrorSpec { code: "E-SOS-005", class: "SosError", cause: Cause::Internal,    remediation: Some("internal: report the target and constraints as a minimal failing example") },
    // E-HOLO — HolonomicError (P1 item 7: creative telescoping / Zeilberger's algorithm)
    ErrorSpec { code: "E-HOLO-001", class: "HolonomicError", cause: Cause::UserInput,   remediation: Some("rewrite the term as R(n,k)*z**k*w**n*prod(gamma(a*n + b*k + c)**e) with integer a, b and rational c; supported function heads are gamma, factorial, binomial, pochhammer") },
    ErrorSpec { code: "E-HOLO-002", class: "HolonomicError", cause: Cause::Resource,    remediation: Some("raise max_order and/or max_degree in ZeilbergerOpts; if the term genuinely has no such recurrence within reach, Zeilberger's algorithm does not apply") },
    ErrorSpec { code: "E-HOLO-003", class: "HolonomicError", cause: Cause::Internal,    remediation: Some("internal: report the term as a minimal failing example") },
    ErrorSpec { code: "E-HOLO-004", class: "HolonomicError", cause: Cause::UserInput,   remediation: Some("n and k must be distinct symbols; max_order and max_degree must be positive") },
    // E-HOLO-005 is Python-only (`python/alkahest/_guess_holonomic.py`): a fit
    // the supplied terms cannot support. Registering a code no Rust
    // `AlkahestError` impl returns would fail `scripts/check_error_codes.py`.
    ErrorSpec { code: "E-HOLO-006", class: "HolonomicError", cause: Cause::UserInput,   remediation: Some("the modulus must be p**k with p prime, k >= 1 and p**k < 2**62; for a composite modulus, evaluate at each prime power and recombine by CRT") },
    ErrorSpec { code: "E-HOLO-007", class: "HolonomicError", cause: Cause::UserInput,   remediation: Some("no modulus repairs this: the recurrence itself leaves Z_p at that index. Supply more initial terms so the evaluation starts past it, use a recurrence whose leading coefficient does not vanish there, or accept that the sequence is not p-integral and rescale it") },
    ErrorSpec { code: "E-HOLO-008", class: "HolonomicError", cause: Cause::Resource,    remediation: Some("lower k, use a smaller prime, or ask for an index the recurrence reaches without crossing so many singular steps") },
    // E-HOLO-02x — QHolonomicError (M4b: q-analogue creative telescoping). A
    // separate block so a caller can tell which of the two engines refused.
    ErrorSpec { code: "E-HOLO-020", class: "HolonomicError", cause: Cause::UserInput,   remediation: Some("write the summand with qbinomial(N, K), qpochhammer(u, d, v), powers of q with a degree-2 exponent in n and k, and rational functions of q, q**n and q**k; a bare n or k outside an exponent is not q-hypergeometric") },
    ErrorSpec { code: "E-HOLO-021", class: "HolonomicError", cause: Cause::Resource,    remediation: Some("raise max_order and/or max_degree; if the sum genuinely satisfies no such q-recurrence, q-Zeilberger does not apply") },
    ErrorSpec { code: "E-HOLO-022", class: "HolonomicError", cause: Cause::Internal,    remediation: Some("internal: report the term as a minimal failing example") },
    ErrorSpec { code: "E-HOLO-023", class: "HolonomicError", cause: Cause::UserInput,   remediation: Some("q, n and k must be three distinct symbols; max_order and max_degree must be at least 1; a q-Pochhammer base step must be at least 1") },
    ErrorSpec { code: "E-HOLO-024", class: "HolonomicError", cause: Cause::Unsupported, remediation: Some("the term is q-hypergeometric in shape but its shift quotient is not a rational function of q**n and q**k — e.g. (q; q**2)_k shifted in k. No algorithm in this family applies; close the branch") },
    // E-HOLO-04x — Telescoping2dError (M4: double-sum / Apagodu-Zeilberger
    // creative telescoping). A separate block so a caller can tell which of
    // the three engines refused.
    ErrorSpec { code: "E-HOLO-040", class: "HolonomicError", cause: Cause::UserInput,   remediation: Some("rewrite the term as R(n,j,k)*z1**j*z2**k*w**n*prod(gamma(a*n+b*j+c*k+d)**e) with integer a, b, c; supported function heads are gamma, factorial, binomial, pochhammer") },
    ErrorSpec { code: "E-HOLO-041", class: "HolonomicError", cause: Cause::Resource,    remediation: Some("raise max_order, max_a_degree and/or max_cert_degree in Telescoping2dOpts; if the term genuinely has no such double-sum certificate within reach — or needs a certificate denominator this module's fixed-denominator ansatz cannot represent — this method does not apply") },
    ErrorSpec { code: "E-HOLO-042", class: "HolonomicError", cause: Cause::UserInput,   remediation: Some("n, j and k must be three distinct symbols") },
    // E-HOLO-06x — DiffTelescopingError (continuous creative telescoping /
    // Almkvist-Zeilberger, the differential twin of the classical engine). Its
    // own sub-block for the same reason the q- and multi-sum engines have one:
    // a caller can tell which engine refused, and the refusals differ in kind
    // (061 in particular closes the branch rather than suggesting new bounds).
    ErrorSpec { code: "E-HOLO-060", class: "HolonomicError", cause: Cause::UserInput,   remediation: Some("write the integrand as R(n,x)*w**n*exp(eta(x))*prod(B(x)**(a*n + b)) with integer a and rational b; supported function heads are exp and sqrt, and a sum of two hyperexponential terms is not itself hyperexponential") },
    ErrorSpec { code: "E-HOLO-061", class: "HolonomicError", cause: Cause::Unsupported, remediation: Some("the x-side is fine but F(n+1,x)/F(n,x) is not rational in x - e.g. exp(n*x), whose ratio is exp(x). No algorithm in this family applies; close the branch") },
    ErrorSpec { code: "E-HOLO-062", class: "HolonomicError", cause: Cause::Resource,    remediation: Some("raise max_order, max_degree and/or max_den_power in AzOpts; if the integrand genuinely has no such certificate within reach - or needs a certificate denominator this module's D**kappa * B ansatz cannot represent - this method does not apply") },
    ErrorSpec { code: "E-HOLO-063", class: "HolonomicError", cause: Cause::Internal,    remediation: Some("internal: report the integrand as a minimal failing example") },
    ErrorSpec { code: "E-HOLO-064", class: "HolonomicError", cause: Cause::UserInput,   remediation: Some("n and x must be distinct symbols; the max_order, max_degree and max_den_power bounds must be within the documented ranges") },
    // E-SMT — SmtError (P2 item 3: SMT/SAT bridge).
    //
    // Only the code Rust actually raises is registered here.  The rest of the
    // family lives entirely in `python/alkahest/smt.py`, which drives the solver
    // process: E-SMT-001 (no solver binary on PATH), E-SMT-003 (a model value —
    // a `root-obj` algebraic number — that cannot be lifted exactly), and
    // E-SMT-004 (a model that failed exact back-substitution).  Registering a
    // code no Rust `AlkahestError` impl returns would fail
    // `scripts/check_error_codes.py`; `E-BATCH-001` in `python/alkahest/_batch.py`
    // is the same precedent.
    ErrorSpec { code: "E-SMT-002", class: "SmtError", cause: Cause::Unsupported, remediation: Some("check alkahest.smt.supported(formula) before exporting; the fragment is polynomial (in)equalities over Int/Real symbols plus boolean structure and quantifiers, and float literals are refused because they are not the exact question they look like") },
    // Codes raised by alkahest-core that were never registered here. The registry
    // is the contract `scripts/check_error_codes.py` enforces, so an unregistered
    // code is an undocumented one. Remediation text is taken from each type's own
    // `AlkahestError::remediation` impl rather than re-worded, so the two cannot
    // disagree about what a caller should do.
    // E-ASYMPT — AsymptoticError
    ErrorSpec { code: "E-ASYMPT-001", class: "AsymptoticError", cause: Cause::UserInput, remediation: Some("pass n_terms >= 1") },
    ErrorSpec { code: "E-ASYMPT-002", class: "AsymptoticError", cause: Cause::UserInput, remediation: Some("the function may not be analytic/Laurent-expandable at infinity; try a simpler form or fewer terms") },
    ErrorSpec { code: "E-ASYMPT-003", class: "AsymptoticError", cause: Cause::UserInput, remediation: Some("ensure all functions are registered primitives with differentiation rules") },
    ErrorSpec { code: "E-ASYMPT-004", class: "AsymptoticError", cause: Cause::UserInput, remediation: Some("the expansion could not be numerically verified at large x; the function may have an oscillatory or non-power-scale tail") },
    ErrorSpec { code: "E-ASYMPT-005", class: "AsymptoticError", cause: Cause::Unsupported, remediation: Some("exp/log scale hierarchies and Gamma/Stirling asymptotics are out of scope for asymptotic_expand; power-scale (rational/algebraic) and single log/exp peels are supported. For the asymptotics of a *sum* use experimental.euler_maclaurin, which also reaches Stirling via the sum of log k") },
    // E-FPS — FpsError
    ErrorSpec { code: "E-FPS-001", class: "FpsError", cause: Cause::Domain, remediation: Some("q(0) = 0 makes p/q singular at the origin: divide out the common factor of x, or expand about a point where q does not vanish") },
    ErrorSpec { code: "E-FPS-002", class: "FpsError", cause: Cause::Domain, remediation: Some("the expression has a pole at x = 0, so its expansion is a Laurent series and not a formal power series; multiply by the polar factor first, or use series/puiseux_series") },
    ErrorSpec { code: "E-FPS-003", class: "FpsError", cause: Cause::UserInput, remediation: Some("Fps carries exact rational coefficients only; substitute rationals for the symbolic parameters") },
    ErrorSpec { code: "E-FPS-004", class: "FpsError", cause: Cause::Domain, remediation: Some("composition, exp and reversion need f(0) = 0; subtract the constant term, e.g. exp(f) = exp(f(0))*exp(f - f(0))") },
    ErrorSpec { code: "E-FPS-005", class: "FpsError", cause: Cause::UserInput, remediation: Some("log and the default n-th root need f(0) = 1 to pick a branch; factor the constant term out, e.g. log(f) = log(f(0)) + log(f/f(0))") },
    ErrorSpec { code: "E-FPS-006", class: "FpsError", cause: Cause::Domain, remediation: Some("the multiplicative inverse needs f(0) != 0; when f vanishes to order k, divide x^k out first") },
    ErrorSpec { code: "E-FPS-007", class: "FpsError", cause: Cause::UserInput, remediation: Some("the underlying Taylor expansion failed; ensure every function has a differentiation rule and the expansion point is regular") },
    // E-TRANSFORM — the integral/sequence transform tables
    // (`transform::laplace`, `transform::fourier`, `transform::ztransform`).
    //
    // One prefix for the whole `transform` module, three disjoint numeric
    // blocks: 00x Laplace, 01x Fourier, 10x Z.  This is the arrangement E-ODE-*
    // already uses for `OdeError`/`DsolveError`/`NumericOdeError` — one Python
    // class per *prefix*, with the number saying which engine and which failure.
    //
    // The blocks are split the same way inside: a table miss (001/002/011/101/102)
    // is a fact about *this implementation* and a wider table would close it,
    // while 004 and 013 are facts about *the mathematics* — the hypothesis the
    // rule rests on is refuted, and no table can be widened to cover it.  That is
    // the branch these codes exist for: the first says "try again later or rewrite
    // the input", the second says "there is nothing to find".
    ErrorSpec { code: "E-TRANSFORM-001", class: "TransformError", cause: Cause::Unsupported, remediation: Some("laplace_transform is table-based: reduce f(t) to constants, t^n, exp(a*t), sin/cos/sinh/cosh of b*t, Heaviside/Dirac shifts, and products of those with exp(a*t) or t^n") },
    ErrorSpec { code: "E-TRANSFORM-002", class: "TransformError", cause: Cause::Unsupported, remediation: Some("inverse_laplace_transform inverts a proper rational F(s) whose denominator factors into poles of degree <= 2 (times an optional delay exp(-a*s)); an improper part would invert to derivatives of the Dirac delta and is declined rather than fabricated") },
    ErrorSpec { code: "E-TRANSFORM-003", class: "TransformError", cause: Cause::UserInput,   remediation: Some("pass distinct symbols for the time and frequency variables") },
    // Not a table gap: the unilateral transform integrates over t >= 0 only, so a
    // step/impulse edge at a < 0 is invisible to it and exp(+a*s) on the inverse is
    // the transform of nothing causal.  Emitting the shifted answer anyway is a
    // clean, plausible, wrong function — L{theta(t+1)} = 1/s, not e^s/s.
    ErrorSpec { code: "E-TRANSFORM-004", class: "TransformError", cause: Cause::Domain,      remediation: Some("the unilateral transform sees t >= 0 only, so no wider table fixes this; shift the edge to a >= 0, or use a bilateral transform") },
    ErrorSpec { code: "E-TRANSFORM-011", class: "TransformError", cause: Cause::Unsupported, remediation: Some("fourier_transform is table-based: reduce f(x) to Gaussians, one- and two-sided exponentials, Lorentzians, rect/sinc, Dirac deltas, constants and polynomials, and modulations of those") },
    ErrorSpec { code: "E-TRANSFORM-012", class: "TransformError", cause: Cause::UserInput,   remediation: Some("pass distinct symbols for the space and frequency variables") },
    // The table entry's own decay hypothesis, refuted rather than unproven: at a
    // non-positive rate the defining integral diverges, and a negative Lorentzian
    // amplitude has a transform of the opposite sign to the tabulated one.
    ErrorSpec { code: "E-TRANSFORM-013", class: "TransformError", cause: Cause::Domain,      remediation: Some("the rate named in the message is not positive, so the defining integral does not converge; supply a positive rate, or leave it symbolic and read the hypothesis off side_conditions") },
    ErrorSpec { code: "E-TRANSFORM-101", class: "TransformError", cause: Cause::Unsupported, remediation: Some("z_transform is table-based: reduce a[n] to constants, n, n^2, a^n, sin/cos of omega*n, and products of those with a^n or n") },
    ErrorSpec { code: "E-TRANSFORM-102", class: "TransformError", cause: Cause::Unsupported, remediation: Some("inverse_z_transform inverts X(z) whose partial-fraction terms are A*z^p/(z - a)^k with k <= 2; a constant term would invert to the Kronecker delta, for which there is no primitive here") },
    ErrorSpec { code: "E-TRANSFORM-103", class: "TransformError", cause: Cause::UserInput,   remediation: Some("pass distinct symbols for the index and frequency variables") },
    // E-INTERP — SparseInterpError
    ErrorSpec { code: "E-INTERP-001", class: "SparseInterpError", cause: Cause::UserInput, remediation: None },
    ErrorSpec { code: "E-INTERP-002", class: "SparseInterpError", cause: Cause::UserInput, remediation: None },
    ErrorSpec { code: "E-INTERP-003", class: "SparseInterpError", cause: Cause::UserInput, remediation: None },
    ErrorSpec { code: "E-INTERP-004", class: "SparseInterpError", cause: Cause::Domain, remediation: None },
    ErrorSpec { code: "E-INTERP-010", class: "SparseGcdError", cause: Cause::Unsupported, remediation: None },
    ErrorSpec { code: "E-INTERP-011", class: "SparseGcdError", cause: Cause::UserInput, remediation: None },
    ErrorSpec { code: "E-INTERP-012", class: "SparseGcdError", cause: Cause::Internal, remediation: None },
    // E-LIMIT — LimitError
    ErrorSpec { code: "E-LIMIT-001", class: "LimitError", cause: Cause::UserInput, remediation: Some("increase truncation order indirectly by simplifying the expression, or rewrite using standard limits") },
    ErrorSpec { code: "E-LIMIT-002", class: "LimitError", cause: Cause::UserInput, remediation: Some("ensure primitives have differentiation rules, or simplify before taking the limit") },
    ErrorSpec { code: "E-LIMIT-003", class: "LimitError", cause: Cause::UserInput, remediation: Some("use LimitDirection::Plus or Minus matching the desired one-sided approach") },
    ErrorSpec { code: "E-LIMIT-004", class: "LimitError", cause: Cause::Resource, remediation: Some("try manual algebra (quotient form, cancellations) or split into simpler sub-expressions") },
    ErrorSpec { code: "E-LIMIT-005", class: "LimitError", cause: Cause::Unsupported, remediation: Some("limit could not be computed — try manual algebra, or the expression may involve oscillation or non-comparable growth not yet handled") },
    ErrorSpec { code: "E-LIMIT-006", class: "LimitError", cause: Cause::UserInput, remediation: Some("state the sign of the parameter — `Assumptions.refine(pool.gt(k, pool.integer(0)))`, or declare it with `Domain.Positive` — and retry inside `alkahest.context(assumptions=…)`") },
    // E-NFM — NormalFormError
    ErrorSpec { code: "E-NFM-001", class: "NormalFormError", cause: Cause::UserInput, remediation: None },
    ErrorSpec { code: "E-NFM-002", class: "NormalFormError", cause: Cause::Unsupported, remediation: None },
    // E-REC — LinearRecurrenceError
    ErrorSpec { code: "E-REC-001", class: "LinearRecurrenceError", cause: Cause::Unsupported, remediation: None },
    ErrorSpec { code: "E-REC-002", class: "LinearRecurrenceError", cause: Cause::UserInput, remediation: None },
    // E-RES — ResultantError
    ErrorSpec { code: "E-RES-001", class: "ResultantError", cause: Cause::UserInput, remediation: None },
    ErrorSpec { code: "E-RES-003", class: "ResultantError", cause: Cause::UserInput, remediation: None },
    // E-ROOT — RealRootError
    ErrorSpec { code: "E-ROOT-001", class: "RealRootError", cause: Cause::UserInput, remediation: None },
    ErrorSpec { code: "E-ROOT-002", class: "RealRootError", cause: Cause::Domain, remediation: None },
    // E-SUM — SumError
    ErrorSpec { code: "E-SUM-001", class: "SumError", cause: Cause::UserInput, remediation: None },
    ErrorSpec { code: "E-SUM-002", class: "SumError", cause: Cause::UserInput, remediation: None },
    ErrorSpec { code: "E-SUM-003", class: "SumError", cause: Cause::UserInput, remediation: None },
    // E-PARAMGB — ParamGroebnerError (M9, Gröbner bases over Q(params))
    ErrorSpec { code: "E-PARAMGB-001", class: "ParamGroebnerError", cause: Cause::UserInput,   remediation: Some("pass at least one polynomial") },
    ErrorSpec { code: "E-PARAMGB-002", class: "ParamGroebnerError", cause: Cause::UserInput,   remediation: Some("build every generator against the same variable and parameter lists") },
    ErrorSpec { code: "E-PARAMGB-003", class: "ParamGroebnerError", cause: Cause::UserInput,   remediation: Some("supply exactly one value per parameter, in the parameter list's order") },
    // Not a malfunction: the basis is a *generic* one, and this is it saying so
    // at a point where it does not apply, rather than specialising anyway.
    ErrorSpec { code: "E-PARAMGB-004", class: "ParamGroebnerError", cause: Cause::Domain,      remediation: Some("compute the basis directly over ℚ at that parameter point, or move the vanishing factors into the generators and recompute") },
    // E-PROB — ProbError (probability distributions and expectations)
    //
    // 001 is the only one of these that is a *user* error. The rest are the
    // module declining to answer, and they are kept apart because "there is no
    // closed form", "this integrator could not find one", "the quantity does
    // not exist" and "one was found and is not trustworthy" call for four
    // different next steps, and collapsing them would tell a caller to go
    // looking for a better integrator when the integral diverges.
    ErrorSpec { code: "E-PROB-001", class: "ProbError", cause: Cause::UserInput,   remediation: Some("pass a parameter inside the distribution's constraint (σ > 0, a < b, 0 ≤ p ≤ 1, n a literal non-negative integer), or leave it symbolic — a symbolic parameter is carried as a side condition on `constraints` rather than decided") },
    ErrorSpec { code: "E-PROB-002", class: "ProbError", cause: Cause::Unsupported, remediation: Some("reduce the query to a supported shape: a polynomial f for an expectation over a countably infinite discrete support, an affine combination for `expectation_affine`, a moment order within MAX_MOMENT_ORDER") },
    ErrorSpec { code: "E-PROB-003", class: "ProbError", cause: Cause::Unsupported, remediation: Some("the named integral has no antiderivative this integrator found; integrate it numerically, or ask for a moment/CDF that is in the closed-form table") },
    // Distinct from 003: no integrator would help, because the closed form
    // needs a function this library does not implement (the incomplete gamma,
    // the incomplete beta, erf⁻¹, ₁F₁) — or, for the log-normal characteristic
    // function, one that does not exist in closed form at all.
    ErrorSpec { code: "E-PROB-004", class: "ProbError", cause: Cause::Unsupported, remediation: Some("use an integer shape parameter where that collapses the special function to a finite sum (Erlang, integer-parameter Beta), or integrate numerically") },
    // The prime-directive code: a closed form *was* computed and is being
    // withheld because the numeric gate could not confirm it against
    // quadrature of its own defining integral. Never downgraded to a warning —
    // a caller cannot tell a checked value from an unchecked one once it is in
    // their hands.
    ErrorSpec { code: "E-PROB-005", class: "ProbError", cause: Cause::Internal,    remediation: Some("an unconfirmed closed form is withheld rather than returned; report the distribution and query as a minimal failing example") },
    ErrorSpec { code: "E-PROB-006", class: "ProbError", cause: Cause::Domain,      remediation: Some("the integral defining this expectation does not converge — there is no value to return; restrict f so that f(x)·p(x) is integrable over the support") },
    // E-VALIDATED — ValidatedError
    ErrorSpec { code: "E-VALIDATED-001", class: "ValidatedError", cause: Cause::Unsupported, remediation: None },
    ErrorSpec { code: "E-VALIDATED-002", class: "ValidatedError", cause: Cause::UserInput, remediation: None },
    ErrorSpec { code: "E-VALIDATED-003", class: "ValidatedError", cause: Cause::Domain, remediation: None },
    ErrorSpec { code: "E-VALIDATED-004", class: "ValidatedError", cause: Cause::Domain, remediation: None },
    ErrorSpec { code: "E-VALIDATED-005", class: "ValidatedError", cause: Cause::UserInput, remediation: None },
    // E-VEC — VectorError (vector calculus over orthogonal curvilinear charts)
    ErrorSpec { code: "E-VEC-001", class: "VectorError", cause: Cause::Unsupported, remediation: Some("the field contains a function with no differentiation rule; register it in PrimitiveRegistry or rewrite the component") },
    ErrorSpec { code: "E-VEC-002", class: "VectorError", cause: Cause::UserInput,   remediation: Some("pass three distinct coordinate symbols, e.g. Coordinates::cylindrical(rho, phi, z)") },
    ErrorSpec { code: "E-VEC-003", class: "VectorError", cause: Cause::UserInput,   remediation: Some("build each coordinate with `pool.symbol(..)` before constructing Coordinates") },
    // Every grad/div/curl/laplacian formula in `vector` assumes the chart is
    // orthogonal.  On a skew chart they still evaluate — to a number that looks
    // like a divergence and is not one — so an *undecided* inner product is a
    // refusal rather than an assumption.
    ErrorSpec { code: "E-VEC-004", class: "VectorError", cause: Cause::Domain,      remediation: Some("these formulas hold only for orthogonal charts; supply an orthogonal embedding, or work in Cartesian coordinates and transform the result by hand") },
    ErrorSpec { code: "E-VEC-005", class: "VectorError", cause: Cause::Domain,      remediation: Some("restrict the chart to a region where the scale factor is non-zero, or re-parametrise; a chart that collapses a direction has no unique orthonormal frame there") },
    // E-QUAT — QuaternionError (Hamilton quaternions and the rotation operator)
    ErrorSpec { code: "E-QUAT-001", class: "QuaternionError", cause: Cause::Domain,    remediation: Some("use a non-zero quaternion; if the components are symbolic, substitute concrete values or declare the parameters so the norm can be decided") },
    // The identity rotation has no axis — *every* unit vector is one.  Returning
    // the conventional (0,0,1) is a stated answer to a question with no answer.
    ErrorSpec { code: "E-QUAT-002", class: "QuaternionError", cause: Cause::Domain,    remediation: Some("the rotation is the identity — report the angle as 0 and pick whatever axis your convention prefers, explicitly, at the call site") },
    ErrorSpec { code: "E-QUAT-003", class: "QuaternionError", cause: Cause::UserInput, remediation: Some("pass a 3×3 matrix of numbers with RᵀR = I and det R = +1; a reflection (det = −1) is not a rotation and has no quaternion") },
    // E-GFQ — FiniteFieldError (linear algebra over GF(q), q = p^k)
    //
    // 001/002 guard the field itself. A composite modulus gives a ring with zero
    // divisors, where Gaussian elimination can meet a pivot that is non-zero and
    // still not invertible — every "rank" reported over it is a guess, so the
    // field is refused rather than the answer caveated.
    ErrorSpec { code: "E-GFQ-001", class: "FiniteFieldError", cause: Cause::UserInput,   remediation: Some("GF(q) needs a prime characteristic: pass a prime p, or factor the modulus and work over each prime factor separately. Z/nZ for composite n is a ring with zero divisors and has no well-defined rank or nullspace") },
    ErrorSpec { code: "E-GFQ-002", class: "FiniteFieldError", cause: Cause::Unsupported, remediation: Some("the nmod/fq_nmod backend is word-sized: use a prime below 2^64. A multi-precision prime field would need FLINT's fmpz_mod_mat, which is not wrapped") },
    ErrorSpec { code: "E-GFQ-003", class: "FiniteFieldError", cause: Cause::UserInput,   remediation: Some("the extension degree must lie in 1..=64; degree 1 is the prime field itself") },
    ErrorSpec { code: "E-GFQ-004", class: "FiniteFieldError", cause: Cause::UserInput,   remediation: Some("supply a polynomial of degree >= 1 that is irreducible over GF(p); coefficients ascend, so x^3+x+1 over GF(2) is [1, 1, 0, 1]. A reducible modulus would make the quotient a ring with zero divisors") },
    ErrorSpec { code: "E-GFQ-005", class: "FiniteFieldError", cause: Cause::UserInput,   remediation: Some("an element of GF(p^k) has at most k coefficients over the prime subfield; reduce it modulo the defining polynomial at the call site if that is what was meant") },
    ErrorSpec { code: "E-GFQ-006", class: "FiniteFieldError", cause: Cause::UserInput,   remediation: Some("rebuild both operands over the same FiniteField; two fields of the same order but different defining polynomials are isomorphic, not interchangeable — an element's coordinates mean different things in each") },
    ErrorSpec { code: "E-GFQ-007", class: "FiniteFieldError", cause: Cause::UserInput,   remediation: Some("check the shapes: A*B needs A.ncols == B.nrows, add and sub need identical shapes, and solve needs A.nrows == B.nrows") },
    ErrorSpec { code: "E-GFQ-008", class: "FiniteFieldError", cause: Cause::UserInput,   remediation: Some("determinant, charpoly and inverse are defined only for square matrices; for a rectangular one use rank, rref or nullspace") },
    ErrorSpec { code: "E-GFQ-009", class: "FiniteFieldError", cause: Cause::Domain,      remediation: Some("the matrix is singular over this field: use nullspace to see the kernel, or solve, which returns a particular solution whenever one exists") },
    ErrorSpec { code: "E-GFQ-010", class: "FiniteFieldError", cause: Cause::Domain,      remediation: Some("the right-hand side is outside the column space, so no solution exists; check that rank([A|b]) == rank(A)") },
    ErrorSpec { code: "E-GFQ-011", class: "FiniteFieldError", cause: Cause::UserInput,   remediation: Some("indices are zero-based and must satisfy i < nrows and j < ncols") },
    ErrorSpec { code: "E-GFQ-012", class: "FiniteFieldError", cause: Cause::Resource,    remediation: Some("the requested shape exceeds what the FLINT backend can allocate; a dense matrix over GF(q) costs at least one machine word per entry") },
    // E-GRP — GroupError (permutation groups: orbits, Schreier–Sims, membership)
    ErrorSpec { code: "E-GRP-001", class: "GroupError", cause: Cause::UserInput,   remediation: Some("supply each of 0..n exactly once as an image; points are 0-based, so a cycle copied from GAP or the ATLAS needs `Permutation::from_cycles_one_based`") },
    // Degrees are never widened by padding with fixed points: S_3 inside S_5 is a
    // different subgroup of a different symmetric group from S_3 itself, and which
    // one the caller meant is not recoverable from the arguments.
    ErrorSpec { code: "E-GRP-002", class: "GroupError", cause: Cause::UserInput,   remediation: Some("embed the smaller permutation with `Permutation::extend_degree(n)` before composing, or rebuild both at the common degree") },
    ErrorSpec { code: "E-GRP-003", class: "GroupError", cause: Cause::UserInput,   remediation: Some("points are 0-based: the valid range is 0..degree") },
    // A group of order 10^20 has a perfectly computable order and no listable
    // element set. Refusing the list keeps those two facts apart.
    ErrorSpec { code: "E-GRP-004", class: "GroupError", cause: Cause::Resource,    remediation: Some("use `order()`, `contains()` or `random_element()` instead of listing the elements, or raise the cap with `elements_with_cap` if the list really fits in memory") },
    ErrorSpec { code: "E-GRP-005", class: "GroupError", cause: Cause::Resource,    remediation: Some("orbits and orbit representatives are available at any degree; only the stabilizer chain (and therefore `order`, `contains` and `elements`) is capped") },
    // Below its stated minimum, a standard family's natural degree-n action is not
    // faithful, so the group returned would not be the group the name promises.
    ErrorSpec { code: "E-GRP-006", class: "GroupError", cause: Cause::Unsupported, remediation: Some("these constructors build the natural degree-n action; below the stated minimum that action is not faithful, so build the group by hand on the point set you actually mean") },
    // E-FFLD — FunctionFieldError (divisors, Pic⁰ and Riemann–Roch on curves)
    //
    // The whole point of this prefix is the boundary it marks. The implemented
    // class is the *imaginary* hyperelliptic model `y² = a(x)` with `a`
    // squarefree of odd degree and ℚ-rational places, because that is exactly
    // what the Mumford/Cantor machinery in `integrate::algebraic` — which this
    // layer reuses rather than reimplements — is scoped to. Everything outside
    // it is a typed refusal, never a guess.
    //
    // Three of these are *not* refusals and must be kept apart by any caller
    // branching on the code: E-FFLD-007 is a **verdict** (the class has
    // infinite order, and no wider search will change that), E-FFLD-006 is the
    // matching **undecided** (neither torsion nor non-torsion was established),
    // and E-FFLD-011 reports an answer that was computed and then **withheld**
    // because it failed its own consistency check.
    ErrorSpec { code: "E-FFLD-001", class: "FunctionFieldError", cause: Cause::Unsupported, remediation: Some("supply c₂·y² + c₁(x)·y + c₀(x) with c₂ a non-zero rational constant, whose discriminant c₁² − 4c₂c₀ is non-constant; higher-degree plane curves are not modelled") },
    ErrorSpec { code: "E-FFLD-002", class: "FunctionFieldError", cause: Cause::Unsupported, remediation: Some("move the model to odd degree by sending a rational root of a(x) to infinity, or use genus() alone, which is model-independent") },
    ErrorSpec { code: "E-FFLD-003", class: "FunctionFieldError", cause: Cause::Unsupported, remediation: Some("restrict to divisors supported on rational places, or work with the divisor class (Mumford form), which represents conjugate places implicitly") },
    ErrorSpec { code: "E-FFLD-004", class: "FunctionFieldError", cause: Cause::UserInput,   remediation: Some("check the sign and the model: the place must satisfy y² = a(x) in the *normalised* coordinates reported by FunctionField::curve()") },
    ErrorSpec { code: "E-FFLD-005", class: "FunctionFieldError", cause: Cause::UserInput,   remediation: Some("replace D by D − deg(D)·∞ before asking for its class") },
    ErrorSpec { code: "E-FFLD-006", class: "FunctionFieldError", cause: Cause::Unsupported, remediation: Some("record the result as undecided — never as non-torsion; a larger prime search or a different model may settle it") },
    ErrorSpec { code: "E-FFLD-007", class: "FunctionFieldError", cause: Cause::Domain,      remediation: Some("this is a verdict: no multiple of the divisor is principal, so stop looking for one") },
    ErrorSpec { code: "E-FFLD-008", class: "FunctionFieldError", cause: Cause::UserInput,   remediation: Some("pass a non-zero function; div(0) is not defined") },
    ErrorSpec { code: "E-FFLD-009", class: "FunctionFieldError", cause: Cause::UserInput,   remediation: Some("rebuild both operands over the same FunctionField") },
    ErrorSpec { code: "E-FFLD-010", class: "FunctionFieldError", cause: Cause::Resource,    remediation: Some("reduce the multiplicities or the degree of the divisor; the Cantor and linear-algebra steps here are bounded by a machine word") },
    ErrorSpec { code: "E-FFLD-011", class: "FunctionFieldError", cause: Cause::Internal,    remediation: Some("report this as a bug with the curve and divisor that produced it; the answer was withheld rather than returned wrong") },
    // E-STAB — StabilizerError (binary symplectic form, Pauli and stabilizer
    // codes, CSS codes, and the classical matrix groups over GF(q))
    //
    // The prefix exists to keep three different kinds of "no" apart. E-STAB-004,
    // -005 and -006 are *structural*: the generators offered do not define a
    // stabilizer group at all, and no amount of extra work will make them.
    // E-STAB-007 and -008 are about the *question*: a [[n, 0]] code has no
    // minimum distance to report, and above the search cap the distance is not
    // unknown-for-now but unreached — the answer on offer instead is a
    // `Distance::UpperBound`, a different variant, so a caller cannot store a
    // bound in a field it reads as a distance. E-STAB-013 is the one that must
    // never fire: a result computed, failed against its own invariant, and
    // withheld.
    //
    // Note that `StabilizerError::FiniteField` does *not* have a code here. It
    // carries a `FiniteFieldError` out of the GF(q) layer unchanged and
    // delegates `.code()` to it, so a shape or singularity refusal still
    // reports its own `E-GFQ-NNN` rather than being relabelled at the boundary.
    ErrorSpec { code: "E-STAB-001", class: "StabilizerError", cause: Cause::Unsupported, remediation: Some("the Pauli and stabilizer surface is GF(2) only; qudit stabilizer codes are a different theory, not a widening of this one. Only MatrixGroup is defined over general GF(q)") },
    ErrorSpec { code: "E-STAB-002", class: "StabilizerError", cause: Cause::UserInput,   remediation: Some("symplectic vectors have length 2n in (x | z) layout, and Sp membership needs a square matrix of even size; check which half of the vector you filled") },
    ErrorSpec { code: "E-STAB-003", class: "StabilizerError", cause: Cause::UserInput,   remediation: Some("qubit counts are not padded with identities automatically; rebuild both operators on the same number of qubits") },
    ErrorSpec { code: "E-STAB-004", class: "StabilizerError", cause: Cause::Domain,      remediation: Some("a stabilizer group is abelian: the two named generators anticommute, so they share no +1 eigenspace. Check the (x | z) layout first — a (z | x) transcription commutes for a different code") },
    ErrorSpec { code: "E-STAB-005", class: "StabilizerError", cause: Cause::Domain,      remediation: Some("the CSS condition H_X · H_Zᵀ = 0 fails at the named entry; the X-type generator from that row of H_X anticommutes with the Z-type one from that row of H_Z") },
    ErrorSpec { code: "E-STAB-006", class: "StabilizerError", cause: Cause::Domain,      remediation: Some("the named generators multiply to −I, so the stabilized subspace is {0}; negate one of them, or drop the dependent generator") },
    ErrorSpec { code: "E-STAB-007", class: "StabilizerError", cause: Cause::Domain,      remediation: Some("an [[n, 0]] code has no logical qubits, so N(S) \\ S is empty and there is no minimum distance to report") },
    ErrorSpec { code: "E-STAB-008", class: "StabilizerError", cause: Cause::Resource,    remediation: Some("minimum distance is NP-hard and the only algorithm here is exhaustive; raise the cap with minimum_distance_with_cap if 2^(n+k) really is affordable, or take distance_upper_bound() and report it as a bound") },
    ErrorSpec { code: "E-STAB-009", class: "StabilizerError", cause: Cause::Resource,    remediation: Some("reduce the qubit count; every symplectic vector here is dense and the centralizer computation is cubic in n") },
    ErrorSpec { code: "E-STAB-010", class: "StabilizerError", cause: Cause::UserInput,   remediation: Some("Pauli bits must be 0 or 1, letters must be I, X, Y or Z, and a stabilizer generator must be Hermitian — parse a string such as \"-XZZXI\" or use PauliOperator::hermitian, which picks the phase for you") },
    ErrorSpec { code: "E-STAB-011", class: "StabilizerError", cause: Cause::Resource,    remediation: Some("use order(), which is a closed-form product and has no size limit, instead of listing the elements; enumeration scans all q^(d²) matrices") },
    ErrorSpec { code: "E-STAB-012", class: "StabilizerError", cause: Cause::UserInput,   remediation: Some("rebuild both operands over the same FiniteField; two GF(q) of the same order with different defining polynomials are not interchangeable") },
    ErrorSpec { code: "E-STAB-013", class: "StabilizerError", cause: Cause::Internal,    remediation: Some("report this as a bug with the generators or check matrices that produced it; the answer was withheld rather than returned wrong") },
    // E-CODE — CodingError
    //
    // Classical linear codes, weight enumerators and the Delsarte linear
    // programme. Two of these guard the only two ways this subsystem could
    // return a confident wrong answer. E-CODE-004 refuses a truncated
    // codeword enumeration, because the minimum weight of *some* of the
    // codewords is an upper bound on d wearing d's name. E-CODE-007 withholds
    // an LP bound whose dual certificate did not pass its own exact
    // feasibility check — an upper bound that is too small is worse than no
    // bound, since it "rules out" codes that exist.
    ErrorSpec { code: "E-CODE-001", class: "CodingError", cause: Cause::UserInput,   remediation: Some("give the code a positive length; a generator or parity-check matrix needs at least one column") },
    ErrorSpec { code: "E-CODE-002", class: "CodingError", cause: Cause::UserInput,   remediation: Some("pass a minimum distance in 1..=n; d = 0 bounds nothing and d > n describes an empty code") },
    ErrorSpec { code: "E-CODE-003", class: "CodingError", cause: Cause::Domain,      remediation: Some("read the embedded E-GFQ code: the finite-field backend refused the elimination, usually for a field mismatch or a shape that does not conform") },
    ErrorSpec { code: "E-CODE-004", class: "CodingError", cause: Cause::Resource,    remediation: Some("reduce k, or accept that q^k codewords cannot be enumerated; there is no partial answer, because a truncated search reports an upper bound on d as if it were d") },
    ErrorSpec { code: "E-CODE-005", class: "CodingError", cause: Cause::Resource,    remediation: Some("reduce n below MAX_LP_LENGTH; the Delsarte programme is dense and exact, so its cost is superlinear in the length") },
    ErrorSpec { code: "E-CODE-006", class: "CodingError", cause: Cause::UserInput,   remediation: Some("check the weight distribution: a linear code has A_0 = 1, no negative multiplicity, and a MacWilliams sum divisible by |C| at every index") },
    ErrorSpec { code: "E-CODE-007", class: "CodingError", cause: Cause::Internal,    remediation: Some("report this as a bug with n, d and q; the uncertified bound was withheld rather than returned, because an upper bound that is too small rules out codes that exist") },
    ErrorSpec { code: "E-CODE-008", class: "CodingError", cause: Cause::UserInput,   remediation: Some("use an alphabet size of at least 2; for the enumeration paths the field must also be small enough to list element by element") },
    // E-NUMF — NumberFieldError (algebraic number fields Q[x]/(f) on FLINT's
    // `nf`/`nf_elem`, and the cyclotomic fields Q(zeta_n))
    //
    // E-NUMF-003 is the one this module exists for. Q[x]/(f) is a field only
    // when f is irreducible; for a reducible f it is a ring with zero divisors,
    // in which `inverse` has no answer for some non-zero elements and `norm`
    // stops being multiplicative. The polynomial is checked, never assumed.
    ErrorSpec { code: "E-NUMF-001", class: "NumberFieldError", cause: Cause::UserInput,   remediation: Some("supply at least one non-zero coefficient; coefficients ascend in degree, so x^2 - 2 is [-2, 0, 1]") },
    ErrorSpec { code: "E-NUMF-002", class: "NumberFieldError", cause: Cause::UserInput,   remediation: Some("the defining polynomial must have degree in 1..=1024; degree 0 would make Q[x]/(f) the zero ring rather than a field, and degree 1 is Q itself") },
    ErrorSpec { code: "E-NUMF-003", class: "NumberFieldError", cause: Cause::UserInput,   remediation: Some("pass an irreducible polynomial — the error names a proper factor. Factor f and build one field per irreducible factor: Q[x]/(f) for reducible f has zero divisors, so inverses and norms there would be wrong rather than merely unavailable") },
    ErrorSpec { code: "E-NUMF-004", class: "NumberFieldError", cause: Cause::UserInput,   remediation: Some("write each coefficient as a decimal integer or a fraction \"p/q\", without spaces") },
    ErrorSpec { code: "E-NUMF-005", class: "NumberFieldError", cause: Cause::Domain,      remediation: Some("zero has no inverse; test with is_zero() before dividing. In a genuine number field zero is the only non-invertible element, which is why the defining polynomial is checked") },
    ErrorSpec { code: "E-NUMF-006", class: "NumberFieldError", cause: Cause::UserInput,   remediation: Some("rebuild both operands over one NumberField; two fields with different canonical defining polynomials are treated as different even when isomorphic, because an element's coordinates mean different things in each") },
    ErrorSpec { code: "E-NUMF-007", class: "NumberFieldError", cause: Cause::UserInput,   remediation: Some("an element of a degree-d field has at most d coordinates in the power basis 1, a, a^2, ...; reduce modulo the defining polynomial at the call site if that is what was meant") },
    ErrorSpec { code: "E-NUMF-008", class: "NumberFieldError", cause: Cause::UserInput,   remediation: Some("Q(zeta_n) needs n >= 1 with phi(n) <= 1024; note phi(n) is the degree, so n = 2048 is allowed (degree 1024) while n = 3^7 is not") },
    // E-THETA — ThetaError (Riemann theta, modular and Weierstrass functions)
    //
    // This prefix guards a surface whose failure mode is uniquely hard to spot
    // downstream: a theta value is a number nobody can sanity-check by eye, so
    // a wrong *error bound* would travel further than a wrong value. Three of
    // these are therefore about the bound rather than about the input.
    //
    // E-THETA-010 is the load-bearing one. It fires when a value was computed
    // and then **withheld** because its enclosure was too wide to be worth
    // reading. It is reachable in two quite different situations and the
    // `achieved_bits` field is what tells them apart: `Some(n)` with `n` far
    // below the request means more precision may help, while a value that is
    // exactly zero — `j(rho)`, `theta_1(0, tau)` — has no relative accuracy at
    // any precision and will refuse forever. That is not a defect: relative
    // accuracy of zero is not a thing, and `Precision::Bits` plus
    // `ComplexBall::contains_zero` is the right question to ask there.
    //
    // E-THETA-002 exists because `arb_struct` and `acb_struct` cross the FFI
    // boundary by value inside arrays, so a size disagreement with the
    // installed FLINT is silent memory corruption rather than a link error. It
    // is raised by a run-time probe that re-derives the sizes from FLINT itself.
    ErrorSpec { code: "E-THETA-001", class: "ThetaError", cause: Cause::Unsupported, remediation: Some("build against FLINT >= 3.1 for the modular functions and >= 3.4 for genus-g Riemann theta; `riemann_theta_available()` and `arb_backend_available()` report what this build has") },
    ErrorSpec { code: "E-THETA-002", class: "ThetaError", cause: Cause::Internal,    remediation: Some("the installed FLINT's ball struct layout differs from the one this binary was compiled against; rebuild alkahest against the FLINT it will run with, and report the measured sizes as a bug") },
    ErrorSpec { code: "E-THETA-003", class: "ThetaError", cause: Cause::UserInput,   remediation: Some("ask for a working precision between 2 and 1048576 bits") },
    ErrorSpec { code: "E-THETA-004", class: "ThetaError", cause: Cause::Resource,    remediation: Some("theta returns 4^g values, so the output alone is exponential in the genus; genus 1 and 2 are the tested regime") },
    ErrorSpec { code: "E-THETA-005", class: "ThetaError", cause: Cause::UserInput,   remediation: Some("the argument z needs exactly g entries and the period matrix g*g (or g(g+1)/2 for the upper triangle)") },
    ErrorSpec { code: "E-THETA-006", class: "ThetaError", cause: Cause::UserInput,   remediation: Some("build the period matrix with SiegelMatrix::from_upper_triangle, which fills the lower half by copying; symmetry is checked as ball identity, so two separately-computed enclosures of one number are two different inputs") },
    ErrorSpec { code: "E-THETA-007", class: "ThetaError", cause: Cause::Domain,      remediation: Some("this means `not proved`, never `proved false`: raise the working precision, or tighten the input balls until Im(tau) is certainly positive definite") },
    ErrorSpec { code: "E-THETA-008", class: "ThetaError", cause: Cause::UserInput,   remediation: Some("characteristics run over 0..4^g; build the index with theta_characteristic_index rather than by hand") },
    ErrorSpec { code: "E-THETA-009", class: "ThetaError", cause: Cause::Domain,      remediation: Some("the modular functions need Im(tau) certainly > 0; as with E-THETA-007 a refusal means `not proved`") },
    ErrorSpec { code: "E-THETA-010", class: "ThetaError", cause: Cause::Resource,    remediation: Some("read `achieved_bits`: a value far below the request may come good at higher precision, but a quantity that is exactly zero has no relative accuracy at any precision — use Precision::Bits and ComplexBall::contains_zero there") },
    ErrorSpec { code: "E-THETA-011", class: "ThetaError", cause: Cause::Domain,      remediation: Some("the result could not be bounded at all; check for a pole (Weierstrass p at a lattice point) or an input ball that was already indeterminate") },
    ErrorSpec { code: "E-THETA-012", class: "ThetaError", cause: Cause::Resource,    remediation: Some("the midpoint or radius has an exponent outside the range that moves between FLINT and MPFR without rounding; rescale the problem") },
    // E-MATGRP — MatGroupError (matrix groups over GF(q) from generators).
    //
    // This is a `#[non_exhaustive]` enum that *wraps* FiniteFieldError and
    // GroupError through delegating variants, so `E-GFQ-*` and `E-GRP-*` reach
    // a caller of `matgroup` with their own codes. No code below is a duplicate
    // of one of those.
    //
    // E-MATGRP-007 is the one to read twice. Schreier-Sims that runs out of
    // budget must *refuse*, because a chain missing a level reports the product
    // of the orbits it did build: a proper divisor of |G|, indistinguishable
    // from a correct answer for a smaller group. Returning a partial chain is
    // the exact confident-wrong-answer shape this registry exists to prevent.
    // The same reasoning makes E-MATGRP-006 a refusal rather than a truncated
    // orbit.
    ErrorSpec { code: "E-MATGRP-001", class: "MatGroupError", cause: Cause::UserInput,   remediation: Some("group elements are d x d matrices; pass a square matrix") },
    ErrorSpec { code: "E-MATGRP-002", class: "MatGroupError", cause: Cause::UserInput,   remediation: Some("build the matrix at the group's degree; vectors are 1 x d row vectors, because the action is v -> v*M") },
    ErrorSpec { code: "E-MATGRP-003", class: "MatGroupError", cause: Cause::UserInput,   remediation: Some("use the same FiniteField object the group was built with; two isomorphic fields with different defining polynomials are different fields here") },
    ErrorSpec { code: "E-MATGRP-004", class: "MatGroupError", cause: Cause::Domain,      remediation: Some("check the determinant of every generator; a singular matrix generates no group") },
    ErrorSpec { code: "E-MATGRP-005", class: "MatGroupError", cause: Cause::Resource,    remediation: Some("these algorithms store explicit transversals and are built for small degrees; large-degree matrix groups need constructive recognition, which is not implemented") },
    ErrorSpec { code: "E-MATGRP-006", class: "MatGroupError", cause: Cause::Resource,    remediation: Some("an orbit on vectors is bounded by q^d - 1, so this caps q^d and not |G|; use the projective action, whose orbits are (q-1) times smaller") },
    ErrorSpec { code: "E-MATGRP-007", class: "MatGroupError", cause: Cause::Resource,    remediation: Some("raise the budget with MatGroup::with_budget if the group really is this large; no order is reported, because an incomplete chain reports a proper divisor of the true one") },
    ErrorSpec { code: "E-MATGRP-008", class: "MatGroupError", cause: Cause::Resource,    remediation: Some("use order(), contains() or random_element() instead of listing the elements, or raise the cap with elements_with_cap") },
    ErrorSpec { code: "E-MATGRP-009", class: "MatGroupError", cause: Cause::Resource,    remediation: Some("order, membership and the orbit of a given vector never enumerate GF(q); the projective point list, the classical constructors and the centre do") },
    ErrorSpec { code: "E-MATGRP-010", class: "MatGroupError", cause: Cause::Unsupported, remediation: Some("build the group from explicit generators with MatGroup::new; the constructors here cover GL, SL, Sp and Singer cycles only") },
    ErrorSpec { code: "E-MATGRP-011", class: "MatGroupError", cause: Cause::Resource,    remediation: Some("a large centralizing algebra means a very reducible module; split it first, or take the centre of a smaller group") },
    ErrorSpec { code: "E-MATGRP-012", class: "MatGroupError", cause: Cause::Domain,      remediation: Some("pass a non-zero row vector; the zero vector spans no line and is fixed by every matrix") },
    ErrorSpec { code: "E-MATGRP-013", class: "MatGroupError", cause: Cause::Internal,    remediation: Some("this is a bug: please report it with the generators that produced it") },
    // E-FPGRP — FpGroupError (finitely presented groups, Todd-Coxeter,
    // Reidemeister-Schreier, low-degree group cohomology).
    //
    // 004 and 005 are the two codes this whole subsystem is arranged around, and
    // they must never be collapsed. 004 says coset enumeration hit its cap — the
    // word problem is undecidable, so it is *not* a claim that the group is
    // infinite, nor that it is finite. 005 says the group **is** infinite,
    // because its abelianisation has an infinite cyclic factor, which is a
    // terminating Smith normal form. An agent told it can branch on a stable code
    // would, if these shared one, read "I could not decide" as "no such order
    // exists". The (2,3,7) triangle group is infinite with a trivial
    // abelianisation and lands on 004.
    ErrorSpec { code: "E-FPGRP-001", class: "FpGroupError", cause: Cause::UserInput,   remediation: Some("word letters are signed and 1-based: +k is the k-th generator, -k its inverse, and 0 is not a letter; build words with FreeGroup::word or FreeGroup::parse, which check against the rank") },
    ErrorSpec { code: "E-FPGRP-002", class: "FpGroupError", cause: Cause::UserInput,   remediation: Some("generator names must be non-empty, pairwise distinct, free of the reserved characters * ^ ( ) + - and not start with a digit; the rank is capped at MAX_FREE_RANK") },
    ErrorSpec { code: "E-FPGRP-003", class: "FpGroupError", cause: Cause::UserInput,   remediation: Some("the word syntax is a product of powers of generator names and parenthesised sub-words, e.g. a^2, (a*b)^5, a*b^-1*a^-1*b; 1 is the identity") },
    ErrorSpec { code: "E-FPGRP-004", class: "FpGroupError", cause: Cause::Resource,    remediation: Some("raise max_cosets if the group may simply be large, or call abelian_invariants(), which always terminates, to look for an infinite cyclic factor; this refusal is NOT evidence either way about whether the group is finite") },
    ErrorSpec { code: "E-FPGRP-005", class: "FpGroupError", cause: Cause::Domain,      remediation: Some("the group is proved infinite, so no order exists: ask for abelian_invariants() instead of order(), or pass to a finite-index subgroup or a finite quotient") },
    ErrorSpec { code: "E-FPGRP-006", class: "FpGroupError", cause: Cause::Resource,    remediation: Some("lower the coset cap or reduce the number of generators; the table costs 2 * rank machine words per coset") },
    ErrorSpec { code: "E-FPGRP-007", class: "FpGroupError", cause: Cause::UserInput,   remediation: Some("cosets are 0-based and coset 0 is the subgroup H itself; the valid range is 0..index()") },
    ErrorSpec { code: "E-FPGRP-008", class: "FpGroupError", cause: Cause::Unsupported, remediation: Some("only H^0, H^1 and H^2 are implemented; H^2 is the one that classifies extensions, and higher degrees need a resolution this module does not build") },
    ErrorSpec { code: "E-FPGRP-009", class: "FpGroupError", cause: Cause::Resource,    remediation: Some("use a smaller group or module: the degree-(d+1) cochain group has rank(M) * |G|^(d+1) generators, so H^2 is cubic in |G| and is capped at MAX_COCHAIN_DIMENSION") },
    ErrorSpec { code: "E-FPGRP-010", class: "FpGroupError", cause: Cause::UserInput,   remediation: Some("module invariants are non-negative (0 for a Z summand, d >= 1 for Z/d), and there must be one square rank-by-rank action matrix per group generator") },
    ErrorSpec { code: "E-FPGRP-011", class: "FpGroupError", cause: Cause::Domain,      remediation: Some("check that every relator's matrix product is the identity on M and that each generator's matrix maps the module's relation lattice into itself; GModule::trivial cannot fail either check") },
    ErrorSpec { code: "E-FPGRP-012", class: "FpGroupError", cause: Cause::Internal,    remediation: Some("this is a bug in the fp-group subsystem: report it with the presentation that produced it") },
    ErrorSpec { code: "E-FPGRP-013", class: "FpGroupError", cause: Cause::Resource,    remediation: Some("exponents nest, so the cap is on the length of the resulting word and not on any one exponent; reduce the exponent or the length of the base word") },
    ErrorSpec { code: "E-FPGRP-014", class: "FpGroupError", cause: Cause::Resource,    remediation: Some("the table is |G|^2 words and |G|^2 transversal traces; use permutation_group(), whose degree is |G| rather than |G|^2, or take a smaller quotient") },
    // E-CHAR — CharacterError (conjugacy classes and character tables)
    //
    // This prefix guards a surface where a wrong answer is unusually hard to
    // notice: a character table is a small grid of algebraic numbers that looks
    // plausible whatever it says, and downstream code branches on it rather
    // than eyeballing it. So the load-bearing code here is E-CHAR-007, which is
    // a table that was **computed, failed its own orthogonality check, and
    // withheld** — not an input error at all. The orthogonality relations are
    // implied by the group axioms, so a violation is a defect in alkahest, and
    // returning the table with a caveat is not an option: a caller cannot tell
    // a checked table from an unchecked one once it is in their hands.
    //
    // E-CHAR-001 and E-CHAR-003 are the two ceilings, and they are ceilings on
    // different things. 001 is on |G|, because conjugacy classes here are the
    // orbits of G acting on itself and every element is held in memory. 003 is
    // on phi(exp G), the degree of the one cyclotomic field every value lives
    // in — a limit on the arithmetic rather than on the group. The two are
    // independent: a group of small exponent passes 003 at any order and meets
    // 001 instead, while the cyclic group of order 1000 is refused by 003
    // despite being small.
    //
    // A failure raised inside the permutation-group layer, the cyclotomic
    // field or the GF(p) matrix layer keeps its own E-GRP / E-NUMF / E-GFQ code
    // rather than being relabelled: `CharacterError`'s three delegating
    // variants forward `code()` and `remediation()` unchanged.
    ErrorSpec { code: "E-CHAR-001", class: "CharacterError", cause: Cause::Resource,    remediation: Some("conjugacy classes here are found by conjugating every element, so the whole group is held in memory; use `order()` and the stabilizer chain instead, or raise the cap with `ConjugacyClasses::of_with_cap` if |G| images arrays really fit. There is no partial class partition on offer — half the classes of a group is a wrong answer, not a coarse one") },
    ErrorSpec { code: "E-CHAR-002", class: "CharacterError", cause: Cause::Resource,    remediation: Some("the hard ceiling bounds memory, not patience: work with a smaller group, or with the subgroup or quotient whose table is actually wanted") },
    ErrorSpec { code: "E-CHAR-003", class: "CharacterError", cause: Cause::Resource,    remediation: Some("every value lives in Q(zeta_e) for e = exp G, and the orthogonality checks multiply there O(r^3) times, so the cap is on phi(exp G) and not on |G| at all; a group of small exponent passes this ceiling at any order and meets the |G| cap instead, while a cyclic group of order 1000 (phi = 400) is refused here despite being small") },
    ErrorSpec { code: "E-CHAR-004", class: "CharacterError", cause: Cause::Internal,    remediation: Some("Dirichlet guarantees a prime p = 1 mod exp G above |G| exists, so this is a search-bound miss rather than a mathematical obstruction; report the group as a bug") },
    ErrorSpec { code: "E-CHAR-005", class: "CharacterError", cause: Cause::UserInput,   remediation: Some("test with `PermutationGroup::contains` first; points are 0-based here and composition is left-to-right, so a generator transcribed from GAP or the ATLAS needs `Permutation::from_cycles_one_based`") },
    ErrorSpec { code: "E-CHAR-006", class: "CharacterError", cause: Cause::UserInput,   remediation: Some("class and character indices run over 0..len(), with class 0 always the identity class and row 0 always the trivial character") },
    ErrorSpec { code: "E-CHAR-007", class: "CharacterError", cause: Cause::Internal,    remediation: Some("this is a defect in alkahest, not in the input: row and column orthogonality, sum of squared degrees = |G|, and the eigenvalue-multiplicity checksums are all implied by the group axioms, so a violation means the table is wrong and it is withheld rather than returned. Report the generators as a minimal failing example") },
    ErrorSpec { code: "E-CHAR-008", class: "CharacterError", cause: Cause::Internal,    remediation: Some("with p prime to |G| and p = 1 mod exp G the class algebra is split semisimple over GF(p), so the class multiplication matrices must split the space into one line per class; report the generators as a minimal failing example") },
    ErrorSpec { code: "E-CHAR-009", class: "CharacterError", cause: Cause::Internal,    remediation: Some("report the generators as a minimal failing example") },
];

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashSet;

    #[test]
    fn no_duplicate_codes() {
        let mut seen = HashSet::new();
        for spec in REGISTRY {
            assert!(
                seen.insert(spec.code),
                "duplicate error code in REGISTRY: {}",
                spec.code
            );
        }
    }

    #[test]
    fn codes_ascending_within_prefix() {
        let mut by_prefix: std::collections::BTreeMap<&str, Vec<u32>> =
            std::collections::BTreeMap::new();
        for spec in REGISTRY {
            if let Some(pos) = spec.code.rfind('-') {
                let prefix = &spec.code[..pos];
                if let Ok(num) = spec.code[pos + 1..].parse::<u32>() {
                    by_prefix.entry(prefix).or_default().push(num);
                }
            }
        }
        for (prefix, nums) in &by_prefix {
            let mut sorted = nums.clone();
            sorted.sort_unstable();
            assert_eq!(
                nums, &sorted,
                "codes under prefix {prefix} are not in ascending order"
            );
        }
    }
}