@cavelang/solver 0.34.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +2155 -3
  2. package/dist/src/adapter.d.ts +6 -0
  3. package/dist/src/adapter.d.ts.map +1 -1
  4. package/dist/src/adapter.js +3 -0
  5. package/dist/src/adapter.js.map +1 -1
  6. package/dist/src/balanced-reduce.d.ts +3 -0
  7. package/dist/src/balanced-reduce.d.ts.map +1 -0
  8. package/dist/src/balanced-reduce.js +10 -0
  9. package/dist/src/balanced-reduce.js.map +1 -0
  10. package/dist/src/canonical-owned.d.ts +5 -0
  11. package/dist/src/canonical-owned.d.ts.map +1 -0
  12. package/dist/src/canonical-owned.js +206 -0
  13. package/dist/src/canonical-owned.js.map +1 -0
  14. package/dist/src/canonical.d.ts +3 -2
  15. package/dist/src/canonical.d.ts.map +1 -1
  16. package/dist/src/canonical.js +12 -85
  17. package/dist/src/canonical.js.map +1 -1
  18. package/dist/src/capability.d.ts.map +1 -1
  19. package/dist/src/capability.js +43 -33
  20. package/dist/src/capability.js.map +1 -1
  21. package/dist/src/clone.d.ts +3 -0
  22. package/dist/src/clone.d.ts.map +1 -0
  23. package/dist/src/clone.js +54 -0
  24. package/dist/src/clone.js.map +1 -0
  25. package/dist/src/constant-sign.d.ts +5 -0
  26. package/dist/src/constant-sign.d.ts.map +1 -0
  27. package/dist/src/constant-sign.js +54 -0
  28. package/dist/src/constant-sign.js.map +1 -0
  29. package/dist/src/context.d.ts +6 -0
  30. package/dist/src/context.d.ts.map +1 -0
  31. package/dist/src/context.js +151 -0
  32. package/dist/src/context.js.map +1 -0
  33. package/dist/src/diagnostic-text.d.ts +3 -0
  34. package/dist/src/diagnostic-text.d.ts.map +1 -0
  35. package/dist/src/diagnostic-text.js +5 -0
  36. package/dist/src/diagnostic-text.js.map +1 -0
  37. package/dist/src/evaluate-expression.d.ts +4 -0
  38. package/dist/src/evaluate-expression.d.ts.map +1 -0
  39. package/dist/src/evaluate-expression.js +24 -0
  40. package/dist/src/evaluate-expression.js.map +1 -0
  41. package/dist/src/exact.d.ts +2 -0
  42. package/dist/src/exact.d.ts.map +1 -1
  43. package/dist/src/exact.js +125 -28
  44. package/dist/src/exact.js.map +1 -1
  45. package/dist/src/explain.d.ts +4 -0
  46. package/dist/src/explain.d.ts.map +1 -1
  47. package/dist/src/explain.js +330 -91
  48. package/dist/src/explain.js.map +1 -1
  49. package/dist/src/explanation-budget.d.ts +14 -0
  50. package/dist/src/explanation-budget.d.ts.map +1 -0
  51. package/dist/src/explanation-budget.js +46 -0
  52. package/dist/src/explanation-budget.js.map +1 -0
  53. package/dist/src/expression-key.d.ts +8 -0
  54. package/dist/src/expression-key.d.ts.map +1 -0
  55. package/dist/src/expression-key.js +54 -0
  56. package/dist/src/expression-key.js.map +1 -0
  57. package/dist/src/fraction-product.d.ts +3 -0
  58. package/dist/src/fraction-product.d.ts.map +1 -0
  59. package/dist/src/fraction-product.js +13 -0
  60. package/dist/src/fraction-product.js.map +1 -0
  61. package/dist/src/fraction-sum.d.ts +3 -0
  62. package/dist/src/fraction-sum.d.ts.map +1 -0
  63. package/dist/src/fraction-sum.js +23 -0
  64. package/dist/src/fraction-sum.js.map +1 -0
  65. package/dist/src/integer-gcd.d.ts +3 -0
  66. package/dist/src/integer-gcd.d.ts.map +1 -0
  67. package/dist/src/integer-gcd.js +50 -0
  68. package/dist/src/integer-gcd.js.map +1 -0
  69. package/dist/src/linear.d.ts +2 -1
  70. package/dist/src/linear.d.ts.map +1 -1
  71. package/dist/src/linear.js +85 -17
  72. package/dist/src/linear.js.map +1 -1
  73. package/dist/src/numeric-size.d.ts +2 -0
  74. package/dist/src/numeric-size.d.ts.map +1 -0
  75. package/dist/src/numeric-size.js +33 -0
  76. package/dist/src/numeric-size.js.map +1 -0
  77. package/dist/src/options.d.ts +4 -0
  78. package/dist/src/options.d.ts.map +1 -0
  79. package/dist/src/options.js +18 -0
  80. package/dist/src/options.js.map +1 -0
  81. package/dist/src/result.d.ts +4 -0
  82. package/dist/src/result.d.ts.map +1 -0
  83. package/dist/src/result.js +29 -0
  84. package/dist/src/result.js.map +1 -0
  85. package/dist/src/solve.d.ts.map +1 -1
  86. package/dist/src/solve.js +34 -3
  87. package/dist/src/solve.js.map +1 -1
  88. package/dist/src/validate.d.ts +5 -0
  89. package/dist/src/validate.d.ts.map +1 -1
  90. package/dist/src/validate.js +323 -65
  91. package/dist/src/validate.js.map +1 -1
  92. package/dist/src/workflow.d.ts.map +1 -1
  93. package/dist/src/workflow.js +98 -26
  94. package/dist/src/workflow.js.map +1 -1
  95. package/package.json +2 -2
  96. package/src/adapter.ts +9 -0
  97. package/src/balanced-reduce.ts +9 -0
  98. package/src/canonical-owned.ts +186 -0
  99. package/src/canonical.ts +14 -90
  100. package/src/capability.ts +41 -30
  101. package/src/clone.ts +47 -0
  102. package/src/constant-sign.ts +59 -0
  103. package/src/context.ts +114 -0
  104. package/src/diagnostic-text.ts +4 -0
  105. package/src/evaluate-expression.ts +21 -0
  106. package/src/exact.ts +122 -27
  107. package/src/explain.ts +342 -94
  108. package/src/explanation-budget.ts +47 -0
  109. package/src/expression-key.ts +45 -0
  110. package/src/fraction-product.ts +10 -0
  111. package/src/fraction-sum.ts +20 -0
  112. package/src/integer-gcd.ts +47 -0
  113. package/src/linear.ts +82 -16
  114. package/src/numeric-size.ts +26 -0
  115. package/src/options.ts +18 -0
  116. package/src/result.ts +28 -0
  117. package/src/solve.ts +34 -3
  118. package/src/validate.ts +312 -66
  119. package/src/workflow.ts +92 -26
package/README.md CHANGED
@@ -5,6 +5,19 @@ defines exact, serializable models; validation; capability negotiation;
5
5
  resource limits; canonical model digests; result states; and linear-subset
6
6
  recognition. It does not depend on Z3, HiGHS, SQLite, or another CAVE package.
7
7
 
8
+ | Task | Start here |
9
+ |---|---|
10
+ | Submit a model to an adapter | [Quick start](#quick-start) |
11
+ | Choose variable sorts, constraints and objectives | [Portable semantics](#portable-semantics) |
12
+ | Understand exact arithmetic and resource limits | [Exact arithmetic and resource limits](#exact-arithmetic-and-resource-limits) |
13
+ | Validate input and understand model identity | [Validation and identity](#validation-and-identity) |
14
+ | Handle statuses, proof markers and result errors | [Adapter results](#adapter-results) |
15
+ | Trace inputs, assignments and local evaluations | [Provenance and explanations](#provenance-and-explanations) |
16
+ | Compare the shipped adapter with evaluated alternatives | [Backend evaluations](#backend-evaluations) |
17
+ | Run feasibility, optimization, counterexample or sensitivity checks | [Verification workflows](#verification-workflows) |
18
+
19
+ ## Quick start
20
+
8
21
  ```ts
9
22
  import { Adapter, Explain, Model, Solve } from '@cavelang/solver'
10
23
 
@@ -32,10 +45,685 @@ const result = await Solve.run(adapter, model, {
32
45
  })
33
46
  ```
34
47
 
48
+ ## Exact arithmetic and resource limits
49
+
50
+ Local constraint evaluation has a `maxExplanationBits` size preflight and a
51
+ `maxExplanationWork` cumulative arithmetic allowance, described under
52
+ [Validation and identity](#validation-and-identity). These bound estimated
53
+ fraction sizes and charged arithmetic work, not elapsed time, heap or all report work. Earlier benchmark tables retain their
54
+ recorded source revisions and are not acceptance guarantees under this budget.
55
+
56
+ Jump to a topic:
57
+
58
+ - [Balanced reductions](#balanced-reductions)
59
+ - [Fraction comparisons](#fraction-comparisons)
60
+ - [Numeric inputs and expansion limits](#numeric-inputs-and-expansion-limits)
61
+ - [Sum and product cancellation](#sum-and-product-cancellation)
62
+ - [Assignment growth and reuse](#assignment-growth-and-reuse)
63
+ - [Equality and zero-product shortcuts](#equality-and-zero-product-shortcuts)
64
+ - [BigInt normalization and chains](#bigint-normalization-and-chains)
65
+ - [GCD normalization costs](#gcd-normalization-costs)
66
+ - [Independent arithmetic checks](#independent-arithmetic-checks)
67
+
68
+ ### Balanced reductions
69
+
70
+ Linear classification and explanation evaluation reduce rational sums in
71
+ balanced pairs through a shared internal reducer. Exact
72
+ addition is associative, so signs, cancellation and zero-divisor detection are
73
+ preserved, while fewer reductions normalize a large accumulated fraction.
74
+ Operands are still evaluated in their original order before reduction, so
75
+ undefined arithmetic is not hidden by later cancellation. Explanation products
76
+ also use balanced pairs; subtraction and division retain operand order.
77
+
78
+ Run `node scripts/linear-rational-sum-bench.mjs` to measure complete linear
79
+ classification of `x` divided by a sum of reciprocals with distinct 31-digit
80
+ denominators. Each case runs four times; the first is warm-up and the table uses
81
+ the median of the remaining three. Local darwin-arm64 measurements compare the
82
+ prior sequential reduction with the balanced reduction under the same default
83
+ model limits:
84
+
85
+ | Terms | Node 24.21.0 sequential / balanced (ms) | Node 26.8.1 sequential / balanced (ms) |
86
+ |---|---:|---:|
87
+ | 32 | 5.35 / 1.50 | 4.67 / 1.44 |
88
+ | 128 | 104.02 / 8.36 | 93.08 / 7.70 |
89
+ | 512 | 3,103.45 / 66.50 | 2,943.03 / 62.68 |
90
+
91
+ The same benchmark with an `explain` argument evaluates a positive-sum constraint
92
+ through `Explain.report` and checks its reported truth value:
93
+ `node scripts/linear-rational-sum-bench.mjs explain`. With the same denominators
94
+ and sampling method, local explanation measurements were:
95
+
96
+ | Terms | Node 24.21.0 sequential / balanced (ms) | Node 26.8.1 sequential / balanced (ms) |
97
+ |---|---:|---:|
98
+ | 32 | 5.34 / 1.50 | 4.48 / 1.42 |
99
+ | 128 | 91.35 / 7.18 | 84.13 / 7.90 |
100
+ | 512 | 2,791.28 / 49.69 | 2,679.90 / 50.75 |
101
+
102
+ Control workloads are available as a third argument: `equal`, `cancelling`, or
103
+ `integers` (the default is `distinct`). For example,
104
+ `node scripts/linear-rational-sum-bench.mjs explain cancelling` measures adjacent
105
+ opposite fractions and verifies that their sum is zero. A separate comparison
106
+ alternated old/new execution order over seven pairs, discarded the first pair,
107
+ and took the median of six measurements. For 512 terms:
108
+
109
+ | Operation / control | Node 24.21.0 sequential / balanced (ms) | Node 26.8.1 sequential / balanced (ms) |
110
+ |---|---:|---:|
111
+ | Linear / equal denominators | 3.84 / 3.74 | 2.86 / 2.90 |
112
+ | Linear / cancelling pairs | 2.84 / 2.82 | 2.50 / 2.38 |
113
+ | Linear / small integers | 1.74 / 1.65 | 1.62 / 1.59 |
114
+ | Explanation / equal denominators | 5.65 / 5.67 | 5.40 / 5.26 |
115
+ | Explanation / cancelling pairs | 8.13 / 8.14 | 7.37 / 7.70 |
116
+ | Explanation / small integers | 4.21 / 4.13 | 3.98 / 3.97 |
117
+
118
+ These controls show small differences in both directions. The large reduction
119
+ measured for distinct denominators does not imply a speedup for ordinary sums.
120
+
121
+ These are workload-specific observations, not timing gates or general speedup
122
+ guarantees. The largest case uses only 16,384 input digits, illustrating why an
123
+ input-size budget alone is not a time budget. Balanced reduction improves this
124
+ case without adding a general intermediate-size or arithmetic-work limit.
125
+
126
+
127
+ ### Fraction comparisons
128
+
129
+ `Exact.compare` normalizes and validates both operands before taking comparison
130
+ shortcuts. Equal denominators, zero operands and opposite signs compare their
131
+ numerators directly. Equal nonzero numerators use denominator order, reversed
132
+ for positive numerators.
133
+ For same-sign fractions, numerator and denominator order can also reinforce
134
+ one another: a smaller positive numerator over a larger denominator is smaller,
135
+ with the corresponding sign reversal for negative fractions. Exact comparison
136
+ and explanation evaluation return that order without allocating cross-products.
137
+ An exhaustive 10,404-case small-integer regression checks the result against
138
+ direct cross-products, including negative denominators and opposing orders.
139
+ See the [reinforcing-order comparison trial](#reinforcing-order-comparison-trial)
140
+ for reproduction instructions and measured costs.
141
+
142
+ The remaining general path compares the two products directly instead of subtracting
143
+ them to construct another intermediate integer. Run
144
+ `node scripts/exact-cross-product-bench.mjs` alone to exercise this path with
145
+ positive and negative fractions through 100,000 decimal digits. Seven-sample
146
+ local medians remained around 37–39 ms on Node 24.21.0 and 26.8.1 before and
147
+ after removing the subtraction; this is not evidence of a material speedup or
148
+ a general memory/work bound.
149
+ A 5,184-pair signed-fraction regression checks ordering against integer arithmetic,
150
+ including negative denominators and equivalent forms; a zero operand does not
151
+ bypass validation of the other operand.
152
+
153
+ Local explanation comparisons use the same sign, zero and shared-component
154
+ shortcuts on their already-normalized BigInt values. General comparisons still
155
+ form cross-products, but compare those products directly without allocating a
156
+ difference. This reduces temporary arithmetic without changing comparison
157
+ semantics or imposing an intermediate-size budget.
158
+
159
+ For the public `Exact.compare` API, run
160
+ `node scripts/exact-comparison-shortcuts-bench.mjs` alone for five-sample comparisons around powers of ten from 10^10 to 10^100000. At the largest fixture,
161
+ medians before → after were:
162
+
163
+ | Comparison | Node 24.16.0 | Node 26.5.0 |
164
+ |---|---:|---:|
165
+ | Opposite signs | 37.53 → 31.77 ms | 37.07 → 30.19 ms |
166
+ | Equal positive numerators | 37.55 → 35.20 ms | 37.27 → 34.52 ms |
167
+ | Equal negative numerators | 37.49 → 35.23 ms | 37.26 → 34.30 ms |
168
+
169
+ Fixture construction and result assertions are outside timing. Normalization and
170
+ parsing remain part of each comparison. These shortcuts avoid unnecessary large
171
+ products for these cases; they impose no general intermediate-arithmetic budget.
172
+
173
+
174
+ ### Numeric inputs and expansion limits
175
+
35
176
  Exact real literals are decimal strings or numerator/denominator pairs. They
36
177
  are normalized with `bigint`; no decimal is routed through JavaScript floating
37
178
  point. Objective array order is lexicographic priority. CAVE confidence is not
38
179
  part of this model and is never interpreted as a soft-constraint weight.
180
+ Model integer inputs and `Exact.integer` accept primitive safe-integer numbers or complete integer strings;
181
+ arrays, boxed numbers, `bigint` values, and other coercible objects are rejected.
182
+ `Exact.rational` and `Exact.isZero` accept a decimal string or a non-null
183
+ numerator/denominator object. Functions and other non-object containers are rejected before their
184
+ properties are read, even if they expose numerator/denominator getters. Direct
185
+ model validation rejects such callable values rather than accepting a fraction
186
+ that numeric preflight counted as zero digits. A corrected pair is checked against
187
+ the normal numeric allowance; standalone exact arithmetic remains unbudgeted.
188
+ The callable-rejection and corrected-budget checks cover real lower/upper bounds,
189
+ real literals and soft-constraint weights. Zero checks still validate the
190
+ denominator even when the numerator is zero.
191
+ Integer and decimal strings must consume the whole input, including any final
192
+ line break. Decimal zero normalizes directly to `0/1` after exponent validation,
193
+ without allocating a power of ten; exponents must still be safe integers.
194
+ For nonzero values, safe-integer exponent syntax does not guarantee that the
195
+ expanded numerator or denominator fits the runtime BigInt limit. Such expansion
196
+ can throw `RangeError`; it does not change later exact computations. This is a
197
+ runtime representability limit, not a portable numeric-size or time budget.
198
+
199
+ `Exact.compare` normalizes both operands and returns `-1`, `0` or `1`; even a
200
+ comparison against zero validates the other operand's denominator. `Exact.isZero`
201
+ checks integer/decimal syntax and the denominator without expanding decimal
202
+ powers or parsing large integer magnitudes when text suffices. For example,
203
+ `Exact.isZero('1e9007199254740991')` returns `false`, although expanding that value
204
+ with `Exact.rational` exceeds practical runtime representability. A successful
205
+ zero check therefore establishes zero/nonzero identity, not that normalization
206
+ or later arithmetic will fit within a caller's time or memory allowance.
207
+
208
+ `Exact.fromBigInts(numerator, denominator)` normalizes existing bigint
209
+ intermediates directly, returning the same decimal-string `Normalized` pair as
210
+ `Exact.rational`. For example, `Exact.fromBigInts(6n, -8n)` returns
211
+ `{ numerator: '-3', denominator: '4' }`. It rejects non-bigint arguments and a
212
+ zero denominator, preserves canonical zero, and shares rational parsing's GCD
213
+ implementation. Scenario arithmetic, linear constant evaluation and explanation
214
+ arithmetic use this entry point to avoid converting intermediates to strings
215
+ and parsing them again. This does not extend model
216
+ integer syntax or impose a size/work budget on standalone Exact helpers.
217
+
218
+ Reproduce expansion costs with `node scripts/exact-number-bench.mjs` from the
219
+ repository root. Each fixed case runs in a fresh child with a 10-second timeout
220
+ and a 128 MB V8 old-space setting. That setting is not a total-process memory
221
+ cap. Timing covers rational normalization and decimal output conversion, excluding
222
+ startup and result assertions. Reported RSS is sampled afterward, includes the
223
+ runtime and imports, and is not peak or incremental memory.
224
+
225
+ A local darwin-arm64 run on 2026-09-08 measured:
226
+
227
+ | Input | Expanded denominator digits | Node 24.16.0 (ms) | Node 26.5.0 (ms) |
228
+ |---|---:|---:|---:|
229
+ | `1e-1000` | 1,001 | 0.184 | 0.191 |
230
+ | `1e-10000` | 10,001 | 0.367 | 0.382 |
231
+ | `1e-100000` | 100,001 | 6.046 | 6.066 |
232
+ | `1e-1000000` | 1,000,001 | 93.512 | 91.599 |
233
+
234
+ These are observations, not CI thresholds. The last input is only 10 bytes.
235
+ Model validation now limits aggregate numeric expansion with `maxNumericDigits`
236
+ (default 100,000), before constructing BigInts for each bound, literal occurrence
237
+ or soft-constraint weight. Direct validation captures each numeric value before
238
+ budgeting and parsing it. Canonical serialization (including digests) and linear
239
+ analysis capture limits once, validate the model, copy it, then revalidate the
240
+ copy under the same limits before their own arithmetic. This
241
+ preserves early structural checks and prevents changing getters from substituting
242
+ an unchecked larger value. A bound that grows during copying is checked against
243
+ the initial numeric budget even if its limit getter would later return a larger
244
+ allowance. The default budget rejects the last two benchmark inputs. The standalone `Exact` helpers remain unrestricted, which lets
245
+ this benchmark measure their expansion directly.
246
+
247
+ The budget counts integer digits without the sign, and both fraction components.
248
+ For decimal strings it counts coefficient digits plus exponent-added zeros and
249
+ the denominator's digits before reduction; leading coefficient zeros count too.
250
+ Zero skips exponent expansion but still counts its coefficient and denominator.
251
+ Exponent spelling is not included in this expansion count: `1e0000` and `1`
252
+ each cost two digits. Consequently, this limit is not an input-text length
253
+ limit; leading exponent zeros still require parsing and capture.
254
+ Shared literal objects count at every expression occurrence, and costs accumulate
255
+ across all bounds, constraints, objectives and soft weights in one validation.
256
+ Repeated squaring does not bypass that accounting through shared object identity.
257
+ A divisor built by squaring one shared 300-digit real literal eight times counts
258
+ 256 literal occurrences and fits the default budget; a ninth squaring counts
259
+ 512 occurrences and exceeds it. Canonical serialization, digests and linear
260
+ analysis reject the larger model during validation. This checks one amplification
261
+ pattern, not a general bound on intermediate arithmetic work.
262
+ Exceeding the budget throws `ModelLimitError` for `maxNumericDigits` before backend
263
+ execution; an explicit larger positive safe-integer limit permits larger models.
264
+ This bounds numeric representation size, not all arithmetic time, intermediate
265
+ expression results or standalone `Exact` calls. Backend memory and timeout
266
+ limits remain separate protections.
267
+ Sensitivity additionally checks the combined numeric text in all samples and
268
+ fixed bindings against `maxNumericDigits` before exact normalization. This
269
+ request budget is separate from the model's budget and counts padded digits
270
+ and every occurrence. Exceeding it throws `WorkflowValidationError` before any
271
+ backend call; a larger explicit limit permits a larger request.
272
+ Sensitivity also validates every generated sample model, including fixed
273
+ bindings and workflow objectives, before starting the batch. A model and request
274
+ can each fit their separate budgets while their combined per-sample model
275
+ exceeds a limit; that failure now occurs before any earlier sample is solved.
276
+ ### Sum and product cancellation
277
+
278
+ Equal normalized denominators now avoid cross-products during exact comparison
279
+ and addition/subtraction in linear constant evaluation and explanations. Addition
280
+ still reduces the resulting numerator/denominator, including exact cancellation
281
+ to zero. This avoids squaring a common denominator solely to reduce it again;
282
+ it does not bound
283
+ all intermediate arithmetic or change the numeric-input budget.
284
+
285
+ Run `node scripts/exact-intermediate-bench.mjs` to reproduce this case across
286
+ 1,000-, 10,000- and 25,000-digit denominators. Each operation/size runs in a fresh
287
+ child with a 10-second timeout and a 128 MB V8 old-space setting (not a total
288
+ process cap). The report contains five timed calls and their median; timing
289
+ excludes fixture creation, process startup and assertions, but includes the full
290
+ comparison, linear analysis or explanation operation. RSS is sampled afterward,
291
+ not a peak-memory measurement. Every call checks its exact result. Measurements
292
+ are observations, not CI thresholds.
293
+
294
+ For 25,000-digit denominators on darwin-arm64, a local 2026-09-08 run measured:
295
+
296
+ | Operation | Node 26.5.0 before (ms) | Node 26.5.0 after (ms) | Node 24.16.0 after (ms) |
297
+ |---|---:|---:|---:|
298
+ | Exact comparison | 2.247 | 1.771 | 1.727 |
299
+ | Linear analysis | 12.328 | 7.443 | 7.581 |
300
+ | Explanation | 15.301 | 10.923 | 11.114 |
301
+
302
+ For unequal denominators, addition/subtraction now probes for a shared factor
303
+ before allocating cross-products when both denominators have at least 1,025
304
+ bits. The probe takes at most six Euclidean remainders. If it finishes with a
305
+ nontrivial GCD, the sum uses the least common denominator; otherwise the ordinary
306
+ cross-products remain. Final normalization is unchanged. This deliberately
307
+ bounds the number of probe steps, not their time or the resulting arithmetic.
308
+ A full preliminary GCD was rejected: on the Fibonacci-denominator fixture below
309
+ it roughly doubled complete-operation time despite the newer batched GCD.
310
+
311
+ `node scripts/exact-sum-bench.mjs` measures addition and subtraction through
312
+ linear analysis and explanations for shared denominators `3N, 5N`, nearby
313
+ coprime denominators `N, N+2`, and consecutive Fibonacci denominators, at roughly
314
+ 1,000, 10,000 and 20,000 digits. Here `N` is a power of ten plus one. It uses the
315
+ same isolated-child, five-call median, timeout and memory methodology above;
316
+ fixture construction is excluded and each call checks classification or the
317
+ constraint evaluation. Signed, zero and exact identity regressions separately
318
+ check the arithmetic. On darwin-arm64 with Node 26.5.0 on 2026-09-08, the largest
319
+ cases measured:
320
+
321
+ | Operation | Denominators | Linear before/after (ms) | Explanation before/after (ms) |
322
+ |---|---|---:|---:|
323
+ | Add | Shared | 8.847 / 6.256 | 10.874 / 8.800 |
324
+ | Add | Nearby coprime | 11.723 / 12.961 | 17.010 / 17.828 |
325
+ | Add | Fibonacci | 21.915 / 23.651 | 30.207 / 29.920 |
326
+ | Subtract | Shared | 8.766 / 5.867 | 12.060 / 9.390 |
327
+ | Subtract | Nearby coprime | 9.453 / 9.109 | 14.440 / 14.464 |
328
+ | Subtract | Fibonacci | 26.294 / 22.226 | 33.921 / 31.998 |
329
+
330
+ Node 24.16.0 shared-denominator addition/subtraction medians were
331
+ 6.017/6.089 ms for linear analysis and 8.280/9.203 ms for explanations. These
332
+ observations show a shared-factor benefit and mixed coprime results, not a
333
+ universal speedup. A common factor requiring more than six remainders to find
334
+ is intentionally left to final normalization; general intermediate growth
335
+ remains outside the numeric-input budget.
336
+
337
+ Version 2 of this harness adds untimed exact-magnitude controls. For the
338
+ fixture's `x = 1`, the reciprocal of `1/b + 1/d` is `b*d/(b+d)`; subtraction
339
+ uses `b*d/(d-b)`. Direct BigInt arithmetic and a separate Euclidean reduction
340
+ construct the expected assignment. Equality must accept that value and reject
341
+ the value plus one under default model and explanation limits. The three-family checkpoint passes all 36 workloads
342
+ and 72 magnitude controls on Node 24.21.0 and 26.8.1; [complete reports and
343
+ source hashes](../../benchmarks/sum-magnitude-review.json) retain the samples.
344
+ The original timed calls are unchanged. Sampled RSS now includes the extra
345
+ reports and is not directly comparable to older observations. These checks
346
+ cover the selected shared, nearby and Fibonacci denominators, not every
347
+ common-factor or cancellation pattern.
348
+
349
+ The current sum harness also covers delayed shared factors with denominators
350
+ `13N, 21N`. Their GCD takes six Euclidean remainders, so the former four-step
351
+ probe fell back to a denominator containing `N` twice. A six-step limit finds
352
+ the factor and avoids that squared intermediate; longer probes still stop and
353
+ use the ordinary exact path. Signed and reversed-denominator regressions retain
354
+ exact values and zero-divisor classification.
355
+
356
+ An isolated trial compared four, six and eight steps. Six alternating paired
357
+ runs at 20,000 digits confirm 15–18% lower delayed-sharing explanation medians
358
+ with six steps; Fibonacci controls range from 0.8% faster to 1.9% slower.
359
+ Six is the smallest tested limit that finds this factor, with less speculative
360
+ coprime work than eight. [Full samples, candidate sources and oracle results](../../benchmarks/six-step-denominator-probe-review.json)
361
+ retain all 48 workloads per variant/major, 96 untimed magnitude controls per
362
+ full run, and the focused paired trials. The selected implementation passes
363
+ all 16,416 independent Python Fraction checks on each supported Node major.
364
+ Timing trials ran without concurrent builds/tests. This is a measured tradeoff,
365
+ not a general speedup or time limit; the older tables retain their four-step
366
+ implementation scope.
367
+
368
+ Run `node scripts/fraction-sum-probe-bench.mjs` alone to reproduce the comparison
369
+ without editing workspace sources. It creates isolated four-, six- and eight-step
370
+ copies, focuses on 20,000-digit delayed-shared and Fibonacci explanations, and
371
+ rotates all six variant orders. Its six default rounds balance execution order;
372
+ `--rounds 1` is a shorter functional smoke check, not stable timing evidence.
373
+ The script records source/variant fingerprints and complete reports, validates
374
+ workload medians, and removes its copies before returning. [Driver verification](../../benchmarks/probe-reproducer-review.json)
375
+ records successful default runs on both Node majors, with 72 workload measurements
376
+ and 144 magnitude controls per major. These use a different balanced order from
377
+ the earlier two-variant paired trial; its historical measurements stay intact.
378
+
379
+ Linear constant multiplication/division and explanation arithmetic cancel common
380
+ factors across numerators and denominators before forming products. The resulting
381
+ fractions still pass through the existing normalizer, preserving signs, exact
382
+ zero and computed division-by-zero behavior. This reduces intermediates when
383
+ factors cancel; coprime products still grow to their full exact representation.
384
+
385
+ `node scripts/exact-product-bench.mjs` measures cancelling and coprime products
386
+ and quotients through complete linear-analysis and explanation calls at 1,000,
387
+ 10,000 and 20,000 digits. It uses the same five-call median, isolated-child,
388
+ timeout, V8 old-space and sampled-RSS methodology described above. Results are
389
+ checked after timing, and model construction is excluded. Its JSON output includes
390
+ the child timeout, V8 old-space setting, sample count and timing/RSS scope, so
391
+ exported observations retain their measurement limits. On darwin-arm64 with
392
+ Node 26.5.0, a 2026-09-08 run measured these 20,000-digit cases:
393
+
394
+ | Operation | Fractions | Linear before/after (ms) | Explanation before/after (ms) |
395
+ |---|---|---:|---:|
396
+ | Multiply | Cancelling | 12.248 / 8.023 | 14.849 / 10.847 |
397
+ | Multiply | Coprime | 9.012 / 9.017 | 12.882 / 12.929 |
398
+ | Divide | Cancelling | 13.303 / 9.873 | 14.719 / 10.711 |
399
+ | Divide | Coprime | 9.576 / 9.588 | 12.741 / 12.823 |
400
+
401
+ These observations show the intended cancellation benefit and the measured
402
+ coprime tradeoff; they do not establish a speedup for every input or add a general
403
+ arithmetic budget. Standalone `Exact` parsing retains its existing behavior.
404
+
405
+ Version 2 of the product harness also runs two untimed magnitude controls per
406
+ case. With the fixture's assignment `x = 1`, the reciprocal of a cancelling
407
+ product/quotient must equal one; the coprime fixture's reciprocal must equal
408
+ `n * m`, computed directly with BigInt. An exact-equality report must satisfy
409
+ that value and reject the value plus one. These checks use default model and
410
+ explanation limits and catch incorrect positive magnitudes that the original
411
+ sign-only predicate could miss. All 24 workloads and 48 magnitude controls pass
412
+ on each supported Node major; [complete reports and source hashes](../../benchmarks/product-magnitude-review.json)
413
+ retain the evidence. Timed workloads remain unchanged, but sampled RSS now
414
+ includes the extra control reports and is not directly comparable with earlier
415
+ RSS observations. This verifies the selected cancelling/coprime fixtures, not
416
+ all intermediate values or arithmetic workloads.
417
+
418
+ Both version-2 sum/product harnesses retain their five timing samples in execution
419
+ order and explicitly report zero discarded warm-up samples. The median is
420
+ computed from a sorted copy, so inspecting the exported samples still reveals
421
+ first-call effects and variation. Their metadata states that sampled RSS includes
422
+ the untimed magnitude controls. A [sample-order checkpoint](../../benchmarks/rational-sample-order-review.json)
423
+ verifies all 120 workload records, 600 retained samples and 240 magnitude
424
+ controls across both Node majors; each reported median matches its samples.
425
+ Earlier saved reports retain their original sorted samples and source hashes.
426
+
427
+ ### Assignment growth and reuse
428
+
429
+ Repeated assignment values can produce intermediates larger than the model's
430
+ numeric-input budget. Run `node scripts/explanation-intermediate-bench.mjs`
431
+ alone from the repository root to measure this separately from backend search.
432
+ It uses powers of ten with 64, 512 or 4,096 digits and 1, 16 or 64 variable
433
+ occurrences in a positive-product constraint, under default limits. One warmup
434
+ and five timed complete explanation calls exclude fixture construction and
435
+ assertions. For the 4,096-digit assignment, the model contains 8,193 numeric
436
+ digits regardless of occurrence count. Before keeping normalized intermediates
437
+ as bigints, the measurements were:
438
+
439
+ | Occurrences | Exact product digits | Node 24.21.0 median (ms) | Node 26.8.1 median (ms) |
440
+ |---:|---:|---:|---:|
441
+ | 1 | 4,096 | 0.89 | 0.86 |
442
+ | 16 | 65,521 | 33.37 | 30.39 |
443
+ | 64 | 262,081 | 652.80 | 654.76 |
444
+
445
+ [Raw measurements](../../benchmarks/explanation-intermediate-runtime-review.json)
446
+ record all nine cases on darwin-arm64. Product digit counts follow algebraically
447
+ from the powers of ten; the explanation reports only the constraint result and
448
+ original assignment. These timings include validation, digesting, capture and
449
+ arithmetic, and do not isolate multiplication cost or impose a resource cap.
450
+ Each explanation report now reuses normalized assigned values across its hard
451
+ and soft constraints. Normalization is lazy, on the first evaluated reference;
452
+ the cache belongs to that report's captured assignment and is discarded afterward.
453
+ A new report sees updated values even when a caller reuses the assignment object.
454
+ This does not cache expression results or bypass operand evaluation.
455
+
456
+ Using the equality workload below with 64 comparisons of 16384-digit fractions,
457
+ per-report reuse reduced unequal-case medians from 175.22/172.99 ms to 3.98/3.89 ms
458
+ on Node 24.21.0/26.8.1. Equivalent cases fell from 175.72/174.27 ms to 4.07/4.01 ms.
459
+ [Assignment reuse samples](../../benchmarks/explanation-assignment-reuse-review.json)
460
+ retain all runs. Use `node scripts/explanation-equality-bench.mjs` alone to reproduce
461
+ the current implementation. Results reflect repeated references to two variables;
462
+ models with many distinct assignments or expensive expression intermediates have
463
+ different costs. The following equality table records the preceding implementation.
464
+
465
+ ### Equality and zero-product shortcuts
466
+
467
+ Explanation numeric equality and inequality compare the reduced numerator and
468
+ positive denominator directly, avoiding cross-products. Ordering comparisons
469
+ retain their separate arithmetic path. Sign, zero and differently scaled
470
+ fraction regressions verify the canonical-pair invariant.
471
+
472
+ `node scripts/explanation-equality-bench.mjs` measures 64 alternating equality
473
+ and inequality constraints over near-one fractions, including capture,
474
+ normalization, validation and digesting. For unequal 16384-digit fractions,
475
+ complete-report medians fell from 195.01 to 175.45 ms on Node 24.21.0 and from
476
+ 194.04 to 172.72 ms on Node 26.8.1. Equivalent-fraction controls were
477
+ 176.14 → 175.67 ms and 180.67 → 173.86 ms. Each case uses one warmup and five
478
+ samples; fixtures and assertions are outside timing, and no backend solver runs.
479
+ [Raw equality comparison](../../benchmarks/explanation-equality-review.json)
480
+ records the fixed-order runs. These bounded cases do not establish general
481
+ speedups or a work limit; repeated operand normalization remains part of the cost.
482
+
483
+ Explanation multiplication evaluates and type-checks every operand before
484
+ checking for zero. If any evaluated numerator is zero, it returns exact zero
485
+ without multiplying the nonzero operands together. Errors such as computed
486
+ division by zero still produce an indeterminate constraint even when another
487
+ operand is zero; hard and soft constraint regressions cover both operand orders.
488
+
489
+ Run `node scripts/explanation-zero-product-bench.mjs` alone to compare zero-first
490
+ and zero-last products with 16/64 nonzero operands of 512/4096 digits. Each case
491
+ uses one warmup and five timed complete reports, excluding fixture construction,
492
+ assertions and backend solving. For 64 nonzero 4096-digit operands plus a final
493
+ zero, medians fell from 32.02 to 3.27 ms on Node 24.21.0 and from 31.77 to 3.32 ms
494
+ on Node 26.8.1. Zero-first controls were 3.28 → 3.64 ms and 3.22 → 3.37 ms;
495
+ small timing changes and fixed run order do not establish a universal speedup.
496
+ [Raw zero-product comparison](../../benchmarks/explanation-zero-product-review.json)
497
+ retains every sample. Nested operand evaluation and nonzero products can still
498
+ grow large; this optimization does not impose a general arithmetic work cap.
499
+
500
+ ### BigInt normalization and chains
501
+
502
+ Explanation normalization now reduces bigint intermediates directly using the
503
+ existing integer GCD, preserving positive denominators and exact zero without
504
+ serializing and reparsing after each arithmetic operation. Public exact-number
505
+ representations and explanation assignments remain unchanged. In the same
506
+ sequential benchmark, the 4,096-digit, 64-occurrence case falls to 31.86 ms on
507
+ Node 24 and 31.53 ms on Node 26, about 95% below the earlier measurements. The
508
+ 16-occurrence case takes 3.19/3.21 ms; the single-occurrence control takes
509
+ 0.82/0.85 ms. [After measurements](../../benchmarks/explanation-bigint-normalization-review.json)
510
+ retain all samples. This avoids conversion overhead; it does not bound bigint
511
+ growth or guarantee the same improvement for other expression shapes.
512
+
513
+ A sequential runtime-refresh check on 2026-09-09 passed all 24 workloads on
514
+ Node 24.21.0 and Node 26.8.1 (darwin-arm64). The 20,000-digit medians were:
515
+
516
+ | Operation | Fractions | Node 24 linear / explanation (ms) | Node 26 linear / explanation (ms) |
517
+ |---|---|---:|---:|
518
+ | Multiply | Cancelling | 8.409 / 11.097 | 8.294 / 10.784 |
519
+ | Multiply | Coprime | 9.285 / 11.524 | 9.078 / 10.961 |
520
+ | Divide | Cancelling | 9.686 / 11.092 | 9.517 / 10.783 |
521
+ | Divide | Coprime | 7.616 / 9.165 | 7.365 / 8.857 |
522
+
523
+ [Full samples and runtime metadata](../../benchmarks/exact-product-runtime-review.json)
524
+ retain all fixture sizes. These observations verify the current implementation
525
+ on refreshed runtimes; they are not an isolated runtime-speed comparison or a
526
+ new release budget. Recorded RSS is measured after each workload, not at its peak.
527
+
528
+ Direct BigInt normalization removes an additional decimal serialization/parse
529
+ round trip from linear and explanation intermediates. Sequential before/after
530
+ trials on 2026-09-09 measured these 20,000-digit coprime cases:
531
+
532
+ | Operation | Node 24.21.0 before / after (ms) | Node 26.8.1 before / after (ms) |
533
+ |---|---:|---:|
534
+ | Multiply, linear analysis | 9.992 / 7.489 | 9.088 / 7.244 |
535
+ | Multiply, explanation | 11.549 / 9.537 | 11.106 / 9.144 |
536
+ | Divide, linear analysis | 7.837 / 5.807 | 7.357 / 5.596 |
537
+ | Divide, explanation | 9.293 / 7.555 | 8.711 / 6.849 |
538
+
539
+ [Full before/after samples](../../benchmarks/direct-intermediate-review.json)
540
+ include cancelling controls and smaller fixtures. Cancelling 20,000-digit
541
+ cases changed little, with both increases and decreases (the largest increase
542
+ was 0.529 ms). This is a measured reduction in redundant conversion work, not
543
+ a speedup for every input or a bound on intermediate growth. Exact signs,
544
+ zero and denominator validation retain the existing normalization semantics.
545
+
546
+ Linear analysis also retains bigint intermediates across constant multiplication
547
+ chains, cross-cancelling each factor and normalizing/serializing the aggregate
548
+ once. Every operand is still evaluated before accumulation, including operands
549
+ after a zero factor. `node scripts/exact-product-chain-bench.mjs` measures 2, 10
550
+ and 18 separately allocated unit fractions with 1,000- or 5,000-digit
551
+ denominators, all within the default numeric input budget. The
552
+ [paired samples](../../benchmarks/exact-product-chain-review.json) include linear
553
+ analysis and unchanged explanation controls, isolated child timeouts and source
554
+ hashes. Add `--baseline` to run the prior linear implementation through a
555
+ benchmark-only child loader. It verifies the restored source against the recorded
556
+ hash and does not edit runtime files; unknown command options fail before any
557
+ workload starts. Run baseline and current separately with the intended Node
558
+ version. The artifact also retains a later rerun using this loader.
559
+ For 5,000-digit denominators, the original linear-analysis medians were:
560
+
561
+ | Factors | Node 24.21.0 before / after (ms) | Node 26.8.1 before / after (ms) |
562
+ |---|---:|---:|
563
+ | 2 | 1.20 / 1.20 | 1.27 / 1.23 |
564
+ | 10 | 22.23 / 10.56 | 21.99 / 10.59 |
565
+ | 18 | 82.62 / 25.22 | 81.89 / 25.38 |
566
+
567
+ These local measurements show reduced conversion work for longer chains; they
568
+ do not impose a general intermediate-size or execution-time bound. The fixture
569
+ checks linearity and the satisfied constraint outcome. Separate exact-chain
570
+ regressions check signed/zero products by subtracting a directly computed rational
571
+ reference and testing the resulting zero/nonzero divisor.
572
+
573
+ ### GCD normalization costs
574
+
575
+ The digit budget is not a normalization-time guarantee. Consecutive Fibonacci
576
+ fractions exercise long Euclidean GCD paths even though their result is already
577
+ reduced. `node scripts/exact-gcd-bench.mjs` measures normalization and full model
578
+ validation for such fractions using fast-doubling fixture construction outside
579
+ the timed calls. Every case asserts that its input fits the default numeric
580
+ budget; normalization results must equal the original reduced fraction.
581
+
582
+ Each operation/index runs in an isolated child with a 10-second allowance for
583
+ fixture creation and up to three timed calls, and a 128 MB V8 old-space setting
584
+ (not a total-process cap). Completed cases report the median. A timed-out child
585
+ reports `status: "timeout"`, its fixture size and any completed samples; the
586
+ benchmark continues so one timeout does not erase earlier evidence. Timeout is
587
+ not evidence that any single call consumed the entire allowance. RSS is sampled
588
+ after completed cases and is not peak memory.
589
+
590
+ Normalization and product cross-cancellation share a private exact GCD helper.
591
+ For operands of at least 1,025 bits it uses leading 64-bit integers to collect
592
+ [Lehmer-style Euclidean steps](https://gmplib.org/manual/Lehmer_0027s-Algorithm)
593
+ into a matrix. Every step has determinant -1, preserving the GCD. A batch is
594
+ accepted only when applying it to the full integers produces nonnegative,
595
+ ordered, strictly smaller operands; otherwise one full Euclidean remainder
596
+ ensures progress. Batches contain at most 64 steps. Smaller operands retain the
597
+ ordinary remainder loop to avoid batching overhead. All calculations use BigInt;
598
+ there is no approximation of the fraction or change to numeric limits.
599
+
600
+ On darwin-arm64 on 2026-09-08, Fibonacci indices 200,000/200,001 produced a
601
+ fraction with 83,596 total digits, within the default 100,000-digit budget.
602
+ Before batching, each largest child timed out before completing all three
603
+ samples. Its one completed normalization/validation sample took
604
+ 7,211.674/8,664.899 ms on Node 26.5.0 and 8,555.076/7,549.708 ms on Node 24.16.0.
605
+ With batching, all cases completed: the largest normalization/validation medians
606
+ were 113.473/113.173 ms on Node 26 and 112.404/112.452 ms on Node 24. The smaller
607
+ 50,000/50,001 case took 13.258/13.342 ms on Node 26, versus the earlier
608
+ 308.771/306.582 ms medians. These are local measurements, not latency guarantees;
609
+ normalization remains synchronous and standalone Exact helpers have no work cap.
610
+
611
+ `node scripts/exact-gcd-strategies-bench.mjs` compares the helper against the
612
+ previous Euclidean loop on deterministic coprime, shared-factor, unit and equal
613
+ inputs at 32, 128, 1,024 and 8,192 bits. Each measurement is the median of seven
614
+ passes over 100 preconstructed pairs, with exact agreement checked outside the
615
+ timed calls. On the same machine, 8,192-bit coprime/shared-factor batches took
616
+ 80.681/85.361 ms versus 401.557/418.195 ms for Euclid on Node 26, and
617
+ 87.666/89.227 ms versus 411.694/428.568 ms on Node 24. At 1,024 bits, coprime
618
+ batches took 6.273 versus 6.022 ms on Node 26 and 6.338 versus 6.026 ms on Node
619
+ 24; batching does not improve every workload. Sub-millisecond ordinary cases
620
+ are especially sensitive to runtime warmup and measurement noise. This benchmark
621
+ compares GCD strategies directly; the Fibonacci benchmark includes parsing,
622
+ string conversion and, for validation, the full model validation call.
623
+
624
+ A [supported-runtime checkpoint](../../benchmarks/exact-resource-current-review.json)
625
+ reruns both trials sequentially on Node 26.8.1 and 24.21.0. Every Fibonacci case
626
+ completes; the largest normalization/validation medians are 118.865/113.250 ms
627
+ and 117.350/119.264 ms respectively. At 8,192 bits, coprime/shared-factor batching
628
+ medians are 81.600/82.952 ms versus Euclid's 411.692/429.575 ms on Node 26, and
629
+ 87.801/89.841 ms versus 417.157/436.452 ms on Node 24. The record retains smaller
630
+ controls, harness limits and source hashes. Its independent Python Fraction
631
+ oracle also passes 3,600 arithmetic, 3,744 explanation, 1,872 classification and
632
+ 7,200 budget checks per major. These are fixed-seed and workload-specific results;
633
+ they do not bound all intermediate arithmetic or change standalone Exact limits.
634
+ A [later oracle checkpoint](../../benchmarks/shared-root-oracle-review.json)
635
+ repeats all 16,416 counted checks per major after shared-root evaluation reuse.
636
+ It records the current solver source hashes and Python 3.14.5 results on Node
637
+ 26.8.1 and 24.21.0. This refreshes correctness evidence after the explanation
638
+ change; the earlier performance measurements retain their original source scope.
639
+
640
+
641
+ ### Independent arithmetic checks
642
+
643
+ The latest retained run of `scripts/exact-oracle-check.mjs` passes 16,416 checks
644
+ per runtime on Node 26.8.1 and 24.21.0 with Python 3.14.5: 3,600 direct arithmetic,
645
+ 3,744 explanation, 1,872 linear-product and 7,200 size-estimate checks across
646
+ 600 cases. `benchmarks/intermediate-arithmetic-current-review.json` records both
647
+ results and solver source hashes. It includes the six-step denominator probe and
648
+ current validation fixes. This is correctness evidence for the cases described
649
+ below; it does not refresh the earlier performance measurements.
650
+
651
+ For an independent arithmetic audit, run `node scripts/exact-oracle-check.mjs`
652
+ from the repository root with `python3` available. Python's standard-library
653
+ `Fraction` produces expected reduced results for 600 fixed-seed cases at ten
654
+ random-generation widths from 1 to 16,384 bits. Families cover arbitrary signs,
655
+ shared denominator factors, additive cancellation, reciprocal products, zero
656
+ numerators and invalid zero denominators, including widths around the GCD
657
+ batching threshold. The script checks normalization and the private sum/product
658
+ helpers for addition, subtraction, multiplication and division, plus public
659
+ exact comparison: 3,600 comparisons or expected-error checks. Comparison checks
660
+ both operand denominators even when zero or opposite signs permit a shortcut.
661
+ Division rejects an invalid right-hand fraction before
662
+ using the product helper, as the calling arithmetic does.
663
+
664
+ The same generator supplies independent Python integer bit lengths for operand
665
+ values, products, sums and fraction cross-sums. The audit checks that explanation
666
+ preflight estimates never fall below those actual sizes, including the raw
667
+ unreduced fraction-input estimate. These add 7,200 checks across the same signed,
668
+ zero and cancellation fixtures. [Results and source hashes](../../benchmarks/explanation-budget-bounds-review.json)
669
+ record both supported Node majors. This checks the estimator's conservatism for
670
+ the generated integer/fraction cases; it is not an exhaustive proof, a decimal
671
+ exponent audit, or a process-memory/latency bound.
672
+
673
+ For decimal-specific coverage, run `node scripts/explanation-decimal-oracle.mjs`.
674
+ Python `Decimal` parses 680 fixed/seeded decimal forms, and `Fraction` supplies
675
+ their exact normalized values. The audit compares preflight estimates with the
676
+ unreduced coefficient/power-of-ten integer sizes, checks acceptance at the
677
+ estimate and rejection one bit below it, and compares exact normalization.
678
+ Forms include signs, leading/trailing zeros, leading/trailing decimal points,
679
+ upper/lowercase exponent markers and positive/negative exponents. Six additional
680
+ checks send extreme safe exponents through the text preflight: nonzero values
681
+ are rejected, while zero values normalize without constructing a power of ten.
682
+ [Recorded decimal checks](../../benchmarks/explanation-decimal-budget-review.json)
683
+ contain 2,726 checks per supported Node major and source hashes. The public-report
684
+ regression also checks small-budget refusal, zero preservation and larger-budget
685
+ recovery. This is local evaluation preflight; model validation/canonicalization
686
+ has its separate input budget and may normalize literals before evaluation.
687
+
688
+ The audit also evaluates addition, subtraction, multiplication and division
689
+ through explanation reports using signed rational assignments. For valid
690
+ assignment denominators, equality with Python's expected fraction must be
691
+ satisfied and inequality must be violated. An assigned zero divisor must instead
692
+ leave both constraint evaluations indeterminate. These add 3,744 explanation
693
+ checks, including the direct bigint normalization path. Invalid assignment
694
+ denominators remain covered by the separate arithmetic checks above. This is
695
+ a deterministic differential audit, not exhaustive proof over all expressions.
696
+
697
+ For valid input denominators, the audit also checks linear analysis of two- and
698
+ four-factor constant products against independently reduced Python fractions.
699
+ Subtracting the reference must produce a zero divisor; offsetting it by one
700
+ must produce a nonzero divisor. These add 1,872 linear-analysis checks, including
701
+ signs, zeros and reciprocal cancellation. The resulting
702
+ [9,216-check audit](../../benchmarks/linear-product-oracle-review.json) passes on
703
+ Node 24.21.0 and 26.8.1 against Python 3.14.5.
704
+ The same 9,216-check audit also passes after the constant-divisor sign analysis
705
+ was introduced, including all 1,872 classification checks;
706
+ [the current audit report](../../benchmarks/linear-sign-oracle-review.json)
707
+ retains both runtime results and source hashes. The ordinary solver suite also
708
+ checks 20,000-level constant divisors under explicit depth limits and counts
709
+ shared variable-bearing factors separately. This supports the sign shortcut
710
+ and its exact-evaluation fallback within the tested families; it does not supply
711
+ an intermediate-work budget or prove all possible expression graphs.
712
+
713
+ On 2026-09-08, the original 3,000 checks passed on Node 24.16.0 and 26.5.0 against
714
+ Python 3.14.5. The expanded 3,600-check audit passed on Node 24.21.0 and 26.8.1
715
+ against the same Python version on 2026-09-09. Reports include runtime versions
716
+ and the seed. Python is an
717
+ explicit dependency of this optional audit, not of the solver or ordinary test
718
+ suite. Fixture generation has a 30-second child allowance and a 64 MB output
719
+ limit; on Python versions with a decimal conversion limit, the generator sets
720
+ it to 25,000 digits for these bounded fixtures, including four-factor references. This audit checks arithmetic
721
+ agreement, not model validation coverage, backend behavior, or resource limits.
722
+
723
+ `Exact.rational` returns a reduced fraction with a positive denominator and
724
+ represents zero as `0/1`. `Exact.compare` compares mathematical values exactly,
725
+ including adjacent integers beyond JavaScript's safe-integer range when supplied
726
+ as strings. Equivalent decimal and fraction forms compare equal.
39
727
 
40
728
  ## Portable semantics
41
729
 
@@ -45,6 +733,10 @@ part of this model and is never interpreted as a soft-constraint weight.
45
733
  - Mixed integer/real arithmetic promotes the integer operand to an exact real.
46
734
  Division always returns an exact real; it never uses JavaScript or
47
735
  backend-specific integer-division behavior.
736
+ Every division therefore requires the `rationals` capability, even when both
737
+ operands are integer literals or bounded integer variables. This is checked
738
+ before invoking the adapter, including divisions nested in constraints or
739
+ objectives.
48
740
  - `add`, `subtract`, `multiply`, and `negate` are exact. Multiplication of two
49
741
  variable-bearing expressions is portable but requires the
50
742
  `nonlinear-arithmetic` capability.
@@ -56,25 +748,975 @@ part of this model and is never interpreted as a soft-constraint weight.
56
748
  one objective.
57
749
  - Enum domains are named finite sets. Enum literals carry the domain ID, so
58
750
  values from unrelated domains can never compare accidentally.
751
+ Domain values must be a nonempty, dense array of distinct primitive strings.
752
+ Empty strings, Unicode, and spaces are valid members; non-string members,
753
+ sparse arrays, and non-array containers fail model validation.
59
754
 
60
755
  Quantifiers, arrays, bit-vectors, recursive definitions, transcendental
61
756
  functions, raw SMT-LIB, and solver tactics are deliberately absent. A new
62
757
  portable operation requires a schema version and adapter capability rather
63
758
  than falling through to backend-specific behavior.
64
759
 
760
+ `Linear.model(model, limits?)` recognizes a conservative LP/MIP subset: numeric
761
+ variables, affine objectives, and non-strict affine comparisons, without soft
762
+ constraints. Division is affine only when its denominator is a defined,
763
+ nonzero numeric constant. A conservative sign analysis first proves unambiguous
764
+ constant signs, avoiding construction of large intermediate fractions merely to
765
+ establish that a divisor is nonzero. Ambiguous cancellation falls back to exact
766
+ rational evaluation, so computed zero divisors are excluded and tiny nonzero
767
+ divisors are not rounded away. General SMT models may use totalized arithmetic; their
768
+ acceptance by `Validate.model` does not imply membership in this linear subset.
769
+ Optional validation limits permit analysis of larger models under the same
770
+ explicit bounds used for solving and hashing. Omitting them keeps the default
771
+ limits; different sufficient limits do not change the classification.
772
+ Classification, sign analysis and exact constant evaluation use explicit stacks and cache
773
+ shared expressions within each analysis, so raised depth limits do not depend
774
+ on the JavaScript call stack. Repeated factors still count separately when
775
+ deciding whether a product is affine.
776
+ Classification follows expression structure rather than general symbolic
777
+ simplification. With a real variable `x`, `x / 1` is accepted, while
778
+ `x / (x - x + 1)` retains a variable-bearing denominator and `0 * x * x`
779
+ retains two variable-bearing factors, so both are outside the recognized subset.
780
+ A `linear: false` result identifies this subset boundary; it does not prove that
781
+ no mathematically equivalent linear formulation exists.
782
+
783
+ Wide sums and products accumulate classification flags as each operand returns,
784
+ without retaining an extra array of operand classifications. Every operand is
785
+ still visited, including after the result is already known to be nonlinear.
786
+ The [25,000-operand trial](../../benchmarks/linear-classification-fold-review.json)
787
+ measures complete calls including validation and capture: product medians move
788
+ from 33.05 to 31.79 ms on Node 24.21.0 and 31.55 to 30.31 ms on Node 26.8.1;
789
+ sum timings are essentially unchanged (34.97 to 34.84 ms and 32.43 to 32.98 ms).
790
+ Run `node scripts/linear-classification-bench.mjs` from the repository root.
791
+ This removes one source of temporary allocation; the trial does not measure
792
+ peak memory or establish a general classification work bound.
793
+
794
+ ### Constant divisor sign trial
795
+
796
+ Run `node scripts/linear-rational-sum-bench.mjs linear distinct` from the root;
797
+ replace `distinct` with `equal`, `cancelling` or `integers` for controls. The
798
+ unchanged trial uses 32, 128 and 512 operands, four calls per case and the median
799
+ of the last three. Input construction is outside timing; validation, capture and
800
+ classification are inside. Every result is checked. Sequential macOS arm64
801
+ runs produced these medians for 512 operands:
802
+
803
+ | Divisor sum | Node 24 before (ms) | Node 24 after (ms) | Node 26 before (ms) | Node 26 after (ms) |
804
+ |---|---:|---:|---:|---:|
805
+ | Positive, distinct denominators | 50.897 | 3.067 | 49.148 | 2.966 |
806
+ | Positive, equal denominators | 3.171 | 3.045 | 3.109 | 2.997 |
807
+ | Cancelling pairs | 3.178 | 3.133 | 3.009 | 2.978 |
808
+ | Integers | 2.013 | 2.077 | 1.994 | 1.930 |
809
+
810
+ [Raw before/after samples and source hashes](../../benchmarks/linear-sign-review.json)
811
+ retain the smaller cases too. The distinct-denominator case improves because a
812
+ positive sum needs no common denominator to prove it is nonzero. Controls stay
813
+ near their earlier timings, including cancellation that still needs exact
814
+ arithmetic. This is a bounded classifier measurement, not a general arithmetic
815
+ work cap or backend benchmark. Validation limits still apply before analysis,
816
+ and undefined subexpressions cannot be hidden by multiplication by zero.
817
+
818
+ ### Default numeric-budget trial
819
+
820
+ Run `node scripts/solver-numeric-budget-bench.mjs` to compare linear
821
+ classification with a full explanation report for a positive sum of fractions
822
+ with distinct 100-digit denominators. The classifier uses the sum as a divisor;
823
+ the explanation checks that it is positive. Both retain default limits.
824
+ The 990-term models charge 99,990 and 99,992 numeric digits respectively against
825
+ the default 100,000-digit budget. Validation rejects both 991-term models.
826
+ Each allowed fixture also verifies rejection when its numeric limit is set one
827
+ digit below its exact charge, so the sign shortcut does not bypass that limit.
828
+
829
+ Before explanation sign proofs, sequential macOS arm64 runs measured these median milliseconds:
830
+
831
+ | Terms | Linear, Node 24 | Explanation, Node 24 | Linear, Node 26 | Explanation, Node 26 |
832
+ |---|---:|---:|---:|---:|
833
+ | 128 | 1.364 | 23.134 | 1.275 | 22.495 |
834
+ | 512 | 3.720 | 308.797 | 4.421 | 303.322 |
835
+ | 990 | 7.110 | 1072.467 | 6.452 | 1061.653 |
836
+
837
+ [Raw samples, validation statistics and source hashes](../../benchmarks/solver-numeric-budget-review.json)
838
+ retain the complete observations. Each case runs in a fresh child with four
839
+ calls; the first is discarded and the other three determine the median.
840
+ Construction and assertions are outside timing; the full classifier or report
841
+ call, including validation and preparation, is inside. Every result is checked.
842
+ The harness allows each child 30 seconds and a 256 MB V8 old-space setting;
843
+ these are harness settings, not solver guarantees or a total-memory limit.
844
+ In that baseline the classifier avoided constructing the positive sum, while
845
+ explanation evaluated it and prepared a report. These timings do not isolate
846
+ either cost. The input
847
+ budget bounds accepted input size, not intermediate work or execution time;
848
+ this trial establishes no bound for other expression shapes or larger overrides.
849
+
850
+ ### Explanation sign trial
851
+
852
+ Constant numeric comparisons with a literal zero now share the classifier's
853
+ conservative sign proof. The proof uses an explicit stack and a report-local
854
+ cache. It requires known signs for every product operand and a known nonzero
855
+ divisor, so multiplying by zero cannot hide undefined arithmetic. Variables,
856
+ conditional expressions and ambiguous cancellation fall back to normal exact
857
+ evaluation; assignment checks and Boolean short-circuiting retain their behavior.
858
+
859
+ Rerunning the default numeric-budget trial on macOS arm64 measured median
860
+ milliseconds for complete explanation reports:
861
+
862
+ | Terms | Node 24 before | Node 24 after | Node 26 before | Node 26 after |
863
+ |---|---:|---:|---:|---:|
864
+ | 128 | 23.134 | 2.498 | 22.495 | 2.453 |
865
+ | 512 | 308.797 | 7.741 | 303.322 | 6.626 |
866
+ | 990 | 1072.467 | 15.095 | 1061.653 | 14.386 |
867
+
868
+ [Before/after samples and source hashes](../../benchmarks/explanation-sign-review.json)
869
+ retain the classifier controls, validation statistics and child settings.
870
+ At 990 terms the report takes about 14–15 ms instead of 1.06–1.07 seconds for
871
+ this positive-sum fixture. Mixed-sign sums may still need exact intermediate
872
+ fractions; this optimization establishes no general execution-time bound.
873
+
874
+ ### Shared-product resource trial
875
+
876
+ Object sharing does not bypass expression-node limits: validation counts each
877
+ occurrence. Intermediate arithmetic can still grow substantially. The
878
+ [shared-product trial](../../scripts/solver-shared-product-bench.mjs) repeatedly
879
+ squares an assigned rational `1 + 10^-50`, then checks equality with one.
880
+ The expected result is independently known to be false. All cases pass default
881
+ model limits and return that result; only the literal one contributes to the
882
+ model's numeric-digit charge. Assignment values are separate from that charge.
883
+
884
+ Median complete `Explain.report` calls on macOS arm64:
885
+
886
+ | Squarings | Counted expression nodes | Result denominator digits | Node 24.21.0 ms | Node 26.8.1 ms |
887
+ |---|---:|---:|---:|---:|
888
+ | 8 | 513 | 12,801 | 42.795 | 43.169 |
889
+ | 10 | 2,049 | 51,201 | 519.741 | 516.765 |
890
+ | 12 | 8,193 | 204,801 | 7,127.377 | 6,923.401 |
891
+
892
+ Run `node scripts/solver-shared-product-bench.mjs`. Each case uses a fresh
893
+ child with a 30-second timeout and 256 MB V8 old-space allowance; four full
894
+ calls are measured, with the first discarded. Those harness allowances are
895
+ not library limits or total process-memory guarantees.
896
+ [Raw samples and source hashes](../../benchmarks/shared-product-review.json)
897
+ record both supported Node majors. This trial establishes a resource gap, not
898
+ an incorrect result. Investigate repeated normalization in rational products
899
+ and a separate intermediate-work policy; neither input counting nor sign
900
+ proofs alone bound arbitrary explanation work.
901
+
902
+ Explanation multiplication now relies on its existing reduced-fraction
903
+ invariant: denominators are positive and numerator/denominator pairs are
904
+ coprime. Equal operands can be squared directly; for other products,
905
+ cross-cancellation already yields a reduced result. Neither path needs another
906
+ GCD over the expanded product. Operand evaluation and type checks still happen
907
+ before multiplication, so zero cannot conceal undefined arithmetic.
908
+
909
+ The same trial after this change measured:
910
+
911
+ | Squarings | Node 24.21.0 ms | Node 26.8.1 ms |
912
+ |---|---:|---:|
913
+ | 8 | 2.080 | 1.405 |
914
+ | 10 | 6.821 | 5.806 |
915
+ | 12 | 26.575 | 25.130 |
916
+
917
+ The depth-12 case improves by roughly 270 times while retaining the same exact
918
+ result. [Before/after samples and numeric-budget controls](../../benchmarks/explanation-product-reduction-review.json)
919
+ record the unchanged fixtures and source hashes. This optimization does not
920
+ limit intermediate size, and standalone arithmetic and a general work policy
921
+ remain separate concerns.
922
+
923
+ Negation (including subtraction's negated operand) now preserves reduction
924
+ directly. Division explicitly rejects a zero divisor, cross-cancels the reduced
925
+ operands, and corrects a negative denominator without another GCD. These paths
926
+ retain their exact values and local division-by-zero diagnostics.
927
+ Run the same harness with `negate` or `divide` to wrap the shared product in
928
+ negation or division by negative one. At depth 12, median full-report times were:
929
+
930
+ | Operation | Node 24 before ms | Node 24 after ms | Node 26 before ms | Node 26 after ms |
931
+ |---|---:|---:|---:|---:|
932
+ | Negate | 2258.438 | 27.562 | 2170.777 | 24.851 |
933
+ | Divide | 2270.091 | 26.183 | 2254.261 | 25.602 |
934
+
935
+ [All depths, samples and product controls](../../benchmarks/explanation-unary-reduction-review.json)
936
+ use the same isolated timing boundaries. This removes redundant reduction
937
+ work; general intermediate growth remains a separate resource cost.
938
+
939
+ Addition retains full reduction for general fractions. Adding zero returns the
940
+ other already-validated value; adding an integer preserves coprimality because
941
+ `gcd(a + k*b, b) = gcd(a, b)`. These cases now avoid another GCD. All operands
942
+ are evaluated and checked before reduction, so an identity cannot hide invalid
943
+ arithmetic. Subtraction uses the same addition logic after negation.
944
+
945
+ The harness's `add` mode adds one to the shared product. Depth-12 median full
946
+ reports improve from 2262.235 to 26.185 ms on Node 24 and 2176.365 to 27.066 ms
947
+ on Node 26. [All depths and numeric-budget controls](../../benchmarks/explanation-integer-sum-review.json)
948
+ retain the raw samples and source hashes. General non-integral sums still
949
+ require reduction, and this shortcut supplies no intermediate-work limit.
950
+
951
+ The combined reduced-fraction shortcuts are also checked against Python's
952
+ independent `Fraction` implementation. Run
953
+ `node scripts/explanation-tree-oracle.mjs` with Python 3 available. Its seeded
954
+ 256 mixed expression trees combine addition, subtraction, multiplication,
955
+ division, negation and conditional branches with unreduced assignments and integer
956
+ operands. Five explicit cases additionally select or skip division by zero in
957
+ both branch directions and reject a failing condition before branch selection.
958
+ Each tree checks equality and inequality against the independent exact result;
959
+ division-by-zero cases must retain indeterminate evaluations and diagnostics,
960
+ including zero multiplied by undefined arithmetic. Version 2 passes 522 outcome
961
+ checks across 261 trees on both supported Node majors, with 49 indeterminate
962
+ trees and matching fixture hashes. This is a correctness audit, not a timing
963
+ benchmark or an exhaustive proof for every expression.
964
+ `benchmarks/conditional-expression-tree-review.json` retains current results and
965
+ solver source hashes. The [earlier arithmetic-only results](../../benchmarks/explanation-arithmetic-oracle-review.json)
966
+ retain their original corpus and the companion wide-arithmetic oracle.
967
+
968
+ ### Balanced n-ary explanation products
969
+
970
+ Explanation multiplication now combines an operand list in balanced pairs,
971
+ using the same reducer as addition. This avoids repeatedly combining a small
972
+ factor with an ever-growing fraction. Exact multiplication is associative;
973
+ all operands are still evaluated and type-checked before reduction, and a zero
974
+ factor still cannot conceal undefined arithmetic in another operand.
975
+
976
+ The [flat-product trial](../../scripts/solver-flat-product-bench.mjs) multiplies
977
+ independently constructed references to an assigned rational `1 + 10^-50`.
978
+ Every factor exceeds one, so the expected comparison is independently known.
979
+ All fixtures pass default model limits. Median complete report times on macOS
980
+ arm64 were:
981
+
982
+ | Factors | Node 24 before ms | Node 24 after ms | Node 26 before ms | Node 26 after ms |
983
+ |---|---:|---:|---:|---:|
984
+ | 128 | 2.662 | 1.443 | 2.604 | 1.323 |
985
+ | 1,024 | 106.927 | 9.629 | 104.924 | 7.768 |
986
+ | 4,096 | 1,648.504 | 40.996 | 1,623.321 | 37.995 |
987
+
988
+ Run `node scripts/solver-flat-product-bench.mjs`. The harness runs each size in
989
+ a fresh child, measures four full calls and discards the first. Timing runs
990
+ are sequential, with a 30-second child timeout and 256 MB V8 old-space allowance;
991
+ these are harness controls, not library resource limits.
992
+ [Raw samples and source hashes](../../benchmarks/explanation-flat-product-review.json)
993
+ retain before/after evidence for both supported Node majors. The largest case
994
+ still constructs a 204,801-digit denominator. This improves the measured
995
+ repeated-factor workload by roughly 40 times; it does not guarantee a speedup
996
+ for every factor distribution or impose an intermediate-work bound.
997
+
65
998
  ## Validation and identity
66
999
 
67
1000
  `Validate.model` checks schema version, identifiers, duplicate declarations,
68
1001
  exact numerals, bounds, enum membership, references, expression sorts,
69
1002
  constraint/objective types, and preflight model-size limits. Invalid or
70
- unsupported models fail before an adapter runs. Operational limits cover time,
71
- working memory, declarations, enum values, expression nodes/depth, and output
72
- size; every limit is passed to the selected adapter.
1003
+ unsupported models fail before an adapter runs.
1004
+
1005
+ Jump to a topic:
1006
+
1007
+ - [Capture order](#capture-order)
1008
+ - [Capture measurements](#capture-measurements)
1009
+ - [Model structure and default limits](#model-structure-and-default-limits)
1010
+ - [Enum membership measurements](#enum-membership-measurements)
1011
+ - [Validation input costs](#validation-input-costs)
1012
+ - [Local explanation budgets](#local-explanation-budgets)
1013
+ - [Evaluation reuse and syntax keys](#evaluation-reuse-and-syntax-keys)
1014
+ - [Canonical serialization costs](#canonical-serialization-costs)
1015
+ - [Cumulative work calibration](#cumulative-work-calibration)
1016
+ - [Preflight and captured inputs](#preflight-and-captured-inputs)
1017
+ - [Canonical model identity](#canonical-model-identity)
1018
+
1019
+ ### Capture order
1020
+
1021
+ Validation and capture occur in this order at the public boundaries. The solve
1022
+ calls below are `Solve.run` and `Solve.runWithExplanation`:
1023
+
1024
+ | Entry point | Order before model use |
1025
+ | --- | --- |
1026
+ | `Validate.model` | Validate caller data directly, capturing individual numeric fields for their checks. |
1027
+ | Solve calls | Capture the model, validate that snapshot, then freeze it before adapter execution. |
1028
+ | `Explain.report` | Capture the model and backend result; validate the model while deriving its digest before local evaluation. |
1029
+ | Canonicalization and linear analysis | Validate input, capture it, then validate the captured model again. |
1030
+
1031
+ The solve/report order keeps changing getters from supplying different values
1032
+ to validation and execution. Declaration, node and numeric limits apply to the
1033
+ validated model; they do not bound every allocation or accessor operation needed
1034
+ to capture it first. Backend time and memory limits likewise do not describe
1035
+ total caller-side model/result copying. Copy failures propagate through the
1036
+ public call, with corrected-input retry covered separately below.
1037
+
1038
+ Portable record/array copying now enumerates keys and inspects one descriptor
1039
+ at a time, then copies values by key. This avoids retaining whole descriptor
1040
+ and entry arrays for wide objects while preserving native fallback for
1041
+ accessors, proxies and other containers. Shared references, cycles, sparse
1042
+ arrays, named array fields and ignored non-enumerable/symbol properties retain
1043
+ the existing copy behavior.
1044
+ ### Capture measurements
1045
+
1046
+ These trials measure different costs and source revisions. Follow each record
1047
+ for its fixtures and source hashes:
1048
+
1049
+ - [Model copying and object sharing](#model-copying-and-object-sharing)
1050
+ - [Rejected primitive preflight](#rejected-primitive-preflight)
1051
+ - [Backend diagnostic baseline](#backend-diagnostic-baseline)
1052
+ - [Current report assembly](#current-report-assembly)
1053
+
1054
+ #### Model copying and object sharing
1055
+
1056
+ The [wide-capture trial](../../scripts/solver-wide-capture-bench.mjs) separates
1057
+ copy timing from full reports at 1,001, 25,001 and 100,000 expression nodes.
1058
+ For the largest case, copy medians change from 58.948 to 35.073 ms on Node
1059
+ 26.8.1 and from 49.961 to 37.047 ms on Node 24.21.0. Full-report medians change
1060
+ from 167.536 to 135.687 ms and 171.823 to 158.420 ms respectively. All output
1061
+ hashes agree. [All samples, source hashes and peak RSS](../../benchmarks/wide-model-capture-review.json)
1062
+ retain small-case controls, including a small report slowdown on Node 24.
1063
+ The trial uses sequential fresh children and four calls, discarding the first.
1064
+ Peak RSS includes construction, reference copies and assertions; lower observed
1065
+ RSS does not establish a total memory limit. The external 60-second/256 MB
1066
+ old-space controls are benchmark allowances, not API guarantees.
1067
+
1068
+ The driver now compares a shared leaf with distinct, equal-valued leaves at each
1069
+ size and mode, checking equal serialized output hashes across both topologies.
1070
+ The [topology comparison](../../benchmarks/capture-topology-review.json) records
1071
+ all solver source hashes and the lockfile. At 100,000 expression occurrences,
1072
+ shared/distinct copy medians are 29.34/92.12 ms on Node 26.8.1 and 33.84/99.18 ms
1073
+ on Node 24.21.0; full-report medians are 130.35/302.39 ms and 148.29/327.85 ms.
1074
+ These are local measurements of the same runtime, not before/after optimization
1075
+ results. Distinct objects require more copying even at an equal validation node
1076
+ count. Lifetime RSS includes the fixture, native reference copy where applicable,
1077
+ assertions and repeated calls; the old-space allowance is not a process RSS cap.
1078
+
1079
+ #### Rejected primitive preflight
1080
+
1081
+ An [isolated primitive-preflight trial](../../benchmarks/capture-primitive-preflight-review.json)
1082
+ checked function/symbol fields immediately and queued only object-valued fields.
1083
+ Output hashes matched, but timings were mixed: at the largest fixture on Node 26,
1084
+ shared copying measured 36.77 ms versus 30.92 ms for the restored control, and
1085
+ distinct-object reports measured 323.72 versus 308.72 ms. Some other cases improved
1086
+ slightly. This sequential trial did not establish a consistent benefit, so the
1087
+ original traversal was restored; it does not justify changing capture semantics.
1088
+
1089
+ #### Backend diagnostic baseline
1090
+
1091
+ The [result-capture driver](../../scripts/solver-result-capture-bench.mjs) measures
1092
+ 1,000, 25,000 and 100,000 backend diagnostic entries with an empty model. Run
1093
+ `node scripts/solver-result-capture-bench.mjs` alone from the repository root.
1094
+ It compares repeated shared entries with distinct equal-valued entries, measuring
1095
+ result capture/validation separately from complete report construction.
1096
+ [Samples and source fingerprints](../../benchmarks/result-diagnostic-capture-review.json)
1097
+ record matching output hashes and detached diagnostic graphs on both Node majors.
1098
+ This baseline predates the report-assembly optimization below. At 100,000 shared/distinct entries, capture medians are 31.81/115.21 ms on Node
1099
+ 26.8.1 and 32.91/110.54 ms on Node 24.21.0; report medians are 64.87/251.37 ms
1100
+ and 64.01/221.89 ms. These measure copying and assembly, not backend execution.
1101
+ Result metadata validation checks each diagnostic's shape but imposes no entry-
1102
+ count limit; model node limits do not bound this list. The backend output ceiling
1103
+ is a separate contract. Fixture/reference copies, hashing and assertions are
1104
+ outside timing but contribute to process peak RSS. The 60-second child timeout
1105
+ and 256 MB old-space allowance are diagnostic controls, not library guarantees.
1106
+
1107
+ #### Current report assembly
1108
+
1109
+ Report assembly now reuses its captured model, result and context data plus newly
1110
+ constructed fields, avoiding a second copy of the completed report envelope.
1111
+ Returned data remains detached from caller inputs; mutating report metadata,
1112
+ assignments, provenance, context or limits cannot change a later report. Shared
1113
+ references inside captured graphs remain shared in the report.
1114
+ The [assembly trial](../../benchmarks/owned-report-assembly-review.json) records
1115
+ identical before/after output hashes. Full-report medians for 100,000 diagnostic
1116
+ entries are:
1117
+
1118
+ | Runtime | Diagnostic objects | Before (ms) | After (ms) |
1119
+ |---|---|---:|---:|
1120
+ | Node 26.8.1 | Shared | 64.87 | 33.56 |
1121
+ | Node 26.8.1 | Distinct | 251.37 | 114.70 |
1122
+ | Node 24.21.0 | Shared | 64.01 | 33.36 |
1123
+ | Node 24.21.0 | Distinct | 221.89 | 116.00 |
1124
+
1125
+ Model-heavy controls also retain equal hashes; their timings vary because the
1126
+ removed copy is small in those fixtures. This reduces repeated copying, not backend execution or the cost of
1127
+ capturing caller inputs, and introduces no new resource limit.
1128
+
1129
+ ### Model structure and default limits
1130
+
1131
+ Every objective must explicitly use `direction: 'minimize'` or
1132
+ `direction: 'maximize'`. Missing or other direction values raise
1133
+ `ModelValidationError` before adapter execution or canonical digest generation;
1134
+ they do not select a default optimization direction. Corrected models can retry.
1135
+
1136
+ The model must be an object with `variables` and `constraints` arrays; optional
1137
+ `enums`, `softConstraints`, and `objectives` must also be arrays when supplied.
1138
+ Declaration lists must contain objects in their own indexed slots. Missing slots
1139
+ reject before any inherited getter runs, across all five declaration lists;
1140
+ defining the missing own entry permits a corrected retry. Expressions must be objects,
1141
+ and `and`, `or`, `add`, and `multiply` require dense operand arrays containing
1142
+ expression objects in their own indexed slots. Inherited entries do not fill
1143
+ holes; validation rejects a missing slot without reading its inherited getter.
1144
+ Defining the missing own entry permits validation of the repaired array. Malformed runtime structures produce `Validate.ModelValidationError`
1145
+ with their field path rather than silently skipping missing declarations.
1146
+ Declaration and operand checks traverse indexed entries directly; caller-defined
1147
+ `map` or `forEach` methods cannot suppress these checks and are not invoked.
1148
+ Model declaration IDs must be primitive strings fully matching
1149
+ `[A-Za-z][A-Za-z0-9._:/-]*`; trailing line breaks and coercible objects are
1150
+ rejected for variables, enum domains, hard/soft constraints, and objectives.
1151
+ Diagnostics for duplicate identifiers, unknown variable/domain references, enum
1152
+ members and unsupported expression kinds quote short primitive text normally. Malformed
1153
+ object, BigInt, function or symbol values are described by type (for example,
1154
+ `<object>` or `<bigint>`) without traversing them or invoking `toJSON`. Cyclic and
1155
+ BigInt values therefore retain classified `ModelValidationError` diagnostics
1156
+ instead of failing during error formatting. Corrected input can be retried.
1157
+ This diagnostic behavior does not change getter or native-clone behavior at
1158
+ entrypoints that capture caller data before validation.
1159
+ Exact-number syntax and exponent-range errors, identifier/domain/member errors,
1160
+ unsupported expression kinds, malformed sorts and invalid option/limit names or
1161
+ primitive values share text previews.
1162
+ Inputs up to 160 UTF-16 code units keep their existing display. Longer values
1163
+ show the first and last 80 code units, JSON-escaped, plus the original length.
1164
+ This bounds each input preview while preserving its ends; it does not bound
1165
+ full-input scanning or aggregate model diagnostics.
1166
+ The supplied value remains unchanged, and corrected input can retry.
1167
+ Provenance reference lists (`evidenceRowIds` and `scenarioInputIds`) validate
1168
+ their own indexed strings. Custom iterators cannot hide duplicates, and sparse
1169
+ slots reject without invoking inherited getters. Repairing the same list permits
1170
+ a fresh validation.
1171
+ Sort-mismatch diagnostics also describe malformed variable sorts by type without
1172
+ coercing caller objects. A reference to an invalid declaration therefore retains
1173
+ the declaration error alongside expression errors.
1174
+ Variables and literals must use a declared portable sort (`bool`, `int`, `real`,
1175
+ or `enum`). Boolean literals require primitive Boolean values; truthy strings
1176
+ and numeric substitutes are rejected, and unknown literal sorts never fall
1177
+ through to variable lookup.
1178
+ Enum membership and duplicate detection share a set built from the domain's own
1179
+ indexed entries for one validation call. Custom array iterators cannot substitute
1180
+ members or hide duplicates, and missing slots reject without inherited reads. Mutating a domain between calls produces a fresh index; duplicate values
1181
+ still reject, and shared literal objects still count at every occurrence.
1182
+ See [Enum membership measurements](#enum-membership-measurements) for the
1183
+ workload and measured costs.
1184
+
1185
+ Operational limits cover time, working memory, declarations, enum values,
1186
+ expression nodes/depth and output size. `Adapter.defaultLimits` supplies these
1187
+ defaults; every resolved limit is passed to the selected adapter:
1188
+
1189
+ | Limit | Default | Meaning |
1190
+ |---|---:|---|
1191
+ | `timeoutMs` | 10,000 | Backend execution deadline in milliseconds. |
1192
+ | `maxMemoryBytes` | 536,870,912 (512 MiB) | Backend working-memory ceiling; adapters must reject or isolate when they cannot enforce it. |
1193
+ | `maxVariables` | 1,000 | Variable declarations. |
1194
+ | `maxConstraints` | 5,000 | Hard and soft constraints combined. |
1195
+ | `maxObjectives` | 16 | Objective declarations. |
1196
+ | `maxEnumValues` | 1,000 | Values across enum domains. |
1197
+ | `maxExpressionNodes` | 100,000 | Expression-node occurrences. |
1198
+ | `maxExpressionDepth` | 128 | Expression nesting depth. |
1199
+ | `maxNumericDigits` | 100,000 | Aggregate pre-reduction digits across numeric bounds, literal occurrences and soft weights. |
1200
+ | `maxExplanationBits` | 1,000,000 | Conservative size estimate for integers used during local hard/soft constraint evaluation. |
1201
+ | `maxExplanationWork` | 10,000,000 | Cumulative estimated-bit charges across local hard/soft constraint evaluation in one report. |
1202
+ | `maxOutputBytes` | 1,000,000 | Backend output-size ceiling in bytes. |
1203
+
1204
+ These limits govern validation, local explanation evaluation and backend execution. Solve entry points capture
1205
+ the model before validation; the validation traversal limits do not bound that
1206
+ initial copy.
1207
+
1208
+ ### Enum membership measurements
1209
+
1210
+ The enum validation trial uses 100, 1,000 and 10,000 values with 20 or 20,000
1211
+ last-member literal occurrences. The table shows the 20,000-occurrence cases;
1212
+ all before/after runs produce identical validated statistics. The current results
1213
+ use the indexed-entry implementation; the baseline used repeated array scans.
1214
+
1215
+ | Node | Enum values | Before (ms) | After (ms) |
1216
+ | --- | ---: | ---: | ---: |
1217
+ | 26 | 1,000 (default limit) | 25.83 | 6.31 |
1218
+ | 24 | 1,000 (default limit) | 58.30 | 7.12 |
1219
+ | 26 | 10,000 (explicit override) | 203.24 | 7.83 |
1220
+ | 24 | 10,000 (explicit override) | 523.48 | 7.89 |
1221
+
1222
+ Small controls are mixed. The index retains one set per domain until validation
1223
+ ends; these timings do not establish a universal time or memory bound.
1224
+ Reproduce with `node scripts/solver-enum-validation-bench.mjs`; full samples and
1225
+ source hashes are in `benchmarks/enum-indexed-membership-review.json`; the
1226
+ original scan baseline and earlier iterator-based trial remain in
1227
+ `benchmarks/enum-membership-validation-review.json`.
1228
+
1229
+ ### Validation input costs
1230
+
1231
+ Small diagnostic previews do not bound the work required to inspect supplied
1232
+ input. A direct-validation trial uses 1,000, 100,000 and 1,000,000 input code
1233
+ units or provenance references under default model limits. The table compares
1234
+ Node 26.8.1 and 24.21.0 at one million code units for each text case and one
1235
+ million references for the provenance case:
1236
+
1237
+ | Input | Node 26 (ms) | Node 24 (ms) |
1238
+ | --- | ---: | ---: |
1239
+ | Invalid integer ending in `x` | 11.99 | 11.99 |
1240
+ | Unknown variable ID; empty variable map | 0.04 | 0.07 |
1241
+ | Distinct evidence references on one Boolean variable | 150.14 | 244.46 |
1242
+
1243
+ The invalid integer is scanned for syntax; it is not a valid numeral charged by
1244
+ `maxNumericDigits`. Its error stays at 272 characters. The unknown-ID case uses
1245
+ an empty lookup map and repeated calls on the same text, so it does not establish
1246
+ first-use or populated-map lookup costs. Provenance references are checked for
1247
+ string validity and duplicates, but their count is not covered by variable,
1248
+ expression-node or numeric-digit limits. Callers managing large workloads must
1249
+ account for reference-list size separately.
1250
+
1251
+ Each case runs four validations in a fresh child and discards the first for its
1252
+ median. Fixtures, assertions and hashing are outside timing. Recorded process
1253
+ peak RSS includes fixture strings, repeated calls and assertions; it is not
1254
+ isolated validator memory or a hard process cap. Full samples, classified output
1255
+ hashes and source fingerprints are in `benchmarks/validation-input-cost-review.json`;
1256
+ reproduce with `node scripts/solver-validation-input-cost-bench.mjs`.
1257
+ Malformed collections also have separate costs. A valid enum domain must contain
1258
+ at least one value, but a large list of empty domains contributes zero members to
1259
+ `maxEnumValues` while accumulating validation problems. A controlled trial of
1260
+ 100,000 empty domains rejects all of them in median 42.67 ms on Node 26 and
1261
+ 60.57 ms on Node 24, producing 100,000 problems and a 4,688,911-character error.
1262
+ Bounded value previews do not cap the number of problems or the final error size.
1263
+ Applications needing a bound on malformed-collection work must constrain raw
1264
+ collection cardinality before calling validation. Full 1,000/10,000/100,000-domain
1265
+ samples and source hashes are in `benchmarks/invalid-enum-cost-review.json`;
1266
+ reproduce with `node scripts/solver-invalid-enum-cost-bench.mjs`.
1267
+
1268
+ Expression diagnostic paths use fixed field names and array indexes rather than
1269
+ caller-supplied identifiers. Their nesting follows `maxExpressionDepth` (128 by
1270
+ default); validation rejects depth 129 before inspecting that child's fields.
1271
+ The parent has already read the child reference and formed its path when this
1272
+ check runs, so this is not a guarantee against parent getters. Raising the depth
1273
+ limit also permits longer paths. Iterative traversal avoids recursive call-stack
1274
+ growth, but retained frame paths and accumulated errors still consume memory;
1275
+ the depth limit is not a total diagnostic-size or process-memory budget.
1276
+
1277
+ These finite trials do not bound all malformed collections, diagnostic counts,
1278
+ string lengths or capture work before validation.
1279
+
1280
+ ### Local explanation budgets
1281
+
1282
+ `maxExplanationBits` is enforced by explanation evaluation, independently of the
1283
+ backend's time and memory limits. Before parsing a numeric value it charges four
1284
+ bits per pre-reduction decimal digit, including both members of a supplied
1285
+ fraction. Before arithmetic it bounds numerator, denominator and comparison
1286
+ cross-products using operand bit lengths; addition allows a carry bit. These
1287
+ estimates are deliberately conservative: a calculation may be refused even if
1288
+ cross-cancellation or later reduction would produce a small answer. Equal
1289
+ denominators, zero/one factors and existing comparison shortcuts avoid needless
1290
+ growth estimates where the implementation does not allocate those products.
1291
+
1292
+ Exceeding this budget makes the affected constraint `indeterminate` with a
1293
+ `maxExplanationBits` evaluation reason. It preserves the backend's satisfied or
1294
+ optimal status, assignment and other report metadata; independent constraints
1295
+ continue evaluating. A later report with a larger limit can evaluate the same
1296
+ captured inputs. Unevaluated Boolean/conditional branches do not consume this
1297
+ budget, and constant-sign proofs may avoid expanded arithmetic altogether.
1298
+ The report records the resolved limit. Historical recorded reports that omit it
1299
+ keep their original limit metadata when read or replayed.
1300
+
1301
+ `maxExplanationWork` accumulates the largest estimated bit bound at each
1302
+ successful size check, including numeric input preflight and guarded arithmetic.
1303
+ The default is 10,000,000 units per report, shared by hard and soft constraints.
1304
+ A charge that exceeds the remaining allowance fails before that guarded operation
1305
+ and leaves the remaining allowance available for cheaper later work. Its local
1306
+ failure names `maxExplanationWork`; the backend result stays intact. Size checks
1307
+ run first, so an operation exceeding both limits reports `maxExplanationBits`.
1308
+ Previously completed identical predicates and captured assignments can be reused
1309
+ without repeating their charged work; unevaluated branches incur no charge.
1310
+ Constraint order can therefore affect which distinct evaluations finish. A fresh
1311
+ report resets the allowance, and its resolved limits record the selected value.
1312
+ Historical recorded reports retain their original fields during read and replay.
1313
+
1314
+ This accounting is an implementation-level estimate, not elapsed time or a
1315
+ portable instruction count. It excludes model/result copying, validation, syntax
1316
+ keys, unguarded traversal, formatting (including soft-weight normalization),
1317
+ standalone `Exact` helpers, linear classification and backend execution. Neither
1318
+ arithmetic guard is a total-process memory limit or a general sandbox.
1319
+
1320
+ The default-budget regression rejects shared assigned growth that still passes
1321
+ model input limits, and a small-budget regression verifies recovery with a larger
1322
+ limit. The permitted 4,096-factor flat-product control has median full-report
1323
+ times of 51.364 ms on Node 24.21.0 and 46.080 ms on Node 26.8.1 with the guard
1324
+ enabled. [Samples, source hashes and independent oracle checks](../../benchmarks/explanation-budget-review.json)
1325
+ record the measurement and exact-result checks; these timings are not deadlines.
1326
+
1327
+ A [cumulative trial](../../scripts/solver-cumulative-explanation-bench.mjs) repeats
1328
+ independently constructed 4,096-factor constraints at 1, 8 and 24 constraints.
1329
+ The largest model has 98,376 expression nodes and passes default model limits;
1330
+ a 25th constraint exceeds `maxExpressionNodes` and is rejected. Every permitted
1331
+ product exceeds one. Median complete report times on macOS arm64 were:
1332
+
1333
+ | Constraints | Node 24 default ms | Node 26 default ms | Node 24 1,024-bit budget ms | Node 26 1,024-bit budget ms |
1334
+ |---|---:|---:|---:|---:|
1335
+ | 1 | 51.679 | 45.382 | 27.379 | 23.923 |
1336
+ | 8 | 371.183 | 331.596 | 210.137 | 163.608 |
1337
+ | 24 | 1,082.233 | 1,005.619 | 561.014 | 501.721 |
1338
+
1339
+ With the default 1,000,000-bit guard every constraint evaluates as satisfied;
1340
+ with 1,024 bits every constraint becomes indeterminate. The smaller budget
1341
+ reduces arithmetic growth but does not remove report preparation or traversal
1342
+ cost. These full-call timings do not isolate individual phases. In particular,
1343
+ the size guard is not a cumulative-work or elapsed-time budget, even when it
1344
+ rejects all constraint evaluations.
1345
+
1346
+ Run `node scripts/solver-cumulative-explanation-bench.mjs` with no concurrent
1347
+ builds or tests. Each count/budget pair uses a fresh child and four complete
1348
+ calls, discarding the first for the median; each child has a 30-second timeout
1349
+ and 256 MB V8 old-space allowance. These harness controls are not runtime limits.
1350
+ [Raw samples and source hashes](../../benchmarks/explanation-cumulative-review.json)
1351
+ record both supported Node majors. This product-only trial did not establish a
1352
+ general threshold; the complementary sum trial and calibration below informed
1353
+ the current arithmetic allowance.
1354
+
1355
+ The report now canonicalizes its already-owned model through a private path.
1356
+ That path still validates the captured model before hashing, but avoids a
1357
+ second model copy and a redundant validation pass. Public `Canonical.serialize`
1358
+ and `Canonical.digest` retain validation before and after capture, including
1359
+ protection against changing getters. Model identity and explanation evaluation
1360
+ continue to use the same captured values.
1361
+
1362
+ Repeating the same trial after this preparation change reduced the 24-constraint
1363
+ default-budget median from 1,082.233 to 955.029 ms on Node 24 and from 1,005.619
1364
+ to 871.218 ms on Node 26. At 1,024 bits the corresponding medians fell from
1365
+ 561.014 to 416.838 ms and from 501.721 to 372.641 ms. The other model sizes and
1366
+ all samples are preserved in the
1367
+ [before/after preparation results](../../benchmarks/explanation-owned-canonical-review.json).
1368
+ The measurements use the same sequential harness controls described above.
1369
+ This removes repeated work; it does not impose a cumulative resource ceiling.
1370
+
1371
+ A complementary [cumulative rational-sum trial](../../scripts/solver-cumulative-sum-bench.mjs)
1372
+ uses 256 assigned positive fractions with distinct 101-digit denominators in
1373
+ each independently constructed constraint. At 384 constraints the model has
1374
+ 99,456 expression nodes and passes default validation; adding three constraints
1375
+ exceeds the node limit. Before report-scoped expression reuse, the full report
1376
+ medians on macOS arm64 were:
1377
+
1378
+ | Constraints | Node 24 default ms | Node 26 default ms | Node 24 1,024-bit budget ms | Node 26 1,024-bit budget ms |
1379
+ |---|---:|---:|---:|---:|
1380
+ | 1 | 84.572 | 81.458 | 5.904 | 5.065 |
1381
+ | 32 | 2,594.413 | 2,474.570 | 48.576 | 49.956 |
1382
+ | 384 | 30,905.307 | 30,024.301 | 480.169 | 428.386 |
1383
+
1384
+ Every default-budget evaluation in that baseline was satisfied: each assigned
1385
+ fraction is positive, so its sum is positive independently of reduction order.
1386
+ With 1,024 bits every evaluation was indeterminate and named the size guard.
1387
+ The measured implementation evaluated the
1388
+ repeated sums separately; these historical measurements include report preparation
1389
+ and do not isolate arithmetic phases. They showed that the product trial's roughly
1390
+ one-second result was not representative of every default-valid arithmetic shape.
1391
+ The synchronous report call then took tens of seconds despite individual values
1392
+ fitting its size limit. The reuse results below describe the subsequent
1393
+ improvement. These measurements motivated the arithmetic allowance above; neither trial
1394
+ establishes a universal safe threshold or a hard deadline.
1395
+
1396
+ Run the script without concurrent builds or tests. Each count/budget pair uses a
1397
+ fresh child, four full calls and the median of the last three. The child has a
1398
+ 180-second timeout and 256 MB V8 old-space allowance; these are diagnostic
1399
+ controls, not library guarantees. [Samples and source hashes](../../benchmarks/explanation-cumulative-sum-review.json)
1400
+ record Node 24.21.0 and 26.8.1 with identical workload construction.
1401
+
1402
+ ### Evaluation reuse and syntax keys
1403
+
1404
+ Reports now reuse the completed local evaluation of identical ordered expression
1405
+ syntax across hard and soft constraints within one report. Each declaration keeps
1406
+ its own ID, evidence, description and soft weight. Keys are constructed iteratively
1407
+ from validated, captured expressions and preserve operand order, arity, literal
1408
+ representation and enum domains; model canonicalization would be unsuitable here
1409
+ because reordered operands can change short-circuit failures. Only Boolean results
1410
+ or failure reasons are retained, not large arithmetic intermediates. A later report
1411
+ starts with a fresh cache, including when assignments or size limits change.
1412
+ Repeated references to the same captured expression root first consult a local
1413
+ WeakMap of completed evaluations, avoiding reconstruction of its syntax key.
1414
+ Separately allocated equivalent expressions still use ordered structural keys.
1415
+ Hard and soft declarations retain independent metadata in both paths. The
1416
+ [shared-predicate trial](../../scripts/solver-shared-predicate-key-bench.mjs)
1417
+ compares one, 32 and 128 predicates with shared versus distinct roots, using
1418
+ identical output hashes. At 128 shared roots, full-report medians change from
1419
+ 34.680 to 31.588 ms on Node 26.8.1 and from 34.341 to 31.326 ms on Node 24.21.0.
1420
+ [All samples and source hashes](../../benchmarks/shared-predicate-key-review.json)
1421
+ retain the smaller and distinct-root controls, which show mixed small changes.
1422
+ The fresh-child four-call trial measures complete reports, with lifetime peak
1423
+ RSS and external 60-second/256 MB old-space controls. It does not establish a
1424
+ general speedup or memory bound.
1425
+
1426
+ Variable references, enum domain names and enum values in these private keys
1427
+ use compact IDs from a shared report-scoped string table. Distinct names and
1428
+ values remain distinct, while long strings are no longer serialized into every
1429
+ repeated reference. Literal sorts and numeric representations stay separate.
1430
+ Canonical model identity and public report names retain their original strings.
1431
+ These private caches are allocated inside one feasible-outcome calculation and
1432
+ are absent from the returned report. Completing or failing that calculation
1433
+ does not place them in a module-level cache or caller-owned object. This is an
1434
+ ownership boundary, not a guarantee about when garbage collection runs. Key
1435
+ construction still allocates traversal and serialized-key storage; the arithmetic
1436
+ work allowance does not meter that construction.
1437
+ The key builder visits each expression occurrence once, appends one syntax token
1438
+ and joins the tokens once; it does not repeatedly concatenate complete subtree
1439
+ keys. A shared subtree inside a new root is still visited at every occurrence.
1440
+ Validation charges those occurrences before report evaluation. The pending-node
1441
+ stack and token array can grow with occurrence count, while serialized numeric
1442
+ literal spelling contributes its own text size. Interned strings still require
1443
+ map lookups and retention for the call. These costs explain why node limits and
1444
+ compact string IDs do not amount to a total key-byte or report-time ceiling.
1445
+ An isolation regression compares 298 predicate shapes evaluated separately with
1446
+ the same predicates combined and repeated in reverse order. Three assignment
1447
+ states and two size budgets cover numeric and Boolean operators, conditional
1448
+ branches, enum domains and escaped enum text, missing assignments and arithmetic
1449
+ failures. Full declaration results and failure reasons must agree; this checks
1450
+ reuse isolation rather than serving as an independent arithmetic oracle.
1451
+
1452
+ Repeating both cumulative trials after this change reduces the largest default
1453
+ rational-sum report from 30,905.307 to 521.557 ms on Node 24 and from 30,024.301 to
1454
+ 452.886 ms on Node 26. The 24-constraint product medians fall from 955.029 to
1455
+ 384.226 ms and from 871.218 to 351.432 ms, respectively. The same sequential
1456
+ harnesses check all evaluations at both size budgets; [all samples and source
1457
+ hashes](../../benchmarks/explanation-expression-reuse-review.json) retain smaller
1458
+ controls and the earlier measurements. This avoids repeated evaluations; distinct
1459
+ expressions still incur their own arithmetic work. Cache keys consume memory
1460
+ proportional to retained expression syntax, and reuse alone supplies neither a
1461
+ general work budget nor a hard report deadline.
1462
+
1463
+ A [long-identifier trial](../../scripts/solver-identifier-size-bench.mjs) holds
1464
+ the model at 8,194 expression nodes while increasing one repeated variable name
1465
+ from 16 to 8,192 characters. Compact references reduce the largest predicate key
1466
+ from 33,619,980 to 61,452 UTF-16 code units. Median full-report times at that size
1467
+ change from 216.216 to 184.648 ms on Node 24 and 147.158 to 109.971 ms on Node 26.
1468
+ Both predicates remain satisfied with `maxExplanationWork: 1`, confirming that
1469
+ this Boolean/key-construction workload lies outside arithmetic accounting.
1470
+ Each case uses a fresh child, four calls and the median of the last three,
1471
+ run sequentially without concurrent builds or tests. The child has a 60-second
1472
+ timeout and 256 MB V8 old-space allowance, neither a library resource guarantee.
1473
+ [Before/after samples and source hashes](../../benchmarks/explanation-identifier-size-review.json)
1474
+ also record lifetime peak RSS, which includes construction and all calls and
1475
+ does not consistently fall. Canonical serialization still repeats authored
1476
+ identifiers, so compact evaluation keys do not bound total report memory.
1477
+
1478
+ A complementary [enum-key trial](../../scripts/solver-enum-key-bench.mjs) uses
1479
+ 6,146 expression nodes with repeated enum domain names and values of 16, 1,024
1480
+ and 8,192 characters each. Interning reduces its largest predicate key from
1481
+ 33,608,716 to 50,188 UTF-16 code units. Largest-case full-report medians change
1482
+ from 237.568 to 165.184 ms on Node 24 and 158.627 to 108.468 ms on Node 26.
1483
+ It uses the same sequential four-call/fresh-child controls as the variable-name
1484
+ trial and remains satisfied with `maxExplanationWork: 1`. The
1485
+ [enum samples and source hashes](../../benchmarks/explanation-enum-key-review.json)
1486
+ retain all sizes and peak RSS; those process-memory observations again do not
1487
+ consistently improve. Canonical enum strings and other report phases remain
1488
+ outside the compact-key optimization and arithmetic counter.
1489
+
1490
+ A [current-runtime checkpoint](../../benchmarks/explanation-key-current-review.json)
1491
+ reruns both unchanged trials sequentially on Node 24.21.0 and 26.8.1. Across all
1492
+ three name lengths, variable keys stay at 61,452 code units and enum keys at
1493
+ 50,188. At 8,192 characters, full-report medians are 123.534/76.417 ms for
1494
+ variables and 114.761/74.242 ms for enums (Node 24/26). These timings include
1495
+ capture, validation, identity and report construction; they do not isolate key
1496
+ construction. The record retains all sizes, samples, lifetime peak RSS and
1497
+ current solver source hashes. Model validation counts shared expression
1498
+ occurrences toward `maxExpressionNodes`, but text length and intermediate key
1499
+ storage are not an overall byte cap. A report-level regression doubles one
1500
+ shared Boolean leaf through eight conjunction levels and uses that graph twice:
1501
+ nine distinct objects represent 1,022 expression occurrences. A 1,021-node
1502
+ limit rejects; 1,022 permits retry. Changing the shared leaf then produces fresh
1503
+ evaluations without changing the earlier report or replacing the caller graph. No additional optimization is inferred
1504
+ from this checkpoint alone.
1505
+
1506
+ A separate [exponent-padding trial](../../scripts/solver-exponent-padding-bench.mjs)
1507
+ keeps a three-node predicate at exactly four budgeted numeric digits while
1508
+ padding the exponent of one real literal with 1, 1,024 or 65,536 zeros. Its key
1509
+ grows from 60 to 1,083 to 65,595 UTF-16 code units because numeric spelling is
1510
+ preserved in syntax identity. Each model rejects a three-digit allowance,
1511
+ passes four, and produces the same complete report hash. Largest-case report
1512
+ medians are 1.407 ms on Node 24.21.0 and 1.450 ms on Node 26.8.1.
1513
+ [All samples and source hashes](../../benchmarks/exponent-padding-review.json)
1514
+ record sequential fresh children, four reports per case and the median after
1515
+ discarding the first. Peak RSS includes construction, key measurement and
1516
+ validation; external 60-second/256 MB controls are not library limits. This
1517
+ isolates another text-size dimension outside the numeric expansion budget;
1518
+ applications accepting arbitrary model text must apply their own input-size
1519
+ limits when required.
1520
+
1521
+
1522
+ ### Canonical serialization costs
1523
+
1524
+ Digest-only operations now hash batches from the canonical token stream instead
1525
+ of assembling the full canonical JSON string first. `Canonical.serialize` still
1526
+ returns the same complete string; `Canonical.digest` and report identity hash
1527
+ the same UTF-8 bytes and preserve validation before/after public input capture.
1528
+ Batches flush at 65,536 UTF-16 code units without splitting tokens, so an
1529
+ individual larger token can exceed that threshold. Canonical model preparation
1530
+ and individual string tokens remain separate allocation costs.
1531
+
1532
+ Repeating the enum trial after this change reduces the largest median full-report
1533
+ time from 168.384 to 146.083 ms on Node 24 and 115.340 to 90.960 ms on Node 26.
1534
+ Lifetime peak RSS changes from 497,216 to 118,672 KiB and 440,896 to 123,296 KiB,
1535
+ respectively. The 16-character control is slightly slower (26.126 to 26.378 ms
1536
+ and 21.381 to 23.861 ms); streaming is a tradeoff, not a universal timing win.
1537
+ [Streaming samples and source hashes](../../benchmarks/explanation-streamed-digest-review.json)
1538
+ use the same sequential fresh-child controls and include all sizes. Peak RSS
1539
+ includes fixture construction and all calls; these observations establish no
1540
+ general process-memory ceiling. Digest-equivalence tests compare public and
1541
+ report hashes with the serialized UTF-8 hash across batch sizes, Unicode and
1542
+ escaped enum values.
1543
+
1544
+ Full serialization now joins the same batches instead of retaining a separate
1545
+ array entry for every JSON token. It still allocates the complete output string.
1546
+ A [serialization trial](../../scripts/solver-canonical-serialization-bench.mjs)
1547
+ uses 5,000 constraints, each containing ten Boolean literals (55,000 expression
1548
+ nodes), and produces 2,604,001 identical UTF-8 bytes before and after batching.
1549
+ Median serialization time changes from 242.430 to 225.194 ms on Node 24 and
1550
+ 205.307 to 186.834 ms on Node 26. Lifetime peak RSS changes from 324,720 to
1551
+ 256,992 KiB and 240,528 to 205,568 KiB, respectively. These are sequential fresh
1552
+ processes with four calls each, the first excluded from the timing median;
1553
+ fixture construction and result checks are outside the timer but included in
1554
+ process memory. The [samples](../../benchmarks/canonical-serialization-batch-review.json)
1555
+ cover this flat Boolean workload only, with a 256 MiB V8 old-space setting,
1556
+ which is not a process-memory limit. They do not establish improvements for
1557
+ all model sizes or remove canonical preparation and complete-output costs.
1558
+
1559
+ Canonical preparation also reuses already converted expression objects across
1560
+ hard constraints, soft constraints and objectives within one captured model.
1561
+ Validation still counts every occurrence, and separate public calls start fresh.
1562
+ This helps shared input graphs; structurally equal but distinct objects are not
1563
+ merged. Repeating the serialization trial, whose 5,000 constraints reference one
1564
+ shared expression, changes median time from 223.263 to 80.950 ms on Node 24 and
1565
+ 195.311 to 77.190 ms on Node 26. Lifetime peak RSS changes from 261,040 to
1566
+ 190,624 KiB and 211,584 to 193,328 KiB. The same bytes and hash are retained.
1567
+ [Preparation-reuse samples](../../benchmarks/canonical-preparation-sharing-review.json)
1568
+ use the same four-call sequential fresh-process controls. The result applies to
1569
+ this shared graph and does not establish a speedup for unshared models or reduce
1570
+ the occurrence-based validation and output-size costs.
1571
+
1572
+ An unshared control expands that same graph through JSON before timing. Median
1573
+ serialization changes from 316.707 to 333.161 ms on Node 24 and 268.574 to
1574
+ 281.136 ms on Node 26: roughly 5% slower in this trial. Lifetime peak RSS is
1575
+ slightly lower, 295,008 to 290,384 KiB and 232,320 to 228,608 KiB. Bytes and
1576
+ hash remain identical. This records the broader cache's tradeoff on an input
1577
+ where no cross-constraint reuse is available. The
1578
+ [unshared samples](../../benchmarks/canonical-preparation-unshared-review.json)
1579
+ use the same sequential fresh-process/four-call controls, including fixture
1580
+ expansion in lifetime RSS but outside timing. Reproduce the current variant
1581
+ with `node --max-old-space-size=256 scripts/solver-canonical-serialization-bench.mjs 5000 unshared`
1582
+ from the repository root; omit `unshared` for the original shared fixture.
1583
+
1584
+ ### Cumulative work calibration
1585
+
1586
+ An [experimental work-policy probe](../../scripts/explanation-work-policy-probe.mjs)
1587
+ changes each sum predicate's nonpositive comparison threshold, so complete
1588
+ expression reuse cannot avoid the arithmetic. Its diagnostic instrumentation
1589
+ charges the maximum estimated bit bound at each successful size check. Rejected
1590
+ charges leave the remaining allowance available for cheaper later operations.
1591
+ On both Node majors the unconstrained diagnostic records 260,443,664 units for
1592
+ 384 satisfied predicates. Candidate allowances produce identical counts:
1593
+
1594
+ | Diagnostic allowance | Satisfied | Indeterminate | Node 24 elapsed ms | Node 26 elapsed ms |
1595
+ |---|---:|---:|---:|---:|
1596
+ | Unconstrained control | 384 | 0 | 32,293.967 | 30,979.379 |
1597
+ | 1,000,000 units | 1 | 383 | 537.501 | 506.233 |
1598
+ | 10,000,000 units | 14 | 370 | 1,644.109 | 1,566.633 |
1599
+
1600
+ These are single instrumented full calls, not warmed medians or deadlines.
1601
+ The probe predates the public work limit. It now explicitly disables that limit
1602
+ while temporarily intercepting guards in isolated children to reproduce the
1603
+ original experimental accounting; its diagnostic allowance is not report metadata. The backend result remains satisfied while affected local
1604
+ evaluations become indeterminate. [Raw observations and source hashes](../../benchmarks/explanation-work-policy-review.json)
1605
+ retain the accounting totals and 120-second/256 MB child controls. This established the deterministic accounting mechanism now used by the runtime.
1606
+ A [calibration harness](../../scripts/explanation-work-calibration.mjs) then ran ten
1607
+ existing workload invocations with unchanged correctness assertions, including
1608
+ 4,096-factor flat products, shared product/negation/division/addition variants,
1609
+ near-budget constant sums, nine intermediate-growth cases and the repeated-sum
1610
+ report. Both Node versions record identical charges with no rejected control at
1611
+ 10,000,000 units; the largest uses 8,851,156. This preserves those useful controls
1612
+ while limiting the distinct-sum workload. [Raw calibration results](../../benchmarks/explanation-work-calibration-review.json)
1613
+ retain every charge count and source hash. Disproportionate arithmetic costs and
1614
+ the uncharged preparation/formatting phases remain separate resource boundaries.
1615
+
1616
+ The shipped guard passes the same calibration assertions on both supported Node
1617
+ versions. Running the distinct-predicate probe with `--native-case 10000000`
1618
+ uses the public limit without intercepting guards: 14 evaluations finish and
1619
+ 370 become indeterminate, with single full-call observations of 1,782.066 ms
1620
+ on Node 24 and 1,605.410 ms on Node 26. [Native results and calibration reruns](../../benchmarks/explanation-work-limit-review.json)
1621
+ record the implementation hashes. These are workload observations, not a promise
1622
+ that every model finishes within two seconds.
1623
+
1624
+ ### Preflight and captured inputs
1625
+
1626
+ Variable, combined hard/soft constraint, and objective counts are checked from
1627
+ array lengths before declaration entries are read. Enum-value counts accumulate
1628
+ by domain and stop before reading the members of a domain that exceeds the
1629
+ budget. For an oversized model these count errors take precedence over errors
1630
+ inside the declarations. Declaration count errors report the full array count;
1631
+ enum-value errors report the cumulative count through the first oversized domain.
1632
+ Expression node/depth limits are checked while traversing, before inspecting
1633
+ an over-budget node, rather than after walking the whole expression. The
1634
+ reported `actual` count is the first exceeded boundary; traversal stops there.
1635
+ Validation uses an explicit traversal stack, so raised depth limits do not rely
1636
+ on JavaScript recursion. Shared subexpressions count at every occurrence and
1637
+ retain their separate diagnostic paths.
1638
+ Capability discovery also uses an explicit traversal stack. Within each root
1639
+ expression it visits shared nodes once and computes variable dependence from
1640
+ completed children, avoiding repeated subtree scans for nonlinear arithmetic.
1641
+ A variable-bearing expression used twice in a product still counts as two
1642
+ factors. Cyclic expressions supplied directly to capability discovery throw
1643
+ `TypeError`; normal solve entry points validate models first.
1644
+
1645
+ Runtime options must be objects containing only `limits` and `unsatCore`;
1646
+ `unsatCore`, when supplied, must be Boolean. Limit overrides must be an object
1647
+ with known limit names and positive safe integer values. Unknown keys and
1648
+ malformed values throw `TypeError` before backend execution, preventing spelling
1649
+ mistakes from silently selecting defaults. Sensitivity's `operation` accepts
1650
+ only `feasibility` or `optimization` (omission defaults to optimization);
1651
+ invalid modes throw `WorkflowValidationError` before solving. Invalid limit
1652
+ objects and functions are described by type without invoking caller-defined
1653
+ string conversion, so diagnostic formatting cannot hide the named limit error.
1654
+ Sensitivity's `maxRuns` diagnostic follows the same rule for malformed objects
1655
+ that survive request copying, and validation rejects them before backend execution.
1656
+ Sensitivity requests must be objects with an array of samples. Missing, null,
1657
+ string and array-like sample containers produce named workflow validation errors;
1658
+ an empty array retains the separate requirement for at least one sample.
1659
+ Optional `fixed` and `observe` fields must also be arrays when supplied; null,
1660
+ strings and array-like objects are rejected before solving. Omit them or use
1661
+ empty arrays when no fixed bindings or observed variables are needed.
1662
+ Sample values and fixed-binding entries must be objects. Null, missing or sparse
1663
+ entries produce validation errors naming the variable or fixed-binding index
1664
+ before any backend call; array holes are not silently skipped.
1665
+ Boolean samples and fixed values require literal `true` or `false`; missing
1666
+ values, strings, numbers and objects are rejected with the variable name.
1667
+ Every sample is validated before the first run, so an invalid later Boolean
1668
+ sample cannot trigger backend work for earlier samples.
1669
+
1670
+ Solver and workflow entry points capture the supplied `unsatCore` and `limits`
1671
+ fields once before defaults and submission. A changing option getter cannot
1672
+ change the core request or replace the limits object between validation and
1673
+ execution. The limit resolver reads each declared override once, including
1674
+ inherited and non-enumerable properties, then fills absent fields with defaults.
1675
+ Explicit `undefined` and invalid inherited values are rejected. Workflows retain
1676
+ the resolved limits before copying their inputs, so those overrides survive
1677
+ submission to the adapter.
1678
+
1679
+ `Solve.run` and `Solve.runWithExplanation` copy the model first, then validate
1680
+ and deeply freeze that same copy before invoking the adapter. Changing getters
1681
+ cannot substitute different model fields between validation and submission.
1682
+ Later caller edits cannot change the submitted model, and adapters must treat
1683
+ it and the resolved limits as read-only. The caller's own objects remain editable.
1684
+
1685
+ Portable record/array copies use an explicit work queue, preserving shared
1686
+ references without relying on native clone recursion. This also applies to
1687
+ workflow inputs and explanation context. Data outside those portable containers
1688
+ is delegated to native structured cloning.
1689
+ Descriptor inspection selects the native path for accessor properties and
1690
+ proxies before copying begins. This preserves native getter count/order and
1691
+ proxy rejection; the iterative path handles portable data properties.
1692
+ Native fallback applies to the whole input graph, not just the accessor or special
1693
+ container that selected it. Deep graphs on that path can therefore fail with a
1694
+ native `RangeError` even when their ordinary-data counterparts copy iteratively.
1695
+ A 20,000-level regression on both supported Node majors verifies the failure,
1696
+ one accessor read and a fresh detached copy after replacing the accessor with a
1697
+ data property. This is a tested fixture, not a portable native depth threshold;
1698
+ raising model depth limits does not raise the native cloning stack limit.
1699
+ Getter mutations follow native cloning semantics, including changes to later
1700
+ properties and shared objects. Capture does not roll back those caller-side
1701
+ effects if a getter throws. The original thrown value propagates; a corrected
1702
+ retry starts a fresh copy rather than reusing a partially captured graph.
1703
+
1704
+ ### Canonical model identity
73
1705
 
74
1706
  `Canonical.serialize` produces stable semantic JSON. Declaration order is
75
1707
  ignored except for objectives, and exact values are reduced to normalized
76
1708
  fractions. Descriptions and evidence row IDs do not affect identity.
77
1709
  `Canonical.digest` returns the full `sha256:<hex>` digest.
1710
+ Both `Canonical.serialize(model, limits?)` and `Canonical.digest(model, limits?)`
1711
+ accept optional validation limits. Limits control acceptance, not identity:
1712
+ the same model has the same serialization and digest under different sufficient
1713
+ limits. Explanations and workflow reports carry their configured limits through
1714
+ hashing and replay checks, so a model accepted with raised limits is not rejected
1715
+ later by the hashing defaults.
1716
+ Expression conversion and canonical JSON rendering use explicit stacks.
1717
+ Commutative operands are ordered by a lazy comparison of their canonical text,
1718
+ avoiding eager rendering of whole subtrees for each comparison. These traversal
1719
+ changes preserve the canonical format and existing model digests.
78
1720
 
79
1721
  ## Adapter results
80
1722
 
@@ -90,23 +1732,219 @@ only then invokes it. Results are disjoint:
90
1732
  - `unknown` carries a structured timeout, resource, cancellation, backend, or
91
1733
  indeterminate reason.
92
1734
 
1735
+ `Solve.run`, `Solve.runWithExplanation`, and direct `Explain.report` calls
1736
+ capture the returned result before checking its status. `optimal` requires
1737
+ literal `optimalityProved: true`; `unsatisfied` requires literal
1738
+ `infeasibilityProved: true`. Missing, false, or coerced markers and unsupported
1739
+ statuses throw `TypeError` instead of producing a proved outcome. A changing
1740
+ getter cannot pass validation and then replace the proof marker in the returned
1741
+ snapshot. `Solve.run` returns an owned copy without mutating the adapter's data.
1742
+ These checks enforce the adapter's assertion contract; the adapter remains
1743
+ responsible for the correctness of its proof assertions and other result fields.
1744
+ Result rejection is local to that call: a later solve invokes the adapter again
1745
+ and validates its newly returned result. If the adapter returns corrected
1746
+ metadata, the same adapter object can be reused. Later changes to its diagnostic
1747
+ objects do not alter a previously captured result or report. If an accessor
1748
+ throws during result capture, that original thrown value propagates before
1749
+ metadata validation. Correcting the same caller-owned result allows a later
1750
+ solve or report call to retry; capture does not retain a partial result. The
1751
+ regression covers all three result boundaries and both Error and unprintable
1752
+ thrown values, including detached diagnostics after successful retry.
1753
+
1754
+ Workflow operations use the same checks before normalizing outcomes. In
1755
+ particular, feasibility may present a proved `optimal` result as `satisfied`,
1756
+ but an optimal result without its proof marker is rejected before that
1757
+ conversion. Optimization retains the validated optimal status and marker.
1758
+
93
1759
  A feasible assignment returned after an optimization timeout is `unknown`,
94
1760
  not `optimal`. A timeout or unsupported capability is never rendered as proof
95
1761
  that the model is infeasible.
96
1762
 
97
1763
  ## Provenance and explanations
98
1764
 
1765
+ Topics in this section:
1766
+
1767
+ - [Declaration provenance](#declaration-provenance)
1768
+ - [Report fields and diagnostic text](#report-fields-and-diagnostic-text)
1769
+ - [Local constraint evaluation](#local-constraint-evaluation)
1770
+ - [Captured reports and input JSON](#captured-reports-and-input-json)
1771
+ - [Workflow context capture](#workflow-context-capture)
1772
+ - [Rendering an explanation](#rendering-an-explanation)
1773
+ - [Binding and snapshot validation](#binding-and-snapshot-validation)
1774
+ - [Shared-expression evaluation trial](#shared-expression-evaluation-trial)
1775
+
1776
+ ### Declaration provenance
1777
+
99
1778
  Variables, constraints, soft constraints, and objectives may carry a stable
100
1779
  declaration URI/line/column, exact CAVE evidence row IDs, and scenario input
101
1780
  IDs. These fields and human descriptions never affect the canonical model
102
1781
  digest.
103
1782
 
1783
+ Descriptions on enum domains, variables, hard/soft constraints and objectives
1784
+ must be strings when supplied; empty descriptions are valid. They remain
1785
+ non-semantic labels and do not affect identity. Checking their runtime types
1786
+ before solving prevents malformed labels from reaching explanation records.
1787
+
1788
+ Declaration locations must be objects with a nonblank string URI and optional
1789
+ positive safe-integer line and column numbers. Either position may be omitted.
1790
+ The text explanation uses `source.cave:7:3` when both are present and
1791
+ `source.cave (column 3)` when only a column is known, preserving the supplied
1792
+ location without inventing a line number. Evidence and scenario input IDs
1793
+ must be dense arrays of unique, nonblank strings; empty arrays are valid.
1794
+ Malformed provenance produces `Validate.ModelValidationError` with the field
1795
+ path before backend execution, including sparse arrays and incorrect runtime
1796
+ types supplied by JavaScript callers.
1797
+
1798
+ ### Report fields and diagnostic text
1799
+
104
1800
  `Solve.runWithExplanation` wraps any adapter result in versioned, plain JSON.
1801
+ `Explain.render` produces deterministic plain text with labeled lines and a final
1802
+ newline. Treat it as text when embedding it in a webpage or another document;
1803
+ that destination supplies its own HTML or Markdown escaping. The renderer expects
1804
+ a typed report produced by the explanation APIs. Its defensive backend-value
1805
+ and input-JSON checks are specific checks, not a general validator for arbitrary
1806
+ caller-edited report structures. It builds the complete output string in memory;
1807
+ input JSON sharing, diagnostic length and declaration count affect output size.
1808
+ The text view quotes fields containing C0/C1 controls, DEL or Unicode
1809
+ line/paragraph separators. This applies to backend metadata, snapshot/scenario
1810
+ text, input queries and IDs, declaration URIs, evidence references and backend
1811
+ assignment/objective/core IDs, as well as values and reasons. Nested input JSON
1812
+ escapes these characters while remaining JSON. Ordinary text keeps its existing
1813
+ display, and rendering does not modify the structured report.
105
1814
  The report records the canonical digest, backend/version, resolved limits,
106
1815
  diagnostics, optional frozen snapshot and authored inputs, assignments,
107
1816
  evaluated hard and soft constraints, objective contributions, or a mapped
108
1817
  unsatisfiable core. Cores are explicitly not promised minimal and `unknown`
109
1818
  keeps its structured reason.
1819
+ Unknown outcomes require an object reason with kind `timeout`, `resource-limit`,
1820
+ `cancelled`, `backend-error`, or `indeterminate`, and a string message (which may
1821
+ be empty). An optional `limit` must name a supported solver limit. Solve and
1822
+ explanation boundaries enforce this after capturing the adapter result.
1823
+ In text rendering, unknown-reason messages containing control characters or
1824
+ Unicode line separators appear as quoted, escaped JSON strings on the same
1825
+ `Unknown:` line. Ordinary single-line messages retain their existing display.
1826
+ The structured report preserves the original message for inspection.
1827
+ The renderer reuses the 35 fixed escapes for DEL, C1 controls and Unicode
1828
+ line/paragraph separators. In a one-million-character U+0085 message, this
1829
+ reduces the measured render median from 97.1 to 34.2 ms on Node 26 and 99.7 to
1830
+ 39.8 ms on Node 24, with identical output. Escaping still expands that message
1831
+ to six characters per input character plus quotes. Numeric solver limits do
1832
+ not bound diagnostic text length, and rendering has no independent text budget.
1833
+ Run `node scripts/solver-diagnostic-render-bench.mjs` alone to reproduce plain
1834
+ and control-heavy cases; [saved measurements](../../benchmarks/diagnostic-render-review.json)
1835
+ include all samples, exact-output checks and source fingerprints. Peak RSS
1836
+ includes fixture construction, expected output and assertions, so it is not an
1837
+ isolated renderer allocation measurement. Small plain-text controls vary; these
1838
+ results do not establish a universal speedup or process-memory bound.
1839
+ `Validate.unknownReason(value)` exposes the same non-mutating check for callers;
1840
+ it throws `TypeError` for malformed reasons and does not capture caller data.
1841
+ Result metadata also requires non-empty backend name/version strings, finite
1842
+ non-negative `elapsedMs` (fractional milliseconds are valid), and a dense array
1843
+ of diagnostic objects. Each diagnostic has level `info`, `warning`, or `error`
1844
+ and string code/message fields, which may be empty. `Validate.resultMetadata`
1845
+ exposes this non-mutating check without capturing caller data. Solver result
1846
+ boundaries apply it after capture, before returning an outcome or explanation.
1847
+ `Validate.explanationContext(context)` exposes the same context checks used by
1848
+ `Explain.report`: scenario identity fields, snapshot policy values, input identity
1849
+ and provenance, and portable input JSON. It validates without capturing or
1850
+ mutating caller data; callers needing an owned report should use `Explain.report`.
1851
+ ### Local constraint evaluation
1852
+
1853
+ Comparisons of constant numeric expressions with literal zero first attempt a
1854
+ conservative sign proof, avoiding unnecessary exact sums when their sign is
1855
+ known. Uncertain or undefined expressions retain ordinary evaluation.
1856
+ Hard and soft constraint evaluation uses an explicit stack, preserving Boolean
1857
+ short-circuiting and selected conditional branches. Deep valid expressions can
1858
+ be evaluated under raised model limits without becoming `indeterminate` from
1859
+ call-stack exhaustion. Missing assignments or undefined selected arithmetic
1860
+ still produce `indeterminate` evaluation. Indeterminate hard and soft constraints
1861
+ include an optional `evaluationReason` describing the local failure, such as a
1862
+ missing assignment, invalid value, or division by zero. `Explain.render` displays
1863
+ that reason as a quoted, escaped string,
1864
+ including escaped C1 controls and Unicode line/paragraph separators, so those
1865
+ characters cannot split the constraint's diagnostic line. Structured reasons
1866
+ retain their original contents. Reasons are human-readable diagnostics,
1867
+ not stable error codes; older reports may omit them. Determinate and skipped
1868
+ branches have no reason, and the backend status remains unchanged.
1869
+ Text rendering labels malformed assignment and objective value shapes as
1870
+ `(invalid backend value)` rather than failing on null values or coercing an
1871
+ invalid Boolean into plausible output. Original values remain in the structured
1872
+ report for inspection. This rendering check concerns value shape; it does not
1873
+ verify numeric validity or backend outcomes. Constraint evaluation separately
1874
+ checks accessed assignments against the model.
1875
+ Integer, rational and enum display strings containing control characters or
1876
+ Unicode line separators are quoted and escaped on their assignment/objective
1877
+ line. This includes multiline enum labels as well as malformed numeric text;
1878
+ the structured values remain unchanged. Ordinary single-line values keep their
1879
+ existing display.
1880
+ Backend boolean assignments must carry
1881
+ primitive booleans: strings such as `"false"`, numbers and objects produce
1882
+ indeterminate hard/soft evaluations when accessed, instead of JavaScript truthiness.
1883
+ Enum variables require assignments with string domain/value fields that match
1884
+ the variable's declared domain and name one of its values. A malformed or out-of-domain
1885
+ assignment likewise makes hard/soft evaluation indeterminate when accessed.
1886
+ Accessed assignments must also match the declared variable kind: boolean,
1887
+ numeric or enum. Integer variables require an integral normalized value. Exact
1888
+ integer and rational representations remain interchangeable when the value
1889
+ belongs to that numeric kind (for example, `2/2` is integral). Wrong-kind or
1890
+ fractional integer assignments make local hard/soft evaluations indeterminate.
1891
+ Numeric assignments must also satisfy any declared minimum and maximum,
1892
+ compared exactly and inclusively. Omitted real bounds impose no restriction on
1893
+ that side. An out-of-range accessed value makes local evaluations indeterminate.
1894
+ Assignment normalization and validation, including failed validation, are cached
1895
+ for one report so repeated hard/soft constraints reuse the same result.
1896
+ Boolean short-circuiting and conditional branch selection still skip unused
1897
+ assignments, even if another constraint has already cached their failure.
1898
+ A fresh report validates its own assignment, so a corrected value can recover.
1899
+ The report retains the backend status and original assignment for inspection;
1900
+ these local evaluations do not reclassify the adapter result.
1901
+
1902
+ ### Captured reports and input JSON
1903
+
1904
+ Direct `Explain.report` calls capture and validate limits once, so model
1905
+ validation and the completed report use the same values even when limits are
1906
+ supplied through changing getters. They copy the model before deriving its digest or
1907
+ evaluating constraints. Numeric validation and outcome evaluation therefore use
1908
+ the same captured literals even when caller properties are changing getters.
1909
+ They also validate the captured explanation context,
1910
+ so getters that change during copying cannot insert non-JSON input values into
1911
+ a completed report.
1912
+ Declared context fields remain included when inherited or non-enumerable.
1913
+ Text rendering likewise captures and revalidates input JSON before serialization,
1914
+ so a nested getter cannot introduce a non-finite number after validation and
1915
+ have it printed as `null`. Custom JSON serializers remain rejected.
1916
+ Input JSON validation and serialization use explicit stacks. Shared acyclic
1917
+ objects remain shared in the captured report, but JSON text repeats them at each
1918
+ occurrence: a 12-level binary graph containing only 13 distinct objects renders
1919
+ 4,096 leaf occurrences. Avoid treating distinct-object count as an output-size
1920
+ bound; model numeric limits do not bound this expansion. Cycles reject before
1921
+ serialization, and repairing a cycle in a caller-edited report permits a fresh
1922
+ render without a stale traversal cache. Failed rendering leaves that report
1923
+ unchanged. These rules apply to both `value` and `authoredValue` input fields.
1924
+ `Explain.report` captures the backend result before inspecting it, so assignment
1925
+ values shown in the report and used to evaluate hard/soft constraints come from
1926
+ the same data even when a programmatic backend supplies changing getters.
1927
+ Completed explanation reports own copies of their backend metadata,
1928
+ diagnostics, assignment and objective values, limits, context and declaration
1929
+ evidence. Changing the original objects afterward cannot rewrite an existing
1930
+ report. This also applies to direct `Explain.report` calls; the copy does not
1931
+ freeze or mutate caller-owned data.
1932
+
1933
+ ### Workflow context capture
1934
+
1935
+ The explanation context is copied before backend execution as well, preserving
1936
+ the submitted descriptions and evidence when callers edit their inputs during
1937
+ an asynchronous solve. A mismatched `context.modelDigest` fails before the
1938
+ backend runs.
1939
+ The workflow entry points copy model, options and context before scope analysis
1940
+ and solving. Feasibility, optimization, counterexample and sensitivity therefore
1941
+ analyze and execute the same captured model, including when supplied fields
1942
+ are getters.
1943
+ Sensitivity additionally copies its full request once, so changes to samples,
1944
+ the selected variable, limits, or evidence during one backend call cannot alter
1945
+ later steps. Replay digests are checked before any workflow backend call.
1946
+
1947
+ ### Rendering an explanation
110
1948
 
111
1949
  ```ts
112
1950
  const report = await Solve.runWithExplanation(adapter, model, {
@@ -129,6 +1967,54 @@ process.stdout.write(Explain.render(report))
129
1967
 
130
1968
  The renderer is a deterministic human view over the same JSON report. Neither
131
1969
  building nor rendering an explanation writes to the CAVE store.
1970
+ Input values and authored values serialize iteratively, so deeply nested valid
1971
+ JSON can render on every supported Node version without relying on the native
1972
+ JSON serializer's recursion limit. Object-key order, string escaping, array
1973
+ order, and omission of undefined object properties follow ordinary JSON output.
1974
+ Input lines retain both normalized and authored values when present, followed
1975
+ by the binding query and sorted evidence-row/scenario-claim references. A
1976
+ missing value is shown as `(no value)`; an explicit JSON `null` remains `null`.
1977
+ ### Binding and snapshot validation
1978
+
1979
+ Explanation bindings require a dense input array with unique nonempty string
1980
+ IDs, optional string queries, and dense arrays of nonempty evidence-row and
1981
+ scenario-claim IDs. Malformed bindings throw `TypeError` when building a report
1982
+ and before backend execution in explanation/workflow calls. These checks
1983
+ validate identity and list shape; external IDs are not resolved against a store.
1984
+ Normalized and authored values must contain finite JSON primitives, dense
1985
+ arrays, or plain objects. Cycles, custom JSON serializers, and non-JSON values
1986
+ are rejected before solving. Shared objects are allowed, as are undefined
1987
+ optional object properties (omitted by JSON serialization). Undefined array
1988
+ members are rejected because JSON would silently change them to null.
1989
+ Snapshot metadata requires a nonempty transaction-time string or explicit null,
1990
+ an optional nonempty valid-time string, supported alias/resolution policies,
1991
+ and a finite minimum confidence in `[0, 1]`. Scenario IDs and input/overlay
1992
+ digests must be nonempty strings. These metadata checks also run before solving;
1993
+ time labels and external digests remain opaque and are not verified against a
1994
+ store. The model digest retains its separate exact replay-identity check.
1995
+ Text output includes supplied snapshot alias/resolution policies and minimum
1996
+ confidence, plus both scenario input and overlay digests. Different evidence
1997
+ scopes therefore remain distinguishable in the human report; omitted policies
1998
+ are not replaced with assumed defaults.
1999
+
2000
+ ### Shared-expression evaluation trial
2001
+
2002
+ `node scripts/explanation-sharing-bench.mjs` compares complete explanation reports
2003
+ for shared addition graphs and equivalent expanded trees at depths 8, 10 and 12.
2004
+ Every repeated operand still contributes to the result. Each fixture runs in an
2005
+ isolated child with five timed calls; fixture construction and assertions are
2006
+ excluded, while model copying, validation, digest generation and evaluation are
2007
+ included. The report records its 10-second child timeout, 128 MB V8 old-space
2008
+ setting and post-workload RSS, which is not peak memory or a process memory cap.
2009
+
2010
+ Sequential darwin-arm64 observations on 2026-09-09 measured depth-12 shared/tree
2011
+ medians of 13.926/87.623 ms on Node 24.21.0 and 15.088/78.198 ms on Node 26.8.1.
2012
+ [Full samples](../../benchmarks/explanation-sharing-review.json) retain all sizes.
2013
+ These full-pipeline observations do not isolate arithmetic evaluation or prove a
2014
+ benefit from adding an evaluator cache. Evaluation currently revisits shared
2015
+ nodes; retaining computed values would also have a memory cost. Assignment
2016
+ isolation, operand multiplicity and short-circuit behavior have an integration
2017
+ regression and must remain intact under any future caching change.
132
2018
 
133
2019
  ## Backend evaluations
134
2020
 
@@ -171,9 +2057,49 @@ const boundary = await Workflow.sensitivity(adapter, model, {
171
2057
  ```
172
2058
 
173
2059
  Workflows require every real variable to have explicit lower and upper bounds.
2060
+ Reported scope theories describe the authored model using capability analysis,
2061
+ including literals, division, enum domains, and rational soft weights as well as
2062
+ variable declarations. A model can require exact rational arithmetic without
2063
+ declaring a real variable. Scope domains still list declared variables only.
174
2064
  Sensitivity accepts an explicit, typed sample list and refuses more than
175
2065
  `maxRuns` checks. Its report includes adjacent result transitions and
176
2066
  contiguous `unknown` regions rather than interpolating across timeouts.
2067
+ Samples are normalized before duplicate detection: object property order and
2068
+ extra fields do not distinguish Boolean or enum values, just as different
2069
+ equivalent numeric spellings do not distinguish exact numbers. Duplicate samples
2070
+ are rejected before backend execution. Reported sample and fixed values contain
2071
+ only their normalized value fields.
2072
+
2073
+ The [sensitivity batch benchmark](../../scripts/sensitivity-batch-bench.mjs)
2074
+ measures complete workflow overhead with an immediate `unknown` adapter: it
2075
+ includes preflight, per-run validation and report construction, but no solver
2076
+ search. Initial macOS arm64 runs on 2026-09-09, before explanation-digest reuse,
2077
+ produced these median times
2078
+ for 64 integer samples (one warmup, five measured calls):
2079
+
2080
+ | Authored constraints | Node 24.21.0 | Node 26.8.1 |
2081
+ |---|---:|---:|
2082
+ | 0 | 6.84 ms | 6.58 ms |
2083
+ | 100 | 188.74 ms | 180.01 ms |
2084
+ | 1,000 | 1,895.71 ms | 1,731.70 ms |
2085
+
2086
+ The fixture repeats simple integer lower-bound constraints; these timings do
2087
+ not predict a real backend's search time or isolate the added preflight cost.
2088
+ A two-sample request whose later generated model exceeds its digit budget
2089
+ rejects before any backend call: medians were 6.72 ms and 6.13 ms with 1,000
2090
+ constraints. [Full samples and runtime metadata](../../benchmarks/sensitivity-batch-runtime-review.json)
2091
+ also include one- and sixteen-sample batches. Batch size and model size both
2092
+ matter even when the backend returns immediately.
2093
+
2094
+ Profiling identified repeated copying and canonical serialization. Workflow
2095
+ reports now reuse the source-model digest already computed by their explanation;
2096
+ the sensitivity summary reuses its first point's digest. This retains per-sample
2097
+ preflight and backend validation. Repeating the same benchmark reduced the
2098
+ 64-sample, 1,000-constraint median to 1,289.74 ms on Node 24.21.0 and 1,207.02 ms
2099
+ on Node 26.8.1, about 32% and 30% below the initial measurements. The
2100
+ [comparison and raw samples](../../benchmarks/workflow-digest-reuse-review.json)
2101
+ retain both runtime results and link the baseline. Actual solver-search costs
2102
+ remain outside this measurement.
177
2103
 
178
2104
  Backend model choices are made deterministic with lexicographic objectives in
179
2105
  stable variable-ID order: false before true, smaller exact numbers first, and
@@ -187,3 +2113,229 @@ Counterexample checks replace one declared invariant with its negation. A
187
2113
  model is a concrete witness; an unsatisfied result means only that the
188
2114
  invariant holds within the report's named assumptions, bounded domains, and
189
2115
  declared Boolean/integer/rational/enum theories. `unknown` remains unknown.
2116
+
2117
+
2118
+ ## Zero rational normalization trial
2119
+
2120
+ `Exact.rational({ numerator: '0', denominator })` validates an integer-string
2121
+ denominator and checks it contains a nonzero digit before returning `0/1`.
2122
+ It avoids constructing the denominator BigInt for this case. Signed integers
2123
+ and leading zeros remain supported; malformed strings and zero denominators
2124
+ still throw. Numeric denominators retain safe-integer validation.
2125
+
2126
+ Run `node scripts/exact-zero-bench.mjs` alone. It compares zero and unit
2127
+ numerators over denominators of 1,000, 100,000 and 1,000,000 decimal digits.
2128
+ Each median uses five calls after one warm-up, with string construction and
2129
+ result assertions outside timing. The unit numerator exercises the unchanged
2130
+ nonzero path. On macOS arm64, the before/after medians were:
2131
+
2132
+ | Denominator digits | Numerator | Node 24.16.0 before → after | Node 26.5.0 before → after |
2133
+ |---|---|---|---|
2134
+ | 1,000 | 0 | 0.006 → 0.003 ms | 0.007 → 0.003 ms |
2135
+ | 100,000 | 0 | 2.115 → 0.068 ms | 2.192 → 0.041 ms |
2136
+ | 1,000,000 | 0 | 29.715 → 0.637 ms | 30.191 → 0.389 ms |
2137
+ | 1,000 | 1 | 0.014 → 0.015 ms | 0.013 → 0.012 ms |
2138
+ | 100,000 | 1 | 6.404 → 6.422 ms | 7.199 → 6.392 ms |
2139
+ | 1,000,000 | 1 | 101.009 → 101.836 ms | 99.966 → 101.443 ms |
2140
+
2141
+ The zero path still scans the supplied text, and this optimization does not
2142
+ bound input size, nonzero normalization, or general intermediate arithmetic.
2143
+ The trial establishes the measured special-case improvement on these runtimes;
2144
+ small timing differences in the nonzero control do not establish a speedup.
2145
+
2146
+
2147
+ ## Unit denominator normalization trial
2148
+
2149
+ For object rationals with an integer-string numerator and a denominator equal
2150
+ to `1` or `-1`, `Exact.rational` validates the integer text and normalizes its
2151
+ sign and leading zeros directly. The numerator avoids a BigInt round trip;
2152
+ negative denominators reverse the sign, and every zero representation remains
2153
+ `0/1`. Both operands retain validation, including safe-integer numeric inputs.
2154
+ The zero/string-denominator shortcut described above remains in place.
2155
+
2156
+ Run `node scripts/exact-unit-denominator-bench.mjs` alone. The numerator is a
2157
+ power of ten with a leading plus and zeros, across 1,000/100,000/1,000,000 digits.
2158
+ Denominators `1` and `-1` exercise the shortcut; `3` controls for general
2159
+ normalization. Each median uses five calls after warm-up, with construction and
2160
+ result assertions outside timing. On macOS arm64:
2161
+
2162
+ | Numerator digits | Denominator | Node 24.16.0 before → after | Node 26.5.0 before → after |
2163
+ |---|---|---|---|
2164
+ | 1,000 | 1 | 0.013 → 0.005 ms | 0.013 → 0.003 ms |
2165
+ | 1,000 | -1 | 0.014 → 0.003 ms | 0.013 → 0.003 ms |
2166
+ | 1,000 | 3 | 0.010 → 0.012 ms | 0.010 → 0.012 ms |
2167
+ | 100,000 | 1 | 6.495 → 0.069 ms | 6.294 → 0.041 ms |
2168
+ | 100,000 | -1 | 6.494 → 0.068 ms | 6.307 → 0.041 ms |
2169
+ | 100,000 | 3 | 6.530 → 6.615 ms | 6.351 → 6.446 ms |
2170
+ | 1,000,000 | 1 | 101.889 → 0.682 ms | 101.306 → 0.390 ms |
2171
+ | 1,000,000 | -1 | 102.394 → 0.685 ms | 101.498 → 0.394 ms |
2172
+ | 1,000,000 | 3 | 101.627 → 103.681 ms | 100.656 → 103.088 ms |
2173
+
2174
+ Input validation still scans text. This special case does not bound input size,
2175
+ non-unit normalization or intermediate arithmetic. The general-path control
2176
+ shows no speedup and slightly higher timings in these sequential runs; it is
2177
+ included to keep the optimization's scope visible.
2178
+
2179
+
2180
+ ## Integer decimal expansion trial
2181
+
2182
+ Exact decimals whose exponent moves the decimal point past all fractional
2183
+ coefficient digits now normalize directly as integer text. Leading zeros and
2184
+ signs normalize before appending zeros; zero returns `0/1` after exponent
2185
+ validation. Trailing coefficient zeros now cancel against positive scales in
2186
+ text before bigint construction: `1.00` returns `1/1` directly, and `0.125000`
2187
+ reduces the smaller `125/1000`. Remaining positive scales use BigInt reduction.
2188
+
2189
+ Run `node scripts/exact-decimal-zero-bench.mjs` alone to measure this cancellation.
2190
+ It checks complete results outside timing and reports five-call medians after
2191
+ warm-up for 1,000, 10,000 and 100,000 trailing zeros. On macOS arm64, the
2192
+ 100,000-zero inputs measured as follows:
2193
+
2194
+ | Prefix before trailing zeros | Node 24.21.0 before → after | Node 26.8.1 before → after |
2195
+ |---|---:|---:|
2196
+ | `1.` | 3.453 → 0.310 ms | 3.442 → 0.292 ms |
2197
+ | `-0.125` | 3.915 → 0.297 ms | 3.868 → 0.277 ms |
2198
+
2199
+ This avoids unnecessary large operands for reducible decimal spellings; it does
2200
+ not cap standalone arithmetic work or alter the model validation budget.
2201
+
2202
+ `python3 scripts/exact-decimal-oracle.py` independently checks 1,504 decimal
2203
+ spellings against Python's `fractions.Fraction`, using seed 2301, signed
2204
+ coefficients, exponents from −100 to 100 and trailing-zero suffixes up to 2,048
2205
+ digits. It uses the Node executable on `PATH`; all cases pass on Node 24.21.0
2206
+ and 26.8.1. This checks exact values, not a timing threshold or general work cap.
2207
+
2208
+ Run `node scripts/exact-integer-decimal-bench.mjs` alone. Inputs are `+001.25eN`
2209
+ and `+001.25e-N`; negative exponents control for fractional reduction. Each
2210
+ median uses five calls after one warm-up, with complete expected-result
2211
+ assertions outside timing. Sequential runs on macOS arm64 produced:
2212
+
2213
+ | Exponent | Node 24.16.0 before → after | Node 26.5.0 before → after |
2214
+ |---|---|---|
2215
+ | +1,000 | 0.013 → 0.003 ms | 0.014 → 0.003 ms |
2216
+ | -1,000 | 0.014 → 0.017 ms | 0.018 → 0.015 ms |
2217
+ | +100,000 | 5.753 → 0.002 ms | 5.678 → 0.002 ms |
2218
+ | -100,000 | 5.927 → 5.947 ms | 5.833 → 5.746 ms |
2219
+ | +1,000,000 | 94.945 → 0.003 ms | 92.589 → 0.002 ms |
2220
+ | -1,000,000 | 96.084 → 95.410 ms | 94.286 → 94.338 ms |
2221
+
2222
+ These measurements cover normalization, not subsequent serialization or
2223
+ arithmetic. JavaScript can defer flattening repeated/concatenated strings;
2224
+ consuming the full result still requires work proportional to its size.
2225
+ Fractional reduction shows no consistent improvement. This shortcut does not
2226
+ establish a general input-size or intermediate-work limit.
2227
+
2228
+ ## Reinforcing-order comparison trial
2229
+
2230
+ Run `node scripts/exact-reinforcing-order-bench.mjs` alone to compare the
2231
+ reinforcing-order shortcut with a reference that normalizes both inputs and
2232
+ compares cross-products directly. The existing
2233
+ `node scripts/exact-cross-product-bench.mjs` trial uses opposing numerator and
2234
+ denominator orders and continues to exercise the general cross-product path.
2235
+
2236
+ An [unequal-denominator checkpoint](../../benchmarks/unequal-comparison-current-review.json)
2237
+ reruns that general path on Node 24.21.0 and 26.8.1. All 108 comparisons per
2238
+ major match the analytically expected sign at 1,000, 10,000 and 100,000 scale
2239
+ digits, for both positive and negative fractions. At the largest size, current
2240
+ medians are 38.362/38.063 ms on Node 24 and 37.755/37.802 ms on Node 26
2241
+ (positive/negative); reference medians range from 37.611 to 38.250 ms.
2242
+ The record retains all samples and source hashes. Runs are sequential, with
2243
+ reference before current in each case, two warmups and seven measured calls;
2244
+ normalization is timed, fixture construction and assertions are not. This
2245
+ standalone comparison fixture exceeds default model numeric budgets. It shows
2246
+ comparable measured costs, not a speedup, memory bound or universal arithmetic
2247
+ limit; no further optimization follows from this case alone.
2248
+
2249
+ The reinforcing-order trial covers positive and negative fractions at 1,000,
2250
+ 10,000 and 100,000 digits, with two warmups and seven measured samples per case.
2251
+ Fixture construction and result assertions are outside timing; normalization is
2252
+ included. At the 100,000-digit scale, local medians were:
2253
+
2254
+ | Runtime | Fraction signs | Cross-product reference | Order shortcut |
2255
+ |---|---|---:|---:|
2256
+ | Node 24.21.0 | Positive | 37.523 ms | 26.507 ms |
2257
+ | Node 24.21.0 | Negative | 37.826 ms | 26.516 ms |
2258
+ | Node 26.8.1 | Positive | 37.203 ms | 26.271 ms |
2259
+ | Node 26.8.1 | Negative | 39.611 ms | 26.324 ms |
2260
+
2261
+ This measures the stated arithmetic reference against the shortcut, rather than
2262
+ comparing releases or complete solver workloads. Normalization still dominates.
2263
+ The current shortcut includes canonical text ordering described below. These
2264
+ measurements do not establish a general speedup or a resource budget.
2265
+
2266
+ The cross-product control script now also runs the direct reference, using two
2267
+ warmups and seven measured samples per case with assertions outside timing.
2268
+ At 100,000 digits, the current comparison stays close to that reference:
2269
+
2270
+ | Runtime | Sign | Cross-product reference | Current comparison |
2271
+ |---|---|---:|---:|
2272
+ | Node 24.21.0 | Positive | 37.552 ms | 37.522 ms |
2273
+ | Node 24.21.0 | Negative | 37.909 ms | 37.707 ms |
2274
+ | Node 26.8.1 | Positive | 37.174 ms | 37.111 ms |
2275
+ | Node 26.8.1 | Negative | 37.302 ms | 37.226 ms |
2276
+
2277
+ Both scripts retain every measured sample. The
2278
+ [fraction control record](../../benchmarks/exact-fraction-order-control-review.json)
2279
+ contains the sequential macOS arm64 runs for both workloads and runtimes. These
2280
+ controls show no material overhead for this cross-product fixture; their small
2281
+ timing differences are not evidence of a general improvement in cross-products.
2282
+
2283
+ ## Canonical integer order trial
2284
+
2285
+ Exact comparison normalizes and validates both operands first, then compares
2286
+ canonical integer text by sign, digit count and lexical order. Equal-denominator,
2287
+ zero, opposite-sign, equal-numerator and reinforcing-order shortcuts therefore
2288
+ avoid reparsing normalized magnitudes into BigInts. Comparisons that need
2289
+ cross-products still allocate BigInts. Fraction normalization may itself require
2290
+ BigInt parsing and GCD work; this does not remove that cost.
2291
+
2292
+ Run `node scripts/exact-integer-order-bench.mjs` alone. It compares integer-valued
2293
+ decimal inputs with a reference that normalizes both operands and reparses their
2294
+ numerators as BigInts. Positive and negative cases use 1,000, 100,000 and
2295
+ 1,000,000 digits, two warmups and seven measured samples. Normalization is timed;
2296
+ fixture construction and assertions are excluded. Local one-million-digit medians:
2297
+
2298
+ | Runtime | Sign | BigInt reference | Text order |
2299
+ |---|---|---:|---:|
2300
+ | Node 24.21.0 | Positive | 127.116 ms | 1.153 ms |
2301
+ | Node 24.21.0 | Negative | 127.804 ms | 1.380 ms |
2302
+ | Node 26.8.1 | Positive | 127.378 ms | 1.108 ms |
2303
+ | Node 26.8.1 | Negative | 128.286 ms | 1.097 ms |
2304
+
2305
+ [Raw samples](../../benchmarks/exact-integer-order-review.json) record the macOS
2306
+ arm64 trial. These are standalone Exact workloads, not model-size limits or
2307
+ complete solver timings. Existing operand validation remains in force, and
2308
+ there is still no general standalone input-size or intermediate-work budget.
2309
+
2310
+ ## Zero detection trial
2311
+
2312
+ `Exact.isZero` validates numerator and denominator integer fields but does not
2313
+ reduce a fraction merely to inspect its numerator. A zero denominator is still
2314
+ invalid even when the numerator is zero. Each field is read once, and an invalid
2315
+ numerator fails before reading the denominator. Decimal strings share the
2316
+ normalizer's syntax and safe-integer exponent checks, then inspect the coefficient
2317
+ without expanding its magnitude. For example, `Exact.isZero('1e9007199254740991')`
2318
+ returns false without allocating that number; this does not mean normalizing or
2319
+ solving with that magnitude is practical or within model limits.
2320
+
2321
+ Linearity analysis uses this predicate after evaluating a constant divisor.
2322
+ The shortcut avoids another normalization of that result; initial evaluation and
2323
+ other arithmetic retain their existing costs and limits.
2324
+
2325
+ Run `node scripts/exact-zero-bench.mjs` alone to compare the predicate with full
2326
+ rational normalization. The trial uses zero/nonzero numerators and denominators
2327
+ of 1,000, 100,000 and 1,000,000 digits, two warmups and seven measured samples.
2328
+ Validation is timed; fixture construction and assertions are excluded. Local
2329
+ one-million-digit medians:
2330
+
2331
+ | Runtime | Numerator | Normalization reference | Zero detection |
2332
+ |---|---|---:|---:|
2333
+ | Node 24.21.0 | Nonzero | 208.396 ms | 0.779 ms |
2334
+ | Node 24.21.0 | Zero | 0.390 ms | 0.389 ms |
2335
+ | Node 26.8.1 | Nonzero | 206.269 ms | 0.778 ms |
2336
+ | Node 26.8.1 | Zero | 0.390 ms | 0.385 ms |
2337
+
2338
+ [Raw samples](../../benchmarks/exact-zero-review.json) record the sequential macOS
2339
+ arm64 trials. Zero numerators already avoided expensive normalization, so the
2340
+ small timing differences in those controls are not evidence of a speedup.
2341
+ This predicate improvement does not add a general arithmetic resource budget.