@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.
- package/README.md +2155 -3
- package/dist/src/adapter.d.ts +6 -0
- package/dist/src/adapter.d.ts.map +1 -1
- package/dist/src/adapter.js +3 -0
- package/dist/src/adapter.js.map +1 -1
- package/dist/src/balanced-reduce.d.ts +3 -0
- package/dist/src/balanced-reduce.d.ts.map +1 -0
- package/dist/src/balanced-reduce.js +10 -0
- package/dist/src/balanced-reduce.js.map +1 -0
- package/dist/src/canonical-owned.d.ts +5 -0
- package/dist/src/canonical-owned.d.ts.map +1 -0
- package/dist/src/canonical-owned.js +206 -0
- package/dist/src/canonical-owned.js.map +1 -0
- package/dist/src/canonical.d.ts +3 -2
- package/dist/src/canonical.d.ts.map +1 -1
- package/dist/src/canonical.js +12 -85
- package/dist/src/canonical.js.map +1 -1
- package/dist/src/capability.d.ts.map +1 -1
- package/dist/src/capability.js +43 -33
- package/dist/src/capability.js.map +1 -1
- package/dist/src/clone.d.ts +3 -0
- package/dist/src/clone.d.ts.map +1 -0
- package/dist/src/clone.js +54 -0
- package/dist/src/clone.js.map +1 -0
- package/dist/src/constant-sign.d.ts +5 -0
- package/dist/src/constant-sign.d.ts.map +1 -0
- package/dist/src/constant-sign.js +54 -0
- package/dist/src/constant-sign.js.map +1 -0
- package/dist/src/context.d.ts +6 -0
- package/dist/src/context.d.ts.map +1 -0
- package/dist/src/context.js +151 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/diagnostic-text.d.ts +3 -0
- package/dist/src/diagnostic-text.d.ts.map +1 -0
- package/dist/src/diagnostic-text.js +5 -0
- package/dist/src/diagnostic-text.js.map +1 -0
- package/dist/src/evaluate-expression.d.ts +4 -0
- package/dist/src/evaluate-expression.d.ts.map +1 -0
- package/dist/src/evaluate-expression.js +24 -0
- package/dist/src/evaluate-expression.js.map +1 -0
- package/dist/src/exact.d.ts +2 -0
- package/dist/src/exact.d.ts.map +1 -1
- package/dist/src/exact.js +125 -28
- package/dist/src/exact.js.map +1 -1
- package/dist/src/explain.d.ts +4 -0
- package/dist/src/explain.d.ts.map +1 -1
- package/dist/src/explain.js +330 -91
- package/dist/src/explain.js.map +1 -1
- package/dist/src/explanation-budget.d.ts +14 -0
- package/dist/src/explanation-budget.d.ts.map +1 -0
- package/dist/src/explanation-budget.js +46 -0
- package/dist/src/explanation-budget.js.map +1 -0
- package/dist/src/expression-key.d.ts +8 -0
- package/dist/src/expression-key.d.ts.map +1 -0
- package/dist/src/expression-key.js +54 -0
- package/dist/src/expression-key.js.map +1 -0
- package/dist/src/fraction-product.d.ts +3 -0
- package/dist/src/fraction-product.d.ts.map +1 -0
- package/dist/src/fraction-product.js +13 -0
- package/dist/src/fraction-product.js.map +1 -0
- package/dist/src/fraction-sum.d.ts +3 -0
- package/dist/src/fraction-sum.d.ts.map +1 -0
- package/dist/src/fraction-sum.js +23 -0
- package/dist/src/fraction-sum.js.map +1 -0
- package/dist/src/integer-gcd.d.ts +3 -0
- package/dist/src/integer-gcd.d.ts.map +1 -0
- package/dist/src/integer-gcd.js +50 -0
- package/dist/src/integer-gcd.js.map +1 -0
- package/dist/src/linear.d.ts +2 -1
- package/dist/src/linear.d.ts.map +1 -1
- package/dist/src/linear.js +85 -17
- package/dist/src/linear.js.map +1 -1
- package/dist/src/numeric-size.d.ts +2 -0
- package/dist/src/numeric-size.d.ts.map +1 -0
- package/dist/src/numeric-size.js +33 -0
- package/dist/src/numeric-size.js.map +1 -0
- package/dist/src/options.d.ts +4 -0
- package/dist/src/options.d.ts.map +1 -0
- package/dist/src/options.js +18 -0
- package/dist/src/options.js.map +1 -0
- package/dist/src/result.d.ts +4 -0
- package/dist/src/result.d.ts.map +1 -0
- package/dist/src/result.js +29 -0
- package/dist/src/result.js.map +1 -0
- package/dist/src/solve.d.ts.map +1 -1
- package/dist/src/solve.js +34 -3
- package/dist/src/solve.js.map +1 -1
- package/dist/src/validate.d.ts +5 -0
- package/dist/src/validate.d.ts.map +1 -1
- package/dist/src/validate.js +323 -65
- package/dist/src/validate.js.map +1 -1
- package/dist/src/workflow.d.ts.map +1 -1
- package/dist/src/workflow.js +98 -26
- package/dist/src/workflow.js.map +1 -1
- package/package.json +2 -2
- package/src/adapter.ts +9 -0
- package/src/balanced-reduce.ts +9 -0
- package/src/canonical-owned.ts +186 -0
- package/src/canonical.ts +14 -90
- package/src/capability.ts +41 -30
- package/src/clone.ts +47 -0
- package/src/constant-sign.ts +59 -0
- package/src/context.ts +114 -0
- package/src/diagnostic-text.ts +4 -0
- package/src/evaluate-expression.ts +21 -0
- package/src/exact.ts +122 -27
- package/src/explain.ts +342 -94
- package/src/explanation-budget.ts +47 -0
- package/src/expression-key.ts +45 -0
- package/src/fraction-product.ts +10 -0
- package/src/fraction-sum.ts +20 -0
- package/src/integer-gcd.ts +47 -0
- package/src/linear.ts +82 -16
- package/src/numeric-size.ts +26 -0
- package/src/options.ts +18 -0
- package/src/result.ts +28 -0
- package/src/solve.ts +34 -3
- package/src/validate.ts +312 -66
- 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.
|
|
71
|
-
|
|
72
|
-
|
|
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.
|