@cavelang/scenario 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 CHANGED
@@ -3,8 +3,56 @@
3
3
  Typed, replayable inputs for decision evaluators and formal solver models.
4
4
  The package binds ordinary CAVE-Q patterns against an explicit transaction-
5
5
  and valid-time snapshot, then overlays hypothetical CAVE claims inside a
6
- rolled-back savepoint. The returned record is plain immutable data; no
7
- database transaction remains open while an evaluator or Wasm solver runs.
6
+ rolled-back savepoint. The returned record is plain immutable data; the scenario
7
+ operation leaves no transaction open while an evaluator or Wasm solver runs.
8
+ A caller-owned outer transaction remains under the caller's control.
9
+
10
+ | Task | Start here |
11
+ |---|---|
12
+ | Choose binding, snapshot and missing-data policies | [Contracts](#contracts) |
13
+ | Convert units or combine exact measurements | [Combining measurements](#combining-measurements) |
14
+ | Save an evaluation, recommendation or decision | [Explicit result governance](#explicit-result-governance) |
15
+ | Recover from a rejected record or retry an existing ID | [Recording and retry](#recording-and-retry) |
16
+ | Check a stored solver result against a model/backend | [Replay compatibility](#replay-compatibility) |
17
+ | Inspect measured parsing and arithmetic costs | [Performance measurements](#performance-measurements) |
18
+
19
+ `bind()` first captures the definition's own enumerable data, including nested
20
+ snapshot and binding fields. Validation, materialization and digesting use that
21
+ captured data, so changing getters cannot give the resulting record a different
22
+ identity or policy from the one validated. The capture does not freeze the
23
+ caller-owned definition.
24
+ Capture traverses nested data iteratively, so deeply nested malformed values
25
+ reach field validation instead of exhausting the JavaScript call stack. It
26
+ preserves shared references and captures sibling getters before descending into
27
+ their values. This removes a stack-depth failure; it does not impose a general
28
+ memory or execution-time budget on caller data.
29
+ After field validation, binding computes the definition digest before database
30
+ access. The complete captured definition, including extra caller metadata, must
31
+ be JSON serializable within the runtime's serialization depth. Cycles, BigInt
32
+ values and excessive serialization depth reject with `invalid-definition`;
33
+ correcting the definition permits a fresh binding on the same store.
34
+
35
+ `bind()` deeply freezes its returned record, including nested values, evidence
36
+ arrays and snapshot fields; `run()` passes that same immutable form to the
37
+ evaluator. Strict-mode mutation attempts throw instead of silently changing data
38
+ under an already-computed digest. The caller's definition remains mutable and
39
+ is not frozen. The returned snapshot contains only the documented snapshot
40
+ fields, not unrelated caller metadata. JSON serialization remains available;
41
+ parsed copies are ordinary mutable objects whose digests are checked on replay.
42
+ Binding checks SQLite's external commit revision and local change count around
43
+ materialization. A change that could mix database states throws
44
+ `ScenarioInputError` with code `snapshot-changed`; retry the binding before
45
+ evaluating. Even a backdated import below the captured transaction head
46
+ invalidates that attempt. Rolled-back overlay inserts are accounted for, and
47
+ bindings without an overlay remain usable on read-only connections.
48
+
49
+ Each binding reads the current requested snapshot without retaining a projection
50
+ cache. Inside a caller transaction it can observe staged rows. An outer or
51
+ savepoint rollback does not rewrite an input record already returned, including
52
+ its values, digest and evidence IDs; a fresh binding reflects the restored store.
53
+
54
+ Given an open `store` containing `system HAS team-size: 8 people`, this
55
+ definition evaluates a hypothetical team of 12 without changing that claim:
8
56
 
9
57
  ```ts
10
58
  import { bind, explanationContext, Model } from '@cavelang/scenario'
@@ -38,17 +86,149 @@ const inputs = bind(store, definition)
38
86
  // inputs.values['team-size'] is exact integer 12; the store still says 8.
39
87
  ```
40
88
 
89
+ ## Combining measurements
90
+
91
+ Choose a common target unit before summing or comparing measurements. This
92
+ complete example converts seconds to milliseconds and sums exactly. Conversion
93
+ factors describe target units per source unit; conversions are directed and
94
+ are not chained automatically.
95
+
96
+ ```ts
97
+ import assert from 'node:assert/strict'
98
+ import { open } from '@cavelang/store'
99
+ import { bind, type Model } from '@cavelang/scenario'
100
+
101
+ const store = open()
102
+ try {
103
+ store.ingest('a HAS duration: 1 s\nb HAS duration: 500 ms')
104
+ const definition: Model.Definition = {
105
+ id: 'total-duration',
106
+ modelDigest: `sha256:${'0'.repeat(64)}`, // replace with your evaluator's model digest
107
+ snapshot: { aliases: 'exact', resolution: 'winner', minimumConfidence: 0.5 },
108
+ bindings: [{
109
+ id: 'duration',
110
+ query: '?job HAS duration: ?value',
111
+ select: 'value',
112
+ expected: {
113
+ kind: 'integer', unit: 'ms',
114
+ conversions: [{ from: 's', to: 'ms', factor: '1000' }]
115
+ },
116
+ cardinality: 'many', reduce: 'sum', scenarioOverride: false,
117
+ policies: { missing: 'reject', contested: 'reject', retracted: 'exclude', unresolved: 'reject' }
118
+ }]
119
+ }
120
+ const inputs = bind(store, definition)
121
+ assert.deepEqual(inputs.values['duration'], {
122
+ kind: 'integer', value: '1500', unit: 'ms', approximate: false
123
+ })
124
+ } finally {
125
+ store.close()
126
+ }
127
+ ```
128
+
129
+ Without the target unit and conversion, these mixed-unit inputs fail with
130
+ `incompatible-unit`. Use `reduce: 'all'` to retain separate measurements instead
131
+ of computing a total. Each candidate retains its evidence even when reduced.
132
+
41
133
  ## Contracts
42
134
 
43
135
  - `asOf`, `at`, alias handling, resolution, and minimum confidence are frozen
44
136
  into every input record. Omitting `asOf` records the exact current head.
137
+ Scenario boundaries use the shared CAVE-Q parsers. Transaction-time `asOf`
138
+ includes the full UTC year, month, day or timestamp second; a UUIDv7 includes
139
+ that exact append. Transaction periods are clipped at the UUID epoch, so
140
+ wholly pre-1970 `asOf` periods contain no recorded transactions. Valid-time
141
+ `at` remains independent and anchors at the start of its named period
142
+ or at the specified timestamp. Both boundaries require valid strings and
143
+ reject impossible calendar dates before database access, even when no
144
+ bindings are requested. UUIDv7 is accepted only for `asOf`.
145
+ - `definitionDigest` identifies the complete authored definition, including
146
+ binding queries, types, policies, snapshot settings, and overlay text. Object
147
+ key order and omitted undefined optional fields do not change the digest.
148
+ - Winner-mode bindings query resolved candidates directly. Coexisting mode
149
+ reads both unresolved and resolved candidates to identify contested evidence;
150
+ winner mode does not perform that unused unresolved read.
45
151
  - Cardinality and missing/contested/retracted/unresolved behavior are explicit.
46
152
  `many` additionally requires `all`, `min`, `max`, or `sum`; there is no
47
153
  implicit first-row policy.
154
+ Unknown or omitted choices in snapshot modes, cardinality, reduction, expected
155
+ value kind, or these policies fail with `ScenarioInputError` (`invalid-definition`)
156
+ before database access. `scenarioOverride` must be an explicit Boolean.
157
+ Malformed definition, snapshot, binding, expected-value and policy containers
158
+ use that same classified error, including nulls and arrays supplied as objects.
159
+ `bindings` must be an array of binding objects without holes; query and supplied
160
+ overlay text must be strings. These checks also protect JavaScript callers
161
+ loading definitions from JSON.
162
+ Numeric target units, when supplied, must be non-empty strings. This is
163
+ checked even for optional or many-valued inputs with no matching rows; omit
164
+ `unit` to retain the authored unit without requesting a conversion.
165
+ A supplied `select` must be a string naming a variable bound by the query,
166
+ without its leading `?` (for example, `select: 'n'` for `?n`). Subject,
167
+ verb, object and attribute value variables are selectable. Invalid selections
168
+ fail before reads, including optional inputs with no matching rows. Only
169
+ Boolean existence inputs may omit `select`.
170
+ Numeric inputs parse the selected variable, just like other input kinds;
171
+ they never substitute an unrelated attribute value from the matching row.
172
+ Selecting an attribute value retains its valid-time interpolation and
173
+ uncertainty. Selecting another slot does not inherit that attribute's
174
+ uncertainty, including when the match comes from an overlay.
175
+ Valid binding names such as `constructor` and `toString` are ordinary keys;
176
+ absent overlay entries never resolve through JavaScript's object prototype.
177
+ Scenario IDs, binding IDs and model digests must be strings at runtime;
178
+ numbers, missing IDs, boxed strings and other coercible values fail before
179
+ database access. The string binding ID `"1"` remains valid, while numeric `1`
180
+ cannot bypass duplicate detection and overwrite that same input slot.
181
+ Enum definitions require a non-empty, dense array of unique strings. A
182
+ string, set, array hole or non-string entry is rejected before reads, even
183
+ for an optional binding with no matching rows. Enum membership uses exact,
184
+ case-sensitive string equality; a substring is not an allowed enum value.
185
+ Boolean `trueValue` and `falseValue` mappings must be strings when supplied
186
+ and must remain distinct after applying the defaults `"true"` and `"false"`.
187
+ Overlapping mappings fail before reads instead of silently favoring true.
188
+ Empty strings remain valid explicit mappings when the two values differ.
189
+ - Decimal trailing zeros cancel before large bigint operands are constructed.
190
+ This preserves multiplier and conversion semantics: `1.25000M USD` is exactly
191
+ `1250000 USD`, and `0.125000ms` converts to `1/8000 s` with factor `0.001`.
192
+ The approximation flag is retained independently of exact arithmetic.
193
+ Scalars without a K/M/B/T multiplier reuse the normalized fraction directly.
194
+ Stored long decimal spellings retain exact converted uncertainty, confidence
195
+ and source row evidence; repeating the binding does not append store history.
48
196
  - Authored decimals and multiplier spellings become exact rationals without a
49
197
  JavaScript `number` round trip. Unit changes require a declared exact
50
198
  conversion. Approximation, uncertainty, sigma level, and confidence remain
51
199
  separate fields and never become solver weights automatically.
200
+ Conversion definitions must be an array of entries with non-empty string
201
+ endpoints and positive exact rational factors. Each directed unit pair may
202
+ appear only once, even with identical factors; the reverse direction is a
203
+ separate pair. Malformed entries and duplicate pairs fail as invalid
204
+ definitions before reads, so array order cannot choose between conflicting
205
+ factors.
206
+ Numeric `sum`, `min` and `max` require all candidates to have the same unit
207
+ after conversion, or all to be unitless. Mixed units (including unitless
208
+ values mixed with units) fail with `incompatible-unit`. Declare a target
209
+ `unit` and explicit conversions to combine compatible measurements. The
210
+ `all` reducer retains each candidate's own unit without combining values.
211
+ Equal-denominator sums add numerators before reduction, and comparisons of
212
+ normalized equal-denominator values compare numerators directly. With positive
213
+ denominators, zero or opposite-sign numerators also determine ordering without
214
+ products; equal nonzero numerators use denominator order with the appropriate
215
+ sign. Same-sign fractions also avoid products when numerator and denominator
216
+ orders reinforce each other, as in the solver's exact comparison. These
217
+ shortcuts avoid expanding intermediate integers in those cases. A 16,900-pair
218
+ integer cross-product regression covers both denominator signs, and large
219
+ reinforcing-order cases check differences below floating-point precision.
220
+ Other comparisons compute each cross-product once and account for the sign of its
221
+ common denominator. Parsing and conversion normalize denominators positive;
222
+ direct internal comparison also orders equivalent negative-denominator forms
223
+ correctly. Binding regressions also cover negative converted values separated
224
+ below floating-point precision: `min` and `max` retain their exact ordering,
225
+ including a positive conversion factor written with two negative components.
226
+ Repeated binding preserves the input record and store history. These paths retain exactness
227
+ while avoiding unnecessarily large intermediates; they impose no work cap.
228
+
229
+ See [Performance measurements](#performance-measurements) for reproducible
230
+ parsing, arithmetic and reduction benchmarks.
231
+
52
232
  - Durable evidence uses exact CAVE row IDs. Rolled-back assumptions use stable
53
233
  `scenario:<scenario-id>:<overlay-digest>#<claim>` IDs, so replay identity
54
234
  does not depend on transient SQLite row IDs.
@@ -56,18 +236,58 @@ const inputs = bind(store, definition)
56
236
  by their authored entity names. This prevents current alias state from
57
237
  leaking into a historical snapshot; transitive overlay composition waits on
58
238
  the shared snapshot-query primitives tracked in the backlog.
239
+ Confidence, tag, context and numeric `WHERE` filters use the shared query
240
+ engine for both stored and hypothetical claims. Overlay candidates retain
241
+ their authored confidence and scenario evidence IDs. Temporary rows are
242
+ rolled back before typed input conversion, including conversions that fail.
243
+ Invalid conversion factors report `invalid-definition`; missing unit mappings
244
+ or unitless values with a required target unit report `incompatible-unit`.
245
+ A valid conversion that produces a fraction for an integer binding reports
246
+ `invalid-value`. These failures preserve transaction history. Corrected
247
+ bindings can run immediately, and subsequent unpinned bindings observe new
248
+ committed facts rather than retaining failed overlay values.
59
249
 
60
250
  Use `run(store, definition, evaluate)` when convenient. It calls `bind`
61
251
  synchronously and invokes `evaluate` only after the overlay and verb registry
62
252
  have been restored, including when the evaluator later times out or crashes.
253
+ If overlay insertion throws after writing temporary claims or vocabulary,
254
+ `run` rejects with that error without invoking the evaluator. The transaction
255
+ restores history and the verb registry; the same definition can be retried
256
+ after the insertion failure is resolved.
257
+ An asynchronous evaluator holds no overlay transaction while its promise is
258
+ pending. Subsequent store writes persist whether that promise resolves or
259
+ rejects. `run` forwards the evaluator's result or rejection; it does not impose
260
+ a deadline or provide cancellation. Configure those in the evaluator itself.
63
261
 
64
262
  For solver runs, `explanationContext(definition, inputs)` converts the frozen
65
263
  record into `@cavelang/solver` explanation metadata. It retains every binding's
66
264
  query, typed and authored value, exact belief/scenario evidence IDs, snapshot
67
- policy, input-record digest, and overlay digest. Passing that context to
265
+ policy, input-record digest, and overlay digest. Input records must use
266
+ `cave.scenario/inputs@1`; unsupported schemas reject even when their content
267
+ digest matches. Each declared binding must occur exactly once in the record;
268
+ missing, duplicate and unknown binding IDs reject before metadata is returned.
269
+ Passing that context to
68
270
  `Solve.runWithExplanation` also rejects replay against a different canonical
69
271
  model digest.
70
272
 
273
+ Before attaching queries to evidence, `explanationContext` verifies both the
274
+ definition digest and the input record's content digest. Changed definitions
275
+ or modified records fail explicitly. It captures the supplied definition once
276
+ before those checks and uses that captured definition for the returned queries.
277
+ Changing getters cannot attach a different query after digest validation;
278
+ a later call captures fresh values and rejects a changed definition.
279
+
280
+ The input record is also copied before validation. Checked values and evidence
281
+ come from that copy, so changing record getters or later mutations of a
282
+ JSON-loaded record cannot rewrite the explanation's validated inputs. A new
283
+ call rechecks the supplied record and rejects contents carrying a stale digest.
284
+
285
+ Older input records without
286
+ `definitionDigest` remain readable as stored artifacts but must be rebound
287
+ with the intended definition before generating a verified explanation.
288
+ Rebinding produces a new input digest; record any new evaluation under a new
289
+ run ID so the earlier artifact keeps its original content and provenance.
290
+
71
291
  ```ts
72
292
  const inputs = bind(store, definition)
73
293
  const report = await Solve.runWithExplanation(
@@ -128,11 +348,104 @@ runs only after rollback; recording, recommendation, and human decision remain
128
348
  three explicit operations. The package test suite exercises this complete
129
349
  non-solver architecture-choice workflow.
130
350
 
351
+ `run()` returns the evaluator's result without copying or freezing it. Its input
352
+ record is frozen, but ownership of the returned result stays with the caller.
353
+ JSON validation and snapshot capture happen when `Record.result()` is called;
354
+ changes to an output before that call become part of the recorded evaluation.
355
+
356
+ Before recording an external evaluation, `Record.result` requires non-empty
357
+ string evaluator names and versions, an explicit JSON output (`null` is valid),
358
+ and an input digest matching the supplied input record's contents. Missing
359
+ metadata or modified inputs carrying an old digest throw `TypeError` without
360
+ appending a claim. This uses the same content-digest check as solver explanation
361
+ construction. Previously stored artifacts remain readable; rebind changed
362
+ inputs and use a new result ID for a corrected evaluation.
363
+
364
+ ### Recording and retry
365
+
366
+ Recording captures one JSON snapshot of the supplied artifact before opening
367
+ its transaction. Getters are read during that capture; validation, predecessor
368
+ checks, identity and stored content all use the captured values. The returned
369
+ outcome contains that stored snapshot, so later changes to caller-owned objects
370
+ do not change it. Predecessor checks and writes still share one transaction.
371
+ The outcome's artifact is a detached, mutable JSON object. Editing it does not
372
+ edit stored history; a fresh `Record.read()` returns the stored content, and
373
+ recording that unchanged content again returns `existing`.
374
+
375
+ When several prepared artifacts must be recorded together, wrap their writes
376
+ in one synchronous caller transaction:
377
+
378
+ ```ts
379
+ store.transaction(() => {
380
+ Record.result(store, evaluation)
381
+ Record.recommendation(store, recommendation)
382
+ Record.decision(store, decision)
383
+ })
384
+ ```
385
+
386
+ Here `recommendation` and `decision` are already prepared artifacts referencing
387
+ the preceding IDs. If any call throws, let the exception escape the callback:
388
+ all writes in this transaction roll back, including newly recorded predecessors.
389
+ Correct the rejected artifact and retry with the same IDs. Repeating a committed
390
+ chain with identical content returns `existing` for each artifact. Perform
391
+ evaluation and obtain the human decision before opening this transaction.
392
+
131
393
  Each stable ID owns one append-only artifact series. Re-recording identical
132
- content returns `existing`; reusing the ID for different content throws
133
- `RecordConflictError`. The write is one atomic claim, with canonical JSON
394
+ content returns `existing`; reusing the ID for different content anywhere in
395
+ its history throws `RecordConflictError`. A retraction makes the record
396
+ unavailable to reads and predecessor checks, but retains ownership of its ID.
397
+ Explicitly recording identical content restores it with a new claim; different
398
+ content requires a new ID. The write is one atomic claim, with canonical JSON
134
399
  encoded as a code value so export, backup, and sync preserve the complete
135
400
  model digest, backend/version, snapshot, inputs, evidence, limits, and outcome.
401
+ Result reads reuse recording validation: solver explanation schemas must match,
402
+ and solver model digests, backend names and versions must be non-empty strings.
403
+ External evaluations require evaluator identity, output and a matching input
404
+ digest. These checks also apply when a result is read as a predecessor for a
405
+ new recommendation or decision. Invalid imported results fail without appending
406
+ history; a corrected payload can be read again.
407
+ Governance record reads also enforce required payloads, author/action fields,
408
+ status choices and non-empty string predecessor IDs using the same validators
409
+ as recording. Optional recommendation rationale/author, decision rationale and
410
+ action message fields must be strings when present. Empty strings and omission
411
+ are accepted; null, numbers, booleans, arrays and objects are rejected without
412
+ appending. Imported reads enforce the same rule and recover after a corrected
413
+ payload is imported. These checks also reject a malformed immediate predecessor
414
+ before a decision, action or external-effect record can be appended. Correcting
415
+ that predecessor permits retry with the previously rejected child ID.
416
+ Reads validate the local
417
+ artifact; predecessor existence and cross-record relationships are checked when
418
+ recording a new artifact.
419
+ A rejected write with a missing predecessor appends nothing and does not reserve
420
+ the attempted artifact ID. Record the required predecessor, then retry the same
421
+ artifact ID; subsequent identical retries return the existing row.
422
+
423
+ Stored artifact payloads must use valid base64url encoding and decode as UTF-8
424
+ JSON. Reads accept unpadded output and correctly padded base64url, and reject
425
+ ignored characters, excess padding and nonzero unused padding bits. Reading malformed
426
+ bytes throws instead of silently replacing them and returning altered audit
427
+ data; reads leave stored history unchanged. Explicit replacement characters
428
+ and other valid Unicode remain readable.
429
+
430
+ Artifact JSON accepts finite numbers, strings, booleans, null, dense arrays,
431
+ and plain objects. Imported JSON numbers that overflow to infinity, such as
432
+ `1e400`, reject at any nesting depth without changing stored history.
433
+ Arrays are captured by length and numeric index; custom
434
+ iterators cannot replace their contents or hide holes. Object keys are sorted, and optional fields whose value is
435
+ `undefined` are omitted. Sparse or undefined array entries, non-JSON objects
436
+ such as dates/maps/sets, and cycles throw `TypeError` before any write. Reusing
437
+ the same non-cyclic object in multiple fields remains valid; each occurrence
438
+ is preserved as JSON data.
439
+ Recording validates and serializes containers with explicit stacks, as reads
440
+ already do, so deeply nested valid artifacts can be recorded and retried without
441
+ call-stack overflow. Serialization retains existing payload bytes, including
442
+ numeric-index property ordering. This removes an engine stack limitation; it
443
+ does not impose a general artifact memory or size budget.
444
+ An ID must remain exactly one CAVE entity name after its namespace prefix is
445
+ added. The recorder verifies the complete parsed claim, including the intended
446
+ subject and artifact payload, before insertion. Whitespace, comments, or extra
447
+ claim syntax that alter this identity are rejected without appending anything;
448
+ the same validation applies when reading predecessor references.
136
449
 
137
450
  `Record.recommendation`, `Record.decision`, `Record.action`, and
138
451
  `Record.externalEffect` use different versioned schemas and entity namespaces.
@@ -140,7 +453,135 @@ Each checks its predecessor before appending. These latter two are audit
140
453
  records only: they never invoke `@cavelang/act` or a hook. Governed action
141
454
  execution remains the sole authority for an external effect.
142
455
 
456
+ Recording entry points also enforce their artifact schema at runtime, before
457
+ opening a transaction. `Record.result` accepts solver results and external
458
+ evaluations; each other entry point accepts only its own versioned schema.
459
+ Passing an action to `Record.result`, for example, throws `TypeError` instead
460
+ of bypassing the action's decision check. Missing and unknown schemas are
461
+ rejected at this same boundary, including for JavaScript callers.
462
+
463
+ Recommendations require `value`, decisions require `selected`, and actions
464
+ require `parameters`; explicit JSON `null` is valid for each. Decision
465
+ `decidedBy`, action `name` and external-effect `kind` must be non-empty strings.
466
+ Action status must be `validated`, `executed` or `failed`; external-effect
467
+ status must be `succeeded`, `failed` or `unknown`. Missing required fields and
468
+ unsupported statuses throw `TypeError` without appending, while predecessor
469
+ checks still run under the recording transaction. Optional rationale, message
470
+ and effect details remain optional. These audit records never execute effects.
471
+
472
+ Solver result records require a supported explanation outcome: `satisfied`,
473
+ `optimal`, `unsatisfied`, or `unknown`. Optimal outcomes require literal
474
+ `optimalityProved: true`; unsatisfied outcomes require literal
475
+ `infeasibilityProved: true` and `coreMinimal: false`, even when no core was
476
+ returned. The portable explanation contract does not certify core minimality;
477
+ missing, true or coerced minimality markers are rejected. Recording, reading
478
+ imported artifacts, replay and
479
+ predecessor checks all enforce these markers before a result can support a
480
+ recommendation or decision. This checks the report contract; it does not
481
+ independently verify a backend's proof. Report inputs and diagnostics must be
482
+ arrays, as must feasible assignments and hard/soft constraints, optimal
483
+ objectives, and an unsatisfied core when supplied. Missing required collections
484
+ and non-array values reject at the same recording and read boundaries.
485
+ Unknown outcomes require a portable reason kind, string message and, when
486
+ present, a supported limit name. Recording and reading use the same
487
+ `Validate.unknownReason` check as solver-result handling, so replay cannot accept
488
+ a malformed unknown reason based only on matching model/backend identity.
489
+ Shared `Validate.resultMetadata` validation also requires finite non-negative
490
+ elapsed time and well-formed diagnostic entries, with supported severity and
491
+ string code/message fields. These checks apply before imported results can be
492
+ replayed or used as predecessors, as well as before initial recording.
493
+ Input and outcome entries must be objects with string IDs and the appropriate
494
+ string provenance arrays. Optional descriptions, queries and evaluation reasons
495
+ must be strings; declaration positions must be positive safe integers.
496
+ Assignments, objectives and core entries require Boolean `declared` flags.
497
+ Constraint evaluation states, objective directions and soft-weight field shapes
498
+ are checked before recording or replay. These structural checks do not resolve
499
+ referenced IDs or independently evaluate a solution. JSON-compatible malformed
500
+ raw backend assignment/objective values remain recordable for inspection and render with the
501
+ invalid-value marker.
502
+ Recorded run context also passes `Validate.explanationContext`, enforcing the
503
+ same snapshot policy, scenario identity and unique input-ID checks as generated
504
+ explanations. Limits must be an object with supported names and positive safe
505
+ integer values. Historical objects that omit newer limits retain exactly their
506
+ declared fields: validation does not insert current defaults into old evidence.
507
+ These checks apply to recording, imported reads, replay and predecessor use.
508
+ Indeterminate local constraint
509
+ evaluations remain recordable, including their optional `evaluationReason`.
510
+ This includes local `maxExplanationBits` exhaustion: replay retains the recorded
511
+ budget and indeterminate evaluations without running arithmetic again or
512
+ reclassifying the backend outcome.
513
+ A fresh solve with a larger explanation budget may evaluate constraints that
514
+ were previously indeterminate, without changing the model digest. Record that
515
+ new report under a new result ID; the earlier result remains immutable, including
516
+ after canonical export/import. Replay reads its recorded evaluation and does
517
+ not invoke the backend or apply the current caller's budget.
518
+
519
+ ### Replay compatibility
520
+
143
521
  `Record.replay` reads an immutable solver report without solving again and returns
144
522
  explicit incompatibility reasons for a different model digest, backend, or
145
- solver version. External evaluations retain their exact evaluator identity and
523
+ solver version. Each replay captures the expected model digest, backend name
524
+ and version once, so compatibility checks and their diagnostics use the same
525
+ values even when supplied through getters. A later replay captures fresh
526
+ expectations; neither call changes recorded history.
527
+ Expected model digests and supplied backend names/versions must be nonempty
528
+ strings. Malformed expectation containers or identities throw `TypeError` before
529
+ store reads; valid but different identities return incompatibility reasons.
530
+ Omit `backend` to compare only the model digest.
531
+ External evaluations retain their exact evaluator identity and
146
532
  input digest through `Record.read`; they are never mistaken for solver replay.
533
+
534
+ ## Performance measurements
535
+
536
+ Run these benchmarks alone from the repository root. They check exact results
537
+ outside the timed calls. Measurements describe the audit machine and the named
538
+ runtime versions; they are not test thresholds or general resource guarantees.
539
+
540
+ ### Scalar parsing
541
+
542
+ Run `node scripts/scenario-parse-normalization-bench.mjs` alone to measure
543
+ parsing at 1,000, 10,000 and 100,000 decimal places. Five-sample medians for
544
+ the 100,000-place fixture fell from 37.64 to 18.64 ms on Node 24.21.0 and
545
+ 37.23 to 18.44 ms on Node 26.8.1 after removing duplicate normalization.
546
+ These are local measurements, not a general input-size or latency guarantee.
547
+
548
+ ### Arithmetic and fraction reduction
549
+
550
+ Run `node scripts/scenario-exact-bench.mjs` from the repository root, alone,
551
+ to measure addition and comparison of near-one fractions at 1,000–64,000
552
+ decimal places. Three-sample medians at 64,000 places on the audit machine:
553
+
554
+ | Runtime | Addition before → after | Comparison before → after |
555
+ |---|---:|---:|
556
+ | Node 24.16.0 | 26.11 → 13.82 ms | 14.56 → 5.67 ms |
557
+ | Node 26.5.0 | 25.93 → 13.75 ms | 14.47 → 5.71 ms |
558
+
559
+ The same script also measures unequal-denominator comparisons at 10,000 and
560
+ 100,000 digits: opposite signs, equal numerators and a general cross-product
561
+ control. These additional cases use two warmups and eleven measured calls,
562
+ reporting the median with runtime/platform and measurement-scope metadata.
563
+ Input construction and exact result assertions are outside timing; BigInt
564
+ parsing inside comparison is included. Run the script alone on each runtime;
565
+ these observations are not a CI threshold or an intermediate-size budget.
566
+
567
+ The before/after table covers common denominators. Unit-conversion products,
568
+ unequal-denominator addition and general intermediate growth remain separate
569
+ resource boundaries; timings are observations, not test thresholds.
570
+
571
+ Fraction reduction shares the solver's `Exact.fromBigInts` normalizer, using
572
+ its guarded Lehmer batches directly on bigint intermediates. This avoids both
573
+ a second GCD implementation and an intermediate decimal serialization step.
574
+ `node scripts/scenario-reduction-bench.mjs` measures adding zero and converting
575
+ one source unit using consecutive Fibonacci fractions at indices
576
+ 5,000–100,000, with fixture construction outside timing and exact-result
577
+ checks afterward. Three-sample medians at index 100,000 (41,798 numerator plus
578
+ denominator digits), measured alone on the audit machine:
579
+
580
+ | Runtime | Add zero before → after | Conversion before → after |
581
+ |---|---:|---:|
582
+ | Node 24.16.0 | 1461.95 → 34.04 ms | 1492.44 → 70.80 ms |
583
+ | Node 26.5.0 | 1449.63 → 32.66 ms | 1483.08 → 66.02 ms |
584
+
585
+ The 64,000-place common-denominator addition benchmark still measured about
586
+ 14 ms on both runtimes. Conversion includes normalizing the declared factor
587
+ and its product; general product growth and normalization work remain unbounded.
@@ -1,5 +1,8 @@
1
1
  import type { Store } from '@cavelang/store';
2
2
  import { type Definition, type InputRecord } from './model.ts';
3
+ /** JSON semantics omit undefined optional fields before stable key ordering. */
4
+ export declare const definitionDigest: (definition: Definition) => string;
5
+ export declare const inputDigest: (record: Omit<InputRecord, "digest">) => string;
3
6
  export declare const bind: (store: Store, definition: Definition) => InputRecord;
4
7
  /** Materializes and rolls back inputs before `evaluate` is invoked. */
5
8
  export declare const run: <Result>(store: Store, definition: Definition, evaluate: (inputs: InputRecord) => Result | Promise<Result>) => Promise<Result>;
@@ -1 +1 @@
1
- {"version":3,"file":"bind.d.ts","sourceRoot":"","sources":["../../src/bind.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAA;AAG5C,OAAO,EAKL,KAAK,UAAU,EAGf,KAAK,WAAW,EAIjB,MAAM,YAAY,CAAA;AAoUnB,eAAO,MAAM,IAAI,GAAI,OAAO,KAAK,EAAE,YAAY,UAAU,KAAG,WAiD3D,CAAA;AAED,uEAAuE;AACvE,eAAO,MAAM,GAAG,GAAU,MAAM,EAC9B,OAAO,KAAK,EACZ,YAAY,UAAU,EACtB,UAAU,CAAC,MAAM,EAAE,WAAW,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,KAC1D,OAAO,CAAC,MAAM,CAAsC,CAAA"}
1
+ {"version":3,"file":"bind.d.ts","sourceRoot":"","sources":["../../src/bind.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAA;AAG5C,OAAO,EAKL,KAAK,UAAU,EAGf,KAAK,WAAW,EAIjB,MAAM,YAAY,CAAA;AAuCnB,gFAAgF;AAChF,eAAO,MAAM,gBAAgB,GAAI,YAAY,UAAU,KAAG,MACe,CAAA;AAEzE,eAAO,MAAM,WAAW,GAAI,QAAQ,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,KAAG,MACG,CAAA;AAqZrE,eAAO,MAAM,IAAI,GAAI,OAAO,KAAK,EAAE,YAAY,UAAU,KAAG,WAyE3D,CAAA;AAED,uEAAuE;AACvE,eAAO,MAAM,GAAG,GAAU,MAAM,EAC9B,OAAO,KAAK,EACZ,YAAY,UAAU,EACtB,UAAU,CAAC,MAAM,EAAE,WAAW,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,KAC1D,OAAO,CAAC,MAAM,CAAsC,CAAA"}