diff --git a/.github/workflows/formal-full.yaml b/.github/workflows/formal-full.yaml index 2c0d17ae..fab8a2d3 100644 --- a/.github/workflows/formal-full.yaml +++ b/.github/workflows/formal-full.yaml @@ -21,10 +21,11 @@ jobs: exploration: if: (github.event_name == 'schedule' && github.ref == 'refs/heads/main') || (github.event_name == 'workflow_dispatch' && inputs.exploration) runs-on: ubuntu-latest - # Run 34660598461 took 28 minutes. Hosted runners vary by about 2x between - # runs (run 34666226055 was 1.8x slower on every Quint phase), so every - # budget in this workflow leaves that headroom. - timeout-minutes: 60 + # Run 35568655690 exhausted the old hour inside the expanded challenge + # campaign, before generation or native replay. Allow the model job's + # budget below plus time for the fresh corpus and both ports; individual + # evaluator and replay commands retain their own hang bounds. + timeout-minutes: 180 steps: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 - uses: ./.github/actions/setup-validation @@ -50,8 +51,12 @@ jobs: # Scheduled workflows run the default branch; keep the intended main scope explicit. if: github.event_name != 'schedule' || github.ref == 'refs/heads/main' runs-on: ubuntu-latest - # Run 34660598461 spent about 8.5 minutes on this work; a 2x slower runner needs 17. - timeout-minutes: 30 + # Shared-library challenges now execute the importing profiles' partitions, + # including clean baselines and mutant checks cached by exact source closure. + # The expanded partition campaign was still running after an hour in + # run 35568655690. Leave room for the full campaign on slower runners + # without reducing models, histories or partitions. + timeout-minutes: 150 steps: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 - uses: ./.github/actions/setup-validation @@ -175,6 +180,7 @@ jobs: with: name: formal-traces path: .formal-traces + # The runner provisions a private Docker Redis server for generated invalidation vectors. - name: Measure one shard of TypeScript assertion-based fault detection run: make mutations-ts # The shard directory alone: the merge job downloads every shard into @@ -278,6 +284,7 @@ jobs: with: name: formal-traces path: .formal-traces + # The runner provisions a private Docker Redis server for generated invalidation vectors. - name: Measure one shard of Go assertion-based fault detection run: make mutations-go - name: Preserve this shard's Go mutation evidence diff --git a/.github/workflows/formal.yaml b/.github/workflows/formal.yaml index 6934b6d8..3636230e 100644 --- a/.github/workflows/formal.yaml +++ b/.github/workflows/formal.yaml @@ -49,12 +49,11 @@ jobs: # intended change. The reference is the merge base with the pull request's # base branch; a push to main has no reference. The step also checks the # composition lint baseline and the kernel fixtures. A kernel change replays - # every composed profile in both directions: six profiles took about 35 - # minutes on the hosted runner, five about 26, and fourteen in one job - # overran the 60-minute budget (57 minutes, then a cancellation at the - # timeout), so the lane runs as four matrix shards, each replaying a - # round-robin quarter of the profiles sorted by name (DIFFERENTIAL_SHARD); - # a change outside the kernel skips unchanged closures in under ten. The + # every composed profile in both directions. One job overran the 60-minute + # budget; alphabetical splitting later left one shard at 47 minutes and + # another at eight. Four shards now balance estimated profile replay time + # (formal/differential.mjs); every history still runs in both directions. + # Replay batches report progress through one bounded pool per job. The # gate is the four statuses `differential (1)` to `differential (4)`; none # is required on main. Grow the matrix, and the count in DIFFERENTIAL_SHARD # with it, before a shard approaches the budget. diff --git a/formal/AUTHORING.md b/formal/AUTHORING.md index 583ca643..ee86e9c6 100644 --- a/formal/AUTHORING.md +++ b/formal/AUTHORING.md @@ -116,6 +116,19 @@ change; the check runs in `make differential` (the pull request lane) and `make formal-check` (the full run), the lanes that have Quint. `make audit` runs without Quint and does not include it. +The same lint checks state shapes separately from composition counts. Held +reads beside held dumps require a lifecycle that accounts for both; shadow +jobs beside a caller source budget require the dark-job lifecycle. A local +fault switch must use the fault-aware transitions and cannot accompany the +healthy held lifecycle. These checks follow record type aliases and fail even +when refreshing the baseline; unsupported combinations are never recorded as +an allowed violation count. Each state is checked against the transitions +assigned to it, so a valid transition on another state cannot satisfy its +requirements, and mixing a valid lifecycle with an incompatible one still +fails. Atomic-release profiles also schedule +`atomicPathSeedsDecodableFrames`, because their reads and shadow comparisons +have no decode-failure settlement. + The [kernel library](./kernel/README.md) holds the concern modules a composed profile assigns through; `formal/dialcache-layers-conformance.qnt` is the first. A rewrite lands only when `node formal/differential.mjs ` replays the @@ -388,15 +401,20 @@ to the challenged model; when the fault sits in a shared library, the reproducer may instead name another profile `model` whose exported run reaches it, which is how a verification model's shared-rule challenge gets a portable reproducer. A `model-run` cites a run that only the model executes and must -carry a `scope` stating why the fault has no native counterpart: a vector model +carry a `scope` explaining its evidence boundary: a vector model whose cases reach the codecs through an exported artifact, or instrumentation such as a receipt that no driver observes. A run that is exported must be cited -as an `exported-regression`; `scope` is rejected on that kind. `profiles` must +as an `exported-regression`; `scope` is rejected on that kind. Public inputs +do not make every model expectation observable: a run may also check private +retention or memo state. Keep its `nativeMutants` classification `model-only` +or `unobservable` when those checks have no native consequence; exporting the +history alone does not establish a native fault mapping. `profiles` must include the challenged model's own profile id, or its path for a model without -a profile, and the cited run's profile. For a fault in a shared library every -known profile must appear in `profiles` or in `exclusions`; a fault in one -model's own file needs no exclusions, because no other profile executes that -text. +a profile, and the cited run's profile. For a fault in a shared library, every +profile that imports the changed source must appear in `profiles` or in +`exclusions`. Profiles outside that import closure are excluded structurally; +do not repeat those relationships as catalog prose. A fault in one model's own +file needs no exclusions, because no other profile executes that text. `check-model-properties.mjs` runs the cited history on the same copy of the sources as the invariant measurement, together with two probes it appends to @@ -411,16 +429,19 @@ fault does not distinguish, or one that fails elsewhere, is a measurement failure, not a survivor to record. The report entry gains `reproducer: { ..., baseline: 'passed', mutant: 'failed', code: 'QNT508' }`. -Existing challenges are backfilled as their models are touched. Until then each -one is listed by id in the manifest's top-level `reproducerBacklog`; -`node formal/execution.mjs` rejects a challenge that is neither listed nor -reproduced, a listed id that does not exist or already has a reproducer, and -reports the backlog size. The backlog is a reported gap, not a gate, and it -only shrinks: the ids that may appear in it are frozen in -`grandfatheredReproducerBacklog` in `formal/execution.mjs`, so a new challenge -cannot opt out by listing itself. Adding to that constant is a reviewed code -change; removing an id once its challenge has a reproducer is the normal path. -The native-mutant backlog below follows the same rule. +For a reproduced library fault, the checker also measures the profile +partition: a profile that does not import the changed source is `structural`, +derived from its imports without a catalog entry. Listing a non-importing +profile as a detector fails validation. Every listed profile must detect the +mutant through a declared run or a scheduled invariant, and every reaching exclusion must keep all its runs +passing (`holds`). A filtered `--only` run also checks the reaching exclusions' +scheduled invariants at the normal exploration bounds. The report records +these results in `partition`; a changed exclusion or a listed profile that +stops detecting the fault fails the check, so dated prose is not its evidence. + +Every challenge must carry a reproducer. The historical `reproducerBacklog` +and `nativeMutantBacklog` fields remain empty for report compatibility; +validation rejects a missing reproducer or any attempt to reopen either backlog. ### Mapping every challenge to native mutants @@ -485,9 +506,7 @@ of the three, `text` is non-empty, within its length ceiling and names the mutant or a port file, `mutant` is present exactly for `mapped`, `crossContract` exactly when the case lacks the contract; two challenges that repeat one `(source, before, after)` fault map it the same way; and every -challenge has a `nativeMutants` entry or is listed in the top-level -`nativeMutantBacklog`, never both. The backlog is frozen in -`grandfatheredNativeMutantBacklog` in `formal/execution.mjs` and only shrinks. +challenge has a `nativeMutants` entry. The summary also counts catalog mutants no challenge cites. Each port's own unit suite is informational for a mutant: when a fault @@ -508,6 +527,53 @@ mutant while authoring it, run `MUTATION_ONLY=M18 make mutations-ts` and complete evidence. Then run the full lanes, or let the weekly workflow run them. +A mapped challenge also records which assertion detects its native mutant. +For an exported-regression reproducer, the runner derives a boundary from its +history, the state index of its failure checkpoint (including initializer +aliases and literal repetitions), and the public observation fields in that +expectation. When the expectation uses a helper or needs another observation +projection, provide `nativeMutants.evidence: { history, step, fields }` for the +same reproducer; fields use the record actually compared by the profile. +The coordinator records every differing observation in a separate replay: +consequence fields count at the checkpoint, while cumulative counters count +only when their divergence first appears there relative to the previous step. +Each mapping reports `confirmed`, `side-effect-only`, `not-divergent`, +`unreached`, or `unreproduced`; incomplete or failed replays never earn +boundary credit. Both ports must replay every selected history cleanly and +confirm its boundary under the mutant; the mutation gate names a failure as +`/boundary:`, independently of cohort detections. A new +mapped challenge needs an exported reproducer whose checkpoint compares the +consequence, or written evidence selecting that consequence in the same run. +Keep the history executable through completion under the fault: end at the +decision when a later command would require an operation the fault removes. +The Go recorder also continues through typed semantic property assertions, +after validating the complete monitor input; those diagnostics alone earn no +boundary credit, and malformed driver or monitor records still terminate it. + +For a mapped vector model-run, supply `nativeMutants.evidence.vector` with +`artifact`, `group`, `rows` keyed by `typescript` and `go`, `fields`, and a +`relation` explaining how those exact generated inputs exercise the named +model regression. Use the same row in both bindings unless native codec sizes +require different inputs for the same semantic boundary. In that case, add +deterministic model checks for both inputs and explain their relationship. +The artifact must be owned by the scheduled vector model, and each named row +must exist exactly once. Expected fields are read from that artifact. +A verification model may cite another vector model through `reproducer.model` +only when both execute the challenged shared-library rule. Retain the original +verification invariant, name both models in the reproducer's coverage, and +measure the shared fault's coverage across the behavioral profiles as usual. + +The native vector workers receive only an operation and its external inputs. +They return actual keys, frame classifications, decoded bytes or compression +outcomes. Invalidation workers execute the production Lua against a private +Redis server and report its response, stored state and measured elapsed server +time. The coordinator bounds TTL drift only by that measurement. Transport, +malformed reply and unexpected Lua errors earn no detection credit; known API rejection is a typed result, while process or output errors +fail the run. The mutation gate requires a clean baseline from the same binding +and row, verifies artifact and input fingerprints, and recomputes the mismatch +from the recorded native value. A failure elsewhere in the vector cohort does +not satisfy this boundary. + ### Exported runs are exactly the public-only runs A profile run is public-only when every transition it takes records a command diff --git a/formal/BEHAVIOR.md b/formal/BEHAVIOR.md index d2af5c3d..71a3ed0d 100644 --- a/formal/BEHAVIOR.md +++ b/formal/BEHAVIOR.md @@ -39,6 +39,7 @@ Every scenario/trace gets a fresh default cache instance and empty Redis environ - `writeDispatch`: actual native adapter invocation index, recorded before its controlled write gate. - `readContext`: the adapter's actual read `index`, effective `timeoutMs`, and initial `aborted` state. `readAbort` records the index when cooperative cancellation is requested; the held raw read remains independently releasable. +- `coalescingState`: an explicit public inspection result, with `instance`, `activeLeaders`, `activeFollowers`, and `oldestLeaderAgeMs` (null when no leader is registered). Counts cover process flights only; request followers, independent calls, detached shadow jobs, and raw work abandoned after timeout are excluded. - `request`, `miss`, `disabled`, `error`, `coalesced`, `invalidation`: public bounded metadata. Operational events use `cacheNamespace`, `useCase`, `keyType`, and `layer` as applicable; miss/disabled events add `reason`, errors add `error`/`inFallback`, and coalescing uses `scope`. - `shadowAge`, `recoveryAge`, `futureOffset`, `get`, `fallback`, `serialization`: actual observations with `seconds`; serialization adds `operation`, and verdict ages add `outcome`. - `size`, `storedSize`: observed `bytes`. `compression` carries its actual `outcome`. @@ -70,6 +71,7 @@ Wall time starts at `2026-09-08T12:00:00Z`; monotonic time starts at zero. Elaps | `shiftWall` | Shift application wall time by signed `ms`, without advancing elapsed time or changing Redis physical retention. This is a clock observation, not a timer delivery. | | `seed` | Environment stores a frame for `key` and optional `useCase`: `value`, `ageMs` (default 0; negative is future), optional physical `ttlMs` (default 60,000). `frameHex` instead supplies exact raw bytes. This is external setup, not a DialCache write. | | `invalidate` | Call public targeted invalidation for `key`, with `futureBufferMs` default 0; await completion. Record its success, controlled mutation failure, or missing-remote-resource error. | +| `inspectCoalescing` | Call public coalescing inspection for `instance` (default `default`) and record its process snapshot. The fixture must select `coalescingState`. | | `observeMarker` | Observe the environment's tracked watermark for optional `key`, including remaining physical TTL. The fixture must select the `marker` observation. | | `policy` | Replace the runtime overlay with `value` (or `null` to inherit). Existing entries and already accepted invocation snapshots retain their contracts. | | `faults` | Update environmental flags: `read`, `write`, `dump`, `load`, `policy`, `observer`, `localStorage`; and gates `holdReads`, `holdWrites`, `holdDumps`, `holdLoads`, `holdPolicies`. Unmentioned flags retain their values. Flags start false. `localStorage` requires the dedicated local-failure fixture described below. | @@ -93,6 +95,8 @@ observations never select a command, effect index or source result. | `runtime-boundaries` | Exact serving cohort thresholds, leaf inheritance and validation, falsy/absent values, and feature toggles. Results and later layer reuse distinguish policy admission from mere policy input selection. | [Fixture and actions](./replay/profiles/runtime-boundaries.mjs) | | `shadow-layers` | Dark and served shadow work combined with request/local publication, independent caller sources, per-instance job capacity, captured fill policy and propagated source errors. Composed from the kernel library: the atomic layered release with held refills and stale-on-error on the atomic path, and the shadow job registry with its budgets. | [Fixture and actions](./replay/profiles/shadow-layers.mjs) | | `source-budgets` | Default, unbounded and finite source budgets; held policy resolution, outside calls, invalid keys, late followers and late results after retry. | [Fixture and actions](./replay/profiles/source-budgets.mjs) | +| `dark-layers` | Held dark shadow work with request/local reuse, independent source settlement, captured fill policy, per-instance capacity, source deadlines, tracked fences, wall rollback and public process-coalescing inspection. | [Fixture and actions](./replay/profiles/dark-layers.mjs) | +| `shadow-read-deadlines` | Separate dark C0/C1 read deadlines with captured policy, cancellation and raw capacity retained past bounded completion. | [Fixture and actions](./replay/profiles/shadow-read-deadlines.mjs) | | `local-clock` | Fractional native time with whole-millisecond local insertion and expiration, including instances constructed at different process times. Composed from the kernel library: the local projection with an inline source (`Serving::callLocal`) over the fractional clock (`Clock::advanceTicks`). | [Inputs and observations](./replay/local-clock.mjs) | This table summarizes scope. The linked modules define exact choices and command @@ -163,6 +167,7 @@ The effects model fixes remote-only policy to 60 seconds, a 10 ms source deadlin | `seedRemote` | Seed value 1 at the current application wall time | | `tick` / `jumpClock` | Advance 10 ms with / without timer delivery | | `rollbackWall` | Move application wall time back 1,000 ms, preserving monotonic time and Redis physical expiry | +| `advanceWall` | Version 3: move application wall time forward by explicit choice 1/59999/60000 ms; preserve monotonic time, deadlines and physical expiry | | `invalidate` / `futureFence` | Invalidate with a 0 / 20 ms future buffer | | `observerFault` | Choice 0/1 restores / fails diagnostic callbacks; cache outcomes remain unchanged | | `adapterReply` | Choice 1..16 supplies one next raw adapter reply; a second cannot be queued until the first is consumed | @@ -182,15 +187,16 @@ Initial choices 0..4 use tracked keys; choice 5 checks untracked reply/fence beh Each ITF state contains authoritative `input.name` and `input.choice`, plus expected state `s`. Choices are ITF integers: read/source settlement records an actual effect index; `observerFault` records 0/1; `init` records 0..5, -`readBudgetPolicy` records 0..4, and `adapterReply` records 1..16. Actions without +`readBudgetPolicy` records 0..4, `adapterReply` records 1..16, and +`advanceWall` records 1, 59999 or 60000 milliseconds. Actions without an external choice use `-1`. Optional MBT compatibility annotations must agree with the explicit input; raw regression exports need no annotations. See the [trace contract](./PORTING.md#trace-and-observation-contract). The parser rejects missing, unexpected, or unsafe choices. Failure actions set a fixture fault, release the selected external gate, drain runnable work, and restore the fault. `s.o`, `s.io` and `s.events` are the asserted channels; the rest of `s` is the model's private prediction and never enters execution or implementation projection. -After every step, replay compares actual caller outcomes, loader/read/write/invalidation/serializer/provider counts, physical write TTLs, actual raw-read context budgets/initial cancellation state, and ordered cancellation IDs. Model caller codes are 0 pending, 1 value 1, 2 original source error, 3 timeout; the composed text records the shared conformance codes in `s.o.calls` (3 source error, 4 deadline error) and the parser maps them to these, so the version-2 contract is unchanged. Asserting `io` and `d` beside `events` under the shared codes, as the feature profiles do, is a version-3 change left to the ports slice. Fixed scenarios additionally compare logical error identity. A read deadline starts a fresh source budget and suppresses refill; successful raw-read completion clears that budget before application-owned decoding. Failed fresh decoding permits refill. Accepted serialization/write outlives the source budget, with the observed fence checked again after preparation. +After every step, replay compares actual caller outcomes, loader/read/write/invalidation/serializer/provider counts, physical write TTLs, actual raw-read context budgets/initial cancellation state, and ordered cancellation IDs. Model caller codes are 0 pending, 1 value 1, 2 original source error, 3 timeout; the composed text records the shared conformance codes in `s.o.calls` (3 source error, 4 deadline error) and the parser maps them to these, so the observation schema introduced by version 2 is unchanged. Version 3 adds the explicit wall-advance domain and exported last-fresh/exact-expiry adapter-frame histories; the differential records this intended input-domain expansion. Asserting `io` and `d` beside `events` under the shared codes, as the feature profiles do, would be a separate observation-schema change. Fixed scenarios additionally compare logical error identity. A read deadline starts a fresh source budget and suppresses refill; successful raw-read completion clears that budget before application-owned decoding. Failed fresh decoding permits refill. Accepted serialization/write outlives the source budget, with the observed fence checked again after preparation. -Full validation exports 512 traces from 4,096 samples, up to 60 transitions, and requires all actions plus 93 witnesses covering abandoned source/read settlement, independent budgets, late settlement guards, application-owned phases, acquired snapshots across invalidation, failure-specific publication, clock rollback at the second fence check, and observer failure isolation. Scheduled deterministic regressions anchor their declared boundaries; [execution.json](./execution.json) distinguishes model checks from exported replays. Sampling favors the narrow rollback-during-publication boundary as well as unrestricted clock changes; it does not restrict that behavior to the favored schedule. +Full validation exports 512 traces from 4,096 samples, up to 60 transitions, and requires all actions plus 95 witnesses covering abandoned source/read settlement, independent budgets, late settlement guards, application-owned phases, acquired snapshots across invalidation, failure-specific publication, clock rollback at the second fence check, and observer failure isolation. Scheduled deterministic regressions anchor their declared boundaries; [execution.json](./execution.json) distinguishes model checks from exported replays. Sampling favors the narrow rollback-during-publication boundary as well as unrestricted clock changes; it does not restrict that behavior to the favored schedule. Full validation runs all profiles in `execution.json` through TypeScript and Go. PR checks run the committed subset alongside ordinary tests. Committed ITF smokes run without Quint. Failure diagnostics include trace, step, action, and both observations. The effects choice/state schema and scope/policy value choices changed with this specification revision; ports must select a matching revision and reject unsupported actions or choices. Behavioral scenario schema 2 is unchanged. @@ -198,11 +204,11 @@ The queued adapter choices are: 1 null, 2 primitive, 3 legacy watermark shape, 4 `s.events` records `{ event, location, detail, amount }`. Location identifies a reached layer or coalescing scope. Amount is an integer millisecond measurement (get, fallback, serialization, future offset), a byte size, a write-dispatch index, or zero. Replay converts model milliseconds to callback seconds and compares actual ordered events, their labels, and source-error attribution. A follower emits its coalescing event without repeating the leader's read/source trail. Fresh decoding is included in remote-get duration; source duration ends at accepted settlement/deadline; dump has its own duration. Late sources do not repeat failures. Size events precede actual adapter dispatch, including when that write remains pending. Scalar value 1 has one serialized byte here; Unicode/binary byte accounting remains covered by protocol vectors and fixed scenarios. -The profile requires 93 witnesses, listed in [coverage-witnesses.json](./coverage-witnesses.json): fixture and race boundaries, every reply class consumed by an active read, untracked fence demotion, normalized fences blocking publication, every selected event kind, error/miss categories, and nonzero held load/dump durations. Its 48 exported public regressions, the model's public-only runs, anchor normalization, phase timing, late-failure suppression, and the sampled boundaries that random generation reaches only rarely, so a fresh seed cannot lose them. Missing/corrupted diagnostic observations fail the harness. +The profile requires 95 witnesses, listed in [coverage-witnesses.json](./coverage-witnesses.json): fixture and race boundaries, every reply class consumed by an active read, untracked fence demotion, normalized fences blocking publication, every selected event kind, error/miss categories, and nonzero held load/dump durations. Its 51 exported public regressions, the model's public-only runs, anchor normalization, phase timing, late-failure suppression, and the sampled boundaries that random generation reaches only rarely, so a fresh seed cannot lose them. Missing/corrupted diagnostic observations fail the harness. ## Generated feature profiles -Seven additional models share the existing driver and a common [observation record](./conformance-observations.qnt). Their ITF states contain authoritative `input.name` and `input.choice`, with `s.o` as the expected observation. State outside `s.o` and the optional diagnostic record `s.d` is model-private prediction, not an implementation observation. The explicit choice is a nonnegative ITF integer on actions with choices below, or `-1` otherwise. Optional `mbt::actionTaken` and `mbt::nondetPicks.choice` annotations must agree with it. Reject unknown actions, unsupported choices, missing observation fields, and integer precision loss. +The generated feature models share the existing driver and a common [observation record](./conformance-observations.qnt). Their ITF states contain authoritative `input.name` and `input.choice`, with `s.o` as the expected observation. State outside `s.o` and the optional diagnostic record `s.d` is model-private prediction, not an implementation observation. The explicit choice is a nonnegative ITF integer on actions with choices below, or `-1` otherwise. Optional `mbt::actionTaken` and `mbt::nondetPicks.choice` annotations must agree with it. Reject unknown actions, unsupported choices, missing observation fields, and integer precision loss. Call observations encode pending as 0, fixture values 1/2 as 1/2, source errors as 3, and deadline errors as 4. Success codes 5..9 represent absent, null, false, zero, and empty string respectively; scope/policy generate these values. Other outcomes fail replay. These profiles compare error categories; the fixed scenarios compare logical error identity. All other observation fields use the scenario vocabulary directly, including zero/empty fields. A driver must use explicit action choices for selected source indices and its actual invocation counts for actions targeting the latest effect. It must never use expected counters, phases, cached values, or fences to select an input or fabricate an observation. @@ -215,6 +221,12 @@ Call observations encode pending as 0, fixture values 1/2 as 1/2, source errors | [Recovery](./dialcache-recovery-conformance.qnt) | Tracked remote with optional request memoization in two contexts, F=1 s, M initially 5 s, 10 ms source deadline, held decoding, up to eight callers | Fresh/stale/future frames; F/M boundaries; allow/deny/failing classifier; coalesced followers; source success versus rejection/deadline; default timeout-only classification and explicit overrides; abandoned source settlement; age checks around decode; invalidation/replacement; read/decode failures; captured recovery policy. Composed from the kernel library: the held remote traversal with its recovery snapshots under the budgeted lifecycle, the read settled at admission, and the diagnosed variants that record the coalesced scope, the failed layer and the recovery age | | [Policy](./dialcache-policy-conformance.qnt) | Untracked local+remote, both TTLs initially 1 s, M=5 s, local capacity one, two keys, one held provider reply, independently settled sources, no source deadline, up to twelve callers | Runtime coalescing on/off, per-source policy snapshots, shared versus independent same-key work, cross-key overlap and reverse settlement; independent runtime leaves; invalid local/remote TTL; provider/read/dump/write failure; policy acquisition and pending publication; local eviction/insertion TTL; logical Redis freshness versus physical retention | | [Shadow](./dialcache-shadow-conformance.qnt) | Tracked remote TTL=60 s, serving ramp=0, shadow ramp initially 100 (runtime 0/100/invalid), optional outcome hook, caller/job deadline=10 ms, all read/load/dump/write effects held, up to eight callers | Independent dark C0/source settlement; captured payload decode; match/mismatch/C1 supersession; confirmation failure; conditional fills; source/read/decode/dump/write failure; deadline during held effects; late work cannot change emitted outcomes. Composed from the kernel library: the dark arm of the shadow module's held lifecycle (the job's C0 read, decode and confirmation read held and released by ordinal) over the diagnosed budgeted held remote traversal, with the held fill and the buffered invalidation | +| [Dark layers](./dialcache-dark-layers-conformance.qnt) | Two instances, two keys, three persistent scopes and two transient contexts; twelve callers, one dark job per instance, 10 ms source/job budget, 60 s local TTL; held read/load/dump/write effects | Request/process followers, independent publication, source-error retry, captured fill retention, strict read/write fences, buffered invalidation, wall rollback, payload faults and failure-layer diagnostics | +| [Shadow read deadlines](./dialcache-shadow-read-deadlines-conformance.qnt) | Two keys, one instance, one shadow slot, eight callers; 10 ms source/job budget, 5 or 20 ms read budgets; held effects and a 100 ms elapsed-time bound | C0/C1 errors at their own read deadlines, raw ownership and readmission, independent job timeout, captured C1 budget and dispatch-relative timing | + +Dark-layers inputs use explicit effect ordinals and source indices. `beginCall` choices 0..9 encode key modulo two and context divided by two; `resolveLoader` choices 1..24 encode source index and value 1/2. Policy choices 0..8 select request/local layers, coalescing and dark-shadow admission, with remote serving always off. Seeds 0..11 select two keys and six frame classes; invalidation 0..3 selects a key and a 0/20 ms future buffer. `advance` accepts 1, 10 or 60000 ms, `rollbackWall` shifts wall time back 1000 ms, and each read/load/dump/write fault toggles 0/1. Version 2 adds `inspect` choices 0/1 for public process snapshots of either instance and `s.d.inspections`. The snapshot records instance, leader/follower counts, and oldest monotonic leader age; the model uses -1 for an idle age and the native projection uses null. Five exported histories require live counts, request/process follower separation, oldest-leader replacement, instance isolation, wall rollback independence, cleanup on success/error/timeout, and exclusion of unfinished shadow or abandoned raw work. Served shadow work and the caller's own held write beside its held read remain outside this profile. + +The dark-layers native replay settled one diagnostics rule against the TypeScript reference: an active local layer attributes its failed source to `local` even inside a request memo; without a shared layer, a transient request attributes it to `request_local`. Named public histories pin both cases in TypeScript and Go. Recovery opens contexts 0 and 1 in every fixture. New calls use open contexts; the separate scope profile covers detached calls after closure. Request misses may join the same process flight, after which each attached open context memoizes its result. A same-request follower records request coalescing; a new request records process coalescing. Recovered values have no shared-cache publication. Closing a context clears its memo and prevents late recovery from populating it. Four required witnesses include a recovered memo hit, later probes of both requests after shared recovery, a new read after closed-scope recovery, and request followers sharing recovery work. Three deterministic regressions anchor these cases. @@ -374,7 +386,7 @@ The scope profile reuses the same `openScope`, `closeScope`, `begin`, policy, an The model predicts memo lifetime and flight ownership. Replay observes actual caller values/errors, source enablement, and provider/source invocations; subsequent calls establish hits or misses. A late source may return to its original caller after closure, but cannot populate a replacement memo. Coalescing admission precedes reading a memo populated by another independently accepted source. -Generation exports 256 traces from 1,024 samples, up to 60 transitions, with at most sixteen calls. Sampling keeps closure reachable while favoring useful call progress; no fairness or exhaustive context-tree claim is made. Full validation requires every action plus 24 witnesses: disabled/detached bypass, policy reply after closure, rejection/retry, shared rejection, source settlement after closure, replacement miss after that settlement, independent scope overlap, uncoalesced same-scope overlap, memo hits, nested/reenabled hits, memo reuse after nested closure, memo reuse after policy bypass, memo hits for all five empty/falsy/absent values, and five failure-attribution/shared-trail witnesses. Eight deterministic model regressions anchor closure, replacement, nested/disabled reuse, rejection/retry, and error attribution. +Generation exports 256 traces from 1,024 samples, up to 60 transitions, with at most sixteen calls. Sampling keeps closure reachable while favoring useful call progress; no fairness or exhaustive context-tree claim is made. Full validation requires every action plus the cataloged witnesses, including disabled/detached bypass, policy reply after closure, rejection/retry, shared rejection, source settlement after closure, replacement miss after that settlement, independent scope overlap, uncoalesced same-scope overlap, memo hits, nested/reenabled hits, memo reuse after nested closure, memo reuse after policy bypass, memo hits for all five empty/falsy/absent values, and five failure-attribution/shared-trail witnesses. Deterministic model regressions anchor closure, replacement, nested/disabled reuse, rejection/retry, and error attribution. The committed `scope-smoke.itf.json` is a generated schedule including absent-value reuse and request closure. It runs without Quint in fast PR checks. Request deadlines, recovery, shared-process flights, arbitrary context trees, and more than two outer lifetimes remain covered separately or outside this generated profile. @@ -398,7 +410,7 @@ Only pending effects/sources may settle. There are at most sixteen calls and six A whole-job timeout emits one diagnostic outcome but keeps the slot until its pending source, decode, or confirmation read settles. Later completion cannot start another decode/read or emit a replacement verdict. Once the owned work drains, a subsequent hit may admit another job. This models resource ownership rather than cancellation or language executor turns. -Full validation exports 128 traces from 1,024 samples, up to 60 transitions, and requires every action plus eighteen witnesses: six terminal outcomes (match, mismatch, superseded, source_error, timeout, dropped), one admission for coalesced hits, uncoalesced hit overlap, accepted shadow policy after a change, unselected hits, duplicate drops while capacity is available, full-capacity drops, admission while the other instance is full, per-instance deduplication, capacity retained by each of the three timed-out phases, and readmission after expired work drains. Eight deterministic model regressions anchor coalesced admission, duplicate drops, per-instance capacity, all three timeout ownership phases, the shadow selection captured with the read across a policy change, and an uncoalesced caller's own read beside the registered leader. +Full validation exports 128 traces from 1,024 samples, up to 60 transitions, and requires every action plus the cataloged witnesses, including six terminal outcomes (match, mismatch, superseded, source_error, timeout, dropped), one admission for coalesced hits, uncoalesced hit overlap, accepted shadow policy after a change, unselected hits, duplicate drops while capacity is available, full-capacity drops, admission while the other instance is full, per-instance deduplication, capacity retained by each of the three timed-out phases, and readmission after expired work drains. Deterministic model regressions anchor coalesced admission, duplicate drops, per-instance capacity, all three timeout ownership phases, the shadow selection captured with the read across a policy change, and an uncoalesced caller's own read beside the registered leader. The committed `admission-smoke.itf.json` retains actions, choices, and observation records from the first 57 states of a generated trace. Model-private prediction fields are omitted to keep it compact. It contains a timed-out decode, a blocked duplicate, completion of the raw decode, and subsequent admission for that key. Full generated ITF states remain in the full workflow artifacts. The existing parser replays both forms without special handling or expected-state input to the driver. diff --git a/formal/CONTRACTS.md b/formal/CONTRACTS.md index aaaf6dc6..bdd0dbc0 100644 --- a/formal/CONTRACTS.md +++ b/formal/CONTRACTS.md @@ -49,7 +49,7 @@ Exporter compatibility and resource-exhaustion limits have explicit native integ | C08 | Local capacity is per instance, shared across operation identities, and least-recently-used | S: `local capacity evicts the least recently used key`, `shared local capacity spans operation identities`, `instances isolate local storage and registered flights`; G: policy (one-slot eviction), layers (two-slot LRU read promotion/eviction and per-instance capacity) | | C09 | Local TTL starts at insertion, reads do not renew it, and wall-clock rollback does not extend it | Policy model insertion history; S: `local exact TTL boundary expires`, `local expiry uses monotonic time across application wall rollback`, `nearly expired remote hit warms local for its full insertion TTL`; G: policy (separate wall/elapsed clocks, rollback reuse and expiry probes) | | C10 | Zero local capacity disables storage while preserving eligible in-flight sharing | S: `zero local capacity retains coalescing but no settled value`; G: layers | -| C11 | Same-key eligible calls share one registered execution; distinct keys/instances remain isolated | Effects profile (one registered source); generated effects; S: `different keys own independent flights`, `instances isolate local storage and registered flights`; G: recovery, policy (cross-key overlap) | +| C11 | Same-key eligible calls share one registered execution; distinct keys/instances remain isolated; public process snapshots report live leaders/followers and oldest monotonic age | Effects profile (one registered source); generated effects; S: `different keys own independent flights`, `instances isolate local storage and registered flights`; G: recovery, policy (cross-key overlap), dark-layers (public inspection, process-only counts, clock/instance isolation and settlement cleanup) | | C12 | Request misses can join a process flight and memoize its result separately | S: `request misses join one process flight then memoize separately`; G: layers | | C13 | Coalescing disabled means independent executions, not disabled settled caching; publication is last-writer-wins | S: `coalescing off keeps settled caching`, `independent local publication is last writer wins`, `independent remote publication is last writer wins`, `uncoalesced request calls still memoize the last settled value`; G: policy (independent sources, reverse settlement, coalescing changes); G: scope | | C14 | With no active serving layer, calls do not coalesce | Runtime-boundaries profile (inherited-disabled sharing never joins); S: `inactive serving layers do not coalesce` | @@ -141,7 +141,7 @@ Exporter compatibility and resource-exhaustion limits have explicit native integ | B01 | Public language binding | API names/aliases, wrapper registration, exact construction/validation timing, generics/type guards, reserved/removed option diagnostics, native error classes, exported helper shapes, and package entry points remain TypeScript tests. | | B02 | Host execution mechanics | Promise identity, exceptions/thenables and rejection consumption, Node timer handles, synchronous event-loop blocking, and native buffer views remain binding tests. Their deadline, failure-isolation, and retained-snapshot consequences are represented above. | | B03 | Native value conventions | Shared object references, constructors/prototypes, JavaScript JSON coercion/lossiness, and default Node deep equality are not universal port requirements. Each binding documents its supported value domain and codec; W01–W09 apply wherever it shares Redis. | -| X01 | Optional observability integration | Backend metric names, histograms, exporter registry compatibility, coalescing-state inspection, bounded JSON logging, and telemetry timing precision are covered by implementation/integration tests. Current portable claims include C57–C60 diagnostic semantics and failure isolation; exact exporter schemas, buckets, registry compatibility, timing precision, and logging resource limits remain separate. | +| X01 | Optional observability integration | Backend metric names, histograms, exporter registry compatibility, coalescing-state inspection, bounded JSON logging, and telemetry timing precision are covered by implementation/integration tests. Current portable claims include C57–C60 diagnostic semantics and failure isolation plus C11 process-coalescing snapshot semantics; exact exporter schemas, buckets, registry compatibility, timing precision, and logging resource limits remain separate. | | X02 | Resource/algorithm implementation | Compression level tuning, native zstd availability/decoder quirks, huge payloads/512 MiB guard, decompression bombs, local allocation strategy, CPU/memory/throughput, connection lifecycle, retry APIs, and command queue limits remain implementation/integration tests. Envelope interoperability remains W06–W08. | ## Source index diff --git a/formal/FEATURE-COVERAGE.md b/formal/FEATURE-COVERAGE.md index b62bb7d4..9c48e122 100644 --- a/formal/FEATURE-COVERAGE.md +++ b/formal/FEATURE-COVERAGE.md @@ -15,7 +15,7 @@ Use [CONTRACTS.md](./CONTRACTS.md) for the obligations and ## What is accounted for -The current catalogs give all **240 behavioral cases** a checked Quint +The current catalogs give every cataloged behavioral case a checked Quint reference and Quint-driven implementation evidence. Wire obligations have separate generated primitive artifacts and complementary fixed vectors; native cases remain separate API, value, clock, exporter and adapter obligations. @@ -157,6 +157,14 @@ Shadow-layers covers mixed served/dark capacity, request/local publication and captured fill policy. Scheduled public-action regressions anchor these corners without relying on random selection. +Dark-layers also requires competing-key rejection after the whole-job timeout +while C0 read, decode, C1 read, dump or write remains unfinished, then admission +only after that raw effect settles. Shadow-read-deadlines separately exercises +C0/C1 read expiry, cancellation and raw-capacity retention, including both +read/job deadline orderings. Confirmation keeps its admitted read budget after +a runtime change and starts that budget at its own dispatch. These histories +replay in both ports; a whole-job timeout alone does not establish a read deadline. + Those additions do not enumerate arbitrary request trees, operation/key/instance sets, capacities or simultaneous failures. The short shadow job budget limits physical-expiry schedules; wall-clock-only confirmation changes are explicit diff --git a/formal/PORTING.md b/formal/PORTING.md index 3c47e791..f3b2bce3 100644 --- a/formal/PORTING.md +++ b/formal/PORTING.md @@ -78,6 +78,13 @@ Invocation counters and IDs also come from actual native observations. The coordinator retains expected state; its command responses do not contain predictions. Duplicate or skipped observation indices are errors. +The `inspectCoalescing` command calls the port's public inspection API and records +`coalescingState` in the selected event channel. Its counts and oldest age come +from the implementation. TypeScript exposes a nullable millisecond age; Go +exposes a duration and projects idle age to null only when the observed leader +count is zero. The dark-layers profile compares whole-millisecond snapshots; +native fractional precision remains a separate binding check. + The shared command definitions and per-profile bindings are in [replay/bindings.mjs](./replay/bindings.mjs). Their normalization, dynamic input selection and assertions are maintained once. The Go transport adapter is @@ -199,9 +206,9 @@ infrastructure error, never an observation mismatch: it carries no | Caller | Definition | Encoding | | --- | --- | --- | -| Feature profiles (12) | `behaviorObservation` | The full behavior record below; `events` is present exactly when the fixture has an `observe` list | +| Feature profiles | `behaviorObservation` | The full behavior record below; `events` is present exactly when the fixture has an `observe` list | | `effects` | `behaviorObservation` | The same record; the fixture observes every metric event kind plus `readContext`/`readAbort` | -| `core` | `coreObservation` | Nine nonnegative integers: `sourceVersion`, `lastResult`, `outsideLoaderCalls`, `requestLoaderCalls`, `localLoaderCalls`, `coalescedLoaderCalls`, `remoteLoaderCalls`, `redisReads`, `redisWrites` | +| `core` | `coreObservation` | `sourceVersion` and the seven loader/Redis counters are nonnegative integers. `lastResult` is the actual returned integer, or `{ "absent": true }` for an absent return; it must remain present even when the implementation returns no value. Core histories offer integers, so absence is a behavioral mismatch. | | `local-clock` | `localClockObservation` | The behavior record without `events`; `calls` holds the plain integer each call returned, `loaders` counts source invocations, every other counter is zero and every other list empty | Behavior observation fields: diff --git a/formal/README.md b/formal/README.md index 743659e5..cd32f98b 100644 --- a/formal/README.md +++ b/formal/README.md @@ -81,16 +81,20 @@ Conformance profiles expose external commands that both language drivers replay: | [shadow-layers](./dialcache-shadow-layers-conformance.qnt) | Dark fills and local/request reuse; independent sources and mixed served/dark capacity | | [local-clock](./dialcache-local-clock-conformance.qnt) | Fractional environment time and the shared whole-millisecond process-local expiry grid | | [source-budgets](./dialcache-source-budgets-conformance.qnt) | Default/unbounded/finite source deadlines, held policy, followers, outside calls and key failures | +| [dark-layers](./dialcache-dark-layers-conformance.qnt) | Held dark work across request/local reuse, source deadlines, instance isolation, captured fill policy, tracked fences and clock rollback | +| [shadow-read-deadlines](./dialcache-shadow-read-deadlines-conformance.qnt) | Separate C0/C1 read deadlines, raw capacity ownership, cancellation, captured read policy and whole-job ordering | These profiles deliberately bound callers, keys, contexts, capacities, payloads and time. Their introduction does not imply that every product of those domains is explored. [profiles.json](./profiles.json) records profile versions, input encodings, smoke traces and implementation declarations. -The [kernel library](./kernel/README.md) states each portable rule once as pure -transitions; a composed profile assigns state only through them. The layers, -runtime-boundaries, scope, source-budgets and policy profiles are composed today, each -verified by the corpus differential; the other profiles remain authoritative and their totals are unchanged. +The [kernel library](./kernel/README.md) states shared portable rules as pure +transitions; a composed profile assigns state only through them. Every profile +except core composes this library. The corpus differential checks that changes +preserve existing profiles' observable behavior; a new profile establishes its +behavior through independent properties, consequential witnesses and replay in +both implementations. ## Generating and replaying behavior @@ -105,7 +109,9 @@ The archive is cached under `~/.cache/dialcache/apalache/0.56.1/`; for offline use, supply `APALACHE_ARCHIVE=/absolute/path/to/apalache-0.56.1.tgz`. Supplied archives must pass the same checksum check. -Real-server tests require Docker. The package floor requires exact Node 22.15.0 +Real-server tests and native mutation campaigns require Docker. Each mutation +shard starts a private Redis 6.2 server, replays all generated invalidation +vectors against the production Lua, and removes its own container afterward. The package floor requires exact Node 22.15.0 provided through `NODE22_BIN`. `make help` lists targets and prerequisites. `make model-check` and `make ci` additionally require Java 21 and `tar`, because the pinned Apalache distribution is unpacked from a checksummed tarball; both @@ -128,7 +134,12 @@ make ci NODE22_BIN=/absolute/path/to/node22/bin/node `make formal-check` is the Quint evidence lane: it typechecks and runs every scheduled model with the Rust evaluator, the public regressions and the model -mutation challenges. `make formal-generate` runs generation, fixture +mutation challenges. Its first command, `node formal/run-models.mjs check`, +runs only the unmodified model checks and regressions; the next step runs the +complete pinned fault campaign. `make formal` and `make ci` require both steps. +`make explore` retains the model checks, generation and both port replays but +omits that identical pinned campaign; its result remains non-acceptance evidence. +`make formal-generate` runs generation, fixture recomputation and the shared witness evaluation; `make formal-ts` and `make formal-go` then complete each port's replay against that exact corpus. `make mutations-ts` and `make mutations-go` split the fault campaigns. The @@ -179,6 +190,22 @@ Core/effects use `DIALCACHE_MBT_TRACE_FILE` or `DIALCACHE_EFFECTS_TRACE_FILE` and their corresponding tests. Local-clock uses feature selectors with `test/formal-local-clock.test.ts` and the Go local-clock replay. +## Shared verification and replay rules + +The verification models and portable profiles share transition judgments for +local expiry, coalescing defaults, remote retention, tracked publication, +recovery acquisition and watermark reads. Their primary invariants remain +independently stated. Each mapped fault must violate its original property, +fail the declared expectation in a deterministic Quint run, and reach the same +public assertion in completed TypeScript and Go recordings. + +The fractional local-clock profile retains the raw insertion instant beside +its expiry. This lets its portable history distinguish a precise-clock TTL +from the required whole-millisecond TTL while also detecting a hit that renews +expiry. Protocol classifiers use the same fence judgment as generated byte +vectors and behavioral remote reads; vector recordings select the exact +reviewed row for each language binding. + ## Evidence and scope [execution.json](./execution.json) schedules model properties, exports and diff --git a/formal/SEMANTIC-COVERAGE.md b/formal/SEMANTIC-COVERAGE.md index 25d5e86b..8286c4b5 100644 --- a/formal/SEMANTIC-COVERAGE.md +++ b/formal/SEMANTIC-COVERAGE.md @@ -5,7 +5,7 @@ Measure named contract cases, exercised boundaries, and detected behavioral defe ## Evidence inventory [semantic-cases.json](./semantic-cases.json) refines the obligations in -[CONTRACTS.md](./CONTRACTS.md) into named behavioral and wire cases. All 240 +[CONTRACTS.md](./CONTRACTS.md) into named behavioral and wire cases. All reviewed behavioral cases now cite checked Quint clauses and Quint-driven implementation evidence. The wire expansion adds Quint-derived artifacts alongside fixed vectors; native cases remain in [feature-coverage.json](./feature-coverage.json). @@ -99,21 +99,24 @@ model owns at least one challenge; a `challengeWaiver` on a model entry is a documented gap, not coverage. A detected challenge shows that the named invariant rejects that one deliberate change under the manifest bounds. Model receipts preserve the timestamp, captured policy and owner at acceptance. -A challenge with a `reproducer` is also pinned to one deterministic history +Every challenge is pinned by its `reproducer` to one deterministic history that passes clean and fails under the fault at a declared expectation, either an exported public regression both ports replay or a documented model-only -run; challenges not yet backfilled are listed in the manifest's -`reproducerBacklog`, whose size `node formal/execution.mjs` reports beside the -challenge counts. See the +run. Missing reproducers fail validation. See the [authoring rules](./AUTHORING.md#challenging-every-model). Each challenge also carries a `nativeMutants` entry naming the TypeScript and Go mutants that inject the same wrong behavior into the ports, or an enumerated explanation of why no native line exists; the mutation lanes must detect every mapped mutant in their generated cohort. A mapped challenge establishes that the corpus would catch the mistake in a port; an explained -one establishes only that the model rejects it. Unmapped challenges are listed -in `nativeMutantBacklog`, which only shrinks. See +one establishes only that the model rejects it. A missing mapping or +explanation fails validation. See [mapping every challenge to native mutants](./AUTHORING.md#mapping-every-challenge-to-native-mutants). +Ownership receipts, failed-read state, closed-scope memo cleanup and initial +recovery retention have exact model checkpoints. The closed-scope and recovery +histories also use public inputs replayed by both ports, but their additional +private-state assertions remain model evidence; they do not create native +mutation coverage where later native checks hide the same internal fault. Boundary properties can challenge an eligibility helper by stating the inequality directly. Connection and composition properties may reuse that helper while checking independently captured inputs, ownership and history; @@ -193,6 +196,24 @@ The runner clears inherited trace selectors, uses the complete generated directo Each catalog entry's `requiredDetections` is a regression gate. The local mutation target and full workflow fail if a required fault survives. Newly detected faults remain visible as improvements; update the required set after inspecting the result. Source edits must match exactly once, so implementation drift requires reviewing the mutation rather than silently skipping it. +The report separately records each mapped challenge's boundary evidence: its +exported history, checkpoint, compared fields, and every divergence seen while +replaying that history through the coordinator. An earlier unrelated counter +mismatch cannot establish the later behavioral consequence. A `confirmed` +boundary requires a complete mutant replay and a clean replay of the same +history; driver failures, settlement violations and missing recordings remain +`unreached`. Every mapped exported reproducer must be confirmed by the mutation +gate in both ports; an entire cohort detecting the mutant cannot replace that +evidence. A mapped vector reproducer has the same requirement at its selected +generated row: record the actual native result after a clean run of that row, +then confirm a mismatch in the declared public fields. The coordinator alone +reads the expectations; native workers receive only the operation and inputs. +Reports retain row identity, binding, artifact and input fingerprints, and the +actual result so the gate can recompute the comparison. Different native codec +sizes may require different declared rows for the same strict-shrink rule. +Missing, malformed or stale results earn no credit. `unreproduced` mappings +remain explicit gaps beside these measured boundaries and cohort detections. + To expand assurance, add a test/doc-derived case and precise executable evidence, require a generated witness where appropriate, then add a representative fault for a previously unchallenged rule. Preserve gaps until execution closes them. Keep code coverage, source accounting, case evidence, and mutation detection as separate measurements. The current Go suite requires every shared profile, exported regression, fixed scenario and protocol case registered by the manifests, with the Go section of every entry in `mutations.json`. A mutant whose fault leaves a goroutine blocked or a pointer nil makes the Go port's own synctest suite panic instead of failing an assertion, and one that settles a promise the TypeScript suite was not awaiting can leave no failed assertion behind; each runner records such an ordinary cohort as `crashed`, outside the detected and survived totals, as it does every cohort of a mutant that does not compile, and the generated cohort remains the required detection. Broader interaction histories and larger domains remain separate assurance work. ## Model properties and cross-language execution @@ -208,3 +229,12 @@ implementation mutation detection. Every effects replay also runs `assertEffectsHistory` over actual external source starts/settlements and public fallback/write observations. Its C23/C25/C26 checks cover source-relative budget/duration, strict deadline acceptance, and a preceding accepted success before publication. It consumes no expected model phases and permits pending prefixes. An additional causal monitor in both drivers ties writes to their actual invocation/source callback and rejects publication after that source settled too late; negative tests distinguish property failures from malformed monitor inputs. This is a bounded connection for selected properties, not full model refinement or liveness proof. The Go driver executes the same generated corpus through its own cache implementation. Its value-domain and API adaptations appear in [`go/README.md`](../go/README.md). `measure-go-semantics.mjs` independently measures the Go fault catalog; TypeScript mutation results are never credited to Go. The Go race detector covers only exercised schedules. + + +Generated invalidation vectors also run inside both standard native mutation +campaigns. Every shard provisions its own real Redis 6.2 server through Docker; +the whole generated set shares a native connection. The exact cutoff and maximum +buffer challenges additionally record their selected input-only vector operation. +Their outcomes come from each port's production Lua. Server elapsed time bounds +positive TTL drift; rejected input must preserve the prior state. Infrastructure +errors and unexpected Lua replies are never assertion-based detections. diff --git a/formal/TEST-MAP.md b/formal/TEST-MAP.md index abe4478b..d6b803e2 100644 --- a/formal/TEST-MAP.md +++ b/formal/TEST-MAP.md @@ -10,7 +10,7 @@ mean that every assertion in it is modeled. [semantic-cases.json](./semantic-cases.json) maps cases to histories and vectors and states, on each Quint citation, the clause it checks; [feature-coverage.json](./feature-coverage.json) keeps native adaptations separate. -All 240 reviewed behavioral cases now have checked Quint references and +All reviewed behavioral cases now have checked Quint references and Quint-driven implementation evidence. This accounts for known cases, not every input or feature combination. [VALIDATION.md](./VALIDATION.md) describes the validation workflow and report requirements. diff --git a/formal/VALIDATION.md b/formal/VALIDATION.md index 578fbb01..84ada5d7 100644 --- a/formal/VALIDATION.md +++ b/formal/VALIDATION.md @@ -35,11 +35,15 @@ driver-asserted observation of its previous corpus, or accepts an input the previous text refused, fails unless the manifest declares the change by bumping its `differential.behaviorVersion`. The job is a matrix of four shards, `differential (1)` to `differential (4)`: each checks the lint baseline and the -kernel fixtures, then replays a round-robin quarter of the composed profiles -sorted by name (`DIFFERENTIAL_SHARD=/4`; one unsharded job overran its -60-minute budget) and preserves its reports and replay logs as the -`formal-differential-` artifact. The reports are migration evidence, not -a conformance completion report; the profile lanes still run. +kernel fixtures, then replays its assigned composed profiles +(`DIFFERENTIAL_SHARD=/4`). Profiles are assigned by estimated replay +time, largest first into the least-loaded shard, with deterministic ties. +The advisory weights in `differential.mjs` include estimates for new profiles; +they change placement only. Every profile still runs both directions through +one bounded worker pool per job, with completed/total batches and elapsed time +logged after each batch. Each shard preserves reports and replay logs as the +`formal-differential-` artifact. These are migration evidence, not a +conformance completion report; the profile lanes still run. The evaluator ends with a per-profile witness report: required labels with at most three sampled hits and no regression, labels pinned by a regression but @@ -89,6 +93,37 @@ and environmental assumptions are defined once in ## Mutation evidence +Each native mutation report includes a `boundary` entry for every mapped +challenge. A separate coordinator replay continues after observation mismatches +and records the differing fields at every step; it preserves the normal driver +and settlement checks. `confirmed` means the intended checkpoint differs on a +consequential field or a newly differing counter; `side-effect-only` means other +observations differ, and `not-divergent` means the history still agrees. +`unreached` records an incomplete history, missing recording, or driver failure. +Exported-vector model runs carry `origin: vector`; both ports execute the named +native API against the exact vector samples and must earn `confirmed` on the +declared fields. Clean boundary baselines must complete without divergences. +The mutation gate requires `confirmed` for every mapping with an exported +history or vector reproducer in both ports, alongside the required-cohort gate. +It recomputes verdicts from current declarations and recordings: missing +mapping entries, stale checkpoints and absent clean baselines fail, even when +the report claims confirmation. Historical `unreproduced` states remain +readable, but both current backlogs are empty and the execution audit forbids +reopening them. A mapped fault cannot replace its portable evidence with a +model run that exports no vector. + +Read the evidence with `node formal/mutation-reports.mjs boundary --report +`. Raw native assertion reports remain available for diagnosis; +without a completed boundary recording, inspection reports `unreached` and +does not reconstruct evidence from assertion text. Ungated inspection can read +historical reports; it does not validate the current checkout. `--gate` +additionally requires a complete report whose catalog, measured source inputs, recorded configuration +and exact corpus fingerprints match the checkout. Keep the measured corpus +artifact when checking a downloaded report; regenerating it may change its +bytes. Missing fingerprints fail the gated command, as does any exported +history or vector boundary that is not `confirmed`. Historical `vector` gap +states remain readable but do not satisfy current vector declarations. + Model mutations challenge the specification's independent properties. Implementation mutations challenge the assertions that connect generated histories to real TS/Go behavior. Report these measurements separately, including @@ -107,18 +142,17 @@ of all possible defects from their scores. The model catalog in `execution.json` covers every scheduled model with no waivers; `node formal/execution.mjs` reports the challenge and distinct fault counts. Its report distinguishes those two counts and marks a filtered `--only` -run as partial; only the complete run is evidence. A challenge with a deterministic reproducer is additionally +run as partial; only the complete run is evidence. Every challenge's deterministic reproducer is additionally replayed on the clean and mutated model and must fail only under the fault, at the expectation the manifest declares; the report records that outcome per -challenge, and `node formal/execution.mjs` reports how many challenges still -wait in `reproducerBacklog`. +challenge. Missing reproducers fail validation. Each challenge also maps to the native mutant that injects the same wrong behavior into both ports through its `nativeMutants` entry, or explains why no native line exists; `node formal/execution.mjs` checks the mapping against the mutant catalog (`formal/mutations.json`, one entry per fault with a TypeScript and a Go section), anchors every catalog edit in the port text, and -reports the challenges still waiting in `nativeMutantBacklog`. The mutation lanes must detect every mapped mutant in +rejects a missing mapping or explanation. The mutation lanes must detect every mapped mutant in their generated cohort, so a mapped challenge is evidence that the corpus would catch that mistake in a port, not only that the model would. See the [authoring rules](./AUTHORING.md#mapping-every-challenge-to-native-mutants). @@ -171,8 +205,11 @@ is never reported as a failure. ## Exploratory runs `make explore` selects and records a fresh seed, copies current tracked and new -source files, and runs Rust model checks and both native replays in that isolated -snapshot. It does not run the separate symbolic lane or require Java. +source files, and runs all unmodified Rust model checks and regressions, +generation, witness checks and both native replays in that isolated snapshot. +It omits the identical pinned model-fault campaign, which remains mandatory in +`make formal-check`, `make formal` and `make ci`. It does not run the separate +symbolic lane or require Java. It preserves the pinned acceptance corpus and reports in the original checkout. The weekly full workflow runs this lane alongside pinned validation. Exploration does not produce an acceptance completion: its separate report distinguishes diff --git a/formal/boundary-replay.d.mts b/formal/boundary-replay.d.mts new file mode 100644 index 00000000..40696808 --- /dev/null +++ b/formal/boundary-replay.d.mts @@ -0,0 +1,15 @@ +import type { ReplayRecording } from './replay/coordinator.mjs'; + +export interface BoundaryRecording extends ReplayRecording { history: string; via: 'coordinator' } +export interface EvidenceHistory { history?: string } +export function boundaryTrace(history: string, traces?: string): { profile: string; path: string }; +export function goBoundarySelection(profile: string, path: string): { test: string; env: Record }; +export function readBoundaryRecording(file: string, history: string, path: string, + result: { status: number | null; signal?: string | null; error?: Error; stdout?: string }): BoundaryRecording; +export function runBoundaryReplay(options: { + port: 'typescript' | 'go'; history: string; label: string; output: string; root: string; + workspace: string; env: NodeJS.ProcessEnv; go?: string; +}): BoundaryRecording; +export function boundaryBaselines(evidence: EvidenceHistory[], replay: (history: string) => BoundaryRecording): Record; +export function mutationBoundaries(evidence: T[], replay: (history: string) => BoundaryRecording, + assess: (entry: T, recording?: BoundaryRecording) => R): R[]; diff --git a/formal/boundary-replay.mjs b/formal/boundary-replay.mjs new file mode 100644 index 00000000..9e2989f4 --- /dev/null +++ b/formal/boundary-replay.mjs @@ -0,0 +1,134 @@ +import { spawnSync } from 'node:child_process'; +import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { isDeepStrictEqual } from 'node:util'; +import { boundaryEvidence } from './execution.mjs'; +import { runVectorBoundary } from './run-vector-boundary.mjs'; + +// A boundary is always an exported regression. Keep the original profile +// directory: Go's single-file selector uses it to choose the driver. +export function boundaryTrace(history, traces = resolve('.formal-traces')) { + const match = /^([a-z][a-z0-9-]*)\/([A-Za-z_]\w*)$/.exec(history); + if (!match) throw new Error(`Invalid boundary history: ${history}`); + return { profile: match[1], path: resolve(traces, 'regressions', `${history}.itf.json`) }; +} + +export function goBoundarySelection(profile, path) { + const selections = { + core: ['TestCoreConformance', 'DIALCACHE_MBT_TRACE_FILE'], + effects: ['TestEffectsConformance', 'DIALCACHE_EFFECTS_TRACE_FILE'], + 'local-clock': ['TestLocalClockConformance', 'DIALCACHE_FEATURE_TRACE_FILE'], + }; + const [test, selector] = selections[profile] ?? ['TestFeatureConformance', 'DIALCACHE_FEATURE_TRACE_FILE']; + return { test, env: { + // Core refuses file+directory selection; none of these targeted runs may + // accidentally replay a cohort or require all of its action witnesses. + DIALCACHE_MBT_TRACE_DIR: '', DIALCACHE_EFFECTS_TRACE_DIR: '', DIALCACHE_FEATURE_TRACE_DIR: '', + DIALCACHE_MBT_TRACE_FILE: '', DIALCACHE_EFFECTS_TRACE_FILE: '', DIALCACHE_FEATURE_TRACE_FILE: '', + DIALCACHE_FEATURE_PROFILE: '', [selector]: path, + } }; +} + +function validRecording(record, path) { + return record !== null && typeof record === 'object' && record.path === path + && typeof record.completed === 'boolean' && Number.isSafeInteger(record.lastStep) && record.lastStep >= -1 + && (!record.completed || record.lastStep >= 1) + && (record.error === undefined || typeof record.error === 'string') + && Array.isArray(record.divergences) && record.divergences.every((item, index) => item !== null && typeof item === 'object' + && Number.isSafeInteger(item.step) && item.step >= 0 && item.step <= record.lastStep + && (index === 0 || item.step > record.divergences[index - 1].step) + && typeof item.action === 'string' && Array.isArray(item.paths) && item.paths.length > 0 + && item.paths.every(path => typeof path === 'string' && path.length > 0)); +} + +// A recorded divergence is evidence only if the whole replay completed and +// its native process succeeded. Preserve partial observations and both errors +// for diagnosis, but never turn a missing recording or a failing cleanup into +// a confirmation merely because an earlier checkpoint diverged. +export function readBoundaryRecording(file, history, path, result) { + const errors = []; + let recording; + try { + const records = readFileSync(file, 'utf8').trim().split('\n').filter(Boolean).map(line => JSON.parse(line)); + if (records.length !== 1 || !validRecording(records[0], path)) throw new Error('expected one valid recording of the selected history'); + recording = records[0]; + } catch (error) { errors.push(`recording unavailable: ${error.message}`); } + if (recording?.error !== undefined) errors.push(recording.error || 'recording reported an unspecified error'); + if (result.error || result.signal || result.status !== 0) { + errors.push(`boundary replay process failed: ${result.error?.message ?? result.error ?? result.signal ?? `exit ${result.status}`}`); + // Go keeps the application failure outside the coordinator's discard + // request. Retain that assertion line alongside the recorded failing step. + const diagnostic = (result.stdout ?? '').split('\n').flatMap(line => { + try { const event = JSON.parse(line); return typeof event.Output === 'string' ? event.Output.split('\n') : []; } + catch { return []; } + }).filter(line => /_test\.go:\d+:|panic:|fatal error:/.test(line)).join('\n'); + if (diagnostic) errors.push(diagnostic.slice(0, 8192)); + } + return { + history, via: 'coordinator', path, + divergences: recording?.divergences ?? [], + completed: recording?.completed === true && errors.length === 0, + lastStep: recording?.lastStep ?? -1, + ...(errors.length ? { error: errors.join('; ') } : {}), + }; +} + +// Boundary runs deliberately bypass the ordinary/generated/fixed cohort +// runners and their test-count comparisons. The same history may support +// several challenges, so callers run each history once and assess it against +// each challenge's own checkpoint afterward. +export function runBoundaryReplay({ port, history, label, output, root, workspace, env, go = 'go' }) { + if (history.startsWith('vector/')) { + const evidence = boundaryEvidence().filter(entry => entry.history === history); + if (!evidence.length || evidence.some(entry => !entry.vector || !isDeepStrictEqual(entry.vector.samples, evidence[0].vector.samples) + || entry.vector.artifact !== evidence[0].vector.artifact || entry.vector.artifactSha256 !== evidence[0].vector.artifactSha256)) + throw new Error(`Vector boundary must select identical native inputs across its citations: ${history}`); + return runVectorBoundary({ evidence: evidence[0], port, history, label, output, root, workspace, env, go }); + } + const { profile, path } = boundaryTrace(history, resolve(root, '.formal-traces')); + const prefix = resolve(output, 'boundary', label, history); + const file = `${prefix}.jsonl`; + mkdirSync(dirname(file), { recursive: true }); + rmSync(file, { force: true }); + let command, args, cwd, selection; + if (port === 'typescript') { + command = process.execPath; + args = [resolve(root, 'node_modules/vitest/vitest.mjs'), 'run', 'test/formal-boundary-evidence.test.ts', '--coverage.enabled=false']; + cwd = workspace; + selection = { DIALCACHE_BOUNDARY_HISTORY: history, DIALCACHE_BOUNDARY_OUT: file }; + } else if (port === 'go') { + const selected = goBoundarySelection(profile, path); + command = go; + args = ['test', '-json', '-count=1', '-timeout=480s', '-run', `^${selected.test}$`, '.']; + cwd = resolve(workspace, 'go'); + selection = { ...selected.env, DIALCACHE_REPLAY_DIVERGENCES: file }; + } else throw new Error(`Unknown boundary replay port: ${port}`); + const result = spawnSync(command, args, { + cwd, env: { ...env, ...selection }, encoding: 'utf8', timeout: 540_000, maxBuffer: 32 * 1024 * 1024, + }); + writeFileSync(`${prefix}.log`, (result.stdout ?? '') + (result.stderr ?? '')); + const recording = readBoundaryRecording(file, history, path, result); + if (recording.error) recording.error += `; see boundary/${label}/${history}.log`; + writeFileSync(`${prefix}.json`, JSON.stringify(recording, null, 2) + '\n'); + return recording; +} + +export function boundaryBaselines(evidence, replay) { + const recordings = {}; + for (const history of new Set(evidence.flatMap(entry => entry.history ? [entry.history] : []))) { + const recording = replay(history); + recordings[history] = recording; + if (!recording.completed || recording.divergences.length) { + throw new Error(`Boundary baseline ${history} must complete with zero divergences: ${recording.error ?? JSON.stringify(recording.divergences)}`); + } + } + return recordings; +} + +export function mutationBoundaries(evidence, replay, assess) { + const recordings = new Map(); + return evidence.map(entry => { + if (entry.history && !recordings.has(entry.history)) recordings.set(entry.history, replay(entry.history)); + return assess(entry, recordings.get(entry.history)); + }); +} diff --git a/formal/cache-contract.qnt b/formal/cache-contract.qnt index a9904342..53f3dc57 100644 --- a/formal/cache-contract.qnt +++ b/formal/cache-contract.qnt @@ -4,6 +4,10 @@ module cache_contract { // Small reference contracts for acquired recovery bytes and source ownership. // Profiles project their external acquisition/start events into these records. // Later storage, policy and caller changes cannot rewrite an acquired record. + // Acquiring bytes keeps their frame stamp. The read time records when the + // acquisition happened, never a new creation time for those bytes. + pure def retainedFrameStamp(frameCreated: int, readAt: int): int = frameCreated + type RecoverySnapshot = { owner: int, payload: int, created: int, maximum: int, acquiredAt: int } diff --git a/formal/cache-rules.qnt b/formal/cache-rules.qnt index 8fa39afb..1b3e43ee 100644 --- a/formal/cache-rules.qnt +++ b/formal/cache-rules.qnt @@ -1,10 +1,24 @@ module cache_rules { - // Canonical transition judgments for C09/C23-C26/C33-C34/C40/C45. + // Canonical transition judgments for C09/C17/C22-C26/C31/C33-C34/C40/C45. // These definitions consume normalized policy and acquired timestamps. They // do not validate wire frames, choose a clock, or decide who owns a snapshot. // Callers supply times in one common unit. Wall-clock age can be negative; // elapsed clocks are monotonic. Assertions independently restate obligations. + // Omitted coalescing is enabled. A configured false remains a different + // input, resolved by the caller before applying its runtime overlay. + pure val libraryCoalescingDefault = true + + // Remote retention keeps bytes for the larger configured age window. + // Tracked physical caps apply later; this is the captured logical policy. + pure def remoteRetentionFor(fresh: int, recovery: int): int = + if (recovery > fresh) recovery else fresh + + // A tracked remote miss cannot authorize direct local publication of a + // source value. A later validated remote hit may warm local storage. + pure def sourceMayWarmLocal(tracked: bool, readRemote: bool): bool = + not(tracked and readRemote) + // Fresh reads and retained recovery both reject future values. Recovery has // no fresh-age lower bound: wall rollback may make retained bytes fresh again. pure def ageBeforeCeiling(created: int, observed: int, ceiling: int): bool = @@ -14,6 +28,12 @@ module cache_rules { pure def recoveryAgeAllowed(created: int, observed: int, maximum: int): bool = ageBeforeCeiling(created, observed, maximum) + // A retained candidate is judged against its acquired maximum at both + // source failure and decode settlement. Initial frame classification uses + // recoveryAgeAllowed directly and does not rebase that maximum. + pure def retainedCandidateAgeAllowed(created: int, observed: int, maximum: int): bool = + recoveryAgeAllowed(created, observed, maximum) + // Classify an initially stale candidate: F <= age < M. A profile may also // retain bytes for fresh decoding. Later recovery uses the acquired maximum // and timestamp, not current storage or a new policy reply. @@ -38,4 +58,8 @@ module cache_rules { // A missing watermark is normalized to zero. Timestamp validity, primary // atomicity and marker lifetime remain explicit responsibilities of callers. pure def fenceAllows(created: int, watermark: int): bool = created > watermark + // Untracked reads ignore a watermark entirely. For a tracked read, equal + // timestamps are fenced before any payload encoding is inspected. + pure def readFenceAllows(tracked: bool, created: int, watermark: int): bool = + not(tracked) or fenceAllows(created, watermark) } diff --git a/formal/check-model-properties.mjs b/formal/check-model-properties.mjs index c6bd6cc2..4273048c 100644 --- a/formal/check-model-properties.mjs +++ b/formal/check-model-properties.mjs @@ -3,7 +3,7 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'nod import { tmpdir } from 'node:os'; import { resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { copySources, quintSources, readExecution, reproducerCheckpoint, validateExecution } from './execution.mjs'; +import { copySources, importClosure, quintSources, readExecution, reproducerCheckpoint, scheduleExecution, validateExecution } from './execution.mjs'; import { printGroup, resolveConcurrency, runPool, seconds, spawnBuffered } from './quint-pool.mjs'; const root = fileURLToPath(new URL('../', import.meta.url)); @@ -32,19 +32,19 @@ export function validatePropertyResult(result, exitCode, expectation) { const escapeRegExp = text => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const EXPECT_FAILED = 'Expect condition does not hold true'; export const probeNames = run => ({ before: `${run}BeforeCheckpointProbe`, through: `${run}ThroughCheckpointProbe` }); +function testOutcome(output, name) { + const escaped = escapeRegExp(name); + const passed = new RegExp(`^\\s*ok ${escaped} passed \\d+ test\\(s\\)$`, 'm').test(output); + const failed = new RegExp(`^\\s*\\d+\\) ${escaped} failed after \\d+ test\\(s\\)$`, 'm').test(output); + if (passed === failed) return undefined; + const error = failed ? new RegExp(`^\\s*\\d+\\) ${escaped}:\\s*\\n\\s*Error \\[(QNT\\d+)\\]: (.*)$`, 'm').exec(output) : undefined; + return { passed, code: error?.[1], message: error?.[2]?.trim() }; +} export function validateReproducerResult(output, exitCode, run, expectation) { if (!['baseline', 'mutant'].includes(expectation)) throw new Error('Unknown model measurement expectation'); if (typeof output !== 'string' || typeof run !== 'string' || !run) throw new Error('Reproducer output and run name are required'); - const outcome = name => { - const escaped = escapeRegExp(name); - const passed = new RegExp(`^\\s*ok ${escaped} passed \\d+ test\\(s\\)$`, 'm').test(output); - const failed = new RegExp(`^\\s*\\d+\\) ${escaped} failed after \\d+ test\\(s\\)$`, 'm').test(output); - if (passed === failed) return undefined; - const error = failed ? new RegExp(`^\\s*\\d+\\) ${escaped}:\\s*\\n\\s*Error \\[(QNT\\d+)\\]: (.*)$`, 'm').exec(output) : undefined; - return { passed, code: error?.[1], message: error?.[2]?.trim() }; - }; const names = probeNames(run); - const results = { run: outcome(run), before: outcome(names.before), through: outcome(names.through) }; + const results = { run: testOutcome(output, run), before: testOutcome(output, names.before), through: testOutcome(output, names.through) }; for (const [part, result] of Object.entries(results)) { if (!result) throw new Error(`Reproducer ${run} did not run: the output names its ${part === 'run' ? 'run' : `${part}-checkpoint probe`} neither passed nor failed`); } @@ -63,6 +63,81 @@ export function validateReproducerResult(output, exitCode, run, expectation) { return { status: 'failed', code: results.through.code }; } +// Every scheduled run must have a terminal result. Only a failed expectation +// or a test returning false is assertion evidence. A named disabled/failed +// history is kept separately and never credited; it does not erase an +// independent assertion failure elsewhere in the same compiled profile. +export function validateProfileTests(output, exitCode, runs) { + if (typeof output !== 'string' || !Array.isArray(runs) || new Set(runs).size !== runs.length) throw new Error('Invalid profile test measurement'); + const failed = [], inconclusive = []; + for (const name of runs) { + const result = testOutcome(output, name); + if (!result) throw new Error(`Profile run ${name} did not complete`); + if (result.passed) continue; + if (!(result.code === 'QNT508' && result.message === EXPECT_FAILED) && + !(result.code === 'QNT511' && result.message === `Test ${name} returned false`)) { + if (!result.code || !result.message) throw new Error(`Profile run ${name} failed without complete Quint diagnostics`); + inconclusive.push({ run: name, code: result.code, message: result.message }); + continue; + } + failed.push(name); + } + if (exitCode !== (failed.length || inconclusive.length ? 1 : 0)) throw new Error('Profile test exit code and assertion results disagree'); + return { status: failed.length ? 'failed' : inconclusive.length ? 'inconclusive' : 'passed', failed, + ...(inconclusive.length ? { inconclusive } : {}) }; +} + +// A library fault partitions every profile by the code it imports, not by +// prose in an exclusion. The cited profile already has a checked reproducer; +// a non-importing listed profile is an impossible claim, even in a partial run. +export function challengePartitionPlan(challenge, manifest, directory = root, closures) { + if (!challenge.reproducer || !manifest.libraries.includes(challenge.source)) return []; + const listed = new Set(challenge.reproducer.profiles); + return manifest.models.filter(model => model.profile).map(model => { + const reaches = (closures?.get(model.path) ?? importClosure(model.path, directory)).includes(challenge.source); + if (listed.has(model.profile) && !reaches) throw new Error(`${challenge.id}/${model.profile}: listed profile does not import ${challenge.source}`); + if (reaches && !listed.has(model.profile) && !Object.hasOwn(challenge.reproducer.exclusions, model.profile)) { + throw new Error(`${challenge.id}/${model.profile}: profile is neither listed nor excluded`); + } + return { model, mode: listed.has(model.profile) ? 'listed' : reaches ? 'excluded' : 'structural' }; + }); +} + +// The callbacks share clean and mutated measurement caches in the real +// runner. Keeping this decision separate makes each exclusion and every +// baseline requirement testable without treating a process error as evidence. +export async function checkChallengePartition(challenge, plan, { tests, invariants, proven = new Set(), checkExclusions = false, onResult = () => {}, onInconclusive = () => {} }) { + const partition = {}; + const record = (profile, state) => { partition[profile] = state; onResult(profile, state); }; + const baseline = (result, model) => { + if (result.status !== 'passed') throw new Error(`${challenge.id}/${model.profile}: unmodified profile must pass before partition measurement`); + }; + for (const { model, mode } of plan) { + const profile = model.profile; + if (mode === 'structural') { record(profile, 'structural'); continue; } + if (mode === 'listed' && proven.has(model.path)) { record(profile, 'detects'); continue; } + baseline(await tests(model, 'baseline'), model); + const tested = await tests(model, 'mutant'); + if (tested.inconclusive?.length) onInconclusive(profile, tested.inconclusive); + if (tested.status === 'failed') { + if (mode === 'excluded') throw new Error(`exclusion no longer holds: ${challenge.id}/${profile}; list the profile in profiles (${tested.failed.join(', ')})`); + record(profile, 'detects'); + continue; + } + if (mode === 'listed' || checkExclusions) { + baseline(await invariants(model, 'baseline'), model); + const checked = await invariants(model, 'mutant'); + if (mode === 'listed' && checked.status !== 'failed') throw new Error(`${challenge.id}/${profile}: listed profile does not detect the mutant in its runs or scheduled invariants`); + if (mode === 'excluded' && checked.status !== 'passed') throw new Error(`exclusion no longer holds: ${challenge.id}/${profile}; list the profile in profiles (scheduled invariant violation)`); + } + if (mode === 'excluded' && tested.status !== 'passed') { + throw new Error(`${challenge.id}/${profile}: exclusion is inconclusive because mutant histories failed without assertion evidence (${tested.inconclusive.map(item => `${item.run}: ${item.code} ${item.message}`).join('; ')})`); + } + record(profile, mode === 'listed' ? 'detects' : 'holds'); + } + return partition; +} + // Select a subset of the catalog by id for local iteration. The complete // catalog remains the only accepted evidence: a filtered report is never final. export function selectChallenges(manifest, only) { @@ -74,23 +149,49 @@ export function selectChallenges(manifest, only) { return selected; } +// Challenge workspaces are independent. Keep their original errors and the +// partial evidence in catalog order, but finish the other measurements before +// failing the campaign so one stale partition cannot hide the next one. +export async function runChallengeMeasurements(report, measure, { concurrency = resolveConcurrency(), save = () => {} } = {}) { + report.complete = false; + const results = await runPool(report.challenges.map((entry, index) => async () => { + try { + await measure(entry, index); + } catch (error) { + entry.error = String(error); + return { entry, error }; + } finally { save(); } + }), { concurrency }); + const failures = results.filter(result => result !== undefined); + if (failures.length) { + throw new AggregateError(failures.map(failure => failure.error), + `Model property measurements failed for ${failures.length} challenge(s):\n${failures.map(({ entry }) => `${entry.id}: ${entry.error}`).join('\n')}`); + } + report.complete = !report.partial; + try { save(); } + catch (error) { report.complete = false; throw error; } +} + export async function measureModelProperties({ only, concurrency = resolveConcurrency() } = {}) { const output = resolve(root, '.formal-traces/model-properties'); const manifest = readExecution(); // A complete measurement validates the whole manifest first; a filtered run // is a local iteration aid and may precede catalog coverage. if (only === undefined) validateExecution(manifest); + const scheduled = scheduleExecution(manifest); + const profileClosures = new Map(scheduled.models.filter(model => model.profile).map(model => [model.path, importClosure(model.path)])); const challenges = selectChallenges(manifest, only); const { settings, check } = manifest; const options = [`--backend=${settings.backend}`, `--n-threads=${settings.threads}`, `--seed=${settings.seed}`, `--max-samples=${check.maxSamples}`, `--max-steps=${check.maxSteps}`]; const sources = new Map(quintSources().map(path => [path, readFileSync(resolve(root, path), 'utf8')])); mkdirSync(output, { recursive: true }); - const report = { schemaVersion: 4, complete: false, partial: only !== undefined, mode: 'bounded-simulation', options, + const report = { schemaVersion: 5, complete: false, partial: only !== undefined, mode: 'bounded-simulation', options, sources: Object.fromEntries([...sources].map(([path, source]) => [path, createHash('sha256').update(source).digest('hex')])), catalogSha256: createHash('sha256').update(readFileSync(resolve(root, 'formal/execution.json'))).digest('hex'), catalog: manifest.challenges.length, reproducers: manifest.challenges.filter(challenge => challenge.reproducer).length, - reproducerBacklog: manifest.reproducerBacklog.length, nativeMutantBacklog: manifest.nativeMutantBacklog.length, challenges: [] }; + reproducerBacklog: manifest.reproducerBacklog.length, nativeMutantBacklog: manifest.nativeMutantBacklog.length, + partitionMeasurements: { executed: 0, cacheHits: 0, commandSeconds: 0 }, challenges: [] }; const save = () => writeFileSync(resolve(output, 'report.json'), JSON.stringify(report, null, 2) + '\n'); save(); async function execute(args) { @@ -98,9 +199,58 @@ export async function measureModelProperties({ only, concurrency = resolveConcur // to 35 s on the slow hosted runner of run 34666226055 before this pool // shared its cores with three siblings. const result = await spawnBuffered('quint', args, { cwd: root, timeoutMs: 180_000 }); - if (result.error || result.signal) throw new Error(`Quint execution failed: ${result.error ?? result.signal}`); + if (result.error || result.signal) throw new Error(`Quint execution failed (${args.slice(0, 2).join(' ')}): ${result.error ?? result.signal}`); return result; } + const partitionCache = new Map(), compileCache = new Map(); + const hash = value => createHash('sha256').update(JSON.stringify(value)).digest('hex'); + // Cache only exact source closures and exact evaluator options. A shared + // library fault can have several challenge ids, but identical names or + // native-mutant ids alone are never enough to share evidence. + async function profileMeasurement(kind, model, label, challenge, workspace) { + const source = sources.get(challenge.source); + writeFileSync(resolve(workspace, challenge.source), label === 'baseline' ? source : source.replace(challenge.before, () => challenge.after)); + const closure = importClosure(model.path, workspace).map(path => [path, readFileSync(resolve(workspace, path), 'utf8')]); + const compileKey = hash({ model: model.path, closure }); + const selected = kind === 'tests' ? model.regressions : model.invariants; + const flags = kind === 'tests' ? [`--backend=${settings.backend}`, '--max-samples=1', `--seed=${settings.seed}`] : options; + const key = hash({ compileKey, kind, selected, flags }); + if (partitionCache.has(key)) { + report.partitionMeasurements.cacheHits++; + return partitionCache.get(key); + } + const pending = (async () => { + report.partitionMeasurements.executed++; + const modelPath = resolve(workspace, model.path); + const prefix = resolve(output, `partition-${model.profile}-${key}`); + if (!compileCache.has(compileKey)) { + compileCache.set(compileKey, (async () => { + const compile = await execute(['typecheck', modelPath]); + writeFileSync(resolve(output, `partition-${model.profile}-${compileKey}-typecheck.log`), compile.stdout + compile.stderr); + report.partitionMeasurements.commandSeconds += compile.durationMs / 1000; + if (compile.status !== 0) throw new Error(`${challenge.id}/${model.profile}/${label}: profile must typecheck before partition measurement`); + })()); + } + await compileCache.get(compileKey); + if (kind === 'tests' && selected.length === 0) return { status: 'passed', failed: [] }; + if (!selected.length) throw new Error(`${challenge.id}/${model.profile}: no scheduled invariants for partition measurement`); + const args = kind === 'tests' + ? ['test', modelPath, ...flags] + : ['run', modelPath, ...flags, '--invariants', ...selected, `--out=${prefix}.json`]; + rmSync(`${prefix}.json`, { force: true }); + const evaluated = await execute(args); + const detail = evaluated.stdout + evaluated.stderr; + writeFileSync(`${prefix}.log`, detail); + report.partitionMeasurements.commandSeconds += evaluated.durationMs / 1000; + save(); + if (kind === 'tests') return validateProfileTests(detail, evaluated.status, selected); + const result = JSON.parse(readFileSync(`${prefix}.json`, 'utf8')); + validatePropertyResult(result, evaluated.status, result.status === 'violation' ? 'mutant' : 'baseline'); + return { status: result.status === 'violation' ? 'failed' : 'passed' }; + })(); + partitionCache.set(key, pending); + return pending; + } // Each challenge measures in its own copy of every Quint source, so challenges // run side by side and no mutation can leak into another baseline. The report // fingerprints every Quint dependency. Both runs of a challenge share the copy @@ -113,7 +263,7 @@ export async function measureModelProperties({ only, concurrency = resolveConcur try { for (const label of ['baseline', 'mutant']) { copySources(root, workspace); - if (label === 'mutant') writeFileSync(resolve(workspace, challenge.source), source.replace(challenge.before, challenge.after)); + if (label === 'mutant') writeFileSync(resolve(workspace, challenge.source), source.replace(challenge.before, () => challenge.after)); const model = resolve(workspace, challenge.model); const prefix = resolve(output, `${challenge.id}-${label}`); const compile = await execute(['typecheck', model]); @@ -143,8 +293,15 @@ export async function measureModelProperties({ only, concurrency = resolveConcur const names = probeNames(name); const end = text.lastIndexOf('}'); writeFileSync(cited, `${text.slice(0, end)} run ${names.before} = ${checkpoint.before}\n run ${names.through} = ${checkpoint.through}\n${text.slice(end)}`); - const test = await execute(['test', cited, `--backend=${settings.backend}`, '--max-samples=1', - `--seed=${settings.seed}`, `--match=^(${name}|${names.before}|${names.through})$`]); + let test; + try { + test = await execute(['test', cited, `--backend=${settings.backend}`, '--max-samples=1', + `--seed=${settings.seed}`, `--match=^(${name}|${names.before}|${names.through})$`]); + } finally { + // Partition runs measure the declared profile, without the temporary + // checkpoint probes, and can share the same clean source fingerprint. + writeFileSync(cited, text); + } detail = test.stdout + test.stderr; writeFileSync(`${prefix}-reproducer.log`, detail); const outcome = validateReproducerResult(detail, test.status, name, label); @@ -152,6 +309,29 @@ export async function measureModelProperties({ only, concurrency = resolveConcur if (label === 'mutant') entry.reproducer.code = outcome.code; save(); lines.push(`${label}: reproducer ${name} ${outcome.status}${outcome.code ? ` (${outcome.code} at the declared checkpoint)` : ''}, ${seconds(test.durationMs)}\n`); + if (label === 'mutant') { + const plan = challengePartitionPlan(challenge, scheduled, root, profileClosures); + if (plan.length) { + const started = performance.now(); + // The cited run and the primary invariant already have clean and + // mutant evidence in this workspace; use those exact checks once. + const primary = scheduled.models.find(model => model.path === challenge.model); + const proven = new Set([challenge.reproducer.model ?? challenge.model]); + if (primary?.invariants.includes(challenge.invariant)) proven.add(challenge.model); + entry.partition = {}; + await checkChallengePartition(challenge, plan, { + proven, checkExclusions: only !== undefined, + onResult: (profile, state) => { entry.partition[profile] = state; }, + onInconclusive: (profile, histories) => { (entry.partitionInconclusive ??= {})[profile] = histories; }, + tests: (model, label) => profileMeasurement('tests', model, label, challenge, workspace), + invariants: (model, label) => profileMeasurement('invariants', model, label, challenge, workspace), + }); + entry.partitionSeconds = Math.round((performance.now() - started) / 1000); + const counts = ['structural', 'holds', 'detects'].map(state => `${state}=${Object.values(entry.partition).filter(value => value === state).length}`); + lines.push(`partition: ${counts.join(', ')}, ${entry.partitionSeconds} s\n`); + save(); + } + } } printGroup(`${challenge.id}: compiling fault violates ${challenge.invariant}`, ...lines); } catch (error) { @@ -176,11 +356,10 @@ export async function measureModelProperties({ only, concurrency = resolveConcur ...(challenge.reproducer === undefined ? {} : { reproducer: { ...challenge.reproducer, baseline: 'pending', mutant: 'pending' } }) }); } save(); - await runPool(challenges.map((challenge, index) => () => measure(challenge, report.challenges[index])), { concurrency }); - report.complete = only === undefined; - save(); + await runChallengeMeasurements(report, (entry, index) => measure(challenges[index], entry), { concurrency, save }); return report; } catch (error) { + report.complete = false; report.error = String(error); save(); throw error; diff --git a/formal/check-semantic-coverage.mjs b/formal/check-semantic-coverage.mjs index 538f50dc..87ffa77c 100644 --- a/formal/check-semantic-coverage.mjs +++ b/formal/check-semantic-coverage.mjs @@ -18,12 +18,12 @@ export function checkProfiles(registry = parse('formal/profiles.json')) { if (registry.schemaVersion !== 1 || registry.specificationVersion !== '0.1.0' || registry.status !== 'experimental') throw new Error('Unsupported specification/profile registry'); if (registry.behavioralSchemaVersion !== parse('formal/behavioral-scenarios.json').schemaVersion || registry.protocolSchemaVersion !== protocol.schemaVersion || registry.invalidationSchemaVersion !== parse('formal/invalidation-vectors.json').schemaVersion) throw new Error('Profile registry schema versions have drifted'); - const expected = ['admission', 'core', 'effects', 'independent', 'layers', 'local-clock', 'local-failure', 'policy', 'recovery', 'recovery-read', 'runtime-boundaries', 'scope', 'shadow', 'shadow-layers', 'source-budgets']; + const expected = ['admission', 'core', 'dark-layers', 'effects', 'independent', 'layers', 'local-clock', 'local-failure', 'policy', 'recovery', 'recovery-read', 'runtime-boundaries', 'scope', 'shadow', 'shadow-layers', 'shadow-read-deadlines', 'source-budgets']; if (!Array.isArray(registry.profiles) || JSON.stringify(registry.profiles.map(p => p.id).sort()) !== JSON.stringify(expected)) throw new Error('Profile inventory changed; review claims'); read(registry.normativeDefinition); if (registry.behavioralAuthority?.kind !== 'quint' || registry.behavioralAuthority.executionManifest !== 'formal/execution.json' || !registry.behavioralAuthority.conflictPolicy) throw new Error('Quint behavioral authority must be explicit'); for (const profile of registry.profiles) { - const supportedVersion = ["policy", "shadow"].includes(profile.id) ? 3 : ["effects", "scope", "shadow", "layers", "independent"].includes(profile.id) ? 2 : 1; + const supportedVersion = ["effects", "policy", "shadow"].includes(profile.id) ? 3 : ["dark-layers", "scope", "layers", "independent"].includes(profile.id) ? 2 : 1; if (profile.version !== supportedVersion) throw new Error(`${profile.id}: unsupported profile version`); read(profile.definition); read(profile.model); for (const path of profile.witnessSources ?? []) read(path); diff --git a/formal/coverage-witnesses.json b/formal/coverage-witnesses.json index 699d6b0d..42896bcd 100644 --- a/formal/coverage-witnesses.json +++ b/formal/coverage-witnesses.json @@ -66,7 +66,8 @@ "decode-timeout-keeps-slot", "confirmation-timeout-keeps-slot", "unselected-hit-skips-job", - "uncoalesced-hit-overlap" + "uncoalesced-hit-overlap", + "later-served-shadow-keeps-own-job-budget" ], "scope": [ "failure-layer:request_local", @@ -278,6 +279,8 @@ "late-dark-read-reports-offset-without-reviving-job" ], "effects": [ + "adapter-frame-last-fresh-millisecond-hits", + "adapter-frame-exact-expiry-refills-and-reuses", "event:request", "event:disabled", "event:miss", @@ -466,5 +469,63 @@ "policy-wait-does-not-spend-source-budget", "invalid-enabled-key-preserves-source-deadline", "abandoned-source-cannot-replace-successful-retry" + ], + "dark-layers": [ + "inspection-counts-process-followers-and-oldest", + "inspection-isolates-instances-and-wall-shifts", + "inspection-excludes-request-and-uncoalesced", + "inspection-clears-timeout-before-raw-settlement", + "inspection-excludes-unfinished-shadow", + "rejected-dark-source-seeds-no-layer", + "uncoalesced-publication-keeps-dark-leader", + "held-dark-fill-keeps-captured-retention", + "dark-fill-at-watermark-is-fenced", + "dark-frame-at-watermark-is-fenced", + "held-dark-fill-rechecks-fence-after-rollback", + "stale-visible-dark-c0-fills-unfenced", + "undecodable-held-dark-c0-is-not-repaired", + "unsupported-encoding-ends-dark-read", + "dark-source-deadline-attributed-to-local", + "dark-flights-are-instance-local", + "closed-scope-bypasses-dark-work", + "held-dark-capacity-is-instance-local", + "held-dark-dump-error-preserves-source", + "dark-source-local-publication-stops-job", + "held-dark-c0-age-at-verdict", + "future-dark-c0-reports-offset-and-fills", + "action:beginCall", + "action:releaseRead", + "action:releaseLoad", + "action:resolveLoader", + "action:rejectLoader", + "action:releaseDump", + "action:releaseWrite", + "action:advance", + "action:policy", + "action:seed", + "action:invalidate", + "action:closeScope", + "action:rollbackWall", + "action:readFault", + "action:loadFault", + "action:dumpFault", + "action:writeFault", + "local-entry-expires-at-its-ttl", + "request-follower-joins-leader", + "transient-request-dark-error-uses-request-layer", + "later-dark-source-keeps-own-budget", + "timed-out-dark-read-retains-capacity-until-raw-release", + "timed-out-dark-decode-retains-capacity-until-raw-release", + "timed-out-dark-confirmation-retains-capacity-until-raw-release", + "timed-out-dark-dump-retains-capacity-until-raw-release", + "timed-out-dark-write-retains-capacity-until-raw-release" + ], + "shadow-read-deadlines": [ + "c0-read-timeout-retains-raw-capacity", + "c1-read-timeout-retains-raw-capacity", + "job-then-read-timeout-has-one-verdict", + "confirmation-keeps-captured-read-budget", + "confirmation-read-budget-starts-at-dispatch", + "timely-shadow-read-cancels-deadline-and-fills" ] } diff --git a/formal/dark-layers-smoke.itf.json b/formal/dark-layers-smoke.itf.json new file mode 100644 index 00000000..3fd22b3d --- /dev/null +++ b/formal/dark-layers-smoke.itf.json @@ -0,0 +1,22 @@ +{ + "#meta": { + "format": "ITF", + "source": "formal/dialcache-dark-layers-conformance.qnt", + "recipe": "formal/fixture-recipes.json#formal/dark-layers-smoke.itf.json/smoke" + }, + "vars": [ + "input", + "s", + "mbt::actionTaken", + "mbt::nondetPicks" + ], + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":["request_local"],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":["request_local"],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":["request_local"],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseDump"},"mbt::actionTaken":"releaseDump","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[{"#bigint":"120000"}],"writes":{"#bigint":"1"}},"d":{"ages":[],"coalesced":["request_local"],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseWrite"},"mbt::actionTaken":"releaseWrite","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["filled"],"sourceScopes":[true],"writeTtls":[{"#bigint":"120000"}],"writes":{"#bigint":"1"}},"d":{"ages":[],"coalesced":["request_local"],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}} + ] +} diff --git a/formal/dialcache-admission-conformance.qnt b/formal/dialcache-admission-conformance.qnt index d85d032b..6f5b0579 100644 --- a/formal/dialcache-admission-conformance.qnt +++ b/formal/dialcache-admission-conformance.qnt @@ -225,6 +225,13 @@ module dialcache_admission_conformance { // -- Independently stated safety properties over the library records -------- + // This served-job shape delivers job budgets, not remote-read deadlines. + val elapsedStaysBelowReadDeadlines = s.now < READ_BUDGET_MS + + // Job budgets are positive and advances deliver every due timeout before + // observation. Work admitted later must not inherit elapsed clock uptime. + val pendingDeadlinesAreFuture = s.deadlines.indices().forall(i => s.deadlines.nth(i).at > s.now) + // A job's identity is its source's (instance, key); the registry is // pending-only, so every record is a live job. def jobInstance(i: int): int = s.sources.nth(s.jobs.nth(i).source).instance @@ -284,6 +291,15 @@ module dialcache_admission_conformance { .then(loadWith(0)) .expect(s.o.calls == List(VALUE_ONE, VALUE_ONE) and s.o.loaders == 1 and s.o.reads == 1 and s.o.shadow == List()) + // A served hit starts a detached job after the clock has already advanced. + // A source rejection one millisecond later is still its own source error, + // not a timeout; the caller keeps the value it acquired from the cache. + run laterServedShadowKeepsItsWholeBudgetTest = init + .then(advanceBy(SHADOW_TIMEOUT_MS)).then(beginWith(0)).then(readWith(0)).then(loadWith(0)) + .then(advanceBy(1)).then(rejectWith(0)) + .expect(s.o.calls == List(VALUE_ONE) and s.o.loaders == 1 and s.o.loads == 1 + and s.o.shadow == List("source_error")) + run duplicateDropsBeforeCapacityIsFullTest = init .then(beginWith(0)) .then(readWith(0)) diff --git a/formal/dialcache-core.qnt b/formal/dialcache-core.qnt index 85c541df..50cc1383 100644 --- a/formal/dialcache-core.qnt +++ b/formal/dialcache-core.qnt @@ -1,4 +1,5 @@ module dialcache_core { + import cache_rules.* from "./cache-rules" // One-key behavioral model for caller-visible traversal and publication. // A single record state keeps every nondeterministic transition on the same // Quint update effect, while preserving the protocol-level distinctions. @@ -353,9 +354,9 @@ module dialcache_core { s.remoteReadFailed, s' = { origin: SourceValue, - localWriteFailures: if (localWriteFails and not(s.callTracked)) s.localWriteFailures + 1 else s.localWriteFailures, - localHasValue: if (localWriteEligible and not(s.callTracked)) true else s.localHasValue, - localWrites: if (localWriteEligible and not(s.callTracked)) s.localWrites + 1 else s.localWrites, + localWriteFailures: if (localWriteFails and sourceMayWarmLocal(s.callTracked, true)) s.localWriteFailures + 1 else s.localWriteFailures, + localHasValue: if (localWriteEligible and sourceMayWarmLocal(s.callTracked, true)) true else s.localHasValue, + localWrites: if (localWriteEligible and sourceMayWarmLocal(s.callTracked, true)) s.localWrites + 1 else s.localWrites, requestHasValue: if (requestMemoEligible) true else s.requestHasValue, requestWrites: if (requestMemoEligible) s.requestWrites + 1 else s.requestWrites, requestValueScope: if (requestMemoEligible) s.callScopeId else s.requestValueScope, @@ -396,9 +397,9 @@ module dialcache_core { s' = { remoteWrites: s.remoteWrites + 1, remoteHasValue: true, - localWriteFailures: if (localWriteFails and not(s.callTracked)) s.localWriteFailures + 1 else s.localWriteFailures, - localHasValue: if (localWriteEligible and not(s.callTracked)) true else s.localHasValue, - localWrites: if (localWriteEligible and not(s.callTracked)) s.localWrites + 1 else s.localWrites, + localWriteFailures: if (localWriteFails and sourceMayWarmLocal(s.callTracked, true)) s.localWriteFailures + 1 else s.localWriteFailures, + localHasValue: if (localWriteEligible and sourceMayWarmLocal(s.callTracked, true)) true else s.localHasValue, + localWrites: if (localWriteEligible and sourceMayWarmLocal(s.callTracked, true)) s.localWrites + 1 else s.localWrites, requestHasValue: if (requestMemoEligible) true else s.requestHasValue, requestWrites: if (requestMemoEligible) s.requestWrites + 1 else s.requestWrites, requestValueScope: if (requestMemoEligible) s.callScopeId else s.requestValueScope, @@ -516,6 +517,17 @@ module dialcache_core { .then(toggleLocalReadHealth).then(startEnabledCall).then(requestLookup).then(localLookup).then(remoteLookupHealthy) .expect(s.origin == RemoteValue and s.localReads == 1 and s.remoteReads == 1 and s.sourceCalls == 0 and s.localWrites == 0) + // Populate local storage first, then fail its read. Ending at the lookup + // decision attributes the fault to illegal reuse rather than a later action + // that becomes disabled after the faulty hit. + run failedLocalReadCannotServePreviouslyStoredValueTest = init.then(toggleLocalPolicy) + .then(openScope).then(startEnabledCall).then(requestLookup).then(localLookup) + .then(skipRemote).then(loaderSuccessWithoutRemote).expect(s.localHasValue) + .then(resetCall).then(toggleLocalReadHealth).then(startEnabledCall) + .then(requestLookup).then(localLookup) + .expect(s.localHasValue and s.phase == RemoteLookup and s.origin == NoResult + and s.localReads == 1 and failedLocalReadSkipsReuseAndPublication) + run localWriteFailurePreservesSourceAndMemoTest = init.then(toggleLocalPolicy).then(toggleRequestPolicy) .then(toggleLocalWriteHealth).then(openScope).then(startEnabledCall).then(requestLookup).then(localLookup) .then(skipRemote).then(loaderSuccessWithoutRemote) diff --git a/formal/dialcache-dark-layers-conformance.qnt b/formal/dialcache-dark-layers-conformance.qnt new file mode 100644 index 00000000..b2688c8d --- /dev/null +++ b/formal/dialcache-dark-layers-conformance.qnt @@ -0,0 +1,668 @@ +module dialcache_dark_layers_conformance { + import cache_rules.* from "./cache-rules" + import conformance_observations.* from "./conformance-observations" + import encodings.* from "./kernel/encodings" + import calls.* from "./kernel/calls" + import layer_policy.* from "./kernel/layer-policy" + import serving as Serving from "./kernel/serving" + import remote_frames as Remote from "./kernel/remote-frames" + import remote_io as RemoteIO from "./kernel/remote-io" + import remote_writes as Writes from "./kernel/remote-writes" + import shadow as Shadow from "./kernel/shadow" + import deadlines as Deadlines from "./kernel/deadlines" + import diagnostics as Diagnostics from "./kernel/diagnostics" + import recovery as Recovery from "./kernel/recovery" + import payloads as Payloads from "./kernel/payloads" + import flights as Flights from "./kernel/flights" + import clock as Clock from "./kernel/clock" + import policy_gate as Gate from "./kernel/policy-gate" + // Interaction profile: dark shadow work over the layers layout on the held + // path. Every reply leaves the remote layer off, so a caller traverses the + // request memo and local storage and starts its own bounded source, while a + // selected key's dark job reads, decodes, confirms and fills the remote + // layer through held effects released by index. Two instances, two keys, + // three persistent scopes and two transient contexts; coalescing on or off + // per reply; 10 ms source deadlines (the job budget, as the library derives + // it from the same fallback timeout); tracked invalidation with a future + // buffer; wall rollbacks; the adapter's read, decode, dump and write + // faults; decode-fault and read-fault payload classes on the job's held + // path. Every rule is a kernel transition (formal/kernel): the dark job + // lifecycle over the diagnosed budgeted held remote traversal (shadow, + // diagnostics, deadlines, remote_io, remote_writes), the frames a seed + // stores (remote_frames), the invalidation with its buffer (serving), the + // wall clock (clock). This module owns the bounded state, the fixture + // decoding (the layers, coalescing, TTLs and selection a policy code + // resolves to; the frame a seed choice names), the input choices and the + // guards. The explicit input envelope is the portable driver input. + + pure val KEYS = 2 + pure val INSTANCES = 2 + pure val SCOPES = 3 + pure val CONTEXTS = 5 + pure val MAX_CALLS = 12 + pure val JOB_CAPACITY = 1 + // One constant: the library derives the job budget from the call's fallback + // timeout (src/dialcache.ts `timeoutMs: fallbackTimeoutMs ?? DEFAULT_FALLBACK_TIMEOUT_MS`, + // go/shadow.go `budget()`), so the two cannot differ (C6 in the exclusions inventory). + pure val SOURCE_BUDGET_MS = 10 + pure val READ_BUDGET_MS = 30000000 + pure val LOCAL_TTL_MS = 60000 + pure val SEED_TTL_MS = 180000 + pure val FUTURE_BUFFER_MS = 20 + pure val WALL_ROLLBACK_MS = 1000 + pure val WALL_ORIGIN_MS = 100000 + pure val MAX_ELAPSED_MS = 600000 + pure val DARK_ALL = 0 + pure val DARK_REQUEST = 1 + pure val DARK_LOCAL = 2 + pure val DARK_NONE = 3 + pure val DARK_ALL_UNCOALESCED = 4 + pure val DARK_LOCAL_UNCOALESCED = 5 + pure val DARK_ALL_NO_SHADOW = 6 + pure val DARK_ALL_CHANGED_FILL = 7 + pure val DARK_NONE_CHANGED_FILL = 8 + pure val LAYOUT: Serving::Layout = { + keysPerInstance: KEYS, keysPerScope: KEYS, persistentContexts: SCOPES, operationsPerEntity: 1, probeSourceScope: true, policyProvider: true + } + + type Channel = { inspections: List[Flights::Inspection], configErrors: int, warnings: int, ages: List[int], coalesced: List[str], fallbackErrors: List[str], futureOffsets: List[Diagnostics::FutureOffset] } + type State = { + o: Observation, d: Channel, io: RemoteIO::IO, + now: int, skew: int, remoteAvailable: bool, tracked: bool, capacity: int, + readFailed: bool, dumpFailed: bool, writeFailed: bool, loadFailed: bool, + policy: int, resolved: Resolution, ttls: Ttls, selected: bool, + closed: List[bool], memo: List[int], requestFlights: List[int], processFlights: List[int], + localValues: List[int], localExpires: List[int], lru: List[List[int]], + remoteValues: List[int], created: List[int], expires: List[int], watermark: List[int], + sources: List[Serving::LayeredSource], owners: List[int], memoSlots: List[int], held: List[Gate::Held], + reads: List[RemoteIO::HeldRead], loads: List[RemoteIO::HeldLoad], loaders: List[int], retained: List[Recovery::Snapshot], + sourceBudget: int, deadlines: List[Deadlines::Due], drained: List[bool], sourceLayers: List[str], + dumps: List[Writes::HeldDump], writes: List[Writes::HeldWrite], jobs: List[Shadow::Job], + callerInstances: List[int], callerKeys: List[int], + inspectionStarted: List[int], inspectionJoins: List[str] + } + var s: State + var input: { name: str, choice: int } + + // Input decoding (reads no state) ------------------------------------------- + pure def zeros(n: int): List[int] = 0.to(n - 1).fold(List(), (out, _) => out.append(0)) + pure def slots(n: int): List[int] = 0.to(n - 1).fold(List(), (out, _) => out.append(NO_OWNER)) + pure def changedFill(choice: int): bool = choice == DARK_ALL_CHANGED_FILL or choice == DARK_NONE_CHANGED_FILL + // The layers a policy code enables (the remote layer never), and whether the reply coalesces. + pure def resolvedFor(choice: int): Resolution = { + layers: { + request: Set(DARK_ALL, DARK_REQUEST, DARK_ALL_UNCOALESCED, DARK_ALL_NO_SHADOW, DARK_ALL_CHANGED_FILL).contains(choice), + local: Set(DARK_ALL, DARK_LOCAL, DARK_ALL_UNCOALESCED, DARK_LOCAL_UNCOALESCED, DARK_ALL_NO_SHADOW, DARK_ALL_CHANGED_FILL).contains(choice), + remote: false + }, + coalesce: not(Set(DARK_ALL_UNCOALESCED, DARK_LOCAL_UNCOALESCED).contains(choice)) + } + // A 60 s local insertion TTL; the changed-fill codes carry a 120 s freshness + // and a 180 s retention, the others 60 s and 120 s (the dark job captures + // these, C18; the caller's source, with the remote layer off, captures none). + pure def ttlsFor(choice: int): Ttls = { + localMs: LOCAL_TTL_MS, + freshMs: if (changedFill(choice)) 120000 else 60000, + retentionMs: if (changedFill(choice)) 180000 else 120000 + } + pure def selectedFor(choice: int): bool = choice != DARK_ALL_NO_SHADOW + pure def shadowing(selected: bool): Shadow::Shadowing = + { hook: true, selected: selected, capacity: JOB_CAPACITY, budgetMs: SOURCE_BUDGET_MS, logging: false } + pure def instance(context: int): int = if (context == 2 or context == 4) 1 else 0 + pure def call(choice: int): Call = + { instance: instance(choice / KEYS), key: choice % KEYS, context: choice / KEYS, enabled: true, keyFailed: false } + pure def resolvedSource(code: int): int = (code - 1) / 2 + pure def resolvedValue(code: int): int = (code - 1) % 2 + VALUE_ONE + // Seeds: per key, a fresh value 1 or 2, value 1 at 59999 ms and at 60000 ms + // (the freshness boundary), a corrupt frame decoding to 1 (the held decode + // fails it, C52) and a frame of an unsupported encoding (the read fails). + pure def seedKey(choice: int): int = choice / 6 + pure def seedPayload(choice: int): int = + if (choice % 6 == 1) VALUE_TWO + else if (choice % 6 == 4) Payloads::CORRUPT + VALUE_ONE + else if (choice % 6 == 5) Payloads::UNSUPPORTED_ENCODING + else VALUE_ONE + pure def seedAge(choice: int): int = if (choice % 6 == 2) 59999 else if (choice % 6 == 3) 60000 else 0 + pure def invalidatedEntity(choice: int): int = choice / 2 + pure def invalidationBuffer(choice: int): int = (choice % 2) * FUTURE_BUFFER_MS + + pure val INITIAL: State = { + o: empty, d: { inspections: List(), configErrors: 0, warnings: 0, ages: List(), coalesced: List(), fallbackErrors: List(), futureOffsets: List() }, + io: RemoteIO::NO_IO, + now: 0, skew: WALL_ORIGIN_MS, remoteAvailable: true, tracked: true, capacity: KEYS, + readFailed: false, dumpFailed: false, writeFailed: false, loadFailed: false, + policy: DARK_ALL, resolved: resolvedFor(DARK_ALL), ttls: ttlsFor(DARK_ALL), selected: selectedFor(DARK_ALL), + closed: List(false, false, false), memo: zeros(SCOPES * KEYS), requestFlights: slots(SCOPES * KEYS), processFlights: slots(INSTANCES * KEYS), + localValues: zeros(INSTANCES * KEYS), localExpires: zeros(INSTANCES * KEYS), lru: List(List(), List()), + remoteValues: zeros(KEYS), created: zeros(KEYS), expires: zeros(KEYS), watermark: zeros(KEYS), + sources: List(), owners: List(), memoSlots: List(), held: List(), + reads: List(), loads: List(), loaders: List(), retained: List(), + sourceBudget: SOURCE_BUDGET_MS, deadlines: List(), drained: List(), sourceLayers: List(), + dumps: List(), writes: List(), jobs: List(), + callerInstances: List(), callerKeys: List(), inspectionStarted: List(), inspectionJoins: List() + } + action init = all { input' = { name: "init", choice: -1 }, s' = INITIAL } + + // Wiring: each s' is one library transition --------------------------------- + action beginAt(choice: int): bool = all { + s.o.calls.length() < MAX_CALLS, + input' = { name: "beginCall", choice: choice }, + s' = Flights::recordInspectionAdmission(s, Flights::recordIdentity( + Shadow::beginDark(s, LAYOUT, call(choice), s.resolved, s.ttls, READ_BUDGET_MS, shadowing(s.selected), Shadow::WELL_FORMED, 0), call(choice))) + } + action beginCall = { nondet choice = 0.to(CONTEXTS * KEYS - 1).oneOf(); beginAt(choice) } + val heldReads = s.reads.indices().map(i => s.reads.nth(i).read) + val heldLoads = s.loads.indices().map(i => s.loads.nth(i).load) + action readAt(index: int): bool = all { + Shadow::holdsRead(s, index), + input' = { name: "releaseRead", choice: index }, + s' = Shadow::releaseRead(s, LAYOUT, index, s.readFailed) + } + action releaseRead = { nondet choice = heldReads.oneOf(); readAt(choice) } + action loadAt(index: int): bool = all { + Shadow::holdsLoad(s, index), + input' = { name: "releaseLoad", choice: index }, + s' = Shadow::releaseLoad(s, LAYOUT, index, s.loadFailed, Shadow::VALUE_COMPARISON, READ_BUDGET_MS) + } + action releaseLoad = { nondet choice = heldLoads.oneOf(); loadAt(choice) } + val pendingLoaders = s.loaders.indices().filter(k => Deadlines::pending(s, k)) + action resolveWith(code: int): bool = all { + pendingLoaders.contains(resolvedSource(code)), + input' = { name: "resolveLoader", choice: code }, + s' = Shadow::settleHeld(s, LAYOUT, resolvedSource(code), resolvedValue(code), false) + } + action resolveLoader = { + nondet choice = 1.to(MAX_CALLS * 2).filter(code => pendingLoaders.contains(resolvedSource(code))).oneOf() + resolveWith(choice) + } + action rejectAt(ordinal: int): bool = all { + pendingLoaders.contains(ordinal), + input' = { name: "rejectLoader", choice: ordinal }, + s' = Shadow::settleHeld(s, LAYOUT, ordinal, SOURCE_ERROR, false) + } + action rejectLoader = { nondet choice = pendingLoaders.oneOf(); rejectAt(choice) } + action releaseDumpAt(index: int): bool = all { + Writes::holdsDump(s, index), + input' = { name: "releaseDump", choice: index }, + s' = Shadow::releaseDump(s, LAYOUT, index, s.dumpFailed) + } + action releaseDump = { nondet choice = s.dumps.indices().map(i => s.dumps.nth(i).dump).oneOf(); releaseDumpAt(choice) } + action releaseWriteAt(index: int): bool = all { + Writes::holdsWrite(s, index), + input' = { name: "releaseWrite", choice: index }, + s' = Shadow::releaseWrite(s, LAYOUT, index, s.writeFailed) + } + action releaseWrite = { nondet choice = s.writes.indices().map(i => s.writes.nth(i).write).oneOf(); releaseWriteAt(choice) } + action advanceBy(ms: int): bool = all { + s.now + ms <= MAX_ELAPSED_MS, + input' = { name: "advance", choice: ms }, + s' = Shadow::advanceHeld(s, LAYOUT, ms) + } + action advance = { nondet choice = Set(1, SOURCE_BUDGET_MS, LOCAL_TTL_MS).oneOf(); advanceBy(choice) } + action setPolicy(choice: int): bool = all { + input' = { name: "policy", choice: choice }, + s' = { policy: choice, resolved: resolvedFor(choice), ttls: ttlsFor(choice), selected: selectedFor(choice), ...s } + } + action policy = { nondet choice = 0.to(DARK_NONE_CHANGED_FILL).oneOf(); setPolicy(choice) } + action seedAt(choice: int): bool = all { + input' = { name: "seed", choice: choice }, + s' = Remote::seedAged(s, seedKey(choice), seedPayload(choice), seedAge(choice), SEED_TTL_MS) + } + action seed = { nondet choice = 0.to(KEYS * 6 - 1).oneOf(); seedAt(choice) } + action invalidateAt(choice: int): bool = all { + input' = { name: "invalidate", choice: choice }, + s' = Serving::invalidate(s, invalidatedEntity(choice), invalidationBuffer(choice)) + } + action invalidate = { nondet choice = 0.to(KEYS * 2 - 1).oneOf(); invalidateAt(choice) } + action closeContext(context: int): bool = all { + not(s.closed.nth(context)), + input' = { name: "closeScope", choice: context }, + s' = Serving::closeScope(s, LAYOUT, context) + } + action closeScope = { nondet choice = 0.to(SCOPES - 1).oneOf(); closeContext(choice) } + // A public point-in-time copy; it neither acquires nor releases work. + action inspectInstance(instance: int): bool = all { + 0.to(INSTANCES - 1).contains(instance), + input' = { name: "inspect", choice: instance }, + s' = Flights::inspectProcess(s, instance, KEYS) + } + action inspect = { nondet choice = 0.to(INSTANCES - 1).oneOf(); inspectInstance(choice) } + action rollbackWall = all { + input' = { name: "rollbackWall", choice: -1 }, + s' = Clock::shiftWall(s, -WALL_ROLLBACK_MS) + } + action readFaultWith(code: int): bool = all { input' = { name: "readFault", choice: code }, s' = { readFailed: code == 1, ...s } } + action loadFaultWith(code: int): bool = all { input' = { name: "loadFault", choice: code }, s' = { loadFailed: code == 1, ...s } } + action dumpFaultWith(code: int): bool = all { input' = { name: "dumpFault", choice: code }, s' = { dumpFailed: code == 1, ...s } } + action writeFaultWith(code: int): bool = all { input' = { name: "writeFault", choice: code }, s' = { writeFailed: code == 1, ...s } } + action readFault = { nondet choice = Set(0, 1).oneOf(); readFaultWith(choice) } + action loadFault = { nondet choice = Set(0, 1).oneOf(); loadFaultWith(choice) } + action dumpFault = { nondet choice = Set(0, 1).oneOf(); dumpFaultWith(choice) } + action writeFault = { nondet choice = Set(0, 1).oneOf(); writeFaultWith(choice) } + + action step = any { + beginCall, releaseRead, releaseLoad, + any { resolveLoader, rejectLoader }, + any { releaseDump, releaseWrite }, + any { advance, policy, seed, invalidate, closeScope, inspect }, + any { rollbackWall, any { readFault, loadFault, dumpFault, writeFault } } + } + + // Inspection invariants independently select pending shared sources from + // source ownership rather than the snapshot helper's registry walk. + val inspectionReceiptsCoverAdmissions = s.inspectionStarted.length() == s.sources.length() + and s.inspectionJoins.length() == s.o.calls.length() + val inspectionsDescribeLiveLeaders = input.name != "inspect" or { + val latest = s.d.inspections.nth(s.d.inspections.length() - 1) + val live = s.sources.indices().filter(i => s.sources.nth(i).shared and s.sources.nth(i).result == CALL_PENDING + and s.sources.nth(i).instance == input.choice) + latest.activeLeaders == live.size() + } + val inspectionsCountOnlyProcessFollowers = input.name != "inspect" or { + val latest = s.d.inspections.nth(s.d.inspections.length() - 1) + val followers = s.o.calls.indices().filter(c => s.o.calls.nth(c) == CALL_PENDING and s.inspectionJoins.nth(c) == "process" + and s.sources.nth(s.owners.nth(c)).instance == input.choice) + latest.activeFollowers == followers.size() + } + val inspectionAgeUsesTheOldestLiveMonotonicStart = input.name != "inspect" or { + val latest = s.d.inspections.nth(s.d.inspections.length() - 1) + val live = s.sources.indices().filter(i => s.sources.nth(i).shared and s.sources.nth(i).result == CALL_PENDING + and s.sources.nth(i).instance == input.choice) + if (live.size() == 0) latest.oldestLeaderAgeMs == -1 + else live.exists(i => latest.oldestLeaderAgeMs == s.now - s.inspectionStarted.nth(i)) + and live.forall(i => latest.oldestLeaderAgeMs >= s.now - s.inspectionStarted.nth(i)) + } + + // Invariants over the library records --------------------------------------- + def jobInstance(i: int): int = s.sources.nth(s.jobs.nth(i).source).instance + def jobKey(i: int): int = s.sources.nth(s.jobs.nth(i).source).key + def owned(source: int): bool = Flights::ownedBy(s, source).size() > 0 + // Carried from shadow-layers: the registry is bounded per instance and holds one job per identity. + val boundedJobRegistry = 0.to(INSTANCES - 1).forall(inst => + s.jobs.indices().filter(i => jobInstance(i) == inst).size() <= JOB_CAPACITY) + val oneJobPerIdentity = s.jobs.indices().forall(a => s.jobs.indices().forall(b => + a == b or jobInstance(a) != jobInstance(b) or jobKey(a) != jobKey(b))) + // Joint (shadow x layers): every job is dark and runs over a source a caller + // owns; every source is a caller's (no detached source) and probed as participating + // exactly when its caller's scope was open (a bypass source is not). + val darkJobsRunOverOwnedSources = s.jobs.indices().forall(i => s.jobs.nth(i).dark and owned(s.jobs.nth(i).source)) + val sourcesAreCallersOwn = s.o.loaders == s.sources.length() and s.o.sourceScopes.length() == s.sources.length() + and s.sources.indices().forall(i => owned(i) and (s.o.sourceScopes.nth(i) == (s.sourceLayers.nth(i) != Diagnostics::NO_LAYER))) + // Joint (shadow x remote_writes): callers never refill (the remote layer is + // off in every reply), so every held dump or write is a job's fill over an + // accepted source, carrying the authority the job captured (C18/C58). + val heldFillsBelongToJobs = s.dumps.indices().forall(i => + Shadow::holdsJob(s, s.dumps.nth(i).source) and accepted(s.sources.nth(s.dumps.nth(i).source).result)) + and s.writes.indices().forall(i => + Shadow::holdsJob(s, s.writes.nth(i).source) and accepted(s.sources.nth(s.writes.nth(i).source).result)) + // A job deadline ends the diagnostic result, not the raw work it owns. + // Every held effect still has its registration until the external release; + // the source-only dark wait is the exception and owns none of these effects. + val heldEffectsKeepTheirJob = s.reads.indices().forall(i => + s.jobs.indices().exists(j => s.jobs.nth(j).source == s.reads.nth(i).flight)) + and s.loads.indices().forall(i => + s.jobs.indices().exists(j => s.jobs.nth(j).source == s.loads.nth(i).flight)) + and s.dumps.indices().forall(i => + s.jobs.indices().exists(j => s.jobs.nth(j).source == s.dumps.nth(i).source)) + and s.writes.indices().forall(i => + s.jobs.indices().exists(j => s.jobs.nth(j).source == s.writes.nth(i).source)) + def captured(source: int): int = Remote::physicalRetention(s, Shadow::jobOf(s, source).ttls.retentionMs) + val fillsCarryCapturedAuthority = s.dumps.indices().forall(i => + not(Shadow::holdsJob(s, s.dumps.nth(i).source)) + or (s.dumps.nth(i).retentionMs == captured(s.dumps.nth(i).source) and s.dumps.nth(i).fence == Shadow::jobOf(s, s.dumps.nth(i).source).fence)) + and s.writes.indices().forall(i => + not(Shadow::holdsJob(s, s.writes.nth(i).source)) or s.writes.nth(i).retentionMs == captured(s.writes.nth(i).source)) + // Joint (shadow x invalidation x clock): a held write's stamp strictly + // cleared the fence its job's C0 read observed (the second fence check, + // C33/C34), compared here directly rather than through cache_rules.fenceAllows + // or the kernel's writeAllowed, so a fault in either is caught, not restated. + val fillsClearTheirFence = s.writes.indices().forall(i => { + val fence = Shadow::jobOf(s, s.writes.nth(i).source).fence + fence == NO_FENCE or s.writes.nth(i).stamp > fence + }) + // Joint (coalescing x instances, C11): every owner of a source asked for the + // identity the source serves, so no flight is joined across instances or keys + // (the layers idiom over the opt-in identity record). + val sourceOwnersShareTheSourcesIdentity = s.owners.indices().forall(c => { + val owner = s.owners.nth(c) + owner < 0 or (s.sources.nth(owner).instance == s.callerInstances.nth(c) and s.sources.nth(owner).key == s.callerKeys.nth(c)) + }) + // Joint (deadlines x shadow): every pending deadline is a live job's budget + // or a pending loader's source deadline; no read deadline is ever + // registered (a job's reads are bounded by its budget alone), and every + // registered job that has not timed out has exactly one budget. + val deadlinesBelongToPendingWork = s.deadlines.indices().forall(i => { + val due = s.deadlines.nth(i) + if (due.kind == Deadlines::JOB) Shadow::holdsJob(s, due.index) and not(Shadow::jobOf(s, due.index).timedOut) + else due.kind == Deadlines::SOURCE and due.index < s.loaders.length() and Deadlines::pending(s, due.index) + and s.sources.nth(s.loaders.nth(due.index)).result == CALL_PENDING + }) and s.jobs.indices().forall(i => s.jobs.nth(i).timedOut + or Deadlines::deadlinesOf(s, Deadlines::JOB, s.jobs.nth(i).source).length() == 1) + // Joint (deadlines x diagnostics x scope): every flight whose failure was + // recorded is attributed to its layer exactly once; a bypass source (a + // closed scope) is attributed to none. + val failedFlightsAreAttributedOnce = s.d.fallbackErrors.length() == s.sources.indices().filter(i => + s.sources.nth(i).result != CALL_PENDING and not(accepted(s.sources.nth(i).result)) and s.sourceLayers.nth(i) != Diagnostics::NO_LAYER).size() + // Joint (coalescing x request memo x local): the coalesced labels count the + // followers, a caller attached to a flight an earlier caller owns. + val coalescedLabelsCountFollowers = s.d.coalesced.length() == s.owners.indices().filter(c => + s.owners.nth(c) >= 0 and s.owners.indices().exists(e => e < c and s.owners.nth(e) == s.owners.nth(c))).size() + // Joint (local x shadow x source errors, C27): a live local entry is the + // accepted result of a caller's source for that identity that captured a + // local TTL; a dark fill never warms local storage and a rejected source + // never seeds it (the consequence the M37 partition never read). + val localEntriesHoldAcceptedOwnedValues = s.localValues.indices().forall(slot => + s.localValues.nth(slot) == NO_VALUE or s.sources.indices().exists(i => + s.sources.nth(i).instance * KEYS + s.sources.nth(i).key == slot and s.sources.nth(i).result == s.localValues.nth(slot) + and accepted(s.sources.nth(i).result) and s.sources.nth(i).localMs > 0)) + val memoHoldsAcceptedValues = s.memo.indices().forall(slot => s.memo.nth(slot) == NO_VALUE or accepted(s.memo.nth(slot))) + // Carried: a caller its source owns is pending or holds that source's result. + val callersOwnTheirResults = s.owners.indices().forall(c => { + val owner = s.owners.nth(c) + owner < 0 or s.o.calls.nth(c) == CALL_PENDING or s.o.calls.nth(c) == s.sources.nth(owner).result + }) + // Joint (clock x shadow read): a future offset is reported only after a wall rollback. + val futureOffsetsFollowRollbacks = s.d.futureOffsets.length() == 0 or s.skew < WALL_ORIGIN_MS + // The profile's own restrictions, executable: no mismatch logging and no + // malformed policy, so the channel's warnings and configuration errors stay zero. + val channelRestrictionsHold = s.d.warnings == 0 and s.d.configErrors == 0 + val agesRequireVerdicts = s.d.ages.length() == s.o.shadow.select(label => label == "match" or label == "mismatch").length() + // Every reply the decoder produces is one the library can be configured with (C10). + // One knob (C6): the job budget is the source budget, as the library derives it from the call's fallback timeout. + val budgetsAreOneKnob = shadowing(s.selected).budgetMs == s.sourceBudget + + // Both finite budgets are positive, and advances deliver due work before + // observation. A newly started source or job cannot inherit clock uptime. + val pendingDeadlinesAreFuture = s.deadlines.indices().forall(i => s.deadlines.nth(i).at > s.now) + + // Regressions: public input schedules --------------------------------------- + // A rejected dark source seeds neither local storage nor the request memo: + // the next caller of the same scope and key starts its own source and its + // own dark read (M25). M37's native publication site is outside this path. + run rejectedDarkSourceSeedsNoLayerTest = init + .then(setPolicy(DARK_ALL)).then(beginAt(0)).then(readAt(0)).then(rejectAt(0)) + .expect(s.o.calls == List(SOURCE_ERROR) and s.o.shadow == List("source_error") and s.d.fallbackErrors == List("local")) + .then(beginAt(0)) + .expect(s.o.calls == List(SOURCE_ERROR, CALL_PENDING) and s.o.loaders == 2 and s.o.reads == 2 and s.d.coalesced == List()) + + // A non-coalescing caller starts beside the registered leader and its dark + // job is dropped for the live identity; its local publication does not + // preempt a later coalescing caller's join; the leader completes both owners. + run uncoalescedPublicationDoesNotPreemptTheRegisteredLeaderTest = init + .then(setPolicy(DARK_ALL)).then(beginAt(0)) + .then(setPolicy(DARK_LOCAL_UNCOALESCED)).then(beginAt(2)) + .expect(s.o.calls == List(CALL_PENDING, CALL_PENDING) and s.o.loaders == 2 and s.o.reads == 1 and s.o.shadow == List("dropped")) + .then(resolveWith(4)) + .expect(s.o.calls == List(CALL_PENDING, VALUE_TWO) and s.localValues.nth(0) == VALUE_TWO) + .then(setPolicy(DARK_ALL)).then(beginAt(6)) + .expect(s.o.calls == List(CALL_PENDING, VALUE_TWO, CALL_PENDING) and s.o.loaders == 2 and s.d.coalesced == List("process")) + .then(resolveWith(1)) + .expect(s.o.calls == List(VALUE_ONE, VALUE_TWO, VALUE_ONE) and s.o.loaders == 2) + + // The dark fill on the held path carries the retention the job captured at + // admission, whatever the policy says when the source settles and when the + // dump is released (C18, the shadow-layers boundary on the held path). + run darkFillCarriesCapturedRetentionOnTheHeldPathTest = init + .then(setPolicy(DARK_NONE)).then(beginAt(0)).then(readAt(0)) + .then(setPolicy(DARK_ALL_CHANGED_FILL)).then(resolveWith(1)) + .expect(s.o.calls == List(VALUE_ONE) and s.o.dumps == 1 and s.o.writes == 0) + .then(releaseDumpAt(0)) + .expect(s.o.writeTtls == List(120000) and s.o.writes == 1) + .then(releaseWriteAt(0)) + .expect(s.o.shadow == List("filled") and s.remoteValues.nth(0) == VALUE_ONE) + + // A fill whose fence is the watermark raised at this very instant is fenced + // before serializing: the fence is strict (W04, cache_rules.fenceAllows). + run fillAtTheWatermarkInstantIsFencedTest = init + .then(invalidateAt(0)).then(setPolicy(DARK_NONE)).then(beginAt(0)).then(readAt(0)).then(resolveWith(1)) + .expect(s.o.calls == List(VALUE_ONE) and s.o.shadow == List("fill_fenced") and s.o.dumps == 0) + + // A frame stamped at the watermark's instant is fenced from the dark read + // (the strict fence on the read side, W04): the job acquires nothing and + // fills once the wall has moved; an inclusive read fence would acquire the + // frame as the C0 and decode instead of filling. + run frameAtTheWatermarkIsFencedFromTheDarkReadTest = init + .then(invalidateAt(0)).then(seedAt(0)).then(setPolicy(DARK_NONE)).then(beginAt(0)).then(readAt(0)) + .then(advanceBy(1)).then(resolveWith(2)) + .expect(s.o.calls == List(VALUE_TWO) and s.o.dumps == 1 and s.o.loads == 0) + + // A fill fenced at settlement is judged again at dump release: a wall + // rollback below the fence stops the write (C33/C34). + run fillFenceIsRejudgedAfterAWallRollbackTest = init + .then(invalidateAt(0)).then(advanceBy(1)).then(setPolicy(DARK_NONE)).then(beginAt(0)).then(readAt(0)).then(resolveWith(1)) + .expect(s.o.dumps == 1 and s.o.shadow == List()) + .then(rollbackWall).then(releaseDumpAt(0)) + .expect(s.o.shadow == List("fill_fenced") and s.o.writes == 0) + + // A visible frame the dark read declined as stale carries no fence (C58): a + // watermark raised with a future buffer after the read fences the fill's + // frame from readers but not the write itself. + run staleVisibleFrameDeclinedByTheDarkReadFillsUnfencedTest = init + .then(setPolicy(DARK_NONE)).then(seedAt(3)).then(beginAt(0)).then(readAt(0)) + .then(invalidateAt(1)).then(resolveWith(2)) + .expect(s.o.dumps == 1 and s.jobs.nth(0).fence == NO_FENCE) + .then(releaseDumpAt(0)).then(releaseWriteAt(0)) + .expect(s.o.writes == 1 and s.o.shadow == List("filled") and s.remoteValues.nth(0) == VALUE_TWO) + .then(beginAt(0)).then(readAt(1)) + .expect(s.o.reads == 2 and s.jobs.nth(0).cached == NO_VALUE and s.jobs.nth(0).fence == s.watermark.nth(0)) + .then(resolveWith(3)) + .expect(s.o.calls == List(VALUE_TWO, VALUE_ONE) and s.o.shadow == List("filled", "fill_fenced") and s.o.dumps == 1 and s.o.writes == 1) + + // A present C0 that fails to decode is never repaired: the held decode ends + // the job without a fill (C52 on the held dark path). + run undecodableDarkC0IsNeverRepairedTest = init + .then(setPolicy(DARK_NONE)).then(seedAt(4)).then(beginAt(0)).then(readAt(0)).then(resolveWith(2)) + .expect(s.o.calls == List(VALUE_TWO) and s.o.loads == 1 and s.o.dumps == 0) + .then(loadAt(0)) + .expect(s.o.shadow == List("deserialization_error") and s.o.dumps == 0 and s.o.reads == 1) + + // A frame of an unsupported encoding fails the dark read: the job ends at + // once and the caller's source still completes it. + run unsupportedEncodingFailsTheDarkReadTest = init + .then(setPolicy(DARK_NONE)).then(seedAt(5)).then(beginAt(0)).then(readAt(0)) + .expect(s.o.shadow == List("redis_error") and s.jobs == List()) + .then(resolveWith(1)) + .expect(s.o.calls == List(VALUE_ONE) and s.o.dumps == 0 and s.o.loads == 0) + + // The caller's source deadline and the job's budget are delivered in one + // round: the caller completes with a deadline error attributed to the + // local layer, the job times out once, its late read drains, and + // the deadline error is memoized nowhere. + run sourceDeadlineExpiresTheDarkJobAndAttributesToTheLocalLayerTest = init + .then(setPolicy(DARK_ALL)).then(beginAt(0)).then(advanceBy(SOURCE_BUDGET_MS)) + .expect(s.o.calls == List(DEADLINE_ERROR) and s.o.shadow == List("timeout") and s.d.fallbackErrors == List("local")) + .then(readAt(0)) + .expect(s.jobs == List() and s.o.shadow == List("timeout") and s.o.dumps == 0) + .then(beginAt(0)) + .expect(s.o.calls == List(DEADLINE_ERROR, CALL_PENDING) and s.o.loaders == 2 and s.d.coalesced == List()) + + // Earlier clock uptime belongs to neither budget. An immediately resolved + // later source publishes for another request scope while its dark read is + // still held; neither caller nor job should have already timed out. + run laterDarkSourceKeepsItsWholeBudgetTest = init + .then(advanceBy(SOURCE_BUDGET_MS)).then(setPolicy(DARK_ALL)).then(beginAt(0)) + .then(resolveWith(1)).then(beginAt(2)) + .expect(s.o.calls == List(VALUE_ONE, VALUE_ONE) and s.o.loaders == 1 and s.o.reads == 1 + and s.o.dumps == 0 and s.o.shadow == List() and s.d.fallbackErrors == List()) + + // Same-key callers on two instances keep separate flights and separate jobs + // (C11): the identity a flight is registered under includes the instance. + run sameKeyCallersOnTwoInstancesKeepSeparateFlightsTest = init + .then(setPolicy(DARK_ALL)).then(beginAt(0)).then(beginAt(4)) + .expect(s.o.calls == List(CALL_PENDING, CALL_PENDING) and s.o.loaders == 2 and s.o.reads == 2 and s.o.shadow == List() and s.d.coalesced == List()) + .then(resolveWith(2)) + .expect(s.o.calls == List(VALUE_TWO, CALL_PENDING) and s.localValues == List(VALUE_TWO, NO_VALUE, NO_VALUE, NO_VALUE)) + + // A caller in a closed scope bypasses every layer and admits no job (C01): + // no policy call, no read, an unbounded source probed as not participating. + run closedScopeCallerBypassesLayersAndAdmitsNoJobTest = init + .then(setPolicy(DARK_ALL)).then(closeContext(0)).then(beginAt(0)) + .expect(s.o.policyCalls == 0 and s.o.reads == 0 and s.o.shadow == List() and s.o.sourceScopes == List(false)) + .then(advanceBy(SOURCE_BUDGET_MS)) + .expect(s.o.calls == List(CALL_PENDING) and s.d.fallbackErrors == List()) + + // While a job holds its C0 read, another key's job on the full instance is + // dropped and a job on the other instance is admitted (C47/C54). + run fullInstanceDropsAnotherKeyWhileTheC0ReadIsHeldTest = init + .then(setPolicy(DARK_NONE)).then(beginAt(0)).then(beginAt(1)) + .expect(s.o.reads == 1 and s.o.shadow == List("dropped") and s.jobs.length() == 1) + .then(beginAt(4)) + .expect(s.o.reads == 2 and s.o.shadow == List("dropped") and s.jobs.length() == 2) + + // The caller is already complete when the job times out. A different key + // is dropped while its raw C0 read still holds the slot; releasing the read + // drains it without decode or fill, and a later competing job is admitted. + run timedOutDarkReadKeepsCapacityUntilRawReleaseTest = init + .then(setPolicy(DARK_NONE)).then(beginAt(0)).then(resolveWith(1)) + .then(advanceBy(SOURCE_BUDGET_MS)) + .expect(s.o.calls == List(VALUE_ONE) and s.o.shadow == List("timeout")) + .then(beginAt(1)) + .expect(s.o.calls == List(VALUE_ONE, CALL_PENDING) and s.o.reads == 1 and s.o.shadow == List("timeout", "dropped")) + .then(resolveWith(3)).then(readAt(0)) + .expect(s.o.calls == List(VALUE_ONE, VALUE_ONE) and s.o.loads == 0 and s.o.dumps == 0 and s.o.writes == 0 and s.o.shadow == List("timeout", "dropped")) + .then(beginAt(1)) + .expect(s.o.calls == List(VALUE_ONE, VALUE_ONE, CALL_PENDING) and s.o.loaders == 3 and s.o.reads == 2 and s.o.shadow == List("timeout", "dropped")) + + // The raw decode likewise retains capacity after the verdict timed out. + // Its late settlement must start no confirmation read before freeing it. + run timedOutDarkDecodeKeepsCapacityUntilRawReleaseTest = init + .then(setPolicy(DARK_NONE)).then(seedAt(0)).then(beginAt(0)).then(readAt(0)).then(resolveWith(2)) + .then(advanceBy(SOURCE_BUDGET_MS)) + .expect(s.o.calls == List(VALUE_TWO) and s.o.loads == 1 and s.o.shadow == List("timeout")) + .then(beginAt(1)) + .expect(s.o.calls == List(VALUE_TWO, CALL_PENDING) and s.o.reads == 1 and s.o.shadow == List("timeout", "dropped")) + .then(resolveWith(3)).then(loadAt(0)) + .expect(s.o.reads == 1 and s.o.dumps == 0 and s.o.writes == 0 and s.o.shadow == List("timeout", "dropped")) + .then(beginAt(1)) + .expect(s.o.calls == List(VALUE_TWO, VALUE_ONE, CALL_PENDING) and s.o.loaders == 3 and s.o.reads == 2 and s.o.shadow == List("timeout", "dropped")) + + // This is the whole-job deadline while C1 is held, distinct from a separate + // Redis read deadline. The latter has its own contract and fixture. + run timedOutDarkConfirmationKeepsCapacityUntilRawReleaseTest = init + .then(setPolicy(DARK_NONE)).then(seedAt(0)).then(beginAt(0)).then(readAt(0)).then(resolveWith(2)).then(loadAt(0)) + .then(advanceBy(SOURCE_BUDGET_MS)) + .expect(s.o.calls == List(VALUE_TWO) and s.o.reads == 2 and s.o.shadow == List("timeout")) + .then(beginAt(1)) + .expect(s.o.calls == List(VALUE_TWO, CALL_PENDING) and s.o.reads == 2 and s.o.shadow == List("timeout", "dropped")) + .then(resolveWith(3)).then(readAt(1)) + .expect(s.o.reads == 2 and s.o.dumps == 0 and s.o.writes == 0 and s.o.shadow == List("timeout", "dropped")) + .then(beginAt(1)) + .expect(s.o.calls == List(VALUE_TWO, VALUE_ONE, CALL_PENDING) and s.o.loaders == 3 and s.o.reads == 3 and s.o.shadow == List("timeout", "dropped")) + + // An accepted source cannot free the slot of its unfinished serialization. + // After timeout its raw completion frees capacity without dispatching SET. + run timedOutDarkDumpKeepsCapacityUntilRawReleaseTest = init + .then(setPolicy(DARK_NONE)).then(beginAt(0)).then(readAt(0)).then(resolveWith(1)) + .then(advanceBy(SOURCE_BUDGET_MS)) + .expect(s.o.calls == List(VALUE_ONE) and s.o.dumps == 1 and s.o.shadow == List("timeout")) + .then(beginAt(1)) + .expect(s.o.calls == List(VALUE_ONE, CALL_PENDING) and s.o.reads == 1 and s.o.shadow == List("timeout", "dropped")) + .then(resolveWith(3)).then(releaseDumpAt(0)) + .expect(s.o.writes == 0 and s.o.shadow == List("timeout", "dropped")) + .then(beginAt(1)) + .expect(s.o.calls == List(VALUE_ONE, VALUE_ONE, CALL_PENDING) and s.o.loaders == 3 and s.o.reads == 2 and s.o.shadow == List("timeout", "dropped")) + + // A dispatched write continues to own capacity after timeout. Its late SET + // can finish, but emits no second verdict and frees the slot for a new job. + run timedOutDarkWriteKeepsCapacityUntilRawReleaseTest = init + .then(setPolicy(DARK_NONE)).then(beginAt(0)).then(readAt(0)).then(resolveWith(1)).then(releaseDumpAt(0)) + .then(advanceBy(SOURCE_BUDGET_MS)) + .expect(s.o.calls == List(VALUE_ONE) and s.o.writes == 1 and s.o.shadow == List("timeout")) + .then(beginAt(1)) + .expect(s.o.calls == List(VALUE_ONE, CALL_PENDING) and s.o.reads == 1 and s.o.shadow == List("timeout", "dropped")) + .then(resolveWith(3)).then(releaseWriteAt(0)) + .expect(s.o.writes == 1 and s.o.shadow == List("timeout", "dropped")) + .then(beginAt(1)) + .expect(s.o.calls == List(VALUE_ONE, VALUE_ONE, CALL_PENDING) and s.o.loaders == 3 and s.o.reads == 2 and s.o.shadow == List("timeout", "dropped")) + + // A dump fault ends the fill without a write; the caller's result stands (C53). + run dumpFaultEndsTheFillWithoutReplacingTheCallerResultTest = init + .then(setPolicy(DARK_NONE)).then(beginAt(0)).then(readAt(0)).then(resolveWith(1)).then(dumpFaultWith(1)).then(releaseDumpAt(0)) + .expect(s.o.calls == List(VALUE_ONE) and s.o.shadow == List("fill_error") and s.o.writes == 0) + + // A local publication by the dark caller's source stops later shadow work + // for the key: the next caller is served locally, no read and no job. + run localPublicationStopsLaterDarkWorkTest = init + .then(setPolicy(DARK_LOCAL)).then(beginAt(0)).then(readAt(0)).then(resolveWith(1)) + .expect(s.o.calls == List(VALUE_ONE) and s.o.dumps == 1) + .then(beginAt(0)) + .expect(s.o.calls == List(VALUE_ONE, VALUE_ONE) and s.o.loaders == 1 and s.o.reads == 1 and s.o.shadow == List()) + + // The C0 frame's age is sampled at the verdict's wall clock: a frame + // acquired one millisecond inside freshness is confirmed a mismatch at the + // boundary and its age is the boundary (C51/C57). + run retainedC0AgeIsSampledAtTheVerdictTest = init + .then(setPolicy(DARK_NONE)).then(seedAt(2)).then(beginAt(0)).then(readAt(0)).then(advanceBy(1)).then(resolveWith(2)) + .expect(s.o.calls == List(VALUE_TWO) and s.o.loads == 1) + .then(loadAt(0)) + .expect(s.o.reads == 2 and s.o.shadow == List()) + .then(readAt(1)) + .expect(s.o.shadow == List("mismatch") and s.d.ages == List(60000) and s.o.dumps == 0) + + // A wall rollback makes a seeded frame future-dated: the dark read declines + // it, reports its offset on the shadow layer, and the fill overwrites it. + run rollbackMakesTheSeededFrameFutureAndReportsItsOffsetTest = init + .then(setPolicy(DARK_NONE)).then(seedAt(0)).then(rollbackWall).then(beginAt(0)).then(readAt(0)) + .expect(s.d.futureOffsets == List({ layer: "remote_shadow", offsetMs: WALL_ROLLBACK_MS }) and s.jobs.nth(0).cached == NO_VALUE and s.jobs.nth(0).fence == NO_FENCE) + .then(resolveWith(2)).then(releaseDumpAt(0)).then(releaseWriteAt(0)) + .expect(s.o.shadow == List("filled") and s.remoteValues.nth(0) == VALUE_TWO) + + // With no active shared layer, a transient request still attributes its + // failed source to request_local although it has no persistent memo slot. + run transientRequestOnlySourceFailureIsAttributedToRequestLayerTest = init + .then(setPolicy(DARK_REQUEST)).then(beginAt(6)).then(readAt(0)).then(rejectAt(0)) + .expect(s.o.calls == List(SOURCE_ERROR) and s.o.shadow == List("source_error") and s.d.fallbackErrors == List("request_local")) + + // Process followers exclude same-scope request followers. Resolving the + // oldest key changes the reported age to the remaining key; rejecting it + // then clears counts and the nullable age. + run inspectionTracksKeysFollowersAndOldestLeaderTest = init.then(setPolicy(DARK_ALL_NO_SHADOW)) + .then(beginAt(0)).then(beginAt(0)).then(beginAt(2)).then(beginAt(2)).then(advanceBy(1)) + .then(beginAt(1)).then(inspectInstance(0)) + .expect(s.d.inspections == List({ instance: 0, activeLeaders: 2, activeFollowers: 1, oldestLeaderAgeMs: 1 })) + .then(resolveWith(1)).then(inspectInstance(0)) + .expect(s.o.calls == List(VALUE_ONE, VALUE_ONE, VALUE_ONE, VALUE_ONE, CALL_PENDING) + and s.d.inspections.nth(1) == { instance: 0, activeLeaders: 1, activeFollowers: 0, oldestLeaderAgeMs: 0 }) + .then(advanceBy(1)).then(rejectAt(1)).then(inspectInstance(0)) + .expect(s.o.calls == List(VALUE_ONE, VALUE_ONE, VALUE_ONE, VALUE_ONE, SOURCE_ERROR) + and s.d.inspections.nth(2) == { instance: 0, activeLeaders: 0, activeFollowers: 0, oldestLeaderAgeMs: -1 }) + run inspectionIsPerInstanceAndUsesMonotonicTimeTest = init.then(setPolicy(DARK_ALL_NO_SHADOW)) + .then(beginAt(0)).then(advanceBy(1)).then(beginAt(4)).then(rollbackWall) + .then(inspectInstance(0)) + .expect(s.d.inspections == List({ instance: 0, activeLeaders: 1, activeFollowers: 0, oldestLeaderAgeMs: 1 })) + .then(inspectInstance(1)) + .expect(s.d.inspections == List( + { instance: 0, activeLeaders: 1, activeFollowers: 0, oldestLeaderAgeMs: 1 }, + { instance: 1, activeLeaders: 1, activeFollowers: 0, oldestLeaderAgeMs: 0 })) + .then(resolveWith(1)).then(inspectInstance(0)).then(inspectInstance(1)) + .expect(s.o.calls == List(VALUE_ONE, CALL_PENDING) + and s.d.inspections.nth(2) == { instance: 0, activeLeaders: 0, activeFollowers: 0, oldestLeaderAgeMs: -1 } + and s.d.inspections.nth(3) == { instance: 1, activeLeaders: 1, activeFollowers: 0, oldestLeaderAgeMs: 0 }) + run inspectionExcludesRequestOnlyAndUncoalescedWorkTest = init.then(setPolicy(DARK_REQUEST)) + .then(beginAt(0)).then(beginAt(0)).then(inspectInstance(0)) + .expect(s.o.calls == List(CALL_PENDING, CALL_PENDING) and s.o.loaders == 1 + and s.d.inspections == List({ instance: 0, activeLeaders: 0, activeFollowers: 0, oldestLeaderAgeMs: -1 })) + .then(setPolicy(DARK_LOCAL_UNCOALESCED)).then(beginAt(2)).then(beginAt(2)).then(inspectInstance(0)) + .expect(s.o.loaders == 3 and s.o.calls == List(CALL_PENDING, CALL_PENDING, CALL_PENDING, CALL_PENDING) + and s.d.inspections.nth(1) == { instance: 0, activeLeaders: 0, activeFollowers: 0, oldestLeaderAgeMs: -1 }) + run inspectionClearsTimedOutCallersWhileRawWorkRemainsTest = init.then(beginAt(0)).then(beginAt(2)) + .then(inspectInstance(0)) + .expect(s.d.inspections == List({ instance: 0, activeLeaders: 1, activeFollowers: 1, oldestLeaderAgeMs: 0 })) + .then(advanceBy(10)).then(inspectInstance(0)) + .expect(s.o.calls == List(DEADLINE_ERROR, DEADLINE_ERROR) and s.o.shadow == List("timeout") + and s.d.inspections.nth(1) == { instance: 0, activeLeaders: 0, activeFollowers: 0, oldestLeaderAgeMs: -1 }) + .then(beginAt(0)).then(inspectInstance(0)) + .expect(s.o.loaders == 2 and s.o.calls == List(DEADLINE_ERROR, DEADLINE_ERROR, CALL_PENDING) + and s.d.inspections.nth(2) == { instance: 0, activeLeaders: 1, activeFollowers: 0, oldestLeaderAgeMs: 0 }) + .then(resolveWith(1)).then(readAt(0)).then(inspectInstance(0)) + .expect(s.o.calls == List(DEADLINE_ERROR, DEADLINE_ERROR, CALL_PENDING) + and s.d.inspections.nth(3) == { instance: 0, activeLeaders: 1, activeFollowers: 0, oldestLeaderAgeMs: 0 }) + run inspectionExcludesUnfinishedShadowAfterCallerSuccessTest = init.then(beginAt(0)).then(readAt(0)) + .then(resolveWith(1)).then(inspectInstance(0)) + .expect(s.o.calls == List(VALUE_ONE) and s.o.dumps == 1 and s.o.writes == 0 and s.o.shadow == List() + and s.d.inspections == List({ instance: 0, activeLeaders: 0, activeFollowers: 0, oldestLeaderAgeMs: -1 })) + .then(releaseDumpAt(0)).then(inspectInstance(0)) + .expect(s.o.writes == 1 and s.o.shadow == List() + and s.d.inspections.nth(1) == { instance: 0, activeLeaders: 0, activeFollowers: 0, oldestLeaderAgeMs: -1 }) + +} diff --git a/formal/dialcache-effects-conformance.qnt b/formal/dialcache-effects-conformance.qnt index a6ef2dc0..5bf02b87 100644 --- a/formal/dialcache-effects-conformance.qnt +++ b/formal/dialcache-effects-conformance.qnt @@ -95,6 +95,9 @@ module dialcache_effects_conformance { readBudget: int, baseReadBudget: int } var s: State + // Immutable raw inputs from the latest successful adapter read. The invariant + // checks its diagnostic independently; no cache transition reads this receipt. + var frameReceipt: Metrics::FrameReadReceipt var input: { name: str, choice: int } // Initial fixture: nothing stored, the zero watermark, the fixture's read budget. @@ -115,6 +118,7 @@ module dialcache_effects_conformance { } action initialized(mode: int): bool = all { (FIXTURE_LIBRARY_BUDGET.to(FIXTURE_UNTRACKED)).contains(mode), + frameReceipt' = Metrics::NO_FRAME_READ_RECEIPT, input' = { name: "init", choice: mode }, s' = initial(mode) } action init = { @@ -133,6 +137,7 @@ module dialcache_effects_conformance { // Call admission: the read is dispatched under the budget in force, or the // caller joins the registered flight. -------------------------------------- action beginCall = all { + frameReceipt' = frameReceipt, input' = { name: "beginCall", choice: -1 }, s.o.calls.length() < MAX_CALLS, s' = Metrics::begin(s, LAYOUT, CALL, REMOTE_SHARED, TTLS, s.readBudget) @@ -142,6 +147,7 @@ module dialcache_effects_conformance { // write; the loader by its ordinal. ------------------------------------------- action readAt(index: int): bool = all { RemoteIO::holdsRead(s, index), + frameReceipt' = Metrics::frameReadReceipt(s, index), input' = { name: "releaseRead", choice: index }, s' = Metrics::settleRead(s, LAYOUT, index, false, Recovery::NONE, FUTURE_BUFFER_MS) } @@ -151,6 +157,7 @@ module dialcache_effects_conformance { } action failReadAt(index: int): bool = all { RemoteIO::holdsRead(s, index), + frameReceipt' = frameReceipt, input' = { name: "failRead", choice: index }, s' = Metrics::settleRead(s, LAYOUT, index, true, Recovery::NONE, FUTURE_BUFFER_MS) } @@ -159,11 +166,13 @@ module dialcache_effects_conformance { failReadAt(choice) } action releaseLoad = all { + frameReceipt' = frameReceipt, input' = { name: "releaseLoad", choice: -1 }, s.loads.length() > 0, s' = Metrics::settleLoad(s, LAYOUT, RemoteIO::latestLoad(s).load, false) } action failLoad = all { + frameReceipt' = frameReceipt, input' = { name: "failLoad", choice: -1 }, s.loads.length() > 0, s' = Metrics::settleLoad(s, LAYOUT, RemoteIO::latestLoad(s).load, true) @@ -171,6 +180,7 @@ module dialcache_effects_conformance { action resolveAt(index: int): bool = all { index < s.loaders.length(), Deadlines::pending(s, index), + frameReceipt' = frameReceipt, input' = { name: "resolveLoader", choice: index }, s' = Metrics::settleLoader(s, LAYOUT, index, VALUE_ONE, false) } @@ -181,6 +191,7 @@ module dialcache_effects_conformance { action rejectAt(index: int): bool = all { index < s.loaders.length(), Deadlines::pending(s, index), + frameReceipt' = frameReceipt, input' = { name: "rejectLoader", choice: index }, s' = Metrics::settleLoader(s, LAYOUT, index, SOURCE_ERROR, false) } @@ -189,21 +200,25 @@ module dialcache_effects_conformance { rejectAt(choice) } action releaseDump = all { + frameReceipt' = frameReceipt, input' = { name: "releaseDump", choice: -1 }, s.dumps.length() > 0, s' = Metrics::releaseDump(s, LAYOUT, Writes::latestDump(s).dump, false, PAYLOAD_BYTES) } action failDump = all { + frameReceipt' = frameReceipt, input' = { name: "failDump", choice: -1 }, s.dumps.length() > 0, s' = Metrics::releaseDump(s, LAYOUT, Writes::latestDump(s).dump, true, PAYLOAD_BYTES) } action releaseWrite = all { + frameReceipt' = frameReceipt, input' = { name: "releaseWrite", choice: -1 }, s.writes.length() > 0, s' = Metrics::releaseWrite(s, LAYOUT, Writes::latestWrite(s).write, false) } action failWrite = all { + frameReceipt' = frameReceipt, input' = { name: "failWrite", choice: -1 }, s.writes.length() > 0, s' = Metrics::releaseWrite(s, LAYOUT, Writes::latestWrite(s).write, true) @@ -211,28 +226,46 @@ module dialcache_effects_conformance { // Environmental clock, storage, policy, and failure inputs ------------------ action tick = all { + frameReceipt' = frameReceipt, input' = { name: "tick", choice: -1 }, s' = Metrics::advance(s, LAYOUT, CLOCK_STEP_MS) } // Clock observation without timer delivery also exercises settlement guards. action jumpClock = all { + frameReceipt' = frameReceipt, input' = { name: "jumpClock", choice: -1 }, tenMillisecondReadOrNone, s' = Clock::advance(s, CLOCK_STEP_MS) } action rollbackWall = all { + frameReceipt' = frameReceipt, input' = { name: "rollbackWall", choice: -1 }, s' = Clock::shiftWall(s, -WALL_ROLLBACK_MS) } + // Wall-only shifts reach logical frame expiry without spending the read or + // source's monotonic budget. Version 3 explicitly adds this finite domain. + action advanceWallBy(ms: int): bool = all { + Set(1, 59999, 60000).contains(ms), + frameReceipt' = frameReceipt, + input' = { name: "advanceWall", choice: ms }, + s' = Clock::shiftWall(s, ms) + } + action advanceWall = { + nondet choice = Set(1, 59999, 60000).oneOf() + advanceWallBy(choice) + } action seedRemote = all { + frameReceipt' = frameReceipt, input' = { name: "seedRemote", choice: -1 }, s' = Remote::seedFrame(s, KEY, VALUE_ONE, REMOTE_TTL_MS) } action invalidate = all { + frameReceipt' = frameReceipt, input' = { name: "invalidate", choice: -1 }, s' = Metrics::invalidate(s, ENTITY, 0) } action futureFence = all { + frameReceipt' = frameReceipt, input' = { name: "futureFence", choice: -1 }, s' = Metrics::invalidate(s, ENTITY, FUTURE_BUFFER_MS) } @@ -240,6 +273,7 @@ module dialcache_effects_conformance { // including abandoned reads. A second cannot be queued until the first is consumed. action replyWith(code: int): bool = all { (1.to(16)).contains(code), + frameReceipt' = frameReceipt, input' = { name: "adapterReply", choice: code }, not(Replies::queued(s)), s' = Replies::queue(s, code) @@ -251,11 +285,13 @@ module dialcache_effects_conformance { // Choice zero inherits the fixture's resolved base; 1..4 override it: two // wrappers guarded on the input alone. action inheritReadBudget = all { + frameReceipt' = frameReceipt, input' = { name: "readBudgetPolicy", choice: 0 }, s' = { readBudget: s.baseReadBudget, ...s } } action overrideReadBudgetWith(choice: int): bool = all { (1.to(4)).contains(choice), + frameReceipt' = frameReceipt, input' = { name: "readBudgetPolicy", choice: choice }, s' = { readBudget: runtimeBudgetOf(choice), ...s } } @@ -267,6 +303,7 @@ module dialcache_effects_conformance { // cache rule, so the model records the input and nothing else. action observerFaultWith(code: int): bool = all { (Set(0, 1)).contains(code), + frameReceipt' = frameReceipt, input' = { name: "observerFault", choice: code }, s' = s } action observerFault = { @@ -282,13 +319,23 @@ module dialcache_effects_conformance { // Favor the narrow second-fence check as well as unrestricted clock changes. all { s.dumps.length() > 0 and Writes::latestDump(s).fence > NO_FENCE, rollbackWall }, any { - tick, jumpClock, seedRemote, + tick, jumpClock, seedRemote, advanceWall, any { rollbackWall, invalidate, futureFence, observerFault, readBudgetPolicy, adapterReply } } } // Invariants: independent checks over the public channels and the library records. // Over the public channels, verbatim from the former text. + // A held, timely frame reply that reaches its freshness ceiling is an + // expired miss. This uses the acquired raw stamp and sampled clocks, not + // the miss-classification or freshness helper it challenges. + val staleFrameRepliesReportExpired = not(frameReceipt.frame and frameReceipt.owns + and frameReceipt.elapsed < frameReceipt.due + and frameReceipt.wall - frameReceipt.stamp >= frameReceipt.freshMs) or { + val misses = s.events.select(e => e.event == "miss") + misses.length() > frameReceipt.missesBefore + and misses.nth(frameReceipt.missesBefore).detail == "expired" + } val oneRequestPerRead = s.events.select(e => e.event == "request").length() == s.o.reads val measurementsAreNonnegative = s.events.select(e => e.amount < 0).length() == 0 val futureOffsetsArePositiveAtObservingLayer = s.events.indices().forall(i => { @@ -355,6 +402,22 @@ module dialcache_effects_conformance { run missDiscriminatorWinsOverFrameFieldsTest = tenMillisecondFixture.then(beginCall) .then(replyWith(Replies::MISS_WITH_FRAME_FIELDS)).then(readAt(0)) .expect(s.o.loads == 0 and s.o.loaders == 1) + // The same queued adapter bytes are fresh one millisecond before F, then + // expired at F. A wall shift alone leaves both source/read budgets intact. + run frameReplyAtLastFreshMillisecondHitsTest = tenMillisecondFixture + .then(replyWith(Replies::FRAME_WITH_MISS_FIELDS)).then(advanceWallBy(59999)) + .then(beginCall).then(readAt(0)).then(releaseLoad) + .expect(s.o.calls == List(VALUE_ONE) and s.o.loads == 1 and s.o.loaders == 0 + and s.o.dumps == 0 and s.o.writes == 0 and missReasons == List()) + run staleFrameReplyExpiresRefillsAndIsReadableTest = tenMillisecondFixture + .then(replyWith(Replies::FRAME_WITH_MISS_FIELDS)).then(advanceWallBy(60000)) + .then(beginCall).then(readAt(0)) + .expect(missReasons == List("expired") and s.o.loads == 0 and s.o.loaders == 1) + .then(resolveAt(0)).then(releaseDump).then(releaseWrite) + .then(beginCall).then(readAt(1)).then(releaseLoad) + .expect(s.o.calls == List(VALUE_ONE, VALUE_ONE) and s.o.loads == 1 and s.o.loaders == 1 + and s.o.dumps == 1 and s.o.writes == 1 and s.o.writeTtls == List(REMOTE_TTL_MS) + and missReasons == List("expired") and amounts("futureOffset") == List()) run frameWithoutMissDiscriminatorIgnoresMetadataTest = tenMillisecondFixture.then(beginCall) .then(replyWith(Replies::FRAME_WITH_MISS_FIELDS)).then(readAt(0)).then(releaseLoad) .expect(s.o.calls == List(VALUE_ONE) and s.o.loaders == 0) @@ -488,7 +551,9 @@ module dialcache_effects_conformance { // projections. This is the serving remote location; shadow locations are a // separate interaction, not implied by this assertion. run futureFrameReportsPositiveObservingOffsetTest = tenMillisecondFixture - .then(seedRemote).then(rollbackWall).then(beginCall).then(readAt(0)).then(rejectAt(0)) + .then(seedRemote).then(rollbackWall).then(beginCall).then(readAt(0)) + .expect(s.o.loads == 0 and s.o.loaders == 1) + .then(rejectAt(0)) .expect(s.o.calls == List(SOURCE_ERROR) and s.o.loads == 0 and s.o.loaders == 1 and s.events.select(e => e.event == "futureOffset") == List({ event: "futureOffset", location: "remote", detail: "", amount: 1000 })) @@ -590,4 +655,12 @@ module dialcache_effects_conformance { .expect(s.o.calls == List(VALUE_ONE, CALL_PENDING) and s.o.loaders == 2 and s.o.loads == 0 and s.o.dumps == 0 and s.o.writes == 0) .expect(missReasons == List("value_absent", "value_absent")) + + // Stop at the observable decision so both the clean and faulty implementation + // can complete this boundary history without a later fault-dependent command. + run fencedSourceSkipsSerializationTest = tenMillisecondFixture + .then(replyWith(Replies::ABSENT_WITH_FENCE)).then(beginCall).then(readAt(0)) + .expect(latestSource.fence == Clock::wallOf(s) + FUTURE_BUFFER_MS and latestSource.retentionMs > 0 and s.o.loaders == 1) + .then(resolveAt(0)) + .expect(s.o.calls == List(VALUE_ONE) and s.o.dumps == 0 and s.o.writes == 0) } diff --git a/formal/dialcache-envelope-vectors.qnt b/formal/dialcache-envelope-vectors.qnt index 8863cab9..feae8900 100644 --- a/formal/dialcache-envelope-vectors.qnt +++ b/formal/dialcache-envelope-vectors.qnt @@ -211,9 +211,12 @@ module dialcache_envelope_vectors { run escapedCompressedMarkerRemainsLiteralTest = init .then(execute({ command: "envelope", bytes: List(0,1,255), decoderSucceeds: true, decodedBytes: List(65), ...DEFAULT })) .expect(result.read.outcome == "passthrough" and result.read.bytes == List(1,255)) + run escapedBinaryMarkerRemainsLiteralTest = init + .then(execute(requestFor(8))) + .expect(result.read.bytes == List(2)) run unknownLegacyZeroPrefixRemainsUntouchedTest = init - .then(execute({ command: "envelope", bytes: List(0,3,255), ...DEFAULT })) - .expect(result.read.bytes == List(0,3,255)) + .then(execute(requestFor(9))) + .expect(result.read.bytes == List(0,3)) run corruptCompressionPreservesMarkedBytesTest = init .then(execute({ command: "decode", bytes: List(1,255), ...DEFAULT })) .expect(result.read.outcome == "fallback_raw" and result.read.bytes == List(1,255)) @@ -229,8 +232,11 @@ module dialcache_envelope_vectors { .then(execute({ command: "write", thresholdBytes: 64, ...writeFixture(11) })) .expect(result.originalBytes == 64 and result.typescript.outcome == "compressed" and result.go.outcome == "compressed") run compressionTieKeepsRawRepresentationTest = init - .then(execute({ command: "write", ...writeFixture(2) })) + .then(execute(requestFor(192))) .expect(result.typescript.outcome == "not_smaller" and result.typescript.storedBytes == 18) + run goCompressionTieKeepsRawRepresentationTest = init + .then(execute(requestFor(219))) + .expect(result.go.outcome == "not_smaller" and result.go.storedBytes == 19) run shrinkComparedWithEscapedRawBytesTest = init .then(execute({ command: "write", ...writeFixture(5) })) .expect(result.originalBytes == 18 and result.escaped.length() == 19 diff --git a/formal/dialcache-frame-vectors.qnt b/formal/dialcache-frame-vectors.qnt index e7780b9a..a3dc99fd 100644 --- a/formal/dialcache-frame-vectors.qnt +++ b/formal/dialcache-frame-vectors.qnt @@ -59,7 +59,7 @@ module dialcache_frame_vectors { val payload = request.bytes.slice(10, request.bytes.length()) if (request.tracked and timestamp == 0) { kind: "miss", reason: "unclassified", fence: fence, ...EMPTY } - else if (request.tracked and fence >= 0 and not(fenceAllows(timestamp, fence))) + else if (request.tracked and fence >= 0 and not(readFenceAllows(request.tracked, timestamp, fence))) { kind: "miss", reason: "watermark_fenced", fence: fence, ...EMPTY } else if (encoding != 0 and encoding != 1) { kind: "payload_encoding_error", ...EMPTY } else { kind: "hit", timestamp: timestamp, payloadType: if (encoding == 1) "binary" else "string", @@ -189,6 +189,9 @@ module dialcache_frame_vectors { run payloadUnpairedSurrogateUsesReplacementTest = init .then(execute({ command: "encode", units: List(55296,65), ...DEFAULT })) .expect(result.bytes == List(1,0,0,0,0,0,0,0,1,0,239,191,189,65)) + run frameAtWatermarkIsDeclinedTest = init + .then(execute(requestFor(546))) + .expect(result.kind == "miss" and result.reason == "watermark_fenced" and result.fence == 1) run trackedFencePrecedesEncodingErrorTest = init .then(execute({ command: "decode", tracked: true, watermarkPresent: true, watermark: List(49), bytes: framed(1,255,List(65)), ...DEFAULT })) diff --git a/formal/dialcache-independent-conformance.qnt b/formal/dialcache-independent-conformance.qnt index 8a92948f..f586a8c2 100644 --- a/formal/dialcache-independent-conformance.qnt +++ b/formal/dialcache-independent-conformance.qnt @@ -332,6 +332,13 @@ module dialcache_independent_conformance { .expect(s.o.calls == List(VALUE_ONE, VALUE_TWO) and s.o.recovery == List("served", "served") and s.o.reads == 2 and s.o.loaders == 2 and s.o.loads == 2 and s.o.dumps == 0 and s.o.writes == 0) + // This private receipt is stricter than native observability: even though + // later recovery rechecks would reject it, a frame already at its captured + // maximum must not become a retained candidate in the model. + run initiallyExpiredFrameIsNotRetainedTest = init.then(seedWith(3)).then(advanceBy(1)) + .then(beginCall).then(readAt(0)) + .expect(retainedSnapshotsWereEligible and s.retained.nth(0).candidate == NO_VALUE) + // A fresh hit retains nothing: when its decode fails, the loader it starts // cannot recover, and a rejection completes with the source error and the // loader's identity, unclassified. diff --git a/formal/dialcache-independent-connection.qnt b/formal/dialcache-independent-connection.qnt index 1b2b5552..29375554 100644 --- a/formal/dialcache-independent-connection.qnt +++ b/formal/dialcache-independent-connection.qnt @@ -76,15 +76,17 @@ module dialcache_independent_connection { if (owner >= 0) sourceSlots.replaceAt(owner, captureSource(owner, before.now, profile::SOURCE_BUDGET_MS)) else sourceSlots } else sourceSlots - action step = all { + action recordBefore = all { snapshots' = acquired, sources' = observedSources, before' = { frame: profile::s.remoteValues.nth(0), created: profile::s.created.nth(0), maximum: profile::s.ttls.retentionMs, wall: Clock::wallOf(profile::s), now: profile::s.now, calls: profile::s.o.calls.length(), loaderCount: profile::s.loaders.length(), reads: profile::s.reads, loads: profile::s.loads, - deadlines: profile::s.deadlines, owners: profile::s.owners }, - profile::step + deadlines: profile::s.deadlines, owners: profile::s.owners } } + action step = all { recordBefore, profile::step } + action admitCaller = all { recordBefore, profile::beginCall } + action releaseReadAt(index: int): bool = all { recordBefore, profile::readAt(index) } // Every held read that owns its flight carries the maximum its caller was // admitted with; every retained snapshot carries that maximum and, when it @@ -132,4 +134,11 @@ module dialcache_independent_connection { (profile::input.name != "resolveLoader" or profile::resolvedSource(profile::input.choice) != ordinal or not(accepted(source.result)) or sourceReturnAllowed(reference, owner, profile::s.now)) }) + + // Each raw read starts the source owned by its own admission. The unchanged + // source-origin property observes the second loader as unowned under the + // owners-table fault; neither native implementation uses that table. + run independentReadSourcesKeepDistinctOwnersTest = init + .then(admitCaller).then(admitCaller).then(releaseReadAt(0)).then(releaseReadAt(1)) + .expect(independentSourceOriginsMatchContract and profile::s.o.loaders == 2) } diff --git a/formal/dialcache-key-protocol.qnt b/formal/dialcache-key-protocol.qnt index db9c12b0..db92e9fc 100644 --- a/formal/dialcache-key-protocol.qnt +++ b/formal/dialcache-key-protocol.qnt @@ -344,6 +344,8 @@ module dialcache_key_protocol { .expect(s.key.logical == List(117,114,110,58,105,100,58,37,70,48,37,57,70,37,57,56,37,56,48,35,71,101,116)) run malformedUtf16RejectsInsteadOfReplacementTest = init .then(execute(inputFor(17 * 12 + 4))).expect(not(s.key.valid)) + run untrackedTypeBraceRemainsValidTest = init + .then(execute(inputFor(50))).expect(s.key.valid) run bracesDependOnEntityTrackingTest = init .then(execute(inputFor(4 * 12 + 4))).expect(s.key.valid) .then(execute(inputFor(4 * 12 + 5))).expect(not(s.key.valid)) diff --git a/formal/dialcache-layers-conformance.qnt b/formal/dialcache-layers-conformance.qnt index 854b9deb..81a10cb4 100644 --- a/formal/dialcache-layers-conformance.qnt +++ b/formal/dialcache-layers-conformance.qnt @@ -1,5 +1,6 @@ module dialcache_layers_conformance { import conformance_observations.* from "./conformance-observations" + import payloads as Payloads from "./kernel/payloads" import encodings.* from "./kernel/encodings" import calls.* from "./kernel/calls" import layer_policy.* from "./kernel/layer-policy" @@ -251,6 +252,9 @@ module dialcache_layers_conformance { (s.localValues.nth(i) > 0) == (s.lru.nth(i / KEYS_PER_INSTANCE).select(key => key == i % KEYS_PER_INSTANCE).length() > 0)) + // Atomic reads and shadow comparisons have no decode-failure settlement. + val atomicPathSeedsDecodableFrames = Payloads::atomicSafe(s) + // -- Deterministic regression schedules ------------------------------------ action untrackedFixture = initialize(FIXTURE_UNTRACKED) @@ -295,6 +299,14 @@ module dialcache_layers_conformance { .then(finishSource(0, VALUE_ONE)) .then(startCall(FRESH_FIRST_INSTANCE_CONTEXT * KEYS_PER_INSTANCE + KEY_A)) .expect(s.o.loaders == 2) + // The second caller must validate the tracked refill through Redis before + // its value is eligible for this process's local cache. + run trackedRefillIsReadAgainBeforeLocalReuseTest = trackedFixture + .then(startCall(FRESH_FIRST_INSTANCE_CONTEXT * KEYS_PER_INSTANCE + KEY_A)) + .then(finishSource(0, VALUE_ONE)) + .then(startCall(FRESH_FIRST_INSTANCE_CONTEXT * KEYS_PER_INSTANCE + KEY_A)) + .expect(s.o.calls == List(VALUE_ONE, VALUE_ONE) and s.o.reads == 2 and s.o.loaders == 1) + run trackedSourceWaitsForValidatedReadToWarmLocalTest = trackedFixture .then(startCall(FRESH_FIRST_INSTANCE_CONTEXT * KEYS_PER_INSTANCE + KEY_A)) .then(finishSource(0, VALUE_ONE)) diff --git a/formal/dialcache-local-clock-conformance.qnt b/formal/dialcache-local-clock-conformance.qnt index ed2f151e..616531e7 100644 --- a/formal/dialcache-local-clock-conformance.qnt +++ b/formal/dialcache-local-clock-conformance.qnt @@ -5,6 +5,7 @@ module dialcache_local_clock_conformance { import layer_policy.* from "./kernel/layer-policy" import serving as Serving from "./kernel/serving" import clock as Clock from "./kernel/clock" + import local_grid as LocalGrid from "./kernel/local-grid" import instances as Instances from "./kernel/instances" import policy_gate as Gate from "./kernel/policy-gate" @@ -16,9 +17,9 @@ module dialcache_local_clock_conformance { // one-second local TTL, and no request memo or remote layer bound this model. // The replay creates real default caches at the declared fractional times. // Every rule is a kernel transition (formal/kernel): the local projection of - // the traversal with an inline source (`Serving::callLocal`) over the - // fractional clock (`Clock::advanceTicks`). This module owns the bounded - // state, the instance decoding, the input choices and the guards. + // the traversal with an inline source and raw insertion receipt + // (`LocalGrid::call`) over the fractional clock (`Clock::advanceTicks`). This + // module owns the bounded state, instance decoding, input choices and guards. pure val TICKS_PER_MS = 1000 pure val LOCAL_TTL_MS = 1000 @@ -46,6 +47,7 @@ module dialcache_local_clock_conformance { processFlights: List[int], localValues: List[int], localExpires: List[int], + localInsertedTicks: List[int], lru: List[List[int]], sources: List[Serving::LocalSource], owners: List[int], @@ -72,6 +74,7 @@ module dialcache_local_clock_conformance { processFlights: List(NO_OWNER, NO_OWNER), localValues: List(NO_VALUE, NO_VALUE), localExpires: List(0, 0), + localInsertedTicks: List(0, 0), lru: List(List(), List()), sources: List(), owners: List(), @@ -103,7 +106,7 @@ module dialcache_local_clock_conformance { action callWith(instance: int, offered: int): bool = all { s.constructed.nth(instance), s.o.calls.length() < MAX_CALLS, - s' = Serving::callLocal(s, LAYOUT, callOn(instance), LOCAL, TTLS, offered), + s' = LocalGrid::call(s, LAYOUT, callOn(instance), LOCAL, TTLS, offered, TICKS_PER_MS), input' = { name: "call", choice: instance * 2 + offered - 1 } } action call = { diff --git a/formal/dialcache-local-failure-conformance.qnt b/formal/dialcache-local-failure-conformance.qnt index 2aed1b92..ba70fec5 100644 --- a/formal/dialcache-local-failure-conformance.qnt +++ b/formal/dialcache-local-failure-conformance.qnt @@ -1,5 +1,6 @@ module dialcache_local_failure_conformance { import conformance_observations.* from "./conformance-observations" + import payloads as Payloads from "./kernel/payloads" import encodings.* from "./kernel/encodings" import calls.* from "./kernel/calls" import layer_policy.* from "./kernel/layer-policy" @@ -15,7 +16,9 @@ module dialcache_local_failure_conformance { // is active at a time, with unbounded source time and no elapsed-time steps. // Native bindings inject failure at local storage/its integer-clock boundary. // A read fault is exercised on a present entry; a write fault is armed while - // source work is pending. Successful operations use the real native storage. + // source work is pending. Go's integer-clock seam is not consulted on a + // missing entry (go/cache.go), so the guards restrict read faults to present + // entries. Successful operations use the real native storage. // Every rule is a kernel transition (formal/kernel): the layered traversal // with the local fault switch read at admission and settlement // (`local_faults`). This module owns the bounded state, the scope and policy @@ -177,6 +180,11 @@ module dialcache_local_failure_conformance { s.memo.indices().forall(slot => s.memo.nth(slot) == NO_VALUE or accepted(s.memo.nth(slot))) } + // Atomic reads and shadow comparisons have no decode-failure settlement. + val atomicPathSeedsDecodableFrames = Payloads::atomicSafe(s) + // The newest-source decoder can name only one pending source. + val oneSourceAtATime = s.sources.select(source => source.result == CALL_PENDING).length() <= 1 + run localReadFailureFallsThroughAndPreservesOldLocalTest = init .then(begin(2)).then(resolve(VALUE_ONE)) .then(seeded(VALUE_TWO)).then(fault(1)).then(begin(0)) diff --git a/formal/dialcache-policy-conformance.qnt b/formal/dialcache-policy-conformance.qnt index 4bb3ce3b..d405b000 100644 --- a/formal/dialcache-policy-conformance.qnt +++ b/formal/dialcache-policy-conformance.qnt @@ -1,5 +1,6 @@ module dialcache_policy_conformance { import conformance_observations.* from "./conformance-observations" + import payloads as Payloads from "./kernel/payloads" import encodings.* from "./kernel/encodings" import calls.* from "./kernel/calls" import layer_policy.* from "./kernel/layer-policy" @@ -337,6 +338,9 @@ module dialcache_policy_conformance { error.layer == "noop" and error.errorType == "config_resolution" }) + // Atomic reads and shadow comparisons have no decode-failure settlement. + val atomicPathSeedsDecodableFrames = Payloads::atomicSafe(s) + // -- Deterministic regression schedules ------------------------------------ run invalidReadBudgetBypassesAllCachingTest = init.then(changePolicy(INVALID_READ_BUDGET)) @@ -487,6 +491,20 @@ module dialcache_policy_conformance { .then(startCall(0)).then(releasePolicy) .expect(s.o.calls == List(VALUE_ONE, SOURCE_ERROR, VALUE_ONE) and s.o.loaders == 2) + // The same preservation rule across a remote-chain failure: both sources + // start from a remote miss, with no stale-recovery classifier. After one + // publishes, the other's error cannot discard its local entry. A later + // remote fault makes a lost local entry observable as a pending source + // rather than a hit on the remote refill. + run independentFailureKeepsLocalValueWhenRemoteFailsTest = init + .then(changePolicy(INDEPENDENT_OFFSET)) + .then(startCall(0)).then(releasePolicy) + .then(startCall(0)).then(releasePolicy) + .then(finishSource(0, VALUE_ONE)).then(finishSource(1, SOURCE_ERROR)) + .then(setReadFault(true)) + .then(startCall(0)).then(releasePolicy) + .expect(s.o.calls == List(VALUE_ONE, SOURCE_ERROR, VALUE_ONE) and s.o.loaders == 2 and s.o.reads == 2) + run independentRemotePublicationUsesLastCompletionTest = init .then(changePolicy(INDEPENDENT_OFFSET + NO_LOCAL)) .then(startCall(0)).then(releasePolicy) @@ -640,4 +658,14 @@ module dialcache_policy_conformance { .expect(s.o.calls == List(VALUE_FALSE, VALUE_FALSE, VALUE_ZERO, VALUE_ZERO, VALUE_EMPTY_STRING, VALUE_EMPTY_STRING) and s.o.loaders == 3 and s.o.reads == 6 and s.o.loads == 3 and s.o.writes == 3 and s.localValues == List(NO_VALUE, NO_VALUE)) + + // Stop at the observable decision so both the clean and faulty implementation + // can complete this boundary history without a later fault-dependent command. + run nonCoalescingCallStartsSeparateSourceTest = init + .then(startCall(0)) + .then(releasePolicy) + .then(changePolicy(INDEPENDENT_OFFSET)) + .then(startCall(0)) + .then(releasePolicy) + .expect(s.o.calls == List(CALL_PENDING, CALL_PENDING) and s.o.loaders == 2 and s.o.reads == 2) } diff --git a/formal/dialcache-recovery-conformance.qnt b/formal/dialcache-recovery-conformance.qnt index f537b0d7..f81119f0 100644 --- a/formal/dialcache-recovery-conformance.qnt +++ b/formal/dialcache-recovery-conformance.qnt @@ -425,6 +425,13 @@ module dialcache_recovery_conformance { .then(beginWith(0)).then(rejectWith(0)).then(advanceBy(4000)).then(releaseLoad) .expect(s.o.calls == List(SOURCE_ERROR) and s.o.loads == 1 and s.o.recovery == List("miss") and s.d.ages == List() and s.o.writes == 0) + // Start strictly beyond freshness so a shared inclusive age fault cannot + // turn the initial read into a hit before the recovery maximum is tested. + run maximumAgeAfterDecodeFromStrictlyStaleFrameTest = initialize(1) + .then(advanceBy(1000)).then(beginWith(0)).then(rejectWith(0)) + .then(3.reps(_ => advanceBy(1000))).then(releaseLoad) + .expect(s.o.calls == List(SOURCE_ERROR) and s.o.loads == 1 + and s.o.recovery == List("miss") and s.d.ages == List() and s.o.writes == 0) run operationAllowReplacesInstanceDenialTest = initialize(2) .then(beginWith(0)).then(rejectWith(0)).then(releaseLoad) .expect(s.o.calls == List(VALUE_ONE) and s.o.classifications == 1 diff --git a/formal/dialcache-recovery-read-conformance.qnt b/formal/dialcache-recovery-read-conformance.qnt index 6e0923be..c9981259 100644 --- a/formal/dialcache-recovery-read-conformance.qnt +++ b/formal/dialcache-recovery-read-conformance.qnt @@ -518,7 +518,9 @@ module dialcache_recovery_read_conformance { // The write's equal timestamp is fenced for a subsequent independent read. run laterInvalidationDoesNotRewriteObservedMissFenceTest = initialized(TRACKED_MODE) .then(beginIn(0)).then(releaseRead).then(invalidate).then(resolveLoader) - .then(beginIn(1)).then(releaseRead).then(rejectLoader) + .then(beginIn(1)).then(releaseRead) + .expect(s.o.loads == 0 and s.o.loaders == 2) + .then(rejectLoader) .expect(s.o.calls == List(VALUE_TWO, SOURCE_ERROR) and s.io.sourceErrors == List(0, 2) and s.o.dumps == 1 and s.o.writes == 1 and s.o.loads == 0 and s.o.reads == 2 and s.o.recovery == List("miss")) diff --git a/formal/dialcache-redis-protocol.qnt b/formal/dialcache-redis-protocol.qnt index f7951b87..fe3bed2e 100644 --- a/formal/dialcache-redis-protocol.qnt +++ b/formal/dialcache-redis-protocol.qnt @@ -45,8 +45,8 @@ module dialcache_redis_protocol { UnclassifiedMiss else if (st.tracked and st.frameTimestamp == 0) UnclassifiedMiss - else if (st.tracked and st.watermarkReply == BufferReply and - st.watermarkState == ValidWatermark and not(fenceAllows(st.frameTimestamp, st.watermarkValue))) + else if (st.watermarkReply == BufferReply and st.watermarkState == ValidWatermark and + not(readFenceAllows(st.tracked, st.frameTimestamp, st.watermarkValue))) WatermarkFenced else if (st.encoding == UnsupportedEncoding) PayloadEncodingError diff --git a/formal/dialcache-runtime-boundaries-conformance.qnt b/formal/dialcache-runtime-boundaries-conformance.qnt index 9739074b..640d90b0 100644 --- a/formal/dialcache-runtime-boundaries-conformance.qnt +++ b/formal/dialcache-runtime-boundaries-conformance.qnt @@ -1,5 +1,7 @@ module dialcache_runtime_boundaries_conformance { + import cache_rules.* from "./cache-rules" import conformance_observations.* from "./conformance-observations" + import payloads as Payloads from "./kernel/payloads" import cohort_boundaries.* from "./cohort-boundaries" import encodings.* from "./kernel/encodings" import calls.* from "./kernel/calls" @@ -94,7 +96,7 @@ module dialcache_runtime_boundaries_conformance { request: layerOf(fixture) == REQUEST_LAYER, local: (layerOf(fixture) == LOCAL_LAYER and configured(fixture)) or fixture == ALL_FEATURES_FIXTURE, remote: (layerOf(fixture) == REMOTE_LAYER and configured(fixture)) or fixture == ALL_FEATURES_FIXTURE, - coalesce: fixture >= DEFAULT_SHARING_FIXTURE_OFFSET, + coalesce: fixture >= DEFAULT_SHARING_FIXTURE_OFFSET and libraryCoalescingDefault, disabled: fixture == DISABLED_BASELINE_FIXTURE } val allFeatures = s.baseline.request and s.baseline.local @@ -230,6 +232,9 @@ module dialcache_runtime_boundaries_conformance { val sourceEffectsMatch = s.sources.length() == s.o.loaders and s.owners.length() == s.o.calls.length() and s.held.length() <= 1 + // Atomic reads and shadow comparisons have no decode-failure settlement. + val atomicPathSeedsDecodableFrames = Payloads::atomicSafe(s) + // -- Deterministic regression schedules (verbatim) -------------------------- run runtimeLocalTtlAddsEarlierLayerToRemoteFixtureTest = initialize(REMOTE_LAYER) @@ -276,6 +281,13 @@ module dialcache_runtime_boundaries_conformance { .then(beginCall).then(releasePolicy) .expect(s.o.calls == List(VALUE_ONE, VALUE_TWO, VALUE_ONE) and s.o.loaders == 2) + // Stop before either independent source settles, so a wrong inherited + // sharing decision is visible without requiring a source it never started. + run inheritedFalseStartsBothSourcesBeforeSettlementTest = initialize(REQUEST_LAYER) + .then(beginCall).then(releasePolicy) + .then(beginCall).then(releasePolicy) + .expect(s.o.calls == List(CALL_PENDING, CALL_PENDING) and s.o.loaders == 2) + run localCohortEqualityBypassesBeforeAboveSampleCachesTest = initialize(LOCAL_LAYER) .then(changePolicy(AT_SAMPLE * 3 + ENABLE_SHARING)) .then(beginCall).then(releasePolicy).then(finishSource(0, 0)) @@ -284,6 +296,15 @@ module dialcache_runtime_boundaries_conformance { .then(beginCall).then(releasePolicy) .expect(s.o.calls == List(VALUE_ONE, VALUE_TWO, VALUE_TWO) and s.o.loaders == 2) + // A bypass at equality must leave no local value for the above-sample call. + // Its pending result exposes the difference before settling the new source. + run localCohortEqualityLeavesNoValueForLaterAdmissionTest = initialize(LOCAL_LAYER) + .then(changePolicy(AT_SAMPLE * 3 + ENABLE_SHARING)) + .then(beginCall).then(releasePolicy).then(finishSource(0, 0)) + .then(changePolicy(ABOVE_SAMPLE * 3 + ENABLE_SHARING)) + .then(beginCall).then(releasePolicy) + .expect(s.o.calls == List(VALUE_ONE, CALL_PENDING) and s.o.loaders == 2) + run remoteCohortEqualityBypassesBeforeAboveSampleCachesTest = initialize(REMOTE_LAYER) .then(changePolicy(AT_SAMPLE * 3 + ENABLE_SHARING)) .then(beginCall).then(releasePolicy).then(finishSource(0, 0)) diff --git a/formal/dialcache-runtime-policy.qnt b/formal/dialcache-runtime-policy.qnt index 2535d97b..361aacfe 100644 --- a/formal/dialcache-runtime-policy.qnt +++ b/formal/dialcache-runtime-policy.qnt @@ -1,4 +1,5 @@ module dialcache_runtime_policy { + import cache_rules.* from "./cache-rules" // Selected rules from C09, C16-C18 and C22: valid overlays and insertion policy. // Runtime/baseline records are environment inputs; captured* and snapshot // belong to an invocation. Entry timestamps are abstracted to insertion TTLs. @@ -69,7 +70,7 @@ module dialcache_runtime_policy { remoteTtl: remoteTtl, remoteRamp: effectiveRamp(runtime.remoteRamp, baseline.remoteRamp, remoteTtl), requestLocal: inherit(runtime.requestLocal, baseline.requestLocal, 0), - coalesce: inherit(runtime.coalesce, baseline.coalesce, 1), + coalesce: inherit(runtime.coalesce, baseline.coalesce, if (libraryCoalescingDefault) 1 else 0), recoveryAge: inherit(runtime.recoveryAge, baseline.recoveryAge, 0), shadowRamp: inherit(runtime.shadowRamp, baseline.shadowRamp, 0) } @@ -184,9 +185,7 @@ module dialcache_runtime_policy { s.snapshot.remoteRamp > 0, s' = { remoteEntryPresent: true, - remotePhysicalTtlAtInsert: - if (s.snapshot.recoveryAge > s.snapshot.remoteTtl) s.snapshot.recoveryAge - else s.snapshot.remoteTtl, + remotePhysicalTtlAtInsert: remoteRetentionFor(s.snapshot.remoteTtl, s.snapshot.recoveryAge), remoteInsertionTtl: s.snapshot.remoteTtl, remoteInsertionRecoveryAge: s.snapshot.recoveryAge, ...s diff --git a/formal/dialcache-shadow-conformance.qnt b/formal/dialcache-shadow-conformance.qnt index 02953639..0106a09e 100644 --- a/formal/dialcache-shadow-conformance.qnt +++ b/formal/dialcache-shadow-conformance.qnt @@ -349,6 +349,7 @@ module dialcache_shadow_conformance { // Invariants: independent checks over the public channels and the library records. // Over the public channels, verbatim from the former text. + val budgetsAreOneKnob = JOB_BUDGET_MS == s.sourceBudget val oneSourcePerCaller = s.o.loaders == s.o.calls.length() val writesHaveRetention = s.o.writes == s.o.writeTtls.length() val missingHookHasNoShadowEffects = s.hook or (s.o.reads == 0 and s.o.loads == 0 and s.o.dumps == 0 and s.o.writes == 0) @@ -621,7 +622,9 @@ module dialcache_shadow_conformance { .then(seedAt(1)).then(rollbackWall).then(beginCall).then(releaseRead) .expect(s.o.loads == 0 and s.o.dumps == 0 and s.d.futureOffsets == List({ layer: "remote_shadow", offsetMs: 1000 })) - .then(resolveAt(VALUE_TWO)).then(releaseDump).then(releaseWrite) + .then(resolveAt(VALUE_TWO)) + .expect(s.o.loads == 0 and s.o.dumps == 1) + .then(releaseDump).then(releaseWrite) .expect(s.o.calls == List(VALUE_TWO) and s.o.shadow == List("filled") and s.o.loads == 0 and s.o.writes == 1 and s.d.futureOffsets == List({ layer: "remote_shadow", offsetMs: 1000 })) @@ -634,4 +637,13 @@ module dialcache_shadow_conformance { .expect(s.o.calls == List(DEADLINE_ERROR) and s.o.shadow == List("timeout") and s.o.loads == 0 and s.o.dumps == 0 and s.o.writes == 0 and s.d.futureOffsets == List({ layer: "remote_shadow", offsetMs: 990 })) + + // Stop at the observable decision so both the clean and faulty implementation + // can complete this boundary history without a later fault-dependent command. + run expiredC0BeginsFillBeforePublicationTest = initialize(2) + .then(seedAt(1)).then(beginCall).then(releaseRead).then(resolveAt(VALUE_TWO)) + .then(releaseLoad).then(advanceWallBy(FRESHNESS_MS)).then(releaseRead) + .expect(s.o.shadow == List("mismatch") and s.o.loads == 1 and s.o.writes == 0) + .then(beginCall).then(releaseRead).then(resolveAt(VALUE_TWO)) + .expect(s.o.calls == List(VALUE_TWO, VALUE_TWO) and s.o.loads == 1 and s.o.dumps == 1) } diff --git a/formal/dialcache-shadow-layers-conformance.qnt b/formal/dialcache-shadow-layers-conformance.qnt index f7092736..e87cb992 100644 --- a/formal/dialcache-shadow-layers-conformance.qnt +++ b/formal/dialcache-shadow-layers-conformance.qnt @@ -1,6 +1,7 @@ module dialcache_shadow_layers_conformance { import cache_rules.* from "./cache-rules" import conformance_observations.* from "./conformance-observations" + import payloads as Payloads from "./kernel/payloads" import cohort_boundaries.* from "./cohort-boundaries" import encodings.* from "./kernel/encodings" import calls.* from "./kernel/calls" @@ -285,6 +286,9 @@ module dialcache_shadow_layers_conformance { val heldWorkHasAcceptedResults = s.dumps.indices().forall(i => accepted(s.sources.nth(s.dumps.nth(i).source).result)) and s.writes.indices().forall(i => accepted(s.sources.nth(s.writes.nth(i).source).result)) + // Atomic reads and shadow comparisons have no decode-failure settlement. + val atomicPathSeedsDecodableFrames = Payloads::atomicSafe(s) + // Each regression uses only public input transitions. Later calls distinguish // real storage reuse/publication from a counter or an internal phase alone. run darkSourcePublishesLocalBeforeShadowWriteTest = init @@ -476,4 +480,10 @@ module dialcache_shadow_layers_conformance { .then(setPolicy(DARK_NONE)).then(seedAt(0)).then(beginAt(9)).then(seedAt(1)).then(resolveAt(2)) .expect(s.o.calls == List(VALUE_TWO) and s.o.loads == 1 and s.o.reads == 2 and s.o.dumps == 0 and s.o.shadow == List("superseded")) + + // Stop at the observable decision so both the clean and faulty implementation + // can complete this boundary history without a later fault-dependent command. + run expiredC0StartsFillWithoutDecodingTest = init + .then(setPolicy(DARK_NONE)).then(seedAt(3)).then(beginAt(9)).then(resolveAt(2)) + .expect(s.o.calls == List(VALUE_TWO) and s.o.reads == 1 and s.o.loads == 0 and s.o.dumps == 1 and s.o.shadow == List()) } diff --git a/formal/dialcache-shadow-read-deadlines-conformance.qnt b/formal/dialcache-shadow-read-deadlines-conformance.qnt new file mode 100644 index 00000000..803bc30a --- /dev/null +++ b/formal/dialcache-shadow-read-deadlines-conformance.qnt @@ -0,0 +1,188 @@ +module dialcache_shadow_read_deadlines_conformance { + import conformance_observations.* from "./conformance-observations" + import encodings.* from "./kernel/encodings" + import calls.* from "./kernel/calls" + import layer_policy.* from "./kernel/layer-policy" + import serving as Serving from "./kernel/serving" + import remote_frames as Remote from "./kernel/remote-frames" + import remote_io as RemoteIO from "./kernel/remote-io" + import remote_writes as Writes from "./kernel/remote-writes" + import shadow as Shadow from "./kernel/shadow" + import shadow_reads as ShadowReads from "./kernel/shadow-reads" + import deadlines as Deadlines from "./kernel/deadlines" + import diagnostics as Diagnostics from "./kernel/diagnostics" + import recovery as Recovery from "./kernel/recovery" + import policy_gate as Gate from "./kernel/policy-gate" + // C54/C56: separate Redis-read and whole-job deadlines, raw read ownership, + // cancellation and captured read policy. Every call is dark: its source is + // caller-owned, while C0 and C1 are diagnostic reads. Two keys, one instance, + // one job slot, eight callers; 10 ms source/job budget and 5 or 20 ms reads. + // All external effects are held by ordinal. Time advances deliver timers; + // reads can ignore cancellation until the explicit raw release. No request + // or local cache, stale recovery, runtime invalidity or wall-clock shifts. + + pure val KEYS = 2 + pure val MAX_CALLS = 8 + pure val SOURCE_BUDGET = 10 + pure val TTL = 60000 + pure val LAYOUT: Serving::Layout = { + keysPerInstance: KEYS, keysPerScope: KEYS, persistentContexts: 0, + operationsPerEntity: 1, probeSourceScope: true, policyProvider: true + } + pure val DARK: Resolution = { layers: { request: false, local: false, remote: false }, coalesce: true } + pure val TTLS: Ttls = { localMs: 0, freshMs: TTL, retentionMs: TTL } + pure val SHADOW: Shadow::Shadowing = { hook: true, selected: true, capacity: 1, budgetMs: SOURCE_BUDGET, logging: false } + pure def call(key: int): Call = { instance: 0, key: key, context: 0, enabled: true, keyFailed: false } + + type Channel = { configErrors: int, warnings: int, ages: List[int], coalesced: List[str], fallbackErrors: List[str], futureOffsets: List[Diagnostics::FutureOffset] } + type State = { + o: Observation, d: Channel, io: RemoteIO::IO, + now: int, skew: int, remoteAvailable: bool, tracked: bool, capacity: int, + readFailed: bool, dumpFailed: bool, writeFailed: bool, readBudget: int, + closed: List[bool], memo: List[int], requestFlights: List[int], processFlights: List[int], + localValues: List[int], localExpires: List[int], lru: List[List[int]], + remoteValues: List[int], created: List[int], expires: List[int], watermark: List[int], + sources: List[Serving::LayeredSource], owners: List[int], memoSlots: List[int], held: List[Gate::Held], + reads: List[RemoteIO::HeldRead], loads: List[RemoteIO::HeldLoad], loaders: List[int], retained: List[Recovery::Snapshot], + sourceBudget: int, deadlines: List[Deadlines::Due], drained: List[bool], sourceLayers: List[str], + dumps: List[Writes::HeldDump], writes: List[Writes::HeldWrite], jobs: List[Shadow::Job], + expiredReads: List[int], capturedReadBudgets: List[ShadowReads::ReadBudget] + } + var s: State + var input: { name: str, choice: int } + action init = all { + input' = { name: "init", choice: -1 }, + s' = { + o: empty, d: { configErrors: 0, warnings: 0, ages: List(), coalesced: List(), fallbackErrors: List(), futureOffsets: List() }, io: RemoteIO::NO_IO, + now: 0, skew: 100000, remoteAvailable: true, tracked: true, capacity: 0, + readFailed: false, dumpFailed: false, writeFailed: false, readBudget: 5, + closed: List(), memo: List(), requestFlights: List(), processFlights: List(NO_OWNER, NO_OWNER), + localValues: List(), localExpires: List(), lru: List(List()), + remoteValues: List(NO_VALUE, NO_VALUE), created: List(0, 0), expires: List(0, 0), watermark: List(0, 0), + sources: List(), owners: List(), memoSlots: List(), held: List(), reads: List(), loads: List(), loaders: List(), retained: List(), + sourceBudget: SOURCE_BUDGET, deadlines: List(), drained: List(), sourceLayers: List(), + dumps: List(), writes: List(), jobs: List(), expiredReads: List(), capturedReadBudgets: List() + } + } + action beginAt(key: int): bool = all { + s.o.calls.length() < MAX_CALLS, + input' = { name: "beginCall", choice: key }, + s' = ShadowReads::begin(s, LAYOUT, call(key), DARK, TTLS, s.readBudget, SHADOW) + } + action beginCall = { nondet choice = Set(0, 1).oneOf(); beginAt(choice) } + action readAt(index: int): bool = all { + RemoteIO::holdsRead(s, index), input' = { name: "releaseRead", choice: index }, + s' = ShadowReads::releaseRead(s, LAYOUT, index, false) + } + action releaseRead = { nondet choice = s.reads.indices().map(i => s.reads.nth(i).read).oneOf(); readAt(choice) } + action loadAt(index: int): bool = all { + Shadow::holdsLoad(s, index), input' = { name: "releaseLoad", choice: index }, + s' = ShadowReads::releaseLoad(s, LAYOUT, index, false, Shadow::VALUE_COMPARISON) + } + action releaseLoad = { nondet choice = s.loads.indices().map(i => s.loads.nth(i).load).oneOf(); loadAt(choice) } + action resolveWith(code: int): bool = all { + (code - 1) / 2 < s.loaders.length(), Deadlines::pending(s, (code - 1) / 2), + input' = { name: "resolveLoader", choice: code }, + s' = Shadow::settleHeld(s, LAYOUT, (code - 1) / 2, (code - 1) % 2 + 1, false) + } + action resolveLoader = { nondet choice = 1.to(MAX_CALLS * 2).oneOf(); resolveWith(choice) } + action rejectAt(index: int): bool = all { + index < s.loaders.length(), Deadlines::pending(s, index), + input' = { name: "rejectLoader", choice: index }, + s' = Shadow::settleHeld(s, LAYOUT, index, SOURCE_ERROR, false) + } + action rejectLoader = { nondet choice = 0.to(MAX_CALLS - 1).oneOf(); rejectAt(choice) } + action dumpAt(index: int): bool = all { + Writes::holdsDump(s, index), input' = { name: "releaseDump", choice: index }, + s' = Shadow::releaseDump(s, LAYOUT, index, false) + } + action releaseDump = { nondet choice = s.dumps.indices().map(i => s.dumps.nth(i).dump).oneOf(); dumpAt(choice) } + action writeAt(index: int): bool = all { + Writes::holdsWrite(s, index), input' = { name: "releaseWrite", choice: index }, + s' = Shadow::releaseWrite(s, LAYOUT, index, false) + } + action releaseWrite = { nondet choice = s.writes.indices().map(i => s.writes.nth(i).write).oneOf(); writeAt(choice) } + action advanceBy(ms: int): bool = all { + s.now + ms <= 100, input' = { name: "advance", choice: ms }, + s' = ShadowReads::advance(s, LAYOUT, ms) + } + action advance = { nondet choice = Set(1, 4, 5, 10, 20).oneOf(); advanceBy(choice) } + action policyWith(budget: int): bool = all { input' = { name: "policy", choice: budget }, s' = { readBudget: budget, ...s } } + action policy = { nondet choice = Set(5, 20).oneOf(); policyWith(choice) } + action seedAt(code: int): bool = all { + input' = { name: "seed", choice: code }, s' = Remote::seedFrame(s, code / 2, code % 2 + 1, TTL) + } + action seed = { nondet choice = 0.to(3).oneOf(); seedAt(choice) } + action step = any { beginCall, releaseRead, releaseLoad, resolveLoader, rejectLoader, releaseDump, releaseWrite, advance, policy, seed } + + // Independent consequences over ownership, time and observations. + val capacityIsBounded = s.jobs.length() <= 1 + val rawReadsKeepTheirRegistration = s.reads.indices().forall(i => + s.jobs.indices().exists(j => s.jobs.nth(j).source == s.reads.nth(i).flight)) + val abortedReadsHaveEndedTheirResult = s.reads.indices().forall(i => + not(s.io.aborted.select(read => read == s.reads.nth(i).read).length() > 0) or s.expiredReads.select(read => read == s.reads.nth(i).read).length() > 0) + val endedReadsHaveNoJobTimer = s.expiredReads.indices().forall(i => { + val read = s.expiredReads.nth(i) + s.reads.indices().exists(j => s.reads.nth(j).read == read) + and not(s.deadlines.indices().exists(j => s.deadlines.nth(j).kind == Deadlines::JOB + and s.deadlines.nth(j).index == RemoteIO::heldRead(s, read).flight)) + }) + val liveReadDeadlinesAreOwned = s.deadlines.indices().forall(i => { + val due = s.deadlines.nth(i) + due.kind != ShadowReads::SHADOW_READ or ( + s.reads.indices().exists(j => s.reads.nth(j).read == due.index) and not(s.expiredReads.select(read => read == due.index).length() > 0)) + }) + val callerResultsBelongToTheirSources = s.owners.indices().forall(c => + s.o.calls.nth(c) == s.sources.nth(s.owners.nth(c)).result) + + run c0ReadDeadlineKeepsRawCapacityTest = init.then(beginAt(0)).then(resolveWith(1)).then(advanceBy(5)) + .expect(s.o.calls == List(VALUE_ONE) and s.o.shadow == List("redis_error") and s.io.aborted == List(0)) + .then(beginAt(1)) + .expect(s.o.reads == 1 and s.o.shadow == List("redis_error", "dropped")) + .then(resolveWith(3)).then(advanceBy(10)) + .expect(s.o.calls == List(VALUE_ONE, VALUE_ONE) and s.o.shadow == List("redis_error", "dropped")) + .then(readAt(0)).then(beginAt(1)) + .expect(s.o.reads == 2 and s.o.loaders == 3 and s.o.loads == 0 and s.o.dumps == 0 and s.o.shadow == List("redis_error", "dropped")) + + run c1ReadDeadlineKeepsRawCapacityTest = init.then(seedAt(0)).then(beginAt(0)).then(readAt(0)) + .expect(s.o.calls == List(CALL_PENDING) and s.o.shadow == List() and s.o.loads == 0 and s.o.dumps == 0) + .then(resolveWith(2)).then(loadAt(0)).then(advanceBy(5)) + .expect(s.o.calls == List(VALUE_TWO) and s.o.shadow == List("confirmation_error") and s.io.aborted == List(1)) + .then(beginAt(1)) + .expect(s.o.reads == 2 and s.o.shadow == List("confirmation_error", "dropped")) + .then(resolveWith(3)).then(advanceBy(10)) + .expect(s.o.shadow == List("confirmation_error", "dropped")) + .then(readAt(1)).then(beginAt(1)) + .expect(s.o.reads == 3 and s.o.loaders == 3 and s.o.dumps == 0 and s.o.shadow == List("confirmation_error", "dropped")) + + run jobDeadlineThenReadDeadlineReportsOneVerdictTest = init.then(policyWith(20)).then(beginAt(0)).then(resolveWith(1)).then(advanceBy(10)) + .expect(s.o.shadow == List("timeout") and s.io.aborted == List()) + .then(beginAt(1)) + .expect(s.o.reads == 1 and s.o.shadow == List("timeout", "dropped")) + .then(resolveWith(3)).then(advanceBy(10)) + .expect(s.o.calls == List(VALUE_ONE, VALUE_ONE) and s.io.aborted == List(0) and s.o.shadow == List("timeout", "dropped")) + .then(readAt(0)).then(beginAt(1)) + .expect(s.o.reads == 2 and s.o.loaders == 3 and s.o.shadow == List("timeout", "dropped")) + + run confirmationKeepsCapturedReadBudgetTest = init.then(seedAt(0)).then(beginAt(0)).then(readAt(0)) + .expect(s.o.calls == List(CALL_PENDING) and s.o.shadow == List() and s.o.loads == 0 and s.o.dumps == 0) + .then(policyWith(20)) + .then(resolveWith(2)).then(loadAt(0)) + .expect(s.io.budgets == List(5, 5)) + .then(advanceBy(5)) + .expect(s.o.shadow == List("confirmation_error") and s.io.aborted == List(1)) + .then(readAt(1)).then(beginAt(1)) + .expect(s.io.budgets == List(5, 5, 20) and s.o.reads == 3) + + run confirmationReadBudgetStartsAtItsOwnDispatchTest = init.then(seedAt(0)).then(beginAt(0)).then(readAt(0)) + .expect(s.o.calls == List(CALL_PENDING) and s.o.shadow == List() and s.o.loads == 0 and s.o.dumps == 0) + .then(advanceBy(4)) + .then(resolveWith(2)).then(loadAt(0)).then(advanceBy(4)) + .expect(s.o.calls == List(VALUE_TWO) and s.o.shadow == List() and s.io.aborted == List()) + .then(advanceBy(1)) + .expect(s.o.shadow == List("confirmation_error") and s.io.aborted == List(1)) + + run readBeforeDeadlineCanFillAndCancelsItsTimerTest = init.then(beginAt(0)).then(resolveWith(1)).then(advanceBy(4)).then(readAt(0)) + .then(dumpAt(0)).then(writeAt(0)).then(advanceBy(10)) + .expect(s.o.calls == List(VALUE_ONE) and s.o.shadow == List("filled") and s.o.writes == 1 and s.io.aborted == List()) +} diff --git a/formal/dialcache-source-connection.qnt b/formal/dialcache-source-connection.qnt index d8126879..47a9cae1 100644 --- a/formal/dialcache-source-connection.qnt +++ b/formal/dialcache-source-connection.qnt @@ -15,12 +15,16 @@ module dialcache_source_connection { var sources: List[SourceExecution] var before: { now: int, calls: int, held: List[Gate::Held], registered: int, budget: int } - action init = all { - profile::init, + action clearMonitor = all { sources' = List(), before' = { now: 0, calls: 0, held: List(), registered: -1, budget: 0 } } + action init = all { profile::init, clearMonitor } + // An exact fixture for the ownership receipt below; outside calls ignore + // this finite budget, but it removes initialization choice from the run. + action initFinite = all { profile::initialize(profile::FINITE_BUDGET_MODE), clearMonitor } + val startedOwner = if (profile::input.name == "releasePolicy") Gate::entry(before, profile::input.choice).caller else before.calls val startedOutside = profile::input.name == "beginCall" and @@ -29,12 +33,13 @@ module dialcache_source_connection { sources.append(captureSource(startedOwner, before.now, if (startedOutside) -1 else before.budget)) else sources - action step = all { + action recordBefore = all { sources' = observedSources, before' = { now: profile::s.now, calls: profile::s.o.calls.length(), - held: profile::s.held, registered: profile::s.processFlights.nth(0), budget: profile::s.sourceBudget }, - profile::step + held: profile::s.held, registered: profile::s.processFlights.nth(0), budget: profile::s.sourceBudget } } + action step = all { recordBefore, profile::step } + action beginOutside = all { recordBefore, profile::begin(profile::OUTSIDE) } val sourceOriginsMatchContract = observedSources.length() == profile::s.sources.length() and observedSources.indices().forall(index => { @@ -55,4 +60,11 @@ module dialcache_source_connection { }) val followerKeepsAcquiredOwner = profile::input.name != "releasePolicy" or before.registered < 0 or profile::s.owners.nth(Gate::entry(before, profile::input.choice).caller) == before.registered + + // The monitor records caller identity from the external admission count, + // independently of registerSource's owners table. Source one must belong + // to the second outside caller even though no sharing registry is used. + run outsideSourcesKeepDistinctOwnersTest = initFinite + .then(beginOutside).then(beginOutside) + .expect(sourceOwnersMatchContract and observedSources.length() == 2) } diff --git a/formal/dialcache-stale-recovery.qnt b/formal/dialcache-stale-recovery.qnt index e0408f3e..03088937 100644 --- a/formal/dialcache-stale-recovery.qnt +++ b/formal/dialcache-stale-recovery.qnt @@ -1,5 +1,6 @@ module dialcache_stale_recovery { import cache_rules.* from "./cache-rules" + import cache_contract as Contract from "./cache-contract" // Stale-on-error for one Redis frame and one caller/coalesced flight. // F is the fresh ceiling; M is the exclusive recovery ceiling. Recovery uses // retained bytes from the initial read and deliberately has no second read. @@ -64,7 +65,7 @@ module dialcache_stale_recovery { s.readHealthy and initialFrameUsable and retainedAgeAllowed(s.frameCreatedAt, s.now, FRESH_AGE, MAX_RECOVERY_AGE) val candidateWithinRecoveryAge = - recoveryAgeAllowed(s.candidateCreatedAt, s.now, MAX_RECOVERY_AGE) + retainedCandidateAgeAllowed(s.candidateCreatedAt, s.now, MAX_RECOVERY_AGE) // Initial state action init = s' = { @@ -169,7 +170,7 @@ module dialcache_stale_recovery { PlainMiss, readReceipt: { recorded: true, frameCreatedAt: s.frameCreatedAt, now: s.now }, candidateRetained: candidateAtInitialRead, - candidateCreatedAt: if (candidateAtInitialRead) s.frameCreatedAt else s.candidateCreatedAt, + candidateCreatedAt: if (candidateAtInitialRead) Contract::retainedFrameStamp(s.frameCreatedAt, s.now) else s.candidateCreatedAt, finalOutcome: if (freshAtInitialRead) ServedFresh else s.finalOutcome, ...s }, diff --git a/formal/differential.mjs b/formal/differential.mjs index 4de63dc9..6fc85883 100644 --- a/formal/differential.mjs +++ b/formal/differential.mjs @@ -13,6 +13,7 @@ import { CommandFailure, printGroup, resolveConcurrency, runPool, seconds, spawn import { parseTrace, profiles } from './replay/features.mjs'; import { localClockDescriptor } from './replay/local-clock.mjs'; import { effectsDescriptor } from './replay/effects.mjs'; +import { diffPaths } from './replay/divergence.mjs'; import { generationArguments } from './run-models.mjs'; // Corpus differential for a composed profile (#165). @@ -137,14 +138,9 @@ export function differentialPlan(referenceManifests, candidateManifests, profile // The step-by-step comparison. Inputs are compared first; then every asserted // channel of the step, naming the differing channel and field. -function differing(expected, actual, prefix = '') { - if (isDeepStrictEqual(expected, actual)) return []; - const composite = value => value !== null && typeof value === 'object'; - if (composite(expected) && composite(actual) && Array.isArray(expected) === Array.isArray(actual)) { - const keys = Array.isArray(expected) ? [...Array(Math.max(expected.length, actual.length)).keys()] : Object.keys({ ...expected, ...actual }).sort(); - return keys.flatMap(key => differing(expected[key], actual[key], `${prefix}${key}.`)); - } - return [`${prefix.slice(0, -1)} ${JSON.stringify(actual)} (reference ${JSON.stringify(expected)})`]; +function differing(expected, actual) { + const valueAt = (value, path) => path === '$' ? value : path.split('.').reduce((part, key) => part?.[key], value); + return diffPaths(expected, actual).map(path => `${path} ${JSON.stringify(valueAt(actual, path))} (reference ${JSON.stringify(valueAt(expected, path))})`); } export function compareHistory(reference, replayed) { const length = Math.min(reference.steps.length, replayed.steps.length); @@ -382,20 +378,34 @@ export function selectProfiles(prepared) { ...composedProfiles(prepared.candidate.manifests.execution, { cwd: prepared.candidate.tree })])].sort(); } -// The profiles one shard of a sharded run replays (--shard=/; -// DIFFERENTIAL_SHARD in the hosted lane, which runs four): the names sorted, -// then dealt round-robin (positions index-1, index-1+count, ...) rather than -// cut into contiguous slices, so alphabetical neighbours (recovery and -// recovery-read, shadow and shadow-layers) fall to different shards and, with -// four shards, the two heaviest profiles, effects and shadow, do not share one. -// Every name lands in exactly one shard; a shard past the number of profiles is -// empty. +// Advisory replay seconds, rounded from Actions run 35634905259. Its slowest +// alphabetical shard took 47 minutes while another took eight. Effects and the +// two new profiles were skipped there; estimate their cost from similarly sized +// corpora instead of assigning zero. New names get a middle-sized estimate. +// These weights only place whole profiles; they never select or drop histories. +const replaySeconds = { + admission: 170, 'dark-layers': 400, effects: 700, independent: 280, + layers: 450, 'local-clock': 80, 'local-failure': 70, policy: 260, + recovery: 500, 'recovery-read': 250, 'runtime-boundaries': 220, scope: 170, + shadow: 1300, 'shadow-layers': 350, 'shadow-read-deadlines': 100, 'source-budgets': 140, +}; +// Place expensive profiles first in the least-loaded shard. Sorted names break +// equal-cost ties; shard index breaks equal-load ties, independently of input +// order. Every name lands in exactly one shard, for any positive shard count. export function shardProfiles(names, index, count) { for (const [label, value] of [['index', index], ['count', count]]) { if (!Number.isSafeInteger(value) || value < 1) throw new Error(`Shard ${label} must be a positive integer; got ${String(value)}`); } if (index > count) throw new Error(`Shard index ${index} exceeds the shard count ${count}; use 1 <= index <= count`); - return [...names].sort().filter((_, position) => position % count === index - 1); + const cost = name => Object.hasOwn(replaySeconds, name) ? replaySeconds[name] : 250; + const ordered = [...new Set(names)].sort().sort((left, right) => cost(right) - cost(left)); + const shards = Array.from({ length: Math.min(count, ordered.length) }, () => ({ names: [], seconds: 0 })); + for (const name of ordered) { + const shard = shards.reduce((lightest, candidate) => candidate.seconds < lightest.seconds ? candidate : lightest); + shard.names.push(name); + shard.seconds += cost(name); + } + return shards[index - 1]?.names.sort() ?? []; } // A generation whose inputs are byte-identical in both revisions is the same @@ -440,7 +450,17 @@ export async function runDifferential(profileId, prepared, { chunk = defaultChun const started = performance.now(); const forwardReplay = await prepareReplay(candidateTree, candidateModel, descriptor, referenceHistories, { chunk, output: resolve(outputDirectory, 'forward') }); const reverseReplay = await prepareReplay(referenceTree, referenceModel, descriptor, candidateHistories, { chunk, output: resolve(outputDirectory, 'reverse') }); - const results = await runPool([...forwardReplay.tasks, ...reverseReplay.tasks], { concurrency }); + log(`${profileId}: ${forwardReplay.tasks.length} forward and ${reverseReplay.tasks.length} reverse replay batches, up to ${concurrency} concurrent processes.`); + const progress = (direction, tasks) => { + let completed = 0; + return tasks.map((task, index) => async () => { + const batchStarted = performance.now(); + const result = await task(); + log(`${profileId} ${direction}: ${++completed}/${tasks.length} batches complete; batch ${index + 1} replayed ${result.length} histories in ${seconds(performance.now() - batchStarted)} (${seconds(performance.now() - started)} elapsed).`); + return result; + }); + }; + const results = await runPool([...progress('forward', forwardReplay.tasks), ...progress('reverse', reverseReplay.tasks)], { concurrency }); const forward = forwardReplay.collect(results.slice(0, forwardReplay.tasks.length)); const reverse = reverseReplay.collect(results.slice(forwardReplay.tasks.length)); const replayMs = performance.now() - started; @@ -513,8 +533,8 @@ A profile whose import closure and generation settings are byte-identical in both revisions is reported as unchanged and not regenerated. --composed selects every profile that imports a kernel module in either revision; one the working tree no longer generates is reported as removed. --shard=/, with ---composed only, replays the index-th of count round-robin shards of those -profiles sorted by name (the hosted lane runs four). Only profiles with an +--composed only, replays the index-th of count shards balanced by estimated +profile replay time (the hosted lane runs four). Only profiles with an explicit-input driver descriptor (formal/replay/features.mjs, or the local-clock descriptor in formal/replay/local-clock.mjs) can be replayed. Reports: diff --git a/formal/execution.json b/formal/execution.json index e6a27737..0ad1daa3 100644 --- a/formal/execution.json +++ b/formal/execution.json @@ -107,7 +107,8 @@ "registeredReadOwnsFlight", "effectCountsMatchRecords", "writeRequiresAcceptedSource", - "acceptedSourcesSettledBeforeTheirDeadline" + "acceptedSourcesSettledBeforeTheirDeadline", + "staleFrameRepliesReportExpired" ], "profile": "effects", "differential": { @@ -158,7 +159,8 @@ "localHitsRespectInsertionExpiry", "remoteHitsRespectAcquiredFreshness", "remoteHitsWarmLocalForTheReplysLocalTtl", - "joinsOnlyJoinableFlights" + "joinsOnlyJoinableFlights", + "atomicPathSeedsDecodableFrames" ], "profile": "policy", "generate": { @@ -183,7 +185,8 @@ "budgetsBelongToPendingWork", "heldEffectsAreTheDrivers", "oneExecution", - "fillsClearTheirFence" + "fillsClearTheirFence", + "budgetsAreOneKnob" ], "profile": "shadow", "generate": { @@ -224,7 +227,9 @@ "jobsAreDiagnostic", "effectsMatchRecords", "selectionsAwaitTheirHit", - "jobBudgetsBelongToLiveJobs" + "jobBudgetsBelongToLiveJobs", + "elapsedStaysBelowReadDeadlines", + "pendingDeadlinesAreFuture" ], "profile": "admission", "differential": { @@ -248,7 +253,8 @@ "callsKeepSourceOutcome", "localMembershipMatchesLru", "absentRemoteHasNoAdapterEffects", - "sourceOwnershipNeverCrossesKeyOrInstance" + "sourceOwnershipNeverCrossesKeyOrInstance", + "atomicPathSeedsDecodableFrames" ], "profile": "layers", "generate": { @@ -304,7 +310,9 @@ "faultedReadNeverUsesLocal", "faultedSettlementPublishesNothingLocally", "callsKeepSourceOutcome", - "storageHoldsAcceptedValuesOnly" + "storageHoldsAcceptedValuesOnly", + "atomicPathSeedsDecodableFrames", + "oneSourceAtATime" ], "differential": { "maxBytesPerStateRatio": 1.45 @@ -325,7 +333,8 @@ "invalidBooleanLeavesBypassAllCaching", "callersKeepTheirSourceValue", "retainedValueFollowsSettlementOrder", - "sourceEffectsMatch" + "sourceEffectsMatch", + "atomicPathSeedsDecodableFrames" ], "generate": { "maxSamples": 1024, @@ -349,7 +358,8 @@ "unboundedCallerKeepsPendingSource", "detachedSourcesCarryNoAuthority", "jobBudgetsBelongToLiveJobs", - "heldWorkHasAcceptedResults" + "heldWorkHasAcceptedResults", + "atomicPathSeedsDecodableFrames" ], "generate": { "maxSamples": 1024, @@ -546,6 +556,60 @@ "retainedC0WasFreshWhenAcquired", "localPublicationBelongsToCaller" ] + }, + { + "path": "formal/dialcache-dark-layers-conformance.qnt", + "profile": "dark-layers", + "invariants": [ + "boundedJobRegistry", + "oneJobPerIdentity", + "darkJobsRunOverOwnedSources", + "sourcesAreCallersOwn", + "heldFillsBelongToJobs", + "fillsCarryCapturedAuthority", + "fillsClearTheirFence", + "sourceOwnersShareTheSourcesIdentity", + "deadlinesBelongToPendingWork", + "failedFlightsAreAttributedOnce", + "coalescedLabelsCountFollowers", + "localEntriesHoldAcceptedOwnedValues", + "memoHoldsAcceptedValues", + "callersOwnTheirResults", + "futureOffsetsFollowRollbacks", + "channelRestrictionsHold", + "agesRequireVerdicts", + "budgetsAreOneKnob", + "pendingDeadlinesAreFuture", + "heldEffectsKeepTheirJob", + "inspectionReceiptsCoverAdmissions", + "inspectionsDescribeLiveLeaders", + "inspectionsCountOnlyProcessFollowers", + "inspectionAgeUsesTheOldestLiveMonotonicStart" + ], + "generate": { + "maxSamples": 1024, + "maxSteps": 60, + "traces": 256, + "outputDirectory": ".formal-traces/features/dark-layers" + } + }, + { + "path": "formal/dialcache-shadow-read-deadlines-conformance.qnt", + "profile": "shadow-read-deadlines", + "invariants": [ + "capacityIsBounded", + "rawReadsKeepTheirRegistration", + "abortedReadsHaveEndedTheirResult", + "endedReadsHaveNoJobTimer", + "liveReadDeadlinesAreOwned", + "callerResultsBelongToTheirSources" + ], + "generate": { + "maxSamples": 1024, + "maxSteps": 40, + "traces": 128, + "outputDirectory": ".formal-traces/features/shadow-read-deadlines" + } } ], "symbolic": { @@ -569,6 +633,34 @@ "kind": "mapped", "mutant": "M01", "text": "M01 is the observable native site of cache-rules.qnt ageBeforeCeiling becoming inclusive, as measured by dialcache-rule-checks.qnt recoveryBoundaryIsExclusive: the post-decode serving check in both ports accepts a retained candidate at exactly the maximum age. Earlier ceiling sites are masked by the post-decode recheck because ages only grow, so this is the one single-site inclusive edit; recovery/maximumAgeAfterDecodePreservesSourceTest already detects it in both ports." + }, + "reproducer": { + "kind": "exported-regression", + "run": "maximumAgeAfterDecodeFromStrictlyStaleFrameTest", + "model": "formal/dialcache-recovery-conformance.qnt", + "failure": "s.o.calls == List(SOURCE_ERROR) and s.o.loads == 1 and s.o.recovery == List(\"miss\") and s.d.ages == List() and s.o.writes == 0", + "family": "inclusive-maximum-boundary", + "profiles": [ + "formal/dialcache-rule-checks.qnt", + "recovery", + "policy", + "independent", + "recovery-read", + "shadow-layers", + "shadow", + "dark-layers", + "effects" + ], + "exclusions": { + "scope": "The request-only projection has no frames and no clock, so no age judgment is evaluated.", + "source-budgets": "The local projection has no remote frames; local expiry is localEntryLiveAt, not ageBeforeCeiling.", + "layers": "Frames are seeded at age 0 under a 60 s freshness and the clock moves 1 ms per tick, so no history reaches an age boundary.", + "admission": "Composes the remote freshness judgment, but every frame is stamped at a wall clock that only advances and is read within 600 ms under a 60 s freshness, so no read meets an age boundary or observes a future stamp.", + "local-failure": "Composes the remote freshness judgment (ageBeforeCeiling), but its wall clock never moves and every frame is stamped at the current instant, so a frame's age is always 0 and never meets a ceiling (re-measured 2026-09-20 over the composed text: every scheduled invariant and regression passes under the mutant).", + "local-clock": "Composes the local projection with no remote layer; its closure reaches cache-rules through wholeMs and localEntryLiveAt only, so ageBeforeCeiling is never evaluated (re-measured 2026-09-20 over the composed text).", + "runtime-boundaries": "No scheduled history reaches an exact age boundary: every regression passes under the mutant.", + "shadow-read-deadlines": "All six exported read-deadline histories and all six scheduled invariants at 2000 x 40 pass under this fault; this profile does not exercise the affected recovery or settled-flight reuse behavior." + } } }, { @@ -584,6 +676,34 @@ "mutant": "M40", "text": "M40 is the native twin of cache-rules.qnt ageBeforeCeiling losing its created-before-observed clause, as measured by dialcache-rule-checks.qnt recoveryBoundaryIsExclusive: both ports classify a frame dated after the reader clock as a valid frame of age zero instead of rejecting it as future. The future clause is implemented once and shared by serving, recovery and shadow reads, so a recovery-only edit is not expressible and the shared edit is the faithful twin.", "crossContract": "M40 sits on C40.future-rejected because the shared frameAge future clause is the cache-rules ageBeforeCeiling rule; C45's replay rollbackDuringRetainedDecodeRejectsFutureSnapshotTest and witness rollback-rejects-retained-future detect the same edit on the recovery path." + }, + "reproducer": { + "kind": "exported-regression", + "run": "wallRollbackRejectsFutureRemoteFrameTest", + "model": "formal/dialcache-policy-conformance.qnt", + "failure": "s.o.calls == List(VALUE_ONE, CALL_PENDING) and s.o.loaders == 2", + "family": "future-timestamp-accepted", + "profiles": [ + "formal/dialcache-rule-checks.qnt", + "recovery", + "policy", + "shadow", + "recovery-read", + "effects", + "dark-layers" + ], + "exclusions": { + "scope": "Evaluates no age judgment from cache-rules, so ageBeforeCeiling is unreachable.", + "admission": "Composes the remote freshness judgment, but every frame is stamped at a wall clock that only advances and is read within 600 ms under a 60 s freshness, so no read meets an age boundary or observes a future stamp.", + "layers": "Frames are stamped from a wall clock that only moves with the monotonic clock (no rollback input), so no history observes a future stamp.", + "independent": "Seeds stamp frames at the wall minus a non-negative age and the clock only advances, so no history observes a future stamp.", + "local-failure": "Composes the remote freshness judgment (ageBeforeCeiling through freshAgeAllowed), but its wall clock never moves and every frame is stamped at the current instant, so no history observes a future stamp.", + "runtime-boundaries": "Frames are stamped from a wall clock that only moves with the monotonic clock (no rollback input), so no history observes a future stamp.", + "shadow-layers": "Frames are stamped with the wall clock at write release (remote_writes.dispatch) or seeded at a non-negative age; the skew is 0 and never shifts, so no history observes a future stamp.", + "local-clock": "Composes the local projection with no remote layer; its closure reaches cache-rules through wholeMs and localEntryLiveAt only, so ageBeforeCeiling is never evaluated.", + "source-budgets": "Uses only localEntryLiveAt and sourceAcceptsAt from cache-rules; no age judgment is evaluated.", + "shadow-read-deadlines": "All six exported read-deadline histories and all six scheduled invariants at 2000 x 40 pass under this fault; this profile does not exercise the affected recovery or settled-flight reuse behavior." + } } }, { @@ -597,9 +717,9 @@ "measures": "Repeats the recovery-inclusive-maximum fault against the recovery connection model: the same shared-rule mutation must also be caught by that monitor's own clause.", "reproducer": { "kind": "exported-regression", - "run": "maximumAgeBeforeDecodePreservesSourceTest", + "run": "maximumAgeAfterDecodeFromStrictlyStaleFrameTest", "model": "formal/dialcache-recovery-conformance.qnt", - "failure": "s.o.calls == List(SOURCE_ERROR) and s.o.loads == 0 and s.o.reads == 1 and s.o.recovery == List(\"miss\")", + "failure": "s.o.calls == List(SOURCE_ERROR) and s.o.loads == 1 and s.o.recovery == List(\"miss\") and s.d.ages == List() and s.o.writes == 0", "family": "inclusive-maximum-boundary", "profiles": [ "formal/dialcache-recovery-connection.qnt", @@ -608,24 +728,25 @@ "independent", "recovery-read", "shadow-layers", - "shadow" + "shadow", + "dark-layers", + "effects" ], "exclusions": { "scope": "The request-only projection has no frames and no clock, so no age judgment is evaluated.", "source-budgets": "The local projection has no remote frames; local expiry is localEntryLiveAt, not ageBeforeCeiling.", "layers": "Frames are seeded at age 0 under a 60 s freshness and the clock moves 1 ms per tick, so no history reaches an age boundary.", - "core": "No scheduled history reaches an exact age boundary: every regression passes under the mutant (measured 2026-09-20).", - "effects": "Frames are stamped at the wall clock and read within 600 ms under a 60 s freshness, and classifier NONE retains no snapshot, so no age judgment meets a boundary (measured 2026-09-20 over the composed text: every scheduled invariant and regression passes under the mutant).", "admission": "Composes the remote freshness judgment, but every frame is stamped at a wall clock that only advances and is read within 600 ms under a 60 s freshness, so no read meets an age boundary or observes a future stamp.", "local-failure": "Composes the remote freshness judgment (ageBeforeCeiling), but its wall clock never moves and every frame is stamped at the current instant, so a frame's age is always 0 and never meets a ceiling (re-measured 2026-09-20 over the composed text: every scheduled invariant and regression passes under the mutant).", "local-clock": "Composes the local projection with no remote layer; its closure reaches cache-rules through wholeMs and localEntryLiveAt only, so ageBeforeCeiling is never evaluated (re-measured 2026-09-20 over the composed text).", - "runtime-boundaries": "No scheduled history reaches an exact age boundary: every regression passes under the mutant (measured 2026-09-20)." + "runtime-boundaries": "No scheduled history reaches an exact age boundary: every regression passes under the mutant.", + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." } }, "nativeMutants": { "kind": "mapped", "mutant": "M01", - "text": "Same native fault as recovery-inclusive-maximum: M01 makes the post-decode serving maximum inclusive in both ports, which is cache-rules.qnt ageBeforeCeiling becoming inclusive as measured here by dialcache-recovery-connection.qnt recoveredValueWasWithinAcceptedAge over the snapshot a served recovery decode settles from. recovery/maximumAgeAfterDecodePreservesSourceTest and witness recovery:exact-maximum-after-decode-rejects already detect it in both ports." + "text": "Same native fault as recovery-inclusive-maximum: M01 makes the post-decode serving maximum inclusive in both ports, which is cache-rules.qnt ageBeforeCeiling becoming inclusive as measured here by dialcache-recovery-connection.qnt recoveredValueWasWithinAcceptedAge over the snapshot a served recovery decode settles from." } }, { @@ -645,11 +766,9 @@ "policy", "source-budgets", "recovery-read", - "shadow-layers", - "local-clock" + "shadow-layers" ], "exclusions": { - "core": "Does not import the kernel library, so the mutated local storage is never executed; its own local expiry is stated in its own text.", "effects": "The local layer is off (capacity 0 and no local TTL), so the mutated local lookup never serves an entry.", "recovery": "The local layer is off: capacity 0 and no local TTL, so local expiry is never judged.", "shadow": "Composes local storage, but every reply leaves the local layer off and the capacity is 0, so no local entry is ever written or read.", @@ -658,13 +777,16 @@ "local-failure": "Composes local storage, but its clock never advances (now stays 0 against a 60 s local TTL), so no read meets an entry at its expiry instant.", "scope": "Composes the request-only projection, which never reads local storage.", "layers": "Composes local storage, but its 60 s entries never reach their insertion expiry within the bound of 1 ms ticks.", - "runtime-boundaries": "Composes local storage, but no input advances the clock, so no entry reaches its expiry." + "runtime-boundaries": "Composes local storage, but no input advances the clock, so no entry reaches its expiry.", + "dark-layers": "The named dark-job histories and invariants do not distinguish inclusive local expiry. The sampled local-expiry witness drives native replay, but this profile supplies no model-side assertion for that exact local boundary.", + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed.", + "local-clock": "The fractional profile supplies its lookup through local_grid using raw insertion ticks and localEntryLiveOnGrid; it does not execute the mutated local_storage.localValue lookup. All named histories and scheduled invariants pass under this fault. Its own precise-grid and hit-renewal faults remain separately checked." } }, "nativeMutants": { "kind": "mapped", "mutant": "M18", - "text": "M18 is the native twin of kernel/local-storage.qnt localValue replacing localEntryLiveAt with an inclusive comparison against the entry's expiry: both ports serve a local entry at the exact millisecond its insertion TTL elapses. policy/localHitDoesNotRenewInsertionTtlTest step 10 and source-budgets/localEntryExpiresAfterItsTtlTest detect it in both ports." + "text": "M18 is the native twin of kernel/local-storage.qnt localValue replacing localEntryLiveAt with an inclusive comparison against the entry's expiry: both ports serve a local entry at the exact millisecond its insertion TTL elapses." } }, { @@ -678,7 +800,35 @@ "nativeMutants": { "kind": "mapped", "mutant": "M19", - "text": "M19 is the native twin of cache-rules.qnt localEntryLiveOnGrid being replaced by a comparison on raw ticks, as measured by dialcache-rule-checks.qnt localGridUsesWholeMilliseconds: both ports compute local insertion and expiry on the precise clock instead of the whole-millisecond grid. The feature-profile driver clocks are integer, so only the local-clock profile observes it; local-clock/fractionalInsertionExpiresAtWholeMillisecondTest step 7 detects both ports." + "text": "M19 replaces whole-millisecond local insertion/expiry with precise time in both ports. The unchanged raw-tick-difference fault now reaches the local-clock lifecycle, which retains raw insertion ticks and judges lookup through localEntryLiveOnGrid. The original localGridUsesWholeMilliseconds property remains independent." + }, + "reproducer": { + "kind": "exported-regression", + "run": "fractionalInsertionExpiresAtWholeMillisecondTest", + "model": "formal/dialcache-local-clock-conformance.qnt", + "failure": "s.o.calls == List(VALUE_ONE, VALUE_ONE, VALUE_TWO) and s.o.loaders == 2", + "family": "raw-versus-gridded-local-age", + "profiles": [ + "formal/dialcache-rule-checks.qnt", + "local-clock" + ], + "exclusions": { + "effects": "This profile never calls localEntryLiveOnGrid. Its local expiry, if present, is already expressed in whole milliseconds; raw insertion/observation projection belongs to local-clock.", + "recovery": "This profile never calls localEntryLiveOnGrid. Its local expiry, if present, is already expressed in whole milliseconds; raw insertion/observation projection belongs to local-clock.", + "policy": "This profile never calls localEntryLiveOnGrid. Its local expiry, if present, is already expressed in whole milliseconds; raw insertion/observation projection belongs to local-clock.", + "shadow": "This profile never calls localEntryLiveOnGrid. Its local expiry, if present, is already expressed in whole milliseconds; raw insertion/observation projection belongs to local-clock.", + "scope": "This profile never calls localEntryLiveOnGrid. Its local expiry, if present, is already expressed in whole milliseconds; raw insertion/observation projection belongs to local-clock.", + "admission": "This profile never calls localEntryLiveOnGrid. Its local expiry, if present, is already expressed in whole milliseconds; raw insertion/observation projection belongs to local-clock.", + "layers": "This profile never calls localEntryLiveOnGrid. Its local expiry, if present, is already expressed in whole milliseconds; raw insertion/observation projection belongs to local-clock.", + "independent": "This profile never calls localEntryLiveOnGrid. Its local expiry, if present, is already expressed in whole milliseconds; raw insertion/observation projection belongs to local-clock.", + "recovery-read": "This profile never calls localEntryLiveOnGrid. Its local expiry, if present, is already expressed in whole milliseconds; raw insertion/observation projection belongs to local-clock.", + "local-failure": "This profile never calls localEntryLiveOnGrid. Its local expiry, if present, is already expressed in whole milliseconds; raw insertion/observation projection belongs to local-clock.", + "runtime-boundaries": "This profile never calls localEntryLiveOnGrid. Its local expiry, if present, is already expressed in whole milliseconds; raw insertion/observation projection belongs to local-clock.", + "shadow-layers": "This profile never calls localEntryLiveOnGrid. Its local expiry, if present, is already expressed in whole milliseconds; raw insertion/observation projection belongs to local-clock.", + "source-budgets": "This profile never calls localEntryLiveOnGrid. Its local expiry, if present, is already expressed in whole milliseconds; raw insertion/observation projection belongs to local-clock.", + "dark-layers": "This profile never calls localEntryLiveOnGrid. Its local expiry, if present, is already expressed in whole milliseconds; raw insertion/observation projection belongs to local-clock.", + "shadow-read-deadlines": "All six scheduled histories and six invariants pass under this shared fault at 2000 samples by 40 steps. No scheduled assertion distinguishes fractional local insertion and lookup expiry in this bounded read-deadline profile; the imported rule alone does not establish detection." + } } }, { @@ -693,6 +843,34 @@ "kind": "mapped", "mutant": "M35", "text": "M35 is the native twin of cache-rules.qnt deadlinePendingAt becoming inclusive, as measured by dialcache-rule-checks.qnt sourceBoundaryIsExclusive: both ports accept a settlement observed at exactly the elapsed deadline. Timer-delivered regressions do not distinguish it from the unchanged timer path, so the effects jumpClock histories (effects/lateReadCannotBecomeHitTest, lateReadRejectionUsesDeadlineCategoryTest and the late-resolve and late-reject witnesses) are the expected evidence." + }, + "reproducer": { + "kind": "exported-regression", + "run": "lateSourceResultIsADeadlineErrorTest", + "failure": "s.o.calls == List(DEADLINE_ERROR) and s.o.dumps == 0 and errorReasons == List(\"fallback\") and amounts(\"fallback\") == List(10)", + "family": "inclusive-deadline-boundary", + "profiles": [ + "formal/dialcache-rule-checks.qnt", + "source-budgets", + "independent", + "shadow-layers", + "recovery", + "admission", + "shadow", + "effects", + "dark-layers", + "policy", + "recovery-read", + "shadow-read-deadlines" + ], + "exclusions": { + "scope": "This profile has no source/read/shadow deadline. It imports the common rules through storage, but its scheduled histories and independent properties do not distinguish equality in deadlinePendingAt for physical retention or marker expiry.", + "layers": "This profile has no source/read/shadow deadline. It imports the common rules through storage, but its scheduled histories and independent properties do not distinguish equality in deadlinePendingAt for physical retention or marker expiry.", + "local-failure": "This profile has no source/read/shadow deadline. It imports the common rules through storage, but its scheduled histories and independent properties do not distinguish equality in deadlinePendingAt for physical retention or marker expiry.", + "runtime-boundaries": "This profile has no source/read/shadow deadline. It imports the common rules through storage, but its scheduled histories and independent properties do not distinguish equality in deadlinePendingAt for physical retention or marker expiry.", + "local-clock": "This profile has no source/read/shadow deadline. It imports the common rules through storage, but its scheduled histories and independent properties do not distinguish equality in deadlinePendingAt for physical retention or marker expiry." + }, + "model": "formal/dialcache-effects-conformance.qnt" } }, { @@ -706,7 +884,35 @@ "nativeMutants": { "kind": "mapped", "mutant": "M06", - "text": "M06 is exactly cache-rules.qnt fenceAllows becoming inclusive, as measured by dialcache-rule-checks.qnt fenceBoundaryIsExclusive: the tracked decoders of both ports accept a frame stamped exactly at its observed watermark. The equal-timestamp row of the generated tracked decode vectors already detects it in both ports." + "text": "M06 makes the tracked native decoder accept a frame stamped exactly at its watermark. The shared model fenceAllows edit also changes fill-side checks; frameAtTheWatermarkIsFencedFromTheDarkReadTest isolates the read boundary through dumps and loads and completes in both mutated ports." + }, + "reproducer": { + "kind": "exported-regression", + "model": "formal/dialcache-dark-layers-conformance.qnt", + "run": "frameAtTheWatermarkIsFencedFromTheDarkReadTest", + "failure": "s.o.calls == List(VALUE_TWO) and s.o.dumps == 1 and s.o.loads == 0", + "family": "strict-read-fence", + "profiles": [ + "formal/dialcache-rule-checks.qnt", + "layers", + "independent", + "recovery-read", + "dark-layers" + ], + "exclusions": { + "effects": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after changing the shared strict read/write fence to an inclusive comparison. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "recovery": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after changing the shared strict read/write fence to an inclusive comparison. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "policy": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after changing the shared strict read/write fence to an inclusive comparison. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "shadow": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after changing the shared strict read/write fence to an inclusive comparison. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "scope": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after changing the shared strict read/write fence to an inclusive comparison. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "admission": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after changing the shared strict read/write fence to an inclusive comparison. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "local-failure": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after changing the shared strict read/write fence to an inclusive comparison. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "runtime-boundaries": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after changing the shared strict read/write fence to an inclusive comparison. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "shadow-layers": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after changing the shared strict read/write fence to an inclusive comparison. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "local-clock": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after changing the shared strict read/write fence to an inclusive comparison. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "source-budgets": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after changing the shared strict read/write fence to an inclusive comparison. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." + } } }, { @@ -720,7 +926,36 @@ "nativeMutants": { "kind": "mapped", "mutant": "M33", - "text": "Same native fault as source-deadline-epoch: M33 judges a source settlement against a budget anchored at the clock origin rather than the source start, which is kernel/deadlines.qnt due registering a deadline at the bare budget instead of the current instant plus the budget, so deliver expires a source, and arrival rejects its late result, at the origin plus its budget. source-budgets/heldPolicyDoesNotSpendSourceBudgetTest is the expected evidence in both ports." + "text": "M33 judges source settlement against the clock origin instead of its start; kernel/deadlines.qnt due makes the same origin mistake when registering the deadline. The model expires during advance while the native faults reject at settlement. After policy waits 100 ms, a source taking 9 ms of its 10 ms budget and the subsequent local hit must both return VALUE_ONE. The final caller outcomes distinguish the shared consequence.", + "evidence": { + "history": "source-budgets/heldPolicyDoesNotSpendSourceBudgetTest", + "step": 7, + "fields": [ + "o.calls" + ] + } + }, + "reproducer": { + "kind": "exported-regression", + "run": "heldPolicyDoesNotSpendSourceBudgetTest", + "model": "formal/dialcache-source-budgets-conformance.qnt", + "failure": "s.o.calls == List(VALUE_ONE, VALUE_ONE) and s.o.loaders == 1", + "family": "source-budget-starts-with-source", + "profiles": [ + "formal/dialcache-source-connection.qnt", + "effects", + "recovery", + "shadow", + "independent", + "shadow-layers", + "source-budgets", + "dark-layers", + "admission", + "shadow-read-deadlines" + ], + "exclusions": { + "scope": "Request-only sources have no deadline or elapsed clock; diagnostics imports the deadline module without invoking its registration transition. All 14 named runs and all six scheduled invariants pass with the wrong-origin fault at 2000 samples by 40 steps." + } } }, { @@ -734,6 +969,33 @@ "nativeMutants": { "kind": "model-only", "text": "The fault rewrites kernel/flights.qnt registerSource to record every source-starting caller as the owner of source 0, so finish (finishOwnedCalls and ownedBy) never completes a caller from the source it started; the connection monitor sourceOwnersMatchContract checks this bookkeeping against a reference captured at start. Neither port has an owners table: the starter awaits the promise its own run returned (TypeScript src/dialcache.ts singleFlightProcess and singleFlightRequestLocal) or its own goroutine result (Go go/engine.go singleFlight), and followers await the flight they looked up by key, so a caller cannot be bound to any source other than the one it started or joined. A wrong-key lookup is the C11 identity fault (M20), not this one." + }, + "reproducer": { + "kind": "model-run", + "run": "outsideSourcesKeepDistinctOwnersTest", + "failure": "sourceOwnersMatchContract and observedSources.length() == 2", + "family": "source-owner-receipt", + "profiles": [ + "formal/dialcache-source-connection.qnt", + "effects", + "recovery", + "policy", + "shadow", + "scope", + "admission", + "layers", + "independent", + "recovery-read", + "local-failure", + "runtime-boundaries", + "shadow-layers", + "local-clock", + "source-budgets", + "dark-layers", + "shadow-read-deadlines" + ], + "exclusions": {}, + "scope": "Two outside callers start distinct sources. The connection monitor captures each caller from the external admission count and rejects an owners-table rewrite at the second source. This checks model bookkeeping only: native callers await their own source promise or goroutine result, with no owners table to mutate." } }, { @@ -742,8 +1004,8 @@ "source": "formal/kernel/recovery.qnt", "model": "formal/dialcache-recovery-read-connection.qnt", "invariant": "retainedSnapshotMatchesAcquisition", - "before": "created: created,", - "after": "created: created + 1,", + "before": "created: Contract::retainedFrameStamp(created, Clock::wallOf(state)),", + "after": "created: Contract::retainedFrameStamp(created, Clock::wallOf(state)) + 1,", "reproducer": { "kind": "exported-regression", "run": "compressedRecoveryRechecksMaximumAfterDecodeTest", @@ -757,24 +1019,20 @@ "recovery" ], "exclusions": { - "core": "Does not import the kernel library, so the mutated candidate retention is never executed; its own snapshot capture is stated in its own text.", "effects": "Classifier NONE: no snapshot is retained, so the mutated candidate retention is never executed.", "shadow": "Composes remote_io with Recovery::NONE and no caller read: no snapshot is ever retained and no recovery decode is held.", "admission": "Passes Recovery::NONE at every read settlement, so no snapshot is retained and no recovery decode is held; the mutated text is never executed.", - "local-failure": "Composes the layered traversal without the recovery module (no stale-on-error snapshot is retained), so the mutated candidate retention is never executed.", "shadow-layers": "Composes recovery on the atomic path, but its guard excludes every stale candidate, so no snapshot holds a candidate and neither its stamp nor its maximum is ever consulted.", - "local-clock": "Composes the local projection without the recovery module, so the mutated candidate retention is never executed.", - "policy": "Composes the atomic layered release (serving.release), which observes the remote frame at release and never dispatches a held read through remote_io.", - "layers": "Composes the atomic layered release (serving.release), which observes the remote frame at release and never dispatches a held read through remote_io.", - "runtime-boundaries": "Composes the atomic layered release (serving.release), which observes the remote frame at release and never dispatches a held read through remote_io.", "scope": "Composes the request-only projection, which has no remote layer and dispatches no held read.", - "source-budgets": "Composes the local projection, which has no remote layer and dispatches no held read." + "source-budgets": "Composes the local projection, which has no remote layer and dispatches no held read.", + "dark-layers": "Remote serving is always off and no caller retains a recovery candidate; dark C0 bytes belong to the shadow job, not a recovery snapshot.", + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." } }, "nativeMutants": { "kind": "mapped", "mutant": "M43", - "text": "M43 is the native twin of kernel/recovery.qnt retain stamping the retained candidate one millisecond after the acquired frame, checked by dialcache-recovery-read-connection.qnt retainedSnapshotMatchesAcquisition: both ports retain the stale candidate with a timestamp one millisecond later than the frame they acquired. recovery-read/compressedRecoveryRechecksMaximumAfterDecodeTest, recovery/maximumAgeAfterDecodePreservesSourceTest and the millisecond diagnostic ages are the evidence." + "text": "M43 is the native twin of kernel/recovery.qnt retain stamping the retained candidate one millisecond after the acquired frame, checked by dialcache-recovery-read-connection.qnt retainedSnapshotMatchesAcquisition: both ports retain the stale candidate with a timestamp one millisecond later than the frame they acquired." } }, { @@ -799,23 +1057,19 @@ "effects" ], "exclusions": { - "core": "Does not import the kernel library, so the mutated read dispatch is never executed; its own admission capture is stated in its own text.", "shadow": "Composes remote_io, but every reply leaves the remote layer off, so the caller never dispatches a read; the job's read is the shadow module's.", - "admission": "Composes the held read dispatch, but every read serves a hit (the three keys always hold a frame fresh for the 60 s TTL inside the 600 ms bound) and Recovery::NONE retains no snapshot, so the retention the held read carries reaches only a served job's fill authority, which a served job never writes (measured 2026-09-20: every scheduled invariant and regression passes under the mutant).", - "local-failure": "Composes the layered release with inline remote reads (serving.release), never remote_io's held read dispatch or decode, so the mutated text is never executed.", + "admission": "Composes the held read dispatch, but every read serves a hit (the three keys always hold a frame fresh for the 60 s TTL inside the 600 ms bound) and Recovery::NONE retains no snapshot, so the retention the held read carries reaches only a served job's fill authority, which a served job never writes.", "shadow-layers": "Composes the atomic release (remote_writes), never remote_io's held read dispatch or decode, so the mutated text is never executed.", - "local-clock": "Composes the local projection with no remote layer, so remote_io is never executed.", - "policy": "Composes the atomic layered release (serving.release), which observes the remote frame at release and never dispatches a held read through remote_io.", - "layers": "Composes the atomic layered release (serving.release), which observes the remote frame at release and never dispatches a held read through remote_io.", - "runtime-boundaries": "Composes the atomic layered release (serving.release), which observes the remote frame at release and never dispatches a held read through remote_io.", "scope": "Composes the request-only projection, which has no remote layer and dispatches no held read.", - "source-budgets": "Composes the local projection, which has no remote layer and dispatches no held read." + "source-budgets": "Composes the local projection, which has no remote layer and dispatches no held read.", + "dark-layers": "Caller remote reads never start because remote serving is off; the mutated caller read-admission policy is not used by the separate held dark C0 read.", + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." } }, "nativeMutants": { "kind": "mapped", "mutant": "M45", - "text": "M45 is the native twin of kernel/remote-io.qnt dispatchRead recording a recovery maximum one unit above the reply's on the held read, checked by dialcache-recovery-read-connection.qnt retainedSnapshotMatchesAcquisition: both ports admit the call with a recovery maximum one second above the configured value, one second being the smallest configurable unit. recovery-read/compressedRecoveryRechecksMaximumAfterDecodeTest and independent/independentRecoveryAtCapturedAgeBoundaryTest are the evidence." + "text": "M45 is the native twin of kernel/remote-io.qnt dispatchRead recording a recovery maximum one unit above the reply's on the held read, checked by dialcache-recovery-read-connection.qnt retainedSnapshotMatchesAcquisition: both ports admit the call with a recovery maximum one second above the configured value, one second being the smallest configurable unit." } }, { @@ -840,23 +1094,19 @@ "effects" ], "exclusions": { - "core": "Does not import the kernel library, so the mutated read dispatch is never executed; its own admission capture is stated in its own text.", "shadow": "Composes remote_io, but every reply leaves the remote layer off, so the caller never dispatches a read; the job's read is the shadow module's.", - "admission": "Composes the held read dispatch, but every read serves a hit (the three keys always hold a frame fresh for the 60 s TTL inside the 600 ms bound) and Recovery::NONE retains no snapshot, so the retention the held read carries reaches only a served job's fill authority, which a served job never writes (measured 2026-09-20: every scheduled invariant and regression passes under the mutant).", - "local-failure": "Composes the layered release with inline remote reads (serving.release), never remote_io's held read dispatch or decode, so the mutated text is never executed.", + "admission": "Composes the held read dispatch, but every read serves a hit (the three keys always hold a frame fresh for the 60 s TTL inside the 600 ms bound) and Recovery::NONE retains no snapshot, so the retention the held read carries reaches only a served job's fill authority, which a served job never writes.", "shadow-layers": "Composes the atomic release (remote_writes), never remote_io's held read dispatch or decode, so the mutated text is never executed.", - "local-clock": "Composes the local projection with no remote layer, so remote_io is never executed.", - "policy": "Composes the atomic layered release (serving.release), which observes the remote frame at release and never dispatches a held read through remote_io.", - "layers": "Composes the atomic layered release (serving.release), which observes the remote frame at release and never dispatches a held read through remote_io.", - "runtime-boundaries": "Composes the atomic layered release (serving.release), which observes the remote frame at release and never dispatches a held read through remote_io.", "scope": "Composes the request-only projection, which has no remote layer and dispatches no held read.", - "source-budgets": "Composes the local projection, which has no remote layer and dispatches no held read." + "source-budgets": "Composes the local projection, which has no remote layer and dispatches no held read.", + "dark-layers": "Caller remote reads never start because remote serving is off; the mutated caller read-admission policy is not used by the separate held dark C0 read.", + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." } }, "nativeMutants": { "kind": "mapped", "mutant": "M46", - "text": "M46 is the native twin of kernel/remote-io.qnt dispatchRead recording a fixed retention on the held read instead of the reply's effective TTLs (the maximum its snapshot is retained under): the ports have no constant to substitute for a generic policy, so admitting with the static default while ignoring the runtime overlay is the faithful analog. independent/independentRecoveryAtCapturedAgeBoundaryTest and recovery-read/physicalCapDoesNotClampLogicalRecoveryTest are the expected evidence." + "text": "M46 is the native twin of kernel/remote-io.qnt dispatchRead recording a fixed retention on the held read instead of the reply's effective TTLs (the maximum its snapshot is retained under): the ports have no constant to substitute for a generic policy, so admitting with the static default while ignoring the runtime overlay is the faithful analog." } }, { @@ -870,7 +1120,37 @@ "nativeMutants": { "kind": "mapped", "mutant": "M41", - "text": "M41 is the native twin of kernel/remote-io.qnt settleLoad completing a served recovery with a constant other than the retained candidate: both ports decode the retained candidate and then serve an undefined or zero value, the only wrong constant expressible for a generic value type. independent/independentRecoveriesServeDistinctAcquiredSnapshotsTest and recovery/coalescedRecoveryClassifiesAndDecodesOnceTest are the expected evidence." + "text": "M41 is the native twin of kernel/remote-io.qnt settleLoad returning a wrong constant instead of each retained candidate. TypeScript serves undefined and Go the zero value; the model serves VALUE_TWO to both callers. After the two independent recoveries settle, caller results must preserve the distinct acquired values even though the stored frame was replaced and invalidated.", + "evidence": { + "history": "independent/independentRecoveriesServeDistinctAcquiredSnapshotsTest", + "step": 10, + "fields": [ + "o.calls" + ] + } + }, + "reproducer": { + "kind": "exported-regression", + "run": "independentRecoveriesServeDistinctAcquiredSnapshotsTest", + "model": "formal/dialcache-independent-conformance.qnt", + "failure": "s.o.calls == List(VALUE_ONE, VALUE_TWO) and s.o.recovery == List(\"served\", \"served\") and s.o.reads == 2 and s.o.loaders == 2 and s.o.loads == 2 and s.o.dumps == 0 and s.o.writes == 0", + "family": "recovery-keeps-acquired-value", + "profiles": [ + "formal/dialcache-independent-connection.qnt", + "recovery", + "independent", + "recovery-read" + ], + "exclusions": { + "effects": "Every held read settles with Recovery::NONE, so no caller retains a recovery candidate and the changed recovery-decode completion arm is unreachable.", + "shadow": "Every reply disables caller remote serving; held C0 decodes belong to shadow jobs, and no caller retains a recovery candidate.", + "scope": "Uses the request-only serving lifecycle, with no caller remote read or held recovery decode; it never executes RemoteIO::settleLoad.", + "admission": "Every caller read settles with Recovery::NONE, so held caller decodes can serve fresh hits but never execute the changed recovery-completion arm.", + "shadow-layers": "Uses the atomic caller traversal and RemoteWrites settlement, never RemoteIO::settleLoad; its guard also excludes ordinary stale caller recovery.", + "source-budgets": "Uses the budgeted local projection, with no remote layer or held caller decode; it never executes RemoteIO::settleLoad.", + "dark-layers": "Every reply disables caller remote serving; held C0 decodes belong to shadow jobs, and no caller retains a recovery candidate.", + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." + } } }, { @@ -889,27 +1169,19 @@ "profiles": [ "effects" ], - "exclusions": { - "core": "Does not import metrics; the mutated text is never executed.", - "recovery": "Does not import metrics; the mutated text is never executed.", - "policy": "Does not import metrics; the mutated text is never executed.", - "shadow": "Does not import metrics; the mutated text is never executed.", - "scope": "Does not import metrics; the mutated text is never executed.", - "admission": "Does not import metrics; the mutated text is never executed.", - "layers": "Does not import metrics; the mutated text is never executed.", - "independent": "Does not import metrics; the mutated text is never executed.", - "recovery-read": "Does not import metrics; the mutated text is never executed.", - "local-failure": "Does not import metrics; the mutated text is never executed.", - "runtime-boundaries": "Does not import metrics; the mutated text is never executed.", - "shadow-layers": "Does not import metrics; the mutated text is never executed.", - "local-clock": "Does not import metrics; the mutated text is never executed.", - "source-budgets": "Does not import metrics; the mutated text is never executed." - } + "exclusions": {} }, "nativeMutants": { "kind": "mapped", "mutant": "M36", - "text": "M36 is the native twin of kernel/metrics.qnt stamping the dump's hold one millisecond late: the serialization-start stamp both ports take one millisecond late is the only native acceptance receipt, and the dump duration comes out one millisecond short. effects/sourceDurationExcludesPublicationTest (dump amount 0.009 instead of 0.010) is the expected evidence in both ports." + "text": "M36 is the native twin of kernel/metrics.qnt stamping the dump's hold one millisecond late: the serialization-start stamp both ports take one millisecond late is the only native acceptance receipt, and the dump duration comes out one millisecond short.", + "evidence": { + "history": "effects/sourceDurationExcludesPublicationTest", + "step": 7, + "fields": [ + "events" + ] + } } }, { @@ -933,22 +1205,23 @@ "runtime-boundaries", "source-budgets", "admission", - "effects" + "effects", + "dark-layers" ], "exclusions": { "independent": "Coalescing is off in every reply: every flight has one owner, so completing the first owner completes them all.", "recovery-read": "One execution at a time: no follower ever joins a flight, so every flight has one owner.", "shadow-layers": "Coalescing is off in every reply: every flight has one owner.", - "core": "Does not import the kernel library, so the mutated completion is never executed; its own completion rule is stated in its own text.", "shadow": "Coalescing is off and one caller runs at a time: every flight has one owner.", - "local-failure": "Composes serving.complete through local_faults.settle, but admits one source at a time (no call begins while a source is pending), so every flight has one owner and the mutant's minimum-owner fold finishes the same call (measured 2026-09-20: every scheduled invariant and regression passes under the mutant).", - "local-clock": "Composes serving.complete through serving.callLocal, whose inline source settles in the step that starts it, so no flight ever gains a second owner and the mutant's minimum-owner fold finishes the same call (measured 2026-09-20: every scheduled invariant and regression passes under the mutant)." + "local-failure": "Composes serving.complete through local_faults.settle, but admits one source at a time (no call begins while a source is pending), so every flight has one owner and the mutant's minimum-owner fold finishes the same call.", + "local-clock": "Composes serving.complete through serving.callLocal, whose inline source settles in the step that starts it, so no flight ever gains a second owner and the mutant's minimum-owner fold finishes the same call.", + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." } }, "nativeMutants": { "kind": "mapped", "mutant": "M34", - "text": "M34 is the native twin of kernel/serving.qnt complete finishing only the flight's first owner instead of every caller it owns: both ports stop followers of a joined flight from receiving the accepted value while errors still propagate. Followers get an undefined or zero value rather than staying pending because a blocked follower would deadlock synctest; recovery/crossRequestRecoveryMemoizesBothScopesTest and coalescedRecoveryClassifiesAndDecodesOnceTest are the evidence." + "text": "M34 is the native twin of kernel/serving.qnt complete finishing only the flight's first owner instead of every caller it owns: both ports stop followers of a joined flight from receiving the accepted value while errors still propagate. Followers get an undefined or zero value rather than staying pending because a blocked follower would deadlock synctest." } }, { @@ -963,7 +1236,36 @@ "nativeMutants": { "kind": "mapped", "mutant": "M33", - "text": "Same native fault as source-deadline-epoch: M33 judges a source settlement against a budget anchored at the clock origin rather than the source start, which is kernel/deadlines.qnt due registering a deadline at the bare budget instead of the current instant plus the budget, so every read and loader deadline is measured from the origin. independent/staggeredSourceStartsKeepIndependentBudgetsTest is the expected evidence in both ports." + "text": "M33 judges a source settlement against the clock origin instead of the source start. kernel/deadlines.qnt due makes the same origin mistake when registering a deadline; independentSourceOriginsMatchContract independently checks held-read/loader origins. The shared held-policy history exposes the consequence: a source starting after 100 ms keeps its full 10 ms budget and both callers return its accepted value.", + "evidence": { + "history": "source-budgets/heldPolicyDoesNotSpendSourceBudgetTest", + "step": 7, + "fields": [ + "o.calls" + ] + } + }, + "reproducer": { + "kind": "exported-regression", + "run": "heldPolicyDoesNotSpendSourceBudgetTest", + "model": "formal/dialcache-source-budgets-conformance.qnt", + "failure": "s.o.calls == List(VALUE_ONE, VALUE_ONE) and s.o.loaders == 1", + "family": "source-budget-starts-with-source", + "profiles": [ + "formal/dialcache-independent-connection.qnt", + "effects", + "recovery", + "shadow", + "independent", + "shadow-layers", + "source-budgets", + "dark-layers", + "admission", + "shadow-read-deadlines" + ], + "exclusions": { + "scope": "Request-only sources have no deadline or elapsed clock; diagnostics imports the deadline module without invoking its registration transition. All 14 named runs and all six scheduled invariants pass with the wrong-origin fault at 2000 samples by 40 steps." + } } }, { @@ -978,6 +1280,33 @@ "nativeMutants": { "kind": "model-only", "text": "The fault rewrites kernel/flights.qnt registerSource to record every caller as the owner of flight 0, so the flight a caller's own read dispatched never completes it and flight 0 completes every caller; the connection monitor independentSourceOriginsMatchContract finds no owner for any other flight's loader. With coalesce false each port caller awaits the promise or goroutine of the source it started itself (TypeScript src/dialcache.ts finishRedisChain through callFallback and withFallbackTimeout; Go go/engine.go execution.source returning into GetOrLoad), so no registry exists that could route a settlement to another caller." + }, + "reproducer": { + "kind": "model-run", + "run": "independentReadSourcesKeepDistinctOwnersTest", + "failure": "independentSourceOriginsMatchContract and profile::s.o.loaders == 2", + "family": "source-owner-receipt", + "profiles": [ + "formal/dialcache-independent-connection.qnt", + "effects", + "recovery", + "policy", + "shadow", + "scope", + "admission", + "layers", + "independent", + "recovery-read", + "local-failure", + "runtime-boundaries", + "shadow-layers", + "local-clock", + "source-budgets", + "dark-layers", + "shadow-read-deadlines" + ], + "exclusions": {}, + "scope": "Two independent held reads start distinct loaders. The unchanged connection property rejects the second loader having no caller in the mutated ownership registry. Both ports return each independent source into its own caller without such a registry; this reproducer checks the model monitor, not a native wrong-key fault." } }, { @@ -992,7 +1321,35 @@ "nativeMutants": { "kind": "mapped", "mutant": "M06", - "text": "Same native fault as fence-inclusive-timestamp: M06 makes the tracked decode fence inclusive in both ports, which is cache-rules.qnt fenceAllows becoming inclusive as measured here by dialcache-tracked-invalidation.qnt servedSnapshotClearedObservedFence. The equal-timestamp tracked decode vector and recovery-read/laterInvalidationDoesNotRewriteObservedMissFenceTest already detect it in both ports." + "text": "M06 makes the tracked native decoder accept a frame stamped exactly at its watermark. The shared model fenceAllows edit also changes fill-side checks; frameAtTheWatermarkIsFencedFromTheDarkReadTest isolates the read boundary through dumps and loads and completes in both mutated ports." + }, + "reproducer": { + "kind": "exported-regression", + "model": "formal/dialcache-dark-layers-conformance.qnt", + "run": "frameAtTheWatermarkIsFencedFromTheDarkReadTest", + "failure": "s.o.calls == List(VALUE_TWO) and s.o.dumps == 1 and s.o.loads == 0", + "family": "strict-read-fence", + "profiles": [ + "formal/dialcache-tracked-invalidation.qnt", + "layers", + "independent", + "recovery-read", + "dark-layers" + ], + "exclusions": { + "effects": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after changing the shared strict read/write fence to an inclusive comparison. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "recovery": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after changing the shared strict read/write fence to an inclusive comparison. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "policy": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after changing the shared strict read/write fence to an inclusive comparison. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "shadow": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after changing the shared strict read/write fence to an inclusive comparison. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "scope": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after changing the shared strict read/write fence to an inclusive comparison. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "admission": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after changing the shared strict read/write fence to an inclusive comparison. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "local-failure": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after changing the shared strict read/write fence to an inclusive comparison. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "runtime-boundaries": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after changing the shared strict read/write fence to an inclusive comparison. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "shadow-layers": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after changing the shared strict read/write fence to an inclusive comparison. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "local-clock": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after changing the shared strict read/write fence to an inclusive comparison. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "source-budgets": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after changing the shared strict read/write fence to an inclusive comparison. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." + } } }, { @@ -1014,24 +1371,25 @@ "recovery-read", "shadow-layers", "recovery", - "shadow" + "shadow", + "dark-layers" ], "exclusions": { - "core": "Does not import the kernel library, so the mutated freshness judgment is never executed; its own freshness rule is stated in its own text.", - "effects": "Frames are stamped at the wall clock and read within 600 ms under a 60 s freshness, so no read meets the freshness boundary (measured 2026-09-20 over the composed text: every scheduled invariant and regression passes under the mutant).", + "effects": "Frames are stamped at the wall clock and read within 600 ms under a 60 s freshness, so no read meets the freshness boundary.", "admission": "Composes the remote freshness judgment, but every frame is stamped at a wall clock that only advances and is read within 600 ms under a 60 s freshness, so no read meets an age boundary or observes a future stamp.", "local-failure": "Composes the remote freshness judgment, but its wall clock never moves and every frame is stamped at the current instant, so a frame's age is always 0 and never meets the fresh boundary.", "local-clock": "Composes the local projection with no remote layer, so the freshness judgment is never executed.", "scope": "Composes the request-only projection, which never reads a remote frame.", "source-budgets": "Composes the local projection, which never reads a remote frame.", "layers": "Composes remote frames, but its 60 s freshness is never reached within the bound of 1 ms ticks.", - "runtime-boundaries": "Composes remote frames, but no input advances the clock, so no frame ages to its freshness boundary." + "runtime-boundaries": "Composes remote frames, but no input advances the clock, so no frame ages to its freshness boundary.", + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." } }, "nativeMutants": { "kind": "mapped", "mutant": "M30", - "text": "M30 is the native twin of kernel/remote-frames.qnt fresh replacing freshAgeAllowed with an inclusive age comparison: both ports serve a remote frame whose age equals the fresh TTL as a hit. Without stale-on-error retention the maximum-age check runs first and masks the flip, as the model's retained masks it when retention equals freshness; policy/exactRemoteFreshBoundaryStartsSourceTest with the 5 s retention fixture and witness policy:remote-exact-fresh-boundary-miss are the evidence." + "text": "M30 is the native twin of kernel/remote-frames.qnt fresh replacing freshAgeAllowed with an inclusive age comparison: both ports serve a remote frame whose age equals the fresh TTL as a hit. Without stale-on-error retention the maximum-age check runs first and masks the flip, as the model's retained masks it when retention equals freshness." } }, { @@ -1044,33 +1402,23 @@ "after": "pure def acquired(observed: Remote::Observed): int = observed.payload", "reproducer": { "kind": "exported-regression", - "run": "c0AtExactFreshnessBoundaryRefillsTest", + "run": "expiredC0BeginsFillBeforePublicationTest", "failure": "s.o.calls == List(VALUE_TWO, VALUE_TWO) and s.o.loads == 1 and s.o.dumps == 1", "family": "inclusive-freshness-boundary", "profiles": [ "shadow", - "shadow-layers" + "shadow-layers", + "dark-layers" ], "exclusions": { - "core": "Does not compose the shadow module, so the mutated dark C0 acquisition is never executed.", - "effects": "Does not compose the shadow module, so the mutated dark C0 acquisition is never executed.", - "scope": "Does not compose the shadow module, so the mutated dark C0 acquisition is never executed.", - "recovery": "Does not compose the shadow module, so the mutated dark C0 acquisition is never executed.", - "policy": "Does not compose the shadow module, so the mutated dark C0 acquisition is never executed.", "admission": "Every reply enables the remote layer, so no dark C0 read is made and the mutated acquisition is never applied.", - "layers": "Does not compose the shadow module, so the mutated dark C0 acquisition is never executed.", - "independent": "Does not compose the shadow module, so the mutated dark C0 acquisition is never executed.", - "recovery-read": "Does not compose the shadow module, so the mutated dark C0 acquisition is never executed.", - "local-failure": "Does not compose the shadow module, so the mutated dark C0 acquisition is never executed.", - "runtime-boundaries": "Does not compose the shadow module, so the mutated dark C0 acquisition is never executed.", - "local-clock": "Does not compose the shadow module, so the mutated dark C0 acquisition is never executed.", - "source-budgets": "Does not compose the shadow module, so the mutated dark C0 acquisition is never executed." + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." } }, "nativeMutants": { "kind": "mapped", "mutant": "M31", - "text": "M31 is the native twin of kernel/shadow.qnt acquired keeping a dark C0 from a visible frame remote_frames.observe declined for the reply's freshness: both ports acquire a dark C0 frame whose age equals the fresh TTL instead of treating it as expired. shadow/c0AtExactFreshnessBoundaryRefillsTest (the second job acquires C0, decodes it and never fills) is the expected evidence in both ports." + "text": "M31 is the native twin of kernel/shadow.qnt acquired keeping a dark C0 from a visible frame remote_frames.observe declined for the reply's freshness: both ports acquire a dark C0 frame whose age equals the fresh TTL instead of treating it as expired." } }, { @@ -1084,76 +1432,250 @@ "nativeMutants": { "kind": "model-only", "text": "The core model keeps localHasValue apart from the environment flag localReadHealthy, so the fault in dialcache-core.qnt localHit serves a value through a read the environment declared failed. In both ports the failed read is the harness seam and yields no value: TypeScript spies LocalCache.prototype.getWithResolvedConfig in test/formal/behavior-driver.ts and throws before delegation, so DialCache.readLocalWithResolvedConfig in src/dialcache.ts returns a disabled result with reason config_error; Go panics in behaviorLocalFaultClock.ElapsedMS (go/behavior_driver_test.go) inside Cache.localGet (go/cache.go) and callSafely returns the zero localResult. No decision point could return the stored value from a failed read; a Go-only bypass of the seam has no TypeScript twin and would test the harness. Moving either seam must re-trigger this review." + }, + "reproducer": { + "kind": "model-run", + "run": "failedLocalReadCannotServePreviouslyStoredValueTest", + "failure": "s.localHasValue and s.phase == RemoteLookup and s.origin == NoResult and s.localReads == 1 and failedLocalReadSkipsReuseAndPublication", + "family": "unhealthy-read-served", + "profiles": [ + "formal/dialcache-core.qnt" + ], + "exclusions": {}, + "scope": "The run first populates local storage, then fails the next read and stops at its lookup decision. The independent property rejects serving that retained model value through a failed read. Native fault seams throw or panic before returning an entry, so there is no native value-selection decision to mutate at this boundary." } }, { "id": "core-tracked-fallback-warms-local", "contract": "C31", - "source": "formal/dialcache-core.qnt", + "source": "formal/cache-rules.qnt", "model": "formal/dialcache-core.qnt", "invariant": "trackedRemoteFallbackSuppressesLocalPublication", - "before": " remoteHasValue: true,\n localWriteFailures: if (localWriteFails and not(s.callTracked)) s.localWriteFailures + 1 else s.localWriteFailures,\n localHasValue: if (localWriteEligible and not(s.callTracked)) true else s.localHasValue,\n localWrites: if (localWriteEligible and not(s.callTracked)) s.localWrites + 1 else s.localWrites,", - "after": " remoteHasValue: true,\n localWriteFailures: if (localWriteFails) s.localWriteFailures + 1 else s.localWriteFailures,\n localHasValue: if (localWriteEligible) true else s.localHasValue,\n localWrites: if (localWriteEligible) s.localWrites + 1 else s.localWrites,", + "before": "pure def sourceMayWarmLocal(tracked: bool, readRemote: bool): bool =\n not(tracked and readRemote)", + "after": "pure def sourceMayWarmLocal(tracked: bool, readRemote: bool): bool =\n true", "nativeMutants": { "kind": "mapped", "mutant": "M10", - "text": "M10 is exactly dialcache-core.qnt finishRemotePublication removing the not-tracked condition from its local write conjuncts: both ports publish an unvalidated tracked source value directly into local storage. layers/trackedSourceWaitsForValidatedReadToWarmLocalTest and recovery-read/trackedSourceNeedsRemoteValidationBeforeLocalReuseTest already detect it in both ports." + "text": "M10 allows a tracked source refill to warm local storage before a validating remote read. sourceMayWarmLocal is the original core publication condition shared with kernel authority; the original trackedRemoteFallbackSuppressesLocalPublication property stays independent. The second public read distinguishes reuse through local versus validated Redis.", + "evidence": { + "history": "layers/trackedRefillIsReadAgainBeforeLocalReuseTest", + "step": 3, + "fields": [ + "o.reads" + ] + } + }, + "reproducer": { + "kind": "exported-regression", + "run": "trackedRefillIsReadAgainBeforeLocalReuseTest", + "model": "formal/dialcache-layers-conformance.qnt", + "failure": "s.o.calls == List(VALUE_ONE, VALUE_ONE) and s.o.reads == 2 and s.o.loaders == 1", + "family": "tracked-source-publication-authority", + "profiles": [ + "formal/dialcache-core.qnt", + "layers", + "recovery-read" + ], + "exclusions": { + "effects": "The local layer is disabled and has zero capacity, so no source can warm local storage.", + "recovery": "The local layer is disabled and has zero capacity, so no source can warm local storage.", + "policy": "All calls are untracked, for which the correct and faulty source publication judgments both permit local writes.", + "shadow": "The local layer is disabled and has zero capacity, so no source can warm local storage.", + "scope": "The request-only projection has no remote read or local storage.", + "admission": "The local layer is disabled and has zero capacity, so no source can warm local storage.", + "independent": "The local layer is disabled and has zero capacity, so no source can warm local storage.", + "local-failure": "All calls are untracked, for which the correct and faulty source publication judgments both permit local writes.", + "runtime-boundaries": "All calls are untracked, for which the correct and faulty source publication judgments both permit local writes.", + "local-clock": "The local projection has no remote read, so no tracked remote miss can withhold local publication.", + "source-budgets": "The local projection has no remote read, so no tracked remote miss can withhold local publication.", + "dark-layers": "Foreground remote serving is always off, so source local publication is permitted by both judgments.", + "shadow-layers": "Dark sources may publish locally because remote serving is off. Its scheduled histories and properties do not detect the changed foreground tracked publication authority; layers and recovery-read supply that boundary.", + "shadow-read-deadlines": "All six scheduled histories and six invariants pass under this shared fault at 2000 samples by 40 steps. No scheduled assertion distinguishes direct local publication after a tracked source refill in this bounded read-deadline profile; the imported rule alone does not establish detection." + } } }, { "id": "runtime-policy-coalesce-defaults-off", "contract": "C17", - "source": "formal/dialcache-runtime-policy.qnt", + "source": "formal/cache-rules.qnt", "model": "formal/dialcache-runtime-policy.qnt", "invariant": "featureLibraryDefaults", - "before": "coalesce: inherit(runtime.coalesce, baseline.coalesce, 1),", - "after": "coalesce: inherit(runtime.coalesce, baseline.coalesce, 0),", + "before": "pure val libraryCoalescingDefault = true", + "after": "pure val libraryCoalescingDefault = false", "nativeMutants": { "kind": "mapped", "mutant": "M28", - "text": "M28 is the native twin of dialcache-runtime-policy.qnt resolve changing the coalesce library default from on to off: both ports treat an omitted static coalesce leaf as no sharing while an explicit true still shares. runtime-boundaries/omittedRampAndSharingUseLibraryDefaultsTest and source-budgets/lateFollowerUsesLeadersRemainingBudgetTest are the expected evidence in both ports." + "text": "M28 treats an omitted static coalesce leaf as false. The verifier resolver and runtime fixture default normalization both consume libraryCoalescingDefault; featureLibraryDefaults still independently requires the true default. The public history observes both callers joined to the same source.", + "evidence": { + "history": "runtime-boundaries/omittedRampAndSharingUseLibraryDefaultsTest", + "step": 7, + "fields": [ + "o.calls" + ] + } + }, + "reproducer": { + "kind": "exported-regression", + "run": "omittedRampAndSharingUseLibraryDefaultsTest", + "model": "formal/dialcache-runtime-boundaries-conformance.qnt", + "failure": "s.o.calls == List(VALUE_ONE, VALUE_ONE, VALUE_ONE) and s.o.loaders == 1\n and s.o.reads == 2 and s.o.loads == 1 and s.o.writeTtls == List(TTL_MS)\n and s.o.shadow == List() and s.o.recovery == List()", + "family": "omitted-sharing-default", + "profiles": [ + "formal/dialcache-runtime-policy.qnt", + "runtime-boundaries" + ], + "exclusions": { + "effects": "The profile supplies an explicit sharing policy and never resolves an omitted coalesce leaf through libraryCoalescingDefault. The omitted-default connection belongs to runtime-boundaries.", + "recovery": "The profile supplies an explicit sharing policy and never resolves an omitted coalesce leaf through libraryCoalescingDefault. The omitted-default connection belongs to runtime-boundaries.", + "policy": "The profile supplies an explicit sharing policy and never resolves an omitted coalesce leaf through libraryCoalescingDefault. The omitted-default connection belongs to runtime-boundaries.", + "shadow": "The profile supplies an explicit sharing policy and never resolves an omitted coalesce leaf through libraryCoalescingDefault. The omitted-default connection belongs to runtime-boundaries.", + "scope": "The profile supplies an explicit sharing policy and never resolves an omitted coalesce leaf through libraryCoalescingDefault. The omitted-default connection belongs to runtime-boundaries.", + "admission": "The profile supplies an explicit sharing policy and never resolves an omitted coalesce leaf through libraryCoalescingDefault. The omitted-default connection belongs to runtime-boundaries.", + "layers": "The profile supplies an explicit sharing policy and never resolves an omitted coalesce leaf through libraryCoalescingDefault. The omitted-default connection belongs to runtime-boundaries.", + "independent": "The profile supplies an explicit sharing policy and never resolves an omitted coalesce leaf through libraryCoalescingDefault. The omitted-default connection belongs to runtime-boundaries.", + "recovery-read": "The profile supplies an explicit sharing policy and never resolves an omitted coalesce leaf through libraryCoalescingDefault. The omitted-default connection belongs to runtime-boundaries.", + "local-failure": "The profile supplies an explicit sharing policy and never resolves an omitted coalesce leaf through libraryCoalescingDefault. The omitted-default connection belongs to runtime-boundaries.", + "shadow-layers": "The profile supplies an explicit sharing policy and never resolves an omitted coalesce leaf through libraryCoalescingDefault. The omitted-default connection belongs to runtime-boundaries.", + "local-clock": "The profile supplies an explicit sharing policy and never resolves an omitted coalesce leaf through libraryCoalescingDefault. The omitted-default connection belongs to runtime-boundaries.", + "source-budgets": "The profile supplies an explicit sharing policy and never resolves an omitted coalesce leaf through libraryCoalescingDefault. The omitted-default connection belongs to runtime-boundaries.", + "dark-layers": "The profile supplies an explicit sharing policy and never resolves an omitted coalesce leaf through libraryCoalescingDefault. The omitted-default connection belongs to runtime-boundaries.", + "shadow-read-deadlines": "All six scheduled histories and six invariants pass under this shared fault at 2000 samples by 40 steps. No scheduled assertion distinguishes the inherited library coalescing default in this bounded read-deadline profile; the imported rule alone does not establish detection." + } } }, { "id": "runtime-policy-physical-ttl-ignores-recovery", "contract": "C22", - "source": "formal/dialcache-runtime-policy.qnt", + "source": "formal/cache-rules.qnt", "model": "formal/dialcache-runtime-policy.qnt", "invariant": "existingRemoteEntryKeepsPhysicalTtl", - "before": " remotePhysicalTtlAtInsert:\n if (s.snapshot.recoveryAge > s.snapshot.remoteTtl) s.snapshot.recoveryAge\n else s.snapshot.remoteTtl,", - "after": " remotePhysicalTtlAtInsert: s.snapshot.remoteTtl,", + "before": "pure def remoteRetentionFor(fresh: int, recovery: int): int =\n if (recovery > fresh) recovery else fresh", + "after": "pure def remoteRetentionFor(fresh: int, recovery: int): int =\n fresh", "nativeMutants": { "kind": "mapped", "mutant": "M32", - "text": "M32 is the native twin of dialcache-runtime-policy.qnt insertRemoteEntry setting the physical TTL to the fresh TTL instead of the larger of recovery age and fresh TTL: both ports write every remote frame with the fresh TTL, ignoring stale-on-error retention. policy/eachSourceKeepsPublicationPolicyTest and every recovery-profile write are the evidence, since the drivers compare writeTtls on each step." - } - }, + "text": "M32 retains remote frames only for freshness, discarding a larger recovery maximum. remoteRetentionFor is shared by verifier insertion and the portable policy resolver; existingRemoteEntryKeepsPhysicalTtl still independently compares the captured policy. The public history observes the actual per-source write TTLs.", + "evidence": { + "history": "policy/eachSourceKeepsPublicationPolicyTest", + "step": 7, + "fields": [ + "o.writeTtls" + ] + } + }, + "reproducer": { + "kind": "exported-regression", + "run": "eachSourceKeepsPublicationPolicyTest", + "model": "formal/dialcache-policy-conformance.qnt", + "failure": "s.o.writeTtls == List(FOUR_SECONDS_MS, DEFAULT_RETENTION_MS) and s.remoteValues == List(1, 0)", + "family": "captured-physical-retention", + "profiles": [ + "formal/dialcache-runtime-policy.qnt", + "policy" + ], + "exclusions": { + "effects": "The profile supplies retention directly and does not derive it through policy-overlay resolved retention. The captured maximum-of-freshness-and-recovery judgment is exercised by policy.", + "recovery": "The profile supplies retention directly and does not derive it through policy-overlay resolved retention. The captured maximum-of-freshness-and-recovery judgment is exercised by policy.", + "shadow": "The profile supplies retention directly and does not derive it through policy-overlay resolved retention. The captured maximum-of-freshness-and-recovery judgment is exercised by policy.", + "scope": "The profile supplies retention directly and does not derive it through policy-overlay resolved retention. The captured maximum-of-freshness-and-recovery judgment is exercised by policy.", + "admission": "The profile supplies retention directly and does not derive it through policy-overlay resolved retention. The captured maximum-of-freshness-and-recovery judgment is exercised by policy.", + "layers": "The profile supplies retention directly and does not derive it through policy-overlay resolved retention. The captured maximum-of-freshness-and-recovery judgment is exercised by policy.", + "independent": "The profile supplies retention directly and does not derive it through policy-overlay resolved retention. The captured maximum-of-freshness-and-recovery judgment is exercised by policy.", + "recovery-read": "The profile supplies retention directly and does not derive it through policy-overlay resolved retention. The captured maximum-of-freshness-and-recovery judgment is exercised by policy.", + "local-failure": "The profile supplies retention directly and does not derive it through policy-overlay resolved retention. The captured maximum-of-freshness-and-recovery judgment is exercised by policy.", + "runtime-boundaries": "The profile supplies retention directly and does not derive it through policy-overlay resolved retention. The captured maximum-of-freshness-and-recovery judgment is exercised by policy.", + "shadow-layers": "The profile supplies retention directly and does not derive it through policy-overlay resolved retention. The captured maximum-of-freshness-and-recovery judgment is exercised by policy.", + "local-clock": "The profile supplies retention directly and does not derive it through policy-overlay resolved retention. The captured maximum-of-freshness-and-recovery judgment is exercised by policy.", + "source-budgets": "The profile supplies retention directly and does not derive it through policy-overlay resolved retention. The captured maximum-of-freshness-and-recovery judgment is exercised by policy.", + "dark-layers": "The profile supplies retention directly and does not derive it through policy-overlay resolved retention. The captured maximum-of-freshness-and-recovery judgment is exercised by policy.", + "shadow-read-deadlines": "All six scheduled histories and six invariants pass under this shared fault at 2000 samples by 40 steps. No scheduled assertion distinguishes physical retention beyond the fresh lifetime for recovery in this bounded read-deadline profile; the imported rule alone does not establish detection." + } + } + }, { "id": "stale-recovery-inclusive-served-maximum", "contract": "C45", - "source": "formal/dialcache-stale-recovery.qnt", + "source": "formal/cache-rules.qnt", "model": "formal/dialcache-stale-recovery.qnt", "invariant": "servedStaleIsStrictlyWithinMaxAge", - "before": "recoveryAgeAllowed(s.candidateCreatedAt, s.now, MAX_RECOVERY_AGE)", - "after": "recoveryAgeAllowed(s.candidateCreatedAt, s.now, MAX_RECOVERY_AGE + 1)", + "before": "pure def retainedCandidateAgeAllowed(created: int, observed: int, maximum: int): bool =\n recoveryAgeAllowed(created, observed, maximum)", + "after": "pure def retainedCandidateAgeAllowed(created: int, observed: int, maximum: int): bool =\n recoveryAgeAllowed(created, observed, maximum + 1)", "nativeMutants": { "kind": "mapped", "mutant": "M01", - "text": "M01 makes the post-decode serving maximum inclusive in both ports, the boundary dialcache-stale-recovery.qnt candidateWithinRecoveryAge widens by one to gate the serve-stale transitions. recovery/maximumAgeAfterDecodePreservesSourceTest and witness recovery:exact-maximum-after-decode-rejects already detect it in both ports." + "text": "M01 serves a retained candidate at its exclusive maximum. The original verifier call-site judgment now lives in retainedCandidateAgeAllowed, also used at kernel recovery failure/decode settlement. servedStaleIsStrictlyWithinMaxAge remains independent; both ports must reject the same decoded candidate at equality." + }, + "reproducer": { + "kind": "exported-regression", + "run": "maximumAgeAfterDecodeFromStrictlyStaleFrameTest", + "model": "formal/dialcache-recovery-conformance.qnt", + "failure": "s.o.calls == List(SOURCE_ERROR) and s.o.loads == 1 and s.o.recovery == List(\"miss\") and s.d.ages == List() and s.o.writes == 0", + "family": "retained-recovery-maximum", + "profiles": [ + "formal/dialcache-stale-recovery.qnt", + "recovery", + "recovery-read", + "independent" + ], + "exclusions": { + "effects": "Classifier NONE: no snapshot is retained, so the mutated retained-candidate judgment is never executed.", + "policy": "Composes the atomic layered release (serving.release), which observes the remote frame at release and never dispatches a held read through remote_io.", + "shadow": "Composes remote_io with Recovery::NONE and no caller read: no snapshot is ever retained and no recovery decode is held.", + "scope": "Composes the request-only projection, which has no remote layer and dispatches no held read.", + "admission": "Passes Recovery::NONE at every read settlement, so no snapshot is retained and no recovery decode is held; the mutated text is never executed.", + "layers": "Composes the atomic layered release (serving.release), which observes the remote frame at release and never dispatches a held read through remote_io.", + "local-failure": "Composes the layered traversal without the recovery module (no stale-on-error snapshot is retained), so the mutated retained-candidate judgment is never executed.", + "runtime-boundaries": "Composes the atomic layered release (serving.release), which observes the remote frame at release and never dispatches a held read through remote_io.", + "shadow-layers": "Composes recovery on the atomic path, but its guard excludes every stale candidate, so no snapshot holds a candidate and neither its stamp nor its maximum is ever consulted.", + "local-clock": "Composes the local projection without the recovery module, so the mutated retained-candidate judgment is never executed.", + "source-budgets": "Composes the local projection, which has no remote layer and dispatches no held read.", + "dark-layers": "Remote serving is always off and no caller retains a recovery candidate; dark C0 bytes belong to the shadow job, not a recovery snapshot.", + "shadow-read-deadlines": "All six scheduled histories and six invariants pass under this shared fault at 2000 samples by 40 steps. No scheduled assertion distinguishes serving a retained recovery candidate at maximum-age equality in this bounded read-deadline profile; the imported rule alone does not establish detection." + } } }, { "id": "stale-recovery-candidate-stamped-at-read", "contract": "C44", - "source": "formal/dialcache-stale-recovery.qnt", + "source": "formal/cache-contract.qnt", "model": "formal/dialcache-stale-recovery.qnt", "invariant": "retainedCandidateWasEligible", - "before": "candidateCreatedAt: if (candidateAtInitialRead) s.frameCreatedAt else s.candidateCreatedAt,", - "after": "candidateCreatedAt: if (candidateAtInitialRead) s.now else s.candidateCreatedAt,", + "before": "pure def retainedFrameStamp(frameCreated: int, readAt: int): int = frameCreated", + "after": "pure def retainedFrameStamp(frameCreated: int, readAt: int): int = readAt", "nativeMutants": { "kind": "mapped", "mutant": "M44", - "text": "M44 is the native twin of dialcache-stale-recovery.qnt performInitialRead stamping the candidate with the read time instead of the frame's creation: both ports retain the stale candidate with its timestamp replaced by the wall clock at read time. M43 shares the anchor with a different wrong behavior and stays distinct; recovery/maximumAgeAfterDecodePreservesSourceTest, recovery-read/compressedRecoveryRechecksMaximumAfterDecodeTest and witness recovery:expired-during-decode are the evidence." + "text": "M44 replaces the retained frame creation stamp with its acquisition clock. retainedFrameStamp is shared by the original verifier transition and kernel retention. The original retainedCandidateWasEligible property keeps the independent read receipt; the public decode history distinguishes an expired candidate from a restamped one.", + "evidence": { + "history": "recovery-read/compressedRecoveryRechecksMaximumAfterDecodeTest", + "step": 6, + "fields": [ + "o.calls" + ] + } + }, + "reproducer": { + "kind": "exported-regression", + "run": "compressedRecoveryRechecksMaximumAfterDecodeTest", + "model": "formal/dialcache-recovery-read-conformance.qnt", + "failure": "s.o.calls == List(SOURCE_ERROR) and s.io.sourceErrors == List(1)\n and s.o.recovery == List(\"miss\") and s.o.dumps == 0 and s.o.writes == 0", + "family": "retained-frame-stamp", + "profiles": [ + "formal/dialcache-stale-recovery.qnt", + "recovery-read", + "recovery", + "independent" + ], + "exclusions": { + "effects": "Classifier NONE: no snapshot is retained, so the mutated retained-candidate judgment is never executed.", + "shadow": "Composes remote_io with Recovery::NONE and no caller read: no snapshot is ever retained and no recovery decode is held.", + "scope": "Composes the request-only projection, which has no remote layer and dispatches no held read.", + "admission": "Passes Recovery::NONE at every read settlement, so no snapshot is retained and no recovery decode is held; the mutated text is never executed.", + "shadow-layers": "Composes recovery on the atomic path, but its guard excludes every stale candidate, so no snapshot holds a candidate and neither its stamp nor its maximum is ever consulted.", + "source-budgets": "Composes the local projection, which has no remote layer and dispatches no held read.", + "dark-layers": "Remote serving is always off and no caller retains a recovery candidate; dark C0 bytes belong to the shadow job, not a recovery snapshot.", + "shadow-read-deadlines": "All six scheduled histories and six invariants pass under this shared fault at 2000 samples by 40 steps. No scheduled assertion distinguishes preserving the original frame timestamp in a retained recovery candidate in this bounded read-deadline profile; the imported rule alone does not establish detection." + } } }, { @@ -1167,7 +1689,7 @@ "nativeMutants": { "kind": "mapped", "mutant": "M04", - "text": "M04 is the native twin of kernel/remote-writes.qnt dispatch skipping the fence recheck at dump release: both ports skip the post-serialization fence recheck in the write routine the shadow fill also uses, so a fenced dark fill writes and reports filled instead of fill_fenced. shadow/fenceRejudgedAtDumpReleaseAfterRollbackTest (a dump released after a wall rollback below the fence dispatches no write) is the expected evidence in both ports.", + "text": "M04 is the native twin of kernel/remote-writes.qnt dispatch skipping the fence recheck at dump release: both ports skip the post-serialization fence recheck in the write routine the shadow fill also uses, so a fenced dark fill writes and reports filled instead of fill_fenced.", "crossContract": "M04 skips the post-serialization fence in the caller write routine (C34.second-fence); the shadow fill reaches the same line through putForShadow (TS) and putRemote (Go), so under it a fenced dark fill writes and reports filled instead of fill_fenced (witness shadow:outcome:fill_fenced)." }, "reproducer": { @@ -1177,22 +1699,13 @@ "family": "fence-recheck-at-dispatch", "profiles": [ "shadow", - "effects" + "effects", + "dark-layers" ], "exclusions": { - "core": "Does not compose remote_writes, so the mutated dispatch is never executed.", - "scope": "Does not compose remote_writes, so the mutated dispatch is never executed.", - "recovery": "Does not compose remote_writes, so the mutated dispatch is never executed.", - "policy": "Does not compose remote_writes, so the mutated dispatch is never executed.", "admission": "Composes the held shadow lifecycle for served jobs, which always carry a C0 and never fill, so no dump is ever held and the mutated dispatch is never executed.", - "layers": "Does not compose remote_writes, so the mutated dispatch is never executed.", - "independent": "Does not compose remote_writes, so the mutated dispatch is never executed.", - "recovery-read": "Does not compose remote_writes, so the mutated dispatch is never executed.", - "local-failure": "Does not compose remote_writes, so the mutated dispatch is never executed.", - "runtime-boundaries": "Does not compose remote_writes, so the mutated dispatch is never executed.", "shadow-layers": "Composes remote_writes::dispatch but never raises a watermark (no invalidate input), so every fence is NO_FENCE and the recheck never refuses a write.", - "local-clock": "Does not compose remote_writes, so the mutated dispatch is never executed.", - "source-budgets": "Does not compose remote_writes, so the mutated dispatch is never executed." + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." } } }, @@ -1207,7 +1720,7 @@ "nativeMutants": { "kind": "mapped", "mutant": "M47", - "text": "M47 is the native twin of kernel/shadow.qnt settleJobRead filling at once when the job's source is still pending instead of waiting for it: both ports continue dark shadow work without waiting for the caller's source, comparing and filling with an undefined or zero value. shadow/darkReadBeforeSourceWaitsForItTest (no dump while the loader is held) and shadow-layers/darkSourcePublishesLocalBeforeShadowWriteTest are the expected evidence." + "text": "M47 is the native twin of kernel/shadow.qnt settleJobRead filling at once when the job's source is still pending instead of waiting for it: both ports continue dark shadow work without waiting for the caller's source, comparing and filling with an undefined or zero value." }, "reproducer": { "kind": "exported-regression", @@ -1215,52 +1728,122 @@ "failure": "s.o.calls == List(CALL_PENDING) and s.o.shadow == List() and s.o.dumps == 0 and s.o.loads == 0", "family": "fill-before-source", "profiles": [ - "shadow" + "shadow", + "dark-layers", + "shadow-read-deadlines" ], "exclusions": { - "core": "Does not compose the shadow module, so the mutated C0 read settlement is never executed.", - "effects": "Does not compose the shadow module, so the mutated C0 read settlement is never executed.", - "scope": "Does not compose the shadow module, so the mutated C0 read settlement is never executed.", - "recovery": "Does not compose the shadow module, so the mutated C0 read settlement is never executed.", - "policy": "Does not compose the shadow module, so the mutated C0 read settlement is never executed.", "admission": "Composes the held shadow lifecycle for served jobs only, whose C0 is the payload the hit served: no job ever holds a C0 read, so the acquisition arm the fault mutates is never executed.", - "layers": "Does not compose the shadow module, so the mutated C0 read settlement is never executed.", - "independent": "Does not compose the shadow module, so the mutated C0 read settlement is never executed.", - "recovery-read": "Does not compose the shadow module, so the mutated C0 read settlement is never executed.", - "local-failure": "Does not compose the shadow module, so the mutated C0 read settlement is never executed.", - "runtime-boundaries": "Does not compose the shadow module, so the mutated C0 read settlement is never executed.", - "shadow-layers": "Composes the atomic lifecycle: the dark C0 read is made at the release and the job waits in the registry until remote_writes::settle, so the held read settlement the fault mutates is never executed.", - "local-clock": "Does not compose the shadow module, so the mutated C0 read settlement is never executed.", - "source-budgets": "Does not compose the shadow module, so the mutated C0 read settlement is never executed." + "shadow-layers": "Composes the atomic lifecycle: the dark C0 read is made at the release and the job waits in the registry until remote_writes::settle, so the held read settlement the fault mutates is never executed." } } }, { "id": "redis-protocol-inclusive-fence", "contract": "C33", - "source": "formal/dialcache-redis-protocol.qnt", + "source": "formal/cache-rules.qnt", "model": "formal/dialcache-redis-protocol.qnt", "invariant": "fenceCanPrecedeEncodingError", - "before": "st.watermarkState == ValidWatermark and not(fenceAllows(st.frameTimestamp, st.watermarkValue)))", - "after": "st.watermarkState == ValidWatermark and st.frameTimestamp < st.watermarkValue)", + "before": "pure def readFenceAllows(tracked: bool, created: int, watermark: int): bool =\n not(tracked) or fenceAllows(created, watermark)", + "after": "pure def readFenceAllows(tracked: bool, created: int, watermark: int): bool =\n not(tracked) or created >= watermark", "nativeMutants": { "kind": "mapped", "mutant": "M06", - "text": "M06 makes the tracked decode fence inclusive in both ports, the exact site of dialcache-redis-protocol.qnt classify inlining a strict timestamp-below-watermark test in place of not fenceAllows in its WatermarkFenced branch. The generated tracked decode vectors and the redis-protocol fence-before-encoding-error ordering vectors already detect it in both ports." + "text": "M06 admits a frame at the tracked watermark in both codecs. The verifier classifier, generated frame decoder and behavioral remote reads consume the same readFenceAllows judgment; fenceCanPrecedeEncodingError remains independently stated in the original verifier. The exact native row matches the cited frame model run.", + "evidence": { + "vector": { + "artifact": "formal/quint-frame-vectors.json", + "group": "trackedDecodeVectors", + "rows": { + "typescript": "Quint frame 546 decode", + "go": "Quint frame 546 decode" + }, + "fields": [ + "kind", + "reason" + ], + "relation": "The named run executes requestFor(546), the exact generated tracked frame row: createdAtMs and watermark are both one. Its miss classification must precede serving the frame." + } + } + }, + "reproducer": { + "kind": "model-run", + "run": "frameAtWatermarkIsDeclinedTest", + "failure": "result.kind == \"miss\" and result.reason == \"watermark_fenced\" and result.fence == 1", + "family": "shared-tracked-read-fence", + "profiles": [ + "formal/dialcache-redis-protocol.qnt", + "formal/dialcache-frame-vectors.qnt", + "layers", + "independent", + "dark-layers", + "recovery-read" + ], + "exclusions": { + "effects": "This profile does not execute the shared tracked-read fence at timestamp equality with an observable value; scheduled histories and independent properties are measured under the fault.", + "recovery": "This profile does not execute the shared tracked-read fence at timestamp equality with an observable value; scheduled histories and independent properties are measured under the fault.", + "policy": "This profile does not execute the shared tracked-read fence at timestamp equality with an observable value; scheduled histories and independent properties are measured under the fault.", + "shadow": "This profile does not execute the shared tracked-read fence at timestamp equality with an observable value; scheduled histories and independent properties are measured under the fault.", + "scope": "This profile does not execute the shared tracked-read fence at timestamp equality with an observable value; scheduled histories and independent properties are measured under the fault.", + "admission": "This profile does not execute the shared tracked-read fence at timestamp equality with an observable value; scheduled histories and independent properties are measured under the fault.", + "local-failure": "This profile does not execute the shared tracked-read fence at timestamp equality with an observable value; scheduled histories and independent properties are measured under the fault.", + "runtime-boundaries": "This profile does not execute the shared tracked-read fence at timestamp equality with an observable value; scheduled histories and independent properties are measured under the fault.", + "shadow-layers": "This profile does not execute the shared tracked-read fence at timestamp equality with an observable value; scheduled histories and independent properties are measured under the fault.", + "local-clock": "This profile does not execute the shared tracked-read fence at timestamp equality with an observable value; scheduled histories and independent properties are measured under the fault.", + "source-budgets": "This profile does not execute the shared tracked-read fence at timestamp equality with an observable value; scheduled histories and independent properties are measured under the fault.", + "shadow-read-deadlines": "All six scheduled histories and six invariants pass under this shared fault at 2000 samples by 40 steps. No scheduled assertion distinguishes tracked read acceptance at watermark equality in this bounded read-deadline profile; the imported rule alone does not establish detection." + }, + "scope": "A deterministic protocol transform exports language-neutral vector inputs rather than an asynchronous driver history. Exact generated rows have their own native input/output recording and clean baseline in both ports. The named run executes requestFor(546), the exact generated tracked frame row: createdAtMs and watermark are both one. Its miss classification must precede serving the frame.", + "model": "formal/dialcache-frame-vectors.qnt" } }, { "id": "redis-protocol-untracked-fence", "contract": "W04", - "source": "formal/dialcache-redis-protocol.qnt", + "source": "formal/cache-rules.qnt", "model": "formal/dialcache-redis-protocol.qnt", "invariant": "untrackedDecodingIgnoresWatermarks", - "before": " else if (st.tracked and st.watermarkReply == BufferReply and\n st.watermarkState == ValidWatermark", - "after": " else if (st.watermarkReply == BufferReply and\n st.watermarkState == ValidWatermark", + "before": "pure def readFenceAllows(tracked: bool, created: int, watermark: int): bool =\n not(tracked) or fenceAllows(created, watermark)", + "after": "pure def readFenceAllows(tracked: bool, created: int, watermark: int): bool =\n fenceAllows(created, watermark)", "nativeMutants": { "kind": "mapped", "mutant": "M52", - "text": "M52 is the native twin of dialcache-redis-protocol.qnt classify dropping the tracked condition from its WatermarkFenced branch: both ports read and apply the entity watermark fence on untracked keys, the TypeScript twin one step earlier at read-request construction because its untracked decoder receives no watermark. Go TestProtocolDecoders untracked vectors (345 and its siblings) and the TypeScript witness layers:untracked-ignores-watermark plus layers replays are the expected evidence." + "text": "M52 applies entity watermarks to untracked reads at read-request construction in both ports. The original verifier classifier and behavioral remote read share readFenceAllows; untrackedDecodingIgnoresWatermarks remains independent. The public history returns both untracked values after grouped invalidation.", + "evidence": { + "history": "layers/untrackedReadsIgnoreGroupedInvalidationTest", + "step": 6, + "fields": [ + "o.calls" + ] + } + }, + "reproducer": { + "kind": "exported-regression", + "run": "untrackedReadsIgnoreGroupedInvalidationTest", + "model": "formal/dialcache-layers-conformance.qnt", + "failure": "s.o.calls == List(VALUE_ONE, VALUE_TWO) and s.o.loaders == 0\n and s.o.reads == 2 and s.o.loads == 2 and s.o.invalidations == 1", + "family": "untracked-read-ignores-fence", + "profiles": [ + "formal/dialcache-redis-protocol.qnt", + "layers", + "local-failure" + ], + "exclusions": { + "effects": "This profile does not execute the affected shared judgment with a distinguishing input; its scheduled histories and independent properties are measured under the same mutation.", + "recovery": "This profile does not execute the affected shared judgment with a distinguishing input; its scheduled histories and independent properties are measured under the same mutation.", + "policy": "This profile does not execute the affected shared judgment with a distinguishing input; its scheduled histories and independent properties are measured under the same mutation.", + "shadow": "This profile does not execute the affected shared judgment with a distinguishing input; its scheduled histories and independent properties are measured under the same mutation.", + "scope": "This profile does not execute the affected shared judgment with a distinguishing input; its scheduled histories and independent properties are measured under the same mutation.", + "admission": "This profile does not execute the affected shared judgment with a distinguishing input; its scheduled histories and independent properties are measured under the same mutation.", + "independent": "This profile does not execute the affected shared judgment with a distinguishing input; its scheduled histories and independent properties are measured under the same mutation.", + "recovery-read": "This profile does not execute the affected shared judgment with a distinguishing input; its scheduled histories and independent properties are measured under the same mutation.", + "runtime-boundaries": "This profile does not execute the affected shared judgment with a distinguishing input; its scheduled histories and independent properties are measured under the same mutation.", + "shadow-layers": "This profile does not execute the affected shared judgment with a distinguishing input; its scheduled histories and independent properties are measured under the same mutation.", + "local-clock": "This profile does not execute the affected shared judgment with a distinguishing input; its scheduled histories and independent properties are measured under the same mutation.", + "source-budgets": "This profile does not execute the affected shared judgment with a distinguishing input; its scheduled histories and independent properties are measured under the same mutation.", + "dark-layers": "This profile does not execute the affected shared judgment with a distinguishing input; its scheduled histories and independent properties are measured under the same mutation.", + "shadow-read-deadlines": "All six scheduled histories and six invariants pass under this shared fault at 2000 samples by 40 steps. No scheduled assertion distinguishes ignoring a grouped watermark on an untracked read in this bounded read-deadline profile; the imported rule alone does not establish detection." + } } }, { @@ -1269,12 +1852,38 @@ "source": "formal/dialcache-frame-vectors.qnt", "model": "formal/dialcache-frame-vectors.qnt", "invariant": "trackedHitsStrictlyClearValidFence", - "before": "else if (request.tracked and fence >= 0 and not(fenceAllows(timestamp, fence)))", + "before": "else if (request.tracked and fence >= 0 and not(readFenceAllows(request.tracked, timestamp, fence)))", "after": "else if (request.tracked and fence >= 0 and timestamp < fence)", "nativeMutants": { "kind": "mapped", "mutant": "M06", - "text": "M06 makes the tracked decode fence inclusive in both ports, the fault dialcache-frame-vectors.qnt decode inlines as a strict timestamp-below-fence test. The exported quint-frame-vectors trackedDecodeVectors are the very rows that detect M06 in both ports." + "text": "M06 makes the tracked decode fence inclusive in both ports, the fault dialcache-frame-vectors.qnt decode inlines as a strict timestamp-below-fence test. The exported quint-frame-vectors trackedDecodeVectors are the very rows that detect M06 in both ports.", + "evidence": { + "vector": { + "artifact": "formal/quint-frame-vectors.json", + "group": "trackedDecodeVectors", + "rows": { + "typescript": "Quint frame 546 decode", + "go": "Quint frame 546 decode" + }, + "fields": [ + "kind", + "reason" + ], + "relation": "The named run executes requestFor(546), the exact generated tracked frame row: createdAtMs and watermark are both one. Its miss classification must precede serving the frame." + } + } + }, + "reproducer": { + "kind": "model-run", + "run": "frameAtWatermarkIsDeclinedTest", + "failure": "result.kind == \"miss\" and result.reason == \"watermark_fenced\" and result.fence == 1", + "family": "frame-fence-equality", + "profiles": [ + "formal/dialcache-frame-vectors.qnt" + ], + "exclusions": {}, + "scope": "A deterministic protocol transform exports language-neutral vector inputs rather than an asynchronous driver history. Exact generated rows have their own native input/output recording and clean baseline in both ports. The named run executes requestFor(546), the exact generated tracked frame row: createdAtMs and watermark are both one. Its miss classification must precede serving the frame." } }, { @@ -1284,7 +1893,37 @@ "model": "formal/dialcache-invalidation-transition.qnt", "invariant": "successfulCutoffIsExactMaximum", "before": "val cutoff = if (priorCutoff > proposed) priorCutoff else proposed", - "after": "val cutoff = proposed" + "after": "val cutoff = proposed", + "nativeMutants": { + "kind": "mapped", + "mutant": "M63", + "text": "M63 changes the production Lua in src/internal/redis-scripts.ts and go/redis_adapter.go at the same invalidation boundary as this model fault. The generated Redis cohort executes the actual scripts, and the exact selected row records their public response and stored state.", + "evidence": { + "vector": { + "artifact": "formal/quint-invalidation-vectors.json", + "group": "vectors", + "rows": { + "typescript": "Quint 002: zero buffer / future finite", + "go": "Quint 002: zero buffer / future finite" + }, + "fields": [ + "content" + ], + "relation": "The named run applies case 2, exactly the generated row with an existing 10000000 cutoff and an older 1000 proposal. The returned stored cutoff must remain 10000000." + } + } + }, + "reproducer": { + "kind": "model-run", + "run": "futureCutoffDoesNotMoveBackwardsTest", + "failure": "s.after.value == decimalText(10000000) and s.after.ttlMs == 13659000", + "family": "invalidation-maximum-cutoff", + "profiles": [ + "formal/dialcache-invalidation-transition.qnt" + ], + "exclusions": {}, + "scope": "Atomic Redis invalidation is a deterministic protocol transition, exported as generated decimal inputs and stored-state vectors. Native workers execute production Lua against a real Redis server; expected results stay in the coordinator. The named run applies case 2, exactly the generated row with an existing 10000000 cutoff and an older 1000 proposal. The returned stored cutoff must remain 10000000." + } }, { "id": "invalidation-transition-inclusive-buffer-limit", @@ -1293,7 +1932,37 @@ "model": "formal/dialcache-invalidation-transition.qnt", "invariant": "argumentValidationIsExact", "before": "if (not(buffer.valid) or buffer.value > MAX_BUFFER_MS or", - "after": "if (not(buffer.valid) or buffer.value >= MAX_BUFFER_MS or" + "after": "if (not(buffer.valid) or buffer.value >= MAX_BUFFER_MS or", + "nativeMutants": { + "kind": "mapped", + "mutant": "M64", + "text": "M64 changes the production Lua in src/internal/redis-scripts.ts and go/redis_adapter.go at the same invalidation boundary as this model fault. The generated Redis cohort executes the actual scripts, and the exact selected row records their public response and stored state.", + "evidence": { + "vector": { + "artifact": "formal/quint-invalidation-vectors.json", + "group": "vectors", + "rows": { + "typescript": "Quint 036: maximum buffer / absent", + "go": "Quint 036: maximum buffer / absent" + }, + "fields": [ + "outcome" + ], + "relation": "The named run first applies case 36, exactly the generated row with the maximum 31536000000 buffer and no existing marker. The script must accept equality and install its buffered cutoff." + } + } + }, + "reproducer": { + "kind": "model-run", + "run": "maximumBufferAndTimestampSumAreAcceptedTest", + "failure": "s.outcome == SUCCESS and s.after.ttlMs == 31539660000", + "family": "invalidation-maximum-buffer", + "profiles": [ + "formal/dialcache-invalidation-transition.qnt" + ], + "exclusions": {}, + "scope": "Atomic Redis invalidation is a deterministic protocol transition, exported as generated decimal inputs and stored-state vectors. Native workers execute production Lua against a real Redis server; expected results stay in the coordinator. The named run first applies case 36, exactly the generated row with the maximum 31536000000 buffer and no existing marker. The script must accept equality and install its buffered cutoff." + } }, { "id": "key-protocol-untracked-brace-rejection", @@ -1306,7 +1975,32 @@ "nativeMutants": { "kind": "mapped", "mutant": "M50", - "text": "M50 is the native twin of dialcache-key-protocol.qnt validKey dropping its untracked disjunct: both ports reject braces in keyType and id for untracked keys as well as tracked ones. quint-key-vectors 050, 052, 062, 064, 411 and 413 (untracked keys with braces expected valid) are the expected evidence in both ports." + "text": "M50 is the native twin of dialcache-key-protocol.qnt validKey dropping its untracked disjunct: both ports reject braces in keyType and id for untracked keys as well as tracked ones. quint-key-vectors 050, 052, 062, 064, 411 and 413 (untracked keys with braces expected valid) are the expected evidence in both ports.", + "evidence": { + "vector": { + "artifact": "formal/quint-key-vectors.json", + "group": "keyVectors", + "rows": { + "typescript": "Quint key 050: opening brace", + "go": "Quint key 050: opening brace" + }, + "fields": [ + "kind" + ], + "relation": "The named run executes inputFor(50), the exact generated untracked identity with an opening brace as keyType. Both bindings must construct that identity successfully." + } + } + }, + "reproducer": { + "kind": "model-run", + "run": "untrackedTypeBraceRemainsValidTest", + "failure": "s.key.valid", + "family": "untracked-brace-acceptance", + "profiles": [ + "formal/dialcache-key-protocol.qnt" + ], + "exclusions": {}, + "scope": "A deterministic protocol transform exports language-neutral vector inputs rather than an asynchronous driver history. Exact generated rows have their own native input/output recording and clean baseline in both ports. The named run executes inputFor(50), the exact generated untracked identity with an opening brace as keyType. Both bindings must construct that identity successfully." } }, { @@ -1320,7 +2014,27 @@ "nativeMutants": { "kind": "mapped", "mutant": "M51", - "text": "M51 is the native twin of cohort-boundaries.qnt cohortAdmits becoming inclusive: both ports admit a key whose cohort sample equals the configured ramp. The rampVectors pin hash samples, not admission, so detection is behavioral through runtime-boundaries/localCohortEqualityBypassesBeforeAboveSampleCachesTest and remoteCohortEqualityBypassesBeforeAboveSampleCachesTest, whose fixture ramps equal the driver key's FNV numerators." + "text": "M51 is the native twin of cohort-boundaries.qnt cohortAdmits becoming inclusive: both ports admit a key whose cohort sample equals the configured ramp. The rampVectors pin hash samples, not admission, so detection is behavioral through runtime-boundaries/localCohortEqualityBypassesBeforeAboveSampleCachesTest and remoteCohortEqualityBypassesBeforeAboveSampleCachesTest, whose fixture ramps equal the driver key's FNV numerators.", + "evidence": { + "history": "runtime-boundaries/localCohortEqualityLeavesNoValueForLaterAdmissionTest", + "step": 7, + "fields": [ + "o.calls" + ] + } + }, + "reproducer": { + "kind": "exported-regression", + "run": "localCohortEqualityLeavesNoValueForLaterAdmissionTest", + "model": "formal/dialcache-runtime-boundaries-conformance.qnt", + "failure": "s.o.calls == List(VALUE_ONE, CALL_PENDING) and s.o.loaders == 2", + "family": "strict-cohort-equality", + "profiles": [ + "formal/dialcache-key-protocol.qnt", + "runtime-boundaries", + "shadow-layers" + ], + "exclusions": {} } }, { @@ -1334,7 +2048,33 @@ "nativeMutants": { "kind": "mapped", "mutant": "M55", - "text": "M55 is the native twin of dialcache-envelope-vectors.qnt selectWrite treating a size tie as smaller: both ports store a compressed payload whose marked size ties the escaped raw size. The compressionWriteVectors tie rows, which record per-binding codec sizes, are the expected evidence in both ports." + "text": "M55 is the native twin of dialcache-envelope-vectors.qnt selectWrite treating a size tie as smaller: both ports store a compressed payload whose marked size ties the escaped raw size. The compressionWriteVectors tie rows, which record per-binding codec sizes, are the expected evidence in both ports.", + "evidence": { + "vector": { + "artifact": "formal/quint-envelope-vectors.json", + "group": "compressionWriteVectors", + "rows": { + "typescript": "Quint envelope 192 write text-size-boundary", + "go": "Quint envelope 219 write escaped-size-boundary" + }, + "fields": [ + "outcome", + "marker" + ], + "relation": "The primary run executes requestFor(192), the TypeScript native codec tie; the independently scheduled goCompressionTieKeepsRawRepresentationTest executes requestFor(219), the Go native codec tie. Both require strict shrink against escaped raw size; codec sizes differ legitimately." + } + } + }, + "reproducer": { + "kind": "model-run", + "run": "compressionTieKeepsRawRepresentationTest", + "failure": "result.typescript.outcome == \"not_smaller\" and result.typescript.storedBytes == 18", + "family": "compression-strict-shrink", + "profiles": [ + "formal/dialcache-envelope-vectors.qnt" + ], + "exclusions": {}, + "scope": "A deterministic protocol transform exports language-neutral vector inputs rather than an asynchronous driver history. Exact generated rows have their own native input/output recording and clean baseline in both ports. The primary run executes requestFor(192), the TypeScript native codec tie; the independently scheduled goCompressionTieKeepsRawRepresentationTest executes requestFor(219), the Go native codec tie. Both require strict shrink against escaped raw size; codec sizes differ legitimately." } }, { @@ -1348,7 +2088,32 @@ "nativeMutants": { "kind": "mapped", "mutant": "M53", - "text": "M53 is the native twin of dialcache-envelope-vectors.qnt readEnvelope lowering the escapable marker range to exclude the binary-zstd marker: both ports leave the escape prefix in place before an escaped binary-zstd marker. Envelope vectors 008, 003 and 013 are the expected evidence in both ports." + "text": "M53 is the native twin of dialcache-envelope-vectors.qnt readEnvelope lowering the escapable marker range to exclude the binary-zstd marker: both ports leave the escape prefix in place before an escaped binary-zstd marker. Envelope vectors 008, 003 and 013 are the expected evidence in both ports.", + "evidence": { + "vector": { + "artifact": "formal/quint-envelope-vectors.json", + "group": "envelopeVectors", + "rows": { + "typescript": "Quint envelope 008 envelope", + "go": "Quint envelope 008 envelope" + }, + "fields": [ + "decodedHex" + ], + "relation": "The named run executes requestFor(8), the exact generated byte sequence 0002. Decoding strips precisely one escape prefix and returns the literal binary marker byte02." + } + } + }, + "reproducer": { + "kind": "model-run", + "run": "escapedBinaryMarkerRemainsLiteralTest", + "failure": "result.read.bytes == List(2)", + "family": "escaped-binary-marker", + "profiles": [ + "formal/dialcache-envelope-vectors.qnt" + ], + "exclusions": {}, + "scope": "A deterministic protocol transform exports language-neutral vector inputs rather than an asynchronous driver history. Exact generated rows have their own native input/output recording and clean baseline in both ports. The named run executes requestFor(8), the exact generated byte sequence 0002. Decoding strips precisely one escape prefix and returns the literal binary marker byte02." } }, { @@ -1362,7 +2127,24 @@ "nativeMutants": { "kind": "mapped", "mutant": "M17", - "text": "M17 is the native reading of dialcache-conformance.qnt localCall recording the environment's sourceVersion as the result of a local hit: no port can read that version without invoking the loader, so the twin is a hit that returns something other than the stored value while loader counts stay unchanged. core/localValueSurvivesSourceChangeTest detects both ports in the generated cohort." + "text": "M17 is the native reading of dialcache-conformance.qnt localCall recording the environment's sourceVersion as the result of a local hit: no port can read that version without invoking the loader, so the twin is a hit that returns something other than the stored value while loader counts stay unchanged. core/localValueSurvivesSourceChangeTest detects both ports in the generated cohort.", + "evidence": { + "history": "core/localValueSurvivesSourceChangeTest", + "step": 3, + "fields": [ + "lastResult" + ] + } + }, + "reproducer": { + "kind": "exported-regression", + "run": "localValueSurvivesSourceChangeTest", + "failure": "s.lastResult == 1 and s.localLoaderCalls == 1", + "family": "local-hit-stored-value", + "profiles": [ + "core" + ], + "exclusions": {} } }, { @@ -1376,7 +2158,24 @@ "nativeMutants": { "kind": "mapped", "mutant": "M38", - "text": "M38 is the native twin of dialcache-conformance.qnt remoteCall dropping remoteReadable from its miss branch: both ports skip the Redis publication after a semantic miss, including a decode failure, so only retained-candidate misses refill. core/invalidationRefillsFromCurrentSourceTest (redisWrites 3 and a later hit) and effects/nullReplyRefillsAndIsReadableTest are the expected evidence in both ports." + "text": "M38 is the native twin of dialcache-conformance.qnt remoteCall dropping remoteReadable from its miss branch: both ports skip the Redis publication after a semantic miss, including a decode failure, so only retained-candidate misses refill. core/invalidationRefillsFromCurrentSourceTest (redisWrites 3 and a later hit) and effects/nullReplyRefillsAndIsReadableTest are the expected evidence in both ports.", + "evidence": { + "history": "core/invalidationRefillsFromCurrentSourceTest", + "step": 3, + "fields": [ + "lastResult" + ] + } + }, + "reproducer": { + "kind": "exported-regression", + "run": "invalidationRefillsFromCurrentSourceTest", + "failure": "s.lastResult == 1 and s.remoteLoaderCalls == 1", + "family": "remote-miss-publication", + "profiles": [ + "core" + ], + "exclusions": {} } }, { @@ -1389,7 +2188,7 @@ "after": "accepted(value) and source.retentionMs > 0", "reproducer": { "kind": "exported-regression", - "run": "absentReplyWithFutureFenceBlocksRefillTest", + "run": "fencedSourceSkipsSerializationTest", "failure": "s.o.calls == List(VALUE_ONE) and s.o.dumps == 0 and s.o.writes == 0", "family": "first-fence-check", "profiles": [ @@ -1398,24 +2197,25 @@ "recovery-read" ], "exclusions": { - "core": "Does not import the kernel library, so the mutated refill authority is never executed; its own fence check is stated in its own text.", - "policy": "Composes refills, but no scheduled regression and no sampled history at 2000 x 40 settles an accepted source while its fence is at or ahead of the wall clock (measured 2026-09-20 over the composed text: every scheduled invariant and regression passes under the mutant).", + "policy": "Composes refills, but no scheduled regression and no sampled history at 2000 x 40 settles an accepted source while its fence is at or ahead of the wall clock.", "shadow": "Every reply leaves the remote layer off, so no caller source refills; the dark fill judges its fence through refillAllowed, which the mutant leaves intact.", "scope": "The request-only projection never refills the remote layer, so refills is never judged.", "admission": "Every read serves a hit and a served job's detached source never fills, so refills is never judged for a refill.", - "layers": "Composes refills, but no scheduled regression and no sampled history at 2000 x 40 settles an accepted source while its fence is at or ahead of the wall clock (measured 2026-09-20 over the composed text: every scheduled invariant and regression passes under the mutant).", - "independent": "Composes refills, but no scheduled regression and no sampled history at 2000 x 40 settles an accepted source while its fence is at or ahead of the wall clock (measured 2026-09-20 over the composed text: every scheduled invariant and regression passes under the mutant).", - "local-failure": "Composes refills, but no scheduled regression and no sampled history at 2000 x 40 settles an accepted source while its fence is at or ahead of the wall clock (measured 2026-09-20 over the composed text: every scheduled invariant and regression passes under the mutant).", - "runtime-boundaries": "Composes refills, but no scheduled regression and no sampled history at 2000 x 40 settles an accepted source while its fence is at or ahead of the wall clock (measured 2026-09-20 over the composed text: every scheduled invariant and regression passes under the mutant).", - "shadow-layers": "Composes refills, but no scheduled regression and no sampled history at 2000 x 40 settles an accepted source while its fence is at or ahead of the wall clock (measured 2026-09-20 over the composed text: every scheduled invariant and regression passes under the mutant).", + "layers": "Composes refills, but no scheduled regression and no sampled history at 2000 x 40 settles an accepted source while its fence is at or ahead of the wall clock.", + "independent": "Composes refills, but no scheduled regression and no sampled history at 2000 x 40 settles an accepted source while its fence is at or ahead of the wall clock.", + "local-failure": "Composes refills, but no scheduled regression and no sampled history at 2000 x 40 settles an accepted source while its fence is at or ahead of the wall clock.", + "runtime-boundaries": "Composes refills, but no scheduled regression and no sampled history at 2000 x 40 settles an accepted source while its fence is at or ahead of the wall clock.", + "shadow-layers": "Composes refills, but no scheduled regression and no sampled history at 2000 x 40 settles an accepted source while its fence is at or ahead of the wall clock.", "local-clock": "The local projection has no remote layer, so refills is never judged.", - "source-budgets": "The local projection never refills the remote layer, so refills is never judged." + "source-budgets": "The local projection never refills the remote layer, so refills is never judged.", + "dark-layers": "Caller remote serving is off, so callers capture no remote-refill authority. Dark fills apply the shadow job fence independently of this caller-publication guard.", + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." } }, "nativeMutants": { "kind": "mapped", "mutant": "M39", - "text": "M39 is the native twin of kernel/serving.qnt refills dropping the fence half of refillAllowed: both ports drop the pre-serialization fence check, so a source accepted behind an observed fence is serialized while the post-serialization recheck still blocks the write. effects/absentReplyWithFutureFenceBlocksRefillTest (dumps 0 becomes 1) and witness effects:normalized-fence-blocks-publication are the expected evidence in both ports." + "text": "M39 is the native twin of kernel/serving.qnt refills dropping the fence half of refillAllowed: both ports drop the pre-serialization fence check, so a source accepted behind an observed fence is serialized while the post-serialization recheck still blocks the write." } }, { @@ -1436,25 +2236,20 @@ "shadow" ], "exclusions": { - "core": "Does not import the kernel library, so the mutated arrival is never executed; its own deadline rule is stated in its own text.", - "recovery": "Composes the budgeted lifecycle, but no scheduled regression and no sampled history at 2000 x 40 resolves a loader after its deadline was reached undelivered: its late results are timer-delivered or drained (measured 2026-09-20 over the composed text: every scheduled invariant and regression passes under the mutant).", - "policy": "Does not compose the deadlines module: its sources have no budget, so arrival never runs.", + "recovery": "Composes the budgeted lifecycle, but no scheduled regression and no sampled history at 2000 x 40 resolves a loader after its deadline was reached undelivered: its late results are timer-delivered or drained.", "scope": "Composes the request-only projection, which registers no deadline (deadlines reaches its closure through diagnostics alone), so arrival never runs.", - "admission": "Composes the budgeted lifecycle, but no scheduled regression and no sampled history at 2000 x 40 resolves a loader after its deadline was reached undelivered: its late results are timer-delivered or drained (measured 2026-09-20 over the composed text: every scheduled invariant and regression passes under the mutant).", - "layers": "Does not compose the deadlines module: its sources have no budget, so arrival never runs.", - "independent": "Composes the budgeted lifecycle, but no scheduled regression and no sampled history at 2000 x 40 resolves a loader after its deadline was reached undelivered: its late results are timer-delivered or drained (measured 2026-09-20 over the composed text: every scheduled invariant and regression passes under the mutant).", - "recovery-read": "Does not compose the deadlines module: its sources have no budget, so arrival never runs.", - "local-failure": "Does not compose the deadlines module: its sources have no budget, so arrival never runs.", - "runtime-boundaries": "Does not compose the deadlines module: its sources have no budget, so arrival never runs.", - "shadow-layers": "Composes the budgeted lifecycle, but no scheduled regression and no sampled history at 2000 x 40 resolves a loader after its deadline was reached undelivered: its late results are timer-delivered or drained (measured 2026-09-20 over the composed text: every scheduled invariant and regression passes under the mutant).", - "local-clock": "Does not compose the deadlines module: its sources have no budget, so arrival never runs.", - "source-budgets": "Its late settlements are delivered deadlines: arrival never finds a reached deadline undelivered (measured 2026-09-20 over the composed text: every scheduled invariant and regression passes under the mutant)." + "admission": "Composes the budgeted lifecycle, but no scheduled regression and no sampled history at 2000 x 40 resolves a loader after its deadline was reached undelivered: its late results are timer-delivered or drained.", + "independent": "Composes the budgeted lifecycle, but no scheduled regression and no sampled history at 2000 x 40 resolves a loader after its deadline was reached undelivered: its late results are timer-delivered or drained.", + "shadow-layers": "Composes the budgeted lifecycle, but no scheduled regression and no sampled history at 2000 x 40 resolves a loader after its deadline was reached undelivered: its late results are timer-delivered or drained.", + "source-budgets": "Its late settlements are delivered deadlines: arrival never finds a reached deadline undelivered.", + "dark-layers": "Elapsed-time advances deliver source deadlines before later settlements; this profile has no clock jump without timer delivery to expose the arrival-time check independently.", + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." } }, "nativeMutants": { "kind": "mapped", "mutant": "M03", - "text": "M03 is the native twin of kernel/deadlines.qnt arrival accepting a result whose deadline was reached undelivered: both ports accept an externally settled result after its elapsed deadline when the timer has not run. effects/lateSourceResultIsADeadlineErrorTest and the late-resolve and late-reject witnesses are the expected evidence in both ports." + "text": "M03 is the native twin of kernel/deadlines.qnt arrival accepting a result whose deadline was reached undelivered: both ports accept an externally settled result after its elapsed deadline when the timer has not run." } }, { @@ -1468,6 +2263,32 @@ "nativeMutants": { "kind": "unobservable", "text": "The closure guard on the memo write (TypeScript RequestLocalCache.set in src/context.ts; Go the owner-live check before the memo write in the run closure of go/engine.go GetOrLoad) is the native line of kernel/request-memo.qnt memoize memoizing into a closed scope, but removing it changes no public history: both ports re-check closure on the memo read (TypeScript RequestLocalCache.read in src/context.ts; the same Go run closure serves the memo only while the owner is live) and bypass closed scopes at admission, mirroring the kernel's admission bypass. Measured once while this table was authored, the edit survived every scope and layers history in both ports; the model detects the fault only through the state invariant closedScopesHaveNoMemo, so the challenge is unobservable rather than mapped." + }, + "reproducer": { + "kind": "exported-regression", + "run": "lateSourceCannotPopulateReplacementTest", + "failure": "s.o.calls == List(VALUE_ONE, CALL_PENDING) and s.o.loaders == 2 and s.memo == List(NO_VALUE, NO_VALUE)", + "family": "closed-scope-memo-publication", + "profiles": [ + "recovery", + "scope", + "layers", + "recovery-read" + ], + "exclusions": { + "effects": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. This profile does not observe a memo populated after its request scope closes; the shared module being imported alone does not establish detection.", + "policy": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. This profile does not observe a memo populated after its request scope closes; the shared module being imported alone does not establish detection.", + "shadow": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. This profile does not observe a memo populated after its request scope closes; the shared module being imported alone does not establish detection.", + "admission": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. This profile does not observe a memo populated after its request scope closes; the shared module being imported alone does not establish detection.", + "independent": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. This profile does not observe a memo populated after its request scope closes; the shared module being imported alone does not establish detection.", + "local-failure": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. This profile does not observe a memo populated after its request scope closes; the shared module being imported alone does not establish detection.", + "runtime-boundaries": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. This profile does not observe a memo populated after its request scope closes; the shared module being imported alone does not establish detection.", + "shadow-layers": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. This profile does not observe a memo populated after its request scope closes; the shared module being imported alone does not establish detection.", + "local-clock": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. This profile does not observe a memo populated after its request scope closes; the shared module being imported alone does not establish detection.", + "source-budgets": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. This profile does not observe a memo populated after its request scope closes; the shared module being imported alone does not establish detection.", + "dark-layers": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. This profile does not observe a memo populated after its request scope closes; the shared module being imported alone does not establish detection.", + "shadow-read-deadlines": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. This profile does not observe a memo populated after its request scope closes; the shared module being imported alone does not establish detection." + } } }, { @@ -1481,7 +2302,34 @@ "nativeMutants": { "kind": "mapped", "mutant": "M25", - "text": "M25 is the native form of kernel/serving.qnt finish memoizing a non-accepted value by dropping its accepted guard: neither port's memo can hold an error, so the request in-flight registry is the only request-scoped error carrier, and keeping a rejected request-local flight registered makes a later same-scope caller receive the rejection instead of retrying. scope/rejectedFlightAllowsRetryTest and the witnesses scope:rejected-flight-retry and shared-rejection are the expected evidence." + "text": "M25 preserves a rejected request-local flight in both ports, making the next same-scope caller receive that error instead of retrying. The kernel fault memoizes a non-accepted value by dropping the accepted guard. The exported held dark-layers history checks a rejected source followed by a new loader and read." + }, + "reproducer": { + "kind": "exported-regression", + "model": "formal/dialcache-dark-layers-conformance.qnt", + "run": "rejectedDarkSourceSeedsNoLayerTest", + "failure": "s.o.calls == List(SOURCE_ERROR, CALL_PENDING) and s.o.loaders == 2 and s.o.reads == 2 and s.d.coalesced == List()", + "family": "failed-source-retry", + "profiles": [ + "scope", + "local-failure", + "dark-layers" + ], + "exclusions": { + "effects": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after memoizing a rejected source result. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "recovery": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after memoizing a rejected source result. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "policy": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after memoizing a rejected source result. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "shadow": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after memoizing a rejected source result. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "admission": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after memoizing a rejected source result. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "layers": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after memoizing a rejected source result. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "independent": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after memoizing a rejected source result. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "recovery-read": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after memoizing a rejected source result. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "runtime-boundaries": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after memoizing a rejected source result. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "shadow-layers": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after memoizing a rejected source result. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "local-clock": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after memoizing a rejected source result. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "source-budgets": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after memoizing a rejected source result. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." + } } }, { @@ -1499,13 +2347,11 @@ "family": "nested-close-evicts-outer-memo", "profiles": [ "scope", - "layers", "recovery-read" ], "exclusions": { - "core": "Composes no kernel module, so request_memo.closeScope is never executed.", "effects": "No request scope (persistentContexts 0), so request_memo.closeScope never runs.", - "recovery": "Closes a scope only through closeScope while the other scope's memo is empty in every scheduled history, so clearing every row clears exactly the closing scope's (measured 2026-09-20).", + "recovery": "Closes a scope only through closeScope while the other scope's memo is empty in every scheduled history, so clearing every row clears exactly the closing scope's.", "policy": "Has no request scopes (persistentContexts 0 and no closeScope input), so the memo fold the fault mutates is never executed.", "shadow": "No request scope (persistentContexts 0) and no closeScope input.", "admission": "No scope is persistent (persistentContexts 0) and no input closes one, so request_memo.closeScope is never executed.", @@ -1514,13 +2360,16 @@ "runtime-boundaries": "One persistent scope owns the only memo row, so a close that clears every row clears exactly its own.", "shadow-layers": "Composes the request memo but has no closeScope input.", "local-clock": "Composes the local projection with no request scope (persistentContexts 0) and no closeScope input.", - "source-budgets": "Composes the local projection with no request scope: no input opens or closes a scope and every reply disables the request layer, so request_memo.closeScope is never executed." + "source-budgets": "Composes the local projection with no request scope: no input opens or closes a scope and every reply disables the request layer, so request_memo.closeScope is never executed.", + "dark-layers": "The fault can clear peer request memos, but named histories never probe a previously populated peer memo after closing another scope. memoHoldsAcceptedValues permits empty rows; named runs and bounded invariant checks pass.", + "layers": "Closing a scope can clear another open scope under this fault, but no scheduled layers regression closes a scope or probes another memo afterward. Scheduled invariants constrain closed rows and accepted ownership, not preservation of open memo rows; named runs and the bounded invariant check pass.", + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." } }, "nativeMutants": { "kind": "mapped", "mutant": "M15", - "text": "M15 is the native reading of kernel/request-memo.qnt closeScope clearing every memo row instead of only the closing context's: a completed nested enable evicts the enclosing request's memo while the outer scope stays open. A nested handle can only reach its own enclosing row, so the port fault evicts one outer row rather than all rows; scope/nestedCloseAndDisabledBypassKeepOuterMemoTest step 11 detects both ports." + "text": "M15 is the native reading of kernel/request-memo.qnt closeScope clearing every memo row instead of only the closing context's: a completed nested enable evicts the enclosing request's memo while the outer scope stays open. A nested handle can only reach its own enclosing row, so the port fault evicts one outer row rather than all rows." } }, { @@ -1541,25 +2390,15 @@ "shadow-layers" ], "exclusions": { - "core": "Does not compose the shadow module, so the mutated admission judgment is never executed.", - "effects": "Does not compose the shadow module, so the mutated admission judgment is never executed.", - "recovery": "Does not compose the shadow module, so the mutated admission judgment is never executed.", - "policy": "Does not compose the shadow module, so the mutated admission judgment is never executed.", "shadow": "Composes the shadow module, but admits one job at a time (a call begins only after the previous job drained), so busy is judged against an empty registry and the mutated judgment never changes an admission.", - "scope": "Does not compose the shadow module, so the mutated admission judgment is never executed.", - "layers": "Does not compose the shadow module, so the mutated admission judgment is never executed.", - "independent": "Does not compose the shadow module, so the mutated admission judgment is never executed.", - "recovery-read": "Does not compose the shadow module, so the mutated admission judgment is never executed.", - "local-failure": "Does not compose the shadow module, so the mutated admission judgment is never executed.", - "runtime-boundaries": "Does not compose the shadow module, so the mutated admission judgment is never executed.", - "local-clock": "Does not compose the shadow module, so the mutated admission judgment is never executed.", - "source-budgets": "Does not compose the shadow module, so the mutated admission judgment is never executed." + "dark-layers": "One job fills the instance capacity of one, so the unchanged full-capacity check still drops a duplicate even when the duplicate test is removed.", + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." } }, "nativeMutants": { "kind": "mapped", "mutant": "M48", - "text": "M48 is the native twin of kernel/shadow.qnt busy judged without its duplicate disjunct (admitServed and the dark arm of admitJobs both consult it): both ports admit a shadow job for an identity that already has a live job, subject only to capacity. admission/duplicateDropsBeforeCapacityIsFullTest and shadow-layers/deduplicatedJobKeepsIndependentCallerSourcesTest are the expected evidence in both ports." + "text": "M48 is the native twin of kernel/shadow.qnt busy judged without its duplicate disjunct (admitServed and the dark arm of admitJobs both consult it): both ports admit a shadow job for an identity that already has a live job, subject only to capacity." } }, { @@ -1577,28 +2416,18 @@ "family": "capacity-off-by-one", "profiles": [ "admission", - "shadow-layers" + "shadow-layers", + "dark-layers", + "shadow-read-deadlines" ], "exclusions": { - "core": "Does not compose the shadow module, so the mutated admission judgment is never executed.", - "effects": "Does not compose the shadow module, so the mutated admission judgment is never executed.", - "recovery": "Does not compose the shadow module, so the mutated admission judgment is never executed.", - "policy": "Does not compose the shadow module, so the mutated admission judgment is never executed.", - "shadow": "Composes the shadow module, but admits one job at a time (a call begins only after the previous job drained), so full is judged against an empty registry and the mutated comparison never changes an admission.", - "scope": "Does not compose the shadow module, so the mutated admission judgment is never executed.", - "layers": "Does not compose the shadow module, so the mutated admission judgment is never executed.", - "independent": "Does not compose the shadow module, so the mutated admission judgment is never executed.", - "recovery-read": "Does not compose the shadow module, so the mutated admission judgment is never executed.", - "local-failure": "Does not compose the shadow module, so the mutated admission judgment is never executed.", - "runtime-boundaries": "Does not compose the shadow module, so the mutated admission judgment is never executed.", - "local-clock": "Does not compose the shadow module, so the mutated admission judgment is never executed.", - "source-budgets": "Does not compose the shadow module, so the mutated admission judgment is never executed." + "shadow": "Composes the shadow module, but admits one job at a time (a call begins only after the previous job drained), so full is judged against an empty registry and the mutated comparison never changes an admission." } }, "nativeMutants": { "kind": "mapped", "mutant": "M49", - "text": "M49 is the native twin of kernel/shadow.qnt full comparing the instance's live jobs strictly against its capacity: both ports admit one shadow job beyond the per-instance capacity. admission/fullInstanceDoesNotBlockOtherInstanceTest and shadow-layers/fullMixedInstanceDoesNotBlockAnotherInstanceTest are the expected evidence in both ports." + "text": "M49 is the native twin of kernel/shadow.qnt full comparing the instance's live jobs strictly against its capacity: both ports admit one shadow job beyond the per-instance capacity." } }, { @@ -1612,7 +2441,34 @@ "nativeMutants": { "kind": "mapped", "mutant": "M20", - "text": "M20 is the native twin of kernel/serving.qnt identity dropping the instance term from the flight identity: both ports register process flights in one process-wide registry, so a pending flight on one instance is joined from another. The C11.distinct-keys quintReplay survives, so detection rests on sampled layers histories and the witness layers:different-instances-own-distinct-flights credited under C08.instance-capacity." + "text": "M20 is the native twin of kernel/serving.qnt identity dropping the instance term from the flight identity: both ports register process flights in one process-wide registry, so a pending flight on one instance is joined from another. The held dark-layers history replays the same public boundary in both ports." + }, + "reproducer": { + "kind": "exported-regression", + "model": "formal/dialcache-dark-layers-conformance.qnt", + "run": "sameKeyCallersOnTwoInstancesKeepSeparateFlightsTest", + "failure": "s.o.calls == List(CALL_PENDING, CALL_PENDING) and s.o.loaders == 2 and s.o.reads == 2 and s.o.shadow == List() and s.d.coalesced == List()", + "family": "instance-flight-identity", + "profiles": [ + "layers", + "admission", + "dark-layers" + ], + "exclusions": { + "effects": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after removing the instance from pending process-flight identity. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "recovery": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after removing the instance from pending process-flight identity. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "policy": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after removing the instance from pending process-flight identity. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "shadow": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after removing the instance from pending process-flight identity. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "scope": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after removing the instance from pending process-flight identity. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "independent": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after removing the instance from pending process-flight identity. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "recovery-read": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after removing the instance from pending process-flight identity. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "local-failure": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after removing the instance from pending process-flight identity. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "runtime-boundaries": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after removing the instance from pending process-flight identity. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "shadow-layers": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after removing the instance from pending process-flight identity. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "local-clock": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after removing the instance from pending process-flight identity. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "source-budgets": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after removing the instance from pending process-flight identity. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." + } } }, { @@ -1627,6 +2483,33 @@ "nativeMutants": { "kind": "unobservable", "text": "The closure guard on the memo write (TypeScript RequestLocalCache.set in src/context.ts; Go the owner-live check before the memo write in the run closure of go/engine.go GetOrLoad) is the native line of kernel/request-memo.qnt memoize memoizing into a closed scope, but removing it changes no public history: both ports re-check closure on the memo read (TypeScript RequestLocalCache.read in src/context.ts; the same Go run closure serves the memo only while the owner is live) and bypass closed scopes at admission, mirroring the kernel's admission bypass. Measured once while this table was authored, the edit survived every scope and layers history in both ports; the model detects the fault only through the state invariant closedScopesHaveNoMemo, so the challenge is unobservable rather than mapped." + }, + "reproducer": { + "kind": "exported-regression", + "model": "formal/dialcache-scope-conformance.qnt", + "run": "lateSourceCannotPopulateReplacementTest", + "failure": "s.o.calls == List(VALUE_ONE, CALL_PENDING) and s.o.loaders == 2 and s.memo == List(NO_VALUE, NO_VALUE)", + "family": "closed-scope-memo-publication", + "profiles": [ + "recovery", + "scope", + "layers", + "recovery-read" + ], + "exclusions": { + "effects": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. This profile does not observe a memo populated after its request scope closes; the shared module being imported alone does not establish detection.", + "policy": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. This profile does not observe a memo populated after its request scope closes; the shared module being imported alone does not establish detection.", + "shadow": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. This profile does not observe a memo populated after its request scope closes; the shared module being imported alone does not establish detection.", + "admission": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. This profile does not observe a memo populated after its request scope closes; the shared module being imported alone does not establish detection.", + "independent": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. This profile does not observe a memo populated after its request scope closes; the shared module being imported alone does not establish detection.", + "local-failure": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. This profile does not observe a memo populated after its request scope closes; the shared module being imported alone does not establish detection.", + "runtime-boundaries": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. This profile does not observe a memo populated after its request scope closes; the shared module being imported alone does not establish detection.", + "shadow-layers": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. This profile does not observe a memo populated after its request scope closes; the shared module being imported alone does not establish detection.", + "local-clock": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. This profile does not observe a memo populated after its request scope closes; the shared module being imported alone does not establish detection.", + "source-budgets": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. This profile does not observe a memo populated after its request scope closes; the shared module being imported alone does not establish detection.", + "dark-layers": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. This profile does not observe a memo populated after its request scope closes; the shared module being imported alone does not establish detection.", + "shadow-read-deadlines": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. This profile does not observe a memo populated after its request scope closes; the shared module being imported alone does not establish detection." + } } }, { @@ -1640,6 +2523,27 @@ "nativeMutants": { "kind": "model-only", "text": "The fault drops the age window from kernel/recovery.qnt retain, so a visible frame the read declined is retained whatever its age. A fresh frame never reaches retain, so the candidate retained under the fault is one past the maximum, consulted only through validCandidate's maximum recheck at loader failure and at decode settlement: no observation changes and only retainedSnapshotsWereEligible sees it; the former literal mutant (retainedAgeAllowed to recoveryAgeAllowed) is equivalent here for the same reason. In both ports retention and serving are mutually exclusive (TypeScript src/internal/redis-cache.ts getWithResolvedConfig returns hit or retained by comparing the age with the fresh age; Go go/engine.go readServing returns kind hit before recover) and the maximum is rechecked before and after decode, so no single edit retains an out-of-window frame a later check does not reject." + }, + "reproducer": { + "kind": "exported-regression", + "run": "initiallyExpiredFrameIsNotRetainedTest", + "failure": "retainedSnapshotsWereEligible and s.retained.nth(0).candidate == NO_VALUE", + "family": "initial-recovery-retention-window", + "profiles": [ + "independent" + ], + "exclusions": { + "effects": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. The profile does not independently reject retaining an initially out-of-window candidate; subsequent maximum-age checks still prevent serving it.", + "recovery": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. The profile does not independently reject retaining an initially out-of-window candidate; subsequent maximum-age checks still prevent serving it.", + "shadow": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. The profile does not independently reject retaining an initially out-of-window candidate; subsequent maximum-age checks still prevent serving it.", + "scope": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. The profile does not independently reject retaining an initially out-of-window candidate; subsequent maximum-age checks still prevent serving it.", + "admission": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. The profile does not independently reject retaining an initially out-of-window candidate; subsequent maximum-age checks still prevent serving it.", + "recovery-read": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. The profile does not independently reject retaining an initially out-of-window candidate; subsequent maximum-age checks still prevent serving it.", + "shadow-layers": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. The profile does not independently reject retaining an initially out-of-window candidate; subsequent maximum-age checks still prevent serving it.", + "source-budgets": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. The profile does not independently reject retaining an initially out-of-window candidate; subsequent maximum-age checks still prevent serving it.", + "dark-layers": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. The profile does not independently reject retaining an initially out-of-window candidate; subsequent maximum-age checks still prevent serving it.", + "shadow-read-deadlines": "All scheduled named histories and invariants pass under this fault at 2000 samples by 40 steps. The profile does not independently reject retaining an initially out-of-window candidate; subsequent maximum-age checks still prevent serving it." + } } }, { @@ -1661,20 +2565,15 @@ "independent" ], "exclusions": { - "core": "Does not compose the deadlines module: its sources have no budget and its own deadline delivery is stated in its own text.", - "effects": "Every deadline is a multiple of the 10 ms step from the origin, so the earliest due instant and the advance's target coincide (measured 2026-09-20 over the composed text: every scheduled invariant and regression passes under the mutant).", - "recovery": "Its one source deadline is 10 ms and every scheduled history that reaches it advances by exactly 10 ms or by 1000 ms with the candidate still inside its maximum, so delivering at the target yields the same channels (measured 2026-09-20).", - "policy": "Does not compose the deadlines module: its sources have no budget.", - "shadow": "Composes deadlines' delivery for a source deadline and a job budget registered at one instant, whose expiries read no clock, so delivering at the target instead of each due instant yields the same channels (measured 2026-09-20 over the composed text: every scheduled invariant and regression passes under the mutant).", + "effects": "Every deadline is a multiple of the 10 ms step from the origin, so the earliest due instant and the advance's target coincide.", + "recovery": "Its one source deadline is 10 ms and every scheduled history that reaches it advances by exactly 10 ms or by 1000 ms with the candidate still inside its maximum, so delivering at the target yields the same channels.", + "shadow": "Composes deadlines' delivery for a source deadline and a job budget registered at one instant, whose expiries read no clock, so delivering at the target instead of each due instant yields the same channels.", "scope": "Does not compose the deadlines module: its sources have no budget.", - "admission": "Composes deadlines' delivery for job budgets only: a job's expiry reads no clock and every budget is the same 10 ms, so registration order is due order and delivering at the target instead of each due instant yields the same labels in the same order (measured 2026-09-20: every scheduled invariant and regression passes under the mutant).", - "layers": "Does not compose the deadlines module: its sources have no budget.", - "recovery-read": "Does not compose the deadlines module: its sources have no budget and its own deadline delivery is stated in its own text.", - "local-failure": "Does not compose the deadlines module: its sources have no budget and no deadline is ever delivered.", - "runtime-boundaries": "Does not compose the deadlines module: its sources have no budget.", + "admission": "Composes deadlines' delivery for job budgets only: a job's expiry reads no clock and every budget is the same 10 ms, so registration order is due order and delivering at the target instead of each due instant yields the same labels in the same order.", "shadow-layers": "Composes deadlines' delivery for job budgets whose expiry reads no clock and whose registration order is job order, so delivering at the target instead of each due instant yields the same labels in the same order.", - "local-clock": "Does not compose the deadlines module: its sources have no budget and no deadline is ever delivered.", - "source-budgets": "Composes the budgeted local lifecycle, whose deliveries inside one advance complete the same sources with the same deadline error whichever instants the fold visits, and no delivery there starts further bounded work; only its private settledAt history sees the instant." + "source-budgets": "Composes the budgeted local lifecycle, whose deliveries inside one advance complete the same sources with the same deadline error whichever instants the fold visits, and no delivery there starts further bounded work; only its private settledAt history sees the instant.", + "dark-layers": "No recovery decode or source-duration observation samples the delivery instant inside an advance. Named runs and scheduled invariants still see the same final errors and retired jobs.", + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." } }, "nativeMutants": { @@ -1703,24 +2602,20 @@ "recovery" ], "exclusions": { - "core": "Does not import the kernel library, so the mutated recovery age check is never executed; its own maximum-age rule is stated in its own text.", "effects": "Classifier NONE: no snapshot is retained and no recovery decode is held, so the mutated recovery age check is never executed.", "shadow": "Composes remote_io with Recovery::NONE and no caller read: no snapshot is ever retained and no recovery decode is held.", "admission": "Passes Recovery::NONE at every read settlement, so no snapshot is retained and no recovery decode is held; the mutated text is never executed.", - "local-failure": "Composes the layered traversal without the recovery module (no stale-on-error snapshot is retained), so the mutated recovery age check is never executed.", "shadow-layers": "Composes recovery on the atomic path, but its guard excludes every stale candidate, so no snapshot holds a candidate and neither its stamp nor its maximum is ever consulted.", - "local-clock": "Composes the local projection without the recovery module, so the mutated recovery age check is never executed.", - "policy": "Composes the atomic layered release (serving.release), which observes the remote frame at release and never dispatches a held read through remote_io.", - "layers": "Composes the atomic layered release (serving.release), which observes the remote frame at release and never dispatches a held read through remote_io.", - "runtime-boundaries": "Composes the atomic layered release (serving.release), which observes the remote frame at release and never dispatches a held read through remote_io.", "scope": "Composes the request-only projection, which has no remote layer and dispatches no held read.", - "source-budgets": "Composes the local projection, which has no remote layer and dispatches no held read." + "source-budgets": "Composes the local projection, which has no remote layer and dispatches no held read.", + "dark-layers": "No caller recovery candidate is retained while remote serving is off; the recovery maximum is never judged.", + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." } }, "nativeMutants": { "kind": "mapped", "mutant": "M01", - "text": "M01 makes the post-decode serving maximum inclusive in both ports, the observable site of kernel/recovery.qnt validCandidate calling recoveryAgeAllowed with the snapshot's maximum plus one, checked by dialcache-recovery-read-connection.qnt recoveryReturnMatchesAcquiredContract; a pre-decode flip alone is masked in the ports by the post-decode recheck. recovery-read/compressedRecoveryRechecksMaximumAfterDecodeTest and witness recovery:exact-maximum-after-decode-rejects are the evidence." + "text": "M01 makes the post-decode serving maximum inclusive in both ports, the observable site of kernel/recovery.qnt validCandidate calling recoveryAgeAllowed with the snapshot's maximum plus one, checked by dialcache-recovery-read-connection.qnt recoveryReturnMatchesAcquiredContract; a pre-decode flip alone is masked in the ports by the post-decode recheck." } }, { @@ -1742,26 +2637,22 @@ "recovery-read" ], "exclusions": { - "core": "Does not import the kernel library, so the mutated recovery-decode settlement is never executed; its own recovery publication rule is stated in its own text.", "effects": "Classifier NONE: no snapshot is retained and no recovery decode is held, so the mutated recovery-decode settlement is never executed.", "recovery": "Local capacity is 0: Local::putLocal stores nothing, so the mutated warm changes no channel.", "shadow": "Composes remote_io with Recovery::NONE and no caller read: no snapshot is ever retained and no recovery decode is held.", "admission": "Passes Recovery::NONE at every read settlement, so no snapshot is retained and no recovery decode is held; the mutated text is never executed.", "independent": "Composes the held remote lifecycle with a local capacity of 0 and replies that leave the local layer off, so the mutated warm stores nothing.", - "local-failure": "Composes the layered release with inline remote reads (serving.release), never remote_io's held read dispatch or decode, so the mutated recovery-decode settlement is never executed.", "shadow-layers": "Composes the atomic release (remote_writes), never remote_io's held read dispatch or decode, so the mutated text is never executed.", - "local-clock": "Composes the local projection with no remote layer, so remote_io is never executed.", - "policy": "Composes the atomic layered release (serving.release), which observes the remote frame at release and never dispatches a held read through remote_io.", - "layers": "Composes the atomic layered release (serving.release), which observes the remote frame at release and never dispatches a held read through remote_io.", - "runtime-boundaries": "Composes the atomic layered release (serving.release), which observes the remote frame at release and never dispatches a held read through remote_io.", "scope": "Composes the request-only projection, which has no remote layer and dispatches no held read.", - "source-budgets": "Composes the local projection, which has no remote layer and dispatches no held read." + "source-budgets": "Composes the local projection, which has no remote layer and dispatches no held read.", + "dark-layers": "No caller enters recovery or releases a recovery decode; dark jobs decode their own C0 and never use the challenged recovery publication arm.", + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." } }, "nativeMutants": { "kind": "mapped", "mutant": "M42", - "text": "M42 is the native twin of kernel/remote-io.qnt settleLoad warming local storage with a served recovery candidate, checked by dialcache-recovery-read-connection.qnt recoveredResultsNeverPublishShared: both ports publish a recovered stale value into process-local storage. The recovery profile has no local TTL, so detection rests on the recovery-read profile; recovery-read/untrackedCompressedRecoveryDoesNotWarmLocalTest and compressedRecoveryMemoizesWithoutLocalPublicationTest are the evidence." + "text": "M42 is the native twin of kernel/remote-io.qnt settleLoad warming local storage with a served recovery candidate, checked by dialcache-recovery-read-connection.qnt recoveredResultsNeverPublishShared: both ports publish a recovered stale value into process-local storage. The recovery profile has no local TTL, so detection rests on the recovery-read profile." } }, { @@ -1781,10 +2672,9 @@ "recovery-read" ], "exclusions": { - "core": "Does not import the kernel library, so the mutated refill authority is never executed; its own retention rule is stated in its own text.", "effects": "The 60 s retention is below the tracked cap, so the mutated cap never binds.", "recovery": "Every retention (2000, 5000) is under the tracked cap, so capped and raw retention coincide.", - "shadow": "Every retention (60 s) is under the tracked cap, so capped and raw retention coincide (measured 2026-09-20 over the composed text: every regression passes under the mutant).", + "shadow": "Every retention (60 s) is under the tracked cap, so capped and raw retention coincide.", "admission": "Every retention (60000 ms) is under the one-hour physical cap, so the capped and raw retention coincide.", "independent": "Composes serving.authority, but its retentions (2000 and 5000 ms) are under the one-hour cap, so capped and raw retention coincide.", "local-failure": "Composes serving.authority with tracking off, so the physical retention is the raw retention and the capped and uncapped texts coincide.", @@ -1794,13 +2684,15 @@ "layers": "Composes serving.authority, but its 60000 ms retention is under the one-hour cap, so capped and raw retention coincide.", "runtime-boundaries": "Composes serving.authority, but its 120000 ms retention is written for an untracked entity, which the cap never applies to.", "scope": "Composes the request-only projection, which never reaches serving.authority.", - "source-budgets": "Composes the local projection, which never reaches serving.authority." + "source-budgets": "Composes the local projection, which never reaches serving.authority.", + "dark-layers": "Caller remote serving is off, so callers capture no refill retention. Held dark fills cap their own captured retention through remote_frames.physicalRetention.", + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." } }, "nativeMutants": { "kind": "mapped", "mutant": "M56", - "text": "M56 is the native twin of kernel/serving.qnt authority capturing a tracked refill's retention uncapped instead of through remote_frames.physicalRetention: both ports write a tracked value for its full configured retention above one hour instead of clamping it to the hour. recovery-read/trackedPhysicalRetentionExpiresAtCapTest (observed write TTL 14400000 against 3600000) is the expected evidence in both ports." + "text": "M56 is the native twin of kernel/serving.qnt authority capturing a tracked refill's retention uncapped instead of through remote_frames.physicalRetention: both ports write a tracked value for its full configured retention above one hour instead of clamping it to the hour." } }, { @@ -1823,22 +2715,7 @@ "profiles": [ "local-failure" ], - "exclusions": { - "core": "Does not import the kernel library, so the mutated definition is never executed; its own rule is stated in its own text.", - "effects": "Does not compose local_faults, so the mutated definition is never executed.", - "recovery": "Does not compose the local fault switch (local_faults): its drivers inject no local storage fault, so the mutated settlement is never executed.", - "policy": "Does not compose the local fault switch (local_faults): its drivers inject no local storage fault, so the mutated settlement is never executed.", - "shadow": "Composes local storage, but every reply leaves the local layer off and the capacity is 0, so no local entry is ever written or read.", - "scope": "Does not compose the local fault switch (local_faults): its drivers inject no local storage fault, so the mutated settlement is never executed.", - "admission": "Does not compose the local fault switch (local_faults).", - "layers": "Does not compose the local fault switch (local_faults): its drivers inject no local storage fault, so the mutated settlement is never executed.", - "independent": "Does not compose the local fault switch (local_faults): its drivers inject no local storage fault, so the mutated settlement is never executed.", - "recovery-read": "Does not compose the local fault switch (local_faults): its drivers inject no local storage fault, so the mutated settlement is never executed.", - "runtime-boundaries": "Does not compose the local fault switch (local_faults): its drivers inject no local storage fault, so the mutated settlement is never executed.", - "shadow-layers": "Does not compose the local fault switch (local_faults): its drivers inject no local storage fault, so the mutated settlement is never executed.", - "local-clock": "Does not compose the local fault switch (local_faults): its drivers inject no local storage fault, so the mutated settlement is never executed.", - "source-budgets": "Does not compose the local fault switch (local_faults): its drivers inject no local storage fault, so the mutated settlement is never executed." - } + "exclusions": {} } }, { @@ -1847,12 +2724,12 @@ "source": "formal/kernel/serving.qnt", "model": "formal/dialcache-local-failure-conformance.qnt", "invariant": "storageHoldsAcceptedValuesOnly", - "before": " if (accepted(value)) warm(frames, layout, source, value, source.localMs) else frames", - "after": " warm(frames, layout, source, value, source.localMs)", + "before": " if (accepted(value)) warm(state, layout, source, value, source.localMs) else state", + "after": " warm(state, layout, source, value, source.localMs)", "nativeMutants": { "kind": "mapped", "mutant": "M37", - "text": "M37 is the native twin of kernel/serving.qnt publish warming local storage without the accepted judgment: both ports publish a rejected source's result into process-local storage before rethrowing. local-failure/rejectedSourceDoesNotSeedLocalTest is the portable evidence: the transient call after the rejection reads remote and starts its own source (calls 3,0 with two loaders and two reads) instead of serving the stored error." + "text": "M37 is the native twin of kernel/serving.qnt publishLocal warming local storage without the accepted-value judgment: both ports publish a rejected source's result into process-local storage before rethrowing." }, "reproducer": { "kind": "exported-regression", @@ -1861,12 +2738,12 @@ "family": "source-error-published", "profiles": [ "local-failure", - "policy" + "policy", + "source-budgets" ], "exclusions": { - "core": "Does not import the kernel library, so the mutated definition is never executed; its own rule is stated in its own text.", "effects": "No local TTL: warm with localMs 0 stores nothing whatever the value, so the mutated local publication is never observable.", - "recovery": "Composes serving.publish only through remote_io.settleLoader, which publishes a settled value only when it is already accepted and classifies a rejected one without publishing, so the removed acceptance gate never decides (measured 2026-09-20: every scheduled invariant and regression passes under the mutant).", + "recovery": "Composes serving.publish only through remote_io.settleLoader, which publishes a settled value only when it is already accepted and classifies a rejected one without publishing, so the removed acceptance gate never decides.", "shadow": "Composes local storage, but every reply leaves the local layer off and the capacity is 0, so no local entry is ever written or read.", "scope": "Composes the request-only projection, whose sources settle through serving.settleRequest, never serving.publish.", "admission": "Composes serving.publish only for a job's detached source, whose local TTL is 0, so the mutated warm stores nothing.", @@ -1875,8 +2752,9 @@ "recovery-read": "Composes the layered shape and reaches the mutated warm when a source is rejected while local storage participates, but none of its scheduled invariants or exported regressions reads the local slot between that rejection and the next publication (measured under the mutant at 2000 x 40 and over its exported regressions).", "runtime-boundaries": "Composes the layered shape and reaches the mutated warm when a source is rejected while local storage participates, but none of its scheduled invariants or exported regressions reads the local slot between that rejection and the next publication (measured under the mutant at 2000 x 40 and over its exported regressions).", "shadow-layers": "Composes the layered shape and reaches the mutated warm when a source is rejected while local storage participates, but none of its scheduled invariants or exported regressions reads the local slot between that rejection and the next publication (measured under the mutant at 2000 x 40 and over its exported regressions).", - "local-clock": "Composes the local projection, which settles through serving.settleLocal, never serving.publish; its inline sources never fail besides.", - "source-budgets": "Composes the local projection, whose sources settle through serving.settleLocal, never serving.publish." + "local-clock": "Its inline sources only return accepted values, so the acceptance judgment in publishLocal never rejects one.", + "dark-layers": "The held source lifecycle rejects errors before the accepted-only serving publication call. Native M37 also edits a serving/recovery failure-publication site outside finishRampedDownRemote; both ports replay this dark corpus unchanged under the absent-value mutation.", + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." } } }, @@ -1891,7 +2769,24 @@ "nativeMutants": { "kind": "mapped", "mutant": "M51", - "text": "Same native fault as cohort-inclusive-threshold: M51 makes the serving-layer cohort comparison inclusive in both ports, which is kernel/runtime-policy.qnt admits inlining an inclusive comparison in place of cohortAdmits. runtime-boundaries/localCohortEqualityBypassesBeforeAboveSampleCachesTest and remoteCohortEqualityBypassesBeforeAboveSampleCachesTest are the expected evidence in both ports." + "text": "Same native fault as cohort-inclusive-threshold: M51 makes the serving-layer cohort comparison inclusive in both ports, which is kernel/runtime-policy.qnt admits inlining an inclusive comparison in place of cohortAdmits. runtime-boundaries/localCohortEqualityBypassesBeforeAboveSampleCachesTest and remoteCohortEqualityBypassesBeforeAboveSampleCachesTest are the expected evidence in both ports.", + "evidence": { + "history": "runtime-boundaries/localCohortEqualityLeavesNoValueForLaterAdmissionTest", + "step": 7, + "fields": [ + "o.calls" + ] + } + }, + "reproducer": { + "kind": "exported-regression", + "run": "localCohortEqualityLeavesNoValueForLaterAdmissionTest", + "failure": "s.o.calls == List(VALUE_ONE, CALL_PENDING) and s.o.loaders == 2", + "family": "strict-cohort-equality", + "profiles": [ + "runtime-boundaries" + ], + "exclusions": {} } }, { @@ -1905,7 +2800,24 @@ "nativeMutants": { "kind": "mapped", "mutant": "M27", - "text": "M27 is the native twin of kernel/runtime-policy.qnt resolve dropping the baseline coalesce condition from its inherit-sharing disjunct: both ports capture the operation's coalesce default as true, so an omitted runtime leaf or a null provider inherits sharing over a configured false. runtime-boundaries/inheritedFalseKeepsIndependentMemoizingSourcesTest and the independent profile replays whose fixture sets coalesce false are the expected evidence." + "text": "M27 is the native twin of kernel/runtime-policy.qnt resolve dropping the baseline coalesce condition from its inherit-sharing disjunct: both ports capture the operation's coalesce default as true, so an omitted runtime leaf or a null provider inherits sharing over a configured false. runtime-boundaries/inheritedFalseKeepsIndependentMemoizingSourcesTest and the independent profile replays whose fixture sets coalesce false are the expected evidence.", + "evidence": { + "history": "runtime-boundaries/inheritedFalseStartsBothSourcesBeforeSettlementTest", + "step": 4, + "fields": [ + "o.loaders" + ] + } + }, + "reproducer": { + "kind": "exported-regression", + "run": "inheritedFalseStartsBothSourcesBeforeSettlementTest", + "failure": "s.o.calls == List(CALL_PENDING, CALL_PENDING) and s.o.loaders == 2", + "family": "inherited-sharing-policy", + "profiles": [ + "runtime-boundaries" + ], + "exclusions": {} } }, { @@ -1918,35 +2830,25 @@ "after": "pure def acquired(observed: Remote::Observed): int = observed.payload", "reproducer": { "kind": "exported-regression", - "run": "expiredC0CanFillAndServeNewValueTest", + "run": "expiredC0StartsFillWithoutDecodingTest", "model": "formal/dialcache-shadow-layers-conformance.qnt", "failure": "s.o.calls == List(VALUE_TWO) and s.o.reads == 1 and s.o.loads == 0 and s.o.dumps == 1 and s.o.shadow == List()", "family": "c0-freshness-boundary", "profiles": [ "formal/dialcache-shadow-layers-connection.qnt", "shadow-layers", - "shadow" + "shadow", + "dark-layers" ], "exclusions": { - "core": "Does not compose the shadow module, so the mutated dark C0 acquisition is never executed.", - "effects": "Does not compose the shadow module, so the mutated dark C0 acquisition is never executed.", - "recovery": "Does not compose the shadow module, so the mutated dark C0 acquisition is never executed.", - "policy": "Does not compose the shadow module, so the mutated dark C0 acquisition is never executed.", - "scope": "Does not compose the shadow module, so the mutated dark C0 acquisition is never executed.", "admission": "Every reply enables the remote layer, so no dark C0 read is made and the mutated acquisition is never applied.", - "layers": "Does not compose the shadow module, so the mutated dark C0 acquisition is never executed.", - "independent": "Does not compose the shadow module, so the mutated dark C0 acquisition is never executed.", - "recovery-read": "Does not compose the shadow module, so the mutated dark C0 acquisition is never executed.", - "local-failure": "Does not compose the shadow module, so the mutated dark C0 acquisition is never executed.", - "runtime-boundaries": "Does not compose the shadow module, so the mutated dark C0 acquisition is never executed.", - "local-clock": "Does not compose the shadow module, so the mutated dark C0 acquisition is never executed.", - "source-budgets": "Does not compose the shadow module, so the mutated dark C0 acquisition is never executed." + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." } }, "nativeMutants": { "kind": "mapped", "mutant": "M31", - "text": "Same native fault as shadow-inclusive-c0-freshness: M31 makes the dark C0 acquisition inclusive in both ports, which is kernel/shadow.qnt acquired keeping a dark C0 from a visible frame remote_frames.observe declined for the reply's freshness (the boundary itself is remote_frames' own partition), checked by dialcache-shadow-layers-connection.qnt retainedC0WasFreshWhenAcquired. shadow-layers/expiredC0CanFillAndServeNewValueTest (seed age equal to the fresh TTL) is the expected evidence." + "text": "Same native fault as shadow-inclusive-c0-freshness: M31 makes the dark C0 acquisition inclusive in both ports, which is kernel/shadow.qnt acquired keeping a dark C0 from a visible frame remote_frames.observe declined for the reply's freshness (the boundary itself is remote_frames' own partition), checked by dialcache-shadow-layers-connection.qnt retainedC0WasFreshWhenAcquired." }, "measures": "Repeats the shadow-inclusive-c0-freshness fault against the shadow-layers connection model: the same acquisition mutation must also be caught by that monitor's own clause." }, @@ -1965,38 +2867,28 @@ "family": "captured-fill-retention", "profiles": [ "shadow-layers", - "shadow" + "shadow", + "dark-layers" ], "exclusions": { - "core": "Does not compose the shadow module, so the mutated fill dump is never executed.", - "effects": "Does not compose the shadow module, so the mutated fill dump is never executed.", - "recovery": "Does not compose the shadow module, so the mutated fill dump is never executed.", - "policy": "Does not compose the shadow module, so the mutated fill dump is never executed.", - "scope": "Does not compose the shadow module, so the mutated fill dump is never executed.", "admission": "Served jobs always carry a C0 and never fill, so the mutated fill is never reached.", - "layers": "Does not compose the shadow module, so the mutated fill dump is never executed.", - "independent": "Does not compose the shadow module, so the mutated fill dump is never executed.", - "recovery-read": "Does not compose the shadow module, so the mutated fill dump is never executed.", - "local-failure": "Does not compose the shadow module, so the mutated fill dump is never executed.", - "runtime-boundaries": "Does not compose the shadow module, so the mutated fill dump is never executed.", - "local-clock": "Does not compose the shadow module, so the mutated fill dump is never executed.", - "source-budgets": "Does not compose the shadow module, so the mutated fill dump is never executed." + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." } }, "nativeMutants": { "kind": "mapped", "mutant": "M29", - "text": "M29 is the native twin of kernel/shadow.qnt fill holding the dark fill's dump with the source's retention (0 for a dark source) instead of the retention the job captured at admission: both ports re-resolve the runtime policy at fill time and write the dark fill with the retention then in force. Detection most likely fires on the extra provider call before writeTtls diverge; shadow-layers/darkFillRetainsPolicyThroughSerializationTest is the expected evidence." + "text": "M29 is the native twin of kernel/shadow.qnt fill holding the dark fill's dump with the source's retention (0 for a dark source) instead of the retention the job captured at admission: both ports re-resolve the runtime policy at fill time and write the dark fill with the retention then in force." } }, { "id": "local-clock-precise-ttl", "contract": "C09", - "source": "formal/kernel/clock.qnt", + "source": "formal/cache-rules.qnt", "model": "formal/dialcache-local-clock-conformance.qnt", "invariant": "clockIsOnTheGrid", - "before": " { ticks: state.ticks + ticks, now: wholeMs(state.ticks + ticks, ticksPerMs), ...state }", - "after": " { ticks: state.ticks + ticks, now: wholeMs(state.ticks + ticks + ticksPerMs / 2, ticksPerMs), ...state }", + "before": "pure def wholeMs(ticks: int, ticksPerMs: int): int = ticks / ticksPerMs", + "after": "pure def wholeMs(ticks: int, ticksPerMs: int): int = (ticks + ticksPerMs / 2) / ticksPerMs", "reproducer": { "kind": "exported-regression", "run": "fractionalInsertionExpiresAtWholeMillisecondTest", @@ -2006,7 +2898,6 @@ "local-clock" ], "exclusions": { - "core": "Does not import the kernel library, so the mutated definition is never executed; its own rule is stated in its own text.", "effects": "Does not compose the fractional clock, so the mutated definition is never executed.", "recovery": "Never composes the fractional clock (clock.advanceTicks): its clock moves through diagnostics.advance and clock.shiftWall on the whole-millisecond grid, so the mutated projection is never executed.", "policy": "Advances its clock in whole milliseconds through clock.advance and never composes the fractional clock, so the mutated projection is never executed.", @@ -2019,13 +2910,15 @@ "local-failure": "Has no elapsed-time input and never composes the fractional clock (clock.advanceTicks), so the mutated projection is never executed.", "runtime-boundaries": "Advances its clock in whole milliseconds through clock.advance and never composes the fractional clock, so the mutated projection is never executed.", "shadow-layers": "Advances its clock in whole milliseconds through clock.advance and never composes the fractional clock, so the mutated projection is never executed.", - "source-budgets": "Advances its clock in whole milliseconds through clock.advance and never composes the fractional clock, so the mutated projection is never executed." + "source-budgets": "Advances its clock in whole milliseconds through clock.advance and never composes the fractional clock, so the mutated projection is never executed.", + "dark-layers": "The environment advances in whole milliseconds and never calls clock.advanceTicks; wall rollback changes no monotonic insertion grid.", + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." } }, "nativeMutants": { "kind": "mapped", "mutant": "M19", - "text": "Same native fault as local-precise-grid: M19 replaces the whole-millisecond grid with the precise clock for local insertion and expiry in both ports, which is kernel/clock.qnt advanceTicks leaving the floor grid (a fill at 0.7 ms lands on grid instant 1, expiring at 1001 ms). The feature-profile driver clocks are integer, so only local-clock observes it; local-clock/fractionalInsertionExpiresAtWholeMillisecondTest step 7 (calls 1,1,1 with one loader instead of 1,1,2 with two) detects both ports." + "text": "M19 replaces the whole-millisecond local clock with precise time. This challenge retains its shifted-grid approximation: wholeMs rounds after adding half a millisecond, so a fill at 0.7 ms expires at 1001 ms. Clock.advanceTicks and raw-insertion lookup now share that projection; clockIsOnTheGrid remains independent and the original public history still detects the changed expiration." } }, { @@ -2039,7 +2932,7 @@ "nativeMutants": { "kind": "mapped", "mutant": "M11", - "text": "M11 is the native twin of kernel/local-storage.qnt promote re-stamping the insertion expiry on a hit instead of renewing recency alone: both ports refresh the insertion time on every hit while keeping the value. policy/localHitDoesNotRenewInsertionTtlTest and local-clock/fractionalInsertionExpiresAtWholeMillisecondTest already detect it in both ports. promote has no TTL parameter, so the mutant stamps a literal second, the local-clock TTL." + "text": "M11 is the native twin of kernel/local-storage.qnt promote re-stamping the insertion expiry on a hit instead of renewing recency alone: both ports refresh the insertion time on every hit while keeping the value. promote has no TTL parameter, so the mutant stamps a literal second, the local-clock TTL." }, "reproducer": { "kind": "exported-regression", @@ -2054,7 +2947,6 @@ "recovery-read" ], "exclusions": { - "core": "Does not import the kernel library, so the mutated definition is never executed; its own rule is stated in its own text.", "effects": "The local layer is off (capacity 0 and no local TTL), so the mutated promotion never runs.", "recovery": "The local layer is off: capacity 0 and no local TTL, so no local entry is ever written or hit and the mutated renewal is never executed.", "shadow": "Composes local storage, but every reply leaves the local layer off and the capacity is 0, so no local entry is ever written or read.", @@ -2064,7 +2956,9 @@ "independent": "Composes local storage, but every reply leaves the local layer off and the capacity is 0, so no local hit is ever served.", "local-failure": "Composes local storage, but no input advances the clock, so a renewed insertion is invisible.", "runtime-boundaries": "Composes local storage, but no input advances the clock, so a renewed insertion is invisible.", - "shadow-layers": "Composes local storage; its one expiry regression reads the entry again only after the advances that expire it, with no hit in between, so a renewed insertion is invisible (measured under the mutant at 2000 x 40 and over its exported regressions)." + "shadow-layers": "Composes local storage; its one expiry regression reads the entry again only after the advances that expire it, with no hit in between, so a renewed insertion is invisible (measured under the mutant at 2000 x 40 and over its exported regressions).", + "dark-layers": "The named dark-job histories and invariants do not constrain renewing local insertion expiry on a hit. Local-clock supplies the detecting model boundary; this profile does not claim that property.", + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." } } }, @@ -2079,7 +2973,34 @@ "nativeMutants": { "kind": "mapped", "mutant": "M14", - "text": "M14 is the native twin of kernel/deadlines.qnt admissionBudget returning the source budget instead of UNBOUNDED for a call whose key did not fail: both ports bound the disabled or outside bypass source with the configured fallback deadline. source-budgets/outsideAndInvalidOutsideCallsIgnoreDeadlineAndSharingTest detects both ports at its advance(100) step." + "text": "M14 is the native twin of kernel/deadlines.qnt admissionBudget applying the configured source budget to outside calls instead of UNBOUNDED. At advance(100), both outside callers must remain pending; the fault completes them with deadline errors. Boundary evidence selects caller outcomes so unrelated policy or loader counters cannot satisfy the mapping.", + "evidence": { + "history": "source-budgets/outsideAndInvalidOutsideCallsIgnoreDeadlineAndSharingTest", + "step": 3, + "fields": [ + "o.calls" + ] + } + }, + "reproducer": { + "kind": "exported-regression", + "run": "outsideAndInvalidOutsideCallsIgnoreDeadlineAndSharingTest", + "failure": "s.o.calls == List(CALL_PENDING, CALL_PENDING) and s.o.policyCalls == 0 and s.o.loaders == 2", + "family": "outside-call-unbounded-source", + "profiles": [ + "source-budgets", + "dark-layers" + ], + "exclusions": { + "effects": "All calls are enabled with valid keys and no closable request scope; sources start at release or read/decode settlement, never from bypass admission.", + "recovery": "Call and join guards admit only enabled callers in open scopes. Closing a scope after admission cannot start a bypass source, so the changed admission budget never registers a deadline.", + "shadow": "All calls are enabled with valid keys and no closable request scope; the caller source starts at participating release, not bypass admission.", + "scope": "Uses Diagnostics::admitRequest and the request-only serving lifecycle, with no budgeted source lifecycle; it never calls the changed admission-budget rule.", + "admission": "Uses Shadow::beginHeld through RemoteIO::begin without a source budget; only shadow jobs register deadlines, so caller admission never uses the changed rule.", + "independent": "All calls are enabled with valid keys and no closable request scope; source loaders start after read/decode settlement, not from bypass admission.", + "shadow-layers": "Uses Shadow::begin with the atomic serving lifecycle and no source budget; only shadow jobs are timed, so caller admission never uses the changed rule.", + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." + } } }, { @@ -2093,7 +3014,40 @@ "nativeMutants": { "kind": "mapped", "mutant": "M26", - "text": "M26 is the native twin of kernel/flights.qnt forgetSource leaving processFlights unchanged instead of clearing the settled index: neither port removes a settled process flight from the registry, so later same-key callers join finished work. Joiners get the settled flight's stale outcome rather than stranding, since a blocked follower would deadlock synctest; source-budgets/defaultSourceBudgetExpiresAtSixtySecondsTest and policy/independentCompletionKeepsRegisteredLeaderTest are the evidence." + "text": "M26 is the native twin of kernel/flights.qnt forgetSource leaving processFlights unchanged instead of clearing the settled index: neither port removes a settled process flight from the registry, so later same-key callers join finished work. Joiners get the settled flight's stale outcome rather than stranding, since a blocked follower would deadlock synctest; source-budgets/defaultSourceBudgetExpiresAtSixtySecondsTest and policy/independentCompletionKeepsRegisteredLeaderTest are the evidence.", + "evidence": { + "history": "source-budgets/defaultSourceBudgetExpiresAtSixtySecondsTest", + "step": 7, + "fields": [ + "o.calls" + ] + } + }, + "reproducer": { + "kind": "exported-regression", + "run": "defaultSourceBudgetExpiresAtSixtySecondsTest", + "failure": "s.o.calls == List(DEADLINE_ERROR, CALL_PENDING) and s.o.loaders == 2", + "family": "settled-process-flight-cleanup", + "profiles": [ + "effects", + "recovery", + "policy", + "admission", + "layers", + "recovery-read", + "local-failure", + "runtime-boundaries", + "local-clock", + "source-budgets", + "dark-layers" + ], + "exclusions": { + "scope": "Only request flights are registered; processFlights stays empty, so removing process-flight cleanup changes no state.", + "shadow": "This single-execution projection cannot admit a second source after its one source/job lifetime; its scheduled properties do not observe the settled process registry.", + "independent": "Every caller has coalesce false and registers no process flight, so processFlights remains empty.", + "shadow-layers": "Coalescing is disabled for its independent-source fixture, so no process flight is registered.", + "shadow-read-deadlines": "All six exported read-deadline histories and all six scheduled invariants at 2000 x 40 pass under this fault; this profile does not exercise the affected recovery or settled-flight reuse behavior." + } } }, { @@ -2117,10 +3071,10 @@ "policy", "shadow", "recovery-read", - "effects" + "effects", + "dark-layers" ], "exclusions": { - "core": "Evaluates no age judgment from cache-rules, so ageBeforeCeiling is unreachable.", "scope": "Evaluates no age judgment from cache-rules, so ageBeforeCeiling is unreachable.", "admission": "Composes the remote freshness judgment, but every frame is stamped at a wall clock that only advances and is read within 600 ms under a 60 s freshness, so no read meets an age boundary or observes a future stamp.", "layers": "Frames are stamped from a wall clock that only moves with the monotonic clock (no rollback input), so no history observes a future stamp.", @@ -2129,13 +3083,14 @@ "runtime-boundaries": "Frames are stamped from a wall clock that only moves with the monotonic clock (no rollback input), so no history observes a future stamp.", "shadow-layers": "Frames are stamped with the wall clock at write release (remote_writes.dispatch) or seeded at a non-negative age; the skew is 0 and never shifts, so no history observes a future stamp.", "local-clock": "Composes the local projection with no remote layer; its closure reaches cache-rules through wholeMs and localEntryLiveAt only, so ageBeforeCeiling is never evaluated.", - "source-budgets": "Uses only localEntryLiveAt and sourceAcceptsAt from cache-rules; no age judgment is evaluated." + "source-budgets": "Uses only localEntryLiveAt and sourceAcceptsAt from cache-rules; no age judgment is evaluated.", + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." } }, "nativeMutants": { "kind": "mapped", "mutant": "M40", - "text": "Same native fault as recovery-future-candidate: M40 classifies a frame dated after the reader clock as a valid frame of age zero in both ports, which is cache-rules.qnt ageBeforeCeiling losing its created-before-observed clause as measured on the read receipt by dialcache-stale-recovery.qnt futureFrameIsNeverFreshOrRetained. policy/wallRollbackRejectsFutureRemoteFrameTest, effects/futureFrameReportsPositiveObservingOffsetTest and witness policy:rollback-rejects-future-remote are the evidence." + "text": "Same native fault as recovery-future-candidate: M40 classifies a frame dated after the reader clock as a valid frame of age zero in both ports, which is cache-rules.qnt ageBeforeCeiling losing its created-before-observed clause as measured on the read receipt by dialcache-stale-recovery.qnt futureFrameIsNeverFreshOrRetained." } }, { @@ -2149,18 +3104,32 @@ "reproducer": { "kind": "model-run", "run": "unknownLegacyZeroPrefixRemainsUntouchedTest", - "failure": "result.read.bytes == List(0, 3, 255)", + "failure": "result.read.bytes == List(0,3)", "family": "escape-prefix-over-consumed", "profiles": [ "formal/dialcache-envelope-vectors.qnt" ], "exclusions": {}, - "scope": "Envelope handling is a deterministic transform with no replayed input history: the vector model exports its cases to quint-envelope-vectors.json and both codecs are checked against that artifact, not through a driver profile. The run pins the unknown zero-prefix payload (bytes 0, 3, 255) that the fault would unescape." + "scope": "A deterministic protocol transform exports language-neutral vector inputs rather than an asynchronous driver history. Exact generated rows have their own native input/output recording and clean baseline in both ports. The named run executes requestFor(9), the exact generated byte sequence0003. Because03 is not an envelope marker, both native readers preserve both bytes." }, "nativeMutants": { "kind": "mapped", "mutant": "M54", - "text": "M54 is the native twin of dialcache-envelope-vectors.qnt readEnvelope dropping its marker guard: both ports strip a leading zero byte even when the next byte is not an envelope marker. M53 shares the anchor with a different edit and stays a distinct entry; envelope vectors 009 (0003 stays 0003) and 010 are the expected evidence in both ports." + "text": "M54 is the native twin of dialcache-envelope-vectors.qnt readEnvelope dropping its marker guard: both ports strip a leading zero byte even when the next byte is not an envelope marker. M53 shares the anchor with a different edit and stays a distinct entry; envelope vectors 009 (0003 stays 0003) and 010 are the expected evidence in both ports.", + "evidence": { + "vector": { + "artifact": "formal/quint-envelope-vectors.json", + "group": "envelopeVectors", + "rows": { + "typescript": "Quint envelope 009 envelope", + "go": "Quint envelope 009 envelope" + }, + "fields": [ + "decodedHex" + ], + "relation": "The named run executes requestFor(9), the exact generated byte sequence0003. Because03 is not an envelope marker, both native readers preserve both bytes." + } + } } }, { @@ -2173,8 +3142,8 @@ "after": "pure def dueAt(now: int, d: Due): bool = now > d.at", "reproducer": { "kind": "exported-regression", - "run": "defaultSourceBudgetExpiresAtSixtySecondsTest", - "failure": "s.o.calls == List(DEADLINE_ERROR, CALL_PENDING) and s.o.loaders == 2", + "run": "lateSourceResultIsADeadlineErrorTest", + "failure": "s.o.calls == List(DEADLINE_ERROR) and s.o.dumps == 0 and errorReasons == List(\"fallback\") and amounts(\"fallback\") == List(10)", "family": "inclusive-deadline-boundary", "profiles": [ "source-budgets", @@ -2183,23 +3152,19 @@ "recovery", "admission", "shadow", - "effects" + "effects", + "dark-layers", + "shadow-read-deadlines" ], "exclusions": { - "core": "does not compose the deadlines module: its sources have no budget", - "policy": "does not compose the deadlines module: its sources have no budget", - "scope": "does not compose the deadlines module: its sources have no budget", - "layers": "does not compose the deadlines module: its sources have no budget", - "recovery-read": "does not compose the deadlines module: its sources have no budget", - "local-failure": "does not compose the deadlines module: its sources have no budget", - "runtime-boundaries": "does not compose the deadlines module: its sources have no budget", - "local-clock": "does not compose the deadlines module: its sources have no budget" - } + "scope": "does not compose the deadlines module: its sources have no budget" + }, + "model": "formal/dialcache-effects-conformance.qnt" }, "nativeMutants": { "kind": "mapped", "mutant": "M35", - "text": "Same native fault as source-inclusive-deadline: M35 accepts a settlement observed at exactly the elapsed deadline in both ports, which is kernel/deadlines.qnt dueAt replacing not deadlinePendingAt with a strict comparison. The declared reproducer source-budgets/defaultSourceBudgetExpiresAtSixtySecondsTest cannot distinguish it: its advance delivers the timer at exactly 60000 ms and the unchanged timer path still times the source out; the effects jumpClock histories are the portable evidence." + "text": "M35 changes native settlement-time deadline equality. The shared dueAt mutation also affects timer delivery, which the dark-layers source/job deadline history detects model-side. Its native advance still delivers the unchanged timer, so that profile supplies no native M35 boundary; effects clock-jump histories isolate settlement." } }, { @@ -2218,12 +3183,12 @@ "profiles": [ "policy", "runtime-boundaries", - "layers" + "layers", + "dark-layers" ], "exclusions": { - "core": "Does not import the kernel library, so the mutated traversal order is never executed; its own order is stated in its own text.", "effects": "No request memo: the memo value is always absent, so the two traversal orders coincide.", - "recovery": "No scheduled history admits a caller into a scope holding a memo while another scope's flight is pending, so the reordered arms decide alike (measured 2026-09-20).", + "recovery": "No scheduled history admits a caller into a scope holding a memo while another scope's flight is pending, so the reordered arms decide alike.", "shadow": "Every reply leaves every layer off and coalescing off: decide's join and serve arms are never taken.", "admission": "Composes decide with the remote layer unobserved and no memo or local layer, so no layer value can precede a join and the reordered arms decide alike.", "independent": "Composes decide with coalesce false, so no process owner is ever looked up, and with no request memo and no local layer no layer value can precede a join.", @@ -2232,13 +3197,14 @@ "shadow-layers": "Coalescing is off in every reply: decide's join arms are never taken and the request registry is never written.", "local-clock": "Composes serving.decide over the local projection, but an inline source settles in the step that starts it, so no flight is pending at any admission and the join arms are never taken.", "scope": "Composes the request-only projection, which passes no layer values to the decision, so no layer hit can precede a join.", - "source-budgets": "Its replies always coalesce and its bypass sources warm nothing, so a live entry and a pending registered flight never coexist." + "source-budgets": "Its replies always coalesce and its bypass sources warm nothing, so a live entry and a pending registered flight never coexist.", + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." } }, "nativeMutants": { "kind": "mapped", "mutant": "M21", - "text": "M21 is the native twin of kernel/serving.qnt decide moving JoinsProcess below the memo, local and remote branches: both ports serve a live local entry before joining the pending process flight registered for the key. policy/independentCompletionKeepsRegisteredLeaderTest step 9 detects both ports, as does runtime-boundaries/defaultSharingJoinsRegisteredLeaderOverWarmedLocalTest." + "text": "M21 is the native twin of kernel/serving.qnt decide moving JoinsProcess below the memo, local and remote branches: both ports serve a live local entry before joining the pending process flight registered for the key." } }, { @@ -2251,16 +3217,16 @@ "after": "if (sharedLayers(layers) and memoValue == NO_VALUE)", "reproducer": { "kind": "exported-regression", - "run": "independentCompletionKeepsRegisteredLeaderTest", + "run": "nonCoalescingCallStartsSeparateSourceTest", "failure": "s.o.calls == List(CALL_PENDING, CALL_PENDING) and s.o.loaders == 2 and s.o.reads == 2", "family": "join-ignores-coalesce", "profiles": [ "policy", "runtime-boundaries", - "admission" + "admission", + "dark-layers" ], "exclusions": { - "core": "Does not import the kernel library, so the mutated join conjunct is never executed; its own coalescing rule is stated in its own text.", "effects": "Every reply coalesces, so the dropped conjunct is always true.", "recovery": "Coalescing is on in every reply, so the conjunct the fault removes is always true and no history observes it.", "shadow": "Every reply leaves every layer off and coalescing off: decide's join and serve arms are never taken.", @@ -2271,52 +3237,55 @@ "local-clock": "Composes serving.decide over the local projection, but an inline source settles in the step that starts it, so no flight is pending at any admission and the join arms are never taken.", "layers": "Its replies always coalesce, so the conjunct the fault removes is always true and no history observes it.", "scope": "Composes the request-only projection, whose process join requires a shared layer this shape never has.", - "source-budgets": "Its replies always coalesce, so the conjunct the fault removes is always true and no history observes it." + "source-budgets": "Its replies always coalesce, so the conjunct the fault removes is always true and no history observes it.", + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." } }, "nativeMutants": { "kind": "mapped", "mutant": "M22", - "text": "M22 is the native twin of kernel/serving.qnt decide dropping the coalesce condition from its process-owner conjunction: both ports let a non-coalescing caller join a pending process flight while registration still respects coalesce. policy/independentCompletionKeepsRegisteredLeaderTest step 5 and policy/independentFailureKeepsRegisteredLeaderTest detect both ports." + "text": "M22 is the native twin of kernel/serving.qnt decide dropping the coalesce condition from its process-owner conjunction: both ports let a non-coalescing caller join a pending process flight while registration still respects coalesce." } }, { "id": "source-budgets-settlement-never-replaces-local-entry", "contract": "C13", - "source": "formal/kernel/serving.qnt", + "source": "formal/kernel/local-storage.qnt", "model": "formal/dialcache-source-budgets-conformance.qnt", "invariant": "localPublicationRequiresAcceptedSource", - "before": "val warmed = if (accepted(value) and source.localMs > 0)", - "after": "val warmed = if (accepted(value) and source.localMs > 0 and state.localValues.nth(source.instance * layout.keysPerInstance + source.key) == NO_VALUE)", + "before": "if (state.capacity == 0) state else {", + "after": "if (state.capacity == 0 or state.localValues.nth(slot) != NO_VALUE) state else {", "reproducer": { "kind": "exported-regression", - "run": "localEntryExpiresAfterItsTtlTest", - "failure": "s.o.calls == List(VALUE_ONE, VALUE_ONE, VALUE_TWO, VALUE_TWO) and s.o.loaders == 2", + "run": "independentLocalPublicationUsesLastCompletionTest", + "failure": "s.o.calls == List(VALUE_ONE, VALUE_TWO, VALUE_ONE) and s.o.loaders == 2", "family": "local-publication-never-replaces", "profiles": [ "source-budgets", - "local-clock" + "local-clock", + "policy", + "runtime-boundaries" ], "exclusions": { - "core": "Composes no kernel module, so serving.settleLocal is never executed.", - "effects": "Composes the held remote lifecycle: its loaders settle through remote_io, never the local projection's settleLocal, so the mutated text is never executed.", - "recovery": "Composes the held remote lifecycle, whose loaders settle through remote_io.settleLoader, so serving.settleLocal is never executed.", - "policy": "Composes no kernel module, so serving.settleLocal is never executed.", - "shadow": "Composes local storage, but every reply leaves the local layer off and the capacity is 0, so no local entry is ever written or read, and publishes through serving.publish, never settleLocal.", - "scope": "Composes the request-only projection without local storage: its sources settle through serving.settleRequest.", - "admission": "Composes the layered shape, which settles through remote_io and serving.publish, never serving.settleLocal.", - "layers": "Composes the layered traversal: its sources settle through serving.settle, not the local projection.", - "independent": "Composes the layered shape: its loaders settle through remote_io and serving.publish, never the local projection's settleLocal.", - "recovery-read": "Composes the layered shape, which publishes through serving.publish, never serving.settleLocal.", - "local-failure": "Composes the layered shape, which publishes through serving.publish, never serving.settleLocal.", - "runtime-boundaries": "Composes the layered traversal: its sources settle through serving.settle, not the local projection.", - "shadow-layers": "Composes the layered shape, which publishes through serving.publish, never serving.settleLocal." - } + "effects": "The fixture gives every source a zero local TTL, so the mutated local put has no observable effect.", + "recovery": "The fixture has zero local capacity, so putLocal stores nothing before or after this fault.", + "shadow": "Every reply leaves the local layer off and local capacity is zero, so no local entry is published.", + "scope": "The request-only traversal never publishes a local entry.", + "admission": "Serving reads are remote hits and detached shadow sources have no local publication authority; no occupied local slot is replaced.", + "layers": "Local publication is reachable, but no scheduled regression or sampled invariant at the check bounds distinguishes skipping an occupied slot; its properties check ownership and storage membership, not a last-completion probe.", + "independent": "Every reply leaves the local layer off and local capacity is zero, so no local entry is published.", + "recovery-read": "Local publication is reachable, but no scheduled regression or sampled invariant at the check bounds distinguishes skipping an occupied slot; the partition checker remeasures that exclusion.", + "local-failure": "Only one source can be pending, time never expires a local entry, and a failed local read withdraws local publication authority. The profile never requires replacing an occupied slot.", + "shadow-layers": "Local publication is reachable, but no scheduled regression or sampled invariant at the check bounds distinguishes skipping an occupied slot; the partition checker remeasures that exclusion.", + "dark-layers": "Local publication is reachable, but no scheduled regression or sampled invariant at the check bounds distinguishes skipping an occupied slot; the partition checker remeasures that exclusion.", + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." + }, + "model": "formal/dialcache-policy-conformance.qnt" }, "nativeMutants": { "kind": "mapped", "mutant": "M23", - "text": "M23 is the native twin of kernel/serving.qnt settleLocal warming a slot only when it holds NO_VALUE: both ports make a local publication skip a slot that is already occupied, even by an expired entry. The declared reproducer source-budgets/localEntryExpiresAfterItsTtlTest does not transfer natively because both ports purge an expired entry at the read that starts the source; policy/independentLocalPublicationUsesLastCompletionTest is the portable evidence (calls 1,2,2 instead of 1,2,1)." + "text": "M23 is the native twin of kernel/local-storage.qnt putLocal refusing an occupied slot: both ports skip a local publication when an entry already occupies the key, even if it has expired. The shared storage rule is reached by both the local-only and layered traversals." } }, { @@ -2325,81 +3294,369 @@ "source": "formal/kernel/serving.qnt", "model": "formal/dialcache-source-budgets-conformance.qnt", "invariant": "localPublicationRequiresAcceptedSource", - "before": "Local::putLocal(state, source.instance, source.key, value, source.localMs, layout.keysPerInstance) else state", - "after": "Local::putLocal(state, source.instance, source.key, value, source.localMs, layout.keysPerInstance) else { localValues: state.localValues.replaceAt(source.instance * layout.keysPerInstance + source.key, NO_VALUE), localExpires: state.localExpires.replaceAt(source.instance * layout.keysPerInstance + source.key, 0), ...state }", + "before": "if (accepted(value)) warm(state, layout, source, value, source.localMs) else state", + "after": "if (accepted(value)) warm(state, layout, source, value, source.localMs) else { localValues: state.localValues.replaceAt(source.instance * layout.keysPerInstance + source.key, NO_VALUE), localExpires: state.localExpires.replaceAt(source.instance * layout.keysPerInstance + source.key, 0), lru: state.lru.replaceAt(source.instance, state.lru.nth(source.instance).select(key => key != source.key)), ...state }", "reproducer": { "kind": "exported-regression", - "run": "rejectedSourceLeavesPublishedLocalEntryTest", - "failure": "s.o.calls == List(VALUE_ONE, SOURCE_ERROR, VALUE_ONE) and s.o.loaders == 2", + "run": "independentFailureKeepsLocalValueWhenRemoteFailsTest", + "failure": "s.o.calls == List(VALUE_ONE, SOURCE_ERROR, VALUE_ONE) and s.o.loaders == 2 and s.o.reads == 2", "family": "failed-settlement-clears-local-entry", "profiles": [ - "source-budgets" + "source-budgets", + "policy", + "local-failure" ], "exclusions": { - "core": "Composes no kernel module, so serving.settleLocal is never executed.", - "effects": "Composes the held remote lifecycle: its loaders settle through remote_io, never the local projection's settleLocal, so the mutated text is never executed.", - "recovery": "Composes the held remote lifecycle, whose loaders settle through remote_io.settleLoader, so serving.settleLocal is never executed.", - "policy": "Composes no kernel module, so serving.settleLocal is never executed.", - "shadow": "Composes local storage, but every reply leaves the local layer off and the capacity is 0, so no local entry is ever written or read, and publishes through serving.publish, never settleLocal.", - "scope": "Composes the request-only projection without local storage: its sources settle through serving.settleRequest.", - "admission": "Composes the layered shape, which settles through remote_io and serving.publish, never serving.settleLocal.", - "layers": "Composes the layered traversal: its sources settle through serving.settle, not the local projection.", - "independent": "Composes the layered shape: its loaders settle through remote_io and serving.publish, never the local projection's settleLocal.", - "recovery-read": "Composes the layered shape, which publishes through serving.publish, never serving.settleLocal.", - "local-failure": "Composes the layered shape, which publishes through serving.publish, never serving.settleLocal.", - "runtime-boundaries": "Composes the layered traversal: its sources settle through serving.settle, not the local projection.", - "shadow-layers": "Composes the layered shape, which publishes through serving.publish, never serving.settleLocal.", - "local-clock": "Composes the local projection through serving.settleLocal, but its inline sources never fail (every offered value is accepted), so the failed-settlement arm is never taken." - } + "effects": "No local TTL is configured, and rejected held sources complete without calling publishLocal.", + "recovery": "The held lifecycle handles rejection without calling publishLocal, and local capacity is zero.", + "shadow": "Every reply leaves the local layer off and local capacity is zero, so no accepted local entry can be lost.", + "scope": "The request-only traversal settles through serving.settleRequest and never publishes local storage.", + "admission": "Serving local storage is disabled; detached shadow sources have no local publication authority and no accepted local entry can be lost.", + "layers": "The consistent removal preserves storage membership and source ownership. No scheduled history or sampled property at the check bounds probes a previously published local value after the rejection.", + "independent": "The held lifecycle handles rejection without calling publishLocal, and local capacity is zero.", + "recovery-read": "Rejected held sources go through remote_io.settleLoader, which handles their error or recovery without calling publishLocal with the rejected value.", + "runtime-boundaries": "No scheduled history or sampled property at the check bounds probes a previously published local value after another source rejects; the partition checker remeasures that exclusion.", + "shadow-layers": "Rejected sources go through remote_writes.failed, which completes or recovers them without calling publishLocal with the rejected value.", + "local-clock": "Its inline sources only return accepted values, so the failure-preserves-local branch is never taken.", + "dark-layers": "Rejected held sources complete without calling publishLocal with the rejected value, so they cannot take the mutated failure branch.", + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." + }, + "model": "formal/dialcache-policy-conformance.qnt" }, "nativeMutants": { "kind": "mapped", "mutant": "M24", - "text": "M24 is the native twin of kernel/serving.qnt settleLocal writing NO_VALUE and a zero expiry into the slot on its not-accepted branch: both ports clear the key's local entry when a source settlement fails. The declared reproducer source-budgets/rejectedSourceLeavesPublishedLocalEntryTest does not transfer natively because its failing source is an outside bypass, so the policy regression independentFailureKeepsSettledLocalValueTest is the portable evidence." + "text": "M24 is the native twin of kernel/serving.qnt publishLocal removing the key's existing local entry on rejection. The shared rule applies to local-only and layered settlements; the native edits affect active fallback paths with an available key, while outside bypass calls never enter those paths." + } + }, + { + "id": "dark-layers-process-flight-crosses-instance", + "contract": "C11", + "source": "formal/kernel/serving.qnt", + "model": "formal/dialcache-dark-layers-conformance.qnt", + "invariant": "sourceOwnersShareTheSourcesIdentity", + "before": "pure def identity(layout: Layout, call: Call): int = call.instance * layout.keysPerInstance + call.key", + "after": "pure def identity(layout: Layout, call: Call): int = call.key", + "nativeMutants": { + "kind": "mapped", + "mutant": "M20", + "text": "M20 is the native twin of kernel/serving.qnt identity dropping the instance term from the flight identity: both ports register process flights in one process-wide registry, so a pending flight on one instance is joined from another. The held dark-layers history replays the same public boundary in both ports." + }, + "reproducer": { + "kind": "exported-regression", + "run": "sameKeyCallersOnTwoInstancesKeepSeparateFlightsTest", + "failure": "s.o.calls == List(CALL_PENDING, CALL_PENDING) and s.o.loaders == 2 and s.o.reads == 2 and s.o.shadow == List() and s.d.coalesced == List()", + "family": "instance-flight-identity", + "profiles": [ + "dark-layers", + "admission", + "layers" + ], + "exclusions": { + "effects": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after removing the instance from pending process-flight identity. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "recovery": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after removing the instance from pending process-flight identity. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "policy": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after removing the instance from pending process-flight identity. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "shadow": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after removing the instance from pending process-flight identity. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "scope": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after removing the instance from pending process-flight identity. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "independent": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after removing the instance from pending process-flight identity. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "recovery-read": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after removing the instance from pending process-flight identity. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "local-failure": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after removing the instance from pending process-flight identity. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "runtime-boundaries": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after removing the instance from pending process-flight identity. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "shadow-layers": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after removing the instance from pending process-flight identity. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "local-clock": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after removing the instance from pending process-flight identity. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "source-budgets": "Every named run and the scheduled invariant check at 2000 samples by 40 steps passes after removing the instance from pending process-flight identity. This bounded profile supplies no detecting assertion for that shared fault; the listed profiles carry its model evidence.", + "shadow-read-deadlines": "All six exported read-deadline regressions and the scheduled 2000 x 40 invariant check pass under this mutation. This profile claims separate dark read deadlines and raw ownership, not this property; detecting profiles are listed." + } + }, + "measures": "Repeats the layers identity fault against the held dark path: every caller must own a source for its own key and instance." + }, + { + "source": "formal/kernel/deadlines.qnt", + "before": "at: state.now + budget }), ...state }", + "after": "at: budget }), ...state }", + "id": "dark-layers-budget-starts-at-clock-origin", + "contract": "C23", + "model": "formal/dialcache-dark-layers-conformance.qnt", + "invariant": "pendingDeadlinesAreFuture", + "measures": "Repeats the shared wrong-origin deadline fault against a later dark caller and its owned diagnostic job.", + "nativeMutants": { + "kind": "mapped", + "mutant": "M33", + "text": "M33 applies the generic elapsed deadline to clock uptime instead of the work start. The shared model fault registers the same wrong absolute deadline. A dark caller starts at elapsed 10 ms and resolves immediately, within its own 10 ms budget. The source must succeed and publish locally for another request scope while the independent dark read remains held. Both faulty variants reject this source and fail the caller-result checkpoint.", + "evidence": { + "history": "dark-layers/laterDarkSourceKeepsItsWholeBudgetTest", + "step": 5, + "fields": [ + "o.calls" + ] + } + }, + "reproducer": { + "kind": "exported-regression", + "run": "laterDarkSourceKeepsItsWholeBudgetTest", + "failure": "s.o.calls == List(VALUE_ONE, VALUE_ONE) and s.o.loaders == 1 and s.o.reads == 1 and s.o.dumps == 0 and s.o.shadow == List() and s.d.fallbackErrors == List()", + "family": "source-budget-starts-with-source", + "profiles": [ + "effects", + "recovery", + "shadow", + "independent", + "shadow-layers", + "source-budgets", + "dark-layers", + "admission", + "shadow-read-deadlines" + ], + "exclusions": { + "scope": "Request-only sources have no deadline or elapsed clock; diagnostics imports the deadline module without invoking its registration transition. All 14 named runs and all six scheduled invariants pass with the wrong-origin fault at 2000 samples by 40 steps." + } + } + }, + { + "source": "formal/kernel/deadlines.qnt", + "before": "at: state.now + budget }), ...state }", + "after": "at: budget }), ...state }", + "id": "admission-budget-starts-at-clock-origin", + "contract": "C54", + "model": "formal/dialcache-admission-conformance.qnt", + "invariant": "pendingDeadlinesAreFuture", + "measures": "Repeats the shared wrong-origin deadline fault against a later detached job admitted after a served cache hit.", + "nativeMutants": { + "kind": "mapped", + "mutant": "M33", + "text": "M33 applies the generic elapsed deadline to clock uptime instead of the job start. This served-hit job starts at 10 ms and its source rejects at 11 ms, inside its own 10 ms budget. Both the wrong model registration and native settlement report timeout instead of source_error; the served caller keeps its acquired value.", + "evidence": { + "history": "admission/laterServedShadowKeepsItsWholeBudgetTest", + "step": 6, + "fields": [ + "o.shadow" + ] + }, + "crossContract": "The C23 mutant changes the generic deadline wrapper used by both caller sources and C54 detached shadow jobs. This mapping requires the shadow outcome from the latter, not a caller-source failure." + }, + "reproducer": { + "kind": "exported-regression", + "run": "laterServedShadowKeepsItsWholeBudgetTest", + "failure": "s.o.calls == List(VALUE_ONE) and s.o.loaders == 1 and s.o.loads == 1 and s.o.shadow == List(\"source_error\")", + "family": "source-budget-starts-with-source", + "profiles": [ + "effects", + "recovery", + "shadow", + "independent", + "shadow-layers", + "source-budgets", + "dark-layers", + "admission", + "shadow-read-deadlines" + ], + "exclusions": { + "scope": "Request-only sources have no deadline or elapsed clock; diagnostics imports the deadline module without invoking its registration transition. All 14 named runs and all six scheduled invariants pass with the wrong-origin fault at 2000 samples by 40 steps." + } + } + }, + { + "id": "dark-layers-timeout-releases-held-capacity", + "contract": "C54", + "source": "formal/kernel/shadow.qnt", + "model": "formal/dialcache-dark-layers-conformance.qnt", + "invariant": "heldEffectsKeepTheirJob", + "before": "if (job.dark and job.phase == WAITING_SOURCE) { jobs: expired.jobs.select(other => other.source != job.source), ...expired }", + "after": "if (true) { jobs: expired.jobs.select(other => other.source != job.source), ...expired }", + "reproducer": { + "kind": "exported-regression", + "run": "timedOutDarkDumpKeepsCapacityUntilRawReleaseTest", + "failure": "s.o.calls == List(VALUE_ONE, CALL_PENDING) and s.o.reads == 1 and s.o.shadow == List(\"timeout\", \"dropped\")", + "family": "shadow-timeout-releases-held-capacity", + "profiles": [ + "shadow", + "admission", + "shadow-layers", + "dark-layers", + "shadow-read-deadlines" + ], + "exclusions": {} + }, + "nativeMutants": { + "kind": "mapped", + "mutant": "M58", + "text": "M58 removes the shadow registration when the whole-job timeout is reported in both ports, while raw read/decode/dump/write work can still be pending. The kernel expireJob edit frees the same held-work capacity at timeout. The exported held-dump history detects the premature admission at its competing-key public checkpoint; companion histories cover C0 read, decode, C1 read and write." + } + }, + { + "id": "effects-stale-frame-reply-unclassified", + "contract": "C58", + "source": "formal/kernel/metrics.qnt", + "model": "formal/dialcache-effects-conformance.qnt", + "invariant": "staleFrameRepliesReportExpired", + "before": "if (Replies::isFrame(state.reply)) declinedFrameReason(state.replyAt, Clock::wallOf(state))", + "after": "if (Replies::isFrame(state.reply)) \"unclassified\"", + "reproducer": { + "kind": "exported-regression", + "run": "staleFrameReplyExpiresRefillsAndIsReadableTest", + "failure": "missReasons == List(\"expired\") and s.o.loads == 0 and s.o.loaders == 1", + "family": "expired-frame-reply-category", + "profiles": [ + "effects" + ], + "exclusions": {} + }, + "nativeMutants": { + "kind": "mapped", + "mutant": "M57", + "text": "M57 is the native twin of a stale queued frame reply losing its expired miss category: both ports report unclassified at the maximum-age guard while the ordinary source and refill still proceed.", + "evidence": { + "history": "effects/staleFrameReplyExpiresRefillsAndIsReadableTest", + "step": 4, + "fields": [ + "events" + ] + } + } + }, + { + "id": "inspection-counts-leaders-as-followers", + "contract": "C11", + "source": "formal/kernel/flights.qnt", + "model": "formal/dialcache-dark-layers-conformance.qnt", + "invariant": "inspectionsCountOnlyProcessFollowers", + "before": "activeFollowers: joined.size()", + "after": "activeFollowers: joined.size() + registered.size()", + "reproducer": { + "kind": "exported-regression", + "run": "inspectionTracksKeysFollowersAndOldestLeaderTest", + "failure": "s.d.inspections == List({ instance: 0, activeLeaders: 2, activeFollowers: 1, oldestLeaderAgeMs: 1 })", + "family": "process-follower-accounting", + "profiles": [ + "dark-layers" + ], + "exclusions": { + "effects": "This profile does not call inspectProcess or compare public coalescing snapshots; the modified inspection helper has no effect on its histories.", + "scope": "This profile does not call inspectProcess or compare public coalescing snapshots; the modified inspection helper has no effect on its histories.", + "recovery": "This profile does not call inspectProcess or compare public coalescing snapshots; the modified inspection helper has no effect on its histories.", + "policy": "This profile does not call inspectProcess or compare public coalescing snapshots; the modified inspection helper has no effect on its histories.", + "shadow": "This profile does not call inspectProcess or compare public coalescing snapshots; the modified inspection helper has no effect on its histories.", + "admission": "This profile does not call inspectProcess or compare public coalescing snapshots; the modified inspection helper has no effect on its histories.", + "layers": "This profile does not call inspectProcess or compare public coalescing snapshots; the modified inspection helper has no effect on its histories.", + "independent": "This profile does not call inspectProcess or compare public coalescing snapshots; the modified inspection helper has no effect on its histories.", + "recovery-read": "This profile does not call inspectProcess or compare public coalescing snapshots; the modified inspection helper has no effect on its histories.", + "local-failure": "This profile does not call inspectProcess or compare public coalescing snapshots; the modified inspection helper has no effect on its histories.", + "runtime-boundaries": "This profile does not call inspectProcess or compare public coalescing snapshots; the modified inspection helper has no effect on its histories.", + "shadow-layers": "This profile does not call inspectProcess or compare public coalescing snapshots; the modified inspection helper has no effect on its histories.", + "local-clock": "This profile does not call inspectProcess or compare public coalescing snapshots; the modified inspection helper has no effect on its histories.", + "source-budgets": "This profile does not call inspectProcess or compare public coalescing snapshots; the modified inspection helper has no effect on its histories.", + "shadow-read-deadlines": "Measured with all six exported read-deadline histories and all six scheduled invariants at 2000 x 40: all pass. The profile imports flights but never calls recordInspectionAdmission or inspectProcess, so this inspection-only fault has no executed path." + } + }, + "nativeMutants": { + "kind": "mapped", + "mutant": "M59", + "text": "M59 is the native twin of counting each registered leader as an extra follower in the public process-coalescing snapshot.", + "evidence": { + "history": "dark-layers/inspectionTracksKeysFollowersAndOldestLeaderTest", + "step": 8, + "fields": [ + "d.inspections" + ] + } + } + }, + { + "id": "inspection-age-uses-wall-clock", + "contract": "C11", + "source": "formal/kernel/flights.qnt", + "model": "formal/dialcache-dark-layers-conformance.qnt", + "invariant": "inspectionAgeUsesTheOldestLiveMonotonicStart", + "before": "oldestLeaderAgeMs: if (registered.size() == 0) NO_LEADER_AGE else state.now - oldest", + "after": "oldestLeaderAgeMs: if (registered.size() == 0) NO_LEADER_AGE else state.now + state.skew - oldest", + "reproducer": { + "kind": "exported-regression", + "run": "inspectionIsPerInstanceAndUsesMonotonicTimeTest", + "failure": "s.d.inspections == List({ instance: 0, activeLeaders: 1, activeFollowers: 0, oldestLeaderAgeMs: 1 })", + "family": "process-inspection-clock", + "profiles": [ + "dark-layers" + ], + "exclusions": { + "effects": "This profile does not call inspectProcess or compare public coalescing snapshots; the modified inspection helper has no effect on its histories.", + "scope": "This profile does not call inspectProcess or compare public coalescing snapshots; the modified inspection helper has no effect on its histories.", + "recovery": "This profile does not call inspectProcess or compare public coalescing snapshots; the modified inspection helper has no effect on its histories.", + "policy": "This profile does not call inspectProcess or compare public coalescing snapshots; the modified inspection helper has no effect on its histories.", + "shadow": "This profile does not call inspectProcess or compare public coalescing snapshots; the modified inspection helper has no effect on its histories.", + "admission": "This profile does not call inspectProcess or compare public coalescing snapshots; the modified inspection helper has no effect on its histories.", + "layers": "This profile does not call inspectProcess or compare public coalescing snapshots; the modified inspection helper has no effect on its histories.", + "independent": "This profile does not call inspectProcess or compare public coalescing snapshots; the modified inspection helper has no effect on its histories.", + "recovery-read": "This profile does not call inspectProcess or compare public coalescing snapshots; the modified inspection helper has no effect on its histories.", + "local-failure": "This profile does not call inspectProcess or compare public coalescing snapshots; the modified inspection helper has no effect on its histories.", + "runtime-boundaries": "This profile does not call inspectProcess or compare public coalescing snapshots; the modified inspection helper has no effect on its histories.", + "shadow-layers": "This profile does not call inspectProcess or compare public coalescing snapshots; the modified inspection helper has no effect on its histories.", + "local-clock": "This profile does not call inspectProcess or compare public coalescing snapshots; the modified inspection helper has no effect on its histories.", + "source-budgets": "This profile does not call inspectProcess or compare public coalescing snapshots; the modified inspection helper has no effect on its histories.", + "shadow-read-deadlines": "Measured with all six exported read-deadline histories and all six scheduled invariants at 2000 x 40: all pass. The profile imports flights but never calls recordInspectionAdmission or inspectProcess, so this inspection-only fault has no executed path." + } + }, + "nativeMutants": { + "kind": "mapped", + "mutant": "M60", + "text": "M60 is the native twin of subtracting the monotonic registration instant from the application wall clock in the public oldest-leader age.", + "evidence": { + "history": "dark-layers/inspectionIsPerInstanceAndUsesMonotonicTimeTest", + "step": 6, + "fields": [ + "d.inspections" + ] + } + } + }, + { + "id": "shadow-read-deadline-ignores-bounded-result", + "contract": "C54", + "source": "formal/kernel/shadow-reads.qnt", + "model": "formal/dialcache-shadow-read-deadlines-conformance.qnt", + "invariant": "abortedReadsHaveEndedTheirResult", + "before": " finishReadResult(aborted, read, job)", + "after": " aborted", + "reproducer": { + "kind": "exported-regression", + "run": "c0ReadDeadlineKeepsRawCapacityTest", + "failure": "s.o.calls == List(VALUE_ONE) and s.o.shadow == List(\"redis_error\") and s.io.aborted == List(0)", + "family": "shadow-read-deadline-ignores-bounded-result", + "profiles": [ + "shadow-read-deadlines" + ], + "exclusions": {} + }, + "nativeMutants": { + "kind": "mapped", + "mutant": "M61", + "text": "M61 is the native twin of this model fault. Both ports keep the read timer and cancellation but wait for the raw reply instead of consuming the bounded read error. The shadow-reads kernel fault records cancellation without ending that read result, and the generated C0 history detects the missing redis_error at exactly 5 ms." + } + }, + { + "id": "shadow-read-deadline-releases-raw-capacity", + "contract": "C54", + "source": "formal/kernel/shadow-reads.qnt", + "model": "formal/dialcache-shadow-read-deadlines-conformance.qnt", + "invariant": "rawReadsKeepTheirRegistration", + "before": " expiredReads: state.expiredReads.append(read),", + "after": " jobs: state.jobs.select(other => other.source != job.source),\n expiredReads: state.expiredReads.append(read),", + "reproducer": { + "kind": "exported-regression", + "run": "c0ReadDeadlineKeepsRawCapacityTest", + "failure": "s.o.reads == 1 and s.o.shadow == List(\"redis_error\", \"dropped\")", + "family": "shadow-read-deadline-releases-raw-capacity", + "profiles": [ + "shadow-read-deadlines" + ], + "exclusions": {} + }, + "nativeMutants": { + "kind": "mapped", + "mutant": "M62", + "text": "M62 is the native twin of this model fault. Both ports complete the bounded read error but release its shadow registration before the raw adapter operation settles. The kernel removes the same job while retaining the raw read, and the generated competing-key admission checkpoint detects the premature capacity release." } } ], - "reproducerBacklog": [ - "recovery-inclusive-maximum", - "recovery-future-candidate", - "local-precise-grid", - "source-inclusive-deadline", - "fence-inclusive-timestamp", - "profile-source-wrong-clock", - "profile-source-wrong-owner", - "independent-wrong-recovered-value", - "independent-source-wrong-clock", - "independent-source-wrong-owner", - "tracked-read-inclusive-fence", - "core-unhealthy-local-read-hits", - "core-tracked-fallback-warms-local", - "runtime-policy-coalesce-defaults-off", - "runtime-policy-physical-ttl-ignores-recovery", - "stale-recovery-inclusive-served-maximum", - "stale-recovery-candidate-stamped-at-read", - "redis-protocol-inclusive-fence", - "redis-protocol-untracked-fence", - "frame-vectors-inclusive-fence", - "invalidation-transition-cutoff-moves-backwards", - "invalidation-transition-inclusive-buffer-limit", - "key-protocol-untracked-brace-rejection", - "cohort-inclusive-threshold", - "envelope-vectors-tie-compresses", - "envelope-vectors-escape-misses-binary-marker", - "conformance-local-hit-returns-source", - "conformance-remote-miss-skips-publication", - "scope-late-source-repopulates-closed-memo", - "scope-source-error-memoized", - "layers-process-flight-crosses-instance", - "layers-late-memo-into-closed-scope", - "independent-fresh-frame-retained", - "runtime-boundaries-inclusive-cohort", - "runtime-boundaries-inherited-sharing-ignores-default", - "source-budgets-outside-call-has-deadline", - "source-budgets-settled-flight-stays-registered" - ], - "nativeMutantBacklog": [ - "invalidation-transition-cutoff-moves-backwards", - "invalidation-transition-inclusive-buffer-limit" - ] + "reproducerBacklog": [], + "nativeMutantBacklog": [] } diff --git a/formal/execution.mjs b/formal/execution.mjs index 777fe060..3da50d71 100644 --- a/formal/execution.mjs +++ b/formal/execution.mjs @@ -1,6 +1,7 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs'; import { dirname, posix, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { resolveVectorEvidence } from './vector-evidence.mjs'; export const root = fileURLToPath(new URL('../', import.meta.url)); // The kernel library's concern modules live in one directory; every tool that @@ -24,12 +25,16 @@ export function quintSources(directory = root) { // A model's import closure: its own text and every Quint source it reaches // through relative imports, in dependency order. What a model's behavior // depends on, so tools that hash, compare or classify a model walk it here. -export function importClosure(path, directory = root) { +export function importClosure(path, directory = root, { + readSource = source => existsSync(resolve(directory, source)) ? readFileSync(resolve(directory, source), 'utf8') : undefined, +} = {}) { const closure = []; const visit = source => { - if (closure.includes(source) || !existsSync(resolve(directory, source))) return; + if (closure.includes(source)) return; + const text = readSource(source); + if (text === undefined) return; closure.push(source); - for (const [, target] of readFileSync(resolve(directory, source), 'utf8').matchAll(/from\s+"(\.\.?\/[^"]+)"/g)) { + for (const [, target] of text.matchAll(/from\s+"(\.\.?\/[^"]+)"/g)) { visit(posix.normalize(posix.join(posix.dirname(source), `${target}.qnt`))); } }; @@ -222,6 +227,185 @@ export function reproducerCheckpoint(source, run, failure, declarations = scanDe throw new Error(`${run} has no top-level expect whose condition is the declared failure`); } +// Count states in the supported deterministic action-chain syntax, then turn +// that count into a zero-based ITF checkpoint. Expectations add no state; +// literal repetitions add every transition they execute. This deliberately +// refuses an unfamiliar expression instead of assigning it an approximate +// index: the challenge can supply reviewed written evidence in that case. +export function checkpointStep(before, declarations) { + const refuse = detail => { throw new Error(`Cannot derive boundary checkpoint: ${detail}; write nativeMutants.evidence`); }; + const matching = (tokens, start) => { + const close = shuts[opens.indexOf(tokens[start])]; + let depth = 1; + for (let i = start + 1; i < tokens.length; i++) { + if (tokens[i] === tokens[start]) depth++; + else if (tokens[i] === close && --depth === 0) return i; + } + return refuse('unbalanced action expression'); + }; + const add = (a, b) => { + const count = a + b; + if (!Number.isSafeInteger(count)) refuse('checkpoint exceeds safe integer range'); + return count; + }; + function count(tokens, trail = new Set()) { + while (tokens[0] === '(' && matching(tokens, 0) === tokens.length - 1) tokens = tokens.slice(1, -1); + if (!tokens.length) return refuse('empty action expression'); + const chain = []; + for (let i = 0; i < tokens.length; i++) { + if (opens.includes(tokens[i])) { i = matching(tokens, i); continue; } + if (tokens[i] === '.' && ['then', 'expect'].includes(tokens[i + 1])) { + if (tokens[i + 2] !== '(') return refuse('unrecognized chain operator'); + const end = matching(tokens, i + 2); + chain.push({ start: i, end, method: tokens[i + 1], argument: tokens.slice(i + 3, end) }); + i = end; + } + } + if (chain.length) { + let total = count(tokens.slice(0, chain[0].start), trail); + let end = chain[0].start - 1; + for (const part of chain) { + if (part.start !== end + 1) return refuse('unsupported expression between chain steps'); + if (part.method === 'then') total = add(total, count(part.argument, trail)); + end = part.end; + } + if (end !== tokens.length - 1) return refuse('unsupported action-chain suffix'); + return total; + } + const dot = tokens.indexOf('.'); + if (dot > 0 && tokens[dot + 1] === 'reps' && tokens[dot + 2] === '(' && matching(tokens, dot + 2) === tokens.length - 1) { + const literal = tokens.slice(0, dot).join(''); + if (!/^\d+$/.test(literal) || !Number.isSafeInteger(Number(literal))) return refuse('repetition count must be a safe integer literal'); + const lambda = tokens.slice(dot + 3, -1); + if (!/^[A-Za-z_]\w*$/.test(lambda[0] ?? '') || lambda[1] !== '=' || lambda[2] !== '>') return refuse('unsupported repetition lambda'); + const repeated = Number(literal) * count(lambda.slice(3), trail); + if (!Number.isSafeInteger(repeated)) return refuse('repetition count exceeds safe integer range'); + return repeated; + } + const name = tokens[0]; + const declaration = declarations.get(name); + if (!declaration || !['action', 'run'].includes(declaration.kind)) return refuse(`unknown action ${name}`); + if (trail.has(name)) return refuse(`cyclic action alias ${name}`); + if (tokens.length !== 1 && !(tokens[1] === '(' && matching(tokens, 1) === tokens.length - 1)) return refuse(`unsupported action expression ${name}`); + const body = declaration.body; + let equals = -1; + for (let i = 0; i < body.length; i++) { + if (opens.includes(body[i])) { i = matching(body, i); continue; } + if (body[i] === '=') { equals = i; break; } + } + if (equals < 0) return refuse(`action ${name} has no body`); + const rhs = body.slice(equals + 1); + const sequences = rhs.some((token, i) => token === '.' && ['then', 'reps'].includes(rhs[i + 1])); + if (['all', 'any', '{'].includes(rhs[0])) { + if (sequences) return refuse(`nested action sequence in ${name}`); + // An atomic wrapper must not hide a multi-state action behind an alias. + for (const [index, token] of rhs.entries()) { + if (rhs[index - 1] === ':' || rhs[index - 1] === '.' || rhs[index + 1] === ':') continue; + if (declarations.get(token)?.kind === 'action' && count([token], new Set([...trail, name])) !== 1) { + return refuse(`nested action sequence in ${name}`); + } + } + return 1; + } + return count(rhs, new Set([...trail, name])); + } + const states = count(tokenize(before).tokens); + if (states < 1) refuse('history has no initial state'); + return states - 1; +} + +const observationFields = new Set(['calls', 'loaders', 'reads', 'loads', 'dumps', 'writes', 'policyCalls', 'invalidations', + 'classifications', 'comparisons', 'maintenance', 'recovery', 'shadow', 'sourceScopes', 'writeTtls']); +const effectsFields = new Set(['calls', 'loaders', 'reads', 'loads', 'dumps', 'writes', 'policyCalls', 'invalidations', + 'writeTtls', 'events', 'readContexts', 'readAborts']); +const coreFields = new Set(['sourceVersion', 'lastResult', 'outsideLoaderCalls', 'requestLoaderCalls', + 'localLoaderCalls', 'coalescedLoaderCalls', 'remoteLoaderCalls', 'redisReads', 'redisWrites']); +function boundaryField(profile, field) { + if (profile === 'core') return coreFields.has(field) ? field : undefined; + if (profile === 'effects' || profile === 'local-clock') { + if (field.startsWith('o.')) field = field.slice(2); + if (profile === 'effects') field = ({ 'io.budgets': 'readContexts', 'io.aborted': 'readAborts' })[field] ?? field; + return (profile === 'effects' ? effectsFields : observationFields).has(field) ? field : undefined; + } + return /^(o|d|io)\.\w+$/.test(field) || /^(markers|compression|policyErrors)$/.test(field) ? field : undefined; +} + +// Fields name the actual assertion record, rather than the model's private +// state. The flat replay bindings (core, effects and local-clock) are explicit; +// feature profiles retain their channel prefix. Tests cross-check these paths +// against the bindings without making the manifest validator import replay. +export function evidenceOf(challenge, models, publicOnly, { readSource = read, scanSource = scanDeclarationBodies } = {}) { + const native = challenge.nativeMutants; + if (native?.kind !== 'mapped') return undefined; + const base = { challenge: challenge.id, mutant: native.mutant }; + const model = models.get(challenge.reproducer?.model ?? challenge.model); + if (!model) throw new Error(`${challenge.id}: boundary evidence model is not scheduled`); + const reproducer = challenge.reproducer; + const written = native.evidence; + if (model.vectorExport && reproducer?.kind === 'model-run') { + if (!written || Object.keys(written).join() !== 'vector') throw new Error(`${challenge.id}: vector reproducer requires exact native vector evidence`); + const vector = resolveVectorEvidence(written.vector, model, readSource); + return { ...base, history: `vector/${reproducer.run}`, step: 0, fields: [...vector.fields], origin: 'vector', vector }; + } + if (reproducer?.kind === 'model-run') { + throw new Error(`${challenge.id}: mapped native mutant requires an exported-regression or an exported-vector model-run reproducer`); + } + if (reproducer?.kind !== 'exported-regression') { + if (written !== undefined) throw new Error(`${challenge.id}: written boundary evidence requires an exported-regression reproducer`); + return { ...base, state: 'unreproduced' }; + } + if (!model.profile || !publicOnly.get(model.path)?.includes(reproducer.run)) throw new Error(`${challenge.id}: boundary evidence requires an exported public-only history`); + const history = `${model.profile}/${reproducer.run}`; + if (written !== undefined) { + if (!written || typeof written !== 'object' || Array.isArray(written) || + Object.keys(written).some(key => !['history', 'step', 'fields'].includes(key))) throw new Error(`${challenge.id}: invalid written boundary evidence`); + if (written.history !== history) throw new Error(`${challenge.id}: evidence history must equal the reproducer history ${history}`); + if (!Number.isSafeInteger(written.step) || written.step < 1) throw new Error(`${challenge.id}: evidence step must be a positive integer`); + if (!Array.isArray(written.fields) || !written.fields.length || new Set(written.fields).size !== written.fields.length || + written.fields.some(field => typeof field !== 'string' || boundaryField(model.profile, field) !== field)) { + throw new Error(`${challenge.id}: evidence fields must be nonempty unique public assertion paths for ${model.profile}`); + } + return { ...base, history, step: written.step, fields: [...written.fields], origin: 'written' }; + } + const source = readSource(model.path), declarations = scanSource(source); + const checkpoint = reproducerCheckpoint(source, reproducer.run, reproducer.failure, declarations); + const tokens = tokenize(reproducer.failure).tokens; + const fields = []; + for (let i = 0; i < tokens.length; i++) { + if (tokens[i] !== 's' || tokens[i + 1] !== '.') continue; + const channel = tokens[i + 2]; + if (model.profile === 'core') { + const field = boundaryField(model.profile, channel); + if (field && !fields.includes(field)) fields.push(field); + continue; + } + // A private field can share a public name (effects' reads/loaders lists). + // Only the public channels, plus the explicitly compared root channels, + // contribute evidence; flat paths are produced by projection below. + if (!['o', 'd', 'io', 'events', 'markers', 'compression', 'policyErrors'].includes(channel)) continue; + const raw = ['o', 'd', 'io'].includes(channel) && tokens[i + 3] === '.' + ? `${channel}.${tokens[i + 4]}` : channel; + const field = boundaryField(model.profile, raw); + if (field && !fields.includes(field)) fields.push(field); + } + if (!fields.length) throw new Error(`${challenge.id}: checkpoint has no derived public fields; write nativeMutants.evidence`); + const step = checkpointStep(checkpoint.before, declarations); + if (step < 1) throw new Error(`${challenge.id}: boundary evidence checkpoint must follow initialization`); + return { ...base, history, step, fields, origin: 'derived' }; +} + +export function boundaryEvidence(manifest = readExecution(), { readSource = read, scanSource = scanDeclarationBodies } = {}) { + const sources = new Map(), declarations = new Map(); + const source = path => { if (!sources.has(path)) sources.set(path, readSource(path)); return sources.get(path); }; + const scanned = text => { if (!declarations.has(text)) declarations.set(text, scanSource(text)); return declarations.get(text); }; + const models = new Map(manifest.models.map(model => [model.path, model])); + const publicOnly = new Map(manifest.models.filter(model => model.profile).map(model => [model.path, classifyRuns(scanned(source(model.path))).publicOnly])); + return manifest.challenges.flatMap(challenge => { + const evidence = evidenceOf(challenge, models, publicOnly, { readSource: source, scanSource: scanned }); + return evidence === undefined ? [] : [evidence]; + }); +} + const positiveInteger = (value, label) => { if (!Number.isSafeInteger(value) || value <= 0) throw new Error(`Invalid positive bound: ${label}`); }; @@ -245,12 +429,11 @@ export const contractIds = text => [...text.matchAll(/^\| ([CWEB]\d{2}) \|/gm)]. // condition the fault breaks. `profiles` names where the fault is observable: // the challenged model's own profile, or its path for a model without one, // and the cited run's profile. `exclusions` explains why a known profile that -// is not listed cannot exercise the fault. A shared-library fault must list or -// exclude every profile; a fault in one model's own file needs no exclusions, -// because no other profile executes that text. +// imports the source but is not listed cannot exercise the fault. Profiles +// outside that import closure are structurally excluded without catalog prose. const reproducerKinds = ['exported-regression', 'model-run']; const reproducerFields = ['kind', 'run', 'model', 'failure', 'family', 'profiles', 'exclusions', 'scope']; -function validateReproducer(challenge, model, { models, libraries, profileIds, publicOnly, source, scanned }) { +function validateReproducer(challenge, model, { models, libraries, profileIds, publicOnly, source, scanned, closures }) { const { id, reproducer } = challenge; if (!reproducer || typeof reproducer !== 'object' || Array.isArray(reproducer)) throw new Error(`${id}: invalid reproducer`); const unknown = Object.keys(reproducer).filter(key => !reproducerFields.includes(key)); @@ -259,8 +442,10 @@ function validateReproducer(challenge, model, { models, libraries, profileIds, p if (!reproducerKinds.includes(kind)) throw new Error(`${id}: reproducer kind must be one of ${reproducerKinds.join(', ')}`); const shared = libraries.includes(challenge.source); const cited = reproducer.model === undefined ? model : models.get(reproducer.model); - if (reproducer.model !== undefined && (!cited?.profile || kind !== 'exported-regression' || !shared || cited === model)) { - throw new Error(`${id}: reproducer model must name another profile model and is allowed only for an exported-regression of a shared-library fault: ${reproducer.model}`); + const sharedProfile = cited?.profile && kind === 'exported-regression'; + const sharedVector = cited?.vectorExport && kind === 'model-run'; + if (reproducer.model !== undefined && (!(sharedProfile || sharedVector) || !shared || cited === model)) { + throw new Error(`${id}: reproducer model must name another profile model or vector model reached by a shared-library fault, with the corresponding reproducer kind: ${reproducer.model}`); } if (typeof run !== 'string' || !cited.regressions.includes(run)) throw new Error(`${id}: reproducer run is not a scheduled regression of ${cited.path}: ${run}`); if (!nonEmptyText(failure)) throw new Error(`${id}: reproducer failure must state the expect condition the fault breaks`); @@ -268,9 +453,10 @@ function validateReproducer(challenge, model, { models, libraries, profileIds, p catch (error) { throw new Error(`${id}: ${error.message}`); } if (!isSlug(family)) throw new Error(`${id}: reproducer family must be a fault family slug`); const own = model.profile ?? model.path; - const required = [...new Set([own, ...(cited.profile ? [cited.profile] : [])])]; + const citedIdentity = cited.profile ?? (reproducer.model && sharedVector ? cited.path : own); + const required = [...new Set([own, citedIdentity])]; if (!Array.isArray(profiles) || !profiles.length || new Set(profiles).size !== profiles.length || - required.some(name => !profiles.includes(name)) || profiles.some(profile => profile !== own && !profileIds.has(profile))) { + required.some(name => !profiles.includes(name)) || profiles.some(profile => !required.includes(profile) && !profileIds.has(profile))) { throw new Error(`${id}: reproducer profiles must name known profiles and include ${required.join(' and ')}`); } if (!exclusions || typeof exclusions !== 'object' || Array.isArray(exclusions)) throw new Error(`${id}: reproducer exclusions must map profiles to reasons`); @@ -280,8 +466,18 @@ function validateReproducer(challenge, model, { models, libraries, profileIds, p } } if (shared) { - const unaccounted = [...profileIds].filter(profile => !profiles.includes(profile) && !Object.hasOwn(exclusions, profile)); - if (unaccounted.length) throw new Error(`${id}: a shared-library fault must list or exclude every profile; missing ${unaccounted.join(', ')}`); + const unaccounted = []; + for (const profileModel of models.values()) { + if (!profileModel.profile) continue; + const reaches = closures.get(profileModel.path).includes(challenge.source); + if (profiles.includes(profileModel.profile) && !reaches) { + throw new Error(`${id}/${profileModel.profile}: listed profile does not import ${challenge.source}`); + } + if (reaches && !profiles.includes(profileModel.profile) && !Object.hasOwn(exclusions, profileModel.profile)) { + unaccounted.push(profileModel.profile); + } + } + if (unaccounted.length) throw new Error(`${id}: a shared-library fault must list or exclude every importing profile; missing ${unaccounted.join(', ')}`); } const exported = cited.replayRegressions?.includes(run) ?? false; if (kind === 'exported-regression') { @@ -307,7 +503,7 @@ function validateReproducer(challenge, model, { models, libraries, profileIds, p // the challenge's contract. The challenge text says why the mutant is the same // fault as the model's; the port-side account lives once on the catalog entry. export const nativeMutantKinds = ['mapped', 'unobservable', 'model-only']; -const nativeMutantFields = ['kind', 'text', 'mutant', 'crossContract']; +const nativeMutantFields = ['kind', 'text', 'mutant', 'crossContract', 'evidence']; const nativeMutantTextLimits = { mapped: 500, unobservable: 900, 'model-only': 900 }; // One catalog of native mutants, each entry a fault described once and @@ -386,7 +582,7 @@ export function checkMutantAnchors(catalog, readText = read) { } const portPath = /\b(?:src|go)\/[\w./-]+/; -function validateNativeMutants(challenge, { catalog }) { +function validateNativeMutants(challenge, { catalog, models, publicOnly, source, scanned }) { const { id, nativeMutants: native } = challenge; if (!native || typeof native !== 'object' || Array.isArray(native)) throw new Error(`${id}: invalid nativeMutants`); const unknown = Object.keys(native).filter(key => !nativeMutantFields.includes(key)); @@ -398,7 +594,7 @@ function validateNativeMutants(challenge, { catalog }) { // keeps the port-side account from creeping back into every citing challenge. if (text.length > nativeMutantTextLimits[kind]) throw new Error(`${id}: nativeMutants text exceeds ${nativeMutantTextLimits[kind]} characters; the port-side account belongs in the mutant's rationale`); if (kind !== 'mapped') { - if (mutant !== undefined || crossContract !== undefined) throw new Error(`${id}: ${kind} nativeMutants name no mutant`); + if (mutant !== undefined || crossContract !== undefined || native.evidence !== undefined) throw new Error(`${id}: ${kind} nativeMutants name no mutant or evidence`); // An explanation names the port code it examined, so a reader can check it. if (!portPath.test(text)) throw new Error(`${id}: ${kind} nativeMutants text must name the port file it examined`); return; @@ -411,24 +607,11 @@ function validateNativeMutants(challenge, { catalog }) { const outside = !catalog.caseContracts.get(entry.case).includes(challenge.contract); if (outside && !nonEmptyText(crossContract)) throw new Error(`${id}: ${mutant} is on case ${entry.case} which does not list ${challenge.contract}; add a crossContract reason`); if (!outside && crossContract !== undefined) throw new Error(`${id}: crossContract note for in-contract mutant ${mutant}`); + return evidenceOf(challenge, models, publicOnly, { + readSource: source, scanSource: () => scanned(challenge.reproducer?.model ?? challenge.model), + }); } -// Challenges that predate the native-mutant requirement. The cutoff fault -// lives only in the Lua invalidation script (src/internal/redis-scripts.ts, -// go/redis_adapter.go), which no in-process cohort executes. The buffer-limit -// fault has client-side lines in both ports (src/internal/duration.ts -// assertSupportedFutureBufferMs, go/cache.go Invalidate), but no generated -// history invalidates with the maximum buffer, so a mapped mutant would lack -// its required detection; it closes when a profile exposes the buffer as an -// input and an exported regression reaches the bound. The list may only -// shrink: a new challenge maps to a native mutant or explains why none exists, -// and listing it here instead is a reviewed change to this constant, never a -// manifest edit. -export const grandfatheredNativeMutantBacklog = Object.freeze([ - 'invalidation-transition-cutoff-moves-backwards', - 'invalidation-transition-inclusive-buffer-limit', -]); - // Which challenges cite each mutant, in manifest order. The mutation reports // print it beside every measured mutant. export function challengesByMutant(manifest) { @@ -442,82 +625,22 @@ export function challengesByMutant(manifest) { return index; } -// A backlog is the exact set of challenges lacking one field, reported and -// frozen: it cannot hide a challenge that has the field or never existed, and -// only the grandfathered ids may sit in it, so it only shrinks. -function validateBacklog(challenges, ids, { name, listed, present, grandfathered, missing, has, lacks, requirement }) { - if (!Array.isArray(listed)) throw new Error(missing); - const backlog = new Set(listed); - if (backlog.size !== listed.length) throw new Error(`Duplicate challenge ids in ${name}`); - for (const id of backlog) { - if (!ids.has(id)) throw new Error(`${name} names an unknown challenge: ${id}`); - } - const frozen = new Set(grandfathered); - for (const challenge of challenges) { - const inBacklog = backlog.has(challenge.id); - if (present(challenge) && inBacklog) throw new Error(`${challenge.id}: ${has} and is listed in ${name}`); - if (!present(challenge) && !inBacklog) throw new Error(`${challenge.id}: ${lacks} and is not listed in ${name}`); - if (inBacklog && !frozen.has(challenge.id)) throw new Error(`${challenge.id}: ${requirement}; ${name} only grandfathers the challenges that predate the requirement`); - } - return backlog.size; -} - -// Challenges that predate the reproducer requirement. The backlog may only -// shrink: a new challenge must carry a reproducer, and listing it here instead -// is a reviewed change to this constant, never a manifest edit. -export const grandfatheredReproducerBacklog = Object.freeze([ - 'recovery-inclusive-maximum', - 'recovery-future-candidate', - 'local-precise-grid', - 'source-inclusive-deadline', - 'fence-inclusive-timestamp', - 'profile-source-wrong-clock', - 'profile-source-wrong-owner', - 'independent-wrong-recovered-value', - 'independent-source-wrong-clock', - 'independent-source-wrong-owner', - 'tracked-read-inclusive-fence', - 'core-unhealthy-local-read-hits', - 'core-tracked-fallback-warms-local', - 'runtime-policy-coalesce-defaults-off', - 'runtime-policy-physical-ttl-ignores-recovery', - 'stale-recovery-inclusive-served-maximum', - 'stale-recovery-candidate-stamped-at-read', - 'redis-protocol-inclusive-fence', - 'redis-protocol-untracked-fence', - 'frame-vectors-inclusive-fence', - 'invalidation-transition-cutoff-moves-backwards', - 'invalidation-transition-inclusive-buffer-limit', - 'key-protocol-untracked-brace-rejection', - 'cohort-inclusive-threshold', - 'envelope-vectors-tie-compresses', - 'envelope-vectors-escape-misses-binary-marker', - 'conformance-local-hit-returns-source', - 'conformance-remote-miss-skips-publication', - 'scope-late-source-repopulates-closed-memo', - 'scope-source-error-memoized', - 'layers-process-flight-crosses-instance', - 'layers-late-memo-into-closed-scope', - 'independent-fresh-frame-retained', - 'runtime-boundaries-inclusive-cohort', - 'runtime-boundaries-inherited-sharing-ignores-default', - 'source-budgets-outside-call-has-deadline', - 'source-budgets-settled-flight-stays-registered', -]); - // Compiling semantic faults, checked against independent model obligations. // Every scheduled model carries at least one challenge or an explicit waiver. -// Every challenge carries a reproducer or is listed in the reported backlog, -// and maps to native mutants in both ports or is listed in that backlog. -function validateChallenges(manifest, { source, scanned, contracts, sources, profileIds, publicOnly, catalog, - grandfathered = grandfatheredReproducerBacklog, grandfatheredNative = grandfatheredNativeMutantBacklog }) { - const { challenges, reproducerBacklog, nativeMutantBacklog } = manifest; +// Every challenge carries a reproducer and a native mapping or explanation. +function validateChallenges(manifest, { source, scanned, contracts, sources, profileIds, publicOnly, catalog, closures }) { + const { challenges } = manifest; if (!Array.isArray(challenges) || !challenges.length) throw new Error('Model property challenge catalog is missing'); + // Closed migration fields remain empty for manifest/report compatibility. + for (const name of ['reproducerBacklog', 'nativeMutantBacklog']) { + if (!Array.isArray(manifest[name]) || manifest[name].length) throw new Error(`${name} must remain empty`); + } const models = new Map(manifest.models.map(model => [model.path, model])); const fields = ['id', 'contract', 'source', 'model', 'invariant', 'before', 'after']; const optional = ['measures', 'reproducer', 'nativeMutants']; const ids = new Set(), faults = new Map(), challengedModels = new Set(); const kinds = { mapped: 0, unobservable: 0, 'model-only': 0 }; + const boundary = { derived: 0, written: 0, unreproduced: 0, vector: 0 }; let reproducers = 0; for (const challenge of challenges) { if (!challenge || typeof challenge !== 'object' || fields.some(key => typeof challenge[key] !== 'string' || !challenge[key])) { @@ -542,27 +665,20 @@ function validateChallenges(manifest, { source, scanned, contracts, sources, pro throw new Error(`${challenge.id}: a measures note is only for a repeated fault`); } challengedModels.add(challenge.model); - if (challenge.reproducer !== undefined) { - validateReproducer(challenge, model, { models, libraries: manifest.libraries, profileIds, publicOnly, source, scanned }); - reproducers++; - } - if (challenge.nativeMutants !== undefined) { - validateNativeMutants(challenge, { catalog }); - kinds[challenge.nativeMutants.kind]++; - } + if (challenge.reproducer === undefined) throw new Error(`${challenge.id}: must carry a reproducer`); + validateReproducer(challenge, model, { models, libraries: manifest.libraries, profileIds, publicOnly, source, scanned, closures }); + reproducers++; + if (challenge.nativeMutants === undefined) throw new Error(`${challenge.id}: must map to a native mutant in both ports or explain why none exists`); + const evidence = validateNativeMutants(challenge, { catalog, models, publicOnly, source, scanned }); + if (evidence) boundary[evidence.origin ?? evidence.state]++; + kinds[challenge.nativeMutants.kind]++; // One fault, one native mapping: a repeated fault measured against another // invariant names the same kind and mutant. The text and any crossContract // reason speak for the challenge's own contract and may differ. - const native = challenge.nativeMutants === undefined ? null - : JSON.stringify([challenge.nativeMutants.kind, challenge.nativeMutants.mutant ?? null]); + const native = JSON.stringify([challenge.nativeMutants.kind, challenge.nativeMutants.mutant ?? null]); if (faults.has(fault) && faults.get(fault).native !== native) throw new Error(`${challenge.id}: maps the fault of ${faults.get(fault).id} differently`); if (!faults.has(fault)) faults.set(fault, { id: challenge.id, native }); } - const reproducerBacklogSize = validateBacklog(challenges, ids, { name: 'reproducerBacklog', listed: reproducerBacklog, present: challenge => challenge.reproducer !== undefined, - grandfathered, missing: 'Challenge reproducer backlog is missing', has: 'has a reproducer', lacks: 'has no reproducer', requirement: 'new challenges must carry a reproducer' }); - const nativeBacklogSize = validateBacklog(challenges, ids, { name: 'nativeMutantBacklog', listed: nativeMutantBacklog, present: challenge => challenge.nativeMutants !== undefined, - grandfathered: grandfatheredNative, missing: 'Challenge native-mutant backlog is missing', has: 'has nativeMutants', lacks: 'has no nativeMutants', - requirement: 'new challenges must map to a native mutant in both ports or explain why none exists' }); const waived = []; for (const model of manifest.models) { if (model.challengeWaiver !== undefined) { @@ -577,16 +693,14 @@ function validateChallenges(manifest, { source, scanned, contracts, sources, pro const cited = challengesByMutant(manifest); const unmappedMutants = [...catalog.mutations.keys()].filter(id => !cited.has(id)).length; return { challenges: challenges.length, distinctFaults: faults.size, challengedModels: challengedModels.size, waivedModels: waived.length, - reproducers, reproducerBacklog: reproducerBacklogSize, - nativeMutants: { mapped: kinds.mapped, unobservable: kinds.unobservable, modelOnly: kinds['model-only'], backlog: nativeBacklogSize }, - unmappedMutants }; + reproducers, reproducerBacklog: 0, + nativeMutants: { mapped: kinds.mapped, unobservable: kinds.unobservable, modelOnly: kinds['model-only'], backlog: 0 }, + boundaryEvidence: boundary, unmappedMutants }; } export function validateExecution(manifest = readExecution(), { readSource = read, scanSource = scanDeclarationBodies, - grandfathered = grandfatheredReproducerBacklog, - grandfatheredNative = grandfatheredNativeMutantBacklog, catalog = readMutantCatalog(), files = quintSources(), profiles = JSON.parse(read('formal/profiles.json')).profiles, @@ -721,12 +835,13 @@ export function validateExecution(manifest = readExecution(), { // so it may not stay in the tree. This is also the inventory tripwire: a // stray or half-deleted stateless source at formal/ is refused here rather // than admitted as a library by the directory listing. - const reached = new Set(manifest.models.flatMap(model => importClosure(model.path))); + const closures = new Map(manifest.models.map(model => [model.path, importClosure(model.path, root, { readSource: source })])); + const reached = new Set([...closures.values()].flat()); const orphans = libraries.filter(path => !reached.has(path)); if (orphans.length) throw new Error(`Quint libraries no scheduled model imports: ${orphans.join(', ')}; import them from a scheduled model or delete them`); if (!sameMembers(profileIds, profiles.map(profile => profile.id))) throw new Error('Generated profile inventory differs from claim registry'); const scheduled = { ...manifest, libraries, models: manifest.models.map(model => ({ ...model, ...schedules.get(model.path) })) }; - const challenges = validateChallenges(scheduled, { source, scanned, contracts, sources: new Set(paths), profileIds: new Set(profileIds), publicOnly, catalog, grandfathered, grandfatheredNative }); + const challenges = validateChallenges(scheduled, { source, scanned, contracts, sources: new Set(paths), profileIds: new Set(profileIds), publicOnly, catalog, closures }); return { models: manifest.models.length, libraries: libraries.length, profiles: profileIds.length, invariants, regressions, generatedTraces, exportedRegressionTraces, vectorModels, generatedVectors, ...challenges }; } diff --git a/formal/explore.mjs b/formal/explore.mjs index 3ae39ebf..c1e099b4 100644 --- a/formal/explore.mjs +++ b/formal/explore.mjs @@ -27,6 +27,9 @@ export function explorationPlan(directory, seed, options = {}) { const normalized = explorationSeed(seed); return validationPlan('formal', { ...options, directory }).flatMap(step => { const script = step.args?.[0]; + // This campaign uses the manifest's pinned seed, not the exploration seed. + // Full acceptance keeps it; exploration retains every unmodified model job. + if (script === 'formal/check-model-properties.mjs') return []; if (step.remove || ['formal/conformance-adapters.mjs', 'formal/check-go-replay.mjs'].includes(script) || script === 'formal/conformance.mjs' && step.args[1] === 'check') return []; if (script === 'formal/conformance.mjs' && step.args[1] === 'prepare') { diff --git a/formal/feature-coverage.json b/formal/feature-coverage.json index f9f8cfdd..01cb4bea 100644 --- a/formal/feature-coverage.json +++ b/formal/feature-coverage.json @@ -92,7 +92,7 @@ "C22.freshness-boundary", "C22.physical-vs-logical", "C06.absent-distinct-from-text", - "C22.freshness-after-read", + "C22.adapter-frame-expiry", "C22.freshness-after-read", "C05.request-hit-stops-traversal", "C05.local-hit-stops-traversal", "C09.fractional-clock-grid" @@ -120,6 +120,7 @@ "cases": [ "C11.distinct-keys", "C11.one-registered-flight", + "C11.process-inspection", "C12.separate-request-memos", "C13.uncoalesced-caching", "C13.local-last-writer", @@ -221,7 +222,8 @@ "C56.late-read", "C56.clear-read-timer", "C24.independent-source-budgets", - "C23.policy-does-not-spend-source-budget" + "C23.policy-does-not-spend-source-budget", + "C23.later-dark-budget-origins" ], "nativeCases": [ "B02.precise-elapsed-deadlines" @@ -414,6 +416,7 @@ "C53.confirmation-fails-open", "C54.deduplication", "C54.source-ownership", + "C54.later-served-job-budget-origin", "C54.decode-ownership", "C54.read-ownership", "C54.comparison-budget", @@ -617,25 +620,25 @@ "test": "reuses a stable use case without registering it and still rejects the reserved name", "path": "test/dialcache-get-or-load.test.ts", "scope": "Asserts that the TypeScript binding reuses a stable use case without registering it and still rejects the reserved name.", - "sha256": "48608eca552ac6d6a01b3c9b314be7e922919e6357c56349a2addc06dc2c043d" + "sha256": "e80d66b1f509806a3a7dc6beb02eb05ed154516999a886d8a6168cfd557ddc67" }, { "test": "validates and snapshots defaultConfig independently for each invocation", "path": "test/dialcache-get-or-load.test.ts", "scope": "Asserts that the TypeScript binding validates and snapshots defaultConfig independently for each invocation.", - "sha256": "48608eca552ac6d6a01b3c9b314be7e922919e6357c56349a2addc06dc2c043d" + "sha256": "e80d66b1f509806a3a7dc6beb02eb05ed154516999a886d8a6168cfd557ddc67" }, { "test": "reads and writes Redis values with the per-invocation serializer", "path": "test/dialcache-get-or-load.test.ts", "scope": "Asserts that the TypeScript binding reads and writes Redis values with the per-invocation serializer.", - "sha256": "48608eca552ac6d6a01b3c9b314be7e922919e6357c56349a2addc06dc2c043d" + "sha256": "e80d66b1f509806a3a7dc6beb02eb05ed154516999a886d8a6168cfd557ddc67" }, { "test": "uses captured values from each invocation and requires them in the direct key", "path": "test/dialcache-get-or-load.test.ts", "scope": "Inline operations use each invocation value and explicit key arguments; distinct captured inputs stay separated by their direct cache keys.", - "sha256": "48608eca552ac6d6a01b3c9b314be7e922919e6357c56349a2addc06dc2c043d" + "sha256": "e80d66b1f509806a3a7dc6beb02eb05ed154516999a886d8a6168cfd557ddc67" } ], "go": [ @@ -683,7 +686,7 @@ "test": "reuses a stable use case without registering it and still rejects the reserved name", "path": "test/dialcache-get-or-load.test.ts", "scope": "Asserts that the TypeScript binding reuses a stable use case without registering it and still rejects the reserved name.", - "sha256": "48608eca552ac6d6a01b3c9b314be7e922919e6357c56349a2addc06dc2c043d" + "sha256": "e80d66b1f509806a3a7dc6beb02eb05ed154516999a886d8a6168cfd557ddc67" }, { "test": "fails open when an explicit cacheKey selector throws", @@ -1609,7 +1612,7 @@ "sha256": "84b3a4b9d2e43b75df4d1565edf66245db26739f3162a56d9c62ff24a499c5c6" } ], - "adaptation": "Inspection is a public diagnostic feature not currently represented by portable event traces.", + "adaptation": "C11.process-inspection covers public snapshot semantics through the same Quint histories in both ports. Native tests retain API shape and fractional duration precision obligations; Go zero idle duration is projected to null using the observed zero leader count.", "gaps": [] }, { diff --git a/formal/fixture-recipes.json b/formal/fixture-recipes.json index e1362724..e9b1b6b3 100644 --- a/formal/fixture-recipes.json +++ b/formal/fixture-recipes.json @@ -134,6 +134,34 @@ "futureOffsets": true } }, + "dark-layers": { + "o": { + "calls": true, + "classifications": true, + "comparisons": true, + "dumps": true, + "invalidations": true, + "loaders": true, + "loads": true, + "maintenance": true, + "policyCalls": true, + "reads": true, + "recovery": true, + "shadow": true, + "sourceScopes": true, + "writeTtls": true, + "writes": true + }, + "d": { + "ages": true, + "coalesced": true, + "configErrors": true, + "fallbackErrors": true, + "warnings": true, + "futureOffsets": true, + "inspections": true + } + }, "policy-observations": { "o": { "calls": true, @@ -293,6 +321,38 @@ }, "markers": true, "compression": true + }, + "shadow-read-deadlines": { + "o": { + "calls": true, + "classifications": true, + "comparisons": true, + "dumps": true, + "invalidations": true, + "loaders": true, + "loads": true, + "maintenance": true, + "policyCalls": true, + "reads": true, + "recovery": true, + "shadow": true, + "sourceScopes": true, + "writeTtls": true, + "writes": true + }, + "d": { + "ages": true, + "coalesced": true, + "configErrors": true, + "fallbackErrors": true, + "warnings": true, + "futureOffsets": true + }, + "io": { + "aborted": true, + "budgets": true, + "sourceErrors": true + } } }, "artifacts": [ @@ -1144,6 +1204,16 @@ "id": "coalescedFailureKeepsOneLeaderAndFollowerTrailTest", "regression": "coalescedFailureKeepsOneLeaderAndFollowerTrailTest", "projection": "effects" + }, + { + "id": "frameReplyAtLastFreshMillisecondHitsTest", + "regression": "frameReplyAtLastFreshMillisecondHitsTest", + "projection": "effects" + }, + { + "id": "staleFrameReplyExpiresRefillsAndIsReadableTest", + "regression": "staleFrameReplyExpiresRefillsAndIsReadableTest", + "projection": "effects" } ] }, @@ -2717,6 +2787,260 @@ "projection": "independent-shadow" } ] + }, + { + "path": "formal/dark-layers-smoke.itf.json", + "model": "formal/dialcache-dark-layers-conformance.qnt", + "format": "smoke", + "recipes": [ + { + "id": "smoke", + "actions": [ + [ + "init", + -1 + ], + [ + "beginCall", + 0 + ], + [ + "beginCall", + 0 + ], + [ + "releaseRead", + 0 + ], + [ + "resolveLoader", + 1 + ], + [ + "releaseDump", + 0 + ], + [ + "releaseWrite", + 0 + ] + ], + "projection": "dark-layers" + } + ] + }, + { + "path": "test/fixtures/dark-layers-witnesses.json", + "model": "formal/dialcache-dark-layers-conformance.qnt", + "format": "named-list", + "recipes": [ + { + "id": "rejectedDarkSourceSeedsNoLayerTest", + "regression": "rejectedDarkSourceSeedsNoLayerTest", + "projection": "dark-layers" + }, + { + "id": "uncoalescedPublicationDoesNotPreemptTheRegisteredLeaderTest", + "regression": "uncoalescedPublicationDoesNotPreemptTheRegisteredLeaderTest", + "projection": "dark-layers" + }, + { + "id": "darkFillCarriesCapturedRetentionOnTheHeldPathTest", + "regression": "darkFillCarriesCapturedRetentionOnTheHeldPathTest", + "projection": "dark-layers" + }, + { + "id": "fillAtTheWatermarkInstantIsFencedTest", + "regression": "fillAtTheWatermarkInstantIsFencedTest", + "projection": "dark-layers" + }, + { + "id": "frameAtTheWatermarkIsFencedFromTheDarkReadTest", + "regression": "frameAtTheWatermarkIsFencedFromTheDarkReadTest", + "projection": "dark-layers" + }, + { + "id": "fillFenceIsRejudgedAfterAWallRollbackTest", + "regression": "fillFenceIsRejudgedAfterAWallRollbackTest", + "projection": "dark-layers" + }, + { + "id": "staleVisibleFrameDeclinedByTheDarkReadFillsUnfencedTest", + "regression": "staleVisibleFrameDeclinedByTheDarkReadFillsUnfencedTest", + "projection": "dark-layers" + }, + { + "id": "undecodableDarkC0IsNeverRepairedTest", + "regression": "undecodableDarkC0IsNeverRepairedTest", + "projection": "dark-layers" + }, + { + "id": "unsupportedEncodingFailsTheDarkReadTest", + "regression": "unsupportedEncodingFailsTheDarkReadTest", + "projection": "dark-layers" + }, + { + "id": "sourceDeadlineExpiresTheDarkJobAndAttributesToTheLocalLayerTest", + "regression": "sourceDeadlineExpiresTheDarkJobAndAttributesToTheLocalLayerTest", + "projection": "dark-layers" + }, + { + "id": "sameKeyCallersOnTwoInstancesKeepSeparateFlightsTest", + "regression": "sameKeyCallersOnTwoInstancesKeepSeparateFlightsTest", + "projection": "dark-layers" + }, + { + "id": "closedScopeCallerBypassesLayersAndAdmitsNoJobTest", + "regression": "closedScopeCallerBypassesLayersAndAdmitsNoJobTest", + "projection": "dark-layers" + }, + { + "id": "fullInstanceDropsAnotherKeyWhileTheC0ReadIsHeldTest", + "regression": "fullInstanceDropsAnotherKeyWhileTheC0ReadIsHeldTest", + "projection": "dark-layers" + }, + { + "id": "dumpFaultEndsTheFillWithoutReplacingTheCallerResultTest", + "regression": "dumpFaultEndsTheFillWithoutReplacingTheCallerResultTest", + "projection": "dark-layers" + }, + { + "id": "localPublicationStopsLaterDarkWorkTest", + "regression": "localPublicationStopsLaterDarkWorkTest", + "projection": "dark-layers" + }, + { + "id": "retainedC0AgeIsSampledAtTheVerdictTest", + "regression": "retainedC0AgeIsSampledAtTheVerdictTest", + "projection": "dark-layers" + }, + { + "id": "rollbackMakesTheSeededFrameFutureAndReportsItsOffsetTest", + "regression": "rollbackMakesTheSeededFrameFutureAndReportsItsOffsetTest", + "projection": "dark-layers" + }, + { + "id": "transientRequestOnlySourceFailureIsAttributedToRequestLayerTest", + "regression": "transientRequestOnlySourceFailureIsAttributedToRequestLayerTest", + "projection": "dark-layers" + }, + { + "id": "laterDarkSourceKeepsItsWholeBudgetTest", + "regression": "laterDarkSourceKeepsItsWholeBudgetTest", + "projection": "dark-layers" + }, + { + "id": "timedOutDarkReadKeepsCapacityUntilRawReleaseTest", + "regression": "timedOutDarkReadKeepsCapacityUntilRawReleaseTest", + "projection": "dark-layers" + }, + { + "id": "timedOutDarkDecodeKeepsCapacityUntilRawReleaseTest", + "regression": "timedOutDarkDecodeKeepsCapacityUntilRawReleaseTest", + "projection": "dark-layers" + }, + { + "id": "timedOutDarkConfirmationKeepsCapacityUntilRawReleaseTest", + "regression": "timedOutDarkConfirmationKeepsCapacityUntilRawReleaseTest", + "projection": "dark-layers" + }, + { + "id": "timedOutDarkDumpKeepsCapacityUntilRawReleaseTest", + "regression": "timedOutDarkDumpKeepsCapacityUntilRawReleaseTest", + "projection": "dark-layers" + }, + { + "id": "timedOutDarkWriteKeepsCapacityUntilRawReleaseTest", + "regression": "timedOutDarkWriteKeepsCapacityUntilRawReleaseTest", + "projection": "dark-layers" + }, + { + "id": "inspectionTracksKeysFollowersAndOldestLeaderTest", + "regression": "inspectionTracksKeysFollowersAndOldestLeaderTest", + "projection": "dark-layers" + }, + { + "id": "inspectionIsPerInstanceAndUsesMonotonicTimeTest", + "regression": "inspectionIsPerInstanceAndUsesMonotonicTimeTest", + "projection": "dark-layers" + }, + { + "id": "inspectionExcludesRequestOnlyAndUncoalescedWorkTest", + "regression": "inspectionExcludesRequestOnlyAndUncoalescedWorkTest", + "projection": "dark-layers" + }, + { + "id": "inspectionClearsTimedOutCallersWhileRawWorkRemainsTest", + "regression": "inspectionClearsTimedOutCallersWhileRawWorkRemainsTest", + "projection": "dark-layers" + }, + { + "id": "inspectionExcludesUnfinishedShadowAfterCallerSuccessTest", + "regression": "inspectionExcludesUnfinishedShadowAfterCallerSuccessTest", + "projection": "dark-layers" + } + ] + }, + { + "path": "test/fixtures/admission-budget-witnesses.json", + "model": "formal/dialcache-admission-conformance.qnt", + "format": "named-list", + "recipes": [ + { + "id": "laterServedShadowKeepsItsWholeBudgetTest", + "regression": "laterServedShadowKeepsItsWholeBudgetTest", + "projection": "observations" + } + ] + }, + { + "path": "formal/shadow-read-deadlines-smoke.itf.json", + "model": "formal/dialcache-shadow-read-deadlines-conformance.qnt", + "format": "smoke", + "recipes": [ + { + "id": "smoke", + "regression": "c0ReadDeadlineKeepsRawCapacityTest", + "projection": "shadow-read-deadlines" + } + ] + }, + { + "path": "test/fixtures/shadow-read-deadlines-witnesses.json", + "model": "formal/dialcache-shadow-read-deadlines-conformance.qnt", + "format": "named-list", + "recipes": [ + { + "id": "c0ReadDeadlineKeepsRawCapacityTest", + "regression": "c0ReadDeadlineKeepsRawCapacityTest", + "projection": "shadow-read-deadlines" + }, + { + "id": "c1ReadDeadlineKeepsRawCapacityTest", + "regression": "c1ReadDeadlineKeepsRawCapacityTest", + "projection": "shadow-read-deadlines" + }, + { + "id": "jobDeadlineThenReadDeadlineReportsOneVerdictTest", + "regression": "jobDeadlineThenReadDeadlineReportsOneVerdictTest", + "projection": "shadow-read-deadlines" + }, + { + "id": "confirmationKeepsCapturedReadBudgetTest", + "regression": "confirmationKeepsCapturedReadBudgetTest", + "projection": "shadow-read-deadlines" + }, + { + "id": "confirmationReadBudgetStartsAtItsOwnDispatchTest", + "regression": "confirmationReadBudgetStartsAtItsOwnDispatchTest", + "projection": "shadow-read-deadlines" + }, + { + "id": "readBeforeDeadlineCanFillAndCancelsItsTimerTest", + "regression": "readBeforeDeadlineCanFillAndCancelsItsTimerTest", + "projection": "shadow-read-deadlines" + } + ] } ] } diff --git a/formal/generated-fixtures.lock.json b/formal/generated-fixtures.lock.json index dddbdee6..77df095b 100644 --- a/formal/generated-fixtures.lock.json +++ b/formal/generated-fixtures.lock.json @@ -6,54 +6,59 @@ "seed": "0xd1a1ca" }, "inputs": { - "formal/cache-rules.qnt": "cc37125b108b6e51011cfa4ff5c0561bb9498cbedd3ffefb0cabd79f3975216c", + "formal/cache-contract.qnt": "fabf5cf46d24d62cee8cbd88ff7233a965b4d038b622b18b92760a9cbd387a35", + "formal/cache-rules.qnt": "ae2a1748e911402c6818e8294265764e06d18d050a89219df7d55faa83fb06e5", "formal/cohort-boundaries.qnt": "700f0b9ee734f2511b3c7f10138b9636cef93bc1045b653479a05be1766e3407", "formal/compact-json.mjs": "fe180d42ea438f915a011accf79728701fd51222877d39adbda8ea3ebf3f4bfb", "formal/conformance-observations.qnt": "76e26fb0c5802a13c63f0819071b5326b1586cab187c73d8b204ec7da70f5c2f", - "formal/dialcache-admission-conformance.qnt": "504da4f58b8f83e8aa0c642e63f4e5dace00c4a30887339787182572c47a045a", + "formal/dialcache-admission-conformance.qnt": "6d2559203dfc49d014c4cc6f426f9bcb8851d20e975a4fc63da5946494815e24", "formal/dialcache-conformance.qnt": "3e91cad0afa6ba44353d651078fdf6284df0354ca82de0e3712dfe4f243244d7", - "formal/dialcache-effects-conformance.qnt": "5d3beba2da8b00c3aa44435d12093da0c91114d82df77cbc6265180524bb61bb", - "formal/dialcache-independent-conformance.qnt": "3533ec3bce4ef1c713c034130654b5cd7eb824d3371b58fe796e31e5b70cdf75", - "formal/dialcache-layers-conformance.qnt": "641b28d78a7480cf996a3208c9f06aeed5a1a6f8f1e4d42b8b6316f1dd4a1c4d", - "formal/dialcache-local-clock-conformance.qnt": "be5ff65ab487d716eb4e4f35f6e85a42edb129da616525024711334fc3256754", - "formal/dialcache-local-failure-conformance.qnt": "02ecbb32957b4c51de7d6e2b4701ddd6f0088cd475f3cb428f463019deaec47e", - "formal/dialcache-policy-conformance.qnt": "0d137e68206188abef3657ddfd05cd9333e6daf0dd92a5078ac682af1c06b027", - "formal/dialcache-recovery-conformance.qnt": "eb00ba4dfe335614b05d2b2697de2cf00a6f7f6904b49e8631ee4fe12e0a9e57", - "formal/dialcache-recovery-read-conformance.qnt": "a56873dbc4be99b9209f2b6895fb228ff09323c4e5bdfcc93e461374213a1a2d", - "formal/dialcache-runtime-boundaries-conformance.qnt": "80b33bf9efb34998e30b59df5872a797709cdb4842e47c59b8470e656c3b3d07", + "formal/dialcache-dark-layers-conformance.qnt": "48eabb9a055a364249b4aa4404642732e022d6d8b67ab35f7c4b82c3412b9ea7", + "formal/dialcache-effects-conformance.qnt": "e0f635a8257355017f8298d258b2b361bc162797e2eb0a38127c95ddb9508900", + "formal/dialcache-independent-conformance.qnt": "a73b277b380f40ef8d57fc0cc88ca2d0a303c688ed3b46e338780a0a2b5303d2", + "formal/dialcache-layers-conformance.qnt": "aff6324feb9908ca21cb49361f46012ac2683a521b1b85dbd11f2cf5ce8fff0d", + "formal/dialcache-local-clock-conformance.qnt": "6a882b2f7d7ff6e86c736bb6944f2ee99da0d5fce2843db4a651728c78aeac95", + "formal/dialcache-local-failure-conformance.qnt": "7265971af30386195a60ed76dd7bd5a3b71501b81cf7d4a183222f9d52cebee6", + "formal/dialcache-policy-conformance.qnt": "762450d1447b9be70fe1abff3594c86445f5f66da44285b7275d76cb64d1f02b", + "formal/dialcache-recovery-conformance.qnt": "608b867914fde3cf62b93d52e476b7ffb1c740627123b875419785e521a3e568", + "formal/dialcache-recovery-read-conformance.qnt": "8b563cd582160695f4aab095c3f79d21ae69ff80d615c506f2e0a9716fe624b0", + "formal/dialcache-runtime-boundaries-conformance.qnt": "0b6a09341290626571e826871a9f962ce7c087291a8c8ba3534a73d0c5a5fa32", "formal/dialcache-scope-conformance.qnt": "7cde548402db5767ad3175baa9266f0c318b68c4cda4eb5b3f7727c350c24e1b", - "formal/dialcache-shadow-conformance.qnt": "46133911278ddef4d6957ed3c43d4c6f33d0bb91adae334ed19fe9def5c3f5a7", - "formal/dialcache-shadow-layers-conformance.qnt": "132de6c45260f368cd42dba12064690e3821c04724024bfb90b3fa494097e6f2", + "formal/dialcache-shadow-conformance.qnt": "b4806f313929597cafd64cd8ef8f9a7c2f38c63d27481da7e36702063f82a7c7", + "formal/dialcache-shadow-layers-conformance.qnt": "76a1219307e707b713acc20998c0d9852fa64f782bf18c7c2c634eab74b1e5c9", + "formal/dialcache-shadow-read-deadlines-conformance.qnt": "b43373789a9cbfd907b50c5ebb49dec738e3621eab8132d16a4101634ed0d9f2", "formal/dialcache-source-budgets-conformance.qnt": "fb9707785725add09358fb8c0c7cb764f86cd42e659ed1f4cc459fd5afe93574", - "formal/fixture-recipes.json": "a7b59202b9466c2f63dae527143c1aec34fda8b174e7e99bea5f58717620f741", + "formal/fixture-recipes.json": "594d29610c37333f816846a3d8afebc0f8c0ae2726e343e2809db7d0dc57ec6e", "formal/generated-fixtures.mjs": "f73a527cbbc0178be167c11ff8ff21985f05976379ae4054d5f61caa889e34c2", "formal/kernel/adapter-replies.qnt": "490e1d65552a174f728aae973f92525ff616b0cf26253873c9fe1804aef7da7d", - "formal/kernel/calls.qnt": "856c2d7249ea053aaf3a18bb5d235350b5d35e1100f4b4421efa20aceba9767f", - "formal/kernel/clock.qnt": "bb46798dd221141a0a0a3f8a0dc9fccf998a68fd9e24be54f12c43b6cad0845a", - "formal/kernel/compression.qnt": "d31ae67c1f33c3f32d9fb5db65a9db48c65ea9119184ec88fe8a35680506158b", + "formal/kernel/calls.qnt": "1fd63856061a9fa9c419b88a736b0d07bce9702eab8b347c936eaaf11f7b4eb3", + "formal/kernel/clock.qnt": "aca2651c3efd52c974de61475266377a8154c7c6911b83dff66254e83f3f39b1", + "formal/kernel/compression.qnt": "955b9d662d587cad5f2ca2e9c3940fa06eb258acbe9cb6ad2bed2b41b35f8b27", "formal/kernel/config-errors.qnt": "5f69ec3e8414360460ec2a3df61cfedbb3217277f31acb48ca005f8a8332a664", - "formal/kernel/deadlines.qnt": "155211176f92930fb3f7855216e55bd583a6378934b04219a15e7af5ab86c5b7", - "formal/kernel/diagnostics.qnt": "8a1f1571d59384f9b09caa07a5b08cf1a84abbd26337299f29d97c071d1023d9", + "formal/kernel/deadlines.qnt": "f4bc6e378403ce27608641f45bc1e91779f6b156209bc5aaacf3861f663d163e", + "formal/kernel/diagnostics.qnt": "04e17706e835f02b15969138ede1a5d671a402cbc188da46426ae04c7c40451c", "formal/kernel/encodings.qnt": "8d8e0ae81ac8560f8f2376a6208a50f8ca33251158ca9c3bbaaf769e18c5b0c7", - "formal/kernel/flights.qnt": "98dbef3fd07c0496330c63c1f6d632de63fd77f5627e25f6d7a18f4aaef06fc4", + "formal/kernel/flights.qnt": "427c9189d653db325a7a01fa776a5fe1edd4f85f01dd3a05600cd35bd5937e20", "formal/kernel/instances.qnt": "addff9c66e28d7ee057c5f73bae5baab67344e53769f33825e45cf6e9c68e7f7", - "formal/kernel/layer-policy.qnt": "e81760252b0fefd5f0ee021333b962c4d4a6cc3801b2bec07f0d3cc00c7a9f62", - "formal/kernel/local-faults.qnt": "9df433694ed10dc0d2a60b108f3d659bf5c2d0df6f5e76e833e425972e0e5843", + "formal/kernel/layer-policy.qnt": "e011331f560a122cb6f12c84d4eb1b781a1ecd6d4f5c9e8b32929560297a7495", + "formal/kernel/local-faults.qnt": "efd83a12a75ec4e9ace5fbbb7b3a4b2eb4686a70526bc828a27090a7373e0d83", + "formal/kernel/local-grid.qnt": "42a37e7fc407335025a7992d20bbe8f30a4a0b7528f25bde2909a80ba797fcfd", "formal/kernel/local-storage.qnt": "7a1880954c1d1fad43039e04e3e051ab8788d2ee508667c9ed217a6361ef39a5", - "formal/kernel/markers.qnt": "cd2401b187f981326468723a83f0d78413d5509408690520edb0dc1e6928f927", - "formal/kernel/metrics.qnt": "92d00b2351c0a3659fe4f0c2eedc77b456ef60ef400bb6079ba8f6a8e7f3506e", - "formal/kernel/payloads.qnt": "8a471a4d467a2c96234044cbfe23ff0cb7db8f2ddde11d17eccb0f077137e7db", + "formal/kernel/markers.qnt": "1fede016de58fdf1643cd41b8138cf04ede40b3be5422a8136c52c10775796aa", + "formal/kernel/metrics.qnt": "487d51f0b6b10a50bdfc29f6fa86042e4e919946b437c17905a6673e459e0321", + "formal/kernel/payloads.qnt": "5d26d8b854e7e2ae57092287a307451d42a08473d99527ef45bbfb5ff6afabd1", "formal/kernel/policy-gate.qnt": "89fc6675d2d7d0b64c912f85b5066a13884e37e0fc5a1df44551552461476313", - "formal/kernel/policy-overlay.qnt": "67c21286676aa255034a2c8160a34d228bcfa8aaa8d7ab48c1f7eee461a0a0c5", + "formal/kernel/policy-overlay.qnt": "cf97e318757ce1c4c3de201ed081ca7540d8c8d731e5b36c399d2001097bed62", "formal/kernel/receipts.qnt": "d73f8ce5293f4b782d3bf12c0ee68c94068d256a931ed415e33f862ef88eec07", - "formal/kernel/recovery.qnt": "af804a4ec6f8766c5931cb05bc173d9be7687e23c009e91d07de713ebbeee8bf", - "formal/kernel/remote-frames.qnt": "30e20de689bc9e6b10cb59e2084f6f4d302c6237a6fb09ad51a92b9338ac5850", + "formal/kernel/recovery.qnt": "a91930754fff119acd3b3b1c4f35cfc7e6590a39316424d1bf1a79f2768534d9", + "formal/kernel/remote-frames.qnt": "3068136536d0d466943eec06e7762b7476a999972d35a41a439df8317f1b10d4", "formal/kernel/remote-io.qnt": "c95c66357d9c8b4dd7208bbcb9f3077d2d46a1376e28b01a08c0d990c73c44ee", "formal/kernel/remote-writes.qnt": "4c4433b90c6bf0fced73074715c55c423218084e22596764a58b65e31c0bb8f3", "formal/kernel/request-memo.qnt": "e7897bfe9949a4d31b8138b35c32a7df0939e81e1f9c13739340a794a3d5073f", "formal/kernel/runtime-policy.qnt": "41ebb180f8b8b561d0db43b237abbee04e69613521436f6c624241cfece536ae", - "formal/kernel/serving.qnt": "4d338ac2bdf1fec67947d510219ccd2ca3907387b2ac04a76e399266a7a5588c", - "formal/kernel/shadow.qnt": "6f1557580943c69ce4356b7c99ccde45067ec8dccb584102279775cf0c2e9a19" + "formal/kernel/serving.qnt": "ce54bfff08e663d94bf9a43697a7070b879a6d9ed57aefa5d00a1135dd8d2954", + "formal/kernel/shadow-reads.qnt": "f07f05499c736301eabe1907e860dd68814446de85d97ca87cec9a8c0f037efd", + "formal/kernel/shadow.qnt": "90f656ec0537186df163cc09049340077f01d0956d43f99f058c8972b1eeffb5" }, "artifacts": { "formal/conformance-smoke.itf.json": "be38ab847b7a5d4c15914bd6bd2fd2fd72cb2fc9af7edb15ba209c53f2ccfa37", @@ -72,7 +77,7 @@ "formal/shadow-layers-smoke.itf.json": "c306392e52a2c2770c182eda48a9d475c94944d3bb2069b813a324a82189952b", "formal/local-clock-smoke.itf.json": "f5bafd7b9975404dcde752754a010ae63afacf12f9e1602cda47b34ef43e976c", "formal/source-budgets-smoke.itf.json": "b013caea2b7044c92fcff17ff32e8628d5352b3aa15f195ad0722b6134c7fef7", - "test/fixtures/effects-authority-witnesses.json": "3bdaab7457460f276a8f303763f193571316f547af3a768ae9f001f5695c12ff", + "test/fixtures/effects-authority-witnesses.json": "c920405268ee77a0ea32ba979fd4b2de1f781facdaf87983abb514efb9f47750", "test/fixtures/formal-runtime-boundary-witnesses.json": "206114f8df2e9dd4a44430eec3bb406a5f187e2370c78dc3d4875e1a9439d450", "test/fixtures/formal-runtime-witness-boundaries.json": "5cc67bcf29c72392fd8c0582131e28f77a340d872d5e72026868456f16287f99", "test/fixtures/formal-witness-attribution.json": "a226222715a111da40ad7a83c311658de617ef1d10a95c91eba6d33a03980ef9", @@ -86,6 +91,11 @@ "test/fixtures/policy-witnesses.json": "96a4ca1c17f90f5185b9119bc0900014fca15f41dff34fac6b58bcba277ce6c4", "test/fixtures/layers-witnesses.json": "2b3d3e70da715b103b3fcbca6754a251b7e1091b872295ab42df15e20dab0962", "test/fixtures/independent-witnesses.json": "d89a21165ac3b401343fd8bebf0f34eaa629f3fdf260ddfe4a3f26b53bac99c9", - "test/fixtures/independent-shadow-witnesses.json": "c28fc3adb21499dc2fb5f3d2ba431d64d46035eb833bf3f548b19a4a8a72356a" + "test/fixtures/independent-shadow-witnesses.json": "c28fc3adb21499dc2fb5f3d2ba431d64d46035eb833bf3f548b19a4a8a72356a", + "formal/dark-layers-smoke.itf.json": "c205714a4169b1d8ff707787f69a89e65433ca0c1d5f8b7d4b03c81c39dc5d40", + "test/fixtures/dark-layers-witnesses.json": "f0855922f055fcae546f0588d6c77eb3fd5082e2ef5c7b4888ccebd36ac95eb1", + "test/fixtures/admission-budget-witnesses.json": "3698e2f6a48575c88687f12794115a8724a3f3a813e0ea7cfe38a8f6b87ae87c", + "formal/shadow-read-deadlines-smoke.itf.json": "1bb6a5937ebeb20beb8155349029143053adb87a35c4a50a82a43a45ac38fb22", + "test/fixtures/shadow-read-deadlines-witnesses.json": "74306eaf69f7b03f7f89966263d3a8f81c7a70d98908b934b5cb577e0b0a7f7b" } } diff --git a/formal/go-parity.json b/formal/go-parity.json index 1ba54403..7645cd7a 100644 --- a/formal/go-parity.json +++ b/formal/go-parity.json @@ -8,32 +8,32 @@ "counts": "Inventory counts establish accounting completeness only; they are not semantic coverage percentages or proof of parity." }, "inputs": { - "semanticCasesSha256": "5bfe68e355c8334ad2f6e17b973618ff2c8c2a13b19c391940945e9d3b99643d", - "executionSha256": "0e4cf73d4db0cde21783c1d7b8ca515893ff55233a9e895ec626449c4cd8608e", - "sourceAuditSha256": "fded3982e16d256ab526bd7f17f93b04206b21291b539494301da1e4abf0b93b", - "featureCoverageSha256": "d238aee55d22387616cc05a6f55a14af227daa00b416dec7378a97b47ec093f9", - "coverageWitnessesSha256": "7af7916b26c4cf11bddc546fc0f72688de6a3a1e726245eeea2661fdc9c3fcc0", - "profilesSha256": "5e28e00d4927c8908938fe4c8a279b1804963138b5d944bc944e4c16d74c6146" + "semanticCasesSha256": "faa768fbf06eddc03dc4f26570094b1df066fb075f898043b40c6ce9d8c952a1", + "executionSha256": "3df06c53eb69f02fb6eafabd51c6cc7a306c6739e04ab3fa521b53389da559f2", + "sourceAuditSha256": "2b749b2b0a17cc7134108fe939fdb7d7792c19883c591379af4b66f2135cd699", + "featureCoverageSha256": "91723cc5398c8fff102d2beb199ebcab82af137e11f1cb9771b8d16d0304a95c", + "coverageWitnessesSha256": "2b2d5d57daacba3ecd3637f5a917d58b0b63fcc88893e651cd602cdba1ef4244", + "profilesSha256": "f8ec93fe5aa23faa69edd0a632a645874a6190a48ed613d2c65f82045219f49c" }, "inventory": { - "behavioralCases": 240, - "withRequiredGeneratedWitnesses": 240, + "behavioralCases": 244, + "withRequiredGeneratedWitnesses": 244, "withoutRequiredGeneratedWitnesses": [], "wireCases": 22, "sourceFiles": 27, "sourceDeclarations": 772, - "casesWithScheduledChecks": 262, - "requiredWitnesses": 440, - "casesWithQuintRegressionReplay": 174, + "casesWithScheduledChecks": 266, + "requiredWitnesses": 497, + "casesWithQuintRegressionReplay": 192, "casesWithQuintGeneratedVectors": 22, "withoutModelDrivenReplay": [], - "quintModels": 30, - "scheduledInvariants": 213, - "scheduledRegressions": 413, + "quintModels": 32, + "scheduledInvariants": 253, + "scheduledRegressions": 468, "quintGeneratedProtocolVectors": 1343, "quintGeneratedInvalidationVectors": 288, - "caseReferencedWitnesses": 306, - "exportedRegressionHistories": 310 + "caseReferencedWitnesses": 343, + "exportedRegressionHistories": 358 }, "cases": [ { @@ -432,6 +432,18 @@ "scope": "Only the linked finite scenarios, witnesses, scoped checks and explicit native boundaries; not universal case proof." } }, + { + "id": "C11.process-inspection", + "kind": "portable-behavior", + "group": "coalescing", + "gaps": [], + "evidence": { + "typescript": ".formal-traces/ts-replay.json", + "go": ".formal-traces/go-replay.jsonl", + "nativeGo": [], + "scope": "Only the linked finite scenarios, witnesses, scoped checks and explicit native boundaries; not universal case proof." + } + }, { "id": "C11.distinct-keys", "kind": "portable-behavior", @@ -2862,6 +2874,18 @@ "kind": "portable-behavior", "gaps": [] }, + { + "id": "C22.adapter-frame-expiry", + "kind": "portable-behavior", + "group": "frame-age-and-retention", + "gaps": [], + "evidence": { + "typescript": ".formal-traces/ts-replay.json", + "go": ".formal-traces/go-replay.jsonl", + "nativeGo": [], + "scope": "The same exported adapter frame histories establish last-fresh hit and exact-expiry miss, source refill, later reuse and expired diagnostic in both ports." + } + }, { "id": "C22.freshness-after-read", "group": "policy", @@ -3199,6 +3223,30 @@ "id": "C09.fractional-clock-grid", "kind": "portable-behavior", "gaps": [] + }, + { + "id": "C23.later-dark-budget-origins", + "group": "deadlines", + "kind": "portable-behavior", + "gaps": [], + "evidence": { + "typescript": ".formal-traces/ts-replay.json", + "go": ".formal-traces/go-replay.jsonl", + "nativeGo": [], + "scope": "An immediately resolved later dark source publishes locally and returns to both caller scopes while its independent dark read remains held; both native ports replay the exact generated schedule." + } + }, + { + "id": "C54.later-served-job-budget-origin", + "group": "shadow", + "kind": "portable-behavior", + "gaps": [], + "evidence": { + "typescript": ".formal-traces/ts-replay.json", + "go": ".formal-traces/go-replay.jsonl", + "nativeGo": [], + "scope": "A later served-hit shadow job rejects within its own ten-millisecond budget and reports source_error, preserving the acquired caller value; both native ports replay the exact generated schedule." + } } ], "sourceDeclarationScope": { @@ -5057,7 +5105,7 @@ { "id": "X01", "boundary": "Optional observability integration", - "existingTreatment": "Backend metric names, histograms, exporter registry compatibility, coalescing-state inspection, bounded JSON logging, and telemetry timing precision are covered by implementation/integration tests. Current portable claims include C57–C60 diagnostic semantics and failure isolation; exact exporter schemas, buckets, registry compatibility, timing precision, and logging resource limits remain separate.", + "existingTreatment": "Backend metric names, histograms, exporter registry compatibility, coalescing-state inspection, bounded JSON logging, and telemetry timing precision are covered by implementation/integration tests. Current portable claims include C57–C60 diagnostic semantics and failure isolation plus C11 process-coalescing snapshot semantics; exact exporter schemas, buckets, registry compatibility, timing precision, and logging resource limits remain separate.", "goStatus": "explicit-native-binding-adaptation", "goTreatment": "Portable diagnostics require shared generated histories. Native metric names, labels, buckets, units, counts and bounded logging have focused Go tests. NewPrometheusMetricsWithBindings reuses caller-owned individually registered CounterVec/HistogramVec objects after checking declared construction schema, public descriptors and registry identity; new collectors register atomically. Because client_golang exposes no bucket configuration for an empty histogram, accurate caller-supplied schema is an explicit binding precondition; construction must not race with collector registration/reconfiguration. No private-field inspection or temporary series is used.", "nativeTests": [ @@ -5092,7 +5140,7 @@ "formal/cache-rules.qnt" ] }, - "artifactSha256": "8483d4849166c95aa5cd77433214402cd68f7a2226e0fb7eee5cafeb88e99204", + "artifactSha256": "ccc462f8093da0d72b6db3f25a10945a3130bace6dffcac65e58ea9eef870064", "status": "executable-vector-schedule" }, { @@ -5124,7 +5172,7 @@ "formal/cohort-boundaries.qnt" ] }, - "artifactSha256": "ec498a23074896062af5b454d56c6c76682cb8498c1aa0149f8802f62a97fa00", + "artifactSha256": "e34a95a7f6a475fa542abf5c80e1626a712f6010e510c31e0bc5559ff1902d51", "status": "executable-vector-schedule" }, { @@ -5140,7 +5188,7 @@ "formal/generate-envelope-vectors.mjs" ] }, - "artifactSha256": "8bf30df4ad878464956aa73a948e3a263c29be7aed5412662f2c22f4ddc866b2", + "artifactSha256": "b154c5d764215a60413eda4248891fa58e17221b784346eaa304872be45de284", "status": "executable-vector-schedule" } ] diff --git a/formal/kernel/README.md b/formal/kernel/README.md index 324b1b67..27b45cb9 100644 --- a/formal/kernel/README.md +++ b/formal/kernel/README.md @@ -18,20 +18,21 @@ design and its history; this file describes what is here and how to use it. | `runtime_policy` | How a runtime policy reply (the runtime-boundaries drivers' codes 0 to 21) resolves against an instance's configured baseline: serving cohorts, omitted, null and invalid leaves, runtime TTLs, the kill switch | `resolve` | | `request_memo` | Request-scoped memo rows and their closure; `openScope` and `Opened` are environment bookkeeping (which contexts an input has created) that no memo rule reads, kept beside the memo rows because the composition lint has no environment allowance yet | `scopeOpen`, `memoSlot`, `memoValue`, `memoize`, `openScope`, `closeScope` | | `local_storage` | Per-instance local storage with LRU eviction, insertion expiry (`cache_rules.localEntryLiveAt`) and a hit that renews recency, not insertion | `localValue`, `promote`, `putLocal` | -| `payloads` | Payload validity classes: the code a remote frame stores. The drivers' value codes below 100 decode to themselves; an enveloped band from 100 up keeps class and value apart, a class hundred (`COMPRESSED`, `CORRUPT`, `UNSUPPORTED_ENCODING`, and the two spellings that decode like a plain value but carry other bytes, `PADDED` and `UNICODE`, the shadow drivers' payloads) plus the value the bytes carry (`classOf`, `valueOf`; a profile spells a seed as `Payloads::COMPRESSED + VALUE_ONE`, arithmetic over constants; the form line `BINARY`: a payload at or above it is the binary spelling of the text payload below it, the same bytes, `bytesOf`, which every class judgment reads and the shadow confirmation compares across the two forms, C51; a compressed or corrupt code reaches a decode step or candidacy only with a positive value, the test `remote_frames::observe` and `recovery::retain` apply before any decode, so a bare class code is routed as a declined visible frame, and `UNSUPPORTED_ENCODING` is spelled bare because it names no value), and says what a frame's bytes do by their class alone, judged once in `remote_frames::observe` for the atomic release and the held read settlement, at the held decode (`remote_io::settleLoad`) and at candidate retention (`recovery::retain`). A code that fails the read never reaches a serve site; one that fails the decode reaches it only through the held decode step, which fails it; every serve site judges the decoded value, never the code (`serving::decide` and `remote_io::settleRead` serve a positive `valueOf` only, `recovery::retain` admits a candidate only when its code decodes to a value), so a code whose `valueOf` is `NO_VALUE`, the integer `CALL_PENDING` shares, never completes a caller. Documented limit: the atomic release has no decode step and serves `valueOf` of a fresh corrupt frame, the value its bytes would have carried, so a profile over the atomic path seeds no decode-fault payload (pinned by the payload-classes fixture) | `bytesOf`, `classOf`, `valueOf`, `readFails`, `decodeFails`, `decodeOutcome` | +| `local_grid` | Fractional local-clock traversal with raw insertion ticks beside the retained expiry. It derives the duration represented by that expiry, then judges lookup through `cache_rules.localEntryLiveOnGrid`; this keeps both precise-clock errors and hit-renewed expiry consequential. Admission, traversal, inline source settlement and publication use the same serving lifecycle. | `call` | +| `payloads` | Payload validity classes: the code a remote frame stores. The drivers' value codes below 100 decode to themselves; an enveloped band from 100 up keeps class and value apart, a class hundred (`COMPRESSED`, `CORRUPT`, `UNSUPPORTED_ENCODING`, and the two spellings that decode like a plain value but carry other bytes, `PADDED` and `UNICODE`, the shadow drivers' payloads) plus the value the bytes carry (`classOf`, `valueOf`; a profile spells a seed as `Payloads::COMPRESSED + VALUE_ONE`, arithmetic over constants; the form line `BINARY`: a payload at or above it is the binary spelling of the text payload below it, the same bytes, `bytesOf`, which every class judgment reads and the shadow confirmation compares across the two forms, C51; a compressed or corrupt code reaches a decode step or candidacy only with a positive value, the test `remote_frames::observe` and `recovery::retain` apply before any decode, so a bare class code is routed as a declined visible frame, and `UNSUPPORTED_ENCODING` is spelled bare because it names no value), and says what a frame's bytes do by their class alone, judged once in `remote_frames::observe` for the atomic release and the held read settlement, at the held decode (`remote_io::settleLoad`) and at candidate retention (`recovery::retain`). A code that fails the read never reaches a serve site; one that fails the decode reaches it only through the held decode step, which fails it; every serve site judges the decoded value, never the code (`serving::decide` and `remote_io::settleRead` serve a positive `valueOf` only, `recovery::retain` admits a candidate only when its code decodes to a value), so a code whose `valueOf` is `NO_VALUE`, the integer `CALL_PENDING` shares, never completes a caller. Atomic release and atomic shadow comparison have no decode-failure settlement; every atomic profile schedules `atomicPathSeedsDecodableFrames = Payloads::atomicSafe(s)` to refuse corrupt payloads (the underlying limit is pinned by the payload-classes fixture) | `bytesOf`, `classOf`, `valueOf`, `readFails`, `decodeFails`, `atomicSafe`, `decodeOutcome` | | `remote_frames` | Remote frames stamped on the wall clock and retained until an instant on the monotonic clock (`cache_rules.deadlinePendingAt`), `visible` while present, retained and (tracked) fence-cleared whatever their age, served while also fresh for the reply's freshness (`cache_rules.freshAgeAllowed`, which rejects a stamp from after a wall rollback); per-entity watermarks and fences on the wall clock (`cache_rules.fenceAllows`); an invalidation never lowers a watermark: it raises it to the wall clock plus the invalidator's future buffer only when that is ahead (`raiseWatermark`, the public `futureBufferMs`; 0 where the drivers pass none), and only a marker's expiry (`markers`) lowers one, to the zero baseline (`clearWatermark`); what one read observes, judged once for the atomic release and the held read settlement (`observe`: whether the read `failed`, the drivers' fault or a visible frame whose payload code fails the read, `payloads::readFails`; a failed read observes nothing; else whether a frame is visible, its `payload` code and the value a fresh one serves, what the code decodes to, `payloads::valueOf`); the tracked one-hour cap on the retention a refill is written with (`TRACKED_RETENTION_CAP_MS`, `physicalRetention`: an untracked write keeps its full retention, and the recovery snapshot's logical maximum is never capped); a refill's fence is the watermark a read observed when it found no visible frame (`fenceFor`, C58: a visible frame the traversal declined carries none; on the atomic path this rule is pinned by the frame-clocks fixture runs `visibleStaleMissCarriesNoFenceTest` and `frameAtTheWatermarkKeepsTheMissFenceTest` and by the TypeScript reading, where `src/internal/redis-cache.ts` returns the observed watermark only for an adapter-side miss and `src/dialcache.ts` forwards it to the refill only for a miss status, not by the composed differentials: the one composed corpus that declines a visible frame, policy, is untracked, where the old and new rules both yield no fence, and the tracked corpus, layers, retains frames exactly as long as they are fresh, so it never declines one); a write with an explicit stamp (`storeFrame`), with the wall clock (`seedFrame`) or at an age (`seedAged`) | `storeFrame`, `seedFrame`, `seedAged`, `raiseWatermark`, `clearWatermark`, `visible`, `fenceCleared`, `observe`, `missFence`, `fenceFor`, `writeAllowed`, `retained`, `fresh`, `physicalRetention` | -| `flights` | Source executions (a record of outcome and process sharing, with whatever payload the traversal that started it needs), the process and request registries that coalesce callers, and per caller its owner and memo slot; settling a source is recording its outcome (`recordResult`) and forgetting it in the registries (`forgetSource`), apart so a flight that first recovers a retained value stays joinable until it completes; a detached source (`registerDetached`, a served shadow job's: appended under the next index and registered nowhere, so `ownedBy` is empty for it); the newest source (`latest`, for a profile whose drivers settle the latest loader rather than one named by index, and for an inline source settled by the call that started it); an opt-in record of the identity each caller asked for | `processOwner`, `requestOwner`, `admitCaller`, `attachCaller`, `joinRequestFlight`, `registerSource`, `registerDetached`, `recordResult`, `forgetSource`, `forgetScope`, `ownedBy`, `latest`, `recordIdentity` | +| `flights` | Source executions (a record of outcome and process sharing, with whatever payload the traversal that started it needs), the process and request registries that coalesce callers, and per caller its owner and memo slot; settling a source is recording its outcome (`recordResult`) and forgetting it in the registries (`forgetSource`), apart so a flight that first recovers a retained value stays joinable until it completes; a detached source (`registerDetached`, a served shadow job's: appended under the next index and registered nowhere, so `ownedBy` is empty for it); the newest source (`latest`, for a profile whose drivers settle the latest loader rather than one named by index, and for an inline source settled by the call that started it); an opt-in record of the identity each caller asked for; public process-coalescing snapshots over registered owners, process-only follower receipts and monotonic registration age | `processOwner`, `requestOwner`, `admitCaller`, `attachCaller`, `joinRequestFlight`, `registerSource`, `registerDetached`, `recordResult`, `forgetSource`, `forgetScope`, `ownedBy`, `latest`, `recordIdentity`, `recordInspectionAdmission`, `inspectProcess` | | `clock` | Elapsed time on the monotonic clock; the wall clock is that clock plus a skew (`wallOf`), so one transition moves both and only the skew shifts on a rollback; the environment's fractional time beside the clock the layers read (`Ticked`: the drivers' ticks, with the monotonic clock as that time on the whole-millisecond grid, `cache_rules.wholeMs`, so a fractional advance moves `now` only across a millisecond boundary and local insertion and expiry fall on the grid whatever an instance's construction phase, C09) | `advance`, `advanceTicks`, `wallOf`, `shiftWall` | | `policy_gate` | Callers whose policy reply the environment holds, with their calls, indexed by their policy call | `hold`, `holding`, `holds`, `latest`, `entry`, `release` | -| `serving` | Admission, traversal order (`decide`), ownership precedence, publication and refill authority with the TTLs each source captured from the reply that started it, the remote adapter's read, dump and write faults along a refill, scope closure, maintenance (`invalidate`, the outcome rule of the write-fault switch: `missing_remote` without remote storage, `mutation_error` under a write fault with the invalidation counted and the watermark unmoved, `ok` raising the watermark to the wall clock plus the invalidator's future buffer, `bufferMs`, 0 where the drivers pass none; `maintains`, whether an invalidation takes effect, the judgment the effective arm branches on and a marker lifetime reads); the layered shape and its local and request-only projections; the layered release judged once (`judgeRelease`, with the remote observation as a parameter; `layeredRelease` observes it; the judgment carries the observation, `remote_frames::Observed`, which `startSource` passes to the authority so a failed read, an adapter fault or a payload that fails the read alike, denies the refill on the atomic path as on the held one, and a source started without the remote layer passes `remote_frames::UNOBSERVED`; `judgeReleaseUnder` takes the local read's fault as an argument, `local_faults`: the local value hidden, the layers and the sharing as the reply resolved them, and the judgment carries the fault so `releaseJudged` publishes for the TTLs it allows, `publishable`: no local TTL for the remote hit it serves or the source it starts) for the transition and the records composed around it, and the steps a held remote effect suspends between, which the atomic path composes too: the release prologue up to the remote layer (`applyRelease`), publication authority fixed when a remote read completes (`authority`, capturing the retention the refill is written with, `remote_frames::physicalRetention`, so the source record and the write agree), warm-on-hit (`warm`), completion apart from the recorded outcome (`complete`, `finish`) and publication apart from completion (`refillAllowed`, whether a captured authority may be written now, the judgment a caller's refill, `refills`, and a shadow job's fill, `shadow::fill`, share; `dispatchWrite`, `publish`, `settle`); the judgment applied apart from the judging (`releaseJudged`, the release after `layeredRelease`, so a variant that records beside the release judges once and applies the same judgment) and a detached source (`startDetachedSource`: a source no caller owns, counted against the profile, probed as not participating, without publication authority, whose settlement completes no caller); whether the drivers install a policy provider is a layout fact (`Layout.policyProvider`: `entered` counts a policy call per participating admission only where one exists; an instance that resolves policy from its key's default configuration, the local-clock drivers' default instances, makes none); the local projection's immediate reply (`beginLocal`) and its inline call (`callLocal`: the loader returns before the call does, so the source a miss starts settles at once with the loader's value and a hit settles nothing) | `admit`, `release`, `judgeReleaseUnder`, `releaseJudged`, `startDetachedSource`, `begin`, `settle`, `publish`, `complete`, `finish`, `admitLocal`, `releaseLocal`, `settleLocal`, `beginLocal`, `callLocal`, `admitRequest`, `releaseRequest`, `settleRequest`, `closeScope`, `invalidate`, `maintains`, `refillAllowed` | -| `local_faults` | The local storage fault switch (C27) over the layered shape (`Faulted`: `serving::Served` with `localFailed`), read at the two points a call touches local storage and passed as an argument, the kernel's fault idiom, never turned into a TTL the layer gating reads: an admission with an immediate reply (`begin`) judges the layered release with the fault as its argument (`serving::judgeReleaseUnder`: the local value hidden, the layers and with them the coalescing untouched, nothing warmed from the release) and applies that judgment (`serving::releaseJudged`), so the traversal reads nothing from local storage, a remote hit warms nothing, the source it starts captures no local TTL (publication-ineligible whatever the switch says at settlement) and two callers under the fault share one source as two healthy callers do, as the implementations decide the process flight from the resolved layer configuration before the read and a read that throws disables only that call's local value and write (`src/dialcache.ts` `getThroughSharedLayers`, `getThroughActiveLocal`); a settlement (`settle`) withdraws the source's captured local TTL, so `serving::publish` warms nothing while the callers complete with the accepted result, the request memo is written and the remote refill proceeds; the traversal and the publication keep their one statement in `serving`, and the remote fault switches stay in `serving::Served` because the traversal reads them mid-release where this one is read at its two entry points; one consumer today, local-failure | `begin`, `settle` | +| `serving` | Admission, traversal order (`decide`), ownership precedence, publication and refill authority with the TTLs each source captured from the reply that started it, the remote adapter's read, dump and write faults along a refill, scope closure, maintenance (`invalidate`, the outcome rule of the write-fault switch: `missing_remote` without remote storage, `mutation_error` under a write fault with the invalidation counted and the watermark unmoved, `ok` raising the watermark to the wall clock plus the invalidator's future buffer, `bufferMs`, 0 where the drivers pass none; `maintains`, whether an invalidation takes effect, the judgment the effective arm branches on and a marker lifetime reads); the layered shape and its local and request-only projections; the layered release judged once (`judgeRelease`, with the remote observation as a parameter; `layeredRelease` observes it; the judgment carries the observation, `remote_frames::Observed`, which `startSource` passes to the authority so a failed read, an adapter fault or a payload that fails the read alike, denies the refill on the atomic path as on the held one, and a source started without the remote layer passes `remote_frames::UNOBSERVED`; `judgeReleaseUnder` takes the local read's fault as an argument, `local_faults`: the local value hidden, the layers and the sharing as the reply resolved them, and the judgment carries the fault so `releaseJudged` publishes for the TTLs it allows, `publishable`: no local TTL for the remote hit it serves or the source it starts) for the transition and the records composed around it, and the steps a held remote effect suspends between, which the atomic path composes too: the release prologue up to the remote layer (`applyRelease`), publication authority fixed when a remote read completes (`authority`, capturing the retention the refill is written with, `remote_frames::physicalRetention`, so the source record and the write agree), warm-on-hit (`warm`) and one local source-publication gate for the layered and local-only traversals (`publishLocal`: only an accepted value may warm the slot; a rejected source preserves every existing entry), completion apart from the recorded outcome (`complete`, `finish`) and publication apart from completion (`refillAllowed`, whether a captured authority may be written now, the judgment a caller's refill, `refills`, and a shadow job's fill, `shadow::fill`, share; `dispatchWrite`, `publish`, `settle`); the judgment applied apart from the judging (`releaseJudged`, the release after `layeredRelease`, so a variant that records beside the release judges once and applies the same judgment) and a detached source (`startDetachedSource`: a source no caller owns, counted against the profile, probed as not participating, without publication authority, whose settlement completes no caller); whether the drivers install a policy provider is a layout fact (`Layout.policyProvider`: `entered` counts a policy call per participating admission only where one exists; an instance that resolves policy from its key's default configuration, the local-clock drivers' default instances, makes none); the local projection's immediate reply (`beginLocal`) and its inline call (`callLocal`: the loader returns before the call does, so the source a miss starts settles at once with the loader's value and a hit settles nothing). The generic behavior drivers always install a policy provider; only local-clock omits one and uses `callLocal` without a held policy gate. | `admit`, `release`, `judgeReleaseUnder`, `releaseJudged`, `startDetachedSource`, `begin`, `settle`, `publish`, `publishLocal`, `complete`, `finish`, `admitLocal`, `releaseLocal`, `settleLocal`, `beginLocal`, `callLocal`, `admitRequest`, `releaseRequest`, `settleRequest`, `closeScope`, `invalidate`, `maintains`, `refillAllowed` | +| `local_faults` | The local storage fault switch (C27) over the layered shape (`Faulted`: `serving::Served` with `localFailed`), read at the two points a call touches local storage and passed as an argument, the kernel's fault idiom, never turned into a TTL the layer gating reads: an admission with an immediate reply (`begin`) judges the layered release with the fault as its argument (`serving::judgeReleaseUnder`: the local value hidden, the layers and with them the coalescing untouched, nothing warmed from the release) and applies that judgment (`serving::releaseJudged`), so the traversal reads nothing from local storage, a remote hit warms nothing, the source it starts captures no local TTL (publication-ineligible whatever the switch says at settlement) and two callers under the fault share one source as two healthy callers do, as the implementations decide the process flight from the resolved layer configuration before the read and a read that throws disables only that call's local value and write (`src/dialcache.ts` `getThroughSharedLayers`, `getThroughActiveLocal`); a settlement (`settle`) withdraws the source's captured local TTL, so `serving::publish` warms nothing while the callers complete with the accepted result, the request memo is written and the remote refill proceeds; the traversal and the publication keep their one statement in `serving`, and the remote fault switches stay in `serving::Served` because the traversal reads them mid-release where this one is read at its two entry points; one consumer today, local-failure. The held lifecycle assumes healthy local storage until fault-aware held transitions exist; shape lint refuses that combination. | `begin`, `settle` | | `markers` | Invalidation markers: the lifetime of an entity's watermark, which the implementations keep as a Redis key with a TTL (`src/internal/redis-scripts.ts`): the larger of the two-hour floor (`MARKER_FLOOR_MS`, twice the tracked cap) and the span from the invalidation's wall clock to the watermark plus the cap and a minute's margin (`lifetime`), keeping a longer remaining lifetime; marker expiry is a time transition that returns the watermark to its zero baseline (`expire`, run by `advance`, through `remote_frames::clearWatermark`), so `remote_frames` keeps reading the raw watermark and the fence stays stated once, and only an expiry lowers a watermark; an invalidation stamps a marker when it takes effect (`serving::maintains`), as a variant of `serving::invalidate` passing the invalidator's future buffer through, whose outcome rule it does not restate (the marker lifetime reads the raised watermark, so a buffer lengthens the marker as the script does); `observe` appends the marker the drivers read (its cutoff against an explicit origin and its remaining lifetime) or `NO_MARKER` (PTTL -2 on a missing key); one consumer today, recovery-read | `invalidate`, `advance`, `expire`, `observe`, `inForce`, `lifetime` | | `deadlines` | Deadlines: one pending-only list of `Due` records (the work's kind, `READ`, `SOURCE` or a shadow job's `JOB`, its index in the drivers' order and the instant it is due), registered when bounded work starts and forgotten when it settles or is delivered, so bounded is presence; the budget a source starts with (a source started at admission is bounded only when its key failed, C27, a disabled context and an outside call run theirs unbounded, C01; a source started at release, with or without the remote layer, or a loader started for a held read is bounded, its caller was enabled when admitted; a held read is bounded by the read budget in force at dispatch); the `Scheduled` shape, the clock beside the pending deadlines, which registration (`due`), forgetting (`forget`) and delivery read and which `Budgeted` extends with the source budget and the drained flags: a module that bounds work of its own kind composes `Scheduled` alone (`shadow`'s jobs, kind `JOB`); instant-ordered delivery (`deliver`: an advance moves both clocks to each due instant inside it and delivers what is due there in registration order, so a loader a read expiry starts is delivered at its own instant in the same advance; a deadline the clock passed undelivered is delivered at the first instant the next advance visits); a result arriving after its deadline is a deadline error (`arrival`); abandoned work drains once, when its result arrives; as budgeted variants of the local lifecycle, which keep per-source history (`started`, `budgets`, `settledAt`), and of the held lifecycle (admission and release apart for a profile whose drivers hold policy replies, `begin` for immediate ones, `beginRead` for immediate ones whose drivers hold decodes and loaders but not reads: the read the step dispatched settles in it, `settleDispatched`, registering no read deadline under the unbounded budget such a read takes, a finite one reached at once only when zero; `releaseJudged` applies a release judgment already made, `release` is its composition with the judgment, so a variant that records beside the release judges once; `settleObserved` is the budgeted read settlement applied from a judgment already made, the read expired first when its deadline passed undelivered, `expiredIfReached`, as `settleRead` does before judging the frames) | `admitLocal`, `releaseLocal`, `settleLocal`, `advanceLocal`, `admit`, `releaseJudged`, `release`, `begin`, `settleDispatched`, `beginRead`, `settleRead`, `settleObserved`, `settleLoad`, `settleLoader`, `advance` | | `remote_io` | Remote effects the drivers hold and release by effect index, over the layered shape (`Held`): the release judged with the remote layer unobserved dispatches the read (counted then, its budget on the drivers' `io` channel) and registers the caller's flight, so a follower released while the read is pending joins it, or starts a source without the remote layer at once; every source started as a new flight takes the drivers' next loader ordinal (`mapLoader`, stated once for admission and release; a loader started for a held read's flight maps itself); the read settles with the state then current (a payload that fails the read fails it like an adapter fault), holding a decode with the frame's payload code or starting the flight's loader with the authority the read fixed; a decode settles by completing the flight with the value its payload decodes to (a fresh one warming local storage) or restarting a loader after a failed decode (the drivers' fault or a payload whose bytes fail to decode, `payloads::decodeFails`, which a recovery decode reports as its deserialization error); a loader settles by the drivers' ordinal (`loaders` maps ordinals to flights) and, failed, consults the flight's recovery snapshot through `recovery::failure`, holding a recovery decode when the candidate serves and completing with the error otherwise (the miss reported by `recovery::failure`); a read whose deadline was delivered owns nothing and its late reply drains; `io` (read budgets, aborted reads, per-caller source-error identities) is written by the transitions that have each fact in hand. Held reads and decodes are pending-only records; `releaseJudged` is the release applied from a judgment already made (`serving::judgeRelease` with the remote layer unobserved) and `release` its composition with the judgment; `begin` is admission and the held release in one step for a profile whose replies are immediate (a read settled in its dispatching step is `deadlines::settleDispatched`, the budgeted variant's: only that shape can forget the read deadline it may have registered); `latestRead`, `latestLoad` and `latestOrdinal` name the most recently held read and decode and the newest loader ordinal for a profile whose drivers name the latest effect rather than an index; `holdRead` is a read held over a flight (counted, budgeted on the io channel, indexed), stated once for the caller's dispatch here and a job's confirmation read in `shadow`; `settleObserved` is the read settlement applied from a judgment already made (what the read observed and the fence the refill it may start carries, `startLoaderFenced`, which takes the fence as an argument where `startLoader` reads it from the frames), so a variant that substitutes what a read observed applies the same settlement, and `settleRead` is its composition with the judgment of the frames | `admit`, `releaseJudged`, `release`, `begin`, `holdRead`, `settleRead`, `settleObserved`, `expireRead`, `settleLoad`, `settleLoader`, `startLoaderFenced`, `latestRead`, `latestLoad`, `latestOrdinal` | | `compression` | The compression channel: the outcome a decode reports as it is dispatched (`payloads::decodeOutcome`: "decompressed", "fallback_raw", nothing for an uncompressed frame), appended by variants of the two held transitions that hold a decode (`remote_io::settleRead` on a fresh frame, `remote_io::settleLoader` on a failed loader with a recovery candidate) over `Compressed`, the held shape with a `compression` channel; a variant rather than a mandatory `Held` field (independent would carry a channel it never reads) or a profile-side wrapper (invisible to the composition lint); one consumer today, recovery-read | `settleRead`, `settleLoader` | | `recovery` | Stale-on-error recovery (C40 to C46, C57) as a consulted record: the snapshot a flight retains when its read completes without serving (the visible frame's payload code when it decodes to a value, `payloads::valueOf`, and its age lies in [freshness, maximum), its stamp at the read, the maximum the reply's retention resolved to, the classifier in force), the loader failure judged once for whichever path consults it (`failure`: the classification counted, whether the candidate then serves, and the miss reported when recovery was allowed and it does not) and the recheck at recovery-decode settlement (`recovered`: the label and whether the candidate serves; a flight that does not serve it completes with the outcome its source recorded); classifier codes `NONE` (no stale-on-error: nothing is retained), `TIMEOUT_ONLY`, `ALLOW`, `DENY` (a profile decodes a classifier that throws to `DENY`, as the implementations do); `remote_io` and `remote_writes` read it (the held path holding a recovery decode, the atomic one decoding at once), no profile composes a recovery transition | `retain`, `failure`, `classified`, `mayRecover`, `validCandidate`, `recovered`, `recordOutcome`, `drop` | | `remote_writes` | The atomic remote lifecycle with held writes, over the layered shape (`Writing`: held dumps and writes as pending-only records keyed by the drivers' effect ordinal, and the recovery snapshots): the layered release applied from its judgment (`releaseJudged`, the one release seam: `shadow` judges the release once through `serving::layeredRelease` and applies it here), retaining the snapshot of a source it starts after a completed remote read that served nothing (`recovery::retain`, as the held read settlement does); a source that settles accepted and refills records its result and holds its dump (`settle`, `holdDump`: the dump counted then, with the retention the source captured and the fence its read observed, C18/C58) and publication completes with the write: the dump released dispatches the write stamped with the wall clock of that release (`dispatch`, `serving::dispatchWrite`, C33; a fence the wall no longer clears stops it and the flight completes without one), the write released stores the frame with the captured stamp, warms local storage for the source's local TTL and completes every owner (`store`, `releaseWrite`); an accepted value that does not refill publishes and completes at once (`serving::publish`); a failed source consults the flight's snapshot once (`recovery::failure`) and, the path being atomic, decodes the candidate at once (`failed`: the decode counted, `recovery::recovered`) or completes with its error (the miss reported by `recovery::failure`); the adapter's dump and write faults are the `failed` argument of the two releases (a failed dump dispatches no write, a failed write stores nothing, the flight completes either way, as `serving::publish` has it); a dump abandoned before its release dispatches no write (`abandonDump`); the newest held dump and write (`latestDump`, `latestWrite`, as `remote_io` names its latest read and decode) for a profile whose drivers release the latest effect; a held dump records the wall clock its first fence check sampled (`HeldDump.sampled`, the pre-serialization receipt, C34: the fence was cleared at that instant whatever the wall clock does before the release's second check); the same held refill over the held read path (`HeldWriting`, `deadlines::BudgetedHeld` with held dumps and writes; `settleLoader`: a loader settled by the drivers' ordinal whose accepted result refills records its outcome, drops its snapshot and holds its dump, every other arrival being `deadlines::settleLoader`) for a profile whose drivers hold reads, decodes, loaders, dumps and writes alike (effects); one consumer of the atomic lifecycle today, shadow-layers | `releaseJudged`, `settle`, `settleLoader`, `failed`, `holdDump`, `dispatch`, `abandonDump`, `abandonWrite`, `releaseDump`, `store`, `releaseWrite`, `complete`, `finish`, `holdsDump`, `heldDump`, `holdsWrite`, `heldWrite`, `latestDump`, `latestWrite` | -| `shadow` | Shadow jobs (C47 to C54, C57, C60): the diagnostic work a served remote hit or a ramped-down miss admits beside the caller, in one per-instance registry of bounded capacity shared by both kinds (`Jobs`: the jobs, their budgets and the sources they run for; pending-only and keyed by the job's source, a finished job leaving no record, as a settled held read or dump does), composed by two lifecycles: the atomic one (`Shadowed`, `remote_writes::Writing` with the jobs and their deadlines) judges the hit and the policy in one step at release and makes the dark C0 read, the decode and the confirmation read at once; the held one (`HeldJobs`, `remote_io::Held` with the held fills, the jobs and their deadlines) holds them as `remote_io` holds a caller's read and decode, in the same lists under the drivers' effect indices, budgeted on the io channel like every held read and never deadline-registered, a held effect over a source whose job is reading or decoding being that job's (`holdsRead`, `holdsLoad`, `latestRead`, `latestLoad`). The policy in force for a call is `Shadowing`: whether the outcome hook is installed (C47), the cohort selection its reply resolved (per call, C18), the instance's capacity and job budget, and whether a confirmed mismatch is logged (C60, captured by the job, `Job.logging`); `Malformed` says whether the ramp or the logging value failed to resolve; the comparator the drivers install is `Comparison` (ordinary value equality, or a callback reporting equal, unequal, or throwing after consuming elapsed time). A job captures the TTLs its reply resolved to (`Job.ttls`, C18) and the fence its C0 read observed when it found no visible frame (C58); the fill authority is `serving::authority` at the fill (`fill`, one statement for callers and jobs through `serving::refillAllowed`: the retention capped for a tracked write, `fill_fenced` before serializing when the fence no longer allows the write, no dump). Atomic admission beside the release judged once (`admitJobs` from `serving::layeredRelease`; `release` and `begin` compose it after `remote_writes::releaseJudged`): nothing unless the hook is installed and the reply selected the cohort (`admits`); a served job over a detached source (`admitServed`, stated once for both lifecycles: `serving::startDetachedSource` with the payload the hit served as its C0), a dark job over the caller's own source reading its C0 from the remote layer itself (the read counted; the payload when the frame is fresh for the reply's freshness, `acquired`, nothing otherwise), a dark C0 read that fails being the label alone, `redis_error`, the read counted, no job, nothing filled; nothing for an ordinary serving miss, a request or local hit or a bypass reply (the caller's scope closed before its release), `dropped` for a live job of the same identity or a full instance (`busy`, C47); settlement of the job's source (`settle` after `remote_writes::settle`, `settleJob`: `sourceOutcome`, `source_error`, or `timeout` for a source the caller's deadline failed; the fill when there is no C0; else the decode counted and `match` when the decoded value agrees, `valuesMatch`, or a confirmation read counted and its verdict, `confirmation`, the one judgment for both lifecycles: `mismatch` when the C0 bytes are still current, `payloads::bytesOf`, so a text and a binary spelling confirm each other, `superseded` otherwise, `confirmation_error` when that read fails); the held fill's releases (`releaseDump`, `releaseWrite`: `filled`, `fill_fenced`, `fill_error`; a dump released after the budget dispatches no write while a write released after it still stores, C53; a job's fill completes nobody). The held lifecycle's rules, stated once over `remote_io`: the source settled fills or holds the decode (`settleJobHeld`), the decode settled says `match` or holds the confirmation read under the read budget in force (`settleJobLoad`: `deserialization_error` when the decode fails; the comparator's elapsed time moving the clock without delivering timers, a budget it reaches ending `timeout` before any verdict, a throwing callback `comparison_error`, the callback counted, `o.comparisons`), the read settled says the verdict or acquires the C0 with its fence and continues at once when the source already settled (`settleJobRead`; `redis_error` for a failed C0 read); a timed-out job ends silently at whichever step finds it so, keeping its slot until then. Served jobs (`HeldShadowed`, the admission profile: `beginHeld` and `releaseHeld` over `remote_io::begin` and `release`): the selection is captured for the flight whose read the step dispatched (`capture`, a pending-only list of flights) and consulted when the hit is judged, at the decode's settlement (`settleLoad`: a fresh frame decoded completes the flight with a served hit, which admits the job through `admitServed` under the hook and limits read then and maps its detached source to the drivers' next loader ordinal; a failed or recovery decode admits nothing), or forgotten at the read's settlement when it holds no decode (`settleRead`); a job's read or decode settles as the held lifecycle has it, a loader by the drivers' ordinal (`settleLoader`). Dark jobs (`DiagnosedShadowed`, over `diagnostics::DiagnosedHeld`: the caller's source deadline, the layer its failure is attributed to and the channel the shadow drivers compare): `beginDark` is the diagnosed held release (`diagnostics::admit`, `serving::judgeRelease`, `diagnostics::releaseJudged`) with the configuration error a malformed policy reports (`diagnostics::recordConfigError`: an invalid ramp always, an invalid logging value only where the hook and the ramp would otherwise admit the job), the job registered with its budget (`admitDark`) before the source's synchronous work moves the clock, and the C0 read dispatched or the job timed out with no read when the work exhausted the budget (`dispatchC0`, C54); `settleHeld` settles a loader through `diagnostics::settleLoader` and continues the job waiting on a flight the step settled; `releaseRead` and `releaseLoad` are the job's read and decode settlements with the channel facts recorded beside the label they reported (`recordVerdict`: a `match` or a confirmed `mismatch` samples the C0 frame's age at the verdict, clamped to zero after a wall rollback, `diagnostics::recordAge`, C57, and a confirmed mismatch warns exactly when the job captured logging on, `diagnostics::recordWarning`, C60; a shadow read of a frame stamped after the wall clock reports the offset on the `remote_shadow` layer whatever follows, `diagnostics::recordFutureOffset`); `advanceHeld` delivers job budgets, read deadlines and source deadlines by kind (`expireHeld`: a source deadline ends the job waiting on its flight `timeout` through `settleJobHeld`, releasing its slot, and a job budget due in the same round finds it gone). Every job's budget is kept by `deadlines` (kind `deadlines::JOB`, keyed by the job's source: registered in `admit`, forgotten in `finishJob`, delivered by `advance` or `advanceHeld` through `expireJob`: `timeout` said once, a dark job still waiting leaving the registry while the caller's source runs on, any other job keeping its record until its held work drains, no later label following); cohort selection stays the profile's decoder over `cohort_boundaries`; three consumers: shadow-layers over the atomic lifecycle, admission over the held served-job arm and shadow over the dark arm | `begin`, `release`, `admitJobs`, `admitServed`, `admits`, `admit`, `dropped`, `busy`, `acquired`, `settle`, `settleJob`, `sourceOutcome`, `fill`, `valuesMatch`, `matches`, `confirmation`, `finishJob`, `releaseDump`, `releaseWrite`, `expireJob`, `advance`, `settleJobHeld`, `settleJobLoad`, `settleJobRead`, `beginHeld`, `releaseHeld`, `settleRead`, `settleLoad`, `settleLoader`, `admitDark`, `dispatchC0`, `beginDark`, `settleHeld`, `recordVerdict`, `releaseRead`, `releaseLoad`, `expireHeld`, `advanceHeld`, `holdsJob`, `jobOf`, `sourceOf`, `duplicate`, `full`, `holdsRead`, `holdsLoad`, `latestRead`, `latestLoad`, `confirming` | +| `shadow` | Shadow jobs (C47 to C54, C57, C60): the diagnostic work a served remote hit or a ramped-down miss admits beside the caller, in one per-instance registry of bounded capacity shared by both kinds (`Jobs`: the jobs, their budgets and the sources they run for; pending-only and keyed by the job's source, a finished job leaving no record, as a settled held read or dump does), composed by two lifecycles: the atomic one (`Shadowed`, `remote_writes::Writing` with the jobs and their deadlines) judges the hit and the policy in one step at release and makes the dark C0 read, the decode and the confirmation read at once; the held one (`HeldJobs`, `remote_io::Held` with the held fills, the jobs and their deadlines) holds them as `remote_io` holds a caller's read and decode, in the same lists under the drivers' effect indices, budgeted on the io channel like every held read and never deadline-registered, a held effect over a source whose job is reading or decoding being that job's (`holdsRead`, `holdsLoad`, `latestRead`, `latestLoad`). The policy in force for a call is `Shadowing`: whether the outcome hook is installed (C47), the cohort selection its reply resolved (per call, C18), the instance's capacity and job budget, and whether a confirmed mismatch is logged (C60, captured by the job, `Job.logging`); `Malformed` says whether the ramp or the logging value failed to resolve; the comparator the drivers install is `Comparison` (ordinary value equality, or a callback reporting equal, unequal, or throwing after consuming elapsed time). A job captures the TTLs its reply resolved to (`Job.ttls`, C18) and the fence its C0 read observed when it found no visible frame (C58); the fill authority is `serving::authority` at the fill (`fill`, one statement for callers and jobs through `serving::refillAllowed`: the retention capped for a tracked write, `fill_fenced` before serializing when the fence no longer allows the write, no dump). Atomic admission beside the release judged once (`admitJobs` from `serving::layeredRelease`; `release` and `begin` compose it after `remote_writes::releaseJudged`): nothing unless the hook is installed and the reply selected the cohort (`admits`); a served job over a detached source (`admitServed`, stated once for both lifecycles: `serving::startDetachedSource` with the payload the hit served as its C0), a dark job over the caller's own source reading its C0 from the remote layer itself (the read counted; the payload when the frame is fresh for the reply's freshness, `acquired`, nothing otherwise), a dark C0 read that fails being the label alone, `redis_error`, the read counted, no job, nothing filled; nothing for an ordinary serving miss, a request or local hit or a bypass reply (the caller's scope closed before its release), `dropped` for a live job of the same identity or a full instance (`busy`, C47); settlement of the job's source (`settle` after `remote_writes::settle`, `settleJob`: `sourceOutcome`, `source_error`, or `timeout` for a source the caller's deadline failed; the fill when there is no C0; else the decode counted and `match` when the decoded value agrees, `valuesMatch`, or a confirmation read counted and its verdict, `confirmation`, the one judgment for both lifecycles: `mismatch` when the C0 bytes are still current, `payloads::bytesOf`, so a text and a binary spelling confirm each other, `superseded` otherwise, `confirmation_error` when that read fails); the held fill's releases (`releaseDump`, `releaseWrite`: `filled`, `fill_fenced`, `fill_error`; a dump released after the budget dispatches no write while a write released after it still stores, C53; a job's fill completes nobody). The held lifecycle's rules, stated once over `remote_io`: the source settled fills or holds the decode (`settleJobHeld`), the decode settled says `match` or holds the confirmation read under the read budget in force (`settleJobLoad`: `deserialization_error` when the decode fails; the comparator's elapsed time moving the clock without delivering timers, a budget it reaches ending `timeout` before any verdict, a throwing callback `comparison_error`, the callback counted, `o.comparisons`), the read settled says the verdict or acquires the C0 with its fence and continues at once when the source already settled (`settleJobRead`; `redis_error` for a failed C0 read); a timed-out job ends silently at whichever step finds it so, keeping its slot until then. Served jobs (`HeldShadowed`, the admission profile: `beginHeld` and `releaseHeld` over `remote_io::begin` and `release`): the selection is captured for the flight whose read the step dispatched (`capture`, a pending-only list of flights) and consulted when the hit is judged, at the decode's settlement (`settleLoad`: a fresh frame decoded completes the flight with a served hit, which admits the job through `admitServed` under the hook and limits read then and maps its detached source to the drivers' next loader ordinal; a failed or recovery decode admits nothing), or forgotten at the read's settlement when it holds no decode (`settleRead`); a job's read or decode settles as the held lifecycle has it, a loader by the drivers' ordinal (`settleLoader`). Dark jobs (`DiagnosedShadowed`, over `diagnostics::DiagnosedHeld`: the caller's source deadline, the layer its failure is attributed to and the channel the shadow drivers compare): `beginDark` is the diagnosed held release (`diagnostics::admit`, `serving::judgeRelease`, `diagnostics::releaseJudged`) with the configuration error a malformed policy reports (`diagnostics::recordConfigError`: an invalid ramp always, an invalid logging value only where the hook and the ramp would otherwise admit the job), the job registered with its budget (`admitDark`) before the source's synchronous work moves the clock, and the C0 read dispatched or the job timed out with no read when the work exhausted the budget (`dispatchC0`, C54); `settleHeld` settles a loader through `diagnostics::settleLoader` and continues the job waiting on a flight the step settled; `releaseRead` and `releaseLoad` are the job's read and decode settlements with the channel facts recorded beside the label they reported (`recordVerdict`: a `match` or a confirmed `mismatch` samples the C0 frame's age at the verdict, clamped to zero after a wall rollback, `diagnostics::recordAge`, C57, and a confirmed mismatch warns exactly when the job captured logging on, `diagnostics::recordWarning`, C60; a shadow read of a frame stamped after the wall clock reports the offset on the `remote_shadow` layer whatever follows, `diagnostics::recordFutureOffset`); `advanceHeld` delivers job budgets, read deadlines and source deadlines by kind (`expireHeld`: a source deadline ends the job waiting on its flight `timeout` through `settleJobHeld`, releasing its slot, and a job budget due in the same round finds it gone). Every job's budget is kept by `deadlines` (kind `deadlines::JOB`, keyed by the job's source: registered in `admit`, forgotten in `finishJob`, delivered by `advance` or `advanceHeld` through `expireJob`: `timeout` said once, a dark job still waiting leaving the registry while the caller's source runs on, any other job keeping its record until its held work drains, no later label following); cohort selection stays the profile's decoder over `cohort_boundaries`; three consumers: shadow-layers over the atomic lifecycle, admission over the held served-job arm and shadow over the dark arm. A job budget above the source deadline is pinned only by `held-dark-shadow.qnt` / `sourceDeadlineAloneEndsTheJobTest`: native drivers derive both budgets from `fallbackTimeoutMs`, and separate budgets would require a per-call `sourceTimeoutMs` input. | `begin`, `release`, `admitJobs`, `admitServed`, `admits`, `admit`, `dropped`, `busy`, `acquired`, `settle`, `settleJob`, `sourceOutcome`, `fill`, `valuesMatch`, `matches`, `confirmation`, `finishJob`, `releaseDump`, `releaseWrite`, `expireJob`, `advance`, `settleJobHeld`, `settleJobLoad`, `settleJobRead`, `beginHeld`, `releaseHeld`, `settleRead`, `settleLoad`, `settleLoader`, `admitDark`, `dispatchC0`, `beginDark`, `settleHeld`, `recordVerdict`, `releaseRead`, `releaseLoad`, `expireHeld`, `advanceHeld`, `holdsJob`, `jobOf`, `sourceOf`, `duplicate`, `full`, `holdsRead`, `holdsLoad`, `latestRead`, `latestLoad`, `confirming` | | `diagnostics` | The diagnostics channel (`d`, a row-polymorphic `Channel[q]` so a profile whose drivers compare more fields carries them beside the four every descriptor parses): the singleflight a caller coalesced into and the layer a failed source is attributed to, as diagnosed variants of the request-only traversal; and, as diagnosed variants of the budgeted held lifecycle (`DiagnosedHeld`) for a profile whose replies are immediate, its reads settled at admission or held, the singleflight a follower coalesced into (`request_local`, or the process registry's `PROCESS_SCOPE`), the layer a flight is attributed to when it starts (`REMOTE_LAYER` for one that dispatched a read, whatever the read observed; the request memo or the shared layers' fallback for a source started without the remote layer; none for a bypass source started at admission), the fallback error recorded against that layer when the flight's failure is recorded (a late result that only drains records nothing) and the recovery age sampled when a recovery decode serves (`recordAge`, C57); each held variant judges the release once, through `deadlines::releaseJudged` (`admit`, the bypass attribution, and `releaseJudged`, the held release applied from a judgment and labelled from it, are the split `begin` composes, admission and the held release in one step for a profile whose drivers hold the read, and `beginRead` settles in the same step through `deadlines::settleDispatched`, so a variant that admits work beside the held release, `shadow::beginDark`, judges once and applies them); the two fields the shadow drivers compare beside the four (`ShadowChannel`: the configuration errors a malformed shadow policy reports, `recordConfigError`, and the offsets of frames a shadow read found stamped after the wall clock, on the `remote_shadow` layer, `recordFutureOffset`, C57) and the warning a confirmed mismatch logs (`recordWarning`, C60), recorded by `shadow` at the moment each fact is known | `admitRequest`, `releaseRequest`, `settleRequest`, `admit`, `releaseJudged`, `begin`, `beginRead`, `settleLoader`, `expireLoader`, `advance`, `settleLoad`, `recordAge`, `recordWarning`, `recordConfigError`, `recordFutureOffset` | | `adapter_replies` | The queued adapter reply (C55): what the remote adapter returns in place of the stored frame, interpreted once at the trust boundary as the ports do (`validateReadResult`, `frameAge`, `decodeTrackedRedisReadResult`). The drivers queue one reply by its class (`queue`, the sixteen classes of the effects `adapterReply` choices, only the ones a rule names spelled); the next successful raw completion consumes it whether or not DialCache still owns that read (`settleRead`: an owning read observes the reply in place of the frames and starts its source with the reply's fence through `deadlines::settleObserved`, an abandoned one drains as any other; a failed completion leaves it queued). A miss reply is read by its discriminator (`reason`: a known cause survives, `value_absent`, `expired`; an unknown reason or shape is `unclassified`; `watermark_fenced` only beside a valid fence); a fence survives only on a tracked key and only as a valid timestamp (`carriesFence`, `fenceOf`: the reply's wall clock plus the drivers' buffer, the refill fence of the source the miss starts, C58); a frame-shaped reply with stray miss metadata is a visible frame stamped at the reply's wall clock and judged fresh like a stored frame (`observed`, a future-dated one declined as an unclassified miss without a fence). No reply is ever stored; one consumer, effects | `queue`, `consume`, `queued`, `isFrame`, `carriesFence`, `fenceOf`, `reason`, `observed`, `settleRead` | | `metrics` | The observer's metric events over the remote layer (C30, C57 to C59), the ordered trail the ports report to their metrics callback and the effects drivers compare as `events` (one record per event: name, layer or scope, detail, amount), stated once as metered variants over `diagnostics::DiagnosedHeld` with held refills and the queued reply (`Metered`): a participating caller's admission reports each shared layer its reply leaves off (`disabled`, `policy_disabled`, local before remote) then its read (`request`) or the singleflight it joined (`coalesced`, the scope `diagnostics` recorded, mirrored from the channel's delta so the rule stays stated there); a read reports its deadline error (`cache_read_timeout`, delivered or found reached at a late reply), its fault (`cache_read`) or its miss with the reason (`missReason`: `value_absent`, `watermark_fenced`, `unclassified` for a future-dated frame after a `futureOffset` of the stamp's lead, `expired`; a queued reply's normalized reason) and its `get` duration from dispatch to settlement or to the fresh decode it held (C59, the decode's own `serialization` `load` duration after its fault, `serialization_load`, and an `unclassified` miss); a loader whose flight's outcome the step records reports its failure against the layer `diagnostics` attributed (`error`, `fallback`) and its `fallback` duration from its start, at arrival or at delivery, a drained late result reporting nothing (C58); a released dump reports `serialization_dump` or its `serialization` `dump` duration from its hold, then `size` and `storedSize`, then `writeDispatch` with the write's index when the second fence check lets it go; a released write reports `cache_write`; a counted invalidation reports `invalidation`. The instants (`readStarted` per flight, `decodeStarted`, `sourceStarted` per loader ordinal, `dumpStarted`) are recorded by the transitions that start each effect (`stamped`). Not stated: a bypass caller's `context` reason, a payload class that fails the read, a recovery decode's events, compression events; one consumer, effects | `event`, `remote`, `error`, `miss`, `stamped`, `readDone`, `begin`, `futureLead`, `missReason`, `settleRead`, `settleLoad`, `sourceDone`, `settleLoader`, `expireLoader`, `expireRead`, `advance`, `releaseDump`, `releaseWrite`, `invalidate` | @@ -237,7 +238,10 @@ by ordinal over the diagnosed budgeted held remote lifecycle, with the held fill and the buffered invalidation; `formal/dialcache-effects-conformance.qnt` the fourteenth, over the held remote lifecycle with its refills held, the queued adapter reply and the metric events, the first whose drivers' channel -is an ordered event trail). It keeps +is an ordered event trail); `formal/dialcache-dark-layers-conformance.qnt` +is the fifteenth, over `Shadow::DiagnosedShadowed`: held dark effects composed +with request/local traversal and source deadlines, so every shadow job uses +its caller-owned source. It keeps its constants, its flat `State`, `var s` and `var input`, its `nondet` input choices, its guards, its invariants and its regressions. Each wrapper action assigns `s'` to one library transition and `input'` to the driver record: @@ -351,6 +355,11 @@ corpus differential is the behavioral check. ## Migrating a profile +A new profile records absolute corpus size and bytes per state. Keep its sampled +corpus below 100 MB and below the measured admission reference (4731 bytes per +state), or state why its required shape exceeds that bound. Historical +per-slice measurements below retain their original corpus layouts. + A rewrite lands when the corpus differential agrees on every history: ```bash @@ -399,8 +408,8 @@ the public wrapper, and is deleted once the merge base with main carries the renamed text. `make differential` runs the lint baseline check and then the differential for every profile that imports a kernel module in either revision (directly or through a -helper library); `DIFFERENTIAL_SHARD=/` narrows it to one -round-robin shard of those profiles sorted by name. The pull request lane runs +helper library); `DIFFERENTIAL_SHARD=/` narrows it to one shard +balanced by estimated profile replay time. The pull request lane runs four such shards against the base branch whenever a Quint input changes and preserves each shard's reports and replay logs as the `formal-differential-` artifact. @@ -593,3 +602,37 @@ baseline was re-recorded, the both-way differential (554 of 554 forward, 560 of The pilot that preceded the library (#171, #172) instantiated one kernel state machine per profile and measured its cost; its conclusions and measurements are recorded in issue #165. + +## Record of the dark-layers interaction profile + +This new profile has no reference corpus to compare. With the manifest seed, +256 sampled histories contain 15,616 states and 69,478,386 bytes (4449 bytes per +state); 18 named histories pin the public interaction boundaries. Its twelve +kernel transitions introduce no profile rule-logic violations. The additional +transient-request diagnostic history records a model correction found by replay: +TypeScript and Go both attribute an active-local failure to local, and a +request-only failure to request_local even without a persistent memo slot. + +The sampled corpus contains nine match verdicts, eight mismatches, one +supersession and no confirmation errors, and 79 request-local follower joins. +The separate shadow profile retains named confirmation-error coverage. + + +## Separate shadow read deadlines + +`shadow-reads.qnt` composes the held dark lifecycle with each read's bounded +result and raw completion. A 5 ms read may end validation before its 10 ms +job deadline; a 20 ms read can outlive that deadline. Bounded errors cancel +the read and forget the job timer while the raw adapter operation keeps its +registration. Late raw completion only releases capacity. The caller source +keeps its independent result. C1 captures the job's admitted read policy, +then measures its read deadline from C1 dispatch. + +`shadow-read-deadlines` schedules six public histories, with required witnesses +for cancellation, contexts, verdicts, competing-key rejection and readmission. +Independent properties require every held raw read to retain its registration, +every cancelled read to have ended its bounded result, and ended reads to have +no remaining job timer. M61 and M62 distinguish losing bounded completion from +releasing raw ownership early, in the model and both native implementations. +Existing profiles retain their simpler projection when their bounds cannot +reach a separate shadow read deadline; factoring `retireJob` preserves it. diff --git a/formal/kernel/calls.qnt b/formal/kernel/calls.qnt index 5448f686..a2db6dd2 100644 --- a/formal/kernel/calls.qnt +++ b/formal/kernel/calls.qnt @@ -9,8 +9,10 @@ module calls { // theirs unbounded (C01). Decoders set `keyFailed` only for a caller whose // key failed to construct inside an enabled, open request, the order the // implementation checks in (enablement before key construction): `keyFailed` - // implies `enabled: false`, and it is never combined with a disabled - // context, a closed scope or an outside caller. The policy gate holds a + // implies `enabled: false` in current decoders. The kernel checks the + // key-failure flag when deciding participation too, so a call that carries + // both flags still bypasses caching with its configured source deadline. + // The policy gate holds a // call with the caller waiting for its reply, so a release never // re-supplies it. type Call = { instance: int, key: int, context: int, enabled: bool, keyFailed: bool } diff --git a/formal/kernel/clock.qnt b/formal/kernel/clock.qnt index 51c92d5d..b2547085 100644 --- a/formal/kernel/clock.qnt +++ b/formal/kernel/clock.qnt @@ -13,7 +13,8 @@ module clock { // (`cache_rules.wholeMs`, the projection `localEntryLiveOnGrid` judges // with, C09), so a fractional advance moves `now` only across a millisecond // boundary and local insertion and expiry fall on the grid whatever the - // instance's construction phase. + // instance's construction phase. Combining advanceTicks with delivered + // deadlines requires a kernel transition not yet defined here. type Ticked[r] = { now: int, ticks: int | r } pure def advance(state: Timed[r], ms: int): Timed[r] = { now: state.now + ms, ...state } diff --git a/formal/kernel/compression.qnt b/formal/kernel/compression.qnt index e50319f5..caf86ea8 100644 --- a/formal/kernel/compression.qnt +++ b/formal/kernel/compression.qnt @@ -14,7 +14,8 @@ module compression { // channel its traversal never reads. A variant rather than a wrapper in the // profile: a wrapper forgotten at one of the two sites is invisible to the // composition lint, a variant is the transition. One consumer today, - // recovery-read. + // recovery-read; no variant yet records compression while delivering + // source deadlines or settling shadow work. import serving as Serving from "./serving" import remote_io as RemoteIO from "./remote-io" import payloads as Payloads from "./payloads" diff --git a/formal/kernel/deadlines.qnt b/formal/kernel/deadlines.qnt index 3f2c605d..c21a5888 100644 --- a/formal/kernel/deadlines.qnt +++ b/formal/kernel/deadlines.qnt @@ -34,7 +34,10 @@ module deadlines { // before the clock moves: an open design point for a budgeted recovery // composition, unreachable in every scheduled profile today (independent's // source budget is 10 ms; the local shape makes no clock judgment at - // expiry). An expired source completes every + // expiry). The current zero-budget delivery is pinned by held-remote.qnt's + // `zeroBudgetLoaderStartedByExpiryDeliversInTheSameAdvanceTest` and + // budgeted-local.qnt's `zeroBudgetSourceSettlesAsDeadlineErrorTest`. + // An expired source completes every // caller it owns with DEADLINE_ERROR; a result arriving after its deadline // is a deadline error (`arrival`); work whose deadline already completed // its callers drains once, when the result arrives. `drained` is per loader diff --git a/formal/kernel/diagnostics.qnt b/formal/kernel/diagnostics.qnt index c903629f..f644c1bf 100644 --- a/formal/kernel/diagnostics.qnt +++ b/formal/kernel/diagnostics.qnt @@ -108,8 +108,8 @@ module diagnostics { // singleflight a caller coalesced into, judged once beside the release // (`Serving::judgeRelease` applied through `Deadlines::releaseJudged`); the // layer a flight is attributed to when it starts (the remote layer for a - // flight that dispatched a read, whatever the read observed; the request - // memo or the shared layers' fallback for a source started without the + // flight that dispatched a read, whatever the read observed; the + // innermost active layer for a source started without the // remote layer; none for a bypass source started at admission); the // fallback error recorded against that layer when the flight's failure is // recorded (a late result that only drains records none); the recovery age @@ -126,13 +126,20 @@ module diagnostics { // `Deadlines::releaseJudged`), labelled from that judgment; a variant that // admits work beside the held release (`shadow::beginHeld`) judges once // and applies this. + // TypeScript getThroughActiveLocal/finishRampedDownRemote attributes a failed + // source to local even inside a request memo. Without a shared layer, + // getThroughRequestLocal attributes it to request_local, including a transient + // request with no persistent model memo slot. Dark-layers pins both cases. pure def releaseJudged(state: DiagnosedHeld[r, q], layout: Serving::Layout, judged: Serving::LayeredRelease, policyCall: int, ttls: Ttls, budget: int): DiagnosedHeld[r, q] = { val released = Deadlines::releaseJudged(state, layout, judged, policyCall, ttls, budget) match judged.decision.outcome { | JoinsRequest(_) => recordCoalesced(released, REQUEST_LOCAL_LAYER) | JoinsProcess(_) => recordCoalesced(released, PROCESS_SCOPE) | Starts => recordSourceLayer(released, - if (judged.effective.layers.remote) REMOTE_LAYER else sourceLayer(judged.open, judged.decision.slot)) + if (not(judged.open)) NO_CACHE_LAYER + else if (judged.effective.layers.remote) REMOTE_LAYER + else if (judged.effective.layers.local) LOCAL_LAYER + else if (judged.effective.layers.request) REQUEST_LOCAL_LAYER else LOCAL_LAYER) | ServesMemo(_) => released | ServesLocal(_) => released | ServesRemote(_) => released diff --git a/formal/kernel/flights.qnt b/formal/kernel/flights.qnt index 13c30cd0..c249fa04 100644 --- a/formal/kernel/flights.qnt +++ b/formal/kernel/flights.qnt @@ -96,4 +96,35 @@ module flights { callerKeys: state.callerKeys.append(call.key), ...state } + + // Optional public inspection channel. Admission receipts preserve the source + // registration instant and the recorded join scope for each caller; they do + // not change flight ownership. A snapshot includes registered process work + // only: request followers, detached jobs and abandoned raw work are absent. + type Inspection = { instance: int, activeLeaders: int, activeFollowers: int, oldestLeaderAgeMs: int } + pure val NO_LEADER_AGE = -1 + type Inspected[r, p, q] = { + now: int, skew: int, sources: List[Source[{ instance: int | p }]], owners: List[int], + processFlights: List[int], inspectionStarted: List[int], inspectionJoins: List[str], + d: { coalesced: List[str], inspections: List[Inspection] | q } | r + } + pure def recordInspectionAdmission(before: Inspected[r, p, q], after: Inspected[r, p, q]): Inspected[r, p, q] = { + inspectionStarted: if (after.sources.length() > before.sources.length()) before.inspectionStarted.append(before.now) else before.inspectionStarted, + inspectionJoins: before.inspectionJoins.append( + if (after.d.coalesced.length() > before.d.coalesced.length()) after.d.coalesced.nth(after.d.coalesced.length() - 1) else ""), + ...after + } + pure def inspectProcess(state: Inspected[r, p, q], instance: int, keysPerInstance: int): Inspected[r, p, q] = { + val registered = state.processFlights.indices().filter(slot => slot / keysPerInstance == instance + and state.processFlights.nth(slot) != NO_OWNER).map(slot => state.processFlights.nth(slot)) + val joined = state.owners.indices().filter(caller => state.inspectionJoins.nth(caller) == "process" + and registered.contains(state.owners.nth(caller))) + val oldest = registered.fold(state.now, (at, owner) => if (state.inspectionStarted.nth(owner) < at) state.inspectionStarted.nth(owner) else at) + val snapshot = { + instance: instance, activeLeaders: registered.size(), activeFollowers: joined.size(), + oldestLeaderAgeMs: if (registered.size() == 0) NO_LEADER_AGE else state.now - oldest + } + { d: { inspections: state.d.inspections.append(snapshot), ...state.d }, ...state } + } + } diff --git a/formal/kernel/layer-policy.qnt b/formal/kernel/layer-policy.qnt index 5f8a2139..65514ab7 100644 --- a/formal/kernel/layer-policy.qnt +++ b/formal/kernel/layer-policy.qnt @@ -18,7 +18,10 @@ module layer_policy { // and the physical retention written with each remote frame. A layer whose // TTL is 0 is off (`gated`), and a source refills the remote layer iff it // captured a positive retention: a reply with `freshMs > 0` and - // `retentionMs` 0 reads the remote layer and never refills it. + // `retentionMs` 0 reads the remote layer and never refills it. That region + // is not native configuration: a positive staleOnErrorMaxAgeSec must exceed + // ttlSec.remote, and every profile decoder keeps retention at or above + // freshness (including policy overlays' fallback for invalid recovery ages). type Ttls = { localMs: int, freshMs: int, retentionMs: int } // The TTLs of a call that uses no layer: the bypass reply's. pure val NO_TTLS: Ttls = { localMs: 0, freshMs: 0, retentionMs: 0 } diff --git a/formal/kernel/local-faults.qnt b/formal/kernel/local-faults.qnt index 39be8ee0..49253990 100644 --- a/formal/kernel/local-faults.qnt +++ b/formal/kernel/local-faults.qnt @@ -28,7 +28,9 @@ module local_faults { // captured local TTL, so `serving::publish` warms nothing. The traversal and // the publication keep their one statement in `serving`; the remote fault // switches live in `serving::Served` because the traversal reads them mid - // release, where this one is read at its two entry points. + // release, where this one is read at its two entry points. The held + // lifecycle still judges a healthy local read; combining it with this + // switch needs fault-aware held transitions and is rejected by shape lint. import layer_policy.* from "./layer-policy" import calls.* from "./calls" import flights as Flights from "./flights" diff --git a/formal/kernel/local-grid.qnt b/formal/kernel/local-grid.qnt new file mode 100644 index 00000000..02028f09 --- /dev/null +++ b/formal/kernel/local-grid.qnt @@ -0,0 +1,35 @@ +module local_grid { + import cache_rules.* from "../cache-rules" + import encodings.* from "./encodings" + import calls.* from "./calls" + import layer_policy.* from "./layer-policy" + import serving as Serving from "./serving" + + // Raw environment instants are retained beside the ordinary local expiry. + // Lookup projects both the observed time and the insertion time onto the + // shared millisecond grid. Keeping both raw instants makes a precise-clock + // expiry error executable; an already rounded insertion cannot expose it. + type State[r] = Serving::LocalServed[{ ticks: int, localInsertedTicks: List[int] | r }] + + pure def call(state: State[r], layout: Serving::Layout, who: Call, resolved: Resolution, ttls: Ttls, value: int, ticksPerMs: int): State[r] = { + val slot = who.instance * layout.keysPerInstance + who.key + val localEnabled = Serving::participating(state, layout, who) and gated(resolved, ttls).layers.local + val observed = if (localEnabled) { + val inserted = state.localInsertedTicks.nth(slot) + // The retained expiry remains authoritative. Deriving its duration from + // the insertion grid keeps expiry edits observable instead of silently + // substituting a second, independently stored TTL. + val ttlMs = state.localExpires.nth(slot) - wholeMs(inserted, ticksPerMs) + val live = localEntryLiveOnGrid(state.ticks, inserted, ttlMs, ticksPerMs) + if (live) state.localValues.nth(slot) else NO_VALUE + } else NO_VALUE + val completed = Serving::callLocalObserved(state, layout, who, resolved, ttls, observed, value) + val started = completed.sources.length() > state.sources.length() + val publishes = started and accepted(value) and state.capacity > 0 + and completed.sources.nth(completed.sources.length() - 1).localMs > 0 + if (publishes) { + localInsertedTicks: completed.localInsertedTicks.replaceAt(slot, state.ticks), + ...completed + } else completed + } +} diff --git a/formal/kernel/markers.qnt b/formal/kernel/markers.qnt index 50e97b8b..2066536d 100644 --- a/formal/kernel/markers.qnt +++ b/formal/kernel/markers.qnt @@ -30,7 +30,10 @@ module markers { // drivers read, the cutoff against the origin the drivers report cutoffs // from (an explicit argument, not the skew, which diverges from it after a // wall rollback) and the remaining lifetime, or NO_MARKER (Redis PTTL -2 on - // a missing key). One consumer today, recovery-read. + // a missing key). One consumer today, recovery-read, whose inputs do not + // roll back the wall clock. Marker expiry during delivered deadlines needs + // a kernel variant that expires markers inside deadlines::deliver's walk; + // applying expire after that walk would miss the intermediate instants. import cache_rules.* from "../cache-rules" import clock as Clock from "./clock" import remote_frames as Remote from "./remote-frames" diff --git a/formal/kernel/metrics.qnt b/formal/kernel/metrics.qnt index 12f1fc34..f23f8a89 100644 --- a/formal/kernel/metrics.qnt +++ b/formal/kernel/metrics.qnt @@ -74,6 +74,27 @@ module metrics { dumps: List[Writes::HeldDump], writes: List[Writes::HeldWrite] | r }, q] + // A profile's independent diagnostic property reads the raw acquisition + // facts beside the resulting event stream. This receipt makes no judgment + // about freshness or the miss category, and no transition consumes it. + type FrameReadReceipt = { frame: bool, owns: bool, stamp: int, wall: int, + freshMs: int, elapsed: int, due: int, missesBefore: int } + pure val NO_FRAME_READ_RECEIPT: FrameReadReceipt = { frame: false, owns: false, + stamp: 0, wall: 0, freshMs: 0, elapsed: 0, due: 0, missesBefore: 0 } + pure def frameReadReceipt(state: Metered[r, q], read: int): FrameReadReceipt = { + val held = RemoteIO::heldRead(state, read) + { + frame: state.reply == Replies::FRAME_WITH_MISS_FIELDS, + owns: held.flight != NO_OWNER, + stamp: state.replyAt, + wall: state.now + state.skew, + freshMs: held.ttls.freshMs, + elapsed: state.now, + due: if (held.flight == NO_OWNER) 0 else state.readStarted.nth(held.flight) + state.io.budgets.nth(read), + missesBefore: state.events.select(e => e.event == "miss").length() + } + } + pure def event(state: Metered[r, q], name: str, location: str, detail: str, amount: int): Metered[r, q] = { events: state.events.append({ event: name, location: location, detail: detail, amount: amount }), ...state } pure def remote(state: Metered[r, q], name: str, detail: str, amount: int): Metered[r, q] = diff --git a/formal/kernel/payloads.qnt b/formal/kernel/payloads.qnt index a31eceed..4658a1d4 100644 --- a/formal/kernel/payloads.qnt +++ b/formal/kernel/payloads.qnt @@ -42,12 +42,13 @@ module payloads { // value, so a class with no decoded value (UNSUPPORTED_ENCODING, or a class // this module names no rule for) yields NO_VALUE from `valueOf` and never // completes a caller with CALL_PENDING, the integer NO_VALUE shares. - // Documented limit: the atomic release - // (`serving::release`) has no decode step, so it serves `valueOf` of a - // fresh corrupt frame, the value its bytes would have carried; a profile - // composed over the atomic path must not seed decode-fault payloads - // (pinned by the payload-classes fixture, - // `atomicReleaseOfAFreshCorruptFrameServesWithoutADecodeStepTest`). + // Atomic-path limit: `serving::release` serves `valueOf` without a decode + // step, and `shadow::settleJob` compares through `valuesMatch` without a + // decode-failure arm. Profiles over either path schedule + // `atomicPathSeedsDecodableFrames = Payloads::atomicSafe(s)` so a future + // corrupt seed is refused by an invariant, rather than silently served or + // compared. The payload-classes fixture pins the underlying limit with + // `atomicReleaseOfAFreshCorruptFrameServesWithoutADecodeStepTest`. import encodings.* from "./encodings" // The classes, as hundreds: a payload in the band is a class plus a value @@ -85,6 +86,10 @@ module payloads { // The bytes fail to decode: a fresh decode restarts the loader, a recovery // decode keeps the loader's error. pure def decodeFails(payload: int): bool = classOf(payload) == classOf(CORRUPT) + // Atomic consumers have no decode-failure settlement; held consumers do. + // Read only the payload slots so this helper stays below remote_frames. + pure def atomicSafe(state: { remoteValues: List[int] | r }): bool = + state.remoteValues.indices().forall(key => not(decodeFails(state.remoteValues.nth(key)))) // The compression outcome a decode of the payload reports, none for an // uncompressed one. pure def decodeOutcome(payload: int): List[str] = diff --git a/formal/kernel/policy-overlay.qnt b/formal/kernel/policy-overlay.qnt index 8d3d8f6c..920fa7e0 100644 --- a/formal/kernel/policy-overlay.qnt +++ b/formal/kernel/policy-overlay.qnt @@ -11,6 +11,7 @@ module policy_overlay { // shape's (local, and remote where remote storage exists), and the traversal // gates each layer on its TTL at release (`layer_policy::gated`): a positive // reply TTL enables the layer, a zero one turns it off. + import cache_rules.* from "../cache-rules" import layer_policy.* from "./layer-policy" pure val INHERIT = 0 @@ -64,8 +65,11 @@ module policy_overlay { else if (base(state.policy) == RECOVERY_TWO_SECONDS) TWO_SECONDS_MS else if (base(state.policy) == REMOTE_FOUR_SECONDS_NO_RECOVERY) FOUR_SECONDS_MS else baseline.retentionMs - pure def ttls(state: Overlaid[r], baseline: Ttls): Ttls = - { localMs: localTtl(state, baseline), freshMs: remoteTtl(state, baseline), retentionMs: retention(state, baseline) } + pure def ttls(state: Overlaid[r], baseline: Ttls): Ttls = { + localMs: localTtl(state, baseline), + freshMs: remoteTtl(state, baseline), + retentionMs: remoteRetentionFor(remoteTtl(state, baseline), retention(state, baseline)) + } // The reply: the shape's layers (no request layer; the remote layer where // remote storage exists) and whether the call coalesces. pure def resolve(state: Overlaid[r]): Resolution = diff --git a/formal/kernel/recovery.qnt b/formal/kernel/recovery.qnt index fbb09b73..43bc5c74 100644 --- a/formal/kernel/recovery.qnt +++ b/formal/kernel/recovery.qnt @@ -41,6 +41,7 @@ module recovery { // (`remote_io::settleLoad`). A recovered value completes its flight without // publication. import cache_rules.* from "../cache-rules" + import cache_contract as Contract from "../cache-contract" import conformance_observations.* from "../conformance-observations" import encodings.* from "./encodings" import layer_policy.* from "./layer-policy" @@ -83,7 +84,7 @@ module recovery { { retained: state.retained.append({ flight: flight, candidate: if (stale and Payloads::valueOf(code) > NO_VALUE) code else NO_VALUE, - created: created, + created: Contract::retainedFrameStamp(created, Clock::wallOf(state)), maximum: ttls.retentionMs, classifier: classifier }), ...state } @@ -94,7 +95,7 @@ module recovery { pure def mayRecover(snapshot: Snapshot, timeout: bool): bool = snapshot.classifier == ALLOW or (snapshot.classifier == TIMEOUT_ONLY and timeout) pure def validCandidate(state: Recovering[r], snapshot: Snapshot): bool = - snapshot.candidate > NO_VALUE and recoveryAgeAllowed(snapshot.created, Clock::wallOf(state), snapshot.maximum) + snapshot.candidate > NO_VALUE and retainedCandidateAgeAllowed(snapshot.created, Clock::wallOf(state), snapshot.maximum) // The classification counted. pure def classified(state: Recovering[r], snapshot: Snapshot): Recovering[r] = { o: { classifications: state.o.classifications + (if (classifies(snapshot)) 1 else 0), ...state.o }, ...state } diff --git a/formal/kernel/remote-frames.qnt b/formal/kernel/remote-frames.qnt index 3556920e..9521f17b 100644 --- a/formal/kernel/remote-frames.qnt +++ b/formal/kernel/remote-frames.qnt @@ -85,7 +85,7 @@ module remote_frames { pure def fresh(state: Frames[r], key: int, freshMs: int): bool = freshAgeAllowed(state.created.nth(key), Clock::wallOf(state), freshMs) pure def fenceCleared(state: Frames[r], key: int, operationsPerEntity: int): bool = - not(state.tracked) or fenceAllows(state.created.nth(key), state.watermark.nth(entityOf(key, operationsPerEntity))) + readFenceAllows(state.tracked, state.created.nth(key), state.watermark.nth(entityOf(key, operationsPerEntity))) // A frame the adapter returns: present, retained and (tracked) fence-cleared, whatever its age. pure def visible(state: Frames[r], key: int, operationsPerEntity: int): bool = state.remoteValues.nth(key) > NO_VALUE and retained(state, key) and fenceCleared(state, key, operationsPerEntity) diff --git a/formal/kernel/serving.qnt b/formal/kernel/serving.qnt index cd3868dc..1bceb80f 100644 --- a/formal/kernel/serving.qnt +++ b/formal/kernel/serving.qnt @@ -30,6 +30,7 @@ module serving { // when a remote read completes (`authority`), completion apart from the // recorded outcome (`complete`, `finish`) and publication apart from // completion (`publish`, `settle`). The atomic path composes the same steps. + import cache_rules.* from "../cache-rules" import conformance_observations.* from "../conformance-observations" import encodings.* from "./encodings" import calls.* from "./calls" @@ -114,9 +115,9 @@ module serving { }, LocalPayload] type RequestServed[r] = Traversed[r, {}] - // A caller participates when its call is enabled and its scope is open. + // A caller participates when enabled, its key is valid and its scope is open. pure def participating(state: Traversed[r, p], layout: Layout, call: Call): bool = - call.enabled and Memo::scopeOpen(state, call.context, layout.persistentContexts) + call.enabled and not(call.keyFailed) and Memo::scopeOpen(state, call.context, layout.persistentContexts) // The process identity a call serves: its instance's row of keys. pure def identity(layout: Layout, call: Call): int = call.instance * layout.keysPerInstance + call.key // The projections of a reply: without the remote layer, or with the request leaf only. @@ -193,8 +194,12 @@ module serving { // participates (a positive effective TTL), whether it serves one caller at // release, completes a flight whose decode was held, or publishes a // settled source. - pure def warm(state: Served[r], layout: Layout, who: { instance: int, key: int | q }, value: int, localMs: int): Served[r] = + pure def warm(state: Local::Local[r], layout: Layout, who: { instance: int, key: int | q }, value: int, localMs: int): Local::Local[r] = if (localMs > 0) Local::putLocal(state, who.instance, who.key, value, localMs, layout.keysPerInstance) else state + // Only an accepted source value is a local publication. A rejected source + // leaves every previously published slot unchanged, in either traversal. + pure def publishLocal(state: Local::Local[r], layout: Layout, source: { instance: int, key: int, localMs: int | q }, value: int): Local::Local[r] = + if (accepted(value)) warm(state, layout, source, value, source.localMs) else state // Publication authority, fixed when a flight's remote read completes (at // release for an atomic read, at read settlement for a held one) from the // effective TTLs of the reply (`layer_policy::effectiveTtls`: a layer the @@ -216,7 +221,7 @@ module serving { { instance: who.instance, key: who.key, - localMs: if (not(state.tracked and read)) ttls.localMs else 0, + localMs: if (sourceMayWarmLocal(state.tracked, read)) ttls.localMs else 0, fence: if (refills) fence else NO_FENCE, retentionMs: if (refills) Remote::physicalRetention(state, ttls.retentionMs) else 0 } @@ -424,14 +429,17 @@ module serving { } pure def release(state: Served[r], layout: Layout, policyCall: int, resolved: Resolution, ttls: Ttls): Served[r] = releaseJudged(state, layout, layeredRelease(state, layout, policyCall, resolved, ttls), policyCall, ttls) - pure def releaseLocal(state: LocalServed[r], layout: Layout, policyCall: int, resolved: Resolution, ttls: Ttls): LocalServed[r] = { + // The local traversal accepts the value observed by its storage clock. + // Ordinary profiles use insertion expiry in milliseconds; the fractional + // profile supplies the same local value judged from its raw insertion ticks. + pure def releaseLocalObserved(state: LocalServed[r], layout: Layout, policyCall: int, resolved: Resolution, ttls: Ttls, observedLocal: int): LocalServed[r] = { val held = Gate::entry(state, policyCall) val caller = held.caller val call = held.call val released = Gate::release(state, policyCall) val open = participating(released, layout, call) val effective = if (open) localOnly(gated(resolved, ttls)) else BYPASS - val localValue = if (effective.layers.local) Local::localValue(released, call.instance, call.key, layout.keysPerInstance) else NO_VALUE + val localValue = if (effective.layers.local) observedLocal else NO_VALUE val decision = decide(released, layout, call, effective, localValue, NO_VALUE) val slot = decision.slot val attached = Flights::attachCaller(released, caller, ownerOf(decision.outcome), slot) @@ -446,6 +454,14 @@ module serving { } } + pure def releaseLocal(state: LocalServed[r], layout: Layout, policyCall: int, resolved: Resolution, ttls: Ttls): LocalServed[r] = { + val call = Gate::entry(state, policyCall).call + val localEnabled = participating(state, layout, call) and gated(resolved, ttls).layers.local + val observedLocal = if (localEnabled) + Local::localValue(state, call.instance, call.key, layout.keysPerInstance) else NO_VALUE + releaseLocalObserved(state, layout, policyCall, resolved, ttls, observedLocal) + } + // The request-only release, judged once for the traversal and for the // diagnostics recorded around it: the held caller and its call, whether its // scope is still open, the resolution in effect (the request-only projection @@ -505,6 +521,16 @@ module serving { if (begun.sources.length() > state.sources.length()) settleLocal(begun, layout, Flights::latest(begun), value) else begun } + // The same inline lifecycle with a value acquired under another local + // clock projection. It never uses that value unless local is enabled. + pure def callLocalObserved(state: LocalServed[r], layout: Layout, call: Call, resolved: Resolution, ttls: Ttls, observedLocal: int, value: int): LocalServed[r] = { + val admitted = admitLocal(state, layout, call) + val begun = if (participating(state, layout, call)) + releaseLocalObserved(admitted, layout, state.o.policyCalls, resolved, ttls, observedLocal) + else admitted + if (begun.sources.length() > state.sources.length()) settleLocal(begun, layout, Flights::latest(begun), value) else begun + } + // -- Settlement ---------------------------------------------------------------- // Every caller the source owns receives the value; an accepted value @@ -549,7 +575,7 @@ module serving { val dumped = { o: { dumps: state.o.dumps + (if (serializes) 1 else 0), ...state.o }, ...state } val dispatched = if (dispatches) dispatchWrite(dumped, source.retentionMs) else dumped val frames = if (stored) Remote::seedFrame(dispatched, source.key, value, source.retentionMs) else dispatched - if (accepted(value)) warm(frames, layout, source, value, source.localMs) else frames + publishLocal(frames, layout, source, value) } pure def settle(state: Served[r], layout: Layout, index: int, value: int): Served[r] = finish(publish(state, layout, index, value), layout, index, value) @@ -557,8 +583,7 @@ module serving { // its source captured, when it captured one. pure def settleLocal(state: LocalServed[r], layout: Layout, index: int, value: int): LocalServed[r] = { val source = state.sources.nth(index) - val warmed = if (accepted(value) and source.localMs > 0) - Local::putLocal(state, source.instance, source.key, value, source.localMs, layout.keysPerInstance) else state + val warmed = publishLocal(state, layout, source, value) finish(warmed, layout, index, value) } pure def settleRequest(state: RequestServed[r], layout: Layout, index: int, value: int): RequestServed[r] = diff --git a/formal/kernel/shadow-reads.qnt b/formal/kernel/shadow-reads.qnt new file mode 100644 index 00000000..e336a4bb --- /dev/null +++ b/formal/kernel/shadow-reads.qnt @@ -0,0 +1,81 @@ +module shadow_reads { + // A shadow Redis read has two completions (C54/C56): its bounded result + // ends at the read deadline, but the raw adapter operation retains the + // shadow slot until it settles. The whole-job deadline is independent. + // This variant composes the ordinary held dark lifecycle; profiles whose + // bounds never reach a shadow read deadline retain that simpler projection. + import conformance_observations.* from "../conformance-observations" + import calls.* from "./calls" + import layer_policy.* from "./layer-policy" + import serving as Serving from "./serving" + import remote_io as RemoteIO from "./remote-io" + import deadlines as Deadlines from "./deadlines" + import shadow as Shadow from "./shadow" + + // Distinct from caller READ expiry, which starts a fallback loader. + pure val SHADOW_READ = 3 + type ReadBudget = { source: int, budget: int } + type Bounded[r, q] = Shadow::DiagnosedShadowed[{ + expiredReads: List[int], capturedReadBudgets: List[ReadBudget] | r + }, q] + pure def budgetFor(state: Bounded[r, q], source: int): int = + state.capturedReadBudgets.select(owned => owned.source == source).nth(0).budget + + // Record only a read actually dispatched by the composed transition. Its + // deadline captures the budget at that dispatch, independently of policy. + pure def registerRead(before: Bounded[r, q], after: Bounded[r, q], budget: int): Bounded[r, q] = + if (after.o.reads == before.o.reads) after + else Deadlines::due(after, SHADOW_READ, before.o.reads, budget) + + pure def begin(state: Bounded[r, q], layout: Serving::Layout, call: Call, resolved: Resolution, ttls: Ttls, + budget: int, shadowing: Shadow::Shadowing): Bounded[r, q] = { + val started = Shadow::beginDark(state, layout, call, resolved, ttls, budget, shadowing, Shadow::WELL_FORMED, 0) + val captured = if (started.jobs.length() == state.jobs.length()) started else { + capturedReadBudgets: started.capturedReadBudgets.append({ source: state.sources.length(), budget: budget }), ...started + } + registerRead(state, captured, budget) + } + + pure def releaseLoad(state: Bounded[r, q], layout: Serving::Layout, load: int, failed: bool, + comparison: Shadow::Comparison): Bounded[r, q] = { + val budget = budgetFor(state, Shadow::decoderOf(state, load).source) + registerRead(state, Shadow::releaseLoad(state, layout, load, failed, comparison, budget), budget) + } + + // A bounded read error completes the validation operation, hence its job + // timer is forgotten. The raw read and the registration remain. If the job + // already timed out, that earlier outcome is the only one reported. + pure def finishReadResult(state: Bounded[r, q], read: int, job: Shadow::Job): Bounded[r, q] = + Deadlines::forget({ + expiredReads: state.expiredReads.append(read), + o: { shadow: if (job.timedOut) state.o.shadow else state.o.shadow.append( + if (job.phase == Shadow::CONFIRMING) "confirmation_error" else "redis_error"), ...state.o }, + ...state + }, Deadlines::JOB, job.source) + + pure def expireRead(state: Bounded[r, q], read: int): Bounded[r, q] = { + val job = Shadow::readerOf(state, read) + val aborted = { io: { aborted: state.io.aborted.append(read), ...state.io }, ...state } + finishReadResult(aborted, read, job) + } + + // Timers are delivered by instant, preserving their registration order. + // Other work keeps the diagnosed source and whole-job expiry semantics. + pure def expire(state: Bounded[r, q], layout: Serving::Layout, due: Deadlines::Due): Bounded[r, q] = + if (due.kind == SHADOW_READ) expireRead(state, due.index) + else Shadow::expireHeld(state, layout, due) + pure def advance(state: Bounded[r, q], layout: Serving::Layout, ms: int): Bounded[r, q] = + Deadlines::deliver(state, ms, (current, due) => expire(current, layout, due)) + + // A raw reply after its bounded result failed is cleanup only: it cannot + // acquire a frame, sample a future offset, decode, fill or emit a verdict. + pure def releaseRead(state: Bounded[r, q], layout: Serving::Layout, read: int, failed: bool): Bounded[r, q] = { + val forgotten = Deadlines::forget(state, SHADOW_READ, read) + if (state.expiredReads.select(expired => expired == read).length() > 0) Shadow::retireJob({ + reads: forgotten.reads.select(other => other.read != read), + expiredReads: forgotten.expiredReads.select(other => other != read), + ...forgotten + }, RemoteIO::heldRead(state, read).flight) + else Shadow::releaseRead(forgotten, layout, read, failed) + } +} diff --git a/formal/kernel/shadow.qnt b/formal/kernel/shadow.qnt index cd119537..f1ef6264 100644 --- a/formal/kernel/shadow.qnt +++ b/formal/kernel/shadow.qnt @@ -115,6 +115,10 @@ module shadow { // (`expireHeld`, through `settleJobHeld`); a job still holding its C0 read // then ends the same way when the read settles; a job budget due in the // same round finds the job gone and says nothing more. + // `held-dark-shadow.qnt` pins a source deadline below its job budget with + // `sourceDeadlineAloneEndsTheJobTest`. That arm is fixture-only: both native + // drivers derive both budgets from the call's fallback timeout; replaying + // separate budgets would require a per-call sourceTimeoutMs input. // // Cohort selection is the drivers' ramp decoded by the profile through // `cohort_boundaries` (strict against the key's numerator); the module reads @@ -285,15 +289,18 @@ module shadow { // -- Settlement ------------------------------------------------------------------------ + // Raw work has drained: remove its registration and any remaining budget. + // A bounded read can have reported its result before reaching this point. + pure def retireJob(state: Jobs[r], source: int): Jobs[r] = + Deadlines::forget({ jobs: state.jobs.select(other => other.source != source), ...state }, Deadlines::JOB, source) // A job ends with its outcome, its record and deadline gone; a timed-out // job said so already and adds nothing. pure def finishJob(state: Jobs[r], source: int, outcome: str): Jobs[r] = { val job = jobOf(state, source) - Deadlines::forget({ - jobs: state.jobs.select(other => other.source != source), + retireJob({ o: { shadow: if (job.timedOut) state.o.shadow else state.o.shadow.append(outcome), ...state.o }, ...state - }, Deadlines::JOB, source) + }, source) } // The label a failed source gives its job: the caller's deadline error is // the job's `timeout` (the implementations race the caller's bounded diff --git a/formal/lint-profiles.mjs b/formal/lint-profiles.mjs index 9fdf5580..f9b51a53 100644 --- a/formal/lint-profiles.mjs +++ b/formal/lint-profiles.mjs @@ -3,7 +3,7 @@ // evidence guarantee 6). `quint parse --out` yields the modules with their // declarations and a lookup table that resolves every name and operator // application to the declaration it refers to, across imports and instances. -// The lint builds the reference graph from that table and checks two rules: +// The lint builds the reference graph from that table and checks three rules: // // - Composition: from each action of the profile module, every value assigned // to a state variable other than the driver input is built from kernel @@ -30,6 +30,10 @@ // monitor's own assignment may read its prior state, so the value of an // assignment to a witness variable is not walked. // +// - Supported shapes: fields that imply held work or local storage faults +// require transitions that define that combination. A record type carrying +// a field alone does not mean a row-polymorphic transition reads it. +// // Quint IR facts the walk relies on (verified against Quint 0.32.0): a // parametrized definition is a `def` whose `expr` is a `lambda`; a declared // type is the declaration's `typeAnnotation`, an `oper` whose `args` are the @@ -285,6 +289,12 @@ export function lintComposition(index, { kernelModules = [] } = {}) { const reachable = new Set(); const assigning = new Set(); const transitions = new Set(); + const transitionsByVariable = new Map(); + const recordTransition = (variable, label) => { + transitions.add(label); + if (!transitionsByVariable.has(variable)) transitionsByVariable.set(variable, new Set()); + transitionsByVariable.get(variable).add(label); + }; const isState = declaration => declaration?.kind === 'var'; const isKernel = declaration => (declaration?.kind === 'def' || declaration?.kind === 'const') && kernel.has(declaration.module); const isProfile = declaration => declaration?.kind === 'def' && declaration.module === index.main; @@ -338,14 +348,17 @@ export function lintComposition(index, { kernelModules = [] } = {}) { const higherOrderLabel = declaration => higherOrder.has(declaration.id) ? index.labelOf(declaration.module, declaration.name) : undefined; // Walks one expression; returns whether its value carries cache state. // `tainted` maps parameter and let names of the walked body to state taint. - const walk = (expr, { node, chain, tainted, variable }) => { - const again = child => walk(child, { node, chain, tainted, variable }); + const walk = (expr, { node, chain, tainted, variable, libraries = new Map() }) => { + const again = child => walk(child, { node, chain, tainted, variable, libraries }); switch (expr.kind) { case 'int': case 'str': case 'bool': return false; case 'name': { const declaration = resolveTarget(index, expr); if (isState(declaration)) return true; - if (declaration === undefined) return tainted.get(expr.name) === true; + if (declaration === undefined) { + for (const label of libraries.get(expr.name) ?? []) recordTransition(variable, label); + return tainted.get(expr.name) === true; + } if (declaration.kind === 'const') return false; // A chosen input is never cache state, whatever domain it was drawn from. if (declaration.qualifier === 'nondet') return false; @@ -359,14 +372,16 @@ export function lintComposition(index, { kernelModules = [] } = {}) { } case 'lambda': { const inner = new Map(tainted); - for (const parameter of expr.params) inner.set(parameter.name, false); - return walk(expr.expr, { node, chain, tainted: inner, variable }); + const scoped = new Map(libraries); + for (const parameter of expr.params) { inner.set(parameter.name, false); scoped.delete(parameter.name); } + return walk(expr.expr, { node, chain, tainted: inner, variable, libraries: scoped }); } case 'let': { const inner = new Map(tainted); inner.set(expr.opdef.name, expr.opdef.qualifier === 'nondet' ? false : again(expr.opdef.expr)); // (the name case answers the same for a nondet reached without this map) - return walk(expr.expr, { node, chain, tainted: inner, variable }); + const scoped = new Map(libraries); scoped.delete(expr.opdef.name); + return walk(expr.expr, { node, chain, tainted: inner, variable, libraries: scoped }); } case 'app': break; default: return false; @@ -380,7 +395,7 @@ export function lintComposition(index, { kernelModules = [] } = {}) { const kernelDefinition = kernelCallee(declaration); if (kernelDefinition) { const label = index.labelOf(kernelDefinition.module, kernelDefinition.name); - if (kernelDefinition.kind === 'def') transitions.add(label); + if (kernelDefinition.kind === 'def') recordTransition(variable, label); if (higherOrderLabel(kernelDefinition)) report(node, chain, `higher-order ${label} instantiated in the value of ${variable}`); return true; } @@ -416,6 +431,7 @@ export function lintComposition(index, { kernelModules = [] } = {}) { // argument is the wrapper's), and the taint it carries reaches the callee's // parameter at every call site before the callee's assignments are judged. const parameterTaint = new Map(); + const parameterLibraries = new Map(); const definitions = topLevelDefinitions(index).filter(node => node.module === index.main); for (const caller of definitions) { const own = caller.expr?.kind === 'lambda' ? new Map(caller.expr.params.map(parameter => [parameter.name, false])) : new Map(); @@ -426,13 +442,17 @@ export function lintComposition(index, { kernelModules = [] } = {}) { const callee = index.nodes.get(declaration.owner); if (!callee || callee.expr?.kind !== 'lambda') continue; const taints = parameterTaint.get(callee.key) ?? new Array(callee.expr.params.length).fill(false); + const carried = parameterLibraries.get(callee.key) ?? callee.expr.params.map(() => new Set()); expr.args.forEach((argument, position) => { // An argument that itself assigns state is a transition fragment the // caller's own assignments cover; a value argument is walked here. if (hasEffects(index, argument)) return; - if (walk(argument, { node: caller, chain: [caller.label], tainted: own, variable: `the argument ${callee.expr.params[position]?.name} of ${callee.label}` })) taints[position] = true; + const variable = `the argument ${callee.expr.params[position]?.name} of ${callee.label}`; + if (walk(argument, { node: caller, chain: [caller.label], tainted: own, variable })) taints[position] = true; + for (const label of transitionsByVariable.get(variable) ?? []) carried[position].add(label); }); parameterTaint.set(callee.key, taints); + parameterLibraries.set(callee.key, carried); } } for (const action of actions) { @@ -451,7 +471,9 @@ export function lintComposition(index, { kernelModules = [] } = {}) { if (variable.name === inputField) continue; const taints = parameterTaint.get(node.key) ?? []; const parameters = node.expr?.kind === 'lambda' ? node.expr.params.map((parameter, position) => [parameter.name, taints[position] === true]) : []; - walk(value, { node, chain, tainted: new Map(parameters), variable: variableLabel(index, variable) }); + const libraries = node.expr?.kind === 'lambda' ? new Map(node.expr.params.map((parameter, position) => + [parameter.name, parameterLibraries.get(node.key)?.[position] ?? new Set()])) : new Map(); + walk(value, { node, chain, tainted: new Map(parameters), variable: variableLabel(index, variable), libraries }); } for (const next of referencesOf(index, node)) { if (!parent.has(next.key)) { parent.set(next.key, node.key); queue.push(next); } @@ -463,6 +485,11 @@ export function lintComposition(index, { kernelModules = [] } = {}) { return { kernelModules: [...kernel].sort(compareStrings), actions: actions.map(node => node.name).sort(compareStrings), publicActions: exposed.map(node => node.name), reachableDefinitions: reachable.size, stateAssigningDefinitions: [...assigning].sort(compareStrings), libraryTransitions: [...transitions].sort(compareStrings), + stateTransitions: Object.fromEntries([...index.declarations.values()].filter(declaration => isState(declaration) + && declaration.module === index.main && declaration.name !== inputField).map(declaration => { + const variable = variableLabel(index, declaration); + return [variable, [...transitionsByVariable.get(variable) ?? []].sort(compareStrings)]; + })), count: sorted.length, violations: sorted }; } @@ -630,7 +657,69 @@ export async function lintModel(model, { main, kernelModules, witnessPattern, ob const index = indexModules(parsed, { main }); const composition = lintComposition(index, { kernelModules: kernelModules ?? kernelModulesOf(cwd) }); const witnessIsolation = lintWitnessIsolation(index, { witnessPattern, observationField }); - return { model, main: index.main, modules: index.modules, tableSize: index.tableSize, composition, witnessIsolation }; + const shapes = lintShapes(index, composition.stateTransitions); + return { model, main: index.main, modules: index.modules, tableSize: index.tableSize, composition, witnessIsolation, shapes }; +} + +// A row-polymorphic transition can carry fields it never reads. These two +// restrictions reject combinations whose lifecycle the selected transitions +// do not model, even when the ordinary composition walk sees only pure calls. +// Admission uses the served-shadow shape, whose refill slots stay empty; its +// beginHeld/settleLoader family is valid here without admitting source budgets. +export const shapeRules = [ + { id: 'held-effects', combinations: [ + { fields: ['reads', 'dumps'], requires: ['shadow::beginDark', 'shadow::settleHeld', 'shadow::beginHeld', + 'shadow::releaseHeld', 'shadow::settleLoader', 'remote_writes::settleLoader', 'metrics::begin', 'metrics::settleLoader'] }, + { fields: ['jobs', 'sourceBudget'], requires: ['shadow::beginDark', 'shadow::settleHeld'], + forbids: ['shadow::begin', 'shadow::release', 'shadow::settle', 'shadow::settleJob', 'shadow::advance', + 'shadow::beginHeld', 'shadow::releaseHeld', 'shadow::settleRead', 'shadow::settleLoad', 'shadow::settleLoader'] }, + ] }, + { id: 'local-fault', combinations: [ + { fields: ['localFailed'], requires: ['local_faults::begin', 'local_faults::settle'], + forbids: ['remote_io::*', 'remote_writes::*', 'deadlines::*', 'diagnostics::*', 'shadow::*', 'metrics::*', + 'compression::*', 'adapter_replies::settleRead'] }, + ] }, +]; + +export function lintShapes(index, stateTransitions) { + const fieldsOf = (type, bindings = new Map(), seen = new Set()) => { + if (!type || (type.id !== undefined && seen.has(type.id))) return []; + const trail = type.id === undefined ? seen : new Set([...seen, type.id]); + if (type.kind === 'var') { + const bound = bindings.get(type.name); + return bound ? fieldsOf(bound.type, bound.bindings, trail) : []; + } + if (type.kind === 'const') return fieldsOf(index.table[type.id]?.type, bindings, trail); + if (type.kind === 'app') { + const target = index.table[type.ctor.id]; + const applied = new Map((target?.params ?? []).map((name, i) => [name, { type: type.args[i], bindings }])); + return fieldsOf(target?.type, applied, trail); + } + if (type.kind === 'rec') return fieldsOf(type.fields, bindings, trail); + if (type.kind === 'row') return [...type.fields.map(field => field.fieldName), ...fieldsOf(type.other, bindings, trail)]; + return []; + }; + const states = [...index.declarations.values()].filter(declaration => + declaration.module === index.main && declaration.kind === 'var' && declaration.name !== inputField) + .map(declaration => ({ variable: declaration.name, fields: [...new Set(fieldsOf(declaration.typeAnnotation))].sort(compareStrings), + transitions: stateTransitions[variableLabel(index, declaration)] ?? [] })); + const matches = (pattern, transition) => pattern.endsWith('::*') ? transition.startsWith(pattern.slice(0, -1)) : pattern === transition; + const violations = []; + for (const state of states) { + for (const rule of shapeRules) { + for (const combination of rule.combinations) { + if (!combination.fields.every(field => state.fields.includes(field))) continue; + if (!combination.requires.some(transition => state.transitions.includes(transition))) { + violations.push({ rule: rule.id, variable: state.variable, fields: combination.fields, + detail: `fields ${combination.fields.join(', ')} require one of ${combination.requires.join(', ')}` }); + } + const forbidden = state.transitions.filter(transition => combination.forbids?.some(pattern => matches(pattern, transition))); + if (forbidden.length) violations.push({ rule: rule.id, variable: state.variable, fields: combination.fields, + detail: `fields ${combination.fields.join(', ')} cannot compose ${forbidden.join(', ')}` }); + } + } + } + return { states, count: violations.length, violations }; } export function loadProfiles(directory = root) { @@ -651,6 +740,8 @@ export async function computeBaseline({ profiles, cwd = root, concurrency } = {} const parsed = await parseModel(profile.model, { cwd }); const index = indexModules(parsed); const composition = lintComposition(index, { kernelModules }); + const shapes = lintShapes(index, composition.stateTransitions); + if (shapes.count) throw new Error(`${profile.id}: unsupported kernel shape\n${shapes.violations.map(violation => ` ${violation.variable}: ${violation.detail}`).join('\n')}`); return { id: profile.id, model: profile.model, module: index.main, libraryTransitions: composition.libraryTransitions, compositionViolations: composition.count }; }), concurrency === undefined ? {} : { concurrency }); return { schemaVersion: 3, quintVersion: version, kernelModules, profiles: entries }; @@ -733,7 +824,7 @@ const usage = `Usage: node formal/lint-profiles.mjs [--main=] [--kernel=] [--witness=] [--observation=] node formal/lint-profiles.mjs baseline --check | --write -The first form prints a JSON report and exits 1 when either rule is violated; +The first form prints a JSON report and exits 1 when any lint rule is violated; --kernel defaults to the modules under formal/kernel. The second recomputes ${baselinePath} over every profile in formal/profiles.json and either checks it against the committed file (--check: library transitions and violation @@ -784,7 +875,7 @@ async function main(argv) { observationField: typeof options.observation === 'string' ? options.observation : defaultObservationField, }); console.log(JSON.stringify(report, null, 2)); - return report.composition.count || report.witnessIsolation.count ? 1 : 0; + return report.composition.count || report.witnessIsolation.count || report.shapes.count ? 1 : 0; } if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { diff --git a/formal/measure-go-semantics.mjs b/formal/measure-go-semantics.mjs index bd86a7de..e9ea7e7a 100644 --- a/formal/measure-go-semantics.mjs +++ b/formal/measure-go-semantics.mjs @@ -1,11 +1,13 @@ +import { startRedisVectorServer } from './redis-vector-server.mjs'; import { createHash } from 'node:crypto'; import { spawnSync } from 'node:child_process'; import { cpSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'; import { resolve, relative } from 'node:path'; import { tmpdir } from 'node:os'; import { fileURLToPath } from 'node:url'; -import { checkMutantAnchors, mutantsForPort, readMutantCatalog } from './execution.mjs'; -import { classifyCohort, fingerprintFiles, finishPartial, gateDetections, languages, noncompilingResult, portableCohort, selectMutations, selectionDirectory, selectionFromArguments } from './mutation-reports.mjs'; +import { boundaryEvidence, checkMutantAnchors, mutantsForPort, readMutantCatalog } from './execution.mjs'; +import { assessBoundary, classifyCohort, fingerprintFiles, finishPartial, gateDetections, languages, noncompilingResult, portableCohort, selectMutations, selectionDirectory, selectionFromArguments } from './mutation-reports.mjs'; +import { boundaryBaselines, boundaryTrace, mutationBoundaries, runBoundaryReplay } from './boundary-replay.mjs'; import { settlementViolationPattern } from './replay/settlement.mjs'; // The whole output line that carries a settlement violation. Anchored per @@ -16,7 +18,7 @@ const root = fileURLToPath(new URL('../', import.meta.url)); const hash = bytes => createHash('sha256').update(bytes).digest('hex'); const json = file => JSON.parse(readFileSync(file, 'utf8')); const protocolNames = ['TestProtocolKeys', 'TestProtocolFrames', 'TestProtocolDecoders', 'TestProtocolCohorts', 'TestProtocolRemainingVectors']; -const generatedNames = ['TestCoreConformance', 'TestEffectsConformance', 'TestFeatureConformance', 'TestLocalClockConformance', 'TestGeneratedWitnessEvidence', ...protocolNames]; +const generatedNames = ['TestGeneratedInvalidationVectors', 'TestCoreConformance', 'TestEffectsConformance', 'TestFeatureConformance', 'TestLocalClockConformance', 'TestGeneratedWitnessEvidence', ...protocolNames]; const fixedNames = ['TestBehaviorConformance', ...protocolNames]; const infrastructureTestFile = /(?:replay|driver|coordinator|protocol|profile|registry|witness_evidence|integration)_test\.go$/; @@ -55,6 +57,7 @@ export function causalPropertyAssertion(output) { // completion, or failing corpus audit is not an assertion-based detection. export function evaluateGoTestEvents(lines, exitCode) { const events = lines.trim().split('\n').filter(Boolean).map(line => JSON.parse(line)); + if (lines.includes('INVALIDATION_INFRASTRUCTURE:')) throw new Error('Redis vector infrastructure failure, not detection'); if (!events.length) throw new Error('empty Go test event stream'); const outputs = new Map(), tests = new Map(), packages = []; const append = (name, value) => outputs.set(name, (outputs.get(name) ?? '') + value); @@ -157,6 +160,7 @@ export function measureGoSemantics({ shard = { index: 1, count: 1 }, only } = {} // run 34666226055 had not finished it after 150 s). One timeout aborts the // whole measurement, so a generous bound costs at most one wait. const timeout = 540_000; + let vectorServer; try { // Copies preserve repo-relative witness definition paths while mutations // remain completely outside the shared checkout. No git resets or writes @@ -172,6 +176,7 @@ export function measureGoSemantics({ shard = { index: 1, count: 1 }, only } = {} const originals = checkMutantAnchors(mutantCatalog, readWorkspace); const catalog = { mutations: mutantsForPort(mutantCatalog, language.port) }; const selected = selectMutations(catalog.mutations, { shard, only }); + const evidence = boundaryEvidence().filter(entry => selected.some(mutation => mutation.id === entry.mutant)); if (!only && shard.count > 1) report.shard = { index: shard.index, count: shard.count, mutationIds: selected.map(m => m.id) }; const ordinaryFiles = readdirSync(moduleDirectory).filter(file => file.endsWith('_test.go') && !infrastructureTestFile.test(file)).sort(); const ordinary = ordinaryFiles.flatMap(file => [...readFileSync(resolve(moduleDirectory, file), 'utf8').matchAll(/^func (Test\w+)\(t \*testing\.T\)/gm)].map(match => match[1])); @@ -186,6 +191,14 @@ export function measureGoSemantics({ shard = { index: 1, count: 1 }, only } = {} DIALCACHE_FEATURE_TRACE_DIR: resolve(root, '.formal-traces/features'), DIALCACHE_WITNESS_EVIDENCE_DIR: witnessDirectory, }); + vectorServer = startRedisVectorServer(); + env.DIALCACHE_VECTOR_REDIS_URL = vectorServer.url; + report.redisVectorImage = vectorServer.image; + const replayBoundary = (label, history) => runBoundaryReplay({ + port: language.port, history, label, output, root, workspace, env, go, + }); + const boundaries = (id, replay = history => replayBoundary(id, history)) => + mutationBoundaries(evidence.filter(entry => entry.mutant === id), replay, assessBoundary); report.revision = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' }).stdout.trim(); report.go = spawnSync(go, ['version'], { cwd: moduleDirectory, encoding: 'utf8' }).stdout?.trim(); report.node = process.version; @@ -204,7 +217,9 @@ export function measureGoSemantics({ shard = { index: 1, count: 1 }, only } = {} if (result.status !== 0) throw new Error(`${label}: noncompiling mutant/baseline, not detection; see compile log`); }; const run = (label, cohort, baseline) => { - const result = spawnSync(go, ['test', '-json', '-count=1', '-timeout=480s', '-run', `^(${cohorts[cohort].join('|')})$`, '.'], { + // Faults can make instance state process-global. Keep independent + // synctest histories isolated; separate mutation shards still parallelize. + const result = spawnSync(go, ['test', '-json', '-count=1', '-parallel=1', '-timeout=480s', '-run', `^(${cohorts[cohort].join('|')})$`, '.'], { cwd: moduleDirectory, env: { ...env, DIALCACHE_PROTOCOL_CORPUS: cohort === 'generated' ? 'generated' : 'fixed' }, encoding: 'utf8', timeout, maxBuffer: 128 * 1024 * 1024, }); writeFileSync(resolve(output, `${label}-${cohort}.jsonl`), result.stdout ?? ''); @@ -237,6 +252,8 @@ export function measureGoSemantics({ shard = { index: 1, count: 1 }, only } = {} console.log(`baseline ${cohort}: ${report.baselines[cohort].passed} passing leaf tests`); save(); } report.baselines.portable = portableCohort(report.baselines.generated, report.baselines.fixed); + report.boundaryBaselines = boundaryBaselines(evidence, history => replayBoundary('baseline', history)); + save(); for (const mutation of selected) { const editedPaths = new Set(); try { @@ -249,13 +266,17 @@ export function measureGoSemantics({ shard = { index: 1, count: 1 }, only } = {} try { compile(mutation.id); } catch (error) { // Recorded, never measured: the gate names the mutant while the rest // of the shard is still measured. - report.mutations.push(noncompilingResult(mutation, [...Object.keys(cohorts), 'portable'], error.message)); + const result = noncompilingResult(mutation, [...Object.keys(cohorts), 'portable'], error.message); + result.boundary = boundaries(mutation.id, history => ({ path: boundaryTrace(history, resolve(root, '.formal-traces')).path, + completed: false, lastStep: -1, divergences: [], error: error.message })); + report.mutations.push(result); console.log(`${mutation.id}: noncompiling`); save(); continue; } const result = { id: mutation.id, case: mutation.case, description: mutation.description, cohorts: {} }; for (const cohort of Object.keys(cohorts)) result.cohorts[cohort] = run(mutation.id, cohort, false); result.cohorts.portable = portableCohort(result.cohorts.generated, result.cohorts.fixed); + result.boundary = boundaries(mutation.id); report.mutations.push(result); console.log(`${mutation.id}: ${Object.entries(result.cohorts).map(([name, value]) => `${name}=${value.state}(${value.failed})`).join(', ')}`); save(); } finally { for (const path of editedPaths) writeFileSync(resolve(workspace, path), originals.get(path)); } @@ -274,7 +295,10 @@ export function measureGoSemantics({ shard = { index: 1, count: 1 }, only } = {} writeFileSync(resolve(output, 'report.md'), language.markdown(report)); return report; } catch (error) { report.error = String(error); save(); throw error; } - finally { rmSync(workspace, { recursive: true, force: true }); } + finally { + rmSync(workspace, { recursive: true, force: true }); + vectorServer?.close(); + } } if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { try { measureGoSemantics(selectionFromArguments(process.argv.slice(2))); } catch (error) { console.error(error); process.exitCode = 1; } diff --git a/formal/measure-semantics.mjs b/formal/measure-semantics.mjs index e9f12e6a..790e6d37 100644 --- a/formal/measure-semantics.mjs +++ b/formal/measure-semantics.mjs @@ -1,3 +1,4 @@ +import { startRedisVectorServer } from './redis-vector-server.mjs'; import { createHash } from 'node:crypto'; import { spawnSync } from 'node:child_process'; import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'; @@ -6,8 +7,9 @@ import { tmpdir } from 'node:os'; import { fileURLToPath } from 'node:url'; import { checkSemanticCoverage } from './check-semantic-coverage.mjs'; import { evaluateSemanticTestReport } from './semantic-reporter.mjs'; -import { checkMutantAnchors, mutantsForPort, readMutantCatalog } from './execution.mjs'; -import { classifyCohort, fingerprintFiles, finishPartial, gateDetections, languages, noncompilingResult, portableCohort, selectMutations, selectionDirectory, selectionFromArguments } from './mutation-reports.mjs'; +import { boundaryEvidence, checkMutantAnchors, mutantsForPort, readMutantCatalog } from './execution.mjs'; +import { assessBoundary, classifyCohort, fingerprintFiles, finishPartial, gateDetections, languages, noncompilingResult, portableCohort, selectMutations, selectionDirectory, selectionFromArguments } from './mutation-reports.mjs'; +import { boundaryBaselines, boundaryTrace, mutationBoundaries, runBoundaryReplay } from './boundary-replay.mjs'; const root = fileURLToPath(new URL('../', import.meta.url)); const language = languages.ts; @@ -41,9 +43,9 @@ if (only) { const declaredCoverage = checkSemanticCoverage(); const mutantCatalog = readMutantCatalog(); const catalog = { mutations: mutantsForPort(mutantCatalog, language.port) }; -const formalTests = ['test/formal-conformance.test.ts', 'test/formal-effects.test.ts', 'test/formal-features.test.ts', 'test/formal-local-clock.test.ts', 'test/formal-protocol-vectors.test.ts']; +const formalTests = ['test/formal-invalidation-native.test.ts', 'test/formal-conformance.test.ts', 'test/formal-effects.test.ts', 'test/formal-features.test.ts', 'test/formal-local-clock.test.ts', 'test/formal-protocol-vectors.test.ts']; const portableTests = ['test/formal-behavior.test.ts', 'test/formal-protocol-vectors.test.ts']; -const generatedPattern = 'replays |reaches every action|covers every action|reaches fractional expiry and shared instance grid|formal protocol conformance vectors (?!keeps |requires )'; +const generatedPattern = 'generated invalidation vectors |replays |reaches every action|covers every action|reaches fractional expiry and shared instance grid|formal protocol conformance vectors (?!keeps |requires )'; // Fixed scenario names carry a feature prefix. Protocol schema/audit checks // start with "keeps"/"requires" and must not count as behavioral detections. const portablePattern = 'portable behavioral scenarios [\\w-]+: |formal protocol conformance vectors (?!keeps |requires )'; @@ -60,6 +62,7 @@ const cohorts = { // run; the originals restore the workspace after each mutant. const sourceText = checkMutantAnchors(mutantCatalog); const selected = selectMutations(catalog.mutations, { shard, only }); +const evidence = boundaryEvidence().filter(entry => selected.some(mutation => mutation.id === entry.mutant)); // A hard CI cancellation may bypass finally. Keep temporary dependency links // outside the artifact tree even when that happens. const workspace = mkdtempSync(resolve(tmpdir(), 'dialcache-semantic-')); @@ -86,6 +89,11 @@ Object.assign(env, { DIALCACHE_EFFECTS_TRACE_DIR: resolve(root, '.formal-traces/effects'), DIALCACHE_FEATURE_TRACE_DIR: resolve(root, '.formal-traces/features'), }); +const replayBoundary = (label, history) => runBoundaryReplay({ + port: language.port, history, label, output, root, workspace, env, +}); +const boundaries = (id, replay = history => replayBoundary(id, history)) => + mutationBoundaries(evidence.filter(entry => entry.mutant === id), replay, assessBoundary); function run(label, cohort, baseline) { const json = resolve(output, `${label}-${cohort}.json`); const meta = resolve(output, `${label}-${cohort}.meta.json`); @@ -116,7 +124,11 @@ function save() { writeFileSync(resolve(output, 'report.json'), JSON.stringify(report, null, 2) + '\n'); } save(); +let vectorServer; try { + vectorServer = startRedisVectorServer(); + env.DIALCACHE_VECTOR_REDIS_URL = vectorServer.url; + report.redisVectorImage = vectorServer.image; for (const path of ['src', 'test', 'formal', 'docs', 'README.md', 'go/README.md', 'package.json', 'tsconfig.json', 'vitest.config.ts']) { cpSync(resolve(root, path), resolve(workspace, path), { recursive: true, filter: source => !source.includes('/docs/.vitepress/cache') && !source.includes('/docs/.vitepress/dist') }); } @@ -133,6 +145,10 @@ try { save(); } report.baselines.portable = portableCohort(report.baselines.generated, report.baselines.fixed); + // These targeted runs have their own baseline and result records. They are + // not cohorts, and repeated challenge citations replay a history only once. + report.boundaryBaselines = boundaryBaselines(evidence, history => replayBoundary('baseline', history)); + save(); // The shared language-neutral evaluator produces the baseline witness // evidence over the unmodified corpus; the TypeScript suite only checks the gate. const evaluated = spawnSync(process.execPath, [resolve(root, 'formal/witnesses.mjs'), 'evaluate', '--profile', 'all', '--out', resolve(output, 'witnesses')], @@ -164,7 +180,11 @@ try { // Recorded, never measured: the gate names the mutant while the rest of // the shard is still measured. writeFileSync(resolve(output, `${mutation.id}-compile.log`), (compile.stdout ?? '') + (compile.stderr ?? '')); - report.mutations.push(noncompilingResult(mutation, [...Object.keys(cohorts), 'portable'], `${mutation.id}: noncompiling mutant; see ${mutation.id}-compile.log`)); + const reason = `${mutation.id}: noncompiling mutant; see ${mutation.id}-compile.log`; + const result = noncompilingResult(mutation, [...Object.keys(cohorts), 'portable'], reason); + result.boundary = boundaries(mutation.id, history => ({ path: boundaryTrace(history, resolve(root, '.formal-traces')).path, + completed: false, lastStep: -1, divergences: [], error: reason })); + report.mutations.push(result); console.log(`${mutation.id}: noncompiling`); save(); continue; @@ -172,6 +192,7 @@ try { const result = { id: mutation.id, case: mutation.case, description: mutation.description, cohorts: {} }; for (const cohort of Object.keys(cohorts)) result.cohorts[cohort] = run(mutation.id, cohort, false); result.cohorts.portable = portableCohort(result.cohorts.generated, result.cohorts.fixed); + result.boundary = boundaries(mutation.id); report.mutations.push(result); console.log(`${mutation.id}: ${Object.entries(result.cohorts).map(([name, run]) => `${name}=${run.state}`).join(', ')}`); save(); @@ -199,4 +220,5 @@ try { } finally { // Only this run's isolated copy is removed. The user's source is never edited. rmSync(workspace, { recursive: true, force: true }); + vectorServer?.close(); } diff --git a/formal/merge-mutation-reports.mjs b/formal/merge-mutation-reports.mjs index 39a29344..f21704f0 100644 --- a/formal/merge-mutation-reports.mjs +++ b/formal/merge-mutation-reports.mjs @@ -8,7 +8,7 @@ const root = fileURLToPath(new URL('../', import.meta.url)); // Fields a shard legitimately owns or that the merge recomputes. Every other // field describes the measured inputs and must be identical across shards. -const perShardFields = new Set(['complete', 'shard', 'startedAt', 'elapsedSeconds', 'baselines', 'mutations', 'error', 'detection', 'requiredDetectionRegressions', 'shards']); +const perShardFields = new Set(['complete', 'shard', 'startedAt', 'elapsedSeconds', 'baselines', 'boundaryBaselines', 'mutations', 'error', 'detection', 'requiredDetectionRegressions', 'shards']); const timingFields = new Set(['startedAt', 'finishedAt', 'elapsedSeconds', 'durationMs', 'duration']); // Stable text for structural comparison: key order is irrelevant, timing is dropped. @@ -83,6 +83,29 @@ export function mergeShardReports(language, shards, { catalog, catalogSha256, in delete merged.requiredDetectionRegressions; merged.startedAt = ordered.map(report => report.startedAt).sort()[0]; merged.mutations = mutations; + // Boundary histories follow each shard's mutant selection. The same history + // may support several mutants, so union the clean recordings and require + // agreement when they overlap. Runner-specific absolute trace paths are not + // semantic differences; all other fields are. + if (ordered.some(report => report.boundaryBaselines !== undefined)) { + const baselines = {}; + for (const report of ordered) { + if (!report.boundaryBaselines || typeof report.boundaryBaselines !== 'object' || Array.isArray(report.boundaryBaselines)) refuse(`shard ${describe(report)} lacks boundary baselines`); + for (const [history, recording] of Object.entries(report.boundaryBaselines)) { + if (recording?.history !== history || recording.completed !== true || recording.error !== undefined || + !Array.isArray(recording.divergences) || recording.divergences.length || !Number.isSafeInteger(recording.lastStep) || recording.lastStep < 0) { + refuse(`boundary baseline ${history} is not a clean completed replay`); + } + const { path, ...stable } = recording; + if (baselines[history] && canonical(baselines[history]) !== canonical(stable)) refuse(`boundary baseline ${history} differs across shards`); + baselines[history] = stable; + } + } + for (const mutation of mutations) for (const entry of mutation.boundary ?? []) { + if (entry.history && !baselines[entry.history]) refuse(`${mutation.id}: boundary history ${entry.history} has no clean baseline`); + } + merged.boundaryBaselines = baselines; + } // Total measurement time across shards, comparable to a single run's wall time. merged.elapsedSeconds = ordered.reduce((sum, report) => sum + (report.elapsedSeconds ?? 0), 0); merged.shards = ordered.map(report => ({ index: report.shard.index, count, mutationIds: report.shard.mutationIds, startedAt: report.startedAt, elapsedSeconds: report.elapsedSeconds })); diff --git a/formal/mutation-reports.mjs b/formal/mutation-reports.mjs index e28eaf32..8a8e67c6 100644 --- a/formal/mutation-reports.mjs +++ b/formal/mutation-reports.mjs @@ -2,7 +2,51 @@ import { createHash } from 'node:crypto'; import { readFileSync, readdirSync } from 'node:fs'; import { resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { challengesByMutant, mutantCatalogPath, mutantIdPattern, mutantPorts } from './execution.mjs'; +import { boundaryEvidence, challengesByMutant, mutantCatalogPath, mutantIdPattern, mutantPorts } from './execution.mjs'; +import { countingPaths } from './replay/divergence.mjs'; +import { assessVectorBoundary } from './vector-evidence.mjs'; + +// Preserve profile/run identity when workspaces and artifact roots differ. +export function historyFromPath(path) { + return /(?:^|\/)regressions\/([a-z-]+\/[^/]+)\.itf\.json$/.exec(String(path))?.[1]; +} + +// Boundary recordings are separate from the ordinary detection cohorts. A +// failed driver or incomplete history never earns credit, even if it diverged +// earlier. Counters already wrong at the previous step cannot impersonate a +// consequence at this checkpoint. +export function assessBoundary(evidence, recording) { + if (evidence.vector) return assessVectorBoundary(evidence, recording); + const result = { ...evidence, via: 'coordinator' }; + if (['vector', 'unreproduced'].includes(evidence.state)) return result; + result.completed = recording?.completed === true; + result.lastStep = recording?.lastStep ?? -1; + const unreached = reason => ({ ...result, state: 'unreached', reason, + divergences: Array.isArray(recording?.divergences) ? recording.divergences : [] }); + if (!recording) return unreached('No boundary recording was produced'); + if (historyFromPath(recording.path) !== evidence.history) return unreached(`Recording names a different history: ${recording.path}`); + if (recording.completed !== true || recording.error !== undefined) return unreached(recording.error ?? `History stopped after observation ${recording.lastStep}`); + if (!Number.isSafeInteger(recording.lastStep) || recording.lastStep < evidence.step) return unreached(`Checkpoint ${evidence.step} was not reached (last observation ${recording.lastStep})`); + const divergences = recording.divergences; + if (!Array.isArray(divergences) || divergences.some((item, index) => !Number.isSafeInteger(item.step) || item.step < 0 || item.step > recording.lastStep || + (index > 0 && item.step <= divergences[index - 1].step) || !Array.isArray(item.paths) || !item.paths.length || item.paths.some(path => typeof path !== 'string'))) { + return unreached('Malformed divergence recording'); + } + const at = divergences.find(item => item.step === evidence.step)?.paths ?? []; + const before = divergences.find(item => item.step === evidence.step - 1)?.paths ?? []; + const matched = countingPaths(evidence.fields, at, before); + return { ...result, state: matched.length ? 'confirmed' : divergences.length ? 'side-effect-only' : 'not-divergent', + divergences, ...(matched.length ? { matched } : {}) }; +} + +export function boundaryColumn(entries) { + if (!entries) return 'not recorded'; + const measured = entries.filter(entry => !['vector', 'unreproduced'].includes(entry.state)); + const counts = `confirmed ${measured.filter(entry => entry.state === 'confirmed').length}/${measured.length}`; + const gaps = ['vector', 'unreproduced'].map(state => `${state} ${entries.filter(entry => entry.state === state).length}`); + const failures = measured.filter(entry => entry.state !== 'confirmed').map(entry => `${entry.challenge}: ${entry.state}`); + return [counts, ...gaps, ...failures].join('; '); +} // Shared by measure-semantics.mjs, measure-go-semantics.mjs and // merge-mutation-reports.mjs: the catalog selection, the input fingerprint, @@ -88,11 +132,15 @@ export function fingerprintFiles(directory, paths) { // Every listed catalog entry must have a measured result; a required cohort // that did not detect the fault is a regression named `/`. -export function requiredDetectionRegressions(entries, results) { +export function requiredDetectionRegressions(entries, results, boundaries = results.flatMap(result => result.boundary ?? [])) { return entries.flatMap(entry => { const result = results.find(item => item.id === entry.id); if (!result) throw new Error(`${entry.id}: catalog entry has no measured result`); - return entry.requiredDetections.filter(cohort => result.cohorts[cohort].state !== 'detected').map(cohort => `${entry.id}/${cohort}`); + return [ + ...entry.requiredDetections.filter(cohort => result.cohorts[cohort].state !== 'detected').map(cohort => `${entry.id}/${cohort}`), + ...boundaries.filter(boundary => boundary.mutant === entry.id && !['confirmed', 'vector', 'unreproduced'].includes(boundary.state)) + .map(boundary => `${entry.id}/boundary:${boundary.challenge}`), + ]; }); } @@ -174,7 +222,7 @@ export function noncompilingResult(mutation, cohorts, reason) { // A partial (--only) run reports its lost required detections and stops: it // is not gated and never completes, so the complete report is untouched. export function finishPartial(report, selected, { output, save }) { - const lost = requiredDetectionRegressions(selected, report.mutations); + const lost = requiredDetectionRegressions(selected, report.mutations, currentBoundaries(report, selected)); save(); console.log(lost.length ? `Lost required detections (a partial run is not gated): ${lost.join(', ')}` : 'Every required detection of the selected mutants held'); console.log(`Partial measurement of ${selected.map(m => m.id).join(', ')}: ${output}/report.json; measure the complete catalog for evidence`); @@ -199,8 +247,8 @@ function typescriptMarkdown(report, directory = root) { '| --- | ---: | ---: | ---: |', ...['cases', 'behavioral', 'protocol'].map(scope => { const c = report.declaredCoverage[scope]; return `| ${scope} | ${c.total} | ${c.portable} | ${c.generated} |`; }), '', 'Protocol references include invalidation vectors exercised separately by integration CI. Model references are a conservative named-property subset, not total model coverage.', '', - '| Mutation | Case | Challenges | Ordinary | Generated | Full portable |', '| --- | --- | --- | --- | --- | --- |', - ...report.mutations.map(m => `| ${m.id} | ${m.case} | ${challenges(m.id)} | ${m.cohorts.ordinary.state} | ${m.cohorts.generated.state} | ${m.cohorts.portable.state} |`), '', + '| Mutation | Case | Challenges | Ordinary | Generated | Full portable | Boundary |', '| --- | --- | --- | --- | --- | --- | --- |', + ...report.mutations.map(m => `| ${m.id} | ${m.case} | ${challenges(m.id)} | ${m.cohorts.ordinary.state} | ${m.cohorts.generated.state} | ${m.cohorts.portable.state} | ${boundaryColumn(m.boundary)} |`), '', 'Challenges are the model property challenges whose fault the mutant injects natively (nativeMutants in formal/execution.json). Full JSON includes exact input/corpus hashes, cohort counts, reached witnesses, behavioral/protocol scores, and failing test names. Adjacent JSON/log files retain assertion diagnostics and trace paths.', ''].join('\n'); } @@ -208,8 +256,8 @@ function goMarkdown(report, directory = root) { const challenges = challengesColumn(directory); return ['# Go semantic mutation measurement', '', `Completed in ${report.elapsedSeconds}s. Counts measure this named fault catalog and exact corpus, not universal equivalence.`, '', ...shardsMarkdown(report), - '| Mutation | Contract case | Challenges | Ordinary | Quint generated | Fixed supplement | Full portable |', '| --- | --- | --- | --- | --- | --- | --- |', - ...report.mutations.map(m => `| ${m.id} | ${m.case} | ${challenges(m.id)} | ${m.cohorts.ordinary.state} | ${m.cohorts.generated.state} | ${m.cohorts.fixed.state} | ${m.cohorts.portable.state} |`), '', + '| Mutation | Contract case | Challenges | Ordinary | Quint generated | Fixed supplement | Full portable | Boundary |', '| --- | --- | --- | --- | --- | --- | --- | --- |', + ...report.mutations.map(m => `| ${m.id} | ${m.case} | ${challenges(m.id)} | ${m.cohorts.ordinary.state} | ${m.cohorts.generated.state} | ${m.cohorts.fixed.state} | ${m.cohorts.portable.state} | ${boundaryColumn(m.boundary)} |`), '', 'Challenges are the model property challenges whose fault the mutant injects natively (nativeMutants in formal/execution.json). Full JSON records snapshot/corpus/witness fingerprints, selected tests, actual passing/failing leaf counts, and assertion diagnostics. Compilation errors, crashes, timeouts, missing witnesses, and skipped executions cannot count as detections.', ''].join('\n'); } @@ -231,14 +279,118 @@ export const languages = { detection: mutations => goDetection(mutations), markdown: goMarkdown, recordsRegressions: true }, }; +// Historical inspection may read a report anywhere. Gated inspection instead +// binds its evidence to this checkout and the exact corpus it claims to test. +// Existing report formats distinguish Go by its toolchain version and +// TypeScript by its separately fingerprinted project configuration; Go's +// module configuration is already covered by the go/ input tree. +export function validateBoundaryReportFreshness(report, { directory = root } = {}) { + const go = typeof report.go === 'string'; + const typescript = report.configurationSha256 !== undefined; + if (go === typescript) throw new Error('Boundary gate requires unambiguous TypeScript or Go report metadata'); + const language = go ? languages.go : languages.ts; + const catalogSha256 = sha256(readFileSync(resolve(directory, language.catalog))); + if (report.catalogSha256 !== catalogSha256) throw new Error('Boundary gate: mutation catalog differs from the measured report'); + const sameFingerprint = (recorded, measured) => recorded?.files === measured.files && recorded?.sha256 === measured.sha256; + if (!sameFingerprint(report.inputs, fingerprintFiles(directory, language.inputs))) { + throw new Error(`Boundary gate: ${language.name} source inputs differ from the measured report`); + } + if (typescript) { + const configuration = ['package.json', 'pnpm-lock.yaml', 'tsconfig.json', 'vitest.config.ts']; + const recorded = report.configurationSha256; + if (!recorded || typeof recorded !== 'object' || Array.isArray(recorded) || + Object.keys(recorded).sort().join() !== [...configuration].sort().join() || + configuration.some(path => recorded[path] !== sha256(readFileSync(resolve(directory, path))))) { + throw new Error('Boundary gate: TypeScript configuration differs from the measured report'); + } + } + const corpus = fingerprintFiles(directory, ['.formal-traces/conformance', '.formal-traces/effects', '.formal-traces/features', '.formal-traces/regressions']); + if (!sameFingerprint(report.corpus, corpus)) throw new Error('Boundary gate: corpus differs from the measured report'); +} + // The same gate for a shard (its own catalog entries, no summary), the // single-process run and the merge (the whole catalog, summary, completion). // A shard therefore fails on its own lost detections before any merge, and the // merged summary is computed by the code the single run uses. export function gateDetections(language, report, entries, { directory = root, summarize = true } = {}) { - const regressions = requiredDetectionRegressions(entries, report.mutations); + // Recompute verdicts from the current declarations and completed recordings. + // An omitted mapping, a stale pin or a claimed confirmation without a clean + // baseline must fail just as a measured non-detection does. + const regressions = requiredDetectionRegressions(entries, report.mutations, currentBoundaries(report, entries, directory)); if (summarize) report.detection = language.detection(report.mutations, directory); if (language.recordsRegressions) report.requiredDetectionRegressions = regressions; if (regressions.length) throw new Error(`Lost required detections: ${regressions.join(', ')}`); if (summarize) report.complete = true; } + +function currentBoundaries(report, entries, directory = root) { + const readSource = path => readFileSync(resolve(directory, path), 'utf8'); + const ids = new Set(entries.map(entry => entry.id)); + const evidence = boundaryEvidence(JSON.parse(readSource('formal/execution.json')), { readSource }) + .filter(entry => ids.has(entry.mutant)); + return boundaryReview(report, evidence, { requireEntries: true }); +} + +// Boundary evidence comes only from explicit recordings. Raw assertion logs +// remain diagnostic artifacts and cannot establish a completed history. +export function boundaryReview(report, evidence, { requireEntries = false } = {}) { + if (!Array.isArray(report.mutations)) throw new Error('Boundary report has no mutation results'); + const goReport = typeof report.go === 'string', typescriptReport = report.configurationSha256 !== undefined; + const reportPort = goReport === typescriptReport ? undefined : goReport ? 'go' : 'typescript'; + if (new Set(report.mutations.map(mutation => mutation.id)).size !== report.mutations.length) throw new Error('Boundary report repeats a mutant'); + for (const mutation of report.mutations) { + if (mutation.boundary && new Set(mutation.boundary.map(entry => entry.challenge)).size !== mutation.boundary.length) { + throw new Error(`Boundary report repeats a challenge for ${mutation.id}`); + } + } + return evidence.map(entry => { + const mutation = report.mutations.find(mutation => mutation.id === entry.mutant); + if (!mutation) return { ...entry, state: 'unreached', reason: 'Mapped mutant is absent from the report', divergences: [] }; + const recorded = mutation.boundary?.find(item => item.challenge === entry.challenge); + if (!recorded && requireEntries) return { ...entry, state: 'unreached', reason: 'No per-challenge boundary result was recorded', divergences: [] }; + if (recorded) { + const same = ['mutant', 'history', 'step', 'fields', 'origin', 'vector'].every(key => JSON.stringify(recorded[key]) === JSON.stringify(entry[key])); + if (!same || (entry.state && recorded.state !== entry.state)) return { ...entry, state: 'unreached', reason: 'Recorded boundary differs from the current evidence declaration', divergences: recorded.divergences ?? [] }; + if (entry.state) return { ...entry, via: 'coordinator' }; + const baseline = report.boundaryBaselines?.[entry.history]; + if (entry.vector) { + if (!reportPort || recorded.vectorResult?.port !== reportPort || baseline?.vectorResult?.port !== reportPort) + return { ...entry, state: 'unreached', reason: 'Vector records do not match the report language', divergences: [] }; + if (assessVectorBoundary(entry, baseline).state !== 'not-divergent') return { ...entry, state: 'unreached', + reason: 'No clean completed baseline for this vector', divergences: [] }; + if (recorded.vectorResult?.port !== baseline.vectorResult.port) return { ...entry, state: 'unreached', + reason: 'Vector baseline and mutation use different bindings', divergences: [] }; + return assessVectorBoundary(entry, { completed: recorded.completed, lastStep: recorded.lastStep, vectorResult: recorded.vectorResult, + ...(recorded.reason ? { error: recorded.reason } : {}) }); + } + if (baseline?.history !== entry.history || baseline.completed !== true || baseline.error !== undefined || + !Array.isArray(baseline.divergences) || baseline.divergences.length || !Number.isSafeInteger(baseline.lastStep) || baseline.lastStep < entry.step) { + return { ...entry, state: 'unreached', reason: 'No clean completed baseline for this boundary', divergences: recorded.divergences ?? [] }; + } + return assessBoundary(entry, { path: `regressions/${entry.history}.itf.json`, completed: recorded.completed, lastStep: recorded.lastStep, + divergences: recorded.divergences, ...(recorded.reason ? { error: recorded.reason } : {}) }); + } + if (entry.state) return { ...entry, via: 'legacy' }; + return { ...entry, via: 'legacy', state: 'unreached', reason: 'No complete boundary recording', divergences: [] }; + }); +} + +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + const [command, ...args] = process.argv.slice(2); + try { + const options = {}; + for (let i = 0; i < args.length; i++) { + const name = args[i]; + if (name === '--gate') options.gate = true; + else if (name === '--report' && args[i + 1] && !args[i + 1].startsWith('--')) options.report = args[++i]; + else throw new Error(`Unknown or incomplete option: ${name}`); + } + if (command !== 'boundary' || !options.report) throw new Error('Usage: node formal/mutation-reports.mjs boundary --report [--gate]'); + const { boundaryEvidence } = await import('./execution.mjs'); + const report = JSON.parse(readFileSync(options.report, 'utf8')); + if (options.gate) validateBoundaryReportFreshness(report); + const entries = boundaryReview(report, boundaryEvidence(), { requireEntries: options.gate === true }); + console.log(JSON.stringify({ sourceReport: resolve(options.report), entries }, null, 2)); + if (options.gate && (report.complete !== true || entries.some(entry => !['confirmed', 'vector', 'unreproduced'].includes(entry.state)))) process.exitCode = 1; + } catch (error) { console.error(error.message); process.exitCode = 1; } +} diff --git a/formal/mutations.json b/formal/mutations.json index c8abf6b7..597dc229 100644 --- a/formal/mutations.json +++ b/formal/mutations.json @@ -5,7 +5,7 @@ "id": "M01", "case": "C45.maximum-age-exclusive", "description": "Accept a retained candidate at the exact maximum age after decoding", - "rationale": "TypeScript src/internal/redis-cache.ts RedisCache.recoverRetainedCandidate and Go go/engine.go recover make the post-decode age check inclusive, so a retained candidate whose re-sampled age equals the configured maximum is served instead of rejected. The ports check the ceiling at initial-read retention, before decode and after decode; flipping an earlier site alone is masked by the post-decode recheck because ages only grow, so this is the one observable single-site inclusive edit. Already detected by recovery/maximumAgeAfterDecodePreservesSourceTest and witness recovery:exact-maximum-after-decode-rejects in both ports.", + "rationale": "TypeScript src/internal/redis-cache.ts RedisCache.recoverRetainedCandidate and Go go/engine.go recover make the post-decode age check inclusive, so a retained candidate whose re-sampled age equals the configured maximum is served instead of rejected. The ports check the ceiling at initial-read retention, before decode and after decode; flipping an earlier site alone is masked by the post-decode recheck because ages only grow, so this is the one observable single-site inclusive edit.", "typescript": { "edits": [ { @@ -104,7 +104,7 @@ "id": "M04", "case": "C34.second-fence", "description": "Skip the tracked fence recheck after serialization", - "rationale": "TypeScript src/internal/redis-cache.ts RedisCache.putWithLayer and Go go/engine.go putRemote skip the fence recheck that runs after serialization, so a frame stamped at or before a watermark observed during the dump is written anyway. The shadow fill reaches the same line through putForShadow in TypeScript and putRemote in Go, so a fenced dark fill writes and reports filled instead of fill_fenced. shadow/fenceRejudgedAtDumpReleaseAfterRollbackTest, whose dump is released after a wall rollback below the fence, is the expected evidence in both ports.", + "rationale": "TypeScript src/internal/redis-cache.ts RedisCache.putWithLayer and Go go/engine.go putRemote skip the fence recheck that runs after serialization, so a frame stamped at or before a watermark observed during the dump is written anyway. The shadow fill reaches the same line through putForShadow in TypeScript and putRemote in Go, so a fenced dark fill writes and reports filled instead of fill_fenced.", "typescript": { "edits": [ { @@ -170,7 +170,7 @@ "id": "M06", "case": "W04.strict-fence", "description": "Accept a tracked frame timestamp equal to its watermark", - "rationale": "TypeScript src/internal/redis-payload.ts decodeTrackedFrame and Go go/protocol.go DecodeFrame make the tracked fence comparison strict, so a frame whose timestamp equals the observed watermark decodes as a hit instead of being fenced. Already detected by the equal-timestamp row of the generated tracked decode vectors in both ports.", + "rationale": "TypeScript src/internal/redis-payload.ts decodeTrackedFrame and Go go/protocol.go DecodeFrame make the tracked fence comparison strict, so a frame whose timestamp equals the observed watermark decodes as a hit instead of being fenced.", "typescript": { "edits": [ { @@ -302,7 +302,7 @@ "id": "M10", "case": "C31.tracked-no-local-fill", "description": "Publish an unvalidated tracked source value directly into local storage", - "rationale": "TypeScript src/dialcache.ts DialCache.finishRedisChain no longer suppresses the local write for a tracked key that reached the Redis write path, and Go go/engine.go shared publishes the source value locally regardless of the Tracked flag. A tracked source value is therefore written into process-local storage before any watermark validation. Already detected by layers/trackedSourceWaitsForValidatedReadToWarmLocalTest and recovery-read/trackedSourceNeedsRemoteValidationBeforeLocalReuseTest in both ports.", + "rationale": "TypeScript src/dialcache.ts DialCache.finishRedisChain no longer suppresses the local write for a tracked key that reached the Redis write path, and Go go/engine.go shared publishes the source value locally regardless of the Tracked flag. A tracked source value is therefore written into process-local storage before any watermark validation.", "typescript": { "edits": [ { @@ -335,7 +335,7 @@ "id": "M11", "case": "C09.fixed-local-ttl", "description": "Renew the local insertion TTL on every cache hit", - "rationale": "TypeScript src/internal/local-cache.ts LocalCache.constructor turns on the lru-cache option that refreshes an entry's age on every read, and Go go/cache.go localGet re-stamps the insertion time and re-adds the entry on a hit. A local entry that is read often therefore never expires on its insertion TTL. Already detected by policy/localHitDoesNotRenewInsertionTtlTest and local-clock/fractionalInsertionExpiresAtWholeMillisecondTest in both ports.", + "rationale": "TypeScript src/internal/local-cache.ts LocalCache.constructor turns on the lru-cache option that refreshes an entry's age on every read, and Go go/cache.go localGet re-stamps the insertion time and re-adds the entry on a hit. A local entry that is read often therefore never expires on its insertion TTL.", "typescript": { "edits": [ { @@ -433,7 +433,7 @@ "id": "M14", "case": "C01.no-deadline", "description": "Bound a disabled or outside call's source with the configured fallback deadline", - "rationale": "TypeScript src/dialcache.ts DialCache.executeCacheOperation wraps the raw fallback of a disabled or outside call in withFallbackTimeout, and Go go/engine.go GetOrLoad runs it under awaitDeadline with the operation's budget. A call that bypasses the cache is therefore bounded by the configured fallback deadline instead of running unbounded. Detected by source-budgets/outsideAndInvalidOutsideCallsIgnoreDeadlineAndSharingTest at its advance(100) step in both ports.", + "rationale": "TypeScript src/dialcache.ts DialCache.executeCacheOperation wraps the raw fallback of a disabled or outside call in withFallbackTimeout, and Go go/engine.go GetOrLoad runs it under awaitDeadline with the operation's budget. A call that bypasses the cache is therefore bounded by the configured fallback deadline instead of running unbounded.", "typescript": { "edits": [ { @@ -465,7 +465,7 @@ "id": "M15", "case": "C02.reenabled-memo", "description": "Evict the enclosing request's memo when a nested enable() completes", - "rationale": "TypeScript src/context.ts DialCacheContext.enable deletes the enclosing holder's request-local cache when a nested enable call completes, and Go go/cache.go Enable's done closure clears the scope's memo on its not-owned branch. A completed nested scope therefore evicts the outer request's memo while the outer scope is still open; the TypeScript edit also drops the outer in-flight registry. A nested handle can only reach its own enclosing row, so the port evicts one outer row rather than every row. Detected by scope/nestedCloseAndDisabledBypassKeepOuterMemoTest step 11 in both ports.", + "rationale": "TypeScript src/context.ts DialCacheContext.enable deletes the enclosing holder's request-local cache when a nested enable call completes, and Go go/cache.go Enable's done closure clears the scope's memo on its not-owned branch. A completed nested scope therefore evicts the outer request's memo while the outer scope is still open; the TypeScript edit also drops the outer in-flight registry. A nested handle can only reach its own enclosing row, so the port evicts one outer row rather than every row.", "typescript": { "edits": [ { @@ -497,7 +497,7 @@ "id": "M17", "case": "C05.local-hit-stops-traversal", "description": "Serve a local hit with a value the layer never stored while still skipping the loader", - "rationale": "TypeScript src/internal/local-cache.ts LocalCache.getWithResolvedConfig reports a hit whose value is undefined, and Go go/engine.go shared builds its localResult literal without the asserted value, so a found entry serves the zero value. The loader is still bypassed, so a local hit serves a value the layer never stored while loader counts stay unchanged. The Go runner records the ordinary (unit) cohort as crashed because TestBorrowedReferencesSurviveFollowersAndCacheHits dereferences the nil pointer a hit now returns inside its synctest bubble, a panic rather than a failed assertion; the generated cohort is the required detection. core/localValueSurvivesSourceChangeTest detects both ports.", + "rationale": "TypeScript src/internal/local-cache.ts LocalCache.getWithResolvedConfig reports a hit whose value is undefined, and Go go/engine.go shared builds its localResult literal without the asserted value, so a found entry serves the zero value. The loader is still bypassed, so a local hit serves a value the layer never stored while loader counts stay unchanged. The Go runner records the ordinary (unit) cohort as crashed because TestBorrowedReferencesSurviveFollowersAndCacheHits dereferences the nil pointer a hit now returns inside its synctest bubble, a panic rather than a failed assertion; the generated cohort is the required detection.", "typescript": { "edits": [ { @@ -529,7 +529,7 @@ "id": "M18", "case": "C09.fixed-local-ttl", "description": "Serve a local entry at its exact insertion expiry", - "rationale": "TypeScript src/internal/local-cache.ts LocalCache.put drops the one-millisecond reduction from the lru-cache ttl that encodes the exclusive expiry boundary, and Go go/cache.go localGet makes the expiry comparison strict. A local entry is therefore served at the exact millisecond its insertion TTL elapses. Detected by policy/localHitDoesNotRenewInsertionTtlTest step 10 and source-budgets/localEntryExpiresAfterItsTtlTest in both ports.", + "rationale": "TypeScript src/internal/local-cache.ts LocalCache.put drops the one-millisecond reduction from the lru-cache ttl that encodes the exclusive expiry boundary, and Go go/cache.go localGet makes the expiry comparison strict. A local entry is therefore served at the exact millisecond its insertion TTL elapses.", "typescript": { "edits": [ { @@ -561,7 +561,7 @@ "id": "M19", "case": "C09.fractional-clock-grid", "description": "Compute local insertion and expiry on the precise clock instead of the whole-millisecond grid", - "rationale": "TypeScript src/internal/local-cache.ts LocalCache.constructor stops flooring the performance clock and LocalCache.put shortens the ttl by a microsecond instead of a millisecond; Go go/cache.go localGet compares the precise clock against a nanosecond ttl and localPut stamps the precise instant. Insertion and expiry therefore fall on the fractional clock instead of the whole-millisecond grid. The feature-profile driver clocks are integer, so the fault is silent outside the local-clock profile; local-clock/fractionalInsertionExpiresAtWholeMillisecondTest detects both ports.", + "rationale": "TypeScript src/internal/local-cache.ts LocalCache.constructor stops flooring the performance clock and LocalCache.put shortens the ttl by a microsecond instead of a millisecond; Go go/cache.go localGet compares the precise clock against a nanosecond ttl and localPut stamps the precise instant. Insertion and expiry therefore fall on the fractional clock instead of the whole-millisecond grid. The feature-profile driver clocks are integer, so the fault is silent outside the local-clock profile.", "typescript": { "edits": [ { @@ -645,7 +645,7 @@ "id": "M21", "case": "C13.uncoalesced-caching", "description": "Serve a live local entry before joining the pending process flight", - "rationale": "TypeScript src/dialcache.ts DialCache.getThroughSharedLayers peeks LocalCache.getWithResolvedConfig before deciding whether to join the process flight, and Go go/engine.go shared peeks localGet before its coalesce branch. A live local entry is therefore served ahead of a pending process flight registered for the key. Detected by policy/independentCompletionKeepsRegisteredLeaderTest step 9 in both ports and by runtime-boundaries/defaultSharingJoinsRegisteredLeaderOverWarmedLocalTest.", + "rationale": "TypeScript src/dialcache.ts DialCache.getThroughSharedLayers peeks LocalCache.getWithResolvedConfig before deciding whether to join the process flight, and Go go/engine.go shared peeks localGet before its coalesce branch. A live local entry is therefore served ahead of a pending process flight registered for the key.", "typescript": { "edits": [ { @@ -677,7 +677,7 @@ "id": "M22", "case": "C13.uncoalesced-caching", "description": "Join a pending process flight from a non-coalescing caller while registration still respects coalesce", - "rationale": "TypeScript src/dialcache.ts DialCache.getThroughSharedLayers and Go go/engine.go shared also enter the single-flight path when a flight for the key is already pending, even though coalesce is false. A non-coalescing caller therefore joins a pending process flight while registration still respects coalesce. Detected by policy/independentCompletionKeepsRegisteredLeaderTest step 5 and policy/independentFailureKeepsRegisteredLeaderTest in both ports.", + "rationale": "TypeScript src/dialcache.ts DialCache.getThroughSharedLayers and Go go/engine.go shared also enter the single-flight path when a flight for the key is already pending, even though coalesce is false. A non-coalescing caller therefore joins a pending process flight while registration still respects coalesce.", "typescript": { "edits": [ { @@ -709,7 +709,7 @@ "id": "M23", "case": "C13.local-last-writer", "description": "Never let a local publication replace an entry already occupying the slot, even an expired one", - "rationale": "TypeScript src/internal/local-cache.ts LocalCache.put writes only when a peek that allows stale entries finds the slot empty, and Go go/cache.go localPut writes only when Peek finds no entry. A local publication therefore never replaces an entry already occupying the slot, even an expired one. Both ports purge an expired entry at the read that starts the source, so the model's declared reproducer does not transfer; policy/independentLocalPublicationUsesLastCompletionTest is the portable evidence (calls 1,2,2 instead of 1,2,1 in both ports).", + "rationale": "TypeScript src/internal/local-cache.ts LocalCache.put writes only when a peek that allows stale entries finds the slot empty, and Go go/cache.go localPut writes only when Peek finds no entry. Both therefore skip a local publication when any entry already occupies the slot, even an expired one. The formal fault sits in local_storage.putLocal, shared by layered and local-only publication. Both native caches purge expired entries on a read, so only live-slot replacement demonstrates the common native boundary.", "typescript": { "edits": [ { @@ -741,7 +741,7 @@ "id": "M24", "case": "C13.local-last-writer", "description": "Clear the key's local entry when a source settlement fails", - "rationale": "TypeScript src/dialcache.ts DialCache.finishRedisChain deletes the key's lru-cache entry before rethrowing a rejected source, and Go go/engine.go shared removes the local entry on the error branch of its no-Redis-client, remote-disabled and remote-read paths. A failed settlement therefore clears a local entry that an earlier accepted settlement published. The ports are asymmetric in three ways that stay off the boundary the regression observes: Go also clears when the remote read itself failed, a path the TypeScript edit never reaches; Go removes the entry before the stale-recovery attempt while TypeScript deletes only after recovery fails to serve; and Go covers the path with no Redis client, which TypeScript finishLocalOnly never reaches. The declared reproducer's failing source is an outside bypass, so policy/independentFailureKeepsSettledLocalValueTest is the portable evidence.", + "rationale": "TypeScript src/dialcache.ts DialCache.finishRedisChain deletes the key's local entry before rethrowing a rejected source, and Go go/engine.go shared removes the local entry on its no-Redis-client, non-ramped remote-disabled and remote-read error paths. A failed active source can therefore discard another source's accepted local publication. The model removes the value, expiry and LRU membership together. Native scope differs: Go also clears after a remote-read failure, before a recovery attempt, and without a Redis client; TypeScript clears only after finishRedisChain cannot recover. Ramped-down dark-source paths and outside bypass calls do not execute these edits.", "typescript": { "edits": [ { @@ -783,7 +783,7 @@ "id": "M25", "case": "C15.shared-error-retry", "description": "Keep a rejected request-local flight registered so a later same-scope caller receives the rejection instead of retrying", - "rationale": "TypeScript src/dialcache.ts DialCache.singleFlightRequestLocal no longer clears the in-flight registry entry when the promise rejects, and Go go/engine.go singleFlight keeps a request-scoped flight registered when it ended in error. A later same-scope caller therefore receives the rejection instead of retrying the source. Neither memo can hold an error (TypeScript RequestLocalCache.set runs only on success and Go writes the memo only without an error), so the request in-flight registry is the only request-scoped error carrier. scope/rejectedFlightAllowsRetryTest and the witnesses scope:rejected-flight-retry and shared-rejection are the expected evidence.", + "rationale": "TypeScript src/dialcache.ts DialCache.singleFlightRequestLocal no longer clears the in-flight registry entry when the promise rejects, and Go go/engine.go singleFlight keeps a request-scoped flight registered when it ended in error. A later same-scope caller therefore receives the rejection instead of retrying the source. Neither memo can hold an error (TypeScript RequestLocalCache.set runs only on success and Go writes the memo only without an error), so the request in-flight registry is the only request-scoped error carrier.", "typescript": { "edits": [ { @@ -815,7 +815,7 @@ "id": "M26", "case": "C15.shared-error-retry", "description": "Never remove a settled process flight from the registry so later same-key callers join finished work", - "rationale": "TypeScript src/dialcache.ts DialCache.singleFlightProcess inverts the identity check in its clear routine so a settled flight stays in the registry, and Go go/engine.go singleFlight deletes a process flight only when an owner is present. Later same-key callers therefore join finished work and receive its stale outcome. Joiners complete rather than strand because a blocked follower would deadlock synctest and the Go runner would classify that as a crash. source-budgets/defaultSourceBudgetExpiresAtSixtySecondsTest and policy/independentCompletionKeepsRegisteredLeaderTest are the expected evidence.", + "rationale": "TypeScript src/dialcache.ts DialCache.singleFlightProcess inverts the identity check in its clear routine so a settled flight stays in the registry, and Go go/engine.go singleFlight deletes a process flight only when an owner is present. Later same-key callers therefore join finished work and receive its stale outcome. Joiners complete rather than strand because a blocked follower would deadlock synctest and the Go runner would classify that as a crash.", "typescript": { "edits": [ { @@ -847,7 +847,7 @@ "id": "M27", "case": "C16.runtime-enables-coalescing", "description": "Capture the operation's coalesce default as true so an omitted runtime leaf or null provider inherits sharing over a configured false", - "rationale": "TypeScript src/dialcache.ts snapshotDefaultConfig records the operation's coalesce default as true whenever it is set, and Go go/policy.go staticPolicyMap always publishes coalesce as true. An omitted runtime leaf or a null provider therefore inherits sharing over a configured false. The TypeScript edit is deliberately in snapshotDefaultConfig rather than the identical text in runtime-config.ts mergeKeyConfig. runtime-boundaries/inheritedFalseKeepsIndependentMemoizingSourcesTest and the independent profile replays whose fixture sets coalesce false are the expected evidence.", + "rationale": "TypeScript src/dialcache.ts snapshotDefaultConfig records the operation's coalesce default as true whenever it is set, and Go go/policy.go staticPolicyMap always publishes coalesce as true. An omitted runtime leaf or a null provider therefore inherits sharing over a configured false. The TypeScript edit is deliberately in snapshotDefaultConfig rather than the identical text in runtime-config.ts mergeKeyConfig.", "typescript": { "edits": [ { @@ -879,7 +879,7 @@ "id": "M28", "case": "C17.library-defaults", "description": "Default coalescing off when the static coalesce leaf is omitted while an explicit true still shares", - "rationale": "TypeScript src/dialcache.ts DialCache.getThroughSharedLayers and DialCache.getThroughRequestLocal treat coalescing as on only when the static leaf is explicitly true, and Go go/policy.go ParsePolicy sets Coalesce to false when the static coalesce leaf is absent. An omitted leaf therefore defaults to no sharing while an explicit true still shares. Go ParsePolicy returns the zero Policy when the fixture supplies no static policy map at all, so that corner keeps coalescing on; both Go drivers pass the fixture's policy map through ParsePolicy, so an omitted leaf inside a supplied map resolves to no sharing. runtime-boundaries/omittedRampAndSharingUseLibraryDefaultsTest and source-budgets/lateFollowerUsesLeadersRemainingBudgetTest are the expected evidence.", + "rationale": "TypeScript src/dialcache.ts DialCache.getThroughSharedLayers and DialCache.getThroughRequestLocal treat coalescing as on only when the static leaf is explicitly true, and Go go/policy.go ParsePolicy sets Coalesce to false when the static coalesce leaf is absent. An omitted leaf therefore defaults to no sharing while an explicit true still shares. Go ParsePolicy returns the zero Policy when the fixture supplies no static policy map at all, so that corner keeps coalescing on; both Go drivers pass the fixture's policy map through ParsePolicy, so an omitted leaf inside a supplied map resolves to no sharing.", "typescript": { "edits": [ { @@ -916,7 +916,7 @@ "id": "M29", "case": "C18.captured-shadow-fill", "description": "Re-resolve the runtime policy at fill time and write the dark fill with the retention then in force", - "rationale": "TypeScript src/dialcache.ts DialCache.runShadowValidation re-fetches the key config through fetchKeyConfig at fill time instead of using the captured shadowFillConfig, and Go go/shadow.go runShadow re-runs ResolvePolicy before putRemote and overwrites the remote TTL and retention. The dark fill is therefore written with the retention in force at fill time rather than the one captured at admission. Detection most likely fires on the extra provider call (policyCalls) before writeTtls diverge; shadow-layers/darkFillRetainsPolicyThroughSerializationTest is the expected evidence.", + "rationale": "TypeScript src/dialcache.ts DialCache.runShadowValidation re-fetches the key config through fetchKeyConfig at fill time instead of using the captured shadowFillConfig, and Go go/shadow.go runShadow re-runs ResolvePolicy before putRemote and overwrites the remote TTL and retention. The dark fill is therefore written with the retention in force at fill time rather than the one captured at admission.", "typescript": { "edits": [ { @@ -948,7 +948,7 @@ "id": "M30", "case": "C22.freshness-boundary", "description": "Serve a remote frame whose age equals the fresh TTL as a hit", - "rationale": "TypeScript src/internal/redis-cache.ts RedisCache.getWithResolvedConfig and Go go/engine.go readServing make the freshness comparison strict, so a remote frame whose age equals the fresh TTL is served as a hit instead of starting a source. When no stale-on-error retention is configured the maximum-age check runs first and masks the flip. policy/exactRemoteFreshBoundaryStartsSourceTest with the 5 s retention fixture and witness policy:remote-exact-fresh-boundary-miss are the expected evidence.", + "rationale": "TypeScript src/internal/redis-cache.ts RedisCache.getWithResolvedConfig and Go go/engine.go readServing make the freshness comparison strict, so a remote frame whose age equals the fresh TTL is served as a hit instead of starting a source. When no stale-on-error retention is configured the maximum-age check runs first and masks the flip.", "typescript": { "edits": [ { @@ -980,7 +980,7 @@ "id": "M31", "case": "C22.freshness-boundary", "description": "Acquire a dark C0 frame whose age equals the fresh TTL instead of treating it as expired", - "rationale": "TypeScript src/internal/redis-cache.ts RedisCache.validateFrameAge, reached only through the shadow read path, accepts an age equal to the maximum, and Go go/shadow.go runShadow treats a C0 frame as expired only when its age exceeds the fresh TTL. A dark C0 frame exactly at the fresh TTL is therefore acquired instead of treated as expired. shadow/c0AtExactFreshnessBoundaryRefillsTest is the expected evidence in both ports.", + "rationale": "TypeScript src/internal/redis-cache.ts RedisCache.validateFrameAge, reached only through the shadow read path, accepts an age equal to the maximum, and Go go/shadow.go runShadow treats a C0 frame as expired only when its age exceeds the fresh TTL. A dark C0 frame exactly at the fresh TTL is therefore acquired instead of treated as expired.", "typescript": { "edits": [ { @@ -1012,7 +1012,7 @@ "id": "M32", "case": "C22.physical-vs-logical", "description": "Write every remote frame with the fresh TTL, ignoring stale-on-error retention", - "rationale": "TypeScript src/internal/redis-cache.ts retentionTtlSecFor returns the fresh TTL instead of the stale-on-error maximum age, and Go go/engine.go putRemote sets the physical ttl to the fresh TTL. Every remote frame is therefore written with the fresh TTL, so no retained candidate survives for stale recovery. policy/eachSourceKeepsPublicationPolicyTest (writeTtls 4000 and 5000) and every recovery-profile write are the evidence, since the drivers compare writeTtls on each step.", + "rationale": "TypeScript src/internal/redis-cache.ts retentionTtlSecFor returns the fresh TTL instead of the stale-on-error maximum age, and Go go/engine.go putRemote sets the physical ttl to the fresh TTL. Every remote frame is therefore written with the fresh TTL, so no retained candidate survives for stale recovery.", "typescript": { "edits": [ { @@ -1044,7 +1044,7 @@ "id": "M33", "case": "C23.policy-does-not-spend-source-budget", "description": "Judge a source settlement against a deadline anchored at the clock origin instead of the source start; the timer still fires at the source's own deadline", - "rationale": "TypeScript src/internal/deadline.ts withMonotonicDeadline (its settleBeforeDeadline closure) judges a settlement by comparing the current reading of performance.now with the budget instead of the time elapsed since the source started, and Go go/deadline.go awaitDeadline compares the clock's elapsed reading with the budget in its settlement branch instead of subtracting the start, so in both ports the settlement is judged against a deadline anchored at the clock origin. Only the settlement judgement changes; the timers still fire at the source's own deadline. src/internal/deadline.ts imports performance from node:perf_hooks, and every TypeScript driver in the generated cohort mocks that object: test/formal/behavior-driver.ts:154 for the feature and the effects profiles, test/formal/local-clock-profile.ts:16 for local-clock, and test/formal-conformance.test.ts:163 pins it to 0 for the core profile, where the fault is therefore inert. So, like Go's manual clocks that start at 0, any settlement at history time at or past the budget is rejected regardless of when its source started. In the ordinary unit suite the clock is real, so detection there depends on worker uptime and is not required; source-budgets/heldPolicyDoesNotSpendSourceBudgetTest and defaultSourceBudgetExpiresAtSixtySecondsTest are the expected evidence in both ports.", + "rationale": "TypeScript src/internal/deadline.ts withMonotonicDeadline (its settleBeforeDeadline closure) judges a settlement by comparing the current reading of performance.now with the budget instead of the time elapsed since the source started, and Go go/deadline.go awaitDeadline compares the clock's elapsed reading with the budget in its settlement branch instead of subtracting the start, so in both ports the settlement is judged against a deadline anchored at the clock origin. Only the settlement judgement changes; the timers still fire at the source's own deadline. src/internal/deadline.ts imports performance from node:perf_hooks, and every TypeScript driver in the generated cohort mocks that object: test/formal/behavior-driver.ts:154 for the feature and the effects profiles, test/formal/local-clock-profile.ts:16 for local-clock, and test/formal-conformance.test.ts:163 pins it to 0 for the core profile, where the fault is therefore inert. So, like Go's manual clocks that start at 0, any settlement at history time at or past the budget is rejected regardless of when its source started. In the ordinary unit suite the clock is real, so detection there depends on worker uptime and is not required.", "typescript": { "edits": [ { @@ -1076,7 +1076,7 @@ "id": "M34", "case": "C24.follower-source", "description": "Complete followers of a joined flight with an undefined or zero value instead of the leader's accepted value while errors still propagate", - "rationale": "TypeScript src/dialcache.ts DialCache.singleFlightProcess and DialCache.singleFlightRequestLocal resolve a follower's promise to undefined instead of the leader's value, and Go go/engine.go singleFlight returns the zero value to followers once the flight completes. Errors still propagate, so followers of a joined flight receive an undefined or zero value instead of the accepted value. Followers complete with a wrong value rather than staying pending because a blocked follower would deadlock synctest and the Go runner would classify that as a crash. The Go runner records the ordinary (unit) cohort as crashed because TestBorrowedReferencesSurviveFollowersAndCacheHits dereferences the nil pointer a follower now receives inside its synctest bubble, a panic rather than a failed assertion; the generated cohort is the required detection. recovery/crossRequestRecoveryMemoizesBothScopesTest and coalescedRecoveryClassifiesAndDecodesOnceTest are the expected evidence.", + "rationale": "TypeScript src/dialcache.ts DialCache.singleFlightProcess and DialCache.singleFlightRequestLocal resolve a follower's promise to undefined instead of the leader's value, and Go go/engine.go singleFlight returns the zero value to followers once the flight completes. Errors still propagate, so followers of a joined flight receive an undefined or zero value instead of the accepted value. Followers complete with a wrong value rather than staying pending because a blocked follower would deadlock synctest and the Go runner would classify that as a crash. The Go runner records the ordinary (unit) cohort as crashed because TestBorrowedReferencesSurviveFollowersAndCacheHits dereferences the nil pointer a follower now receives inside its synctest bubble, a panic rather than a failed assertion; the generated cohort is the required detection.", "typescript": { "edits": [ { @@ -1113,7 +1113,7 @@ "id": "M35", "case": "C25.late-source-rejected", "description": "Accept a settlement observed at exactly the elapsed deadline", - "rationale": "TypeScript src/internal/deadline.ts withMonotonicDeadline and Go go/deadline.go awaitDeadline accept a settlement observed at exactly the elapsed deadline, making the settlement-time boundary inclusive while timer-delivered timeouts are unchanged. M03 shares this anchor as the coarse late-acceptance fault and stays a separate entry. Timer-delivered regressions do not distinguish this mutant, so the effects jumpClock histories (effects/lateReadCannotBecomeHitTest, lateReadRejectionUsesDeadlineCategoryTest and the late-resolve and late-reject witnesses) are the expected evidence.", + "rationale": "TypeScript src/internal/deadline.ts withMonotonicDeadline and Go go/deadline.go awaitDeadline accept a settlement observed at exactly the elapsed deadline, making the settlement-time boundary inclusive while timer-delivered timeouts are unchanged. M03 shares this anchor as the coarse late-acceptance fault and stays a separate entry.", "typescript": { "edits": [ { @@ -1145,7 +1145,7 @@ "id": "M36", "case": "C25.late-source-rejected", "description": "Stamp the serialization-start receipt one millisecond late so the dump duration is measured short", - "rationale": "TypeScript src/internal/redis-cache.ts RedisCache.putWithLayer and Go go/engine.go putRemote take the serialization-start receipt one millisecond late, so the dump duration is measured one millisecond short. The connection monitor's acceptance receipt is model bookkeeping with no native line, so this stamp is the only native acceptance receipt. effects/sourceDurationExcludesPublicationTest (dump amount 0.009 instead of 0.010) is the expected evidence.", + "rationale": "TypeScript src/internal/redis-cache.ts RedisCache.putWithLayer and Go go/engine.go putRemote take the serialization-start receipt one millisecond late, so the dump duration is measured one millisecond short. The connection monitor's acceptance receipt is model bookkeeping with no native line, so this stamp is the only native acceptance receipt.", "typescript": { "edits": [ { @@ -1177,13 +1177,13 @@ "id": "M37", "case": "C27.source-error-no-publication", "description": "Publish a rejected source's result into process-local storage before rethrowing", - "rationale": "TypeScript src/dialcache.ts DialCache.finishRedisChain publishes the rejected source's result into local storage on a local miss before rethrowing, and Go go/engine.go shared calls putLocal under localMiss before returning the Redis-chain error. A rejected source therefore seeds process-local storage. M24 anchors the same TypeScript statement with its indentation and stays a distinct entry. None of the four pinned local-failure regressions reach reject-then-re-begin on a healthy miss; local-failure/rejectedSourceDoesNotSeedLocalTest is the portable evidence.", + "rationale": "TypeScript src/dialcache.ts DialCache.finishRedisChain publishes the absent result of a rejected source into local storage before rethrowing. Go go/engine.go shared publishes its nil result on the Redis-chain error branch. Both edits seed a local hit without an accepted source; the next caller observes the cached absent result instead of starting source work. The absent value is portable in both drivers, so an observation comparison detects the fault without a schema failure.", "typescript": { "edits": [ { "path": "src/dialcache.ts", "before": "throw fallbackError;", - "after": "if (local.status === \"miss\") {\n await this.putLocalFailOpen(key, fallbackError, local.config);\n }\n throw fallbackError;" + "after": "if (local.status === \"miss\") {\n await this.putLocalFailOpen(key, undefined, local.config);\n }\n throw fallbackError;" } ], "requiredDetections": [ @@ -1209,7 +1209,7 @@ "id": "M38", "case": "C28.no-refill-after-read-failure", "description": "Skip the Redis publication after a semantic miss, including a decode failure, so only retained-candidate misses refill", - "rationale": "TypeScript src/dialcache.ts DialCache.finishRedisChain writes to Redis only when a retained frame exists, and Go go/engine.go shared also skips the publication for miss and decode_error reads. After a semantic miss, including a decode failure, the source value is therefore never published to Redis; only retained-candidate misses refill. core/invalidationRefillsFromCurrentSourceTest (redisWrites 3 and a later hit) and effects/nullReplyRefillsAndIsReadableTest are the expected evidence in both ports.", + "rationale": "TypeScript src/dialcache.ts DialCache.finishRedisChain writes to Redis only when a retained frame exists, and Go go/engine.go shared also skips the publication for miss and decode_error reads. After a semantic miss, including a decode failure, the source value is therefore never published to Redis; only retained-candidate misses refill.", "typescript": { "edits": [ { @@ -1241,7 +1241,7 @@ "id": "M39", "case": "C34.first-fence", "description": "Drop the pre-serialization fence check so a source accepted behind an observed fence is serialized while the post-serialization recheck still blocks the write", - "rationale": "TypeScript src/internal/redis-cache.ts RedisCache.putWithLayer and Go go/engine.go putRemote drop the fence check that runs before serialization. A source accepted behind an observed fence is therefore serialized, while the post-serialization recheck still blocks the write. effects/absentReplyWithFutureFenceBlocksRefillTest (dumps 0 becomes 1) and witness effects:normalized-fence-blocks-publication are the expected evidence in both ports.", + "rationale": "TypeScript src/internal/redis-cache.ts RedisCache.putWithLayer and Go go/engine.go putRemote drop the fence check that runs before serialization. A source accepted behind an observed fence is therefore serialized, while the post-serialization recheck still blocks the write.", "typescript": { "edits": [ { @@ -1273,7 +1273,7 @@ "id": "M40", "case": "C40.future-rejected", "description": "Classify a frame dated after the reader clock as a valid frame of age zero instead of rejecting it as future", - "rationale": "TypeScript src/internal/redis-cache.ts RedisCache.frameAge and Go go/engine.go frameAge classify a frame dated after the reader clock as valid with age zero instead of rejecting it as future. The age is clamped to zero because the drivers' observation schema rejects a negative age before any comparison, which the measurement would classify as a malformed observation rather than detection. Both ports implement the future clause once in frameAge and use it for serving, retained-candidate recovery and shadow reads, so a recovery-only edit is not expressible. recovery/rollbackDuringRetainedDecodeRejectsFutureSnapshotTest, policy/wallRollbackRejectsFutureRemoteFrameTest and the witnesses recovery:rollback-rejects-retained-future and policy:rollback-rejects-future-remote are the expected evidence.", + "rationale": "TypeScript src/internal/redis-cache.ts RedisCache.frameAge and Go go/engine.go frameAge classify a frame dated after the reader clock as valid with age zero instead of rejecting it as future. The age is clamped to zero because the drivers' observation schema rejects a negative age before any comparison, which the measurement would classify as a malformed observation rather than detection. Both ports implement the future clause once in frameAge and use it for serving, retained-candidate recovery and shadow reads, so a recovery-only edit is not expressible.", "typescript": { "edits": [ { @@ -1305,7 +1305,7 @@ "id": "M41", "case": "C43.independent-values", "description": "Decode the retained candidate but serve an undefined or zero value instead of it from stale recovery", - "rationale": "TypeScript src/internal/redis-cache.ts RedisCache.recoverRetainedCandidate decodes the retained candidate but returns a hit whose value is undefined, and Go go/engine.go recover discards the decoded value and serves the zero value. Stale recovery therefore serves a value other than the decoded candidate. The port is generic in its value type, so the zero value is the only expressible wrong constant. independent/independentRecoveriesServeDistinctAcquiredSnapshotsTest and recovery/coalescedRecoveryClassifiesAndDecodesOnceTest are the expected evidence.", + "rationale": "TypeScript src/internal/redis-cache.ts RedisCache.recoverRetainedCandidate decodes the retained candidate but returns a hit whose value is undefined, and Go go/engine.go recover discards the decoded value and serves the zero value. Stale recovery therefore serves a value other than the decoded candidate. The port is generic in its value type, so the zero value is the only expressible wrong constant.", "typescript": { "edits": [ { @@ -1337,7 +1337,7 @@ "id": "M42", "case": "C43.request-memo", "description": "Publish a recovered stale value into process-local storage", - "rationale": "TypeScript src/dialcache.ts DialCache.finishRedisChain publishes a recovered stale value into local storage on a local miss before returning it, and Go go/engine.go shared calls putLocal under localMiss once recover succeeds. A recovered value therefore warms process-local storage. The recovery profile has no local TTL, so detection rests on the recovery-read profile with its 1 s local TTL; recovery-read/untrackedCompressedRecoveryDoesNotWarmLocalTest and compressedRecoveryMemoizesWithoutLocalPublicationTest are the expected evidence.", + "rationale": "TypeScript src/dialcache.ts DialCache.finishRedisChain publishes a recovered stale value into local storage on a local miss before returning it, and Go go/engine.go shared calls putLocal under localMiss once recover succeeds. A recovered value therefore warms process-local storage. The recovery profile has no local TTL, so detection rests on the recovery-read profile with its 1 s local TTL.", "typescript": { "edits": [ { @@ -1369,7 +1369,7 @@ "id": "M43", "case": "C44.retained-invalidation", "description": "Retain the stale candidate with a creation timestamp one millisecond after the acquired frame", - "rationale": "TypeScript src/internal/redis-cache.ts RedisCache.getWithResolvedConfig retains the stale candidate as a copy whose creation timestamp is one millisecond later than the acquired frame, and Go go/engine.go readServing retains a Frame copy with CreatedAtMS advanced by one. The candidate's recorded snapshot therefore differs from the acquired frame by one millisecond. M44 shares this anchor with a different wrong behavior and stays a distinct entry. recovery/maximumAgeAfterDecodePreservesSourceTest and maximumAgeBeforeDecodePreservesSourceTest plus the recovery diagnostic ages compared to the millisecond are the expected evidence in both ports.", + "rationale": "TypeScript src/internal/redis-cache.ts RedisCache.getWithResolvedConfig retains the stale candidate as a copy whose creation timestamp is one millisecond later than the acquired frame, and Go go/engine.go readServing retains a Frame copy with CreatedAtMS advanced by one. The candidate's recorded snapshot therefore differs from the acquired frame by one millisecond. M44 shares this anchor with a different wrong behavior and stays a distinct entry.", "typescript": { "edits": [ { @@ -1401,7 +1401,7 @@ "id": "M44", "case": "C44.retained-invalidation", "description": "Stamp the retained candidate with the read time instead of its frame timestamp", - "rationale": "TypeScript src/internal/redis-cache.ts RedisCache.getWithResolvedConfig retains the stale candidate with its creation timestamp replaced by the wall clock at read time, and Go go/engine.go readServing copies the frame with CreatedAtMS set to the injected clock's wall reading. TypeScript reads Date.now, which the drivers fake, while Go reads the injected clock, so both stamps are the history's wall time. The retained candidate therefore appears to have been created at the read and always passes the recovery maximum. recovery/maximumAgeAfterDecodePreservesSourceTest, recovery-read/compressedRecoveryRechecksMaximumAfterDecodeTest and witness recovery:expired-during-decode are the expected evidence.", + "rationale": "TypeScript src/internal/redis-cache.ts RedisCache.getWithResolvedConfig retains the stale candidate with its creation timestamp replaced by the wall clock at read time, and Go go/engine.go readServing copies the frame with CreatedAtMS set to the injected clock's wall reading. TypeScript reads Date.now, which the drivers fake, while Go reads the injected clock, so both stamps are the history's wall time. The retained candidate therefore appears to have been created at the read and always passes the recovery maximum.", "typescript": { "edits": [ { @@ -1433,7 +1433,7 @@ "id": "M45", "case": "C45.captured-policy", "description": "Admit calls with a recovery maximum one second larger than the configured policy", - "rationale": "TypeScript src/internal/runtime-config.ts resolveRemoteLayerConfigResult admits the call with a stale-on-error maximum one second larger than configured, and Go go/policy.go ResolvePolicy adds one second to the resolved StaleOnErrorMaxAge. TypeScript config granularity is seconds, so one second is the smallest unit and Go mirrors it. recovery/maximumAgeAfterDecodePreservesSourceTest and independent/independentRecoveryAtCapturedAgeBoundaryTest are the expected evidence.", + "rationale": "TypeScript src/internal/runtime-config.ts resolveRemoteLayerConfigResult admits the call with a stale-on-error maximum one second larger than configured, and Go go/policy.go ResolvePolicy adds one second to the resolved StaleOnErrorMaxAge. TypeScript config granularity is seconds, so one second is the smallest unit and Go mirrors it.", "typescript": { "edits": [ { @@ -1465,7 +1465,7 @@ "id": "M46", "case": "C45.captured-policy", "description": "Capture the static default recovery maximum at admission, ignoring the runtime overlay (in the policy profile this also hides overlay configuration errors)", - "rationale": "TypeScript src/internal/runtime-config.ts mergeKeyConfig takes the stale-on-error maximum from the static default only, ignoring the runtime overlay, and Go go/policy.go ResolvePolicy drops staleOnErrorMaxAgeSec from its runtime leaf merge loop. The admitted recovery maximum is therefore the static default even when the overlay changes it. The port has no constant to substitute for a generic policy, so ignoring the overlay is the faithful analog; in the policy profile the mutant is broader because it also hides overlay configuration errors. independent/independentRecoveryAtCapturedAgeBoundaryTest and recovery-read/physicalCapDoesNotClampLogicalRecoveryTest are the expected evidence.", + "rationale": "TypeScript src/internal/runtime-config.ts mergeKeyConfig takes the stale-on-error maximum from the static default only, ignoring the runtime overlay, and Go go/policy.go ResolvePolicy drops staleOnErrorMaxAgeSec from its runtime leaf merge loop. The admitted recovery maximum is therefore the static default even when the overlay changes it. The port has no constant to substitute for a generic policy, so ignoring the overlay is the faithful analog; in the policy profile the mutant is broader because it also hides overlay configuration errors.", "typescript": { "edits": [ { @@ -1497,7 +1497,7 @@ "id": "M47", "case": "C49.dark-source-reuse", "description": "Continue dark shadow work without waiting for the caller source", - "rationale": "TypeScript src/dialcache.ts DialCache.runShadowValidation replaces the branch that waits for the caller's source with an already-resolved undefined value, and Go go/shadow.go runShadow replaces the case that waits on the source's done channel with a default case. Dark shadow work therefore compares and fills with an undefined or zero value without waiting for the source. shadow/darkReadBeforeSourceWaitsForItTest (no dump while the loader is held) and shadow-layers/darkSourcePublishesLocalBeforeShadowWriteTest are the expected evidence.", + "rationale": "TypeScript src/dialcache.ts DialCache.runShadowValidation replaces the branch that waits for the caller's source with an already-resolved undefined value, and Go go/shadow.go runShadow replaces the case that waits on the source's done channel with a default case. Dark shadow work therefore compares and fills with an undefined or zero value without waiting for the source.", "typescript": { "edits": [ { @@ -1529,7 +1529,7 @@ "id": "M48", "case": "C54.deduplication", "description": "Admit a shadow job for an identity that already has a live job", - "rationale": "TypeScript src/dialcache.ts DialCache.scheduleShadowValidation and Go go/shadow.go scheduleShadow drop the live-job check from the admission guard. A shadow job is therefore admitted for an identity that already has one, subject only to capacity. admission/duplicateDropsBeforeCapacityIsFullTest and shadow-layers/deduplicatedJobKeepsIndependentCallerSourcesTest are the expected evidence in both ports.", + "rationale": "TypeScript src/dialcache.ts DialCache.scheduleShadowValidation and Go go/shadow.go scheduleShadow drop the live-job check from the admission guard. A shadow job is therefore admitted for an identity that already has one, subject only to capacity.", "typescript": { "edits": [ { @@ -1561,7 +1561,7 @@ "id": "M49", "case": "C54.instance-isolation", "description": "Admit one shadow job beyond the per-instance capacity", - "rationale": "TypeScript src/dialcache.ts DialCache.scheduleShadowValidation and Go go/shadow.go scheduleShadow make the capacity comparison strict. One shadow job beyond the per-instance capacity is therefore admitted. admission/fullInstanceDoesNotBlockOtherInstanceTest and shadow-layers/fullMixedInstanceDoesNotBlockAnotherInstanceTest are the expected evidence in both ports.", + "rationale": "TypeScript src/dialcache.ts DialCache.scheduleShadowValidation and Go go/shadow.go scheduleShadow make the capacity comparison strict. One shadow job beyond the per-instance capacity is therefore admitted.", "typescript": { "edits": [ { @@ -1593,7 +1593,7 @@ "id": "M50", "case": "W01.key-identity", "description": "Reject braces in keyType and id for untracked keys as well", - "rationale": "TypeScript src/key.ts DialCacheKey.constructor builds the untracked prefix through invalidationPrefix, which throws on braces, instead of joinUrnComponents, and Go go/protocol.go Keys always includes KeyType and ID among the hash-tag components instead of only for tracked keys. Braces in keyType or id are therefore rejected for untracked keys as well. quint-key-vectors 050, 052, 062, 064, 411 and 413 (untracked keys with braces expected valid) are the expected evidence in the TypeScript constructs test and Go TestProtocolKeys.", + "rationale": "TypeScript src/key.ts DialCacheKey.constructor builds the untracked prefix through invalidationPrefix, which throws on braces, instead of joinUrnComponents, and Go go/protocol.go Keys always includes KeyType and ID among the hash-tag components instead of only for tracked keys. Braces in keyType or id are therefore rejected for untracked keys as well.", "typescript": { "edits": [ { @@ -1625,7 +1625,7 @@ "id": "M51", "case": "W03.cohort-assignment", "description": "Admit a key whose cohort sample equals the configured ramp", - "rationale": "TypeScript src/internal/runtime-config.ts resolveLayerConfigResult and Go go/policy.go ResolvePolicy make the serving-layer cohort comparison inclusive, so a key whose cohort sample equals the configured ramp is admitted. The rampVectors pin hash samples, not admission, so detection is behavioral through runtime-boundaries/localCohortEqualityBypassesBeforeAboveSampleCachesTest and remoteCohortEqualityBypassesBeforeAboveSampleCachesTest, whose fixture ramps equal the driver key's FNV numerators. The shadow-cohort comparison is a separate site left to a follow-up.", + "rationale": "TypeScript src/internal/runtime-config.ts resolveLayerConfigResult and Go go/policy.go ResolvePolicy make the serving-layer cohort comparison inclusive, so a key whose cohort sample equals the configured ramp is admitted. The shadow-cohort comparison is a separate site left to a follow-up.", "typescript": { "edits": [ { @@ -1657,7 +1657,7 @@ "id": "M52", "case": "W04.untracked-decoder", "description": "Read and apply the entity watermark fence on untracked keys", - "rationale": "TypeScript src/internal/redis-cache.ts RedisCache.startRawPayloadRead always requests the watermark key instead of only for tracked keys, and Go go/protocol.go DecodeFrame applies a parsed watermark fence without consulting the tracked flag. The entity watermark fence is therefore read and applied on untracked keys. The TypeScript twin sits at read-request construction because TypeScript splits the decoders and its untracked decoder receives no watermark, while the Go twin sits at the decoder. Go TestProtocolDecoders untracked vectors (345 and its siblings) and the TypeScript witness layers:untracked-ignores-watermark plus layers replays are the expected evidence.", + "rationale": "TypeScript src/internal/redis-cache.ts RedisCache.startRawPayloadRead and Go go/engine.go execution.rawRead always request the grouped entity watermark, including for untracked keys. Go go/protocol.go DecodeFrame additionally applies an explicitly supplied watermark without consulting tracked, preserving the direct decoder-vector challenge. Both edits express the same wrong behavior at the actual cache-read and direct-codec API boundaries. Correct Go request construction otherwise supplies no watermark and masks the decoder-only fault from shared behavioral histories.", "typescript": { "edits": [ { @@ -1673,6 +1673,11 @@ }, "go": { "edits": [ + { + "path": "go/engine.go", + "before": "raw := startPending(func() (ReadResult, error) { return x.cache.settings.remote.Read(ctx, x.remoteKey, x.watermark) })", + "after": "watermark := \"{\" + escape(x.op.Identity.Namespace) + \":\" + escape(x.op.Identity.KeyType) + \":\" + escape(x.op.Identity.ID) + \"}#watermark\"\n\traw := startPending(func() (ReadResult, error) { return x.cache.settings.remote.Read(ctx, x.remoteKey, watermark) })" + }, { "path": "go/protocol.go", "before": "if tracked && fence != nil && stamp <= *fence {", @@ -1689,7 +1694,7 @@ "id": "M53", "case": "W06.envelope-markers", "description": "Leave the escape prefix in place before an escaped binary-zstd marker", - "rationale": "TypeScript src/internal/compression.ts decompressPayload and Go go/codec.go DecompressPayload stop treating the binary-zstd marker as an escapable byte. The escape prefix is therefore left in place before an escaped binary-zstd marker. Envelope vectors 008 (0002 to 02), 003 and 013 are the expected evidence in both ports.", + "rationale": "TypeScript src/internal/compression.ts decompressPayload and Go go/codec.go DecompressPayload stop treating the binary-zstd marker as an escapable byte. The escape prefix is therefore left in place before an escaped binary-zstd marker.", "typescript": { "edits": [ { @@ -1721,7 +1726,7 @@ "id": "M54", "case": "W06.envelope-markers", "description": "Strip a leading zero byte even when the next byte is not an envelope marker", - "rationale": "TypeScript src/internal/compression.ts decompressPayload and Go go/codec.go DecompressPayload strip a leading zero byte whenever a second byte exists, no longer requiring it to be an envelope marker. M53 shares this anchor with a different edit and stays a distinct entry. Envelope vectors 009 (0003 stays 0003) and 010 are the expected evidence in both ports.", + "rationale": "TypeScript src/internal/compression.ts decompressPayload and Go go/codec.go DecompressPayload strip a leading zero byte whenever a second byte exists, no longer requiring it to be an envelope marker. M53 shares this anchor with a different edit and stays a distinct entry.", "typescript": { "edits": [ { @@ -1753,7 +1758,7 @@ "id": "M55", "case": "W07.compression-selection", "description": "Store a compressed payload whose marked size ties the escaped raw size", - "rationale": "TypeScript src/internal/compression.ts compressPayload and Go go/codec.go CompressPayload make the size comparison strict, so a compressed payload whose marked size ties the escaped raw size is stored compressed instead of raw. The compressionWriteVectors tie rows, which record per-binding codec sizes (TypeScript 192, 193, 195, 196, 210, 211, 213 and 214; Go 219, 220, 222, 223, 264, 265, 267 and 268), are the expected evidence.", + "rationale": "TypeScript src/internal/compression.ts compressPayload and Go go/codec.go CompressPayload make the size comparison strict, so a compressed payload whose marked size ties the escaped raw size is stored compressed instead of raw.", "typescript": { "edits": [ { @@ -1785,7 +1790,7 @@ "id": "M56", "case": "C37.physical-cap", "description": "Write a tracked value for its full configured retention above the one-hour cap", - "rationale": "TypeScript src/internal/redis-cache.ts RedisCache.putWithLayer writes a tracked value with its configured TTL instead of the smaller of that and MAX_TRACKED_REDIS_VALUE_TTL_MS, and Go go/engine.go putRemote skips the one-hour clamp (tracked_ttl_clamped) for a tracked write. A tracked value is therefore retained for its full configured retention, which its watermark, kept for twice the cap, need not outlive. recovery-read/trackedPhysicalRetentionExpiresAtCapTest (write TTL 14400000 against 3600000) is the expected evidence in both ports.", + "rationale": "TypeScript src/internal/redis-cache.ts RedisCache.putWithLayer writes a tracked value with its configured TTL instead of the smaller of that and MAX_TRACKED_REDIS_VALUE_TTL_MS, and Go go/engine.go putRemote skips the one-hour clamp (tracked_ttl_clamped) for a tracked write. A tracked value is therefore retained for its full configured retention, which its watermark, kept for twice the cap, need not outlive.", "typescript": { "edits": [ { @@ -1812,6 +1817,262 @@ "portable" ] } + }, + { + "id": "M58", + "case": "C54.dump-ownership", + "description": "Release shadow capacity at the whole-job timeout before its raw work settles", + "rationale": "TypeScript runShadowValidation calls release from the whole-job timeout hook and Go runShadow deletes the matching registration after awaitDeadline reports the timeout. Both free the slot while the raw source/read/decode/dump/write work still runs, allowing a competing job too early. The edits keep abandonment and late-result suppression unchanged.", + "typescript": { + "edits": [ + { + "path": "src/dialcache.ts", + "before": " signalShadowTimeout();\n },", + "after": " signalShadowTimeout();\n release();\n }," + } + ], + "requiredDetections": [ + "generated", + "portable" + ] + }, + "go": { + "edits": [ + { + "path": "go/shadow.go", + "before": "if err != nil {\n\t\tverdict = shadowVerdict{outcome: \"timeout\"}\n\t}", + "after": "if err != nil {\n\t\tverdict = shadowVerdict{outcome: \"timeout\"}\n\t\tx.cache.mu.Lock()\n\t\tif x.cache.shadows[x.key] == f {\n\t\t\tdelete(x.cache.shadows, x.key)\n\t\t}\n\t\tx.cache.mu.Unlock()\n\t}" + } + ], + "requiredDetections": [ + "generated", + "portable" + ] + } + }, + { + "id": "M57", + "case": "C22.adapter-frame-expiry", + "description": "Report a frame at its maximum age as an unclassified miss instead of expired", + "rationale": "TypeScript src/internal/redis-cache.ts getWithResolvedConfig and Go go/engine.go readServing change the first maximum-age guard's miss diagnostic from expired to unclassified. Source fallback, refill authority and later reuse remain unchanged; the generated held-adapter history distinguishes the diagnostic at exactly 60000ms, before any source settlement.", + "typescript": { + "edits": [ + { + "path": "src/internal/redis-cache.ts", + "before": "if (frameAge.ageMs >= maximumAgeMs) {\n this.recordMiss(key, metricLayer, \"expired\");", + "after": "if (frameAge.ageMs >= maximumAgeMs) {\n this.recordMiss(key, metricLayer, \"unclassified\");" + } + ], + "requiredDetections": [ + "generated", + "portable" + ] + }, + "go": { + "edits": [ + { + "path": "go/engine.go", + "before": "if age >= maxAge {\n\t\tx.event(\"miss\", \"remote\", map[string]any{\"reason\": \"expired\"})", + "after": "if age >= maxAge {\n\t\tx.event(\"miss\", \"remote\", map[string]any{\"reason\": \"unclassified\"})" + } + ], + "requiredDetections": [ + "generated", + "portable" + ] + } + }, + { + "id": "M59", + "case": "C11.process-inspection", + "description": "Include process leaders in the reported follower count", + "rationale": "TypeScript src/dialcache.ts getCoalescingState adds the registered leader count to activeProcessFollowers; Go go/bindings.go GetCoalescingState counts one extra follower per registered flight. The snapshot reports initiating callers as followers while caller outcomes and sharing remain unchanged.", + "typescript": { + "edits": [ + { + "path": "src/dialcache.ts", + "before": "activeFollowers: this.activeProcessFollowers,", + "after": "activeFollowers: this.activeProcessFollowers + this.processFlights.size," + } + ], + "requiredDetections": [ + "generated", + "portable" + ] + }, + "go": { + "edits": [ + { + "path": "go/bindings.go", + "before": "state.ActiveFollowers += f.followers", + "after": "state.ActiveFollowers += f.followers + 1" + } + ], + "requiredDetections": [ + "generated", + "portable" + ] + } + }, + { + "id": "M60", + "case": "C11.process-inspection", + "description": "Measure the oldest process leader age with the application wall clock", + "rationale": "TypeScript src/dialcache.ts getCoalescingState uses Date.now instead of performance.now, while Go go/bindings.go GetCoalescingState uses Clock.WallMS instead of elapsedNow. Both subtract a monotonic registration instant from an unrelated application wall clock; the public snapshot changes under wall shifts.", + "typescript": { + "edits": [ + { + "path": "src/dialcache.ts", + "before": "Math.max(performance.now() - oldestFlight.startedAtMs, 0)", + "after": "Math.max(Date.now() - oldestFlight.startedAtMs, 0)" + } + ], + "requiredDetections": [ + "generated", + "portable" + ] + }, + "go": { + "edits": [ + { + "path": "go/bindings.go", + "before": "age := elapsedNow(c.settings.clock) - f.started", + "after": "age := time.Duration(c.settings.clock.WallMS())*time.Millisecond - f.started" + } + ], + "requiredDetections": [ + "generated", + "portable" + ] + } + }, + { + "id": "M61", + "case": "C54.read-ownership", + "description": "Ignore the bounded shadow read result while the raw adapter operation remains pending", + "rationale": "Both ports retain the read deadline and cancellation signal, but shadow validation waits for the raw result instead of consuming its bounded result. The C0 or C1 error therefore fails to appear at its own read deadline, while whole-job timeout and raw ownership continue independently.", + "typescript": { + "edits": [ + { + "path": "src/internal/redis-cache.ts", + "before": "result: bounded.then((result) => this.validateReadResult(key, result)),", + "after": "result: (unrefTimer ? (void bounded.catch(() => undefined), pending) : bounded).then((result) => this.validateReadResult(key, result))," + } + ], + "requiredDetections": [ + "generated", + "portable" + ] + }, + "go": { + "edits": [ + { + "path": "go/shadow.go", + "before": "bounded, raw := x.rawRead()\n\t\treads = append(reads, raw)", + "after": "bounded, raw := x.rawRead()\n\t\tbounded = raw\n\t\treads = append(reads, raw)" + } + ], + "requiredDetections": [ + "generated", + "portable" + ] + } + }, + { + "id": "M62", + "case": "C54.read-ownership", + "description": "Release shadow capacity at bounded read failure before the raw adapter operation settles", + "rationale": "Both ports remove the shadow registration once validation has produced its bounded read error, omitting the additional raw read wait. A competing key is admitted before that raw read settles; the existing cancellation and error result remain intact.", + "typescript": { + "edits": [ + { + "path": "src/dialcache.ts", + "before": "if (operationFinished && pendingRedisReads.size === 0) {", + "after": "if (operationFinished) {" + } + ], + "requiredDetections": [ + "generated", + "portable" + ] + }, + "go": { + "edits": [ + { + "path": "go/shadow.go", + "before": "\t\t\t\tfor _, r := range reads {\n\t\t\t\t\t<-r.done\n\t\t\t\t}\n", + "after": "" + } + ], + "requiredDetections": [ + "generated", + "portable" + ] + } + }, + { + "id": "M63", + "case": "C38.marker-transition", + "description": "Move an invalidation cutoff backwards when an older request arrives", + "rationale": "Both ports change their production invalidation Lua at the same boundary: use only the new proposal instead of the maximum of the stored and proposed cutoff. The generated Redis cohort and exact vector boundary execute each changed script against a private real Redis server.", + "typescript": { + "edits": [ + { + "path": "src/internal/redis-scripts.ts", + "before": "local watermark = math.max(current_watermark, proposed_watermark)", + "after": "local watermark = proposed_watermark" + } + ], + "requiredDetections": [ + "generated", + "portable" + ] + }, + "go": { + "edits": [ + { + "path": "go/redis_adapter.go", + "before": "local watermark = math.max(current_watermark, proposed_watermark)", + "after": "local watermark = proposed_watermark" + } + ], + "requiredDetections": [ + "generated", + "portable" + ] + } + }, + { + "id": "M64", + "case": "C39.numeric-limits", + "description": "Reject the maximum supported invalidation buffer", + "rationale": "Both ports change their production invalidation Lua at the same boundary: reject equality at the 31536000000ms maximum buffer. Client prevalidation remains unchanged; generated decimal inputs exercise the Lua itself. The generated Redis cohort and exact vector boundary execute each changed script against a private real Redis server.", + "typescript": { + "edits": [ + { + "path": "src/internal/redis-scripts.ts", + "before": "future_buffer_ms > ${MAX_SUPPORTED_DURATION_MS}", + "after": "future_buffer_ms >= ${MAX_SUPPORTED_DURATION_MS}" + } + ], + "requiredDetections": [ + "generated", + "portable" + ] + }, + "go": { + "edits": [ + { + "path": "go/redis_adapter.go", + "before": "future_buffer_ms > 31536000000", + "after": "future_buffer_ms >= 31536000000" + } + ], + "requiredDetections": [ + "generated", + "portable" + ] + } } ] } diff --git a/formal/profile-lint-baseline.json b/formal/profile-lint-baseline.json index e9ab1c28..0b64fa13 100644 --- a/formal/profile-lint-baseline.json +++ b/formal/profile-lint-baseline.json @@ -14,6 +14,7 @@ "instances", "layer_policy", "local_faults", + "local_grid", "local_storage", "markers", "metrics", @@ -28,7 +29,8 @@ "request_memo", "runtime_policy", "serving", - "shadow" + "shadow", + "shadow_reads" ], "profiles": [ { @@ -48,6 +50,7 @@ "clock::shiftWall", "metrics::advance", "metrics::begin", + "metrics::frameReadReceipt", "metrics::invalidate", "metrics::releaseDump", "metrics::releaseWrite", @@ -248,7 +251,7 @@ "libraryTransitions": [ "clock::advanceTicks", "instances::construct", - "serving::callLocal" + "local_grid::call" ], "compositionViolations": 0 }, @@ -263,6 +266,44 @@ "deadlines::settleLocal" ], "compositionViolations": 0 + }, + { + "id": "dark-layers", + "model": "formal/dialcache-dark-layers-conformance.qnt", + "module": "dialcache_dark_layers_conformance", + "libraryTransitions": [ + "clock::shiftWall", + "flights::inspectProcess", + "flights::recordIdentity", + "flights::recordInspectionAdmission", + "remote_frames::seedAged", + "serving::closeScope", + "serving::invalidate", + "shadow::advanceHeld", + "shadow::beginDark", + "shadow::releaseDump", + "shadow::releaseLoad", + "shadow::releaseRead", + "shadow::releaseWrite", + "shadow::settleHeld" + ], + "compositionViolations": 0 + }, + { + "id": "shadow-read-deadlines", + "model": "formal/dialcache-shadow-read-deadlines-conformance.qnt", + "module": "dialcache_shadow_read_deadlines_conformance", + "libraryTransitions": [ + "remote_frames::seedFrame", + "shadow::releaseDump", + "shadow::releaseWrite", + "shadow::settleHeld", + "shadow_reads::advance", + "shadow_reads::begin", + "shadow_reads::releaseLoad", + "shadow_reads::releaseRead" + ], + "compositionViolations": 0 } ] } diff --git a/formal/profiles.json b/formal/profiles.json index ae8bbecc..4a32f6bb 100644 --- a/formal/profiles.json +++ b/formal/profiles.json @@ -23,7 +23,7 @@ }, { "id": "effects", - "version": 2, + "version": 3, "definition": "formal/BEHAVIOR.md", "model": "formal/dialcache-effects-conformance.qnt", "smoke": "formal/effects-smoke.itf.json", @@ -262,6 +262,41 @@ "formal/replay/witnesses/source-budgets.mjs", "formal/replay/witnesses/public-prefix.mjs" ] + }, + { + "id": "dark-layers", + "version": 2, + "definition": "formal/BEHAVIOR.md", + "model": "formal/dialcache-dark-layers-conformance.qnt", + "smoke": "formal/dark-layers-smoke.itf.json", + "inputEncoding": "explicit-v1", + "witnessSources": [ + "formal/witnesses.mjs", + "formal/replay/witnesses/index.mjs", + "formal/replay/witnesses/trace.mjs", + "formal/replay/witnesses/evidence.mjs", + "formal/replay/witnesses/labels.mjs", + "formal/replay/witnesses/dark-layers.mjs", + "formal/replay/witnesses/public-prefix.mjs" + ] + }, + { + "id": "shadow-read-deadlines", + "version": 1, + "definition": "formal/BEHAVIOR.md", + "model": "formal/dialcache-shadow-read-deadlines-conformance.qnt", + "smoke": "formal/shadow-read-deadlines-smoke.itf.json", + "inputEncoding": "explicit-v1", + "witnessSources": [ + "formal/witnesses.mjs", + "formal/replay/witnesses/index.mjs", + "formal/replay/witnesses/trace.mjs", + "formal/replay/witnesses/evidence.mjs", + "formal/replay/witnesses/labels.mjs", + "formal/replay/witnesses/shadow-read-deadlines.mjs", + "formal/replay/witnesses/public-prefix.mjs" + ], + "limits": "Eight callers, two keys, one instance and one shadow slot; 10 ms source/job budget, 5 or 20 ms read budgets, held C0/C1, decode, dump and write gates. No local/request layers, runtime invalidity, recovery or wall shifts." } ], "implementations": [ @@ -282,7 +317,9 @@ "runtime-boundaries", "shadow-layers", "local-clock", - "source-budgets" + "source-budgets", + "dark-layers", + "shadow-read-deadlines" ], "limits": "See formal/TEST-MAP.md; finite scenarios and bounded profiles do not cover every interaction." }, @@ -303,7 +340,9 @@ "runtime-boundaries", "shadow-layers", "local-clock", - "source-budgets" + "source-budgets", + "dark-layers", + "shadow-read-deadlines" ], "definition": "go/README.md", "limits": "Same generated histories and portable scenarios as TypeScript, all protocol vectors, plus native Redis/Valkey/Cluster and cross-language integration. Claims require current corpus and witness fingerprints; finite evidence does not prove every possible schedule. Native API and value adaptations are documented in the Go README and parity ledger." @@ -318,6 +357,8 @@ "formal/replay/coordinator.mjs", "formal/replay/core.d.mts", "formal/replay/core.mjs", + "formal/replay/divergence.d.mts", + "formal/replay/divergence.mjs", "formal/replay/effects.d.mts", "formal/replay/effects.mjs", "formal/replay/features.d.mts", @@ -328,6 +369,8 @@ "formal/replay/local-clock.mjs", "formal/replay/observation.d.mts", "formal/replay/observation.mjs", + "formal/replay/profiles/dark-layers.d.mts", + "formal/replay/profiles/dark-layers.mjs", "formal/replay/profiles/local-failure.d.mts", "formal/replay/profiles/local-failure.mjs", "formal/replay/profiles/recovery-read.d.mts", @@ -336,6 +379,8 @@ "formal/replay/profiles/runtime-boundaries.mjs", "formal/replay/profiles/shadow-layers.d.mts", "formal/replay/profiles/shadow-layers.mjs", + "formal/replay/profiles/shadow-read-deadlines.d.mts", + "formal/replay/profiles/shadow-read-deadlines.mjs", "formal/replay/profiles/source-budgets.d.mts", "formal/replay/profiles/source-budgets.mjs", "formal/replay/protocol.schema.json", @@ -349,6 +394,8 @@ "formal/replay/validation.mjs", "formal/replay/witnesses/admission.d.mts", "formal/replay/witnesses/admission.mjs", + "formal/replay/witnesses/dark-layers.d.mts", + "formal/replay/witnesses/dark-layers.mjs", "formal/replay/witnesses/effects-authority.d.mts", "formal/replay/witnesses/effects-authority.mjs", "formal/replay/witnesses/effects.d.mts", @@ -391,6 +438,8 @@ "formal/replay/witnesses/shadow-diagnostics.mjs", "formal/replay/witnesses/shadow-layers.d.mts", "formal/replay/witnesses/shadow-layers.mjs", + "formal/replay/witnesses/shadow-read-deadlines.d.mts", + "formal/replay/witnesses/shadow-read-deadlines.mjs", "formal/replay/witnesses/shadow.d.mts", "formal/replay/witnesses/shadow.mjs", "formal/replay/witnesses/source-budgets.d.mts", diff --git a/formal/quint-envelope-vectors.json b/formal/quint-envelope-vectors.json index b7ba4dab..14528218 100644 --- a/formal/quint-envelope-vectors.json +++ b/formal/quint-envelope-vectors.json @@ -3,7 +3,7 @@ "provenance": { "model": "formal/dialcache-envelope-vectors.qnt", "sourceSha256": { - "formal/dialcache-envelope-vectors.qnt": "aa0c584f679ccfb5b472e443018f5f6991a390daeb4cc911f15e06d98affd88b", + "formal/dialcache-envelope-vectors.qnt": "1f636d98ce1576237f25fd4c5ba5831ebe2268c958b131e1ead9c1d3280fd3c5", "formal/wire-text.qnt": "c78f8c674f4e1470b0d2b605b209d82e3aab0e35fb4e0efae813247bf093eb27", "formal/generate-envelope-vectors.mjs": "0df0a70737886cc5bc3c135d156e49db5f3513ed0a8b0e8c4c04d5785d309b5a" } diff --git a/formal/quint-frame-vectors.json b/formal/quint-frame-vectors.json index d8fbb885..e7df9e72 100644 --- a/formal/quint-frame-vectors.json +++ b/formal/quint-frame-vectors.json @@ -3,10 +3,10 @@ "provenance": { "model": "formal/dialcache-frame-vectors.qnt", "sourceSha256": { - "formal/dialcache-frame-vectors.qnt": "d7cfaff472fce8888563ce81f62ca3b4bed1a76d51b97b46543a99362548cff8", + "formal/dialcache-frame-vectors.qnt": "128094c8b54e28796d81f0886f94ccb4c06fc6f5aa68d6f42d2bfd0af8a4ad21", "formal/wire-text.qnt": "c78f8c674f4e1470b0d2b605b209d82e3aab0e35fb4e0efae813247bf093eb27", "formal/generate-frame-vectors.mjs": "f27411412fc3a2593270d4747983305543ec5e8991027648558ef7d3ace0b3da", - "formal/cache-rules.qnt": "cc37125b108b6e51011cfa4ff5c0561bb9498cbedd3ffefb0cabd79f3975216c" + "formal/cache-rules.qnt": "ae2a1748e911402c6818e8294265764e06d18d050a89219df7d55faa83fb06e5" } }, "frameVectors": [ diff --git a/formal/quint-key-vectors.json b/formal/quint-key-vectors.json index 60f2c494..563da173 100644 --- a/formal/quint-key-vectors.json +++ b/formal/quint-key-vectors.json @@ -3,7 +3,7 @@ "provenance": { "model": "formal/dialcache-key-protocol.qnt", "sourceSha256": { - "formal/dialcache-key-protocol.qnt": "c1a1c4e692d4f60f3ed0be0242c62c28e473bacdfdbdf0f634569cf22683548f", + "formal/dialcache-key-protocol.qnt": "3e08df8a9af2182b66e8927542edabe5f82cef8d833563747fc68883660be1a2", "formal/generate-key-vectors.mjs": "05764a348b161ac01a30d7a723e5018cad200020a2e95a0888b544f2401fbeea", "formal/wire-text.qnt": "c78f8c674f4e1470b0d2b605b209d82e3aab0e35fb4e0efae813247bf093eb27", "formal/cohort-boundaries.qnt": "700f0b9ee734f2511b3c7f10138b9636cef93bc1045b653479a05be1766e3407" diff --git a/formal/quint-pool.mjs b/formal/quint-pool.mjs index c97f32cb..43b7cb20 100644 --- a/formal/quint-pool.mjs +++ b/formal/quint-pool.mjs @@ -1,5 +1,7 @@ import { spawn } from 'node:child_process'; -import { availableParallelism, totalmem } from 'node:os'; +import { closeSync, mkdtempSync, openSync, readFileSync, rmSync } from 'node:fs'; +import { availableParallelism, tmpdir, totalmem } from 'node:os'; +import { join } from 'node:path'; // Process-level concurrency for the formal scripts that spawn Quint. // @@ -42,30 +44,48 @@ export class CommandFailure extends Error { } } -// Spawn with piped stdout/stderr and collect both. Never rejects: a spawn error -// (for example a missing binary) or a timeout is reported in `error`, and -// `status`/`signal` come from the child's close event. -export function spawnBuffered(command, args, { cwd, env, timeoutMs } = {}) { - return new Promise(settle => { - const started = performance.now(); - let stdout = '', stderr = '', error, timer; - const child = spawn(command, args, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] }); - child.stdout.setEncoding('utf8'); - child.stderr.setEncoding('utf8'); - child.stdout.on('data', chunk => { stdout += chunk; }); - child.stderr.on('data', chunk => { stderr += chunk; }); - if (timeoutMs !== undefined) { - timer = setTimeout(() => { - error ??= Object.assign(new Error(`Timed out after ${timeoutMs} ms`), { code: 'ETIMEDOUT' }); - child.kill('SIGTERM'); - }, timeoutMs); - } - child.once('error', cause => { error ??= cause; }); - child.once('close', (status, signal) => { - clearTimeout(timer); - settle({ status, signal, error, stdout, stderr, durationMs: performance.now() - started }); +// Quint calls process.exit after printing failed tests. Its piped output can +// lose buffered diagnostics at that exit; regular files make Node's writes +// synchronous. Read both streams only after close, then remove the private files. +// Failures (including capture failures) stay in `error`, never property evidence. +export async function spawnBuffered(command, args, { cwd, env, timeoutMs } = {}) { + const started = performance.now(); + const result = { status: null, signal: null, error: undefined, stdout: '', stderr: '', durationMs: 0 }; + const descriptors = []; + let directory; + try { + directory = mkdtempSync(join(tmpdir(), 'dialcache-quint-output-')); + const paths = ['stdout', 'stderr'].map(name => join(directory, name)); + for (const path of paths) descriptors.push(openSync(path, 'wx')); + await new Promise(settle => { + const child = spawn(command, args, { cwd, env, stdio: ['ignore', ...descriptors] }); + let timer; + if (timeoutMs !== undefined) { + timer = setTimeout(() => { + result.error ??= Object.assign(new Error(`Timed out after ${timeoutMs} ms`), { code: 'ETIMEDOUT' }); + child.kill('SIGTERM'); + }, timeoutMs); + } + child.once('error', cause => { result.error ??= cause; }); + child.once('close', (status, signal) => { + clearTimeout(timer); + Object.assign(result, { status, signal }); + settle(); + }); }); - }); + [result.stdout, result.stderr] = paths.map(path => readFileSync(path, 'utf8')); + } catch (error) { + result.error ??= error; + } finally { + for (const descriptor of descriptors) { + try { closeSync(descriptor); } catch (error) { result.error ??= error; } + } + if (directory) { + try { rmSync(directory, { recursive: true, force: true }); } catch (error) { result.error ??= error; } + } + result.durationMs = performance.now() - started; + } + return result; } export function formatGroup(title, ...texts) { diff --git a/formal/redis-vector-server.mjs b/formal/redis-vector-server.mjs new file mode 100644 index 00000000..c1a787cc --- /dev/null +++ b/formal/redis-vector-server.mjs @@ -0,0 +1,32 @@ +import { randomUUID } from 'node:crypto'; +import { spawnSync } from 'node:child_process'; + +// Each mutation shard owns one real server and destroys only that container. +// No user endpoint is accepted: fixture setup must never touch user data. +export function startRedisVectorServer() { + const name = `dialcache-formal-${randomUUID()}`; + const docker = args => { + const result = spawnSync('docker', args, { encoding: 'utf8', timeout: 120_000 }); + if (result.error || result.status !== 0) throw new Error(`Redis vector infrastructure: docker ${args[0]} failed: ${result.error?.message ?? result.stderr}`); + return result.stdout.trim(); + }; + const close = () => docker(['rm', '-f', name]); + let created = false; + try { + docker(['run', '--detach', '--name', name, '--publish', '127.0.0.1::6379', 'redis:6.2-alpine', 'redis-server', '--save', '', '--appendonly', 'no']); + created = true; + const address = docker(['port', name, '6379/tcp']); + if (!/^127\.0\.0\.1:\d+$/.test(address)) throw new Error('Unexpected private Redis port binding'); + const until = Date.now() + 20_000; + while (true) { + const ping = spawnSync('docker', ['exec', name, 'redis-cli', 'PING'], { encoding: 'utf8', timeout: 5000 }); + if (ping.status === 0 && ping.stdout.trim() === 'PONG') break; + if (Date.now() >= until) throw new Error('Private Redis server did not become ready'); + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100); + } + return { url: `redis://${address}`, image: docker(['inspect', '--format', '{{.Image}}', name]), close }; + } catch (error) { + if (created) close(); + throw error; + } +} diff --git a/formal/replay/bindings.mjs b/formal/replay/bindings.mjs index 17d83e39..e540441c 100644 --- a/formal/replay/bindings.mjs +++ b/formal/replay/bindings.mjs @@ -1,4 +1,4 @@ -import assert from "node:assert/strict"; +import { assertObservation } from "./divergence.mjs"; import { profiles, parseTrace, featureInput, assertFeatureObservation } from "./features.mjs"; import * as effects from "./effects.mjs"; import * as localClock from "./local-clock.mjs"; @@ -87,7 +87,7 @@ export function bindTrace(name, raw, path) { return effects.inputsFor(input, observed, environment); }, assert(index, observed) { - assert.deepEqual(effects.project(observed), expected[index]); + assertObservation(effects.project(observed), expected[index]); }, }; } diff --git a/formal/replay/coordinator.d.mts b/formal/replay/coordinator.d.mts index 62c83007..19361ed0 100644 --- a/formal/replay/coordinator.d.mts +++ b/formal/replay/coordinator.d.mts @@ -1,7 +1,18 @@ -import type { Binding } from "./bindings.mjs"; +import type { Divergence } from "./divergence.mjs"; export const protocolVersion: 1; export const settlement: "causally-ready-v1"; +export interface ReplayRecording { + path: string; + divergences: Divergence[]; + completed: boolean; + /** Last observation that passed shape and settlement checks; -1 before any. */ + lastStep: number; + error?: string; +} export class ReplayCoordinator { + constructor(options?: { record?: boolean; onRecord?: (recording: ReplayRecording) => void }); dispatch(request: unknown): Record; + recording(session: string): ReplayRecording | undefined; + abort(session: string, error: unknown): void; } diff --git a/formal/replay/coordinator.mjs b/formal/replay/coordinator.mjs index 388b629a..2cb8e2d8 100644 --- a/formal/replay/coordinator.mjs +++ b/formal/replay/coordinator.mjs @@ -1,10 +1,11 @@ import { AssertionError } from "node:assert"; -import { readFileSync } from "node:fs"; +import { appendFileSync, readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { profileActions, bindTrace } from "./bindings.mjs"; import { parseJSON, replayLines } from "./validation.mjs"; import { assertSchema, schemaViolation } from "./schema.mjs"; import { SettlementLedger, receiptDefinition } from "./settlement.mjs"; +import { diffPaths, isObservationComparison } from "./divergence.mjs"; export const protocolVersion = 1; export const settlement = "causally-ready-v1"; @@ -15,6 +16,39 @@ export class ReplayCoordinator { #sessions = new Map(); #nextSession = 0; #lastRequest = 0; + #record; + #onRecord; + #recordings = new Map(); + + constructor({ record = false, onRecord } = {}) { + this.#record = record; + this.#onRecord = onRecord; + } + + // Recordings are deliberately outside the protocol. Existing drivers keep + // exact response shapes and never receive expectations or comparison paths. + recording(session) { + const value = this.#sessions.get(session)?.recording ?? this.#recordings.get(session); + return value === undefined ? undefined : structuredClone(value); + } + + #finish(sessionId, completed, error) { + const session = this.#sessions.get(sessionId); + this.#sessions.delete(sessionId); + if (session?.recording === undefined) return; + const record = { ...session.recording, completed, ...(error === undefined ? {} : { error }) }; + this.#recordings.set(sessionId, record); + this.#onRecord?.(structuredClone(record)); + } + + // In-process drivers can name an application failure before discarding the + // session. JSONL drivers' discard retains the step; their test log carries + // the native failure. Neither is a completed comparison history. + abort(session, error) { + if (!this.#sessions.has(session)) return; + const current = this.#sessions.get(session); + this.#finish(session, false, `${current.trace.path} step ${current.index} action ${current.trace.steps[current.index].action}: ${error instanceof Error ? error.message : String(error)}`); + } dispatch(request) { assertSchema(request, "request"); @@ -39,7 +73,8 @@ export class ReplayCoordinator { // driver actually ran, the core session's wall clock alone. The // local-clock driver reports the real process clock, which no rule reads. const ledger = binding.settlement === "none" ? undefined : new SettlementLedger(binding.fixture, binding.setup); - this.#sessions.set(session, { binding, trace, index: 0, ledger }); + this.#sessions.set(session, { binding, trace, index: 0, ledger, + ...(this.#record ? { recording: { path: trace.path, divergences: [], completed: false, lastStep: -1 } } : {}) }); // `observation` and `receipt` name the $defs definitions the driver's // records must satisfy, so a port can validate them locally before each // round trip; a null receipt means the session carries none. @@ -50,7 +85,8 @@ export class ReplayCoordinator { }; } if (request.op === "discard") { - if (!this.#sessions.delete(request.session)) throw new Error("Unknown replay session"); + if (!this.#sessions.has(request.session)) throw new Error("Unknown replay session"); + this.abort(request.session, "Replay discarded before observation"); return { discarded: true }; } @@ -86,32 +122,42 @@ export class ReplayCoordinator { // an assertion raised while comparing observations; parsing, transport, // mapping and lifecycle errors retain their infrastructure diagnostics. if (!(cause instanceof AssertionError)) throw cause; - const expected = JSON.stringify(cause.expected) ?? '{"absent":true}'; - const actual = JSON.stringify(cause.actual) ?? '{"absent":true}'; - throw new Error(`Observation mismatch\nexpected: ${expected}\nactual: ${actual}`, { cause }); + if (session.recording !== undefined) { + if (!isObservationComparison(cause)) throw new Error(`Observation projection assertion did not compare a complete record: ${cause.message}`, { cause }); + session.recording.divergences.push({ step: index, action: trace.steps[index].action, paths: diffPaths(cause.expected, cause.actual) }); + } else { + const expected = JSON.stringify(cause.expected) ?? '{"absent":true}'; + const actual = JSON.stringify(cause.actual) ?? '{"absent":true}'; + throw new Error(`Observation mismatch\nexpected: ${expected}\nactual: ${actual}`, { cause }); + } } + if (session.recording !== undefined) session.recording.lastStep = index; const nextIndex = index + 1; if (nextIndex === trace.steps.length) { - this.#sessions.delete(request.session); + this.#finish(request.session, true); return { complete: true, steps: trace.steps.length }; } // Only the explicit action descriptor, actual observations, and actual // environment reach the mapping. Predictions never choose commands. + session.index = nextIndex; const inputs = binding.commands(nextIndex, request.observed, request.environment); for (const input of inputs) assertSchema(input, "command"); if (inputs.length === 0) throw new Error("Replay action produced no command"); ledger?.issue(inputs); - session.index = nextIndex; return { complete: false, index: nextIndex, inputs }; } catch (cause) { - this.#sessions.delete(request.session); - throw new Error(`${trace.path} step ${index} action ${trace.steps[index].action}: ${cause.message}`); + const error = new Error(`${trace.path} step ${session.index} action ${trace.steps[session.index].action}: ${cause.message}`); + this.#finish(request.session, false, error.message); + throw error; } } } if (process.argv[1] === fileURLToPath(import.meta.url)) { - const coordinator = new ReplayCoordinator(); + const recordingPath = process.env.DIALCACHE_REPLAY_DIVERGENCES; + const coordinator = new ReplayCoordinator(recordingPath === undefined ? {} : { + record: true, onRecord: record => appendFileSync(recordingPath, `${JSON.stringify(record)}\n`), + }); const lines = replayLines(process.stdin); for await (const line of lines) { let request; diff --git a/formal/replay/core.d.mts b/formal/replay/core.d.mts index 9c743764..276f280b 100644 --- a/formal/replay/core.d.mts +++ b/formal/replay/core.d.mts @@ -2,7 +2,9 @@ import type { Policy } from "../../test/formal/behavior-driver.js"; export declare const actionNames: readonly ["init", "bumpSource", "outsideCall", "requestLocalPair", "localCall", "coalescedLocalPair", "remoteCall", "invalidateRemote", "remoteReadFailureCall"]; export type ActionName = typeof actionNames[number]; export declare const observationFields: readonly ["sourceVersion", "lastResult", "outsideLoaderCalls", "requestLoaderCalls", "localLoaderCalls", "coalescedLoaderCalls", "remoteLoaderCalls", "redisReads", "redisWrites"]; -export type Observation = Pick; +export type Observation = Omit, "lastResult"> & { + lastResult: number | { absent: true }; +}; export interface Snapshot { sourceVersion: number; lastResult: number; diff --git a/formal/replay/core.mjs b/formal/replay/core.mjs index a5544f79..4c90deb5 100644 --- a/formal/replay/core.mjs +++ b/formal/replay/core.mjs @@ -1,4 +1,4 @@ -import assert from "node:assert/strict"; +import { assertObservation } from "./divergence.mjs"; import { assertInputMetadata, record, itfInteger, itfSignedInteger } from "./itf.mjs"; export const actionNames = [ "init", "bumpSource", "outsideCall", "requestLocalPair", "localCall", @@ -98,5 +98,5 @@ export function expectedCoreObservation(state) { } export function assertCoreObservation(state, observed) { - assert.deepEqual(observed, expectedCoreObservation(state)); + assertObservation(observed, expectedCoreObservation(state)); } diff --git a/formal/replay/divergence.d.mts b/formal/replay/divergence.d.mts new file mode 100644 index 00000000..87b1c19a --- /dev/null +++ b/formal/replay/divergence.d.mts @@ -0,0 +1,5 @@ +export interface Divergence { step: number; action: string; paths: string[] } +export function assertObservation(actual: unknown, expected: unknown): void; +export function isObservationComparison(error: unknown): error is import("node:assert").AssertionError; +export function diffPaths(expected: unknown, actual: unknown, prefix?: string): string[]; +export function countingPaths(fields: readonly string[], at: readonly string[], before?: readonly string[]): string[]; diff --git a/formal/replay/divergence.mjs b/formal/replay/divergence.mjs new file mode 100644 index 00000000..630393cd --- /dev/null +++ b/formal/replay/divergence.mjs @@ -0,0 +1,56 @@ +import assert, { AssertionError } from "node:assert/strict"; +import { isDeepStrictEqual } from "node:util"; + +const observationComparison = Symbol("complete observation comparison"); + +// Projection may itself assert a value's domain or an event's labels. Only +// this final comparison has both complete records and can safely be recorded +// while continuing: otherwise an unexamined counter could look newly wrong +// at the next step. Arguments are projected before this function is entered. +export function assertObservation(actual, expected) { + try { assert.deepEqual(actual, expected); } + catch (error) { + if (error instanceof AssertionError) Object.defineProperty(error, observationComparison, { value: true }); + throw error; + } +} +export const isObservationComparison = error => error instanceof AssertionError && error[observationComparison] === true; + +// Paths name the record actually compared by a binding, including array +// indices. A scalar comparison has no named field and is recorded as `$`; +// it cannot establish evidence for an unrelated observation field. +export function diffPaths(expected, actual, prefix = "") { + if (isDeepStrictEqual(expected, actual)) return []; + const composite = value => value !== null && typeof value === "object"; + const path = key => prefix ? `${prefix}.${key}` : String(key); + if (composite(expected) && composite(actual) && Array.isArray(expected) === Array.isArray(actual)) { + const keys = Array.isArray(expected) + ? [...Array(Math.max(expected.length, actual.length)).keys()] + : Object.keys({ ...expected, ...actual }).sort(); + const paths = keys.flatMap(key => Object.hasOwn(expected, key) !== Object.hasOwn(actual, key) + ? [path(key)] : diffPaths(expected[key], actual[key], path(key))); + if (Array.isArray(expected) && expected.length !== actual.length) paths.push(path("length")); + return paths.length ? paths : [prefix || "$"]; + } + return [prefix || "$"]; +} + +const counters = new Set([ + "o.loaders", "o.reads", "o.loads", "o.dumps", "o.writes", "o.policyCalls", "o.invalidations", "o.classifications", "o.comparisons", + "d.configErrors", "d.warnings", + // Effects and local-clock compare these same counters without an `o` + // wrapper. Paths retain the binding's spelling in every evidence packet. + "loaders", "reads", "loads", "dumps", "writes", "policyCalls", "invalidations", "classifications", "comparisons", + // Core's counters are also flat; an omitted earlier publication must not + // count again merely because redisWrites is still wrong at a later call. + "outsideLoaderCalls", "requestLoaderCalls", "localLoaderCalls", "coalescedLoaderCalls", "remoteLoaderCalls", "redisReads", "redisWrites", +]); +const within = (path, field) => path === field || path.startsWith(`${field}.`); + +// Consequences count whenever they differ at the requested checkpoint. A +// cumulative counter counts only when it was not already divergent at the +// immediately preceding observation, even if an earlier step differed too. +export function countingPaths(fields, at, before = []) { + return at.filter(path => fields.some(field => within(path, field) + && (!counters.has(field) || !before.some(previous => within(previous, field))))); +} diff --git a/formal/replay/effects.d.mts b/formal/replay/effects.d.mts index f7a38d79..2a7051cf 100644 --- a/formal/replay/effects.d.mts +++ b/formal/replay/effects.d.mts @@ -3,7 +3,7 @@ export const actions: readonly string[]; export const observedFields: readonly string[]; export interface Event { event: string; location: string; detail: string; amount: number } export interface ReadContext { index: number; timeoutMs: number; aborted: boolean } -// The observation the version-2 contract asserts at a step, parsed from the composed layout (s.o, s.io, s.events) or the retired one. +// The observation versions 2 and 3 assert at a step, parsed from the composed layout (s.o, s.io, s.events) or the retired one. export type Expected = Record<"loaders"|"reads"|"writes"|"invalidations"|"loads"|"dumps"|"policyCalls", number> & { calls: number[]; events: Event[]; writeTtls: number[]; readAborts: number[]; readContexts: ReadContext[] }; export interface Step { action: string; choice: number; expected: Expected } export interface Trace { path: string; steps: Step[] } @@ -11,7 +11,7 @@ export function parseTrace(raw: unknown, path: string): Trace; export interface EffectsDescriptor { explicitInputs: true; parseTrace: typeof parseTrace; actions: Record } export const effectsDescriptor: EffectsDescriptor; export function fixtureFor(mode: number): Fixture; -// The choice is read only for the actions that record one (init, the reply, the budget policy, the observer fault, the loader and read settlements). +// The choice is read only for the actions that record one (init, wall advance, the reply, the budget policy, the observer fault, the loader and read settlements). export function inputsFor(step: { action: string; choice?: number }, observed: Observation, environment: { wallMs: number }): Input[]; export function project(observed: Observation): Record; export function expectedObservations(trace: Trace): Expected[]; diff --git a/formal/replay/effects.mjs b/formal/replay/effects.mjs index 0987593e..e56fb755 100644 --- a/formal/replay/effects.mjs +++ b/formal/replay/effects.mjs @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { assertSubset } from "./validation.mjs"; import { assertInputMetadata, itfInteger, itfSignedInteger, record } from "./itf.mjs"; -export const actions = ["init", "beginCall", "resolveLoader", "rejectLoader", "releaseRead", "failRead", "releaseLoad", "failLoad", "releaseDump", "failDump", "releaseWrite", "failWrite", "seedRemote", "tick", "jumpClock", "rollbackWall", "observerFault", "readBudgetPolicy", "adapterReply", "invalidate", "futureFence"]; +export const actions = ["init", "beginCall", "resolveLoader", "rejectLoader", "releaseRead", "failRead", "releaseLoad", "failLoad", "releaseDump", "failDump", "releaseWrite", "failWrite", "seedRemote", "tick", "jumpClock", "rollbackWall", "advanceWall", "observerFault", "readBudgetPolicy", "adapterReply", "invalidate", "futureFence"]; export const observedFields = ["loaders", "reads", "writes", "invalidations", "loads", "dumps", "policyCalls"]; const eventNames = ["request", "disabled", "miss", "error", "coalesced", "invalidation", "get", "fallback", "serialization", "futureOffset", "size", "storedSize", "writeDispatch"]; const timedEvents = new Set(["get", "fallback", "serialization", "futureOffset"]); @@ -13,7 +13,8 @@ const timedEvents = new Set(["get", "fallback", "serialization", "futureOffset"] const sharedToEffectsCode = { 0: 0, 1: 1, 3: 2, 4: 3 }; // The fixture's remote TTL, the retention every write carries. const REMOTE_TTL_MS = 60000; -const chosenActions = new Set(["init", "adapterReply", "readBudgetPolicy", "observerFault", "resolveLoader", "rejectLoader", "releaseRead", "failRead"]); +const chosenActions = new Set(["init", "advanceWall", "adapterReply", "readBudgetPolicy", "observerFault", "resolveLoader", "rejectLoader", "releaseRead", "failRead"]); +const wallAdvances = [1, 59999, 60000]; const choiceBounds = { init: [0, 5], readBudgetPolicy: [0, 4], observerFault: [0, 1], adapterReply: [1, 16] }; function parseEvents(raw, context) { @@ -89,6 +90,7 @@ export function parseTrace(value, path) { if (chosen) { if (encoded < 0) throw new Error(`${context}: missing effect choice`); choice = encoded; + if (action === "advanceWall" && !wallAdvances.includes(choice)) throw new Error(`${context}: unsupported wall advance`); const bounds = choiceBounds[action]; if (bounds && (choice < bounds[0] || choice > bounds[1])) throw new Error(`${context}: unsupported effect choice`); } else if (encoded !== -1) throw new Error(`${context}: unexpected effect choice`); @@ -105,7 +107,7 @@ export function parseTrace(value, path) { // with: the actions and their choice domains, over the record above. export const effectsDescriptor = { explicitInputs: true, parseTrace, actions: Object.fromEntries(actions.filter(name => name !== "init").map(name => [name, - chosenActions.has(name) ? { choices: choiceBounds[name] ? Array.from({ length: choiceBounds[name][1] - choiceBounds[name][0] + 1 }, (_, i) => choiceBounds[name][0] + i) : "index" } : {}])) }; + name === "advanceWall" ? { choices: wallAdvances } : chosenActions.has(name) ? { choices: choiceBounds[name] ? Array.from({ length: choiceBounds[name][1] - choiceBounds[name][0] + 1 }, (_, i) => choiceBounds[name][0] + i) : "index" } : {}])) }; // Concrete JSON encodings of the model's semantic reply classes. Timestamps // come from the controlled external clock, never expected model state. function adapterReply(choice, stamp) { @@ -146,6 +148,7 @@ export function inputsFor(step, observed, environment) { case "readBudgetPolicy": return [{ op: "policy", value: step.choice === 0 ? {} : { remoteReadTimeoutMs: [0, 10, 20, 30, 50][step.choice] } }]; case "adapterReply": return [{ op: "adapterReply", value: adapterReply(step.choice, environment.wallMs) }]; case "observerFault": return [{ op: "faults", value: { observer: step.choice === 1 } }]; + case "advanceWall": return [{ op: "shiftWall", ms: step.choice }]; case "rollbackWall": return [{ op: "shiftWall", ms: -1000 }]; case "invalidate": return [{ op: "invalidate" }]; case "futureFence": return [{ op: "invalidate", futureBufferMs: 20 }]; diff --git a/formal/replay/features.d.mts b/formal/replay/features.d.mts index 43ee8d1f..6240b414 100644 --- a/formal/replay/features.d.mts +++ b/formal/replay/features.d.mts @@ -1,7 +1,8 @@ import type { Observation, Input } from "../../test/formal/behavior-driver.js"; import type { Profile } from "../../test/formal/feature-profile.js"; export type Projected = Omit & { calls: number[] }; -export interface Diagnostics { warnings: number; ages: number[]; coalesced: string[]; fallbackErrors: string[]; configErrors?: number; futureOffsets?: Array<{ layer: string; offsetMs: number }> } +export interface Inspection { instance: number; activeLeaders: number; activeFollowers: number; oldestLeaderAgeMs: number | null } +export interface Diagnostics { inspections?: Inspection[]; warnings: number; ages: number[]; coalesced: string[]; fallbackErrors: string[]; configErrors?: number; futureOffsets?: Array<{ layer: string; offsetMs: number }> } export interface ReadIO { budgets: number[]; aborted: number[]; sourceErrors: number[] } export interface Marker { cutoffMs: number; ttlMs: number } export interface Step { policyErrors?: Array<{ layer: string; errorType: string }>; compression?: string[]; markers?: Marker[]; io?: ReadIO; action: string; choice: number; expected: Projected; diagnostics?: Diagnostics } diff --git a/formal/replay/features.mjs b/formal/replay/features.mjs index 517dc974..3a4e3c46 100644 --- a/formal/replay/features.mjs +++ b/formal/replay/features.mjs @@ -1,5 +1,6 @@ import assert from "node:assert/strict"; import { assertSubset } from "./validation.mjs"; +import { assertObservation } from "./divergence.mjs"; import { emptyObservation } from "./observation.mjs"; import { assertInputMetadata, itfInteger, itfSignedInteger, record } from "./itf.mjs"; import { recoveryReadProfile } from "./profiles/recovery-read.mjs"; @@ -7,6 +8,8 @@ import { localFailureProfile } from "./profiles/local-failure.mjs"; import { runtimeBoundariesProfile } from "./profiles/runtime-boundaries.mjs"; import { shadowLayersProfile } from "./profiles/shadow-layers.mjs"; import { sourceBudgetsProfile } from "./profiles/source-budgets.mjs"; +import { shadowReadDeadlinesProfile } from "./profiles/shadow-read-deadlines.mjs"; +import { darkLayersProfile } from "./profiles/dark-layers.mjs"; const settle = (op) => ({ ...(op === "resolve" ? { choices: [1, 2] } : {}), input: (choice, o) => op === "resolve" ? { op, loader: o.loaders - 1, value: choice } : { op, loader: o.loaders - 1 }, @@ -45,6 +48,8 @@ const shadowSeed = { choices: [1, 2, 3, 4, 5, 6, 7, 8], input: (choice) => choic : choice === 8 ? { op: "seed", payloadHex: "22636166c3a922" } : choice === 6 ? { op: "seed", payloadText: " 1" } : { op: "seed", payloadHex: choice === 3 ? "31" : choice === 4 ? "32" : "2031" } }; export const profiles = { + "dark-layers": darkLayersProfile, + "shadow-read-deadlines": shadowReadDeadlinesProfile, "source-budgets": sourceBudgetsProfile, "runtime-boundaries": runtimeBoundariesProfile, "shadow-layers": shadowLayersProfile, @@ -221,9 +226,9 @@ function observation(raw, context) { })); return result; } -function diagnostics(raw, context, configErrors = false, futureOffsets = false) { +function diagnostics(raw, context, configErrors = false, futureOffsets = false, inspect = false) { const value = record(raw, context); - const fields = ["ages", "coalesced", "fallbackErrors", "warnings", ...(configErrors ? ["configErrors"] : []), ...(futureOffsets ? ["futureOffsets"] : [])]; + const fields = ["ages", "coalesced", "fallbackErrors", "warnings", ...(configErrors ? ["configErrors"] : []), ...(futureOffsets ? ["futureOffsets"] : []), ...(inspect ? ["inspections"] : [])]; if (Object.keys(value).sort().join() !== fields.sort().join() || !Array.isArray(value.ages)) throw new Error(`${context}: invalid diagnostics`); const labels = (items, allowed) => { @@ -244,7 +249,18 @@ function diagnostics(raw, context, configErrors = false, futureOffsets = false) return { layer: offset.layer, offsetMs }; }); })() : []; - return { ...(futureOffsets ? { futureOffsets: offsets } : {}), ...(configErrors ? { configErrors: itfInteger(value.configErrors, context) } : {}), warnings: itfInteger(value.warnings, context), ages: value.ages.map(age => itfInteger(age, context) / 1000), + const inspections = inspect ? (() => { + if (!Array.isArray(value.inspections)) throw new Error(`${context}: missing coalescing inspections`); + return value.inspections.map(raw => { + const entry = record(raw, context); + if (Object.keys(entry).sort().join() !== "activeFollowers,activeLeaders,instance,oldestLeaderAgeMs") throw new Error(`${context}: invalid coalescing inspection`); + const age = itfSignedInteger(entry.oldestLeaderAgeMs, context); + if (age < -1) throw new Error(`${context}: invalid oldest leader age`); + return { instance: itfInteger(entry.instance, context), activeLeaders: itfInteger(entry.activeLeaders, context), + activeFollowers: itfInteger(entry.activeFollowers, context), oldestLeaderAgeMs: age === -1 ? null : age }; + }); + })() : []; + return { ...(inspect ? { inspections } : {}), ...(futureOffsets ? { futureOffsets: offsets } : {}), ...(configErrors ? { configErrors: itfInteger(value.configErrors, context) } : {}), warnings: itfInteger(value.warnings, context), ages: value.ages.map(age => itfInteger(age, context) / 1000), coalesced: labels(value.coalesced, ["process", "request_local"]), fallbackErrors: labels(value.fallbackErrors, ["noop", "local", "remote", "request_local"]) }; } function readIO(raw, context, callCount) { @@ -299,7 +315,7 @@ export function parseTrace(raw, path, profile) { ...(profile.compressionIO ? { compression: compressionIO(record(state.s, context).compression, context) } : {}), ...(profile.markerIO ? { markers: markerIO(record(state.s, context).markers, context) } : {}), ...(profile.readIO ? { io: readIO(record(state.s, context).io, context, expected.calls.length) } : {}), - ...(profile.diagnosticAge === undefined ? {} : { diagnostics: diagnostics(record(state.s, context).d, context, profile.diagnosticConfigErrors, profile.diagnosticFutureOffsets) }) }; + ...(profile.diagnosticAge === undefined ? {} : { diagnostics: diagnostics(record(state.s, context).d, context, profile.diagnosticConfigErrors, profile.diagnosticFutureOffsets, profile.diagnosticInspections) }) }; }) }; } function valueCode(value) { @@ -395,6 +411,7 @@ function projectBaseObservation(profile, observed) { throw new Error("Unknown actual source error identity"); return Number(call.error.slice("source:".length)) + 1; }) }; + const diagnostics = []; for (const event of events) { if (event.event === "readContext") { assert.deepEqual(event.index, io.budgets.length); @@ -408,13 +425,15 @@ function projectBaseObservation(profile, observed) { throw new Error("Missing actual cancellation identity"); io.aborted.push(event.index); } + else if (profile.diagnosticAge !== undefined) diagnostics.push(event); else { if (typeof event.event !== "string") throw new Error("Missing actual read event kind"); assertObservedDomain(false, { event: event.event }, { event: ["readContext", "readAbort"] }); } } - return { o: project(base), io }; + return { ...(profile.diagnosticAge === undefined ? { o: project(base) } + : projectBaseObservation({ ...profile, readIO: false }, { ...base, events: diagnostics })), io }; } if (profile.diagnosticAge === undefined) return { o: project(observed) }; @@ -424,20 +443,30 @@ function projectBaseObservation(profile, observed) { const ages = []; const futureOffsets = []; let warnings = 0, configErrors = 0; - const coalesced = [], fallbackErrors = []; + const coalesced = [], fallbackErrors = [], inspections = []; const outcomes = profile.diagnosticAge === "shadowAge" ? observed.shadow.filter(x => x === "match" || x === "mismatch") : observed.recovery.filter(x => x === "served"); for (const event of events) { + if (profile.diagnosticInspections && event.event === "coalescingState") { + if (typeof event.instance !== "string" || !/^[01]$/.test(event.instance) + || !Number.isSafeInteger(event.activeLeaders) || event.activeLeaders < 0 + || !Number.isSafeInteger(event.activeFollowers) || event.activeFollowers < 0 + || !(event.oldestLeaderAgeMs === null || (Number.isFinite(event.oldestLeaderAgeMs) && event.oldestLeaderAgeMs >= 0))) + throw new Error("Malformed actual coalescing inspection"); + inspections.push({ instance: Number(event.instance), activeLeaders: event.activeLeaders, + activeFollowers: event.activeFollowers, oldestLeaderAgeMs: event.oldestLeaderAgeMs }); + continue; + } // This profile selects source failures; other error trails are specified // by effects. Maintenance errors deliberately carry another use case. if (profile.diagnosticConfigErrors && event.event === "error" && event.error === "config_resolution") { - assertSubset(event, { cacheNamespace: "urn", useCase: "Behavior", keyType: "id", layer: "remote", inFallback: false }); + assertSubset(event, { cacheNamespace: "urn", useCase: profile.diagnosticUseCase ?? "Behavior", keyType: "id", layer: "remote", inFallback: false }); configErrors++; continue; } if (event.event === "error" && event.error !== "fallback") continue; - assertSubset(event, { cacheNamespace: "urn", useCase: "Behavior", keyType: "id" }); + assertSubset(event, { cacheNamespace: "urn", useCase: profile.diagnosticUseCase ?? "Behavior", keyType: "id" }); if (event.event === "coalesced") { if (typeof event.scope !== "string") throw new Error("Missing coalescing scope"); @@ -475,9 +504,9 @@ function projectBaseObservation(profile, observed) { ages.push(event.seconds); } } - return { o: project(base), d: { warnings, ages, coalesced, fallbackErrors, ...(profile.diagnosticFutureOffsets ? { futureOffsets } : {}), ...(profile.diagnosticConfigErrors ? { configErrors } : {}) } }; + return { o: project(base), d: { warnings, ages, coalesced, fallbackErrors, ...(profile.diagnosticInspections ? { inspections } : {}), ...(profile.diagnosticFutureOffsets ? { futureOffsets } : {}), ...(profile.diagnosticConfigErrors ? { configErrors } : {}) } }; } export function expectedObservation(step) { return { o: step.expected, ...(step.policyErrors === undefined ? {} : { policyErrors: step.policyErrors }), ...(step.diagnostics === undefined ? {} : { d: step.diagnostics }), ...(step.io === undefined ? {} : { io: step.io }), ...(step.markers === undefined ? {} : { markers: step.markers }), ...(step.compression === undefined ? {} : { compression: step.compression }) }; } export function featureInput(profile, action, choice, observed, environment) { const binding = profile.actions[action]; if (!binding || (binding.choices ? !binding.choices.includes(choice) : choice !== -1 && choice !== 0)) throw new Error("Unknown feature action or choice"); return binding.input(choice, observed, environment); } -export function assertFeatureObservation(profile, step, observed) { assert.deepEqual(projectObservation(profile, observed), expectedObservation(step)); } +export function assertFeatureObservation(profile, step, observed) { assertObservation(projectObservation(profile, observed), expectedObservation(step)); } diff --git a/formal/replay/local-clock.mjs b/formal/replay/local-clock.mjs index 96940f7a..106223a1 100644 --- a/formal/replay/local-clock.mjs +++ b/formal/replay/local-clock.mjs @@ -1,4 +1,4 @@ -import assert from "node:assert/strict"; +import { assertObservation } from "./divergence.mjs"; import { emptyObservation } from "./observation.mjs"; import { assertInputMetadata, itfInteger, itfSignedInteger, record } from "./itf.mjs"; const advances = [1, 100, 300, 400, 700, 999200, 999999, 1000000]; @@ -56,7 +56,7 @@ export function localClockInput(action, choice) { } } export function assertLocalClockObservation(step, observed) { - assert.deepEqual(observed, step.expected); + assertObservation(observed, step.expected); } export const localClockActions = Object.keys(choices).filter(action => action !== "init"); // The explicit-input descriptor the corpus differential replays this profile diff --git a/formal/replay/profiles/dark-layers.d.mts b/formal/replay/profiles/dark-layers.d.mts new file mode 100644 index 00000000..b355bf1d --- /dev/null +++ b/formal/replay/profiles/dark-layers.d.mts @@ -0,0 +1,2 @@ +import type { Profile } from "../../../test/formal/feature-profile.js"; +export const darkLayersProfile: Profile; diff --git a/formal/replay/profiles/dark-layers.mjs b/formal/replay/profiles/dark-layers.mjs new file mode 100644 index 00000000..8888846b --- /dev/null +++ b/formal/replay/profiles/dark-layers.mjs @@ -0,0 +1,56 @@ +const range = length => Array.from({ length }, (_, index) => index); +const useCase = "DarkLayers"; +function policy(choice) { + const changed = choice === 7 || choice === 8; + return { + requestLocal: [0, 1, 4, 6, 7].includes(choice), coalesce: ![4, 5].includes(choice), + ttlSec: { local: 60, remote: changed ? 120 : 60 }, + staleOnErrorMaxAgeSec: changed ? 180 : 120, + ramp: { local: [0, 2, 4, 5, 6, 7].includes(choice) ? 100 : 0, remote: 0 }, + shadow: { ramp: choice === 6 ? 0 : 100 }, + }; +} +function seed(choice, _observed, environment) { + const kind = choice % 6; + const base = { op: "seed", key: String(Math.floor(choice / 6)), useCase, + ageMs: kind === 2 ? 59_999 : kind === 3 ? 60_000 : 0, ttlMs: 180_000 }; + if (kind === 4) return { ...base, payloadHex: "016e6f742061207a737464206672616d65" }; + if (kind === 5) { + const frame = Buffer.alloc(11); + frame[0] = 1; frame.writeBigUInt64BE(BigInt(environment.wallMs), 1); frame[9] = 255; frame[10] = 49; + return { ...base, frameHex: frame.toString("hex") }; + } + return { ...base, value: kind === 1 ? 2 : 1 }; +} +const release = effect => ({ choices: range(24), input: index => ({ op: "release", effect, index }) }); +const fault = field => ({ choices: [0, 1], input: choice => ({ op: "faults", value: { [field]: choice === 1 } }) }); + +// The held dark path composes source deadlines, scopes, local storage and +// tracked invalidation. Every timestamp and effect id is an external input. +export const darkLayersProfile = { + explicitInputs: true, diagnosticAge: "shadowAge", diagnosticUseCase: useCase, + diagnosticConfigErrors: true, diagnosticFutureOffsets: true, diagnosticInspections: true, + fixture: { policy: policy(0), tracked: true, fallbackTimeoutMs: 10, readTimeoutMs: 30_000_000, + shadowMaxInFlight: 1, recovery: "allow", probeSourceScope: true, + observe: ["shadowAge", "futureOffset", "coalesced", "error", "coalescingState"] }, + setup: range(3).map(scope => ({ op: "openScope", id: String(scope), instance: scope === 2 ? "1" : "0" })) + .concat([{ op: "faults", value: { holdReads: true, holdLoads: true, holdDumps: true, holdWrites: true } }]), + actions: { + beginCall: { choices: range(10), input: choice => { + const context = Math.floor(choice / 2); + return { op: "begin", key: String(choice % 2), useCase, + ...(context < 3 ? { scope: String(context) } : { instance: context === 4 ? "1" : "0" }) }; + } }, + resolveLoader: { choices: range(24).map(index => index + 1), input: choice => ({ op: "resolve", loader: Math.floor((choice - 1) / 2), value: (choice - 1) % 2 + 1 }) }, + rejectLoader: { choices: range(12), input: loader => ({ op: "reject", loader }) }, + releaseRead: release("read"), releaseLoad: release("load"), releaseDump: release("dump"), releaseWrite: release("write"), + advance: { choices: [1, 10, 60_000], input: ms => ({ op: "advance", ms }) }, + policy: { choices: range(9), input: choice => ({ op: "policy", value: policy(choice) }) }, + seed: { choices: range(12), input: seed }, + invalidate: { choices: range(4), input: choice => ({ op: "invalidate", key: String(Math.floor(choice / 2)), futureBufferMs: (choice % 2) * 20 }) }, + closeScope: { choices: range(3), input: choice => ({ op: "closeScope", id: String(choice) }) }, + inspect: { choices: [0, 1], input: instance => ({ op: "inspectCoalescing", instance: String(instance) }) }, + rollbackWall: { input: () => ({ op: "shiftWall", ms: -1000 }) }, + readFault: fault("read"), loadFault: fault("load"), dumpFault: fault("dump"), writeFault: fault("write"), + }, +}; diff --git a/formal/replay/profiles/shadow-read-deadlines.d.mts b/formal/replay/profiles/shadow-read-deadlines.d.mts new file mode 100644 index 00000000..13ef8607 --- /dev/null +++ b/formal/replay/profiles/shadow-read-deadlines.d.mts @@ -0,0 +1,2 @@ +import type { Profile } from "../../../test/formal/feature-profile.js"; +export const shadowReadDeadlinesProfile: Profile; diff --git a/formal/replay/profiles/shadow-read-deadlines.mjs b/formal/replay/profiles/shadow-read-deadlines.mjs new file mode 100644 index 00000000..be29af44 --- /dev/null +++ b/formal/replay/profiles/shadow-read-deadlines.mjs @@ -0,0 +1,24 @@ +const range = length => Array.from({ length }, (_, index) => index); +const release = effect => ({ choices: range(16), input: index => ({ op: "release", effect, index }) }); + +// Dark source/job and read budgets are separate external inputs. Actual read +// ordinals bind raw releases, cancellation signals and captured contexts. +export const shadowReadDeadlinesProfile = { + explicitInputs: true, readIO: true, diagnosticAge: "shadowAge", + diagnosticConfigErrors: true, diagnosticFutureOffsets: true, + fixture: { policy: { requestLocal: false, ttlSec: { local: 60, remote: 60 }, + ramp: { local: 0, remote: 0 }, shadow: { ramp: 100 } }, + tracked: true, fallbackTimeoutMs: 10, readTimeoutMs: 5, + shadowMaxInFlight: 1, probeSourceScope: true, + observe: ["shadowAge", "futureOffset", "coalesced", "error", "readContext", "readAbort"] }, + setup: [{ op: "faults", value: { holdReads: true, holdLoads: true, holdDumps: true, holdWrites: true } }], + actions: { + beginCall: { choices: [0, 1], input: choice => ({ op: "begin", key: String(choice) }) }, + resolveLoader: { choices: range(16).map(index => index + 1), input: choice => ({ op: "resolve", loader: Math.floor((choice - 1) / 2), value: (choice - 1) % 2 + 1 }) }, + rejectLoader: { choices: range(8), input: loader => ({ op: "reject", loader }) }, + releaseRead: release("read"), releaseLoad: release("load"), releaseDump: release("dump"), releaseWrite: release("write"), + advance: { choices: [1, 4, 5, 10, 20], input: ms => ({ op: "advance", ms }) }, + policy: { choices: [5, 20], input: choice => ({ op: "policy", value: { remoteReadTimeoutMs: choice } }) }, + seed: { choices: range(4), input: choice => ({ op: "seed", key: String(Math.floor(choice / 2)), value: choice % 2 + 1, ttlMs: 60000 }) }, + }, +}; diff --git a/formal/replay/protocol.schema.json b/formal/replay/protocol.schema.json index 90919eaa..59db6884 100644 --- a/formal/replay/protocol.schema.json +++ b/formal/replay/protocol.schema.json @@ -54,6 +54,7 @@ "enum": [ "admission", "core", + "dark-layers", "effects", "independent", "layers", @@ -66,6 +67,7 @@ "scope", "shadow", "shadow-layers", + "shadow-read-deadlines", "source-budgets" ] }, @@ -757,6 +759,21 @@ "id" ], "additionalProperties": false + }, + { + "type": "object", + "properties": { + "op": { + "const": "inspectCoalescing" + }, + "instance": { + "type": "string" + } + }, + "required": [ + "op" + ], + "additionalProperties": false } ] }, @@ -1719,6 +1736,44 @@ "additionalProperties": { "$ref": "#/$defs/primitive" } + }, + { + "type": "object", + "description": "Public process-coalescing snapshot for one instance; null age means no registered leader.", + "properties": { + "event": { + "const": "coalescingState" + }, + "instance": { + "type": "string" + }, + "activeLeaders": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "activeFollowers": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "oldestLeaderAgeMs": { + "type": [ + "number", + "null" + ], + "minimum": 0, + "maximum": 9007199254740991 + } + }, + "required": [ + "event", + "instance", + "activeLeaders", + "activeFollowers", + "oldestLeaderAgeMs" + ], + "additionalProperties": false } ] }, @@ -1847,7 +1902,7 @@ }, "coreObservation": { "type": "object", - "description": "Flat integer observation of the core conformance driver.", + "description": "Flat core observation. Counters and offered source versions are nonnegative integers; lastResult also represents an absent actual return explicitly so it is compared as a behavioral mismatch.", "properties": { "sourceVersion": { "type": "integer", @@ -1855,9 +1910,15 @@ "maximum": 9007199254740991 }, "lastResult": { - "type": "integer", - "minimum": 0, - "maximum": 9007199254740991 + "oneOf": [ + { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, + { + "type": "object", + "properties": { "absent": { "const": true } }, + "required": ["absent"], + "additionalProperties": false + } + ] }, "outsideLoaderCalls": { "type": "integer", @@ -2220,7 +2281,8 @@ "serialization", "mismatchWarning", "writeDispatch", - "marker" + "marker", + "coalescingState" ] }, "description": "Event kinds journaled into observation.events. Its presence, even empty, makes events required in every observation." diff --git a/formal/replay/witnesses/admission.d.mts b/formal/replay/witnesses/admission.d.mts index 749873b9..49226822 100644 --- a/formal/replay/witnesses/admission.d.mts +++ b/formal/replay/witnesses/admission.d.mts @@ -1,3 +1,5 @@ import type { FeatureHistory } from "./index.mjs"; import type { WitnessRecorder } from "./recorder.mjs"; export function admissionWitnesses(histories: readonly FeatureHistory[], recorder?: WitnessRecorder): Set; +import type { PublicPrefixWitnessRule } from "./public-prefix.mjs"; +export const admissionWitnessRules: readonly PublicPrefixWitnessRule[]; diff --git a/formal/replay/witnesses/admission.mjs b/formal/replay/witnesses/admission.mjs index ebeda095..ed140d19 100644 --- a/formal/replay/witnesses/admission.mjs +++ b/formal/replay/witnesses/admission.mjs @@ -1,6 +1,15 @@ +import { publicPrefixWitnesses, publicPrefixRule as rule, publicCheckpoint as check, witnessCommand as command } from "./public-prefix.mjs"; import { createWitnessRecorder } from "./recorder.mjs"; import { fidelityBinding } from "./fidelity.mjs"; +export const admissionWitnessRules = [ + rule("later-served-shadow-keeps-own-job-budget", "laterServedShadowKeepsItsWholeBudgetTest", + [command("init"), command("advance", 10), command("beginCall", 0), command("releaseRead", 0), command("releaseLoad", 0), command("advance", 1), command("rejectLoader", 0)], + check(5, { calls: [1], loaders: 1, loads: 1, shadow: [] }), + check(6, { calls: [1], loaders: 1, loads: 1, shadow: ["source_error"] })), +]; + + // Shadow-job admission witnesses use declared inputs and public observations // only. Job bookkeeping below mirrors the observed effect indices; it never // reads private model admissions. Wherever a history still carries the @@ -11,6 +20,7 @@ import { fidelityBinding } from "./fidelity.mjs"; // caller flight's (its identity, captured selection and callers) or a job's, // the flights registered for sharing, and the clock. export function admissionWitnesses(histories, recorder = createWitnessRecorder()) { + publicPrefixWitnesses(histories, admissionWitnessRules, recorder); for (const { path, steps, predictions } of histories) { recorder.enter(path); let overlay = 0; diff --git a/formal/replay/witnesses/dark-layers.d.mts b/formal/replay/witnesses/dark-layers.d.mts new file mode 100644 index 00000000..635c275a --- /dev/null +++ b/formal/replay/witnesses/dark-layers.d.mts @@ -0,0 +1,5 @@ +import type { PublicPrefixWitnessRule } from "./public-prefix.mjs"; +import type { WitnessRecorder } from "./recorder.mjs"; +import type { RawHistory } from "./trace.mjs"; +export const darkLayersWitnessRules: readonly PublicPrefixWitnessRule[]; +export function darkLayersWitnesses(histories: readonly RawHistory[], recorder?: WitnessRecorder): Set; diff --git a/formal/replay/witnesses/dark-layers.mjs b/formal/replay/witnesses/dark-layers.mjs new file mode 100644 index 00000000..4aac9f66 --- /dev/null +++ b/formal/replay/witnesses/dark-layers.mjs @@ -0,0 +1,188 @@ +import { publicPrefixWitnesses, publicPrefixRule as rule, publicCheckpoint as check, witnessCommand as command } from "./public-prefix.mjs"; +import { decodeIntegers, explicitInput } from "./trace.mjs"; +import { createWitnessRecorder } from "./recorder.mjs"; + +// Public commands identify the schedule; consequences at each boundary earn +// credit independently of filenames and the model's private storage. +const inspection = (instance, activeLeaders, activeFollowers, oldestLeaderAgeMs) => + ({ instance, activeLeaders, activeFollowers, oldestLeaderAgeMs }); +export const darkLayersWitnessRules = [ + rule("later-dark-source-keeps-own-budget", "laterDarkSourceKeepsItsWholeBudgetTest", + [command("init"), command("advance", 10), command("policy", 0), command("beginCall", 0), command("resolveLoader", 1), command("beginCall", 2)], + check(4, { calls: [1], loaders: 1, reads: 1, dumps: 0, shadow: [] }, { fallbackErrors: [] }), + check(5, { calls: [1, 1], loaders: 1, reads: 1, dumps: 0, shadow: [] }, { fallbackErrors: [] })), + rule("inspection-counts-process-followers-and-oldest", "inspectionTracksKeysFollowersAndOldestLeaderTest", + [command("init"), command("policy", 6), command("beginCall", 0), command("beginCall", 0), command("beginCall", 2), command("beginCall", 2), command("advance", 1), command("beginCall", 1), command("inspect", 0), command("resolveLoader", 1), command("inspect", 0), command("advance", 1), command("rejectLoader", 1), command("inspect", 0)], + check(8, { calls: [0, 0, 0, 0, 0], loaders: 2 }, { inspections: [inspection(0, 2, 1, 1)] }), + check(10, { calls: [1, 1, 1, 1, 0] }, { inspections: [inspection(0, 2, 1, 1), inspection(0, 1, 0, 0)] }), + check(13, { calls: [1, 1, 1, 1, 3] }, { inspections: [inspection(0, 2, 1, 1), inspection(0, 1, 0, 0), inspection(0, 0, 0, -1)] })), + rule("inspection-isolates-instances-and-wall-shifts", "inspectionIsPerInstanceAndUsesMonotonicTimeTest", + [command("init"), command("policy", 6), command("beginCall", 0), command("advance", 1), command("beginCall", 4), command("rollbackWall"), command("inspect", 0), command("inspect", 1), command("resolveLoader", 1), command("inspect", 0), command("inspect", 1)], + check(6, { calls: [0, 0], loaders: 2 }, { inspections: [inspection(0, 1, 0, 1)] }), + check(7, {}, { inspections: [inspection(0, 1, 0, 1), inspection(1, 1, 0, 0)] }), + check(10, { calls: [1, 0] }, { inspections: [inspection(0, 1, 0, 1), inspection(1, 1, 0, 0), inspection(0, 0, 0, -1), inspection(1, 1, 0, 0)] })), + rule("inspection-excludes-request-and-uncoalesced", "inspectionExcludesRequestOnlyAndUncoalescedWorkTest", + [command("init"), command("policy", 1), command("beginCall", 0), command("beginCall", 0), command("inspect", 0), command("policy", 5), command("beginCall", 2), command("beginCall", 2), command("inspect", 0)], + check(4, { calls: [0, 0], loaders: 1 }, { inspections: [inspection(0, 0, 0, -1)] }), + check(8, { calls: [0, 0, 0, 0], loaders: 3 }, { inspections: [inspection(0, 0, 0, -1), inspection(0, 0, 0, -1)] })), + rule("inspection-clears-timeout-before-raw-settlement", "inspectionClearsTimedOutCallersWhileRawWorkRemainsTest", + [command("init"), command("beginCall", 0), command("beginCall", 2), command("inspect", 0), command("advance", 10), command("inspect", 0), command("beginCall", 0), command("inspect", 0), command("resolveLoader", 1), command("releaseRead", 0), command("inspect", 0)], + check(3, { calls: [0, 0], loaders: 1 }, { inspections: [inspection(0, 1, 1, 0)] }), + check(5, { calls: [4, 4], shadow: ["timeout"] }, { inspections: [inspection(0, 1, 1, 0), inspection(0, 0, 0, -1)] }), + check(7, { calls: [4, 4, 0], loaders: 2 }, { inspections: [inspection(0, 1, 1, 0), inspection(0, 0, 0, -1), inspection(0, 1, 0, 0)] }), + check(10, { calls: [4, 4, 0] }, { inspections: [inspection(0, 1, 1, 0), inspection(0, 0, 0, -1), inspection(0, 1, 0, 0), inspection(0, 1, 0, 0)] })), + rule("inspection-excludes-unfinished-shadow", "inspectionExcludesUnfinishedShadowAfterCallerSuccessTest", + [command("init"), command("beginCall", 0), command("releaseRead", 0), command("resolveLoader", 1), command("inspect", 0), command("releaseDump", 0), command("inspect", 0)], + check(4, { calls: [1], dumps: 1, writes: 0, shadow: [] }, { inspections: [inspection(0, 0, 0, -1)] }), + check(6, { calls: [1], writes: 1, shadow: [] }, { inspections: [inspection(0, 0, 0, -1), inspection(0, 0, 0, -1)] })), + rule("rejected-dark-source-seeds-no-layer", "rejectedDarkSourceSeedsNoLayerTest", + [command("init"), command("policy", 0), command("beginCall", 0), command("releaseRead", 0), command("rejectLoader", 0), command("beginCall", 0)], + check(4, {"calls": [3], "shadow": ["source_error"], "loads": 0, "dumps": 0, "writes": 0}, {"fallbackErrors": ["local"]}), + check(5, {"calls": [3, 0], "loaders": 2, "reads": 2}, {"coalesced": []})), + rule("uncoalesced-publication-keeps-dark-leader", "uncoalescedPublicationDoesNotPreemptTheRegisteredLeaderTest", + [command("init"), command("policy", 0), command("beginCall", 0), command("policy", 5), command("beginCall", 2), command("resolveLoader", 4), command("policy", 0), command("beginCall", 6), command("resolveLoader", 1)], + check(4, {"calls": [0, 0], "loaders": 2, "reads": 1, "shadow": ["dropped"]}), + check(7, {"calls": [0, 2, 0], "loaders": 2}, {"coalesced": ["process"]}), + check(8, {"calls": [1, 2, 1], "loaders": 2})), + rule("held-dark-fill-keeps-captured-retention", "darkFillCarriesCapturedRetentionOnTheHeldPathTest", + [command("init"), command("policy", 3), command("beginCall", 0), command("releaseRead", 0), command("policy", 7), command("resolveLoader", 1), command("releaseDump", 0), command("releaseWrite", 0)], + check(5, {"calls": [1], "dumps": 1, "writes": 0}), + check(6, {"writeTtls": [120000], "writes": 1}), + check(7, {"shadow": ["filled"]})), + rule("dark-fill-at-watermark-is-fenced", "fillAtTheWatermarkInstantIsFencedTest", + [command("init"), command("invalidate", 0), command("policy", 3), command("beginCall", 0), command("releaseRead", 0), command("resolveLoader", 1)], + check(5, {"calls": [1], "shadow": ["fill_fenced"], "dumps": 0})), + rule("dark-frame-at-watermark-is-fenced", "frameAtTheWatermarkIsFencedFromTheDarkReadTest", + [command("init"), command("invalidate", 0), command("seed", 0), command("policy", 3), command("beginCall", 0), command("releaseRead", 0), command("advance", 1), command("resolveLoader", 2)], + check(7, {"calls": [2], "dumps": 1, "loads": 0})), + rule("held-dark-fill-rechecks-fence-after-rollback", "fillFenceIsRejudgedAfterAWallRollbackTest", + [command("init"), command("invalidate", 0), command("advance", 1), command("policy", 3), command("beginCall", 0), command("releaseRead", 0), command("resolveLoader", 1), command("rollbackWall"), command("releaseDump", 0)], + check(8, {"shadow": ["fill_fenced"], "writes": 0})), + rule("stale-visible-dark-c0-fills-unfenced", "staleVisibleFrameDeclinedByTheDarkReadFillsUnfencedTest", + [command("init"), command("policy", 3), command("seed", 3), command("beginCall", 0), command("releaseRead", 0), command("invalidate", 1), command("resolveLoader", 2), command("releaseDump", 0), command("releaseWrite", 0), command("beginCall", 0), command("releaseRead", 1), command("resolveLoader", 3)], + check(8, {"writes": 1, "shadow": ["filled"]}), + check(10, {"reads": 2}), + check(11, {"calls": [2, 1], "shadow": ["filled", "fill_fenced"], "dumps": 1, "writes": 1})), + rule("undecodable-held-dark-c0-is-not-repaired", "undecodableDarkC0IsNeverRepairedTest", + [command("init"), command("policy", 3), command("seed", 4), command("beginCall", 0), command("releaseRead", 0), command("resolveLoader", 2), command("releaseLoad", 0)], + check(5, {"calls": [2], "loads": 1, "dumps": 0}), + check(6, {"shadow": ["deserialization_error"], "dumps": 0, "reads": 1})), + rule("unsupported-encoding-ends-dark-read", "unsupportedEncodingFailsTheDarkReadTest", + [command("init"), command("policy", 3), command("seed", 5), command("beginCall", 0), command("releaseRead", 0), command("resolveLoader", 1)], + check(4, {"shadow": ["redis_error"]}), + check(5, {"calls": [1], "dumps": 0, "loads": 0})), + rule("dark-source-deadline-attributed-to-local", "sourceDeadlineExpiresTheDarkJobAndAttributesToTheLocalLayerTest", + [command("init"), command("policy", 0), command("beginCall", 0), command("advance", 10), command("releaseRead", 0), command("beginCall", 0)], + check(3, {"calls": [4], "shadow": ["timeout"]}, {"fallbackErrors": ["local"]}), + check(4, {"shadow": ["timeout"], "dumps": 0}), + check(5, {"calls": [4, 0], "loaders": 2}, {"coalesced": []})), + rule("dark-flights-are-instance-local", "sameKeyCallersOnTwoInstancesKeepSeparateFlightsTest", + [command("init"), command("policy", 0), command("beginCall", 0), command("beginCall", 4), command("resolveLoader", 2)], + check(3, {"calls": [0, 0], "loaders": 2, "reads": 2, "shadow": []}, {"coalesced": []}), + check(4, {"calls": [2, 0]})), + rule("closed-scope-bypasses-dark-work", "closedScopeCallerBypassesLayersAndAdmitsNoJobTest", + [command("init"), command("policy", 0), command("closeScope", 0), command("beginCall", 0), command("advance", 10)], + check(3, {"policyCalls": 0, "reads": 0, "shadow": [], "sourceScopes": [false]}), + check(4, {"calls": [0]}, {"fallbackErrors": []})), + rule("held-dark-capacity-is-instance-local", "fullInstanceDropsAnotherKeyWhileTheC0ReadIsHeldTest", + [command("init"), command("policy", 3), command("beginCall", 0), command("beginCall", 1), command("beginCall", 4)], + check(3, {"reads": 1, "shadow": ["dropped"]}), + check(4, {"reads": 2, "shadow": ["dropped"]})), + rule("held-dark-dump-error-preserves-source", "dumpFaultEndsTheFillWithoutReplacingTheCallerResultTest", + [command("init"), command("policy", 3), command("beginCall", 0), command("releaseRead", 0), command("resolveLoader", 1), command("dumpFault", 1), command("releaseDump", 0)], + check(6, {"calls": [1], "shadow": ["fill_error"], "writes": 0})), + rule("dark-source-local-publication-stops-job", "localPublicationStopsLaterDarkWorkTest", + [command("init"), command("policy", 2), command("beginCall", 0), command("releaseRead", 0), command("resolveLoader", 1), command("beginCall", 0)], + check(4, {"calls": [1], "dumps": 1}), + check(5, {"calls": [1, 1], "loaders": 1, "reads": 1, "shadow": []})), + rule("held-dark-c0-age-at-verdict", "retainedC0AgeIsSampledAtTheVerdictTest", + [command("init"), command("policy", 3), command("seed", 2), command("beginCall", 0), command("releaseRead", 0), command("advance", 1), command("resolveLoader", 2), command("releaseLoad", 0), command("releaseRead", 1)], + check(6, {"calls": [2], "loads": 1}), + check(7, {"reads": 2, "shadow": []}), + check(8, {"shadow": ["mismatch"], "dumps": 0}, {"ages": [60000]})), + rule("future-dark-c0-reports-offset-and-fills", "rollbackMakesTheSeededFrameFutureAndReportsItsOffsetTest", + [command("init"), command("policy", 3), command("seed", 0), command("rollbackWall"), command("beginCall", 0), command("releaseRead", 0), command("resolveLoader", 2), command("releaseDump", 0), command("releaseWrite", 0)], + check(5, {}, {"futureOffsets": [{"layer": "remote_shadow", "offsetMs": 1000}]}), + check(8, {"shadow": ["filled"]})), + rule("transient-request-dark-error-uses-request-layer", "transientRequestOnlySourceFailureIsAttributedToRequestLayerTest", + [command("init"), command("policy", 1), command("beginCall", 6), command("releaseRead", 0), command("rejectLoader", 0)], + check(4, { calls: [3], shadow: ["source_error"] }, { fallbackErrors: ["request_local"] })), + rule("timed-out-dark-read-retains-capacity-until-raw-release", "timedOutDarkReadKeepsCapacityUntilRawReleaseTest", + [command("init"), command("policy", 3), command("beginCall", 0), command("resolveLoader", 1), command("advance", 10), command("beginCall", 1), command("resolveLoader", 3), command("releaseRead", 0), command("beginCall", 1)], + check(4, {"calls": [1], "shadow": ["timeout"]}), + check(5, {"calls": [1, 0], "loaders": 2, "reads": 1, "shadow": ["timeout", "dropped"]}), + check(7, {"reads": 1, "shadow": ["timeout", "dropped"], "dumps": 0, "writes": 0, "loads": 0}), + check(8, {"calls": [1, 1, 0], "loaders": 3, "reads": 2, "shadow": ["timeout", "dropped"]})), + rule("timed-out-dark-decode-retains-capacity-until-raw-release", "timedOutDarkDecodeKeepsCapacityUntilRawReleaseTest", + [command("init"), command("policy", 3), command("seed", 0), command("beginCall", 0), command("releaseRead", 0), command("resolveLoader", 2), command("advance", 10), command("beginCall", 1), command("resolveLoader", 3), command("releaseLoad", 0), command("beginCall", 1)], + check(6, {"calls": [2], "shadow": ["timeout"]}), + check(7, {"calls": [2, 0], "loaders": 2, "reads": 1, "shadow": ["timeout", "dropped"]}), + check(9, {"reads": 1, "shadow": ["timeout", "dropped"], "dumps": 0, "writes": 0}), + check(10, {"calls": [2, 1, 0], "loaders": 3, "reads": 2, "shadow": ["timeout", "dropped"]})), + rule("timed-out-dark-confirmation-retains-capacity-until-raw-release", "timedOutDarkConfirmationKeepsCapacityUntilRawReleaseTest", + [command("init"), command("policy", 3), command("seed", 0), command("beginCall", 0), command("releaseRead", 0), command("resolveLoader", 2), command("releaseLoad", 0), command("advance", 10), command("beginCall", 1), command("resolveLoader", 3), command("releaseRead", 1), command("beginCall", 1)], + check(7, {"calls": [2], "shadow": ["timeout"]}), + check(8, {"calls": [2, 0], "loaders": 2, "reads": 2, "shadow": ["timeout", "dropped"]}), + check(10, {"reads": 2, "shadow": ["timeout", "dropped"], "dumps": 0, "writes": 0}), + check(11, {"calls": [2, 1, 0], "loaders": 3, "reads": 3, "shadow": ["timeout", "dropped"]})), + rule("timed-out-dark-dump-retains-capacity-until-raw-release", "timedOutDarkDumpKeepsCapacityUntilRawReleaseTest", + [command("init"), command("policy", 3), command("beginCall", 0), command("releaseRead", 0), command("resolveLoader", 1), command("advance", 10), command("beginCall", 1), command("resolveLoader", 3), command("releaseDump", 0), command("beginCall", 1)], + check(5, {"calls": [1], "shadow": ["timeout"]}), + check(6, {"calls": [1, 0], "loaders": 2, "reads": 1, "shadow": ["timeout", "dropped"]}), + check(8, {"reads": 1, "shadow": ["timeout", "dropped"], "writes": 0}), + check(9, {"calls": [1, 1, 0], "loaders": 3, "reads": 2, "shadow": ["timeout", "dropped"]})), + rule("timed-out-dark-write-retains-capacity-until-raw-release", "timedOutDarkWriteKeepsCapacityUntilRawReleaseTest", + [command("init"), command("policy", 3), command("beginCall", 0), command("releaseRead", 0), command("resolveLoader", 1), command("releaseDump", 0), command("advance", 10), command("beginCall", 1), command("resolveLoader", 3), command("releaseWrite", 0), command("beginCall", 1)], + check(6, {"calls": [1], "shadow": ["timeout"]}), + check(7, {"calls": [1, 0], "loaders": 2, "reads": 1, "shadow": ["timeout", "dropped"]}), + check(9, {"reads": 1, "shadow": ["timeout", "dropped"], "writes": 1}), + check(10, {"calls": [1, 1, 0], "loaders": 3, "reads": 2, "shadow": ["timeout", "dropped"]})), +]; + +// The two sampled interactions also use only external commands and the public +// observations. A local insertion is inferred from a successful owned source; +// the expiry probe bypasses request memoization and must start another loader. +export function darkLayersWitnesses(histories, recorder = createWitnessRecorder()) { + publicPrefixWitnesses(histories, darkLayersWitnessRules, recorder); + for (const { path, states } of histories) { + recorder.enter(path); + let now = 0, policy = 0, previous; + const closed = new Set(), loaders = new Map(), published = new Map(); + for (const [step, raw] of states.entries()) { + const input = explicitInput(raw, path), state = decodeIntegers(raw.s, path); + const observation = state.o, diagnostics = state.d; + if (input.name === "advance") now += input.choice; + if (input.name === "policy") policy = input.choice; + if (input.name === "closeScope") closed.add(input.choice); + if (previous && input.name === "beginCall") { + const context = Math.floor(input.choice / 2), key = input.choice % 2; + const instance = context === 2 || context === 4 ? 1 : 0; + const identity = `${instance}:${key}`; + const local = [0, 2, 4, 5, 6, 7].includes(policy) && !closed.has(context); + if (observation.loaders === previous.o.loaders + 1) { + loaders.set(previous.o.loaders, { identity, local, call: previous.o.calls.length, step }); + const insertion = published.get(identity); + if (local && context >= 3 && insertion && now - insertion.time >= 60_000 + && observation.calls.at(-1) === 0) { + recorder.credit("local-entry-expires-at-its-ttl", insertion.step, step); + } + } + if (observation.calls.length === previous.o.calls.length + 1 && observation.calls.at(-1) === 0 + && observation.loaders === previous.o.loaders + && diagnostics.coalesced.length === previous.d.coalesced.length + 1 + && diagnostics.coalesced.at(-1) === "request_local") { + recorder.credit("request-follower-joins-leader", step); + } + } + if (previous && input.name === "resolveLoader") { + const loader = loaders.get(Math.floor((input.choice - 1) / 2)); + if (loader?.local && previous.o.calls[loader.call] === 0 + && [1, 2].includes(observation.calls[loader.call])) { + published.set(loader.identity, { time: now, step }); + } + } + previous = state; + } + } + return recorder.labels(); +} diff --git a/formal/replay/witnesses/effects-authority.mjs b/formal/replay/witnesses/effects-authority.mjs index 7c34f711..cf796155 100644 --- a/formal/replay/witnesses/effects-authority.mjs +++ b/formal/replay/witnesses/effects-authority.mjs @@ -14,7 +14,18 @@ const publishedAndRead = o => same(o.calls, [1, 1]) && o.reads === 2 && o.loads // history's `expected`: counters, caller codes and events in callback // seconds); no phase, private timestamp, stored value or model authorization // predicate enters these rules. +const noMiss = o => !o.events.some(e => e.event === "miss"); +const expiredOnly = o => same(o.events.filter(e => e.event === "miss"), + [{ event: "miss", location: "remote", detail: "expired", amount: 0 }]) + && !o.events.some(e => e.event === "futureOffset"); + export const effectsAuthorityRules = [ + { name: "adapter-frame-last-fresh-millisecond-hits", regression: "frameReplyAtLastFreshMillisecondHitsTest", + commands: [init, cmd("adapterReply", 15), cmd("advanceWall", 59999), begin, read, load], + consequence: o => hit(o) && o.dumps === 0 && noMiss(o) }, + { name: "adapter-frame-exact-expiry-refills-and-reuses", regression: "staleFrameReplyExpiresRefillsAndIsReadableTest", + commands: [init, cmd("adapterReply", 15), cmd("advanceWall", 60000), begin, read, resolve, dump, write, begin, cmd("releaseRead", 1), load], + consequence: o => publishedAndRead(o) && expiredOnly(o) && same(o.writeTtls, [60000]) }, { name: "observer-failure-hit", regression: "observerFailuresCannotPreventCacheHitTest", commands: [init, fault, cmd("seedRemote"), begin, read, load], consequence: hit }, { name: "observer-failure-publication", regression: "observerFailuresCannotPreventPublicationTest", diff --git a/formal/replay/witnesses/effects.mjs b/formal/replay/witnesses/effects.mjs index 07a6ed3c..6931afcd 100644 --- a/formal/replay/witnesses/effects.mjs +++ b/formal/replay/witnesses/effects.mjs @@ -119,6 +119,7 @@ function advanceShadow(before, step, previous, o) { break; } case "jumpClock": s.now += CLOCK_STEP_MS; s.wall += CLOCK_STEP_MS; break; + case "advanceWall": s.wall += step.choice; break; case "rollbackWall": s.wall -= WALL_ROLLBACK_MS; break; case "invalidate": s.watermark = Math.max(s.watermark, s.wall); break; case "futureFence": s.watermark = Math.max(s.watermark, s.wall + FUTURE_BUFFER_MS); break; diff --git a/formal/replay/witnesses/index.mjs b/formal/replay/witnesses/index.mjs index 3dd08e93..d83b46a1 100644 --- a/formal/replay/witnesses/index.mjs +++ b/formal/replay/witnesses/index.mjs @@ -22,7 +22,9 @@ import { scopeWitnesses } from "./scope.mjs"; import { shadowWitnesses } from "./shadow.mjs"; import { shadowDiagnosticsWitnesses } from "./shadow-diagnostics.mjs"; import { shadowLayersWitnesses } from "./shadow-layers.mjs"; +import { darkLayersWitnesses } from "./dark-layers.mjs"; import { sourceBudgetsWitnesses } from "./source-budgets.mjs"; +import { shadowReadDeadlinesWitnesses } from "./shadow-read-deadlines.mjs"; import { readTrace, witnessStates } from "./trace.mjs"; // One language-neutral witness evaluator for every profile with a completion @@ -72,6 +74,8 @@ export function evaluateCorpus(profile, corpus, recorder = createWitnessRecorder case "shadow": shadowWitnesses(corpus, recorder); shadowDiagnosticsWitnesses(corpus, recorder); break; case "recovery-read": actionLabels(corpus, recorder); recoveryReadWitnesses(corpus, recorder); recoveryAdmissionWitnesses(corpus, recorder); break; case "shadow-layers": actionLabels(corpus, recorder); shadowLayersWitnesses(corpus, recorder); break; + case "shadow-read-deadlines": actionLabels(corpus, recorder); shadowReadDeadlinesWitnesses(corpus, recorder); break; + case "dark-layers": actionLabels(corpus, recorder); darkLayersWitnesses(corpus, recorder); break; case "local-failure": actionLabels(corpus, recorder); localFailureWitnesses(corpus, recorder); break; case "source-budgets": actionLabels(corpus, recorder); sourceBudgetsWitnesses(corpus, recorder); break; case "runtime-boundaries": flowLabels(corpus, false, recorder); runtimeBoundaryWitnesses(profile, corpus, recorder); break; diff --git a/formal/replay/witnesses/public-prefix.d.mts b/formal/replay/witnesses/public-prefix.d.mts index f2016a0a..f9961a61 100644 --- a/formal/replay/witnesses/public-prefix.d.mts +++ b/formal/replay/witnesses/public-prefix.d.mts @@ -5,6 +5,7 @@ export interface PublicCheckpoint { step: number; observation: Record; diagnostics?: Record; + io?: Record; } export interface PublicPrefixWitnessRule { name: string; @@ -13,6 +14,6 @@ export interface PublicPrefixWitnessRule { checkpoints: PublicCheckpoint[]; } export function witnessCommand(name: string, choice?: number): string; -export function publicCheckpoint(step: number, observation: Record, diagnostics?: Record): PublicCheckpoint; +export function publicCheckpoint(step: number, observation: Record, diagnostics?: Record, io?: Record): PublicCheckpoint; export function publicPrefixRule(name: string, regression: string, commands: string[], ...checkpoints: PublicCheckpoint[]): PublicPrefixWitnessRule; export function publicPrefixWitnesses(histories: readonly RawHistory[], rules: readonly PublicPrefixWitnessRule[], recorder?: WitnessRecorder): Set; diff --git a/formal/replay/witnesses/public-prefix.mjs b/formal/replay/witnesses/public-prefix.mjs index 9b66d15d..cca980ae 100644 --- a/formal/replay/witnesses/public-prefix.mjs +++ b/formal/replay/witnesses/public-prefix.mjs @@ -1,15 +1,16 @@ +import { isDeepStrictEqual } from "node:util"; import { record } from "../itf.mjs"; import { decodeIntegers, explicitInput, witnessCommand } from "./trace.mjs"; import { createWitnessRecorder } from "./recorder.mjs"; export { witnessCommand }; -export const publicCheckpoint = (step, observation, diagnostics) => - ({ step, observation, ...(diagnostics === undefined ? {} : { diagnostics }) }); +export const publicCheckpoint = (step, observation, diagnostics, io) => + ({ step, observation, ...(diagnostics === undefined ? {} : { diagnostics }), ...(io === undefined ? {} : { io }) }); export const publicPrefixRule = (name, regression, commands, ...checkpoints) => ({ name, regression, commands, checkpoints }); function contains(observed, expected) { - return Object.entries(expected).every(([key, value]) => JSON.stringify(observed[key]) === JSON.stringify(value)); + return Object.entries(expected).every(([key, value]) => isDeepStrictEqual(observed[key], value)); } // Read declared inputs and public observations only. A rule requires every @@ -25,14 +26,15 @@ export function publicPrefixWitnesses(histories, rules, recorder = createWitness const input = explicitInput(state, path); commands.push(witnessCommand(input.name, input.choice)); const publicState = record(state.s, path); - return { observation: record(decodeIntegers(publicState.o, path), path), diagnostics: decodeIntegers(publicState.d, path) }; + return { observation: record(decodeIntegers(publicState.o, path), path), diagnostics: decodeIntegers(publicState.d, path), io: decodeIntegers(publicState.io, path) }; }); for (const rule of rules) { if (commands.length < rule.commands.length || !rule.commands.every((value, index) => value === commands[index])) continue; if (rule.checkpoints.every(check => { const actual = observations[check.step]; return contains(actual.observation, check.observation) && (check.diagnostics === undefined - || (actual.diagnostics !== undefined && contains(record(actual.diagnostics, path), check.diagnostics))); + || (actual.diagnostics !== undefined && contains(record(actual.diagnostics, path), check.diagnostics))) + && (check.io === undefined || (actual.io !== undefined && contains(record(actual.io, path), check.io))); })) recorder.credit(rule.name, ...rule.checkpoints.map(check => check.step)); } } diff --git a/formal/replay/witnesses/shadow-read-deadlines.d.mts b/formal/replay/witnesses/shadow-read-deadlines.d.mts new file mode 100644 index 00000000..d86bcf72 --- /dev/null +++ b/formal/replay/witnesses/shadow-read-deadlines.d.mts @@ -0,0 +1,5 @@ +import type { PublicPrefixWitnessRule } from "./public-prefix.mjs"; +import type { WitnessRecorder } from "./recorder.mjs"; +import type { RawHistory } from "./trace.mjs"; +export const shadowReadDeadlinesWitnessRules: readonly PublicPrefixWitnessRule[]; +export function shadowReadDeadlinesWitnesses(histories: readonly RawHistory[], recorder?: WitnessRecorder): Set; diff --git a/formal/replay/witnesses/shadow-read-deadlines.mjs b/formal/replay/witnesses/shadow-read-deadlines.mjs new file mode 100644 index 00000000..26c2b075 --- /dev/null +++ b/formal/replay/witnesses/shadow-read-deadlines.mjs @@ -0,0 +1,40 @@ +import { publicPrefixWitnesses, publicPrefixRule as rule, publicCheckpoint as check, witnessCommand as command } from "./public-prefix.mjs"; + +// Inputs select the schedule; public verdicts, read contexts and cancellation +// signals establish the consequences without reading model-owned jobs/timers. +export const shadowReadDeadlinesWitnessRules = [ + rule("c0-read-timeout-retains-raw-capacity", "c0ReadDeadlineKeepsRawCapacityTest", + [command("init"), command("beginCall", 0), command("resolveLoader", 1), command("advance", 5), command("beginCall", 1), command("resolveLoader", 3), command("advance", 10), command("releaseRead", 0), command("beginCall", 1)], + check(3, { calls: [1], shadow: ["redis_error"] }, undefined, { aborted: [0] }), + check(4, { reads: 1, shadow: ["redis_error", "dropped"] }), + check(6, { calls: [1, 1], shadow: ["redis_error", "dropped"] }), + check(8, { reads: 2, loaders: 3, loads: 0, dumps: 0, shadow: ["redis_error", "dropped"] })), + rule("c1-read-timeout-retains-raw-capacity", "c1ReadDeadlineKeepsRawCapacityTest", + [command("init"), command("seed", 0), command("beginCall", 0), command("releaseRead", 0), command("resolveLoader", 2), command("releaseLoad", 0), command("advance", 5), command("beginCall", 1), command("resolveLoader", 3), command("advance", 10), command("releaseRead", 1), command("beginCall", 1)], + check(3, { calls: [0], shadow: [], loads: 0, dumps: 0 }), + check(6, { calls: [2], shadow: ["confirmation_error"] }, undefined, { aborted: [1] }), + check(7, { reads: 2, shadow: ["confirmation_error", "dropped"] }), + check(9, { shadow: ["confirmation_error", "dropped"] }), + check(11, { reads: 3, loaders: 3, dumps: 0, shadow: ["confirmation_error", "dropped"] })), + rule("job-then-read-timeout-has-one-verdict", "jobDeadlineThenReadDeadlineReportsOneVerdictTest", + [command("init"), command("policy", 20), command("beginCall", 0), command("resolveLoader", 1), command("advance", 10), command("beginCall", 1), command("resolveLoader", 3), command("advance", 10), command("releaseRead", 0), command("beginCall", 1)], + check(4, { shadow: ["timeout"] }, undefined, { aborted: [] }), + check(5, { reads: 1, shadow: ["timeout", "dropped"] }), + check(7, { calls: [1, 1], shadow: ["timeout", "dropped"] }, undefined, { aborted: [0] }), + check(9, { reads: 2, loaders: 3, shadow: ["timeout", "dropped"] })), + rule("confirmation-keeps-captured-read-budget", "confirmationKeepsCapturedReadBudgetTest", + [command("init"), command("seed", 0), command("beginCall", 0), command("releaseRead", 0), command("policy", 20), command("resolveLoader", 2), command("releaseLoad", 0), command("advance", 5), command("releaseRead", 1), command("beginCall", 1)], + check(3, { calls: [0], shadow: [], loads: 0, dumps: 0 }), + check(6, {}, undefined, { budgets: [5, 5] }), + check(7, { shadow: ["confirmation_error"] }, undefined, { aborted: [1] }), + check(9, { reads: 3 }, undefined, { budgets: [5, 5, 20] })), + rule("confirmation-read-budget-starts-at-dispatch", "confirmationReadBudgetStartsAtItsOwnDispatchTest", + [command("init"), command("seed", 0), command("beginCall", 0), command("releaseRead", 0), command("advance", 4), command("resolveLoader", 2), command("releaseLoad", 0), command("advance", 4), command("advance", 1)], + check(3, { calls: [0], shadow: [], loads: 0, dumps: 0 }), + check(7, { calls: [2], shadow: [] }, undefined, { aborted: [] }), + check(8, { shadow: ["confirmation_error"] }, undefined, { aborted: [1] })), + rule("timely-shadow-read-cancels-deadline-and-fills", "readBeforeDeadlineCanFillAndCancelsItsTimerTest", + [command("init"), command("beginCall", 0), command("resolveLoader", 1), command("advance", 4), command("releaseRead", 0), command("releaseDump", 0), command("releaseWrite", 0), command("advance", 10)], + check(7, { calls: [1], shadow: ["filled"], writes: 1 }, undefined, { aborted: [] })), +]; +export const shadowReadDeadlinesWitnesses = (histories, recorder) => publicPrefixWitnesses(histories, shadowReadDeadlinesWitnessRules, recorder); diff --git a/formal/run-models.mjs b/formal/run-models.mjs index e3a4b215..9a26ad83 100644 --- a/formal/run-models.mjs +++ b/formal/run-models.mjs @@ -1,4 +1,3 @@ -import { spawnSync } from 'node:child_process'; import { mkdirSync, readFileSync, readdirSync, rmSync } from 'node:fs'; import { basename, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -51,9 +50,6 @@ export function executionPlan(mode, manifest = readExecution(), seed = process.e } } } - // Every compiling fault in the manifest catalog runs once, after the models - // it mutates have been checked in their unmodified form. - if (mode === 'check') commands.push({ command: 'node', args: ['formal/check-model-properties.mjs'] }); return commands; } @@ -68,14 +64,11 @@ export function bindGeneratedTrace(profile, text, path) { // Group a plan into chains that run side by side. A model's Quint jobs keep // their plan order inside one chain (check: typecheck, run, test; generate: // sampled run, then regression export); jobs for different models and the -// vector exports are independent. The closing challenge run is not a chain: it -// mutates its own copies of several models and starts only after every chain -// has checked those models unmodified. -const isChallengeRun = job => job.command === 'node' && job.args[0] === 'formal/check-model-properties.mjs'; +// vector exports are independent. The full validation plan runs the pinned +// fault campaign separately, after these unmodified model checks complete. export function executionChains(commands) { const chains = [], byModel = new Map(); for (const job of commands) { - if (isChallengeRun(job)) continue; if (job.command !== 'quint') { chains.push([job]); continue; } const model = job.args[1]; if (!byModel.has(model)) { byModel.set(model, []); chains.push(byModel.get(model)); } @@ -114,13 +107,6 @@ async function executePlan(mode, manifest, commands) { const chains = executionChains(commands); console.log(`Running ${chains.length} Quint job chains, ${concurrency} at a time, one thread each`); await runPool(chains.map(chain => async () => { for (const job of chain) await executeJob(job); }), { concurrency }); - for (const job of commands.filter(isChallengeRun)) { - // The challenge run prints its own log groups; wrapping it would nest them. - console.log(`${job.command} ${job.args.join(' ')}`); - const result = spawnSync(job.command, job.args, { cwd: root, stdio: 'inherit' }); - if (result.error) throw result.error; - if (result.status !== 0) throw new CommandFailure(`${job.args.join(' ')} failed (${result.signal ?? `exit ${result.status}`})`, result); - } } if (process.argv[1] === fileURLToPath(import.meta.url)) { diff --git a/formal/run-vector-boundary.mjs b/formal/run-vector-boundary.mjs new file mode 100644 index 00000000..58ead1d1 --- /dev/null +++ b/formal/run-vector-boundary.mjs @@ -0,0 +1,38 @@ +import { spawnSync } from 'node:child_process'; +import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { assessVectorBoundary, validVectorResult } from './vector-evidence.mjs'; + +export function runVectorBoundary({ evidence, port, history, label, output, root, workspace, env, go = 'go' }) { + const sample = evidence.vector.samples[port]; + if (!sample) throw new Error(`Unknown vector binding: ${port}`); + const prefix = resolve(output, 'boundary', label, history); + mkdirSync(dirname(prefix), { recursive: true }); + const request = `${prefix}.request.json`, out = `${prefix}.native.json`; + writeFileSync(request, JSON.stringify(sample.request) + '\n'); + rmSync(out, { force: true }); + const selected = { ...env, DIALCACHE_VECTOR_REQUEST: request, DIALCACHE_VECTOR_OUT: out }; + const result = port === 'typescript' + ? spawnSync(process.execPath, [resolve(root, 'node_modules/vitest/vitest.mjs'), 'run', 'test/formal-vector-boundary.test.ts', '--coverage.enabled=false'], + { cwd: workspace, env: selected, encoding: 'utf8', timeout: 120_000 }) + : spawnSync(go, ['test', '-count=1', '-timeout=60s', '-run', '^TestVectorBoundaryDriver$', '.'], + { cwd: resolve(workspace, 'go'), env: selected, encoding: 'utf8', timeout: 120_000 }); + writeFileSync(`${prefix}.log`, (result.stdout ?? '') + (result.stderr ?? '')); + const recording = { history, path: evidence.vector.artifact, via: 'vector', completed: false, lastStep: -1, divergences: [] }; + try { + if (result.error || result.signal || result.status !== 0) throw new Error(`Native vector process failed: ${result.error?.message ?? result.signal ?? result.status}`); + const native = JSON.parse(readFileSync(out, 'utf8')); + if (!native || Object.keys(native).join() !== 'actual' || !validVectorResult(sample.request.operation, native.actual)) throw new Error('Missing or malformed native vector output'); + Object.assign(recording, { completed: true, lastStep: 0, vectorResult: { + history, port, row: sample.row, artifactSha256: evidence.vector.artifactSha256, inputSha256: sample.inputSha256, actual: native.actual, + } }); + const assessed = assessVectorBoundary(evidence, recording); + if (assessed.state === 'unreached') throw new Error(assessed.reason); + recording.divergences = assessed.divergences; + } catch (error) { + recording.completed = false; + recording.error = `${error.message}; see boundary/${label}/${history}.log`; + } + writeFileSync(`${prefix}.json`, JSON.stringify(recording, null, 2) + '\n'); + return recording; +} diff --git a/formal/semantic-cases.json b/formal/semantic-cases.json index 85dd31d3..68f505d8 100644 --- a/formal/semantic-cases.json +++ b/formal/semantic-cases.json @@ -2,9 +2,10 @@ "schemaVersion": 1, "scope": "Reviewed feature and known-corner-case inventory derived from C01-C60/W01-W09, docs, tests, and all positive portable scenarios. Evidence describes bounded observations; this is not an exhaustive input or schedule proof. Native binding cases are catalogued separately.", "cases": [ - {"id": "C01.no-cache-plumbing", "rule": "disabled calls bypass policy and caches", "scenarios": ["disabled calls bypass policy and caches"], "generated": [{"profile": "scope", "witness": "disabled-bypass"}], "vectors": [], "contracts": ["C01"], "quintReplays": ["core/disabledAndRequestCallsKeepTheirOwnLifetimesTest"], "models": [ + {"id": "C01.no-cache-plumbing", "rule": "disabled calls bypass policy and caches", "scenarios": ["disabled calls bypass policy and caches"], "generated": [{"profile": "scope", "witness": "disabled-bypass"}, {"profile": "dark-layers", "witness": "closed-scope-bypasses-dark-work"}], "vectors": [], "contracts": ["C01"], "quintReplays": ["core/disabledAndRequestCallsKeepTheirOwnLifetimesTest", "dark-layers/closedScopeCallerBypassesLayersAndAdmitsNoJobTest"], "models": [ {"ref": "formal/dialcache-core.qnt:passThroughSkipsCacheMachinery", "scope": "Disabled/outside/closed calls make zero policy, key, cache read, or cache write operations in the presence-bit model; no clock claim."}, - {"ref": "formal/dialcache-conformance.qnt:disabledAndRequestCallsKeepTheirOwnLifetimesTest", "scope": "The introductory profile alternates disabled calls and separate request-local pairs across a source change; results and source/read/write counts establish those lifetimes. This does not separately observe key construction or runtime policy."}]}, + {"ref": "formal/dialcache-conformance.qnt:disabledAndRequestCallsKeepTheirOwnLifetimesTest", "scope": "The introductory profile alternates disabled calls and separate request-local pairs across a source change; results and source/read/write counts establish those lifetimes. This does not separately observe key construction or runtime policy."}, + {"ref": "formal/dialcache-dark-layers-conformance.qnt:closedScopeCallerBypassesLayersAndAdmitsNoJobTest", "scope": "A closed-scope caller starts an unbounded bypass source without policy lookup, cache reads, shadow admission or fallback-error diagnostics."}], "definitions": [{"ref": "formal/kernel/shadow.qnt:beginDark", "scope": "A closed-scope caller starts an unbounded bypass source without policy lookup, cache reads, shadow admission or fallback-error diagnostics."}]}, {"id": "C01.no-deadline", "rule": "outside calls have no fallback deadline or sharing", "scenarios": ["outside calls have no fallback deadline or sharing"], "generated": [{"profile": "source-budgets", "witness": "outside-calls-skip-source-deadlines-sharing-and-publication"}], "vectors": [], "contracts": ["C01"], "provenance": ["test/dialcache-liveness.test.ts:keeps initially disabled calls as true pass-through", "src/dialcache.ts:executeCacheOperation"], "quintReplays": ["source-budgets/outsideAndInvalidOutsideCallsIgnoreDeadlineAndSharingTest"], "models": [ {"ref": "formal/dialcache-source-budgets-conformance.qnt:outsideSourcesHaveNoDeadline", "scope": "Held policy and source gates, eight callers, and local-only storage exercise default/unbounded/ten-millisecond budgets. Public result and subsequent local reuse distinguish source deadline ownership, pre-source policy time, and enabled key-error behavior; no remote/read/shadow timing claim."}, {"ref": "formal/dialcache-source-budgets-conformance.qnt:outsideAndInvalidOutsideCallsIgnoreDeadlineAndSharingTest", "scope": "Held policy and source gates, eight callers, and local-only storage exercise default/unbounded/ten-millisecond budgets. Public result and subsequent local reuse distinguish source deadline ownership, pre-source policy time, and enabled key-error behavior; no remote/read/shadow timing claim."}]}, @@ -143,17 +144,19 @@ {"id": "C10.zero-capacity", "rule": "zero local capacity retains coalescing but no settled value", "scenarios": ["zero local capacity retains coalescing but no settled value"], "generated": [{"profile": "layers", "witness": "zero-capacity-still-shares"}, {"profile": "layers", "witness": "zero-capacity-reloads"}], "vectors": [], "contracts": ["C10"], "quintReplays": ["layers/zeroCapacityKeepsSharingWithoutStorageTest"], "models": [ {"ref": "formal/dialcache-layers-conformance.qnt:zeroCapacityHasNoLocalValues", "scope": "Zero capacity stores no local values, shares concurrent work, and starts a new source after settlement."}, {"ref": "formal/dialcache-layers-conformance.qnt:zeroCapacityKeepsSharingWithoutStorageTest", "scope": "Zero capacity stores no local values, shares concurrent work, and starts a new source after settlement."}]}, + {"id": "C11.process-inspection", "rule": "Public process-coalescing snapshots count only registered leaders and process followers for the selected instance, report the oldest monotonic registration age, and clear when callers settle even while raw source or shadow work continues.", "contracts": ["C11", "C25", "C58"], "scenarios": [], "generated": [{"profile": "dark-layers", "witness": "inspection-counts-process-followers-and-oldest"}, {"profile": "dark-layers", "witness": "inspection-isolates-instances-and-wall-shifts"}, {"profile": "dark-layers", "witness": "inspection-excludes-request-and-uncoalesced"}, {"profile": "dark-layers", "witness": "inspection-clears-timeout-before-raw-settlement"}, {"profile": "dark-layers", "witness": "inspection-excludes-unfinished-shadow"}], "vectors": [], "quintReplays": ["dark-layers/inspectionTracksKeysFollowersAndOldestLeaderTest", "dark-layers/inspectionIsPerInstanceAndUsesMonotonicTimeTest", "dark-layers/inspectionExcludesRequestOnlyAndUncoalescedWorkTest", "dark-layers/inspectionClearsTimedOutCallersWhileRawWorkRemainsTest", "dark-layers/inspectionExcludesUnfinishedShadowAfterCallerSuccessTest"], "models": [{"ref": "formal/dialcache-dark-layers-conformance.qnt:inspectionTracksKeysFollowersAndOldestLeaderTest", "scope": "Two live keys and request/process followers produce two leaders and one process follower. Success removes the oldest key and selects the remaining age; rejection empties the snapshot."}, {"ref": "formal/dialcache-dark-layers-conformance.qnt:inspectionIsPerInstanceAndUsesMonotonicTimeTest", "scope": "Two instances retain separate live counts and monotonic ages despite wall rollback; settling instance zero leaves instance one active."}, {"ref": "formal/dialcache-dark-layers-conformance.qnt:inspectionExcludesRequestOnlyAndUncoalescedWorkTest", "scope": "Pending request-only shared work and pending coalesce:false sources both remain absent from process snapshots."}, {"ref": "formal/dialcache-dark-layers-conformance.qnt:inspectionClearsTimedOutCallersWhileRawWorkRemainsTest", "scope": "Source timeout clears leader/follower counts before held raw effects settle. A retry has its own live snapshot, unchanged by the abandoned source and read completing."}, {"ref": "formal/dialcache-dark-layers-conformance.qnt:inspectionExcludesUnfinishedShadowAfterCallerSuccessTest", "scope": "A successful caller clears process ownership while its admitted dark job still holds serialization and then transport completion."}, {"ref": "formal/dialcache-dark-layers-conformance.qnt:inspectionsDescribeLiveLeaders", "scope": "At each public inspection the leader count equals independently selected pending shared sources of the requested instance."}, {"ref": "formal/dialcache-dark-layers-conformance.qnt:inspectionsCountOnlyProcessFollowers", "scope": "At each public inspection the follower count equals pending callers whose admission receipt joined process scope on the requested instance."}, {"ref": "formal/dialcache-dark-layers-conformance.qnt:inspectionAgeUsesTheOldestLiveMonotonicStart", "scope": "At each public inspection the age is absent if no shared source remains; otherwise it equals a live source age and is at least every other live age, using monotonic start receipts."}], "definitions": [{"ref": "formal/kernel/flights.qnt:inspectProcess", "scope": "Snapshots select currently registered process owners of one instance and report their acquired registration ages and process-only followers; inspection neither acquires nor releases work."}]}, {"id": "C11.distinct-keys", "rule": "different keys own independent flights", "scenarios": ["different keys own independent flights"], "generated": [{"profile": "policy", "witness": "cross-key-overlap"}], "vectors": [], "contracts": ["C11"], "quintReplays": ["layers/pendingSourcesKeepEntityAndOperationIdentityTest"], "models": [ {"ref": "formal/dialcache-layers-conformance.qnt:pendingSourcesKeepEntityAndOperationIdentityTest", "scope": "different keys own independent flights. The deterministic regression checks the public result and subsequent reuse for this bounded fixture."}, {"ref": "formal/dialcache-layers-conformance.qnt:sourceOwnershipNeverCrossesKeyOrInstance", "scope": "different keys own independent flights. The deterministic regression checks the public result and subsequent reuse for this bounded fixture."}], "definitions": [ {"ref": "formal/dialcache-policy-conformance.qnt:releasePolicy", "scope": "Registration is indexed by key; settlement removes only the matching source ownership and publishes to that source key."}, {"ref": "formal/kernel/serving.qnt:settle", "scope": "Registration is indexed by key; settlement removes only the matching source ownership and publishes to that source key."}]}, - {"id": "C11.one-registered-flight", "rule": "Eligible concurrent same-key calls share one registered source", "scenarios": [], "generated": [{"profile": "policy", "witness": "coalesced-result"}], "vectors": [], "contracts": ["C11"], "quintReplays": ["runtime-boundaries/runtimeLocalTtlEnablesSharingWithoutRemoteTtlTest", "core/coalescedPairPublishesOneReusableValueTest", "scope/pendingRequestFlightIsJoinedBeforeMemoReadTest", "layers/coalescingCallerJoinsPendingFlightBeforeSeededFrameTest"], "models": [ + {"id": "C11.one-registered-flight", "rule": "Eligible concurrent same-key calls share one registered source", "scenarios": [], "generated": [{"profile": "policy", "witness": "coalesced-result"}, {"profile": "dark-layers", "witness": "uncoalesced-publication-keeps-dark-leader"}], "vectors": [], "contracts": ["C11"], "quintReplays": ["runtime-boundaries/runtimeLocalTtlEnablesSharingWithoutRemoteTtlTest", "core/coalescedPairPublishesOneReusableValueTest", "scope/pendingRequestFlightIsJoinedBeforeMemoReadTest", "layers/coalescingCallerJoinsPendingFlightBeforeSeededFrameTest", "dark-layers/uncoalescedPublicationDoesNotPreemptTheRegisteredLeaderTest"], "models": [ {"ref": "formal/dialcache-effects-conformance.qnt:oneRegisteredSource", "scope": "At most one source has registered ownership in the one-key effects profile. Abandoned external work is distinct."}, {"ref": "formal/dialcache-runtime-boundaries-conformance.qnt:runtimeLocalTtlEnablesSharingWithoutRemoteTtlTest", "scope": "The newly enabled local layer makes two pending same-key calls share one source, and retains its Absent result for a later local hit without any Redis read/write."}, {"ref": "formal/dialcache-conformance.qnt:coalescedPairPublishesOneReusableValueTest", "scope": "Two controlled same-key call pairs, separated by an external source change, share one initial source and reuse its published local value."}, {"ref": "formal/dialcache-scope-conformance.qnt:pendingRequestFlightIsJoinedBeforeMemoReadTest", "scope": "A coalescing caller joins the request flight registered under its row before the memo is read, although an independent source has since memoized a value into that row; the joined caller completes with the flight's value."}, - {"ref": "formal/dialcache-layers-conformance.qnt:coalescingCallerJoinsPendingFlightBeforeSeededFrameTest", "scope": "A coalescing caller joins the flight registered for its identity before reading the remote layer; a frame seeded while the flight is pending does not serve it. This check covers the bounded model clause: coalescingCallerJoinsPendingFlightBeforeSeededFrameTest."}]}, + {"ref": "formal/dialcache-layers-conformance.qnt:coalescingCallerJoinsPendingFlightBeforeSeededFrameTest", "scope": "A coalescing caller joins the flight registered for its identity before reading the remote layer; a frame seeded while the flight is pending does not serve it. This check covers the bounded model clause: coalescingCallerJoinsPendingFlightBeforeSeededFrameTest."}, + {"ref": "formal/dialcache-dark-layers-conformance.qnt:uncoalescedPublicationDoesNotPreemptTheRegisteredLeaderTest", "scope": "An independent source settles beside a registered dark leader; a later coalescing caller still joins that leader and receives its result rather than the independent result."}]}, {"id": "C12.separate-request-memos", "rule": "request misses join one process flight then memoize separately", "scenarios": ["request misses join one process flight then memoize separately"], "generated": [{"profile": "layers", "witness": "request-misses-share-process-flight"}], "vectors": [], "contracts": ["C12"], "quintReplays": ["layers/requestMissesShareProcessAndMemoizeSeparatelyTest"], "models": [ {"ref": "formal/dialcache-layers-conformance.qnt:requestMissesShareProcessAndMemoizeSeparatelyTest", "scope": "Two request scopes join one process source and both reuse their separate memos after shared settlement."}]}, {"id": "C13.uncoalesced-caching", "rule": "coalescing off keeps settled caching", "scenarios": ["coalescing off keeps settled caching"], "generated": [{"profile": "policy", "witness": "uncoalesced-local-settled-hit"}, {"profile": "policy", "witness": "uncoalesced-remote-settled-hit"}, {"profile": "scope", "witness": "uncoalesced-request-settled-hit"}], "vectors": [], "contracts": ["C13"], "provenance": ["test/dialcache-coalescing.test.ts:performs independent Redis reads and writes when coalescing is disabled", "test/dialcache-coalescing.test.ts:memoizes sequential request-local reads without coalescing concurrent ones when coalescing is disabled"], "quintReplays": ["policy/independentLocalCallsStillReuseSettledValueTest", "runtime-boundaries/disabledSharingDoesNotJoinRegisteredFlightTest", "scope/independentSourceIsNotRegisteredForLaterSharingTest", "runtime-boundaries/defaultSharingJoinsRegisteredLeaderOverWarmedLocalTest", "admission/uncoalescedCallerReadsAloneTest"], "models": [ @@ -164,13 +167,14 @@ {"ref": "formal/dialcache-admission-conformance.qnt:uncoalescedCallerReadsAloneTest", "scope": "A caller whose reply disables coalescing dispatches its own remote read beside the registered shared leader for the same identity; its hit is judged first and admits the job, and the leader's later hit drops as a duplicate."}], "definitions": [ {"ref": "formal/dialcache-policy-conformance.qnt:releasePolicy", "scope": "Independent-policy fixture disables registration but still captures publication TTLs and replaces local/remote values on accepted source completion. No independent property asserts general last-writer behavior."}, {"ref": "formal/kernel/serving.qnt:settle", "scope": "Independent-policy fixture disables registration but still captures publication TTLs and replaces local/remote values on accepted source completion. No independent property asserts general last-writer behavior."}]}, - {"id": "C13.local-last-writer", "rule": "independent local publication is last writer wins", "scenarios": ["independent local publication is last writer wins"], "generated": [{"profile": "policy", "witness": "independent-local-last-completion-probed"}], "vectors": [], "contracts": ["C13"], "quintReplays": ["policy/independentLocalPublicationUsesLastCompletionTest", "policy/independentFailureKeepsSettledLocalValueTest", "source-budgets/rejectedSourceLeavesPublishedLocalEntryTest", "source-budgets/republishedEqualValueRestampsLocalExpiryTest"], "models": [ + {"id": "C13.local-last-writer", "rule": "independent local publication is last writer wins", "scenarios": ["independent local publication is last writer wins"], "generated": [{"profile": "policy", "witness": "independent-local-last-completion-probed"}], "vectors": [], "contracts": ["C13"], "quintReplays": ["policy/independentLocalPublicationUsesLastCompletionTest", "policy/independentFailureKeepsSettledLocalValueTest", "policy/independentFailureKeepsLocalValueWhenRemoteFailsTest", "source-budgets/rejectedSourceLeavesPublishedLocalEntryTest", "source-budgets/republishedEqualValueRestampsLocalExpiryTest"], "models": [ {"ref": "formal/dialcache-policy-conformance.qnt:independentLocalPublicationUsesLastCompletionTest", "scope": "independent local publication is last writer wins. This check covers the bounded model clause: independentLocalPublicationUsesLastCompletionTest. Host representations and other feature combinations retain separate fixed/native evidence."}, {"ref": "formal/dialcache-policy-conformance.qnt:independentFailureKeepsSettledLocalValueTest", "scope": "In the policy profile with coalescing off and no remote layer, two callers on one key start their own sources; the first completes and publishes locally, the second settles with the source error and is not a local writer: the third call is served the entry the earlier completed source published, without a third source. No remote or memo claim."}, + {"ref": "formal/dialcache-policy-conformance.qnt:independentFailureKeepsLocalValueWhenRemoteFailsTest", "scope": "Two uncoalesced sources begin after remote misses. The first publishes locally and remotely; the second rejects with no stale-recovery classifier configured, so it cannot recover the newer refill. With remote reads then failing, the retry still serves the surviving local value without a third source or remote read. This exposes local deletion on the active remote-chain failure path."}, {"ref": "formal/dialcache-source-budgets-conformance.qnt:rejectedSourceLeavesPublishedLocalEntryTest", "scope": "In the composed source-budgets profile a source that settles with the source error completes its caller with that error and leaves the local entry an earlier accepted source published: the next enabled call is served locally without a third source. The rejected source is a bypass (outside) one, since a source that may warm local starts only while no live entry exists; no remote or memo claim."}, {"ref": "formal/dialcache-source-budgets-conformance.qnt:republishedEqualValueRestampsLocalExpiryTest", "scope": "In the composed source-budgets profile the source started at the exact 1000ms expiry of a local entry republishes the same value and stamps a new insertion expiry: an enabled call one millisecond before the second boundary is served locally without a third source. Distinguishes a publication that skips an equal value from one that replaces it; no remote or memo claim."}], "definitions": [ {"ref": "formal/dialcache-policy-conformance.qnt:releasePolicy", "scope": "Independent-policy fixture disables registration but still captures publication TTLs and replaces local/remote values on accepted source completion. No independent property asserts general last-writer behavior."}, - {"ref": "formal/kernel/serving.qnt:settle", "scope": "Independent-policy fixture disables registration but still captures publication TTLs and replaces local/remote values on accepted source completion. No independent property asserts general last-writer behavior."}]}, + {"ref": "formal/kernel/serving.qnt:publishLocal", "scope": "The local-only and layered traversals share the accepted-source gate and captured local TTL. A rejected source leaves existing local storage unchanged; an accepted publication replaces the occupied slot through local_storage.putLocal."}]}, {"id": "C13.remote-last-writer", "rule": "independent remote publication is last writer wins", "scenarios": ["independent remote publication is last writer wins"], "generated": [{"profile": "policy", "witness": "independent-remote-last-completion-probed"}], "vectors": [], "contracts": ["C13"], "quintReplays": ["policy/independentRemotePublicationUsesLastCompletionTest", "runtime-boundaries/remoteHitPromotesIntoNewlyEnabledLocalLayerTest"], "models": [ {"ref": "formal/dialcache-policy-conformance.qnt:independentRemotePublicationUsesLastCompletionTest", "scope": "independent remote publication is last writer wins. This check covers the bounded model clause: independentRemotePublicationUsesLastCompletionTest. Host representations and other feature combinations retain separate fixed/native evidence."}, {"ref": "formal/dialcache-runtime-boundaries-conformance.qnt:remoteHitPromotesIntoNewlyEnabledLocalLayerTest", "scope": "A remote hit warms the newly enabled local layer; another already-running remote-only source then replaces Redis while the captured local value remains unchanged, distinguished by later local-enabled and remote-only reads."}], "definitions": [ @@ -207,8 +211,9 @@ {"id": "C18.captured-insertion", "rule": "pending invocation keeps insertion TTL snapshot", "scenarios": ["pending invocation keeps insertion TTL snapshot"], "generated": [{"profile": "policy", "witness": "publication-after-policy-change"}], "vectors": [], "contracts": ["C18"], "models": [ {"ref": "formal/dialcache-runtime-policy.qnt:existingLocalEntryKeepsInsertionTtl", "scope": "An invocation captures its TTL before later overlay changes; the installed local TTL remains the insertion value."}, {"ref": "formal/dialcache-runtime-policy.qnt:policyChangeDuringInvocationTest", "scope": "An invocation captures its TTL before later overlay changes; the installed local TTL remains the insertion value."}]}, - {"id": "C18.captured-shadow-fill", "rule": "dark fill preserves accepted runtime TTL and retention after policy changes", "scenarios": ["dark fill preserves accepted runtime TTL and retention after policy changes"], "generated": [{"profile": "shadow-layers", "witness": "dark-fill-policy-snapshot-controls-physical-expiry"}], "vectors": [], "contracts": ["C18"], "quintReplays": ["shadow-layers/darkFillRetainsPolicyThroughSerializationTest"], "models": [ - {"ref": "formal/dialcache-shadow-layers-conformance.qnt:darkFillRetainsPolicyThroughSerializationTest", "scope": "The job captures fresh60s/retention120s. Runtime policy changes to fresh120s/retention180s while its caller source and then serialization are held; dispatched write TTL remains120000ms. At that physical expiry a serving probe under the newer logical policy rejects its source and reports recovery miss, proving the old value was not retained for180s."}], "definitions": [ + {"id": "C18.captured-shadow-fill", "rule": "dark fill preserves accepted runtime TTL and retention after policy changes", "scenarios": ["dark fill preserves accepted runtime TTL and retention after policy changes"], "generated": [{"profile": "shadow-layers", "witness": "dark-fill-policy-snapshot-controls-physical-expiry"}, {"profile": "dark-layers", "witness": "held-dark-fill-keeps-captured-retention"}], "vectors": [], "contracts": ["C18"], "quintReplays": ["shadow-layers/darkFillRetainsPolicyThroughSerializationTest", "dark-layers/darkFillCarriesCapturedRetentionOnTheHeldPathTest"], "models": [ + {"ref": "formal/dialcache-shadow-layers-conformance.qnt:darkFillRetainsPolicyThroughSerializationTest", "scope": "The job captures fresh60s/retention120s. Runtime policy changes to fresh120s/retention180s while its caller source and then serialization are held; dispatched write TTL remains120000ms. At that physical expiry a serving probe under the newer logical policy rejects its source and reports recovery miss, proving the old value was not retained for180s."}, + {"ref": "formal/dialcache-dark-layers-conformance.qnt:darkFillCarriesCapturedRetentionOnTheHeldPathTest", "scope": "A held dark fill dispatches with the retention captured at admission despite a later policy reply."}], "definitions": [ {"ref": "formal/kernel/shadow.qnt:admitJobs", "scope": "The job captures fresh60s/retention120s. Runtime policy changes to fresh120s/retention180s while its caller source and then serialization are held; dispatched write TTL remains120000ms. At that physical expiry a serving probe under the newer logical policy rejects its source and reports recovery miss, proving the old value was not retained for180s."}, {"ref": "formal/kernel/remote-writes.qnt:holdDump", "scope": "The job captures fresh60s/retention120s. Runtime policy changes to fresh120s/retention180s while its caller source and then serialization are held; dispatched write TTL remains120000ms. At that physical expiry a serving probe under the newer logical policy rejects its source and reports recovery miss, proving the old value was not retained for180s."}, {"ref": "formal/kernel/shadow.qnt:releaseDump", "scope": "The job captures fresh60s/retention120s. Runtime policy changes to fresh120s/retention180s while its caller source and then serialization are held; dispatched write TTL remains120000ms. At that physical expiry a serving probe under the newer logical policy rejects its source and reports recovery miss, proving the old value was not retained for180s."}, @@ -271,8 +276,8 @@ {"ref": "formal/dialcache-effects-conformance.qnt:operationReadBudgetOverridesInstanceTest", "scope": "Initial operation read budget10 overrides instance20; read timeout starts source at10 and source value survives without refill."}], "definitions": [ {"ref": "formal/dialcache-effects-conformance.qnt:initial", "scope": "Finite fixture modes provide distinct instance/operation/runtime accepted budgets; beginCall captures the read budget. The binding supplies the corresponding precedence inputs; this is not a general policy resolver proof."}, {"ref": "formal/dialcache-effects-conformance.qnt:beginCall", "scope": "Finite fixture modes provide distinct instance/operation/runtime accepted budgets; beginCall captures the read budget. The binding supplies the corresponding precedence inputs; this is not a general policy resolver proof."}]}, - {"id": "C23.runtime-read-budget", "rule": "runtime read deadline precedence", "scenarios": ["runtime read deadline precedence"], "generated": [{"profile": "effects", "witness": "initial-budget:3"}], "vectors": [], "contracts": ["C23"], "quintReplays": ["effects/runtimeReadBudgetOverridesOperationTest"], "models": [ - {"ref": "formal/dialcache-effects-conformance.qnt:runtimeReadBudgetOverridesOperationTest", "scope": "Initial runtime read budget30 overrides operation10 and instance20; no source at20, timeout starts source at30."}], "definitions": [ + {"id": "C23.runtime-read-budget", "rule": "Runtime read deadline precedence applies to new work; an admitted shadow job retains its captured read budget for confirmation", "scenarios": ["runtime read deadline precedence"], "generated": [{"profile": "shadow-read-deadlines", "witness": "confirmation-keeps-captured-read-budget"}, {"profile": "effects", "witness": "initial-budget:3"}], "vectors": [], "contracts": ["C23"], "quintReplays": ["shadow-read-deadlines/confirmationKeepsCapturedReadBudgetTest", "effects/runtimeReadBudgetOverridesOperationTest"], "models": [{"ref": "formal/dialcache-shadow-read-deadlines-conformance.qnt:confirmationKeepsCapturedReadBudgetTest", "scope": "An admitted job keeps its 5 ms read budget when runtime policy changes to 20 ms before C1 dispatch. That C1 expires at its captured budget; a subsequently admitted job advertises the new 20 ms budget."}, + {"ref": "formal/dialcache-effects-conformance.qnt:runtimeReadBudgetOverridesOperationTest", "scope": "Initial runtime read budget30 overrides operation10 and instance20; no source at20, timeout starts source at30."}], "definitions": [{"ref": "formal/kernel/shadow-reads.qnt:begin", "scope": "An admitted job keeps its 5 ms read budget when runtime policy changes to 20 ms before C1 dispatch. That C1 expires at its captured budget; a subsequently admitted job advertises the new 20 ms budget."}, {"ref": "formal/kernel/shadow-reads.qnt:budgetFor", "scope": "An admitted job keeps its 5 ms read budget when runtime policy changes to 20 ms before C1 dispatch. That C1 expires at its captured budget; a subsequently admitted job advertises the new 20 ms budget."}, {"ref": "formal/kernel/shadow-reads.qnt:releaseLoad", "scope": "An admitted job keeps its 5 ms read budget when runtime policy changes to 20 ms before C1 dispatch. That C1 expires at its captured budget; a subsequently admitted job advertises the new 20 ms budget."}, {"ref": "formal/dialcache-effects-conformance.qnt:initial", "scope": "Finite fixture modes provide distinct instance/operation/runtime accepted budgets; beginCall captures the read budget. The binding supplies the corresponding precedence inputs; this is not a general policy resolver proof."}, {"ref": "formal/dialcache-effects-conformance.qnt:beginCall", "scope": "Finite fixture modes provide distinct instance/operation/runtime accepted budgets; beginCall captures the read budget. The binding supplies the corresponding precedence inputs; this is not a general policy resolver proof."}]}, {"id": "C23.source-start", "rule": "fallback deadline starts after remote read completes", "scenarios": ["fallback deadline starts after remote read completes"], "generated": [{"profile": "effects", "witness": "read-timeout-starts-source"}], "vectors": [], "contracts": ["C23"], "models": [ @@ -356,10 +361,12 @@ {"ref": "formal/kernel/metrics.qnt:event", "scope": "Observer-failure fixture changes exporter behavior but no cache/source transition branch. The generated actual-API replay checks consequences; the model does not execute exporter code."}, {"ref": "formal/kernel/metrics.qnt:settleRead", "scope": "Observer-failure fixture changes exporter behavior but no cache/source transition branch. The generated actual-API replay checks consequences; the model does not execute exporter code."}, {"ref": "formal/kernel/remote-writes.qnt:settleLoader", "scope": "Observer-failure fixture changes exporter behavior but no cache/source transition branch. The generated actual-API replay checks consequences; the model does not execute exporter code."}]}, - {"id": "C31.tracked-no-local-fill", "rule": "tracked refill suppresses local until a Redis hit warms it", "scenarios": ["tracked refill suppresses local until a Redis hit warms it"], "generated": [{"profile": "layers", "witness": "tracked-refill-needs-remote-validation"}, {"profile": "layers", "witness": "validated-tracked-hit-warms-local"}, {"profile": "recovery-read", "witness": "tracked-source-requires-validation-before-local-reuse"}], "vectors": [], "contracts": ["C31"], "quintReplays": ["recovery-read/trackedSourceNeedsRemoteValidationBeforeLocalReuseTest", "layers/trackedSourceWaitsForValidatedReadToWarmLocalTest"], "models": [ + {"id": "C31.tracked-no-local-fill", "rule": "tracked refill suppresses local until a Redis hit warms it", "scenarios": ["tracked refill suppresses local until a Redis hit warms it"], "generated": [{"profile": "layers", "witness": "tracked-refill-needs-remote-validation"}, {"profile": "layers", "witness": "validated-tracked-hit-warms-local"}, {"profile": "recovery-read", "witness": "tracked-source-requires-validation-before-local-reuse"}], "vectors": [], "contracts": ["C31"], "quintReplays": ["recovery-read/trackedSourceNeedsRemoteValidationBeforeLocalReuseTest", "layers/trackedSourceWaitsForValidatedReadToWarmLocalTest", "layers/trackedRefillIsReadAgainBeforeLocalReuseTest"], "models": [ {"ref": "formal/dialcache-core.qnt:trackedRemoteFallbackSuppressesLocalPublication", "scope": "Tracked source after semantic remote miss cannot publish to local storage."}, + {"ref": "formal/dialcache-layers-conformance.qnt:trackedRefillIsReadAgainBeforeLocalReuseTest", "scope": "A second public caller after a tracked refill returns the stored value only after a fresh remote read; the completed history observes the read count at that decision."}, {"ref": "formal/dialcache-layers-conformance.qnt:trackedSourceWaitsForValidatedReadToWarmLocalTest", "scope": "Tracked source leaves local empty; a later validated remote hit warms it and the next local hit avoids remote."}, {"ref": "formal/dialcache-recovery-read-conformance.qnt:trackedSourceNeedsRemoteValidationBeforeLocalReuseTest", "scope": "Tracked source success memoizes in its request but requires remote read/decode before another scope can warm local; the subsequent transient caller reuses local."}], "definitions": [ + {"ref": "formal/cache-rules.qnt:sourceMayWarmLocal", "scope": "The core verifier and the portable layered source share this publication authority. The core property independently requires zero local writes after a tracked semantic miss."}, {"ref": "formal/kernel/remote-io.qnt:settleLoader", "scope": "Tracked source success memoizes in its request but requires remote read/decode before another scope can warm local; the subsequent transient caller reuses local."}]}, {"id": "C32.grouped-invalidation", "rule": "one invalidation fences all tracked operation variants only for its entity", "scenarios": ["one invalidation fences all tracked operation variants only for its entity"], "generated": [{"profile": "layers", "witness": "invalidation-fences-both-operations"}], "vectors": [], "contracts": ["C32"], "quintReplays": ["layers/invalidationGroupsOperationsButPreservesAcquiredCachesTest", "core/invalidationRefillsFromCurrentSourceTest"], "models": [ {"ref": "formal/dialcache-layers-conformance.qnt:invalidationGroupsOperationsButPreservesAcquiredCachesTest", "scope": "one invalidation fences all tracked operation variants only for its entity. The deterministic regression checks the public result and subsequent reuse for this bounded fixture."}, @@ -374,8 +381,9 @@ {"ref": "formal/dialcache-tracked-invalidation.qnt:refillAtFenceEqualitySkipsWriteTest", "scope": "observed future fence suppresses serialization and refill"}, {"ref": "formal/dialcache-effects-conformance.qnt:absentReplyWithFutureFenceBlocksRefillTest", "scope": "An observed future fence carried by a normalized adapter reply suppresses serialization and refill of the accepted source; the following read finds no stored value. Fence equality and invalidation-issued fences keep their tracked-invalidation regressions."}], "definitions": [ {"ref": "formal/kernel/remote-writes.qnt:settleLoader", "scope": "A successful source settled at or behind its observed fence records its value and holds no dump (serving::refills, the first fence check); the callers complete at once."}]}, - {"id": "C34.second-fence", "rule": "tracked refill rechecks clock after serialization", "scenarios": ["tracked refill rechecks clock after serialization"], "generated": [{"profile": "effects", "witness": "dump-rechecks-fence-after-rollback"}], "vectors": [], "contracts": ["C34"], "models": [ - {"ref": "formal/dialcache-effects-conformance.qnt:rollbackDuringDumpRechecksObservedFenceTest", "scope": "Rollback during serialization makes the second observed-fence check skip native write while preserving source result."}]}, + {"id": "C34.second-fence", "rule": "tracked refill rechecks clock after serialization", "scenarios": ["tracked refill rechecks clock after serialization"], "generated": [{"profile": "effects", "witness": "dump-rechecks-fence-after-rollback"}, {"profile": "dark-layers", "witness": "held-dark-fill-rechecks-fence-after-rollback"}], "vectors": [], "contracts": ["C34"], "models": [ + {"ref": "formal/dialcache-effects-conformance.qnt:rollbackDuringDumpRechecksObservedFenceTest", "scope": "Rollback during serialization makes the second observed-fence check skip native write while preserving source result."}, + {"ref": "formal/dialcache-dark-layers-conformance.qnt:fillFenceIsRejudgedAfterAWallRollbackTest", "scope": "A wall rollback after serialization starts prevents a held dark write from dispatching below its captured fence."}], "quintReplays": ["dark-layers/fillFenceIsRejudgedAfterAWallRollbackTest"], "definitions": [{"ref": "formal/kernel/shadow.qnt:releaseDump", "scope": "A wall rollback after serialization starts prevents a held dark write from dispatching below its captured fence."}]}, {"id": "C34.write-timestamp", "rule": "admitted tracked refill timestamps after serialization", "scenarios": ["admitted tracked refill timestamps after serialization"], "generated": [{"profile": "effects", "witness": "write-stamp-after-serialization"}], "vectors": [], "contracts": ["C34"], "quintReplays": ["effects/writeStampAfterSerializationClearsInterveningFenceTest"], "models": [ {"ref": "formal/dialcache-effects-conformance.qnt:writeStampAfterSerializationClearsInterveningFenceTest", "scope": "Serialization is held across invalidation and a wall-clock advance; native write stamp must clear the intervening watermark, verified by a subsequent public cache hit."}], "definitions": [ {"ref": "formal/dialcache-effects-conformance.qnt:releaseDump", "scope": "Native dispatch captures current writer wall clock after serialization; later completion commits that captured timestamp."}, @@ -526,14 +534,16 @@ {"id": "C47.requires-hook", "rule": "shadow requires an outcome observer", "scenarios": ["shadow requires an outcome observer"], "generated": [{"profile": "shadow", "witness": "missing-hook-skips-job"}], "vectors": [], "contracts": ["C47"], "models": [ {"ref": "formal/dialcache-shadow-conformance.qnt:missingHookHasNoShadowEffects", "scope": "Without the outcome hook no dark cache/read/load/write machinery runs; caller still returns its source value."}, {"ref": "formal/dialcache-shadow-conformance.qnt:missingHookSkipsDarkJobTest", "scope": "Without the outcome hook no dark cache/read/load/write machinery runs; caller still returns its source value."}]}, - {"id": "C47.requires-capacity", "rule": "shadow global capacity drops another key instead of queueing", "scenarios": ["shadow global capacity drops another key instead of queueing"], "generated": [{"profile": "admission", "witness": "full-capacity-drop"}, {"profile": "shadow-layers", "witness": "mixed-shadow-capacity-retains-only-owned-source"}], "vectors": [], "contracts": ["C47"], "provenance": [{"path": "formal/SPEC.md", "section": "Recovery, shadow work, and invalidation races"}], "quintReplays": ["shadow-layers/servedAndDarkJobsShareCapacityButNotSourceOwnershipTest", "admission/fullInstanceDoesNotBlockOtherInstanceTest"], "models": [ + {"id": "C47.requires-capacity", "rule": "shadow global capacity drops another key instead of queueing", "scenarios": ["shadow global capacity drops another key instead of queueing"], "generated": [{"profile": "admission", "witness": "full-capacity-drop"}, {"profile": "shadow-layers", "witness": "mixed-shadow-capacity-retains-only-owned-source"}, {"profile": "dark-layers", "witness": "held-dark-capacity-is-instance-local"}], "vectors": [], "contracts": ["C47"], "provenance": [{"path": "formal/SPEC.md", "section": "Recovery, shadow work, and invalidation races"}], "quintReplays": ["shadow-layers/servedAndDarkJobsShareCapacityButNotSourceOwnershipTest", "admission/fullInstanceDoesNotBlockOtherInstanceTest", "dark-layers/fullInstanceDropsAnotherKeyWhileTheC0ReadIsHeldTest"], "models": [ {"ref": "formal/dialcache-admission-conformance.qnt:capacityIsPerInstance", "scope": "Per-instance capacity bounds admitted jobs; a full instance drops its next key while another instance can admit."}, {"ref": "formal/dialcache-admission-conformance.qnt:fullInstanceDoesNotBlockOtherInstanceTest", "scope": "Per-instance capacity bounds admitted jobs; a full instance drops its next key while another instance can admit."}, {"ref": "formal/dialcache-shadow-layers-conformance.qnt:deduplicatedJobKeepsIndependentCallerSourcesTest", "scope": "A dark job dropped for a live job of its identity leaves the caller's own source to complete with its value: a drop never queues and never touches the caller."}, - {"ref": "formal/dialcache-shadow-layers-conformance.qnt:servedAndDarkJobsShareCapacityButNotSourceOwnershipTest", "scope": "A served-hit job and dark job jointly fill two per-instance slots, so another key is dropped while its independent caller source still runs. At60000ms the dark-source wait releases its slot, but a timed-out served raw source still consumes the other. A distinct-key probe is dropped until that raw source settles, after which another probe admits. Late settlements do not replace served hits or fill expired jobs."}], "definitions": [ + {"ref": "formal/dialcache-shadow-layers-conformance.qnt:servedAndDarkJobsShareCapacityButNotSourceOwnershipTest", "scope": "A served-hit job and dark job jointly fill two per-instance slots, so another key is dropped while its independent caller source still runs. At60000ms the dark-source wait releases its slot, but a timed-out served raw source still consumes the other. A distinct-key probe is dropped until that raw source settles, after which another probe admits. Late settlements do not replace served hits or fill expired jobs."}, + {"ref": "formal/dialcache-dark-layers-conformance.qnt:fullInstanceDropsAnotherKeyWhileTheC0ReadIsHeldTest", "scope": "A held C0 read keeps its instance slot occupied: another key is dropped there while the same key on another instance is admitted."}], "definitions": [ {"ref": "formal/kernel/shadow.qnt:admitJobs", "scope": "A served-hit job and dark job jointly fill two per-instance slots, so another key is dropped while its independent caller source still runs. At60000ms the dark-source wait releases its slot, but a timed-out served raw source still consumes the other. A distinct-key probe is dropped until that raw source settles, after which another probe admits. Late settlements do not replace served hits or fill expired jobs."}, {"ref": "formal/kernel/shadow.qnt:expireJob", "scope": "A served-hit job and dark job jointly fill two per-instance slots, so another key is dropped while its independent caller source still runs. At60000ms the dark-source wait releases its slot, but a timed-out served raw source still consumes the other. A distinct-key probe is dropped until that raw source settles, after which another probe admits. Late settlements do not replace served hits or fill expired jobs."}, - {"ref": "formal/kernel/remote-writes.qnt:settle", "scope": "A served-hit job and dark job jointly fill two per-instance slots, so another key is dropped while its independent caller source still runs. At60000ms the dark-source wait releases its slot, but a timed-out served raw source still consumes the other. A distinct-key probe is dropped until that raw source settles, after which another probe admits. Late settlements do not replace served hits or fill expired jobs."}]}, + {"ref": "formal/kernel/remote-writes.qnt:settle", "scope": "A served-hit job and dark job jointly fill two per-instance slots, so another key is dropped while its independent caller source still runs. At60000ms the dark-source wait releases its slot, but a timed-out served raw source still consumes the other. A distinct-key probe is dropped until that raw source settles, after which another probe admits. Late settlements do not replace served hits or fill expired jobs."}, + {"ref": "formal/kernel/shadow.qnt:beginDark", "scope": "A held C0 read keeps its instance slot occupied: another key is dropped there while the same key on another instance is admitted."}]}, {"id": "C47.requires-cohort", "rule": "shadow cohort excludes equality and admits just above its exact sample", "scenarios": ["shadow cohort excludes equality and admits just above its exact sample"], "generated": [{"profile": "shadow-layers", "witness": "shadow-cohort-below-does-not-admit"}, {"profile": "shadow-layers", "witness": "shadow-cohort-equality-does-not-admit"}, {"profile": "shadow-layers", "witness": "shadow-cohort-above-admits"}], "vectors": [], "contracts": ["C47"], "quintReplays": ["shadow-layers/shadowCohortBelowDoesNotAdmitTest", "shadow-layers/shadowCohortEqualityDoesNotAdmitTest", "shadow-layers/shadowCohortAboveAdmitsTest"], "models": [ {"ref": "formal/dialcache-shadow-layers-conformance.qnt:shadowCohortBelowDoesNotAdmitTest", "scope": "For the tracked key{urn:id:0}#ShadowLayers and independent shadow FNV numerator3203834406, thresholds exactly one uint32unit below/equal/above the sample produce no-read/no-job below/equal and an actual C0 read/fill serialization above. Caller success remains independent of diagnostic admission."}, {"ref": "formal/dialcache-shadow-layers-conformance.qnt:shadowCohortEqualityDoesNotAdmitTest", "scope": "For the tracked key{urn:id:0}#ShadowLayers and independent shadow FNV numerator3203834406, thresholds exactly one uint32unit below/equal/above the sample produce no-read/no-job below/equal and an actual C0 read/fill serialization above. Caller success remains independent of diagnostic admission."}, @@ -566,11 +576,13 @@ {"ref": "formal/dialcache-shadow-conformance.qnt:sameDecodedValueWithDifferentBytesIsSupersededTest", "scope": "The common confirmation rule is checked on the dark generated profile and on served-hit fixed scenarios; this witness does not establish source detachment."}, {"ref": "formal/dialcache-shadow-layers-conformance.qnt:darkC0ReplacedBeforeConfirmationIsSupersededTest", "scope": "In the composed shadow-layers profile a dark job's fresh C0 (value 1) differs from its source (value 2) and the frame was replaced (value 2) before the confirmation read, so the second read finds other bytes and the job ends superseded: one decode, two reads, no dump, the caller completing with its source value. A dark job, not a served hit; no repair claim."}], "definitions": [ {"ref": "formal/kernel/shadow.qnt:confirmation", "scope": "The one confirmation judgment for both lifecycles: a failed read is confirmation_error, the C0 bytes still current a mismatch, other bytes a supersession."}]}, - {"id": "C51.confirmation-age", "rule": "shadow confirmation compares payload bytes after C0 freshness expires", "scenarios": ["shadow confirmation compares payload bytes after C0 freshness expires"], "generated": [{"profile": "shadow", "witness": "shadow-confirmation-past-freshness-preserves-payload-and-age"}], "vectors": [], "contracts": ["C51"], "quintReplays": ["shadow/confirmationPastFreshnessKeepsOriginalPayloadAndAgeTest"], "models": [ - {"ref": "formal/dialcache-shadow-conformance.qnt:confirmationPastFreshnessKeepsOriginalPayloadAndAgeTest", "scope": "C0=1 is acquired fresh and retained; source2 compares unequal and C1 is held. Advancing application time exactly60000ms crosses F while elapsed job time and physical retention stay unchanged. C1 matching bytes confirm mismatch with read count2, no repair, source caller2 preserved and age60000ms."}], "definitions": [ + {"id": "C51.confirmation-age", "rule": "shadow confirmation compares payload bytes after C0 freshness expires", "scenarios": ["shadow confirmation compares payload bytes after C0 freshness expires"], "generated": [{"profile": "shadow", "witness": "shadow-confirmation-past-freshness-preserves-payload-and-age"}, {"profile": "dark-layers", "witness": "held-dark-c0-age-at-verdict"}], "vectors": [], "contracts": ["C51"], "quintReplays": ["shadow/confirmationPastFreshnessKeepsOriginalPayloadAndAgeTest", "dark-layers/retainedC0AgeIsSampledAtTheVerdictTest"], "models": [ + {"ref": "formal/dialcache-shadow-conformance.qnt:confirmationPastFreshnessKeepsOriginalPayloadAndAgeTest", "scope": "C0=1 is acquired fresh and retained; source2 compares unequal and C1 is held. Advancing application time exactly60000ms crosses F while elapsed job time and physical retention stay unchanged. C1 matching bytes confirm mismatch with read count2, no repair, source caller2 preserved and age60000ms."}, + {"ref": "formal/dialcache-dark-layers-conformance.qnt:retainedC0AgeIsSampledAtTheVerdictTest", "scope": "A C0 acquired one millisecond inside freshness remains the comparison snapshot, and its confirmed mismatch reports the age at the later verdict."}], "definitions": [ {"ref": "formal/kernel/shadow.qnt:settleJobRead", "scope": "C1 confirmation compares valid unfenced payload bytes without applying C0 freshness or current wall-clock age; no wall-age filter runs on the confirmation branch."}, {"ref": "formal/kernel/shadow.qnt:settleJobLoad", "scope": "C0=1 is acquired fresh and retained; source2 compares unequal and C1 is held. Advancing application time exactly60000ms crosses F while elapsed job time and physical retention stay unchanged. C1 matching bytes confirm mismatch with read count2, no repair, source caller2 preserved and age60000ms."}, - {"ref": "formal/kernel/clock.qnt:shiftWall", "scope": "C0=1 is acquired fresh and retained; source2 compares unequal and C1 is held. Advancing application time exactly60000ms crosses F while elapsed job time and physical retention stay unchanged. C1 matching bytes confirm mismatch with read count2, no repair, source caller2 preserved and age60000ms."}]}, + {"ref": "formal/kernel/clock.qnt:shiftWall", "scope": "C0=1 is acquired fresh and retained; source2 compares unequal and C1 is held. Advancing application time exactly60000ms crosses F while elapsed job time and physical retention stay unchanged. C1 matching bytes confirm mismatch with read count2, no repair, source caller2 preserved and age60000ms."}, + {"ref": "formal/kernel/shadow.qnt:releaseRead", "scope": "A C0 acquired one millisecond inside freshness remains the comparison snapshot, and its confirmed mismatch reports the age at the later verdict."}]}, {"id": "C51.confirmation-rollback", "rule": "shadow confirmation preserves payload comparison across wall clock rollback", "scenarios": ["shadow confirmation preserves payload comparison across wall clock rollback", "shadow confirmation retains future dated payload for comparison"], "generated": [{"profile": "shadow", "witness": "shadow-confirmation-rollback-keeps-payload-and-reports-offset"}], "vectors": [], "contracts": ["C51"], "quintReplays": ["shadow/confirmationRollbackKeepsPayloadAndClampsAgeTest"], "models": [ {"ref": "formal/dialcache-shadow-conformance.qnt:confirmationRollbackKeepsPayloadAndClampsAgeTest", "scope": "After fresh C0/source inequality starts C1, application time rolls backward1000ms. The now-future C1 still confirms identical payload bytes, preserves source caller2, writes nothing and emits mismatch with age clamped0 and remote_shadow future offset1000ms."}], "definitions": [ {"ref": "formal/kernel/shadow.qnt:settleJobRead", "scope": "C1 confirmation compares valid unfenced payload bytes without applying C0 freshness or current wall-clock age; no wall-age filter runs on the confirmation branch."}, @@ -584,11 +596,13 @@ {"ref": "formal/dialcache-shadow-conformance.qnt:fillsRequireMissAndAcceptedSource", "scope": "Every held dump or write belongs to a job whose C0 found nothing: a present C0 is never repaired, whatever the comparison verdict."}, {"ref": "formal/dialcache-shadow-layers-conformance.qnt:darkPresentC0NeverBecomesLocalPublicationTest", "scope": "Fresh present C0=1 and caller source=2 produce a confirmed mismatch with one C1 read and zero Redis writes. Local publication of2 belongs only to the caller source; diagnostic work never repairs the present C0."}], "definitions": [ {"ref": "formal/kernel/shadow.qnt:settleJob", "scope": "Fresh present C0=1 and caller source=2 produce a confirmed mismatch with one C1 read and zero Redis writes. Local publication of2 belongs only to the caller source; diagnostic work never repairs the present C0."}]}, - {"id": "C52.undecodable-not-repaired", "rule": "dark present undecodable value is never repaired", "scenarios": ["dark present undecodable value is never repaired"], "generated": [{"profile": "shadow", "witness": "outcome:deserialization_error"}, {"profile": "shadow", "witness": "present-undecodable-c0-is-not-repaired"}], "vectors": [], "contracts": ["C52"], "provenance": [{"path": "formal/SPEC.md", "section": "Recovery, shadow work, and invalidation races"}, {"path": "src/dialcache.ts", "lines": [1101, 1126, 1147, 1175, 1206, 1220], "scope": "Caller/source ownership, detached decoding, conditional fill, byte confirmation."}], "models": [ - {"ref": "formal/dialcache-shadow-conformance.qnt:fillsRequireMissAndAcceptedSource", "scope": "Every held dump or write belongs to a job whose C0 found nothing: a present C0 whose decode fails ends deserialization_error with no fill."}]}, - {"id": "C53.fill-fenced", "rule": "ramped down shadow fill respects observed fence", "scenarios": ["ramped down shadow fill respects observed fence"], "generated": [{"profile": "shadow", "witness": "outcome:fill_fenced"}], "vectors": [], "contracts": ["C53"], "quintReplays": ["shadow/fenceRejudgedAtDumpReleaseAfterRollbackTest"], "models": [ + {"id": "C52.undecodable-not-repaired", "rule": "dark present undecodable value is never repaired", "scenarios": ["dark present undecodable value is never repaired"], "generated": [{"profile": "shadow", "witness": "outcome:deserialization_error"}, {"profile": "shadow", "witness": "present-undecodable-c0-is-not-repaired"}, {"profile": "dark-layers", "witness": "undecodable-held-dark-c0-is-not-repaired"}], "vectors": [], "contracts": ["C52"], "provenance": [{"path": "formal/SPEC.md", "section": "Recovery, shadow work, and invalidation races"}, {"path": "src/dialcache.ts", "lines": [1101, 1126, 1147, 1175, 1206, 1220], "scope": "Caller/source ownership, detached decoding, conditional fill, byte confirmation."}], "models": [ + {"ref": "formal/dialcache-shadow-conformance.qnt:fillsRequireMissAndAcceptedSource", "scope": "Every held dump or write belongs to a job whose C0 found nothing: a present C0 whose decode fails ends deserialization_error with no fill."}, + {"ref": "formal/dialcache-dark-layers-conformance.qnt:undecodableDarkC0IsNeverRepairedTest", "scope": "A held decode failure for a present dark C0 ends the job without serializing a replacement or changing the source result."}], "quintReplays": ["dark-layers/undecodableDarkC0IsNeverRepairedTest"], "definitions": [{"ref": "formal/kernel/shadow.qnt:releaseLoad", "scope": "A held decode failure for a present dark C0 ends the job without serializing a replacement or changing the source result."}]}, + {"id": "C53.fill-fenced", "rule": "ramped down shadow fill respects observed fence", "scenarios": ["ramped down shadow fill respects observed fence"], "generated": [{"profile": "shadow", "witness": "outcome:fill_fenced"}, {"profile": "dark-layers", "witness": "dark-fill-at-watermark-is-fenced"}], "vectors": [], "contracts": ["C53"], "quintReplays": ["shadow/fenceRejudgedAtDumpReleaseAfterRollbackTest", "dark-layers/fillAtTheWatermarkInstantIsFencedTest"], "models": [ {"ref": "formal/dialcache-shadow-conformance.qnt:fillsClearTheirFence", "scope": "A held write was stamped after the fence its job's C0 read observed: the fence recheck at dump release (remote_writes::dispatch) is judged one step later, so a fenced fill outcome has no write."}, - {"ref": "formal/dialcache-shadow-conformance.qnt:fenceRejudgedAtDumpReleaseAfterRollbackTest", "scope": "The fence a miss observed is judged again when the dump is released: a wall rollback below it between settlement and release ends the job fill_fenced with no write, the caller keeping its value."}]}, + {"ref": "formal/dialcache-shadow-conformance.qnt:fenceRejudgedAtDumpReleaseAfterRollbackTest", "scope": "The fence a miss observed is judged again when the dump is released: a wall rollback below it between settlement and release ends the job fill_fenced with no write, the caller keeping its value."}, + {"ref": "formal/dialcache-dark-layers-conformance.qnt:fillAtTheWatermarkInstantIsFencedTest", "scope": "A dark fill at the acquired watermark instant returns the source result without starting serialization."}], "definitions": [{"ref": "formal/kernel/shadow.qnt:settleHeld", "scope": "A dark fill at the acquired watermark instant returns the source result without starting serialization."}]}, {"id": "C53.confirmation-fails-open", "rule": "shadow confirmation failure does not repair cache", "scenarios": ["shadow confirmation failure does not repair cache"], "generated": [{"profile": "shadow", "witness": "outcome:confirmation_error"}], "vectors": [], "contracts": ["C53"], "models": [ {"ref": "formal/dialcache-shadow-conformance.qnt:fillsRequireMissAndAcceptedSource", "scope": "A confirmation read fails only for a job whose C0 was present, and every held fill belongs to a job whose C0 found nothing, so a confirmation failure never initiates a repair; caller preservation is separately replayed."}]}, {"id": "C54.deduplication", "rule": "shadow admission drops duplicate work", "scenarios": ["shadow admission drops duplicate work"], "generated": [{"profile": "admission", "witness": "duplicate-with-free-capacity"}, {"profile": "shadow-layers", "witness": "dark-job-dedup-preserves-independent-source-values"}], "vectors": [], "contracts": ["C54"], "quintReplays": ["shadow-layers/deduplicatedJobKeepsIndependentCallerSourcesTest", "admission/duplicateDropsBeforeCapacityIsFullTest"], "models": [ @@ -604,11 +618,12 @@ {"ref": "formal/kernel/shadow.qnt:admitJobs", "scope": "A served-hit job and dark job jointly fill two per-instance slots, so another key is dropped while its independent caller source still runs. At60000ms the dark-source wait releases its slot, but a timed-out served raw source still consumes the other. A distinct-key probe is dropped until that raw source settles, after which another probe admits. Late settlements do not replace served hits or fill expired jobs."}, {"ref": "formal/kernel/shadow.qnt:expireJob", "scope": "A served-hit job and dark job jointly fill two per-instance slots, so another key is dropped while its independent caller source still runs. At60000ms the dark-source wait releases its slot, but a timed-out served raw source still consumes the other. A distinct-key probe is dropped until that raw source settles, after which another probe admits. Late settlements do not replace served hits or fill expired jobs."}, {"ref": "formal/kernel/remote-writes.qnt:settle", "scope": "A served-hit job and dark job jointly fill two per-instance slots, so another key is dropped while its independent caller source still runs. At60000ms the dark-source wait releases its slot, but a timed-out served raw source still consumes the other. A distinct-key probe is dropped until that raw source settles, after which another probe admits. Late settlements do not replace served hits or fill expired jobs."}]}, - {"id": "C54.decode-ownership", "rule": "served shadow decode retains capacity after timeout until raw load settles", "scenarios": ["served shadow decode retains capacity after timeout until raw load settles"], "generated": [{"profile": "admission", "witness": "decode-timeout-keeps-slot"}, {"profile": "shadow", "witness": "late-shadow-decode-cannot-start-c1"}], "vectors": [], "contracts": ["C54"], "evidenceNotes": ["This witness proves late-effect suppression; existing admission decode-timeout-keeps-slot proves capacity ownership."], "provenance": [{"path": "formal/SPEC.md", "section": "Recovery, shadow work, and invalidation races"}, {"path": "src/dialcache.ts", "lines": [1101, 1126, 1147, 1175, 1206, 1220], "scope": "Caller/source ownership, detached decoding, conditional fill, byte confirmation."}], "quintReplays": ["admission/timeoutRetainsDecodeCapacityTest"], "models": [ + {"id": "C54.decode-ownership", "rule": "shadow decode retains capacity after timeout until raw load settles", "scenarios": ["served shadow decode retains capacity after timeout until raw load settles"], "generated": [{"profile": "dark-layers", "witness": "timed-out-dark-decode-retains-capacity-until-raw-release"}, {"profile": "admission", "witness": "decode-timeout-keeps-slot"}, {"profile": "shadow", "witness": "late-shadow-decode-cannot-start-c1"}], "vectors": [], "contracts": ["C54"], "evidenceNotes": ["A dark decode retains capacity across the whole-job timeout, starts no late confirmation, and permits admission after raw release.", "The shadow late-decode witness proves late-effect suppression; admission proves the served-job capacity boundary and dark-layers proves the dark-job boundary."], "provenance": [{"path": "formal/SPEC.md", "section": "Recovery, shadow work, and invalidation races"}, {"path": "src/dialcache.ts", "lines": [1101, 1126, 1147, 1175, 1206, 1220], "scope": "Caller/source ownership, detached decoding, conditional fill, byte confirmation."}], "quintReplays": ["dark-layers/timedOutDarkDecodeKeepsCapacityUntilRawReleaseTest", "admission/timeoutRetainsDecodeCapacityTest"], "models": [ + {"ref": "formal/dialcache-dark-layers-conformance.qnt:timedOutDarkDecodeKeepsCapacityUntilRawReleaseTest", "scope": "A dark decode retains capacity across the whole-job timeout, starts no late confirmation, and permits admission after raw release."}, {"ref": "formal/dialcache-admission-conformance.qnt:timeoutRetainsDecodeCapacityTest", "scope": "Job timeout keeps raw decode capacity live until load settlement; no later confirmation is started."}, {"ref": "formal/dialcache-shadow-conformance.qnt:lateDecodeCannotStartConfirmationTest", "scope": "This witness proves late-effect suppression; existing admission decode-timeout-keeps-slot proves capacity ownership."}]}, - {"id": "C54.read-ownership", "rule": "shadow confirmation read retains capacity after its separate read deadline", "scenarios": ["shadow confirmation read retains capacity after its separate read deadline"], "generated": [{"profile": "shadow", "witness": "late-c1-cannot-emit-second-verdict"}], "vectors": [], "contracts": ["C54"], "evidenceNotes": ["This witness proves late-effect suppression; existing admission confirmation-timeout-keeps-slot and fixed separate-read-deadline scenario cover owned capacity.", "C54.read-ownership is not linked to timeoutRetainsConfirmationCapacityTest: that model regression exhausts the job deadline, not the separate read deadline specified by this case."], "provenance": [{"path": "formal/SPEC.md", "section": "Recovery, shadow work, and invalidation races"}, {"path": "src/dialcache.ts", "lines": [1101, 1126, 1147, 1175, 1206, 1220], "scope": "Caller/source ownership, detached decoding, conditional fill, byte confirmation."}], "models": [ - {"ref": "formal/dialcache-shadow-conformance.qnt:lateConfirmationCannotEmitAnotherVerdictTest", "scope": "This witness proves late-effect suppression; existing admission confirmation-timeout-keeps-slot and fixed separate-read-deadline scenario cover owned capacity."}]}, + {"id": "C54.read-ownership", "rule": "shadow confirmation read retains capacity after its separate read deadline", "scenarios": ["shadow confirmation read retains capacity after its separate read deadline"], "generated": [{"profile": "shadow-read-deadlines", "witness": "job-then-read-timeout-has-one-verdict"}, {"profile": "shadow-read-deadlines", "witness": "c1-read-timeout-retains-raw-capacity"}, {"profile": "shadow", "witness": "late-c1-cannot-emit-second-verdict"}], "vectors": [], "contracts": ["C54"], "evidenceNotes": ["The shadow-read-deadlines profile checks separate C1 expiry, raw ownership and both deadline orderings through generated public histories replayed in both ports."], "provenance": [{"path": "formal/SPEC.md", "section": "Recovery, shadow work, and invalidation races"}, {"path": "src/dialcache.ts", "lines": [1101, 1126, 1147, 1175, 1206, 1220], "scope": "Caller/source ownership, detached decoding, conditional fill, byte confirmation."}], "models": [{"ref": "formal/dialcache-shadow-read-deadlines-conformance.qnt:jobDeadlineThenReadDeadlineReportsOneVerdictTest", "scope": "A 10 ms whole-job deadline reports timeout while a 20 ms read remains live; the later read deadline requests cancellation without a second verdict, and raw ownership persists until release."}, {"ref": "formal/dialcache-shadow-read-deadlines-conformance.qnt:c1ReadDeadlineKeepsRawCapacityTest", "scope": "A separately bounded C1 read reports confirmation_error and requests cancellation at 5 ms; its raw read still rejects a competing key until released. The caller keeps its value and a later key is admitted without any second verdict."}, + {"ref": "formal/dialcache-shadow-conformance.qnt:lateConfirmationCannotEmitAnotherVerdictTest", "scope": "This older witness isolates late-effect suppression after the job deadline; shadow-read-deadlines separately proves the read deadline and raw-capacity boundary."}], "quintReplays": ["shadow-read-deadlines/jobDeadlineThenReadDeadlineReportsOneVerdictTest", "shadow-read-deadlines/c1ReadDeadlineKeepsRawCapacityTest"], "definitions": [{"ref": "formal/kernel/shadow-reads.qnt:advance", "scope": "A 10 ms whole-job deadline reports timeout while a 20 ms read remains live; the later read deadline requests cancellation without a second verdict, and raw ownership persists until release."}, {"ref": "formal/kernel/shadow-reads.qnt:expireRead", "scope": "A separately bounded C1 read reports confirmation_error and requests cancellation at 5 ms; its raw read still rejects a competing key until released. The caller keeps its value and a later key is admitted without any second verdict."}, {"ref": "formal/kernel/shadow-reads.qnt:finishReadResult", "scope": "A separately bounded C1 read reports confirmation_error and requests cancellation at 5 ms; its raw read still rejects a competing key until released. The caller keeps its value and a later key is admitted without any second verdict."}, {"ref": "formal/kernel/shadow-reads.qnt:releaseRead", "scope": "A separately bounded C1 read reports confirmation_error and requests cancellation at 5 ms; its raw read still rejects a competing key until released. The caller keeps its value and a later key is admitted without any second verdict."}]}, {"id": "C54.comparison-budget", "rule": "Comparison work can exhaust the job deadline before confirmation", "scenarios": [], "generated": [{"profile": "shadow", "witness": "comparison-crosses-deadline:9"}, {"profile": "shadow", "witness": "comparison-crosses-deadline:10"}], "vectors": [], "contracts": ["C54"], "models": [ {"ref": "formal/dialcache-shadow-conformance.qnt:slowComparisonTimesOutBeforeConfirmationTest", "scope": "Successful or failing comparator work reaching the job budget produces timeout before any confirmation read; caller result remains unchanged."}, {"ref": "formal/dialcache-shadow-conformance.qnt:slowComparisonFailureStillTimesOutTest", "scope": "Successful or failing comparator work reaching the job budget produces timeout before any confirmation read; caller result remains unchanged."}]}, @@ -624,8 +639,8 @@ {"id": "C55.ignore-untracked-fence", "rule": "adapter untracked fence preserves only trustworthy miss metadata", "scenarios": ["adapter untracked fence preserves only trustworthy miss metadata"], "generated": [{"profile": "effects", "witness": "untracked-demotes-fenced-reply"}], "vectors": [], "contracts": ["C55"], "quintReplays": ["effects/untrackedFencedReplyRefillsAndIsReadableTest"], "models": [ {"ref": "formal/dialcache-effects-conformance.qnt:untrackedReplyCannotCarryFenceTest", "scope": "Untracked reply strips observed fence and normalizes reason, allowing serialization."}, {"ref": "formal/dialcache-effects-conformance.qnt:untrackedFencedReplyRefillsAndIsReadableTest", "scope": "An untracked watermark-fenced reply is normalized to an unclassified miss with zero fence; one source refill is subsequently readable without a second source."}]}, - {"id": "C56.cooperative-cancel", "rule": "read deadline requests cooperative cancellation once for the shared execution", "scenarios": ["read deadline requests cooperative cancellation once for the shared execution"], "generated": [{"profile": "effects", "witness": "read-timeout-starts-source"}], "vectors": [], "contracts": ["C56"], "models": [ - {"ref": "formal/dialcache-effects-conformance.qnt:readTimeoutStartsIndependentSourceDeadlineTest", "scope": "One shared read deadline requests one cancellation for two pending callers."}]}, + {"id": "C56.cooperative-cancel", "rule": "read deadline requests cooperative cancellation once for the shared execution", "scenarios": ["read deadline requests cooperative cancellation once for the shared execution"], "generated": [{"profile": "shadow-read-deadlines", "witness": "confirmation-read-budget-starts-at-dispatch"}, {"profile": "effects", "witness": "read-timeout-starts-source"}], "vectors": [], "contracts": ["C56"], "models": [{"ref": "formal/dialcache-shadow-read-deadlines-conformance.qnt:confirmationReadBudgetStartsAtItsOwnDispatchTest", "scope": "C1 starts after 4 ms of earlier work and receives its full 5 ms read budget. It remains live at overall 8 ms and requests exactly one cancellation with confirmation_error at 9 ms."}, + {"ref": "formal/dialcache-effects-conformance.qnt:readTimeoutStartsIndependentSourceDeadlineTest", "scope": "One shared read deadline requests one cancellation for two pending callers."}], "quintReplays": ["shadow-read-deadlines/confirmationReadBudgetStartsAtItsOwnDispatchTest"], "definitions": [{"ref": "formal/kernel/shadow-reads.qnt:registerRead", "scope": "C1 starts after 4 ms of earlier work and receives its full 5 ms read budget. It remains live at overall 8 ms and requests exactly one cancellation with confirmation_error at 9 ms."}, {"ref": "formal/kernel/shadow-reads.qnt:releaseLoad", "scope": "C1 starts after 4 ms of earlier work and receives its full 5 ms read budget. It remains live at overall 8 ms and requests exactly one cancellation with confirmation_error at 9 ms."}, {"ref": "formal/kernel/shadow-reads.qnt:expireRead", "scope": "C1 starts after 4 ms of earlier work and receives its full 5 ms read budget. It remains live at overall 8 ms and requests exactly one cancellation with confirmation_error at 9 ms."}]}, {"id": "C56.late-read", "rule": "late read fulfillment checks deadline before timer delivery", "scenarios": ["late read fulfillment checks deadline before timer delivery", "late read rejection checks deadline before timer delivery"], "generated": [{"profile": "effects", "witness": "read-late-settlement"}], "vectors": [], "contracts": ["C56"], "models": [ {"ref": "formal/dialcache-effects-conformance.qnt:lateReadCannotBecomeHitTest", "scope": "A reply observed after elapsed read budget cannot start fresh decode even before timer delivery."}, {"ref": "formal/dialcache-effects-conformance.qnt:lateReadRejectionUsesDeadlineCategoryTest", "scope": "late read fulfillment checks deadline before timer delivery The cited property covers its bounded modeled clause; exact values, adapter errors and unmodeled combinations retain their separate evidence."}]}, @@ -635,18 +650,20 @@ {"id": "C57.recovery-age", "rule": "recovery age is sampled after asynchronous decode and only on served outcome", "scenarios": ["recovery age is sampled after asynchronous decode and only on served outcome", "recovery miss emits no served value age", "recovery deserialization_error emits no served value age"], "generated": [{"profile": "recovery", "witness": "recovery-age-sampled-at-successful-decode"}], "vectors": [], "contracts": ["C57"], "evidenceNotes": ["Witness checks successful decode age against observed diagnostic; existing agesRequireRecovery invariant checks no nonserved age samples."], "provenance": [{"path": "formal/SPEC.md", "section": "Recovery, shadow work, and invalidation races"}, {"path": "src/internal/redis-cache.ts", "lines": [169, 198, 210, 224, 228], "scope": "Fresh/recovery acquisition and pre/post-decode age checks."}], "models": [ {"ref": "formal/dialcache-recovery-conformance.qnt:agesRequireRecovery", "scope": "Witness checks successful decode age against observed diagnostic; existing agesRequireRecovery invariant checks no nonserved age samples."}], "definitions": [ {"ref": "formal/dialcache-recovery-conformance.qnt:releaseLoad", "scope": "Successful recovery samples retained-frame age after decode; miss/error/fresh decode do not append recovery age."}]}, - {"id": "C57.shadow-age", "rule": "shadow mismatch samples original frame age at verdict", "scenarios": ["shadow mismatch samples original frame age at verdict", "shadow match samples original frame age at verdict"], "generated": [{"profile": "shadow", "witness": "age-at-verdict"}], "vectors": [], "contracts": ["C57"], "models": [ - {"ref": "formal/dialcache-shadow-conformance.qnt:confirmationKeepsOriginalAgeTest", "scope": "A mismatch records C0's age at verdict, not the replacement C1 timestamp."}]}, + {"id": "C57.shadow-age", "rule": "shadow mismatch samples original frame age at verdict", "scenarios": ["shadow mismatch samples original frame age at verdict", "shadow match samples original frame age at verdict"], "generated": [{"profile": "shadow", "witness": "age-at-verdict"}, {"profile": "dark-layers", "witness": "held-dark-c0-age-at-verdict"}], "vectors": [], "contracts": ["C57"], "models": [ + {"ref": "formal/dialcache-shadow-conformance.qnt:confirmationKeepsOriginalAgeTest", "scope": "A mismatch records C0's age at verdict, not the replacement C1 timestamp."}, + {"ref": "formal/dialcache-dark-layers-conformance.qnt:retainedC0AgeIsSampledAtTheVerdictTest", "scope": "A C0 acquired one millisecond inside freshness remains the comparison snapshot, and its confirmed mismatch reports the age at the later verdict."}], "quintReplays": ["dark-layers/retainedC0AgeIsSampledAtTheVerdictTest"], "definitions": [{"ref": "formal/kernel/shadow.qnt:releaseRead", "scope": "A C0 acquired one millisecond inside freshness remains the comparison snapshot, and its confirmed mismatch reports the age at the later verdict."}]}, {"id": "C57.clamp-age", "rule": "shadow verdict age clamps application clock rollback to zero", "scenarios": ["shadow verdict age clamps application clock rollback to zero"], "generated": [{"profile": "shadow", "witness": "age-clamped-after-rollback"}, {"profile": "shadow", "witness": "shadow-confirmation-rollback-keeps-payload-and-reports-offset"}], "vectors": [], "contracts": ["C57"], "quintReplays": ["shadow/confirmationRollbackKeepsPayloadAndClampsAgeTest"], "models": [ {"ref": "formal/dialcache-shadow-conformance.qnt:ageClampsAfterWallRollbackTest", "scope": "A match age sample after wall rollback clamps to zero."}, {"ref": "formal/dialcache-shadow-conformance.qnt:confirmationRollbackKeepsPayloadAndClampsAgeTest", "scope": "A held C1 confirmation after1000ms wall rollback still confirms mismatch, and reported retained-C0 age is exactly0 rather than a negative or absolute duration."}], "definitions": [ {"ref": "formal/kernel/clock.qnt:shiftWall", "scope": "A held C1 confirmation after1000ms wall rollback still confirms mismatch, and reported retained-C0 age is exactly0 rather than a negative or absolute duration."}, {"ref": "formal/kernel/shadow.qnt:recordVerdict", "scope": "A held C1 confirmation after1000ms wall rollback still confirms mismatch, and reported retained-C0 age is exactly0 rather than a negative or absolute duration."}]}, - {"id": "C57.future-offset", "rule": "serving future offset uses the observing layer and positive seconds", "scenarios": ["serving future offset uses the observing layer and positive seconds", "dark future offset uses the observing layer and positive seconds", "confirmation future offset uses the observing layer and positive seconds"], "generated": [{"profile": "effects", "witness": "event:futureOffset"}, {"profile": "effects", "witness": "future-offset-observing-layer-positive-seconds"}, {"profile": "shadow", "witness": "shadow-confirmation-rollback-keeps-payload-and-reports-offset"}, {"profile": "shadow", "witness": "future-dark-c0-reports-offset-and-fills-semantic-miss"}, {"profile": "shadow", "witness": "late-dark-read-reports-offset-without-reviving-job"}], "vectors": [], "contracts": ["C57"], "quintReplays": ["effects/futureFrameReportsPositiveObservingOffsetTest", "shadow/confirmationRollbackKeepsPayloadAndClampsAgeTest", "shadow/futureDarkAcquisitionReportsOffsetAndFillsMissTest", "shadow/futureLateDarkReadKeepsTimeoutAndReportsOffsetTest"], "models": [ + {"id": "C57.future-offset", "rule": "serving future offset uses the observing layer and positive seconds", "scenarios": ["serving future offset uses the observing layer and positive seconds", "dark future offset uses the observing layer and positive seconds", "confirmation future offset uses the observing layer and positive seconds"], "generated": [{"profile": "effects", "witness": "event:futureOffset"}, {"profile": "effects", "witness": "future-offset-observing-layer-positive-seconds"}, {"profile": "shadow", "witness": "shadow-confirmation-rollback-keeps-payload-and-reports-offset"}, {"profile": "shadow", "witness": "future-dark-c0-reports-offset-and-fills-semantic-miss"}, {"profile": "shadow", "witness": "late-dark-read-reports-offset-without-reviving-job"}, {"profile": "dark-layers", "witness": "future-dark-c0-reports-offset-and-fills"}], "vectors": [], "contracts": ["C57"], "quintReplays": ["effects/futureFrameReportsPositiveObservingOffsetTest", "shadow/confirmationRollbackKeepsPayloadAndClampsAgeTest", "shadow/futureDarkAcquisitionReportsOffsetAndFillsMissTest", "shadow/futureLateDarkReadKeepsTimeoutAndReportsOffsetTest", "dark-layers/rollbackMakesTheSeededFrameFutureAndReportsItsOffsetTest"], "models": [ {"ref": "formal/dialcache-effects-conformance.qnt:futureFrameReportsPositiveObservingOffsetTest", "scope": "Serving future frame emits one remote futureOffset at +1000 modeled milliseconds, projected as +1 public second, skips serializer and preserves source error. Dark and confirmation observing locations remain outside this model."}, {"ref": "formal/dialcache-shadow-conformance.qnt:confirmationRollbackKeepsPayloadAndClampsAgeTest", "scope": "A successful C1 read of now-future bytes reports exactly one futureTimestampOffset event with namespace urn, useCase Behavior, keyType id, layer remote_shadow and offset1000ms. The frame remains a valid C1 payload-identity participant and confirms mismatch without repair."}, {"ref": "formal/dialcache-shadow-conformance.qnt:futureDarkAcquisitionReportsOffsetAndFillsMissTest", "scope": "A fresh seeded C0 becomes1000ms future after wall rollback. Dark acquisition emits exactly one futureTimestampOffset event labeled urn/Behavior/id/remote_shadow with1000ms; it treats the frame as semantic miss, never decodes it, and may fill from accepted source2."}, - {"ref": "formal/dialcache-shadow-conformance.qnt:futureLateDarkReadKeepsTimeoutAndReportsOffsetTest", "scope": "A raw dark read returns after the10ms job/caller deadline but inside its separate60s read budget. Its now-future frame emits remote_shadow offset990ms while caller timeout and single job timeout verdict remain; no decode, serialization or write can resume."}], "definitions": [ + {"ref": "formal/dialcache-shadow-conformance.qnt:futureLateDarkReadKeepsTimeoutAndReportsOffsetTest", "scope": "A raw dark read returns after the10ms job/caller deadline but inside its separate60s read budget. Its now-future frame emits remote_shadow offset990ms while caller timeout and single job timeout verdict remain; no decode, serialization or write can resume."}, + {"ref": "formal/dialcache-dark-layers-conformance.qnt:rollbackMakesTheSeededFrameFutureAndReportsItsOffsetTest", "scope": "A wall rollback makes a seeded C0 future-dated; the held dark read reports the remote_shadow offset, skips decode and permits a source fill."}], "definitions": [ {"ref": "formal/kernel/metrics.qnt:settleRead", "scope": "Only an otherwise readable future frame produces a positive offset, attributed to the remote layer in this profile. Other observing layers and unit conversion require their binding evidence."}, {"ref": "formal/kernel/metrics.qnt:remote", "scope": "Only an otherwise readable future frame produces a positive offset, attributed to the remote layer in this profile. Other observing layers and unit conversion require their binding evidence."}, {"ref": "formal/kernel/shadow.qnt:releaseRead", "scope": "A successful C1 read of now-future bytes reports exactly one futureTimestampOffset event with namespace urn, useCase Behavior, keyType id, layer remote_shadow and offset1000ms. The frame remains a valid C1 payload-identity participant and confirms mismatch without repair."}]}, @@ -768,13 +785,14 @@ {"ref": "formal/dialcache-envelope-vectors.qnt:disabledNewWritesStillReadLegacyCompressionTest", "scope": "The envelope reader ignores the model new-write policy input. Its native helper has no policy argument; combine primitive vectors with the actual compression:false recovery-read cached compressed history for the cache binding."}, {"ref": "formal/dialcache-recovery-read-conformance.qnt:compressedRecoveryMemoizesWithoutLocalPublicationTest", "scope": "Actual cache with new compression writes disabled still decodes the acquired compressed stale candidate and memoizes only in its request scope. This connects the unconditional envelope reader to resolved cache policy."}], "definitions": [ {"ref": "formal/dialcache-envelope-vectors.qnt:readEnvelope", "scope": "The envelope reader ignores the model new-write policy input. Its native helper has no policy argument; combine primitive vectors with the actual compression:false recovery-read cached compressed history for the cache binding."}]}, - {"id": "W04.strict-fence", "rule": "Reject a tracked frame equal to its watermark", "scenarios": [], "generated": [], "vectors": ["protocol/trackedDecodeVectors/equal timestamp is fenced"], "contracts": ["W04", "C33"], "generatedVectors": [{"artifact": "formal/quint-frame-vectors.json", "group": "trackedDecodeVectors", "name": "*"}], "models": [ + {"id": "W04.strict-fence", "rule": "Reject a tracked frame equal to its watermark", "scenarios": [], "generated": [{"profile": "dark-layers", "witness": "dark-frame-at-watermark-is-fenced"}], "vectors": ["protocol/trackedDecodeVectors/equal timestamp is fenced"], "contracts": ["W04", "C33"], "generatedVectors": [{"artifact": "formal/quint-frame-vectors.json", "group": "trackedDecodeVectors", "name": "*"}], "models": [ {"ref": "formal/dialcache-redis-protocol.qnt:fenceCanPrecedeEncodingError", "scope": "Tracked supported positive frames at equality are fenced; zero timestamp cannot yield a hit."}, {"ref": "formal/dialcache-redis-protocol.qnt:trackedZeroTimestampIsNotHit", "scope": "Tracked supported positive frames at equality are fenced; zero timestamp cannot yield a hit."}, {"ref": "formal/dialcache-tracked-invalidation.qnt:servedSnapshotClearedObservedFence", "scope": "Every served tracked snapshot has timestamp strictly above its acquired watermark."}, {"ref": "formal/dialcache-frame-vectors.qnt:trackedHitsStrictlyClearValidFence", "scope": "Tracked hit requires positive timestamp strictly greater than an observed valid watermark. A distinguishing equality regression rejects before unsupported payload encoding."}, {"ref": "formal/dialcache-frame-vectors.qnt:trackedFencePrecedesEncodingErrorTest", "scope": "Tracked hit requires positive timestamp strictly greater than an observed valid watermark. A distinguishing equality regression rejects before unsupported payload encoding."}, - {"ref": "formal/dialcache-rule-checks.qnt:fenceBoundaryIsExclusive", "scope": "Finite symbolic timestamp/watermark domains independently characterize strict timestamp-greater-than-fence acceptance; wire numeric validation is separate."}]}, + {"ref": "formal/dialcache-rule-checks.qnt:fenceBoundaryIsExclusive", "scope": "Finite symbolic timestamp/watermark domains independently characterize strict timestamp-greater-than-fence acceptance; wire numeric validation is separate."}, + {"ref": "formal/dialcache-dark-layers-conformance.qnt:frameAtTheWatermarkIsFencedFromTheDarkReadTest", "scope": "A frame stamped exactly at the watermark is fenced from the dark read; after time advances, the source starts a fill instead of decoding the fenced frame."}], "quintReplays": ["dark-layers/frameAtTheWatermarkIsFencedFromTheDarkReadTest"], "definitions": [{"ref": "formal/kernel/shadow.qnt:releaseRead", "scope": "A frame stamped exactly at the watermark is fenced from the dark read; after time advances, the source starts a fill instead of decoding the fenced frame."}]}, {"id": "C38.retention", "contracts": ["C38", "W09"], "rule": "Preserve persistence and longer TTLs while meeting the required retention floor", "scenarios": [], "generated": [], "vectors": ["invalidation/absent marker gets the two hour floor", "invalidation/large buffer extends retention beyond the floor", "invalidation/existing longer TTL never shrinks", "invalidation/persistent valid marker stays persistent"], "generatedVectors": [{"artifact": "formal/quint-invalidation-vectors.json", "name": "Quint 000: zero buffer / absent"}, {"artifact": "formal/quint-invalidation-vectors.json", "name": "Quint 001: zero buffer / valid finite"}, {"artifact": "formal/quint-invalidation-vectors.json", "name": "Quint 003: zero buffer / valid longer TTL"}, {"artifact": "formal/quint-invalidation-vectors.json", "name": "Quint 004: zero buffer / valid persistent"}, {"artifact": "formal/quint-invalidation-vectors.json", "name": "Quint 024: retention above floor / absent"}, {"artifact": "formal/quint-invalidation-vectors.json", "name": "Quint 025: retention above floor / valid finite"}, {"artifact": "formal/quint-invalidation-vectors.json", "name": "Quint 027: retention above floor / valid longer TTL"}, {"artifact": "formal/quint-invalidation-vectors.json", "name": "Quint 028: retention above floor / valid persistent"}, {"artifact": "formal/quint-invalidation-vectors.json", "name": "Quint 036: maximum buffer / absent"}, {"artifact": "formal/quint-invalidation-vectors.json", "name": "Quint 037: maximum buffer / valid finite"}, {"artifact": "formal/quint-invalidation-vectors.json", "name": "Quint 039: maximum buffer / valid longer TTL"}, {"artifact": "formal/quint-invalidation-vectors.json", "name": "Quint 040: maximum buffer / valid persistent"}], "models": [ {"ref": "formal/dialcache-invalidation-transition.qnt:successfulRetentionMeetsBothFloors", "scope": "Finite results meet the two-hour floor and cutoff-relative value-retention horizon exactly, preserve longer string TTL and preserve string persistence."}, {"ref": "formal/dialcache-invalidation-transition.qnt:stringsKeepExistingRetention", "scope": "Finite results meet the two-hour floor and cutoff-relative value-retention horizon exactly, preserve longer string TTL and preserve string persistence."}, @@ -861,12 +879,15 @@ {"ref": "formal/dialcache-runtime-boundaries-conformance.qnt:invalidBooleanLeavesBypassAllCaching", "scope": "An invalid runtime requestLocal leaf bypasses every layer rather than disabling only request memoization.. The deterministic regression checks the public result and subsequent reuse for this bounded fixture."}]}, {"id": "C42.operation-denial", "rule": "An operation recovery denial replaces an instance allow predicate and preserves the original source failure.", "contracts": ["C42"], "scenarios": ["recovery deny overrides allow policy safely"], "generated": [{"profile": "recovery", "witness": "operation-denial-overrides-instance-allow"}], "vectors": [], "quintReplays": ["recovery/operationDenialReplacesInstanceAllowTest"], "models": [ {"ref": "formal/dialcache-recovery-conformance.qnt:operationDenialReplacesInstanceAllowTest", "scope": "An operation recovery denial replaces an instance allow predicate and preserves the original source failure. The cited property covers its bounded modeled clause; exact values, adapter errors and unmodeled combinations retain their separate evidence."}]}, - {"id": "C54.dump-ownership", "rule": "A timed-out shadow job retains its capacity while its serialization is unfinished; late serialization cannot initiate a write.", "contracts": ["C54"], "scenarios": ["shadow timeout retains held dump capacity and ignores late completion"], "generated": [{"profile": "shadow", "witness": "late-shadow-dump-cannot-dispatch-write"}], "vectors": [], "evidenceNotes": ["This witness proves late-effect suppression only. Fixed capacity probes must remain attached to this case."], "provenance": [{"path": "formal/SPEC.md", "section": "Recovery, shadow work, and invalidation races"}, {"path": "src/dialcache.ts", "lines": [1101, 1126, 1147, 1175, 1206, 1220], "scope": "Caller/source ownership, detached decoding, conditional fill, byte confirmation."}], "models": [ + {"id": "C54.dump-ownership", "rule": "A timed-out shadow job retains its capacity while its serialization is unfinished; late serialization cannot initiate a write.", "contracts": ["C54"], "scenarios": ["shadow timeout retains held dump capacity and ignores late completion"], "generated": [{"profile": "dark-layers", "witness": "timed-out-dark-dump-retains-capacity-until-raw-release"}, {"profile": "shadow", "witness": "late-shadow-dump-cannot-dispatch-write"}], "vectors": [], "evidenceNotes": ["Generated dark-layers history now requires a competing key to be dropped after the job timeout and admitted only after the raw dump settles; the late dump dispatches no write.", "The shadow late-dump witness proves late-effect suppression; dark-layers adds the required capacity rejection and readmission consequence."], "provenance": [{"path": "formal/SPEC.md", "section": "Recovery, shadow work, and invalidation races"}, {"path": "src/dialcache.ts", "lines": [1101, 1126, 1147, 1175, 1206, 1220], "scope": "Caller/source ownership, detached decoding, conditional fill, byte confirmation."}], "quintReplays": ["dark-layers/timedOutDarkDumpKeepsCapacityUntilRawReleaseTest"], "models": [ + {"ref": "formal/dialcache-dark-layers-conformance.qnt:timedOutDarkDumpKeepsCapacityUntilRawReleaseTest", "scope": "Generated dark-layers history now requires a competing key to be dropped after the job timeout and admitted only after the raw dump settles; the late dump dispatches no write."}, {"ref": "formal/dialcache-shadow-conformance.qnt:lateFillSerializationCannotDispatchWriteTest", "scope": "This witness proves late-effect suppression only. Fixed capacity probes must remain attached to this case."}]}, - {"id": "C54.write-ownership", "rule": "A timed-out shadow job retains its capacity while a dispatched write is unfinished; late completion cannot produce a second outcome.", "contracts": ["C54"], "scenarios": ["shadow timeout retains held write capacity and ignores late completion"], "generated": [{"profile": "shadow", "witness": "late-shadow-write-cannot-change-caller-or-verdict"}], "vectors": [], "evidenceNotes": ["This witness proves post-timeout completion does not alter result/verdict; fixed capacity probe proves retained slot."], "provenance": [{"path": "formal/SPEC.md", "section": "Recovery, shadow work, and invalidation races"}, {"path": "src/dialcache.ts", "lines": [1101, 1126, 1147, 1175, 1206, 1220], "scope": "Caller/source ownership, detached decoding, conditional fill, byte confirmation."}], "models": [ + {"id": "C54.write-ownership", "rule": "A timed-out shadow job retains its capacity while a dispatched write is unfinished; late completion cannot produce a second outcome.", "contracts": ["C54"], "scenarios": ["shadow timeout retains held write capacity and ignores late completion"], "generated": [{"profile": "dark-layers", "witness": "timed-out-dark-write-retains-capacity-until-raw-release"}, {"profile": "shadow", "witness": "late-shadow-write-cannot-change-caller-or-verdict"}], "vectors": [], "evidenceNotes": ["Generated dark-layers history now requires a competing key to be dropped after the job timeout and admitted only after the dispatched raw write settles, with no second verdict.", "The shadow late-write witness proves result/verdict preservation; dark-layers adds the required capacity rejection and readmission consequence."], "provenance": [{"path": "formal/SPEC.md", "section": "Recovery, shadow work, and invalidation races"}, {"path": "src/dialcache.ts", "lines": [1101, 1126, 1147, 1175, 1206, 1220], "scope": "Caller/source ownership, detached decoding, conditional fill, byte confirmation."}], "quintReplays": ["dark-layers/timedOutDarkWriteKeepsCapacityUntilRawReleaseTest"], "models": [ + {"ref": "formal/dialcache-dark-layers-conformance.qnt:timedOutDarkWriteKeepsCapacityUntilRawReleaseTest", "scope": "Generated dark-layers history now requires a competing key to be dropped after the job timeout and admitted only after the dispatched raw write settles, with no second verdict."}, {"ref": "formal/dialcache-shadow-conformance.qnt:lateFillWriteCannotEmitAnotherVerdictTest", "scope": "This witness proves post-timeout completion does not alter result/verdict; fixed capacity probe proves retained slot."}]}, - {"id": "C54.dark-read-ownership", "rule": "A timed-out dark read retains capacity until the raw read settles, and its late result cannot initiate decoding or publication.", "contracts": ["C54"], "scenarios": ["shadow read timeout retains raw read capacity"], "generated": [{"profile": "shadow", "witness": "late-c0-cannot-start-new-shadow-work"}], "vectors": [], "evidenceNotes": ["This witness proves late-effect suppression only. Capacity retention is a separate fixed-scenario observation for the dark read."], "provenance": [{"path": "formal/SPEC.md", "section": "Recovery, shadow work, and invalidation races"}, {"path": "src/dialcache.ts", "lines": [1101, 1126, 1147, 1175, 1206, 1220], "scope": "Caller/source ownership, detached decoding, conditional fill, byte confirmation."}], "models": [ - {"ref": "formal/dialcache-shadow-conformance.qnt:lateDarkReadCannotStartDecodeOrFillTest", "scope": "This witness proves late-effect suppression only. Capacity retention is a separate fixed-scenario observation for the dark read."}]}, + {"id": "C54.dark-read-ownership", "rule": "A timed-out dark read retains capacity until the raw read settles, and its late result cannot initiate decoding or publication.", "contracts": ["C54"], "scenarios": ["shadow read timeout retains raw read capacity"], "generated": [{"profile": "shadow-read-deadlines", "witness": "timely-shadow-read-cancels-deadline-and-fills"}, {"profile": "shadow-read-deadlines", "witness": "c0-read-timeout-retains-raw-capacity"}, {"profile": "dark-layers", "witness": "timed-out-dark-read-retains-capacity-until-raw-release"}, {"profile": "shadow", "witness": "late-c0-cannot-start-new-shadow-work"}], "vectors": [], "evidenceNotes": ["Generated dark-layers history requires capacity retention after the whole-job timeout while C0 is held, followed by admission after raw release. The shadow-read-deadlines profile independently checks the separate read-deadline capacity boundary.", "The shadow late-C0 witness proves late-effect suppression; dark-layers adds capacity consequences at the whole-job deadline. Generated shadow-read-deadlines histories now supply the separate Redis-read deadline boundary."], "provenance": [{"path": "formal/SPEC.md", "section": "Recovery, shadow work, and invalidation races"}, {"path": "src/dialcache.ts", "lines": [1101, 1126, 1147, 1175, 1206, 1220], "scope": "Caller/source ownership, detached decoding, conditional fill, byte confirmation."}], "quintReplays": ["shadow-read-deadlines/readBeforeDeadlineCanFillAndCancelsItsTimerTest", "shadow-read-deadlines/c0ReadDeadlineKeepsRawCapacityTest", "dark-layers/timedOutDarkReadKeepsCapacityUntilRawReleaseTest"], "models": [{"ref": "formal/dialcache-shadow-read-deadlines-conformance.qnt:readBeforeDeadlineCanFillAndCancelsItsTimerTest", "scope": "A C0 released at 4 ms inside a 5 ms read budget can fill from the completed caller source; subsequent time advancement emits no read cancellation or timeout verdict."}, {"ref": "formal/dialcache-shadow-read-deadlines-conformance.qnt:c0ReadDeadlineKeepsRawCapacityTest", "scope": "A separately bounded C0 read reports redis_error at 5 ms while the caller keeps its source value; another key is dropped until the raw adapter operation settles, after which a new job is admitted without late decode or fill."}, + {"ref": "formal/dialcache-dark-layers-conformance.qnt:timedOutDarkReadKeepsCapacityUntilRawReleaseTest", "scope": "Generated dark-layers history requires capacity retention after the whole-job timeout while C0 is held, followed by admission after raw release. The shadow-read-deadlines profile independently checks the separate read-deadline capacity boundary."}, + {"ref": "formal/dialcache-shadow-conformance.qnt:lateDarkReadCannotStartDecodeOrFillTest", "scope": "This witness proves late-effect suppression only. Capacity retention is a separate fixed-scenario observation for the dark read."}], "definitions": [{"ref": "formal/kernel/shadow-reads.qnt:registerRead", "scope": "A C0 released at 4 ms inside a 5 ms read budget can fill from the completed caller source; subsequent time advancement emits no read cancellation or timeout verdict."}, {"ref": "formal/kernel/shadow-reads.qnt:expireRead", "scope": "A separately bounded C0 read reports redis_error at 5 ms while the caller keeps its source value; another key is dropped until the raw adapter operation settles, after which a new job is admitted without late decode or fill."}, {"ref": "formal/kernel/shadow-reads.qnt:finishReadResult", "scope": "A separately bounded C0 read reports redis_error at 5 ms while the caller keeps its source value; another key is dropped until the raw adapter operation settles, after which a new job is admitted without late decode or fill."}, {"ref": "formal/kernel/shadow-reads.qnt:releaseRead", "scope": "A separately bounded C0 read reports redis_error at 5 ms while the caller keeps its source value; another key is dropped until the raw adapter operation settles, after which a new job is admitted without late decode or fill."}]}, {"id": "C06.absent-distinct-from-text", "rule": "An absent value and the literal string undefined remain distinct cacheable results.", "contracts": ["C06"], "scenarios": ["literal undefined string is distinct from absent result"], "generated": [{"profile": "runtime-boundaries", "witness": "absent-distinct-from-text:request"}, {"profile": "runtime-boundaries", "witness": "absent-distinct-from-text:local"}, {"profile": "runtime-boundaries", "witness": "absent-distinct-from-text:remote"}], "vectors": [], "quintReplays": ["runtime-boundaries/requestAbsenceAndLiteralTextStayDistinctTest", "runtime-boundaries/localAbsenceAndLiteralTextStayDistinctTest", "runtime-boundaries/remoteAbsenceAndLiteralTextStayDistinctTest"], "models": [ {"ref": "formal/dialcache-runtime-boundaries-conformance.qnt:requestAbsenceAndLiteralTextStayDistinctTest", "scope": "Both actual absence and literal string undefined settle as independent same-key source results and are distinguished by subsequent request/local/remote reads."}, {"ref": "formal/dialcache-runtime-boundaries-conformance.qnt:localAbsenceAndLiteralTextStayDistinctTest", "scope": "Both actual absence and literal string undefined settle as independent same-key source results and are distinguished by subsequent request/local/remote reads."}, @@ -901,12 +922,14 @@ {"ref": "formal/dialcache-shadow-layers-conformance.qnt:servedPropagatedTimeoutPreservesHitTest", "scope": "The detached served-hit source runs with caching disabled and rejects its controlled nested timeout at time0. The caller keeps its already served C0=1 and the job reports source_error, with no write or own job deadline event."}], "definitions": [ {"ref": "formal/kernel/remote-writes.qnt:settle", "scope": "The detached served-hit source runs with caching disabled and rejects its controlled nested timeout at time0. The caller keeps its already served C0=1 and the job reports source_error, with no write or own job deadline event."}, {"ref": "formal/kernel/shadow.qnt:settleJob", "scope": "The detached served-hit source runs with caching disabled and rejects its controlled nested timeout at time0. The caller keeps its already served C0=1 and the job reports source_error, with no write or own job deadline event."}]}, - {"id": "C49.local-publication", "rule": "Dark work never publishes C0 into process-local storage; eligible local publication uses only the caller source result.", "contracts": ["C49"], "scenarios": ["dark shadow publishes only caller source into local cache"], "generated": [{"profile": "shadow-layers", "witness": "dark-present-c0-never-publishes-local"}, {"profile": "shadow-layers", "witness": "dark-local-source-publication-stops-later-shadow"}], "vectors": [], "quintReplays": ["shadow-layers/darkSourcePublishesLocalBeforeShadowWriteTest", "shadow-layers/darkPresentC0NeverBecomesLocalPublicationTest"], "models": [ + {"id": "C49.local-publication", "rule": "Dark work never publishes C0 into process-local storage; eligible local publication uses only the caller source result.", "contracts": ["C49"], "scenarios": ["dark shadow publishes only caller source into local cache"], "generated": [{"profile": "shadow-layers", "witness": "dark-present-c0-never-publishes-local"}, {"profile": "shadow-layers", "witness": "dark-local-source-publication-stops-later-shadow"}, {"profile": "dark-layers", "witness": "dark-source-local-publication-stops-job"}], "vectors": [], "quintReplays": ["shadow-layers/darkSourcePublishesLocalBeforeShadowWriteTest", "shadow-layers/darkPresentC0NeverBecomesLocalPublicationTest", "dark-layers/localPublicationStopsLaterDarkWorkTest"], "models": [ {"ref": "formal/dialcache-shadow-layers-conformance.qnt:darkPresentC0NeverBecomesLocalPublicationTest", "scope": "With request memo disabled, a present C0=1 cannot satisfy a later dark caller before source settlement; source=2 alone becomes the subsequent local hit. A separate absent-C0 history proves source publication precedes the held shadow fill write and a local hit stops new shadow admission."}, - {"ref": "formal/dialcache-shadow-layers-conformance.qnt:darkSourcePublishesLocalBeforeShadowWriteTest", "scope": "With request memo disabled, a present C0=1 cannot satisfy a later dark caller before source settlement; source=2 alone becomes the subsequent local hit. A separate absent-C0 history proves source publication precedes the held shadow fill write and a local hit stops new shadow admission."}], "definitions": [ + {"ref": "formal/dialcache-shadow-layers-conformance.qnt:darkSourcePublishesLocalBeforeShadowWriteTest", "scope": "With request memo disabled, a present C0=1 cannot satisfy a later dark caller before source settlement; source=2 alone becomes the subsequent local hit. A separate absent-C0 history proves source publication precedes the held shadow fill write and a local hit stops new shadow admission."}, + {"ref": "formal/dialcache-dark-layers-conformance.qnt:localPublicationStopsLaterDarkWorkTest", "scope": "A dark caller publishes its successful source locally before the held fill; a subsequent local hit starts neither another source nor dark read."}], "definitions": [ {"ref": "formal/kernel/serving.qnt:publish", "scope": "With request memo disabled, a present C0=1 cannot satisfy a later dark caller before source settlement; source=2 alone becomes the subsequent local hit. A separate absent-C0 history proves source publication precedes the held shadow fill write and a local hit stops new shadow admission."}, {"ref": "formal/kernel/shadow.qnt:begin", "scope": "With request memo disabled, a present C0=1 cannot satisfy a later dark caller before source settlement; source=2 alone becomes the subsequent local hit. A separate absent-C0 history proves source publication precedes the held shadow fill write and a local hit stops new shadow admission."}, - {"ref": "formal/kernel/shadow.qnt:settleJob", "scope": "With request memo disabled, a present C0=1 cannot satisfy a later dark caller before source settlement; source=2 alone becomes the subsequent local hit. A separate absent-C0 history proves source publication precedes the held shadow fill write and a local hit stops new shadow admission."}]}, + {"ref": "formal/kernel/shadow.qnt:settleJob", "scope": "With request memo disabled, a present C0=1 cannot satisfy a later dark caller before source settlement; source=2 alone becomes the subsequent local hit. A separate absent-C0 history proves source publication precedes the held shadow fill write and a local hit stops new shadow admission."}, + {"ref": "formal/kernel/shadow.qnt:settleHeld", "scope": "A dark caller publishes its successful source locally before the held fill; a subsequent local hit starts neither another source nor dark read."}]}, {"id": "C49.request-publication", "rule": "Dark work never publishes C0 into request memoization; memoization uses only the caller source result.", "contracts": ["C49"], "scenarios": ["dark shadow publishes only caller source into request cache"], "generated": [{"profile": "shadow-layers", "witness": "dark-present-c0-never-publishes-request"}, {"profile": "shadow-layers", "witness": "dark-request-source-publication-is-scope-local"}], "vectors": [], "quintReplays": ["shadow-layers/darkSourcePublishesRequestBeforeShadowWriteTest", "shadow-layers/darkPresentC0NeverBecomesRequestPublicationTest"], "models": [ {"ref": "formal/dialcache-shadow-layers-conformance.qnt:darkPresentC0NeverBecomesRequestPublicationTest", "scope": "With local disabled, present C0=1 cannot satisfy a same-scope caller before source settlement; source=2 alone becomes that scope's memo hit. A sibling scope starts an independent source. The absent-C0 history proves request publication precedes held shadow fill."}, {"ref": "formal/dialcache-shadow-layers-conformance.qnt:darkSourcePublishesRequestBeforeShadowWriteTest", "scope": "With local disabled, present C0=1 cannot satisfy a same-scope caller before source settlement; source=2 alone becomes that scope's memo hit. A sibling scope starts an independent source. The absent-C0 history proves request publication precedes held shadow fill."}], "definitions": [ @@ -931,15 +954,18 @@ {"ref": "formal/kernel/remote-io.qnt:complete", "scope": "With tracked stale bytes, a selected 100% shadow policy, installed outcome hook and available capacity, source failure recovers the retained value, including absence. Recovery and its same-request memo probe start no diagnostic source/read/job and publish no remote/local value; an independent request repeats source failure and recovery for the absence case."}]}, {"id": "C47.coalesced-hit-one-job", "rule": "Coalesced callers sharing one remote hit admit only one detached shadow source.", "contracts": ["C47"], "scenarios": ["coalesced remote hits admit only one shadow source"], "generated": [{"profile": "admission", "witness": "coalesced-hit-one-job"}], "vectors": [], "quintReplays": ["admission/coalescedHitAdmitsOneJobTest"], "models": [ {"ref": "formal/dialcache-admission-conformance.qnt:coalescedHitAdmitsOneJobTest", "scope": "Coalesced callers sharing one remote hit admit only one detached shadow source. The cited property covers its bounded modeled clause; exact values, adapter errors and unmodeled combinations retain their separate evidence."}]}, - {"id": "C52.future-miss-may-fill", "rule": "A future C0 is a semantic miss; after caller success a dark job may fill from that source subject to its observed fence.", "contracts": ["C52"], "scenarios": ["dark future C0 is a miss that permits source fill"], "generated": [{"profile": "shadow", "witness": "future-dark-c0-fills-without-decoding"}], "vectors": [], "provenance": [{"path": "formal/SPEC.md", "section": "Recovery, shadow work, and invalidation races"}, {"path": "src/dialcache.ts", "lines": [1101, 1126, 1147, 1175, 1206, 1220], "scope": "Caller/source ownership, detached decoding, conditional fill, byte confirmation."}], "models": [ - {"ref": "formal/dialcache-shadow-conformance.qnt:futureDarkAcquisitionReportsOffsetAndFillsMissTest", "scope": "A future C0 is a semantic miss; after caller success a dark job may fill from that source subject to its observed fence."}]}, - {"id": "C52.expired-miss-may-fill", "rule": "An expired C0 is a semantic miss; a dark job may fill from the accepted caller source.", "contracts": ["C52"], "scenarios": ["dark expired C0 is a miss that permits source fill"], "generated": [{"profile": "shadow-layers", "witness": "expired-c0-miss-fills-source-and-serves-probe"}], "vectors": [], "quintReplays": ["shadow-layers/expiredC0CanFillAndServeNewValueTest", "shadow/c0AtExactFreshnessBoundaryRefillsTest"], "models": [ + {"id": "C52.future-miss-may-fill", "rule": "A future C0 is a semantic miss; after caller success a dark job may fill from that source subject to its observed fence.", "contracts": ["C52"], "scenarios": ["dark future C0 is a miss that permits source fill"], "generated": [{"profile": "shadow", "witness": "future-dark-c0-fills-without-decoding"}, {"profile": "dark-layers", "witness": "future-dark-c0-reports-offset-and-fills"}], "vectors": [], "provenance": [{"path": "formal/SPEC.md", "section": "Recovery, shadow work, and invalidation races"}, {"path": "src/dialcache.ts", "lines": [1101, 1126, 1147, 1175, 1206, 1220], "scope": "Caller/source ownership, detached decoding, conditional fill, byte confirmation."}], "models": [ + {"ref": "formal/dialcache-shadow-conformance.qnt:futureDarkAcquisitionReportsOffsetAndFillsMissTest", "scope": "A future C0 is a semantic miss; after caller success a dark job may fill from that source subject to its observed fence."}, + {"ref": "formal/dialcache-dark-layers-conformance.qnt:rollbackMakesTheSeededFrameFutureAndReportsItsOffsetTest", "scope": "A wall rollback makes a seeded C0 future-dated; the held dark read reports the remote_shadow offset, skips decode and permits a source fill."}], "quintReplays": ["dark-layers/rollbackMakesTheSeededFrameFutureAndReportsItsOffsetTest"], "definitions": [{"ref": "formal/kernel/shadow.qnt:releaseRead", "scope": "A wall rollback makes a seeded C0 future-dated; the held dark read reports the remote_shadow offset, skips decode and permits a source fill."}]}, + {"id": "C52.expired-miss-may-fill", "rule": "An expired C0 is a semantic miss; a dark job may fill from the accepted caller source.", "contracts": ["C52"], "scenarios": ["dark expired C0 is a miss that permits source fill"], "generated": [{"profile": "shadow-layers", "witness": "expired-c0-miss-fills-source-and-serves-probe"}, {"profile": "dark-layers", "witness": "stale-visible-dark-c0-fills-unfenced"}], "vectors": [], "quintReplays": ["shadow-layers/expiredC0CanFillAndServeNewValueTest", "shadow/c0AtExactFreshnessBoundaryRefillsTest", "dark-layers/staleVisibleFrameDeclinedByTheDarkReadFillsUnfencedTest"], "models": [ {"ref": "formal/dialcache-shadow-layers-conformance.qnt:expiredC0CanFillAndServeNewValueTest", "scope": "C0=1 acquired exactly at freshness age60000ms is a semantic miss despite physical presence. Source=2 fills once without decoding old bytes; a later serving probe reads2 and starts no new source."}, {"ref": "formal/dialcache-shadow-conformance.qnt:c0AcquisitionsRespectFreshness", "scope": "At the step that releases a read, a surviving job acquired the frame exactly when it was visible and strictly younger than the freshness at that instant (nonnegative age strictly below the ceiling); later comparison and C1 confirmation do not reclassify the acquired C0."}, - {"ref": "formal/dialcache-shadow-conformance.qnt:c0AtExactFreshnessBoundaryRefillsTest", "scope": "A public C1 confirmation completes using retained bytes at age 60000 ms. A subsequent job reacquires the same stored bytes as C0 at exact freshness age 60000 ms, does not decode them, and fills once from the accepted caller source. This checks C0 reacquisition rather than changing the prior job's retained decision."}], "definitions": [ + {"ref": "formal/dialcache-shadow-conformance.qnt:c0AtExactFreshnessBoundaryRefillsTest", "scope": "A public C1 confirmation completes using retained bytes at age 60000 ms. A subsequent job reacquires the same stored bytes as C0 at exact freshness age 60000 ms, does not decode them, and fills once from the accepted caller source. This checks C0 reacquisition rather than changing the prior job's retained decision."}, + {"ref": "formal/dialcache-dark-layers-conformance.qnt:staleVisibleFrameDeclinedByTheDarkReadFillsUnfencedTest", "scope": "A stale but visible C0 is declined without retaining a fence; invalidation after that read does not stop the captured fill from dispatching and completing."}], "definitions": [ {"ref": "formal/kernel/remote-frames.qnt:observe", "scope": "C0=1 acquired exactly at freshness age60000ms is a semantic miss despite physical presence. Source=2 fills once without decoding old bytes; a later serving probe reads2 and starts no new source."}, {"ref": "formal/kernel/shadow.qnt:admitJobs", "scope": "C0=1 acquired exactly at freshness age60000ms is a semantic miss despite physical presence. Source=2 fills once without decoding old bytes; a later serving probe reads2 and starts no new source."}, - {"ref": "formal/kernel/shadow.qnt:settleJob", "scope": "C0=1 acquired exactly at freshness age60000ms is a semantic miss despite physical presence. Source=2 fills once without decoding old bytes; a later serving probe reads2 and starts no new source."}]}, + {"ref": "formal/kernel/shadow.qnt:settleJob", "scope": "C0=1 acquired exactly at freshness age60000ms is a semantic miss despite physical presence. Source=2 fills once without decoding old bytes; a later serving probe reads2 and starts no new source."}, + {"ref": "formal/kernel/shadow.qnt:releaseRead", "scope": "A stale but visible C0 is declined without retaining a fence; invalidation after that read does not stop the captured fill from dispatching and completing."}]}, {"id": "C53.acquired-miss-no-reread", "rule": "A dark job uses its acquired C0 miss and observed fence without rereading C0 after an external replacement.", "contracts": ["C53"], "scenarios": ["dark acquired miss survives later Redis replacement without a second C0 read"], "generated": [{"profile": "shadow-layers", "witness": "acquired-c0-miss-does-not-reread-before-fill"}], "vectors": [], "quintReplays": ["shadow-layers/acquiredMissDoesNotRereadBeforeFillTest"], "models": [ {"ref": "formal/dialcache-shadow-layers-conformance.qnt:acquiredMissDoesNotRereadBeforeFillTest", "scope": "An absent C0 is acquired, external storage is then seeded to1, and source=2 fills without rereading/reclassifying C0. Read count stays1 through write completion; a later serving probe observes2."}], "definitions": [ {"ref": "formal/kernel/shadow.qnt:admitJobs", "scope": "An absent C0 is acquired, external storage is then seeded to1, and source=2 fills without rereading/reclassifying C0. Read count stays1 through write completion; a later serving probe observes2."}, @@ -952,20 +978,23 @@ {"ref": "formal/dialcache-scope-conformance.qnt:independentRequestMemoUsesLastCompletionTest", "scope": "Disabling coalescing preserves request memoization; independent successful completions replace its value in settlement order.. The deterministic regression checks the public result and subsequent reuse for this bounded fixture."}, {"ref": "formal/dialcache-scope-conformance.qnt:memoFollowsLatestSuccessfulSettlement", "scope": "Disabling coalescing preserves request memoization; independent successful completions replace its value in settlement order.. The deterministic regression checks the public result and subsequent reuse for this bounded fixture."}, {"ref": "formal/dialcache-scope-conformance.qnt:rejectedIndependentSourceKeepsMemoizedValueTest", "scope": "A failed independent completion neither replaces nor clears the memoized value: the row keeps the last successful settlement and a later call in the same scope is served from it without a new source."}]}, - {"id": "C16.runtime-enables-coalescing", "rule": "Explicit runtime coalesce true replaces an operation default of false and enables eligible sharing.", "contracts": ["C16"], "scenarios": ["runtime coalescing override enables sharing over a disabled default"], "generated": [{"profile": "runtime-boundaries", "witness": "runtime-enables-sharing"}, {"profile": "runtime-boundaries", "witness": "inherited-false-request-last-writer"}], "vectors": [], "quintReplays": ["runtime-boundaries/runtimeTrueEnablesSharingOverFalseDefaultTest", "runtime-boundaries/inheritedFalseKeepsIndependentMemoizingSourcesTest"], "models": [ + {"id": "C16.runtime-enables-coalescing", "rule": "Omitted runtime coalesce preserves a configured false default; explicit true replaces that default and enables eligible sharing.", "contracts": ["C16"], "scenarios": ["runtime coalescing override enables sharing over a disabled default"], "generated": [{"profile": "runtime-boundaries", "witness": "runtime-enables-sharing"}, {"profile": "runtime-boundaries", "witness": "inherited-false-request-last-writer"}], "vectors": [], "quintReplays": ["runtime-boundaries/runtimeTrueEnablesSharingOverFalseDefaultTest", "runtime-boundaries/inheritedFalseKeepsIndependentMemoizingSourcesTest", "runtime-boundaries/inheritedFalseStartsBothSourcesBeforeSettlementTest"], "models": [ + {"ref": "formal/dialcache-runtime-boundaries-conformance.qnt:inheritedFalseStartsBothSourcesBeforeSettlementTest", "scope": "An omitted runtime sharing leaf preserves the configured false baseline: two request callers start independent sources before either settles."}, {"ref": "formal/dialcache-runtime-boundaries-conformance.qnt:runtimeTrueEnablesSharingOverFalseDefaultTest", "scope": "Explicit runtime coalesce true replaces an operation default of false and enables eligible sharing.. The deterministic regression checks the public result and subsequent reuse for this bounded fixture."}, - {"ref": "formal/dialcache-runtime-boundaries-conformance.qnt:inheritedFalseKeepsIndependentMemoizingSourcesTest", "scope": "Explicit runtime coalesce true replaces an operation default of false and enables eligible sharing.. The deterministic regression checks the public result and subsequent reuse for this bounded fixture."}, - {"ref": "formal/dialcache-runtime-boundaries-conformance.qnt:inheritedDisabledSharingNeverJoins", "scope": "Explicit runtime coalesce true replaces an operation default of false and enables eligible sharing.. The deterministic regression checks the public result and subsequent reuse for this bounded fixture."}]}, + {"ref": "formal/dialcache-runtime-boundaries-conformance.qnt:inheritedFalseKeepsIndependentMemoizingSourcesTest", "scope": "Omitted runtime coalesce inherits the configured false default; two independent completions retain their own results and memoize the last successful settlement."}, + {"ref": "formal/dialcache-runtime-boundaries-conformance.qnt:inheritedDisabledSharingNeverJoins", "scope": "At each admission with an omitted sharing leaf and configured false baseline, the caller starts independent work instead of joining an existing flight."}]}, {"id": "C03.late-recovery-publication", "rule": "Late stale recovery returns to original callers but cannot populate a closed or replacement request scope.", "contracts": ["C03"], "scenarios": ["late recovery cannot memoize into a closed or replacement request scope"], "generated": [{"profile": "recovery", "witness": "closed-recovery-does-not-memoize-another-scope"}, {"profile": "recovery-read", "witness": "closed-request-recovery-keeps-new-request-cold"}], "vectors": [], "quintReplays": ["recovery-read/closedRequestCannotReceiveCompressedRecoveryTest", "recovery/closedRequestCannotReceiveLateRecoveryMemoTest"], "models": [ {"ref": "formal/dialcache-recovery-conformance.qnt:closedRequestCannotReceiveLateRecoveryMemoTest", "scope": "Late stale recovery returns to original callers but cannot populate a closed or replacement request scope. The cited property covers its bounded modeled clause; exact values, adapter errors and unmodeled combinations retain their separate evidence."}, {"ref": "formal/dialcache-recovery-read-conformance.qnt:closedContextsStayEmpty", "scope": "Closing the attached request during held recovery decode permits the caller result but prevents request/local publication, distinguished by a new-scope read."}, {"ref": "formal/dialcache-recovery-read-conformance.qnt:closedRequestCannotReceiveCompressedRecoveryTest", "scope": "Closing the attached request during held recovery decode permits the caller result but prevents request/local publication, distinguished by a new-scope read."}], "definitions": [ {"ref": "formal/kernel/remote-io.qnt:complete", "scope": "Closing the attached request during held recovery decode permits the caller result but prevents request/local publication, distinguished by a new-scope read."}]}, - {"id": "C54.instance-isolation", "rule": "Shadow capacity and job identity deduplication apply within each cache instance independently.", "contracts": ["C54"], "scenarios": ["shadow capacity and job deduplication are isolated per instance"], "generated": [{"profile": "admission", "witness": "per-instance-deduplication"}, {"profile": "admission", "witness": "other-instance-full-admission"}, {"profile": "shadow-layers", "witness": "mixed-shadow-capacity-is-per-instance"}], "vectors": [], "quintReplays": ["shadow-layers/fullMixedInstanceDoesNotBlockAnotherInstanceTest", "admission/fullInstanceDoesNotBlockOtherInstanceTest"], "models": [ + {"id": "C54.instance-isolation", "rule": "Shadow capacity and job identity deduplication apply within each cache instance independently.", "contracts": ["C54"], "scenarios": ["shadow capacity and job deduplication are isolated per instance"], "generated": [{"profile": "admission", "witness": "per-instance-deduplication"}, {"profile": "admission", "witness": "other-instance-full-admission"}, {"profile": "shadow-layers", "witness": "mixed-shadow-capacity-is-per-instance"}, {"profile": "dark-layers", "witness": "dark-flights-are-instance-local"}], "vectors": [], "quintReplays": ["shadow-layers/fullMixedInstanceDoesNotBlockAnotherInstanceTest", "admission/fullInstanceDoesNotBlockOtherInstanceTest", "dark-layers/sameKeyCallersOnTwoInstancesKeepSeparateFlightsTest"], "models": [ {"ref": "formal/dialcache-admission-conformance.qnt:capacityIsPerInstance", "scope": "Shadow capacity and job identity deduplication apply within each cache instance independently. The cited property covers its bounded modeled clause; exact values, adapter errors and unmodeled combinations retain their separate evidence."}, {"ref": "formal/dialcache-admission-conformance.qnt:fullInstanceDoesNotBlockOtherInstanceTest", "scope": "Shadow capacity and job identity deduplication apply within each cache instance independently. The cited property covers its bounded modeled clause; exact values, adapter errors and unmodeled combinations retain their separate evidence."}, - {"ref": "formal/dialcache-shadow-layers-conformance.qnt:fullMixedInstanceDoesNotBlockAnotherInstanceTest", "scope": "A served and a dark job fill instance0; its third key is dropped, while the same key on instance1 admits a C0 read. Recorded source contexts distinguish the disabled served source from enabled independent caller sources."}], "definitions": [ - {"ref": "formal/kernel/shadow.qnt:admitJobs", "scope": "A served and a dark job fill instance0; its third key is dropped, while the same key on instance1 admits a C0 read. Recorded source contexts distinguish the disabled served source from enabled independent caller sources."}]}, + {"ref": "formal/dialcache-shadow-layers-conformance.qnt:fullMixedInstanceDoesNotBlockAnotherInstanceTest", "scope": "A served and a dark job fill instance0; its third key is dropped, while the same key on instance1 admits a C0 read. Recorded source contexts distinguish the disabled served source from enabled independent caller sources."}, + {"ref": "formal/dialcache-dark-layers-conformance.qnt:sameKeyCallersOnTwoInstancesKeepSeparateFlightsTest", "scope": "Same-key callers on distinct instances start distinct sources and dark reads; settling one leaves the other pending."}], "definitions": [ + {"ref": "formal/kernel/shadow.qnt:admitJobs", "scope": "A served and a dark job fill instance0; its third key is dropped, while the same key on instance1 admits a C0 read. Recorded source contexts distinguish the disabled served source from enabled independent caller sources."}, + {"ref": "formal/kernel/shadow.qnt:beginDark", "scope": "Same-key callers on distinct instances start distinct sources and dark reads; settling one leaves the other pending."}]}, {"id": "C18.captured-shadow-admission", "rule": "Disabling shadow prevents new admission while an already admitted comparison retains its captured policy.", "contracts": ["C18"], "scenarios": ["disabling shadow prevents new jobs but preserves an admitted comparison"], "generated": [{"profile": "shadow", "witness": "admitted-job-keeps-shadow-policy"}, {"profile": "admission", "witness": "accepted-shadow-policy"}], "vectors": [], "quintReplays": ["admission/capturedShadowPolicySurvivesPolicyChangeTest"], "models": [ {"ref": "formal/dialcache-shadow-conformance.qnt:admittedComparisonSurvivesPolicyDisableTest", "scope": "Disabling shadow prevents new admission while an already admitted comparison retains its captured policy. The cited property covers its bounded modeled clause; exact values, adapter errors and unmodeled combinations retain their separate evidence."}, {"ref": "formal/dialcache-admission-conformance.qnt:capturedShadowPolicySurvivesPolicyChangeTest", "scope": "A caller admitted under a selected shadow policy keeps its admission when the policy is disabled before its decode completes, and one admitted under a disabled policy gains none when it is re-enabled: the selection is captured with the read, not read at the hit."}]}, @@ -990,7 +1019,8 @@ {"ref": "formal/dialcache-effects-conformance.qnt:expiredZeroFenceReplyRefillsAndIsReadableTest", "scope": "A tracked expired-reason reply with a zero fence is normalized to an expired miss with no retained fence; one source refill is serialized and written, and the next public read hits it without a second source."}]}, {"id": "C55.frame-ignores-miss-metadata", "rule": "A valid frame without a miss discriminator remains a frame despite stray miss-reason or fence fields.", "contracts": ["C55"], "scenarios": ["frame without miss discriminator ignores stray miss metadata"], "generated": [{"profile": "effects", "witness": "reply:15"}], "vectors": [], "evidenceNotes": ["The scheduled regression asserts the frame is served without a source. Generated reply15 is an additional required input category, not by itself proof that a frame was served."], "models": [ {"ref": "formal/dialcache-effects-conformance.qnt:frameWithoutMissDiscriminatorIgnoresMetadataTest", "scope": "The scheduled regression asserts the frame is served without a source. Generated reply15 is an additional required input category, not by itself proof that a frame was served."}]}, - {"id": "C19.exact-serving-cohort", "rule": "A serving ramp excludes a key at equality with its cohort sample and includes it just above that sample, independently for each layer.", "contracts": ["C19"], "scenarios": ["local cohort excludes equality and admits just above its exact sample", "remote cohort excludes equality and admits just above its exact sample"], "generated": [{"profile": "runtime-boundaries", "witness": "exact-serving-cohort:local"}, {"profile": "runtime-boundaries", "witness": "exact-serving-cohort:remote"}], "vectors": [], "quintReplays": ["runtime-boundaries/localCohortEqualityBypassesBeforeAboveSampleCachesTest", "runtime-boundaries/remoteCohortEqualityBypassesBeforeAboveSampleCachesTest"], "models": [ + {"id": "C19.exact-serving-cohort", "rule": "A serving ramp excludes a key at equality with its cohort sample and includes it just above that sample, independently for each layer.", "contracts": ["C19"], "scenarios": ["local cohort excludes equality and admits just above its exact sample", "remote cohort excludes equality and admits just above its exact sample"], "generated": [{"profile": "runtime-boundaries", "witness": "exact-serving-cohort:local"}, {"profile": "runtime-boundaries", "witness": "exact-serving-cohort:remote"}], "vectors": [], "quintReplays": ["runtime-boundaries/localCohortEqualityBypassesBeforeAboveSampleCachesTest", "runtime-boundaries/localCohortEqualityLeavesNoValueForLaterAdmissionTest", "runtime-boundaries/remoteCohortEqualityBypassesBeforeAboveSampleCachesTest"], "models": [ + {"ref": "formal/dialcache-runtime-boundaries-conformance.qnt:localCohortEqualityLeavesNoValueForLaterAdmissionTest", "scope": "A local call at exact cohort equality bypasses storage; the next above-sample call starts a source and remains pending instead of finding a value published by the bypassed call."}, {"ref": "formal/dialcache-runtime-boundaries-conformance.qnt:localCohortEqualityBypassesBeforeAboveSampleCachesTest", "scope": "One fixed independently published canonical key numerator per serving layer, strict below/equality/above integer sample thresholds bound to floating percentage inputs; full FNV/hash key construction stays W03 wire-vector authority."}, {"ref": "formal/dialcache-runtime-boundaries-conformance.qnt:remoteCohortEqualityBypassesBeforeAboveSampleCachesTest", "scope": "One fixed independently published canonical key numerator per serving layer, strict below/equality/above integer sample thresholds bound to floating percentage inputs; full FNV/hash key construction stays W03 wire-vector authority."}, {"ref": "formal/dialcache-runtime-boundaries-conformance.qnt:equalAndLowerCohortsNeverTraverse", "scope": "One fixed independently published canonical key numerator per serving layer, strict below/equality/above integer sample thresholds bound to floating percentage inputs; full FNV/hash key construction stays W03 wire-vector authority."}]}, @@ -1006,16 +1036,25 @@ {"ref": "formal/dialcache-recovery-read-conformance.qnt:untrackedEncodingFailureStillPublishesLocalTest", "scope": "Unsupported encoding is classified after presence/fence checks and denies remote refill; source success remains visible, and untracked local publication remains eligible."}, {"ref": "formal/dialcache-recovery-read-conformance.qnt:encodingFailedReadLeavesNoCandidateTest", "scope": "An encoding-failed read retains no recovery snapshot: the source failure that follows is unclassified, completes the caller with the source's own error, reports no recovery label and refills nothing."}], "definitions": [ {"ref": "formal/kernel/remote-io.qnt:settleRead", "scope": "Unsupported encoding is classified after presence/fence checks and denies remote refill; source success remains visible, and untracked local publication remains eligible."}]}, + {"id": "C22.adapter-frame-expiry", "rule": "An adapter-returned frame is served through the last fresh millisecond, but at its exact freshness ceiling it is an expired miss that permits source refill and later cache reuse.", "contracts": ["C22", "C58"], "scenarios": [], "generated": [{"profile": "effects", "witness": "adapter-frame-last-fresh-millisecond-hits"}, {"profile": "effects", "witness": "adapter-frame-exact-expiry-refills-and-reuses"}], "vectors": [], "quintReplays": ["effects/frameReplyAtLastFreshMillisecondHitsTest", "effects/staleFrameReplyExpiresRefillsAndIsReadableTest"], "models": [ + {"ref": "formal/dialcache-effects-conformance.qnt:frameReplyAtLastFreshMillisecondHitsTest", "scope": "The queued adapter frame stamped at100000ms is decoded and served at age59999ms, without source, refill or miss diagnostic."}, + {"ref": "formal/dialcache-effects-conformance.qnt:staleFrameReplyExpiresRefillsAndIsReadableTest", "scope": "At age60000ms the queued frame is rejected before decoding with one expired miss and no future offset; source success is serialized, written for60000ms, and reused by a subsequent public call without another source."}, + {"ref": "formal/dialcache-effects-conformance.qnt:staleFrameRepliesReportExpired", "scope": "The independently stated property uses the raw acquired frame stamp, wall sample and monotonic read deadline to require the expired diagnostic for each timely stale queued frame reply."}], "definitions": [ + {"ref": "formal/kernel/metrics.qnt:missReason", "scope": "A queued frame is classified by its timestamp independently of stray miss metadata; a non-future frame declined at freshness reports expired."}, + {"ref": "formal/kernel/adapter-replies.qnt:observed", "scope": "Adapter frame freshness is judged at settlement using the frame timestamp and captured freshness, with an exclusive ceiling."}]}, {"id": "C22.freshness-after-read", "rule": "Logical remote freshness is evaluated at read settlement, so age accumulated during the bounded read can make a frame stale.", "contracts": ["C22"], "scenarios": ["freshness samples wall time after the bounded Redis read settles"], "generated": [{"profile": "recovery-read", "witness": "read-settlement-crosses-freshness-and-recovers"}, {"profile": "recovery-read", "witness": "read-settlement-before-freshness-hits"}], "vectors": [], "quintReplays": ["recovery-read/freshnessAfterHeldReadTest", "recovery-read/heldReadJustBeforeFreshnessBoundaryStillHitsTest"], "models": [ {"ref": "formal/dialcache-recovery-read-connection.qnt:freshnessUsesReadSettlement", "scope": "Read acquisition samples the settlement wall clock; a frame initially fresh becomes stale during the held read and subsequently recovers after source rejection."}, {"ref": "formal/dialcache-recovery-read-conformance.qnt:freshnessAfterHeldReadTest", "scope": "Read acquisition samples the settlement wall clock; a frame initially fresh becomes stale during the held read and subsequently recovers after source rejection."}, {"ref": "formal/dialcache-recovery-read-conformance.qnt:heldReadJustBeforeFreshnessBoundaryStillHitsTest", "scope": "Read acquisition samples the settlement wall clock; a frame initially fresh becomes stale during the held read and subsequently recovers after source rejection."}], "definitions": [ {"ref": "formal/kernel/remote-io.qnt:settleRead", "scope": "Read acquisition samples the settlement wall clock; a frame initially fresh becomes stale during the held read and subsequently recovers after source rejection."}]}, - {"id": "C58.failure-categories", "rule": "Read, policy, load, dump, and write failures retain their stable phase-specific diagnostic classification while their documented fail-open path continues.", "contracts": ["C58"], "scenarios": ["read failure retains its stable diagnostic category", "dump failure retains its stable diagnostic category", "write failure retains its stable diagnostic category", "load failure retains its stable diagnostic category", "policy failure retains its stable diagnostic category"], "generated": [{"profile": "effects", "witness": "error:cache_read"}, {"profile": "effects", "witness": "error:serialization_load"}, {"profile": "effects", "witness": "error:serialization_dump"}, {"profile": "effects", "witness": "error:cache_write"}], "vectors": [], "evidenceNotes": ["Each added model regression requires the phase label alongside its source-result and write/no-write consequence. Fixed scenarios additionally check policy-resolution diagnostics."], "models": [ + {"id": "C58.failure-categories", "rule": "Read, policy, load, dump, and write failures retain their stable phase-specific diagnostic classification while their documented fail-open path continues. Source rejections and deadlines record a fallback diagnostic at the layer running the source while preserving the caller failure.", "contracts": ["C58"], "scenarios": ["read failure retains its stable diagnostic category", "dump failure retains its stable diagnostic category", "write failure retains its stable diagnostic category", "load failure retains its stable diagnostic category", "policy failure retains its stable diagnostic category"], "generated": [{"profile": "effects", "witness": "error:cache_read"}, {"profile": "effects", "witness": "error:serialization_load"}, {"profile": "effects", "witness": "error:serialization_dump"}, {"profile": "effects", "witness": "error:cache_write"}, {"profile": "dark-layers", "witness": "dark-source-deadline-attributed-to-local"}, {"profile": "dark-layers", "witness": "transient-request-dark-error-uses-request-layer"}], "vectors": [], "evidenceNotes": ["Each added model regression requires the phase label alongside its source-result and write/no-write consequence. Fixed scenarios additionally check policy-resolution diagnostics."], "models": [ {"ref": "formal/dialcache-effects-conformance.qnt:readFailureKeepsCategoryAndDeniesRefillTest", "scope": "Each added model regression requires the phase label alongside its source-result and write/no-write consequence. Fixed scenarios additionally check policy-resolution diagnostics."}, {"ref": "formal/dialcache-effects-conformance.qnt:decodeFailureKeepsCategoryAndAllowsRefillTest", "scope": "Each added model regression requires the phase label alongside its source-result and write/no-write consequence. Fixed scenarios additionally check policy-resolution diagnostics."}, {"ref": "formal/dialcache-effects-conformance.qnt:dumpFailureKeepsCategoryAndSourceValueTest", "scope": "Each added model regression requires the phase label alongside its source-result and write/no-write consequence. Fixed scenarios additionally check policy-resolution diagnostics."}, - {"ref": "formal/dialcache-effects-conformance.qnt:writeFailureKeepsCategoryAndSourceValueTest", "scope": "Each added model regression requires the phase label alongside its source-result and write/no-write consequence. Fixed scenarios additionally check policy-resolution diagnostics."}]}, + {"ref": "formal/dialcache-effects-conformance.qnt:writeFailureKeepsCategoryAndSourceValueTest", "scope": "Each added model regression requires the phase label alongside its source-result and write/no-write consequence. Fixed scenarios additionally check policy-resolution diagnostics."}, + {"ref": "formal/dialcache-dark-layers-conformance.qnt:sourceDeadlineExpiresTheDarkJobAndAttributesToTheLocalLayerTest", "scope": "A shared source/job deadline ends dark work once, attributes the failed active-local source to local, and permits a subsequent same-scope retry."}, + {"ref": "formal/dialcache-dark-layers-conformance.qnt:transientRequestOnlySourceFailureIsAttributedToRequestLayerTest", "scope": "A transient request with no shared layer attributes its dark source rejection to request_local despite having no persistent model memo slot."}], "quintReplays": ["dark-layers/sourceDeadlineExpiresTheDarkJobAndAttributesToTheLocalLayerTest", "dark-layers/transientRequestOnlySourceFailureIsAttributedToRequestLayerTest"], "definitions": [{"ref": "formal/kernel/shadow.qnt:advanceHeld", "scope": "A shared source/job deadline ends dark work once, attributes the failed active-local source to local, and permits a subsequent same-scope retry."}, + {"ref": "formal/kernel/shadow.qnt:settleHeld", "scope": "A transient request with no shared layer attributes its dark source rejection to request_local despite having no persistent model memo slot."}]}, {"id": "C58.request-follower-trail", "rule": "Request followers report request-local coalescing without repeating lower cache traversal or the leader error trail.", "contracts": ["C58"], "scenarios": ["request followers report request scope without repeating lower traversal"], "generated": [{"profile": "scope", "witness": "one-error-for-request-followers"}], "vectors": [], "quintReplays": ["scope/sharedFailureHasOneAttributedErrorTest"], "models": [ {"ref": "formal/dialcache-scope-conformance.qnt:sharedFailureHasOneAttributedErrorTest", "scope": "Request followers report request-local coalescing without repeating lower cache traversal or the leader error trail. The cited property covers its bounded modeled clause; exact values, adapter errors and unmodeled combinations retain their separate evidence."}]}, {"id": "C58.late-rejection-not-recounted", "rule": "A source deadline records one failure; a later rejection of the abandoned loader does not emit another fallback failure.", "contracts": ["C58"], "scenarios": ["source timeout records one failure even after a late rejection"], "generated": [{"profile": "effects", "witness": "late-rejection-does-not-repeat-error"}], "vectors": [], "models": [ @@ -1038,10 +1077,13 @@ {"ref": "formal/dialcache-recovery-conformance.qnt:rollbackBelowFreshCeilingStillRecoversTest", "scope": "After retaining stale bytes, wall rollback to a nonnegative age below the fresh ceiling still permits recovery; only future age or the exclusive maximum rejects."}, {"ref": "formal/dialcache-stale-recovery.qnt:rollbackBelowFreshAgeStillRecoversTest", "scope": "After retaining stale bytes, wall rollback to a nonnegative age below the fresh ceiling still permits recovery; only future age or the exclusive maximum rejects."}, {"ref": "formal/dialcache-rule-checks.qnt:rollbackCanRecoverRetainedBytesTest", "scope": "Direct canonical-rule example accepts nonnegative recovery age below the fresh ceiling. Actual retention followed by rollback is exercised by the profile evidence."}]}, - {"id": "C53.dark-read-error-keeps-caller", "rule": "A failed dark C0 read completes the diagnostic job but an independent caller source can still succeed; no decode or fill follows the failed read.", "contracts": ["C53"], "scenarios": [], "generated": [{"profile": "shadow", "witness": "dark-read-error-still-allows-source-result"}], "vectors": [], "provenance": [{"path": "formal/SPEC.md", "section": "Recovery, shadow work, and invalidation races"}, {"path": "src/dialcache.ts", "lines": [1101, 1126, 1147, 1175, 1206, 1220], "scope": "Caller/source ownership, detached decoding, conditional fill, byte confirmation."}], "models": [ - {"ref": "formal/dialcache-shadow-conformance.qnt:darkReadErrorCannotReplaceCallerSourceTest", "scope": "A failed dark C0 read completes the diagnostic job but an independent caller source can still succeed; no decode or fill follows the failed read."}]}, - {"id": "C53.dark-source-error", "rule": "A rejected dark caller source is preserved as the caller failure, emits source_error, and never decodes or fills C0.", "contracts": ["C53"], "scenarios": [], "generated": [{"profile": "shadow", "witness": "dark-source-error-never-decodes-or-fills"}], "vectors": [], "provenance": [{"path": "formal/SPEC.md", "section": "Recovery, shadow work, and invalidation races"}, {"path": "src/dialcache.ts", "lines": [1101, 1126, 1147, 1175, 1206, 1220], "scope": "Caller/source ownership, detached decoding, conditional fill, byte confirmation."}], "models": [ - {"ref": "formal/dialcache-shadow-conformance.qnt:sourceErrorDoesNotDecodePresentDarkBytesTest", "scope": "A rejected dark caller source is preserved as the caller failure, emits source_error, and never decodes or fills C0."}]}, + {"id": "C53.dark-read-error-keeps-caller", "rule": "A failed dark C0 read completes the diagnostic job but an independent caller source can still succeed; no decode or fill follows the failed read.", "contracts": ["C53"], "scenarios": [], "generated": [{"profile": "shadow", "witness": "dark-read-error-still-allows-source-result"}, {"profile": "dark-layers", "witness": "unsupported-encoding-ends-dark-read"}], "vectors": [], "provenance": [{"path": "formal/SPEC.md", "section": "Recovery, shadow work, and invalidation races"}, {"path": "src/dialcache.ts", "lines": [1101, 1126, 1147, 1175, 1206, 1220], "scope": "Caller/source ownership, detached decoding, conditional fill, byte confirmation."}], "models": [ + {"ref": "formal/dialcache-shadow-conformance.qnt:darkReadErrorCannotReplaceCallerSourceTest", "scope": "A failed dark C0 read completes the diagnostic job but an independent caller source can still succeed; no decode or fill follows the failed read."}, + {"ref": "formal/dialcache-dark-layers-conformance.qnt:unsupportedEncodingFailsTheDarkReadTest", "scope": "An unsupported frame encoding ends the dark read as redis_error while the caller receives its independently resolved source value."}], "quintReplays": ["dark-layers/unsupportedEncodingFailsTheDarkReadTest"], "definitions": [{"ref": "formal/kernel/shadow.qnt:releaseRead", "scope": "An unsupported frame encoding ends the dark read as redis_error while the caller receives its independently resolved source value."}]}, + {"id": "C53.dark-source-error", "rule": "A rejected dark caller source is preserved as the caller failure, emits source_error, and never decodes or fills C0.", "contracts": ["C53"], "scenarios": [], "generated": [{"profile": "shadow", "witness": "dark-source-error-never-decodes-or-fills"}, {"profile": "dark-layers", "witness": "rejected-dark-source-seeds-no-layer"}], "vectors": [], "provenance": [{"path": "formal/SPEC.md", "section": "Recovery, shadow work, and invalidation races"}, {"path": "src/dialcache.ts", "lines": [1101, 1126, 1147, 1175, 1206, 1220], "scope": "Caller/source ownership, detached decoding, conditional fill, byte confirmation."}], "models": [ + {"ref": "formal/dialcache-shadow-conformance.qnt:sourceErrorDoesNotDecodePresentDarkBytesTest", "scope": "A rejected dark caller source is preserved as the caller failure, emits source_error, and never decodes or fills C0."}, + {"ref": "formal/dialcache-dark-layers-conformance.qnt:rejectedDarkSourceSeedsNoLayerTest", "scope": "A rejected dark source returns SOURCE_ERROR, emits source_error and starts no decode or fill; a same-scope retry starts a new source and dark read."}], "quintReplays": ["dark-layers/rejectedDarkSourceSeedsNoLayerTest"], "definitions": [ + {"ref": "formal/kernel/shadow.qnt:settleHeld", "scope": "A rejected dark source returns SOURCE_ERROR, emits source_error and starts no decode or fill; a same-scope retry starts a new source and dark read."}]}, {"id": "C52.fenced-miss-may-fill", "rule": "A fenced C0 is a semantic miss; a source-success fill may proceed once its own timestamp clears the observed fence, without decoding C0.", "contracts": ["C52"], "scenarios": [], "generated": [{"profile": "shadow", "witness": "fenced-dark-c0-can-fill-after-cutoff"}], "vectors": [], "provenance": [{"path": "formal/SPEC.md", "section": "Recovery, shadow work, and invalidation races"}, {"path": "src/dialcache.ts", "lines": [1101, 1126, 1147, 1175, 1206, 1220], "scope": "Caller/source ownership, detached decoding, conditional fill, byte confirmation."}], "models": [ {"ref": "formal/dialcache-shadow-conformance.qnt:fencedDarkBytesCanAdmitNewerFillTest", "scope": "A fenced C0 is a semantic miss; a source-success fill may proceed once its own timestamp clears the observed fence, without decoding C0."}]}, {"id": "C51.fenced-confirmation", "rule": "A watermark-fenced C1 supersedes an unequal C0/source comparison without repair, warning, or value-age verdict.", "contracts": ["C51"], "scenarios": [], "generated": [{"profile": "shadow", "witness": "fenced-c1-supersedes-without-repair"}], "vectors": [], "provenance": [{"path": "formal/SPEC.md", "section": "Recovery, shadow work, and invalidation races"}, {"path": "src/dialcache.ts", "lines": [1101, 1126, 1147, 1175, 1206, 1220], "scope": "Caller/source ownership, detached decoding, conditional fill, byte confirmation."}], "models": [ @@ -1051,14 +1093,17 @@ {"id": "C50.equal-skips-confirmation", "rule": "A semantic C0/source match emits match without issuing a C1 read or repairing the cache.", "contracts": ["C50"], "scenarios": [], "generated": [{"profile": "shadow", "witness": "equal-comparison-skips-c1"}], "vectors": [], "quintReplays": ["shadow-layers/darkC0EqualToSourceIsAMatchTest"], "provenance": [{"path": "formal/SPEC.md", "section": "Recovery, shadow work, and invalidation races"}, {"path": "src/dialcache.ts", "lines": [1101, 1126, 1147, 1175, 1206, 1220], "scope": "Caller/source ownership, detached decoding, conditional fill, byte confirmation."}], "models": [ {"ref": "formal/dialcache-shadow-conformance.qnt:binarySnapshotComparesDecodedValueTest", "scope": "A semantic C0/source match emits match without issuing a C1 read or repairing the cache."}, {"ref": "formal/dialcache-shadow-layers-conformance.qnt:darkC0EqualToSourceIsAMatchTest", "scope": "In the composed shadow-layers profile a dark job whose fresh C0 decodes to the caller source's value ends match after one decode with no confirmation read (the read count stays at the C0 read) and holds no dump; the caller completes with its source value. Public inputs only; no served-hit or repair claim."}]}, - {"id": "C53.fill-serialization-error", "rule": "A dark fill serialization failure emits fill_error without dispatching a Redis write and preserves the accepted caller result.", "contracts": ["C53"], "scenarios": [], "generated": [{"profile": "shadow", "witness": "fill-serialization-error-preserves-caller"}], "vectors": [], "provenance": [{"path": "formal/SPEC.md", "section": "Recovery, shadow work, and invalidation races"}, {"path": "src/dialcache.ts", "lines": [1101, 1126, 1147, 1175, 1206, 1220], "scope": "Caller/source ownership, detached decoding, conditional fill, byte confirmation."}], "models": [ - {"ref": "formal/dialcache-shadow-conformance.qnt:fillSerializationFailureNeverDispatchesWriteTest", "scope": "A dark fill serialization failure emits fill_error without dispatching a Redis write and preserves the accepted caller result."}]}, + {"id": "C53.fill-serialization-error", "rule": "A dark fill serialization failure emits fill_error without dispatching a Redis write and preserves the accepted caller result.", "contracts": ["C53"], "scenarios": [], "generated": [{"profile": "shadow", "witness": "fill-serialization-error-preserves-caller"}, {"profile": "dark-layers", "witness": "held-dark-dump-error-preserves-source"}], "vectors": [], "provenance": [{"path": "formal/SPEC.md", "section": "Recovery, shadow work, and invalidation races"}, {"path": "src/dialcache.ts", "lines": [1101, 1126, 1147, 1175, 1206, 1220], "scope": "Caller/source ownership, detached decoding, conditional fill, byte confirmation."}], "models": [ + {"ref": "formal/dialcache-shadow-conformance.qnt:fillSerializationFailureNeverDispatchesWriteTest", "scope": "A dark fill serialization failure emits fill_error without dispatching a Redis write and preserves the accepted caller result."}, + {"ref": "formal/dialcache-dark-layers-conformance.qnt:dumpFaultEndsTheFillWithoutReplacingTheCallerResultTest", "scope": "A failed held dark dump records fill_error without a remote write and preserves the source result already delivered to the caller."}], "quintReplays": ["dark-layers/dumpFaultEndsTheFillWithoutReplacingTheCallerResultTest"], "definitions": [{"ref": "formal/kernel/shadow.qnt:releaseDump", "scope": "A failed held dark dump records fill_error without a remote write and preserves the source result already delivered to the caller."}]}, {"id": "C53.fill-write-error", "rule": "A dispatched dark Redis write failure emits fill_error and preserves the accepted caller result; it does not retry or emit a second verdict.", "contracts": ["C53"], "scenarios": [], "generated": [{"profile": "shadow", "witness": "fill-write-error-preserves-caller"}], "vectors": [], "provenance": [{"path": "formal/SPEC.md", "section": "Recovery, shadow work, and invalidation races"}, {"path": "src/dialcache.ts", "lines": [1101, 1126, 1147, 1175, 1206, 1220], "scope": "Caller/source ownership, detached decoding, conditional fill, byte confirmation."}], "models": [ {"ref": "formal/dialcache-shadow-conformance.qnt:fillWriteFailurePreservesCallerResultTest", "scope": "A dispatched dark Redis write failure emits fill_error and preserves the accepted caller result; it does not retry or emit a second verdict."}]}, - {"id": "C34.observed-fence-snapshot", "rule": "A conditional refill uses the miss-time observed fence; later invalidation can fence a dispatched write on subsequent reads without retroactively rewriting that miss.", "contracts": ["C34"], "scenarios": ["invalidation during source does not replace the acquired miss fence"], "generated": [{"profile": "recovery-read", "witness": "later-invalidation-keeps-captured-miss-fence-new-read-fences-write"}], "vectors": [], "provenance": [{"path": "formal/SPEC.md", "section": "Recovery, shadow work, and invalidation races"}], "quintReplays": ["recovery-read/laterInvalidationDoesNotRewriteObservedMissFenceTest"], "models": [ + {"id": "C34.observed-fence-snapshot", "rule": "A conditional refill uses the miss-time observed fence; later invalidation can fence a dispatched write on subsequent reads without retroactively rewriting that miss.", "contracts": ["C34"], "scenarios": ["invalidation during source does not replace the acquired miss fence"], "generated": [{"profile": "recovery-read", "witness": "later-invalidation-keeps-captured-miss-fence-new-read-fences-write"}, {"profile": "dark-layers", "witness": "stale-visible-dark-c0-fills-unfenced"}], "vectors": [], "provenance": [{"path": "formal/SPEC.md", "section": "Recovery, shadow work, and invalidation races"}], "quintReplays": ["recovery-read/laterInvalidationDoesNotRewriteObservedMissFenceTest", "dark-layers/staleVisibleFrameDeclinedByTheDarkReadFillsUnfencedTest"], "models": [ {"ref": "formal/dialcache-tracked-invalidation.qnt:laterInvalidationDoesNotRewriteMissFenceTest", "scope": "A conditional refill uses the miss-time observed fence; later invalidation can fence a dispatched write on subsequent reads without retroactively rewriting that miss."}, - {"ref": "formal/dialcache-recovery-read-conformance.qnt:laterInvalidationDoesNotRewriteObservedMissFenceTest", "scope": "A miss captures the absent watermark baseline before invalidation. Source publication still dispatches at the later watermark timestamp; a new request rejects that now-fenced write, proving both captured-refill authority and subsequent fence visibility."}], "definitions": [ - {"ref": "formal/kernel/remote-io.qnt:settleRead", "scope": "A miss captures the absent watermark baseline before invalidation. Source publication still dispatches at the later watermark timestamp; a new request rejects that now-fenced write, proving both captured-refill authority and subsequent fence visibility."}]}, + {"ref": "formal/dialcache-recovery-read-conformance.qnt:laterInvalidationDoesNotRewriteObservedMissFenceTest", "scope": "A miss captures the absent watermark baseline before invalidation. Source publication still dispatches at the later watermark timestamp; a new request rejects that now-fenced write, proving both captured-refill authority and subsequent fence visibility."}, + {"ref": "formal/dialcache-dark-layers-conformance.qnt:staleVisibleFrameDeclinedByTheDarkReadFillsUnfencedTest", "scope": "A stale but visible C0 is declined without retaining a fence; invalidation after that read does not stop the captured fill from dispatching and completing."}], "definitions": [ + {"ref": "formal/kernel/remote-io.qnt:settleRead", "scope": "A miss captures the absent watermark baseline before invalidation. Source publication still dispatches at the later watermark timestamp; a new request rejects that now-fenced write, proving both captured-refill authority and subsequent fence visibility."}, + {"ref": "formal/kernel/shadow.qnt:releaseRead", "scope": "A stale but visible C0 is declined without retaining a fence; invalidation after that read does not stop the captured fill from dispatching and completing."}]}, {"id": "C38.value-work-does-not-extend-marker", "rule": "Ordinary tracked reads and complete value writes do not create, advance, or extend invalidation watermarks.", "contracts": ["C38"], "scenarios": [], "generated": [{"profile": "recovery-read", "witness": "ordinary-value-work-preserves-absent-marker"}, {"profile": "recovery-read", "witness": "ordinary-value-work-preserves-marker-cutoff-and-expiry"}], "vectors": [], "evidenceNotes": ["The tracked-invalidation regression checks cutoff preservation only. The recovery-read profile additionally exports marker probes that check continued absence or unchanged cutoff and expiry across ordinary reads and value writes. These controlled adapter observations complement real-server protocol integration; they do not prove native Redis internals."], "provenance": [{"path": "formal/SPEC.md", "section": "Recovery, shadow work, and invalidation races"}], "quintReplays": ["recovery-read/ordinaryValueWorkDoesNotCreateMarkerTest", "recovery-read/ordinaryValueWorkDoesNotExtendMarkerTest", "recovery-read/expiredMarkerIsGoneTest", "recovery-read/lapsedMarkerUnfencesTheFrameStampedAtItsCutoffTest"], "models": [ {"ref": "formal/dialcache-tracked-invalidation.qnt:readsAndValueWritesDoNotMoveWatermarkTest", "scope": "This regression checks cutoff preservation only; it does not model marker existence or TTL. The separate recovery-read regressions and required witnesses probe continued marker absence or unchanged cutoff and expiry across ordinary reads and value writes, with real-server protocol integration remaining additional evidence."}, {"ref": "formal/dialcache-recovery-read-conformance.qnt:ordinaryValueWorkDoesNotCreateMarkerTest", "scope": "External marker cutoff and TTL probes before/after ordinary tracked reads and value writes distinguish missing-marker creation, cutoff movement and lifetime extension. Controlled adapter observation, not a proof of native Redis internals."}, @@ -1090,13 +1135,15 @@ {"ref": "formal/dialcache-policy-conformance.qnt:invalidLocalRampPreservesRemoteTest", "scope": "Invalid local ramp preserves valid remote hits. This check covers the bounded model clause: invalidLocalRampPreservesRemoteTest. Host representations and other feature combinations retain separate fixed/native evidence."}]}, {"id": "C21.invalid-remote-ramp", "rule": "Invalid remote ramp preserves valid local hits", "contracts": ["C21"], "scenarios": [], "generated": [{"profile": "policy", "witness": "invalid-remote-ramp-preserves-local-hit"}], "vectors": [], "provenance": ["test/dialcache-config-ramp.test.ts:rejects runtime ramps outside the inclusive 0-100 domain"], "quintReplays": ["policy/invalidRemoteRampPreservesLocalTest"], "models": [ {"ref": "formal/dialcache-policy-conformance.qnt:invalidRemoteRampPreservesLocalTest", "scope": "Invalid remote ramp preserves valid local hits. This check covers the bounded model clause: invalidRemoteRampPreservesLocalTest. Host representations and other feature combinations retain separate fixed/native evidence."}]}, - {"id": "C27.source-error-no-publication", "rule": "A rejected source preserves its failure and publishes no successful value to participating cache layers", "contracts": ["C15", "C27"], "scenarios": ["source rejection publishes to none of the participating layers"], "generated": [{"profile": "local-failure", "witness": "rejected-source-preserves-previous-local-publication"}], "vectors": [], "provenance": ["test/dialcache-request-local.test.ts:removes a rejected request-local in-flight entry so the same scope can retry"], "quintReplays": ["local-failure/sourceFailureKeepsPreviouslyAcceptedStorageTest", "local-failure/rejectedSourceDoesNotSeedLocalTest"], "models": [ + {"id": "C27.source-error-no-publication", "rule": "A rejected source preserves its failure and publishes no successful value to participating cache layers", "contracts": ["C15", "C27"], "scenarios": ["source rejection publishes to none of the participating layers"], "generated": [{"profile": "local-failure", "witness": "rejected-source-preserves-previous-local-publication"}, {"profile": "dark-layers", "witness": "rejected-dark-source-seeds-no-layer"}], "vectors": [], "provenance": ["test/dialcache-request-local.test.ts:removes a rejected request-local in-flight entry so the same scope can retry"], "quintReplays": ["local-failure/sourceFailureKeepsPreviouslyAcceptedStorageTest", "local-failure/rejectedSourceDoesNotSeedLocalTest", "dark-layers/rejectedDarkSourceSeedsNoLayerTest"], "models": [ {"ref": "formal/dialcache-core.qnt:sourceFailureNeverPublishes", "scope": "A rejected source preserves its failure and publishes no successful value to participating cache layers. This check covers the bounded model clause: sourceFailureNeverPublishes. Host representations and other feature combinations retain separate fixed/native evidence."}, {"ref": "formal/dialcache-core.qnt:rejectedSourceNeverPublishesTest", "scope": "A rejected source preserves its failure and publishes no successful value to participating cache layers. This check covers the bounded model clause: rejectedSourceNeverPublishesTest. Host representations and other feature combinations retain separate fixed/native evidence."}, {"ref": "formal/dialcache-local-failure-conformance.qnt:storageHoldsAcceptedValuesOnly", "scope": "The local slot, the remote frame and every memo row hold no value or an accepted one, so a rejected source publishes and memoizes nothing and an error code is never stored. Real local storage remains behind a native exception/clock-failure injection seam; no elapsed-time or concurrent-source claim."}, {"ref": "formal/dialcache-local-failure-conformance.qnt:sourceFailureKeepsPreviouslyAcceptedStorageTest", "scope": "Real local storage remains behind a native exception/clock-failure injection seam. Distinct request and transient public probes check the old local value, accepted caller result and request-only memo; read failure remains publication-ineligible after the injected fault is cleared. No elapsed-time or concurrent-source claim."}, - {"ref": "formal/dialcache-local-failure-conformance.qnt:rejectedSourceDoesNotSeedLocalTest", "scope": "A source rejected on an empty local store seeds nothing: the transient call that follows misses locally, reads remote again and starts its own source, so the rejected result never becomes a local publication. Healthy storage throughout; no memo, elapsed-time or concurrent-source claim."}], "definitions": [ - {"ref": "formal/kernel/serving.qnt:publish", "scope": "Publication warms local storage only for an accepted value with a positive captured local TTL; a rejected source refills and warms nothing and completes its callers with the error."}]}, + {"ref": "formal/dialcache-local-failure-conformance.qnt:rejectedSourceDoesNotSeedLocalTest", "scope": "A source rejected on an empty local store seeds nothing: the transient call that follows misses locally, reads remote again and starts its own source, so the rejected result never becomes a local publication. Healthy storage throughout; no memo, elapsed-time or concurrent-source claim."}, + {"ref": "formal/dialcache-dark-layers-conformance.qnt:rejectedDarkSourceSeedsNoLayerTest", "scope": "A rejected dark caller seeds neither request memo nor local storage; the next same-scope call starts a fresh source and dark read."}], "definitions": [ + {"ref": "formal/kernel/serving.qnt:publish", "scope": "Publication warms local storage only for an accepted value with a positive captured local TTL; a rejected source refills and warms nothing and completes its callers with the error."}, + {"ref": "formal/kernel/shadow.qnt:settleHeld", "scope": "A rejected dark caller seeds neither request memo nor local storage; the next same-scope call starts a fresh source and dark read."}]}, {"id": "C09.fractional-clock-grid", "rule": "Local insertion and expiry use the common process monotonic whole-millisecond grid, including fractional insertion times and cache instances constructed at different fractional times.", "contracts": ["C09"], "scenarios": [], "generated": [{"profile": "local-clock", "witness": "fractional-insertion-expiry"}, {"profile": "local-clock", "witness": "shared-instance-grid"}], "vectors": [], "evidenceNotes": ["The fixture advances native time in microseconds and constructs real default cache instances. It does not replace either local clock with the shared integer behavior clock. Precise source, read and shadow deadlines remain separately bound."], "quintReplays": ["local-clock/fractionalInsertionExpiresAtWholeMillisecondTest", "local-clock/fractionalConstructionKeepsCommonInstanceGridTest", "local-clock/nonzeroOriginKeepsWholeMillisecondBoundaryTest", "local-clock/integerInsertionRetainsFullTtlTest"], "models": [ {"ref": "formal/dialcache-local-clock-conformance.qnt:fractionalInsertionExpiresAtWholeMillisecondTest", "scope": "Local insertion and expiry use the common process monotonic whole-millisecond grid, including fractional insertion times and cache instances constructed at different fractional times.. The deterministic regression checks the public result and subsequent reuse for this bounded fixture."}, {"ref": "formal/dialcache-local-clock-conformance.qnt:fractionalConstructionKeepsCommonInstanceGridTest", "scope": "Local insertion and expiry use the common process monotonic whole-millisecond grid, including fractional insertion times and cache instances constructed at different fractional times.. The deterministic regression checks the public result and subsequent reuse for this bounded fixture."}, @@ -1105,6 +1152,74 @@ {"ref": "formal/dialcache-local-clock-conformance.qnt:clockIsOnTheGrid", "scope": "Local insertion and expiry use the common process monotonic whole-millisecond grid, including fractional insertion times and cache instances constructed at different fractional times. The clock the layers read is the integer projection of the raw ticks, spelled without the clock module, so a precise-clock substitution violates it."}, {"ref": "formal/dialcache-local-clock-conformance.qnt:callsServeLiveEntriesOrRunTheSource", "scope": "Local insertion and expiry use the common process monotonic whole-millisecond grid, including fractional insertion times and cache instances constructed at different fractional times. At a call step the caller served its instance's live entry (strictly before the entry's grid expiry) and started no source, or ran its source, returned the offered value and left the entry stamped from this grid instant for the local TTL."}, {"ref": "formal/dialcache-local-clock-conformance.qnt:publicCallsMatchSources", "scope": "Local insertion and expiry use the common process monotonic whole-millisecond grid, including fractional insertion times and cache instances constructed at different fractional times. One source per loader, every owned caller completed by its own source's outcome and every entry the value of a source on its instance, so the public observation agrees with the library records."}], "definitions": [ - {"ref": "formal/kernel/local-storage.qnt:promote", "scope": "A hit renews recency, never insertion: promote writes the LRU order only, so the entry keeps the grid expiry its fill stamped; the regression fractionalInsertionExpiresAtWholeMillisecondTest pins it."}]} + {"ref": "formal/kernel/local-storage.qnt:promote", "scope": "A hit renews recency, never insertion: promote writes the LRU order only, so the entry keeps the grid expiry its fill stamped; the regression fractionalInsertionExpiresAtWholeMillisecondTest pins it."}]}, + { + "id": "C23.later-dark-budget-origins", + "rule": "A later dark caller and its shadow job receive full budgets from their own starts.", + "contracts": [ + "C23" + ], + "scenarios": [], + "generated": [ + { + "profile": "dark-layers", + "witness": "later-dark-source-keeps-own-budget" + } + ], + "vectors": [], + "quintReplays": [ + "dark-layers/laterDarkSourceKeepsItsWholeBudgetTest" + ], + "models": [ + { + "ref": "formal/dialcache-dark-layers-conformance.qnt:pendingDeadlinesAreFuture", + "scope": "Positive budgets remain strictly in the future after registration; time advancement delivers due work before observation." + }, + { + "ref": "formal/dialcache-dark-layers-conformance.qnt:laterDarkSourceKeepsItsWholeBudgetTest", + "scope": "The caller and dark job start at 10 ms; immediate source success publishes locally for another request scope while the dark read remains held without timing out." + } + ], + "definitions": [ + { + "ref": "formal/kernel/deadlines.qnt:due", + "scope": "Registers the absolute deadline from the current elapsed time plus the captured positive work budget." + } + ] + }, + { + "id": "C54.later-served-job-budget-origin", + "rule": "A later served-hit shadow job receives its full budget from admission, independent of process uptime.", + "contracts": [ + "C54" + ], + "scenarios": [], + "generated": [ + { + "profile": "admission", + "witness": "later-served-shadow-keeps-own-job-budget" + } + ], + "vectors": [], + "quintReplays": [ + "admission/laterServedShadowKeepsItsWholeBudgetTest" + ], + "models": [ + { + "ref": "formal/dialcache-admission-conformance.qnt:pendingDeadlinesAreFuture", + "scope": "Positive budgets remain strictly in the future after registration; time advancement delivers due work before observation." + }, + { + "ref": "formal/dialcache-admission-conformance.qnt:laterServedShadowKeepsItsWholeBudgetTest", + "scope": "After admission at 10 ms, source rejection at 11 ms reports source_error and the caller retains its served value. A clock-origin budget would report timeout." + } + ], + "definitions": [ + { + "ref": "formal/kernel/deadlines.qnt:due", + "scope": "Registers the absolute deadline from the current elapsed time plus the captured positive work budget." + } + ] + } ] } diff --git a/formal/semantic-reporter.mjs b/formal/semantic-reporter.mjs index eee962a1..4f001a40 100644 --- a/formal/semantic-reporter.mjs +++ b/formal/semantic-reporter.mjs @@ -19,7 +19,7 @@ export function evaluateSemanticTestReport(data, execution, exitCode, label = 'c const passed = assertions.filter(test => test.status === 'passed').length; const violation = failed.flatMap(test => test.failureMessages).find(message => settlementViolationPattern.test(message)); const infrastructure = violation !== undefined - || failed.some(test => test.failureMessages.some(message => /(?:Test|Hook) timed out in/.test(message))); + || failed.some(test => test.failureMessages.some(message => /(?:Test|Hook) timed out in|(?:^|\n)(?:Error: )?INVALIDATION_INFRASTRUCTURE:/.test(message))); const { unhandledErrors } = execution; if (execution.reason !== (failed.length ? 'failed' : 'passed') || execution.collectionErrors.length || (unhandledErrors.length && !failed.length) || passed + failed.length === 0 || infrastructure || diff --git a/formal/shadow-read-deadlines-smoke.itf.json b/formal/shadow-read-deadlines-smoke.itf.json new file mode 100644 index 00000000..b98ad4e3 --- /dev/null +++ b/formal/shadow-read-deadlines-smoke.itf.json @@ -0,0 +1,24 @@ +{ + "#meta": { + "format": "ITF", + "source": "formal/dialcache-shadow-read-deadlines-conformance.qnt", + "recipe": "formal/fixture-recipes.json#formal/shadow-read-deadlines-smoke.itf.json/smoke" + }, + "vars": [ + "input", + "s", + "mbt::actionTaken", + "mbt::nondetPicks" + ], + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[],"sourceErrors":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"5"},"name":"advance"},"mbt::actionTaken":"advance","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"5"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["redis_error"],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[{"#bigint":"0"}],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["redis_error","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[{"#bigint":"0"}],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"},{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"3"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"3"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["redis_error","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[{"#bigint":"0"}],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"},{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"10"},"name":"advance"},"mbt::actionTaken":"advance","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"10"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["redis_error","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[{"#bigint":"0"}],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"},{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["redis_error","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[{"#bigint":"0"}],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"},{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"3"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"3"},"reads":{"#bigint":"2"},"recovery":[],"shadow":["redis_error","dropped"],"sourceScopes":[true,true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[{"#bigint":"0"}],"budgets":[{"#bigint":"5"},{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"},{"#bigint":"0"},{"#bigint":"0"}]}}} + ] +} diff --git a/formal/source-audit.json b/formal/source-audit.json index 88b4a941..1b553d4b 100644 --- a/formal/source-audit.json +++ b/formal/source-audit.json @@ -416,7 +416,7 @@ }, { "path": "test/dialcache-get-or-load.test.ts", - "sha256": "48608eca552ac6d6a01b3c9b314be7e922919e6357c56349a2addc06dc2c043d", + "sha256": "e80d66b1f509806a3a7dc6beb02eb05ed154516999a886d8a6168cfd557ddc67", "entries": [ {"line":69,"title":"reuses a stable use case without registering it and still rejects the reserved name","contracts":["B01"]}, {"line":111,"title":"uses captured values from each invocation and requires them in the direct key","contracts":["C11","E04"]}, @@ -424,10 +424,10 @@ {"line":180,"title":"is true pass-through outside an enabled scope","contracts":["C01"]}, {"line":203,"title":"validates and snapshots defaultConfig independently for each invocation","contracts":["C18","B01"]}, {"line":243,"title":"reads and writes Redis values with the per-invocation serializer","contracts":["W06","B03"]}, - {"line":281,"title":"preserves tracked invalidation behavior","contracts":["C31","C32"]}, - {"line":310,"title":"fails open without attempting a Redis write after a read failure","contracts":["C28"]}, - {"line":338,"title":"propagates loader errors, records them as fallback failures, and clears the flight","contracts":["C15","C58"]}, - {"line":372,"title":"applies the per-invocation fallback deadline inside enabled scopes","contracts":["C23","B01"]} + {"line":282,"title":"preserves tracked invalidation behavior","contracts":["C31","C32"]}, + {"line":311,"title":"fails open without attempting a Redis write after a read failure","contracts":["C28"]}, + {"line":339,"title":"propagates loader errors, records them as fallback failures, and clears the flight","contracts":["C15","C58"]}, + {"line":373,"title":"applies the per-invocation fallback deadline inside enabled scopes","contracts":["C23","B01"]} ] }, { @@ -1005,20 +1005,20 @@ "reviewedGuides": [ { "path": "formal/AUTHORING.md", - "sha256": "abbda4d86306771ce8c8bd4df66134acaac0eb66966accba70d3b3d544f19b8f", + "sha256": "a6db6620cffc19ca004b94084d2b3ae9b782dcdfa02862889c5c4a03a0d15ce9", "entries": [ {"line":1,"title":"Reading and extending the DialCache models"}, {"line":20,"title":"Reading a model"}, {"line":61,"title":"Writing conventions"}, - {"line":159,"title":"Codifying the next behavior"}, - {"line":208,"title":"Give reviewers focused context"}, - {"line":223,"title":"Exporting a deterministic regression"}, - {"line":261,"title":"Maintaining case and witness evidence"}, - {"line":314,"title":"Reproducible committed fixtures"}, - {"line":324,"title":"Refactoring and execution"}, - {"line":352,"title":"Challenging every model"}, - {"line":425,"title":"Mapping every challenge to native mutants"}, - {"line":511,"title":"Exported runs are exactly the public-only runs"} + {"line":172,"title":"Codifying the next behavior"}, + {"line":221,"title":"Give reviewers focused context"}, + {"line":236,"title":"Exporting a deterministic regression"}, + {"line":274,"title":"Maintaining case and witness evidence"}, + {"line":327,"title":"Reproducible committed fixtures"}, + {"line":337,"title":"Refactoring and execution"}, + {"line":365,"title":"Challenging every model"}, + {"line":446,"title":"Mapping every challenge to native mutants"}, + {"line":577,"title":"Exported runs are exactly the public-only runs"} ], "review": { "kind": "tooling-guide", @@ -1027,23 +1027,23 @@ }, { "path": "formal/BEHAVIOR.md", - "sha256": "999ead7066aa1b178912530020bdef6f9aa10670a1dc1351feacde2c442d0527", + "sha256": "8efeb741c004ee558f7d1d64b9cccc059577a25f9d0fbe62b9f9612206cb64d7", "entries": [ {"line":1,"title":"Portable behavioral scenarios and generated feature profiles"}, {"line":17,"title":"Scenario format"}, {"line":36,"title":"Optional observed events"}, - {"line":50,"title":"Fixture"}, - {"line":62,"title":"Inputs and completion boundaries"}, - {"line":82,"title":"Additional composition profiles"}, - {"line":134,"title":"Observations"}, - {"line":146,"title":"Additional fixed-scenario inputs"}, - {"line":152,"title":"Generated pending-effect profile"}, - {"line":203,"title":"Generated feature profiles"}, - {"line":303,"title":"Generated independent-caller profile"}, - {"line":324,"title":"Generated layer-composition profile"}, - {"line":358,"title":"Generated request-scope profile"}, - {"line":381,"title":"Generated served-hit shadow admission profile"}, - {"line":405,"title":"Port workflow and limits"} + {"line":51,"title":"Fixture"}, + {"line":63,"title":"Inputs and completion boundaries"}, + {"line":84,"title":"Additional composition profiles"}, + {"line":138,"title":"Observations"}, + {"line":150,"title":"Additional fixed-scenario inputs"}, + {"line":156,"title":"Generated pending-effect profile"}, + {"line":209,"title":"Generated feature profiles"}, + {"line":315,"title":"Generated independent-caller profile"}, + {"line":336,"title":"Generated layer-composition profile"}, + {"line":370,"title":"Generated request-scope profile"}, + {"line":393,"title":"Generated served-hit shadow admission profile"}, + {"line":417,"title":"Port workflow and limits"} ], "review": { "kind": "contract-guide", @@ -1154,7 +1154,7 @@ }, { "path": "formal/CONTRACTS.md", - "sha256": "5dd880c3ca427206a3c28fd0e26c4f0ff202e37a053f0b10ea050c3a5869b267", + "sha256": "022a0f5ad14944f3cdfd34ef0bf4ed7a621fd3d16bdfcc97a809ff89515707cf", "entries": [ {"line":1,"title":"Portable contract inventory"}, {"line":11,"title":"Where the boundary lies"}, @@ -1257,7 +1257,7 @@ }, { "path": "formal/FEATURE-COVERAGE.md", - "sha256": "81312d747f7af78d518462f3b0ab7641c1aa101dcbdf478cb3f9105c0f48b1b3", + "sha256": "7fd6d5342cade1c22661461377cc7efa8e493e4b96d34f0b9580e9beac260329", "entries": [ {"line":1,"title":"DialCache features and known behavioral corners"}, {"line":16,"title":"What is accounted for"}, @@ -1288,18 +1288,18 @@ }, { "path": "formal/PORTING.md", - "sha256": "2807f483a12eabe71fa7ead0d19df2d1dcf9f106da86b69d03555956472c167e", + "sha256": "9eed42f975c129b1b001b7189c3564dff79f25ae9a38f299853209a8f091e300", "entries": [ {"line":1,"title":"Implementing another DialCache port"}, {"line":14,"title":"What a port supplies"}, {"line":45,"title":"Shared coordinator protocol"}, - {"line":88,"title":"Trace and observation contract"}, - {"line":189,"title":"Observation contract"}, - {"line":287,"title":"Generated fixtures and fast local tests"}, - {"line":322,"title":"Complete corpus and reusable acceptance checks"}, - {"line":430,"title":"Witness evidence"}, - {"line":520,"title":"Current limitations for a third port"}, - {"line":542,"title":"New-port acceptance"} + {"line":95,"title":"Trace and observation contract"}, + {"line":196,"title":"Observation contract"}, + {"line":294,"title":"Generated fixtures and fast local tests"}, + {"line":329,"title":"Complete corpus and reusable acceptance checks"}, + {"line":437,"title":"Witness evidence"}, + {"line":527,"title":"Current limitations for a third port"}, + {"line":549,"title":"New-port acceptance"} ], "review": { "kind": "tooling-guide", @@ -1347,14 +1347,15 @@ }, { "path": "formal/README.md", - "sha256": "7edd8ccbb9d6dc4d3e528c085e6b4337b540f4e793f827b1a7867dc64f30c86a", + "sha256": "acb61cc27f29cb9b3f8996fef1d72ae301ebff65ff0793515c62778cc657a6a0", "entries": [ {"line":1,"title":"Executable DialCache specification"}, {"line":10,"title":"Start with your task"}, {"line":26,"title":"How the specification connects to code"}, {"line":46,"title":"Models and composition profiles"}, - {"line":95,"title":"Generating and replaying behavior"}, - {"line":182,"title":"Evidence and scope"} + {"line":99,"title":"Generating and replaying behavior"}, + {"line":193,"title":"Shared verification and replay rules"}, + {"line":209,"title":"Evidence and scope"} ], "review": { "kind": "tooling-guide", @@ -1363,16 +1364,16 @@ }, { "path": "formal/SEMANTIC-COVERAGE.md", - "sha256": "1d026f63937685a63f9f7f90339aa1d88696046a56083dc16a82b19a64adb807", + "sha256": "788ccd40ccc5f3e3462ea25fab7ba577d84da0f52a0f6d5a1d1a63b7a7a18a7f", "entries": [ {"line":1,"title":"Measuring semantic coverage"}, {"line":5,"title":"Evidence inventory"}, {"line":65,"title":"Model assurance"}, - {"line":128,"title":"Generated boundary evidence"}, - {"line":136,"title":"Behavioral mutation comparison"}, - {"line":150,"title":"Recorded measurements"}, - {"line":156,"title":"Reproduction and CI"}, - {"line":198,"title":"Model properties and cross-language execution"} + {"line":131,"title":"Generated boundary evidence"}, + {"line":139,"title":"Behavioral mutation comparison"}, + {"line":153,"title":"Recorded measurements"}, + {"line":159,"title":"Reproduction and CI"}, + {"line":219,"title":"Model properties and cross-language execution"} ], "review": { "kind": "coverage-guide", @@ -1486,7 +1487,7 @@ }, { "path": "formal/TEST-MAP.md", - "sha256": "5e900759ff92b754130a856e49b5e977b97a359bce6e725957cd40118fe167c7", + "sha256": "ae631913cf2665ef0971b56a40d400905268d35e91e92724ef6f4fe32b573b77", "entries": [ {"line":1,"title":"Formal contract and implementation evidence map"}, {"line":18,"title":"Behavioral map"}, @@ -1502,19 +1503,19 @@ }, { "path": "formal/VALIDATION.md", - "sha256": "b7211947e99a84098ae07879c8be22862c00bcb83f91d0ef0f0ee3e4143bcf6c", + "sha256": "7747e7e1668b8b9557306cf7724182c0995b84b0044c3434501ae0a9e37d3f21", "entries": [ {"line":1,"title":"Validation and evidence"}, {"line":7,"title":"Choosing a run"}, - {"line":72,"title":"Reading a completion report"}, - {"line":90,"title":"Mutation evidence"}, - {"line":171,"title":"Exploratory runs"}, - {"line":219,"title":"A retained sampling regression"}, - {"line":256,"title":"Historical results"} + {"line":76,"title":"Reading a completion report"}, + {"line":94,"title":"Mutation evidence"}, + {"line":205,"title":"Exploratory runs"}, + {"line":256,"title":"A retained sampling regression"}, + {"line":293,"title":"Historical results"} ], "review": { "kind": "tooling-guide", - "scope": "Operating guide for local/hosted validation, exact completion report identity, mutation interpretation, separate alternate-seed diagnostics and versioned historical results. Fast PR checks do not enforce the stated full-validation policy." + "scope": "Operating guide for local/hosted validation, exact completion report identity, mutation interpretation, separate alternate-seed diagnostics and versioned historical results. Fast PR checks do not enforce the stated full-validation policy. Differential profiles are balanced by advisory replay estimates, with per-batch progress and unchanged comparison coverage." } }, { diff --git a/formal/validation.mjs b/formal/validation.mjs index e4700bdd..12714603 100644 --- a/formal/validation.mjs +++ b/formal/validation.mjs @@ -28,12 +28,12 @@ export const targetDescriptions = { 'formal-go': 'Complete prepared Go replay of the generated corpus with race detection', 'fixtures-check': 'Recompute every committed model-derived artifact with pinned Quint', 'kernel-fixtures': 'Typecheck the kernel library fixtures (test/fixtures/kernel) and run every run they declare', - differential: 'Check the composition lint baseline, then replay every composed profile against its reference corpus (merge base with DIFFERENTIAL_REFERENCE, default origin/main) in both directions (DIFFERENTIAL_SHARD=/ replays one round-robin shard of the composed profiles, as the hosted lane does with four)', + differential: 'Check the composition lint baseline, then replay every composed profile against its reference corpus (merge base with DIFFERENTIAL_REFERENCE, default origin/main) in both directions (DIFFERENTIAL_SHARD=/ replays one shard balanced by estimated profile replay time, as the hosted lane does with four)', explore: 'Explore a new recorded seed and replay both ports in an isolated source snapshot', 'model-check': 'Symbolically verify the scheduled finite rules with pinned Quint/Apalache (Java 21)', - mutations: 'Measure TypeScript and Go semantic mutations over the generated corpus and shared witness evidence', - 'mutations-ts': 'Measure TypeScript semantic mutations over the generated corpus (MUTATION_SHARD=/ measures one shard; MUTATION_ONLY=, measures the named mutants into a partial report)', - 'mutations-go': 'Measure Go semantic mutations over the generated corpus and shared witness evidence (MUTATION_SHARD=/ measures one shard; MUTATION_ONLY=, measures the named mutants into a partial report)', + mutations: 'Measure TypeScript and Go semantic mutations over generated histories, real Redis vectors and shared witnesses (Docker)', + 'mutations-ts': 'Measure TypeScript semantic mutations over generated histories and real Redis vectors; requires Docker (MUTATION_SHARD=/ measures one shard; MUTATION_ONLY=, measures the named mutants into a partial report)', + 'mutations-go': 'Measure Go semantic mutations over generated histories, real Redis vectors and shared witnesses; requires Docker (MUTATION_SHARD=/ measures one shard; MUTATION_ONLY=, measures the named mutants into a partial report)', 'mutations-merge-ts': 'Merge TypeScript mutation shards into the complete report; refuses inconsistent or missing shards', 'mutations-merge-go': 'Merge Go mutation shards into the complete report; refuses inconsistent or missing shards', integration: 'Run real TypeScript and Go Redis/Valkey/Cluster integration checks', @@ -76,7 +76,7 @@ export function mutationSelectionArguments(target, environment = process.env) { } // DIFFERENTIAL_SHARD=/ narrows make differential to one -// round-robin shard of the composed profiles (formal/differential.mjs +// shard balanced by estimated profile replay time (formal/differential.mjs // shardProfiles), the way the hosted lane's four-shard matrix does; every shard // keeps the lint baseline and the kernel fixtures, so each is self-contained. // No aggregate includes the differential, so other targets ignore the variable. @@ -142,8 +142,12 @@ export function validationPlan(target, { directory = root, environment = process // The complete replay outlives Go's default 10-minute test timeout on a slow // runner (run 34667733523 was killed at 10m0s); bound it explicitly, under // the go-parity job budget. The smoke run keeps the default. + // Opt-in native workers need inputs from the mutation/vector coordinator. + // Exclude exactly those roots from the full corpus command so the completed + // report can keep rejecting every actual skip, including required cases. const nativeGo = full => ({ ...go(full ? 'Replay complete Go corpus with race detection' : 'Run Go default tests with race detection', - 'test', '-race', '-count=1', ...(full ? ['-json', '-timeout=35m'] : []), './...'), + 'test', '-race', '-count=1', ...(full ? ['-json', '-timeout=35m', + '-skip', '^(TestGeneratedInvalidationVectors|TestVectorBoundaryDriver)$'] : []), './...'), ...(full ? { env: { ...replayEnv, DIALCACHE_WITNESS_EVIDENCE_DIR: witnessDirectory }, stdoutFile: '.formal-traces/go-replay.jsonl' } : {}) }); const node22 = floorExecutable(environment, runnerNode, nodeVersion) ?? ''; const selection = mutationSelectionArguments(target, environment); @@ -166,7 +170,8 @@ export function validationPlan(target, { directory = root, environment = process // The model check is evidence about the Quint models (typechecks, bounded // runs, regressions and the mutation challenges). Nothing downstream reads // its output, so it is a sibling of generation rather than a prefix of it. - 'formal-check': [node('Check every scheduled Quint model', 'formal/run-models.mjs', 'check'), lintBaseline, kernelFixtures], + 'formal-check': [node('Check every scheduled Quint model', 'formal/run-models.mjs', 'check'), + node('Measure every pinned model fault', 'formal/check-model-properties.mjs'), lintBaseline, kernelFixtures], // Generation is the single shared producer: the corpus, wire artifacts and // witness evidence depend only on the models. Both ports' replays and both // mutation measurements read that output and can run in parallel off it. @@ -224,7 +229,7 @@ export function checkPrerequisites(target, { directory = root, environment = pro try { probe('tar', ['--version'], { directory, environment }); } catch (error) { throw new Error(`Symbolic checking requires tar to unpack the pinned Apalache archive. ${error.message}`); } } - if (targets.some(name => name.startsWith('integration-'))) probe('docker', ['info', '--format', '{{.ServerVersion}}'], { directory, environment }); + if (targets.some(name => name.startsWith('integration-') || ['mutations-ts', 'mutations-go'].includes(name))) probe('docker', ['info', '--format', '{{.ServerVersion}}'], { directory, environment }); if (targets.includes('package-floor')) { const executable = floorExecutable(environment, runnerNode, nodeVersion); if (!executable || !isAbsolute(executable)) throw new Error('Node 22.15.0 is required for package-floor. Set NODE22_BIN=/absolute/path/to/node22/bin/node (or run make package-floor under exact Node 22.15.0). No runtime is downloaded automatically.'); @@ -377,7 +382,7 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) { console.log('formal-check is the Quint evidence lane (models, regressions, challenges); the port and mutation lanes read only the formal-generate output and do not wait for it.'); console.log('Sharded mutation runs: MUTATION_SHARD=/ make mutations-ts for every index, matching the workflow matrix, on any machines with the same corpus, then make mutations-merge-ts; the merged report is the only complete evidence.'); console.log('One mutant locally: MUTATION_ONLY=M14,M15 make mutations-ts (or mutations-go) writes a partial report under partial/ and leaves the complete report alone.'); - console.log('Sharded differential runs: DIFFERENTIAL_SHARD=/ make differential replays one round-robin shard of the composed profiles, matching the pull request workflow\'s four-shard matrix; every shard checks the lint baseline and the kernel fixtures.'); + console.log('Sharded differential runs: DIFFERENTIAL_SHARD=/ make differential replays one shard balanced by estimated profile replay time, matching the pull request workflow\'s four-shard matrix; every shard checks the lint baseline and the kernel fixtures.'); console.log('Full local CI: make ci NODE22_BIN=/absolute/path/to/node22/bin/node (exact 22.15.0).'); } else { try { await runTarget(target); } diff --git a/formal/vector-evidence.d.mts b/formal/vector-evidence.d.mts new file mode 100644 index 00000000..94a8dfa3 --- /dev/null +++ b/formal/vector-evidence.d.mts @@ -0,0 +1,20 @@ +export interface VectorSample { + row: string; + request: { operation: string; input: Record }; + expected: Record; + inputSha256: string; +} +export interface VectorEvidence { + artifact: string; + group: string; + rows: Record<'typescript' | 'go', string>; + fields: string[]; + relation: string; + artifactSha256: string; + samples: Record<'typescript' | 'go', VectorSample>; +} +export function validVectorResult(operation: string, value: unknown): boolean; +export function resolveVectorEvidence(written: unknown, model: { vectorExport?: { artifact: string } }, readSource: (path: string) => string): VectorEvidence; +export function assessVectorBoundary(evidence: { vector: VectorEvidence; history: string; fields: string[] }, recording?: unknown): { + state: string; completed: boolean; lastStep: number; reason?: string; divergences: Array<{ step: number; action: string; paths: string[] }>; +}; diff --git a/formal/vector-evidence.mjs b/formal/vector-evidence.mjs new file mode 100644 index 00000000..0458a7a5 --- /dev/null +++ b/formal/vector-evidence.mjs @@ -0,0 +1,113 @@ +import { createHash } from 'node:crypto'; +import { isDeepStrictEqual } from 'node:util'; +import { countingPaths, diffPaths } from './replay/divergence.mjs'; + +const hash = value => createHash('sha256').update(value).digest('hex'); +const object = value => value !== null && typeof value === 'object' && !Array.isArray(value); +const exact = (value, keys) => object(value) && isDeepStrictEqual(Object.keys(value).sort(), [...keys].sort()); +const hex = value => typeof value === 'string' && /^(?:[a-f0-9]{2})*$/.test(value); +const natural = value => Number.isSafeInteger(value) && value >= 0; +const readOutcomes = new Set(['passthrough', 'decompressed', 'fallback_raw', 'read_over_limit']); +const writeOutcomes = new Set(['compressed', 'below_threshold', 'not_smaller', 'write_over_limit']); +const missReasons = new Set(['value_absent', 'expired', 'watermark_fenced', 'unclassified']); +const fieldsByGroup = { + vectors: ['outcome', 'kind', 'content', 'ttlMs'], + keyVectors: ['kind', 'logicalKey', 'valueKey', 'watermarkKey'], + trackedDecodeVectors: ['kind', 'reason', 'observedWatermarkMs', 'createdAtMs', 'payloadType', 'payloadHex', 'payloadUtf8'], + envelopeVectors: ['decodedHex', 'outcome'], + compressionWriteVectors: ['outcome', 'storedBytes', 'marker'], +}; + +// These adapters split a generated vector into external input and expected +// public output. Only the input crosses into either native process. +function sample(group, row, port) { + switch (group) { + case 'vectors': return { operation: 'invalidation', + input: { existing: row.existing, futureBufferMs: row.futureBufferMs, invalidatedAtMs: row.invalidatedAtMs }, + expected: { outcome: row.expected.error ? 'rejected' : 'success', kind: row.expected.state.kind, + content: row.expected.state.kind === 'string' ? row.expected.state.value : row.expected.state.kind === 'list' ? row.expected.state.values : [], + ttlMs: row.expected.state.ttlMs, elapsedMs: 0 } }; + case 'keyVectors': return { operation: 'key', input: row.input, + expected: { kind: 'key', logicalKey: row.logicalKey, valueKey: row.valueKey, watermarkKey: row.watermarkKey } }; + case 'trackedDecodeVectors': return { operation: 'trackedDecode', + input: { frameHex: row.frameHex, watermarkUtf8: row.watermarkUtf8 ?? null }, expected: row.expected }; + case 'envelopeVectors': return { operation: 'envelope', input: { inputHex: row.inputHex }, + expected: { decodedHex: row.decodedHex, outcome: row.outcome } }; + case 'compressionWriteVectors': return { operation: 'compression', + input: { payloadType: row.payloadType, ...(row.payloadType === 'binary' ? { payloadHex: row.payloadHex } : { payloadUtf8: row.payloadUtf8 }), + thresholdBytes: row.thresholdBytes, maxDecompressedBytes: row.maxDecompressedBytes }, expected: row.expectedByBinding?.[port] }; + default: throw new Error(`Unsupported vector boundary group: ${group}`); + } +} + +export function validVectorResult(operation, value) { + if (!object(value)) return false; + if (operation === 'invalidation') return exact(value, ['outcome', 'kind', 'content', 'ttlMs', 'elapsedMs']) + && ['success', 'rejected'].includes(value.outcome) && ['absent', 'string', 'list'].includes(value.kind) + && (value.kind === 'string' ? typeof value.content === 'string' : Array.isArray(value.content) + && value.content.every(item => typeof item === 'string') && (value.kind === 'list' ? value.content.length > 0 : value.content.length === 0)) + && Number.isSafeInteger(value.ttlMs) && value.ttlMs >= -2 && natural(value.elapsedMs); + if (operation === 'key') return value.kind === 'key_error' ? exact(value, ['kind']) : + exact(value, ['kind', 'logicalKey', 'valueKey', 'watermarkKey']) && value.kind === 'key' + && typeof value.logicalKey === 'string' && typeof value.valueKey === 'string' + && (value.watermarkKey === null || typeof value.watermarkKey === 'string'); + if (operation === 'envelope') return exact(value, ['decodedHex', 'outcome']) && hex(value.decodedHex) && readOutcomes.has(value.outcome); + if (operation === 'compression') return exact(value, ['outcome', 'storedBytes', 'marker']) && writeOutcomes.has(value.outcome) + && natural(value.storedBytes) && [-1, 1, 2].includes(value.marker); + if (operation !== 'trackedDecode') return false; + if (value.kind === 'payload_encoding_error') return exact(value, ['kind']); + if (value.kind === 'miss') return exact(value, ['kind', 'reason', ...(value.observedWatermarkMs === undefined ? [] : ['observedWatermarkMs'])]) + && missReasons.has(value.reason) && (value.observedWatermarkMs === undefined || natural(value.observedWatermarkMs)); + const binary = value.payloadType === 'binary'; + return value.kind === 'hit' && exact(value, ['kind', 'createdAtMs', 'payloadType', binary ? 'payloadHex' : 'payloadUtf8']) + && natural(value.createdAtMs) && (binary ? hex(value.payloadHex) : value.payloadType === 'string' && typeof value.payloadUtf8 === 'string'); +} + +export function resolveVectorEvidence(written, model, readSource) { + if (!exact(written, ['artifact', 'group', 'rows', 'fields', 'relation']) || written.artifact !== model.vectorExport?.artifact + || !Object.hasOwn(fieldsByGroup, written.group) || !exact(written.rows, ['typescript', 'go']) + || typeof written.relation !== 'string' || written.relation.trim().length < 30 + || !Array.isArray(written.fields) || !written.fields.length || new Set(written.fields).size !== written.fields.length + || written.fields.some(field => !fieldsByGroup[written.group].includes(field))) throw new Error('Invalid vector boundary declaration'); + const text = readSource(written.artifact), corpus = JSON.parse(text), rows = corpus[written.group]; + if (!Array.isArray(rows)) throw new Error('Missing vector boundary group'); + const samples = {}; + for (const port of ['typescript', 'go']) { + const matches = rows.filter(row => row.name === written.rows[port]); + if (typeof written.rows[port] !== 'string' || matches.length !== 1) throw new Error(`Vector boundary must select exactly one ${port} row`); + const selected = sample(written.group, matches[0], port); + if (!validVectorResult(selected.operation, selected.expected) || written.fields.some(field => !Object.hasOwn(selected.expected, field))) + throw new Error(`Malformed ${port} vector expectation or absent assertion field`); + const request = { operation: selected.operation, input: selected.input }; + samples[port] = { row: matches[0].name, request, expected: selected.expected, inputSha256: hash(JSON.stringify(request)) }; + } + return { ...written, artifactSha256: hash(text), samples }; +} + +// Recompute the comparison from the native value, not a test's failure count +// or the verdict in an older report. A missing, malformed or stale result +// remains an infrastructure failure and cannot earn detection credit. +export function assessVectorBoundary(evidence, recording) { + const base = { ...evidence, via: 'vector', completed: recording?.completed === true, lastStep: recording?.lastStep ?? -1, + ...(recording?.vectorResult ? { vectorResult: recording.vectorResult } : {}) }; + const fail = reason => ({ ...base, state: 'unreached', reason, divergences: [] }); + if (!recording || recording.completed !== true || recording.error !== undefined || recording.lastStep !== 0) + return fail(recording?.error ?? 'No completed native vector result'); + const result = recording.vectorResult, selected = evidence.vector.samples[result?.port]; + if (!selected || result.history !== evidence.history || result.row !== selected.row + || result.artifactSha256 !== evidence.vector.artifactSha256 || result.inputSha256 !== selected.inputSha256) + return fail('Vector identity or source fingerprint differs from the declaration'); + if (!validVectorResult(selected.request.operation, result.actual)) return fail('Malformed native vector result'); + const expected = { ...selected.expected }, actual = { ...result.actual }; + if (selected.request.operation === 'invalidation') { + // Redis 6.2 advances TTL time even inside an atomic Lua execution. Only + // measured server elapsed time may explain a smaller positive TTL. + delete expected.elapsedMs; delete actual.elapsedMs; + if (expected.ttlMs >= 0 && actual.ttlMs >= Math.max(0, expected.ttlMs - result.actual.elapsedMs) + && actual.ttlMs <= expected.ttlMs) actual.ttlMs = expected.ttlMs; + } + const paths = diffPaths(expected, actual); + const matched = countingPaths(evidence.fields, paths, []); + return { ...base, state: matched.length ? 'confirmed' : paths.length ? 'side-effect-only' : 'not-divergent', + divergences: paths.length ? [{ step: 0, action: selected.request.operation, paths }] : [], ...(matched.length ? { matched } : {}) }; +} diff --git a/formal/witness-baseline.json b/formal/witness-baseline.json index c2810568..34892e8c 100644 --- a/formal/witness-baseline.json +++ b/formal/witness-baseline.json @@ -7,7 +7,7 @@ "profiles": { "admission": { "sampledHistories": 128, - "corpusSha256": "706a2b21115095297825cc537e7ef4459cbbb48887de3f39c07b53249dc78619", + "corpusSha256": "27d0a291fdb2f93e01a01a0dc202ec951ab70006e5bebc377c1c2bd0a83db0de", "labels": { "outcome:match": 57, "outcome:mismatch": 35, @@ -26,111 +26,168 @@ "decode-timeout-keeps-slot": 7, "confirmation-timeout-keeps-slot": 7, "unselected-hit-skips-job": 99, - "uncoalesced-hit-overlap": 55 + "uncoalesced-hit-overlap": 55, + "later-served-shadow-keeps-own-job-budget": 0 + } + }, + "dark-layers": { + "sampledHistories": 256, + "corpusSha256": "e30409007315d928e5ff104c3c11868f7b9664a75a2e1ca7cb90389ce13fa68d", + "labels": { + "inspection-counts-process-followers-and-oldest": 0, + "inspection-isolates-instances-and-wall-shifts": 0, + "inspection-excludes-request-and-uncoalesced": 0, + "inspection-clears-timeout-before-raw-settlement": 0, + "inspection-excludes-unfinished-shadow": 0, + "rejected-dark-source-seeds-no-layer": 0, + "uncoalesced-publication-keeps-dark-leader": 0, + "held-dark-fill-keeps-captured-retention": 0, + "dark-fill-at-watermark-is-fenced": 0, + "dark-frame-at-watermark-is-fenced": 0, + "held-dark-fill-rechecks-fence-after-rollback": 0, + "stale-visible-dark-c0-fills-unfenced": 0, + "undecodable-held-dark-c0-is-not-repaired": 0, + "unsupported-encoding-ends-dark-read": 0, + "dark-source-deadline-attributed-to-local": 0, + "dark-flights-are-instance-local": 0, + "closed-scope-bypasses-dark-work": 0, + "held-dark-capacity-is-instance-local": 0, + "held-dark-dump-error-preserves-source": 0, + "dark-source-local-publication-stops-job": 0, + "held-dark-c0-age-at-verdict": 0, + "future-dark-c0-reports-offset-and-fills": 0, + "action:beginCall": 256, + "action:releaseRead": 254, + "action:releaseLoad": 35, + "action:resolveLoader": 255, + "action:rejectLoader": 254, + "action:releaseDump": 132, + "action:releaseWrite": 91, + "action:advance": 243, + "action:policy": 246, + "action:seed": 248, + "action:invalidate": 251, + "action:closeScope": 240, + "action:rollbackWall": 256, + "action:readFault": 230, + "action:loadFault": 234, + "action:dumpFault": 232, + "action:writeFault": 230, + "local-entry-expires-at-its-ttl": 27, + "request-follower-joins-leader": 71, + "transient-request-dark-error-uses-request-layer": 0, + "later-dark-source-keeps-own-budget": 0, + "timed-out-dark-read-retains-capacity-until-raw-release": 0, + "timed-out-dark-decode-retains-capacity-until-raw-release": 0, + "timed-out-dark-confirmation-retains-capacity-until-raw-release": 0, + "timed-out-dark-dump-retains-capacity-until-raw-release": 0, + "timed-out-dark-write-retains-capacity-until-raw-release": 0 } }, "effects": { "sampledHistories": 512, - "corpusSha256": "2349717b738cc931d9ae740e2e5c35f06f63e3a73fffb4533d018755558b7145", + "corpusSha256": "e0456e899a35bad39f306e4c12f1f45b9bb97f59736c11d417130599e228795d", "labels": { + "adapter-frame-last-fresh-millisecond-hits": 0, + "adapter-frame-exact-expiry-refills-and-reuses": 0, "event:request": 512, "event:disabled": 512, - "event:miss": 419, - "event:error": 512, - "event:coalesced": 512, - "event:invalidation": 502, + "event:miss": 449, + "event:error": 510, + "event:coalesced": 510, + "event:invalidation": 486, "event:get": 512, "event:fallback": 512, - "event:serialization": 341, - "event:futureOffset": 28, - "event:size": 117, - "event:storedSize": 117, - "event:writeDispatch": 114, - "reply:1": 8, - "reply:2": 9, - "reply:3": 2, - "reply:4": 6, - "reply:5": 5, - "reply:6": 8, - "reply:7": 8, - "reply:8": 7, - "reply:9": 5, - "reply:10": 6, - "reply:11": 8, - "reply:12": 3, - "reply:13": 4, - "reply:14": 6, - "reply:15": 4, - "reply:16": 6, - "error:cache_read": 457, - "error:cache_read_timeout": 160, - "error:serialization_load": 161, - "error:serialization_dump": 111, - "error:cache_write": 58, - "error:fallback": 480, - "miss:value_absent": 229, - "miss:watermark_fenced": 102, - "miss:unclassified": 222, - "miss:expired": 4, - "duration:load": 67, - "duration:dump": 52, + "event:serialization": 353, + "event:futureOffset": 20, + "event:size": 127, + "event:storedSize": 127, + "event:writeDispatch": 125, + "reply:1": 5, + "reply:2": 8, + "reply:3": 5, + "reply:4": 3, + "reply:5": 8, + "reply:6": 7, + "reply:7": 7, + "reply:8": 5, + "reply:9": 3, + "reply:10": 7, + "reply:11": 7, + "reply:12": 5, + "reply:13": 6, + "reply:14": 8, + "reply:15": 8, + "reply:16": 4, + "error:cache_read": 454, + "error:cache_read_timeout": 138, + "error:serialization_load": 157, + "error:serialization_dump": 140, + "error:cache_write": 73, + "error:fallback": 489, + "miss:value_absent": 286, + "miss:watermark_fenced": 80, + "miss:unclassified": 204, + "miss:expired": 58, + "duration:load": 56, + "duration:dump": 59, "untracked-demotes-fenced-reply": 1, - "normalized-fence-blocks-publication": 3, - "abandoned-overlap": 22, - "publication-after-deadline": 25, + "normalized-fence-blocks-publication": 6, + "abandoned-overlap": 29, + "publication-after-deadline": 24, "delayed-fenced-write": 1, - "late-settlement": 154, - "read-timeout-starts-source": 160, - "read-late-settlement": 79, - "abandoned-read-settles": 106, - "decode-outlives-deadline": 33, - "acquired-hit-survives-invalidation": 16, - "failed-read-no-refill": 327, - "failed-decode-refills": 68, - "dump-failure-preserves-value": 111, - "write-failure-preserves-value": 58, - "serialize-outlives-deadline": 26, - "dump-rechecks-fence-after-rollback": 5, + "late-settlement": 139, + "read-timeout-starts-source": 138, + "read-late-settlement": 63, + "abandoned-read-settles": 93, + "decode-outlives-deadline": 30, + "acquired-hit-survives-invalidation": 11, + "failed-read-no-refill": 308, + "failed-decode-refills": 69, + "dump-failure-preserves-value": 140, + "write-failure-preserves-value": 73, + "serialize-outlives-deadline": 25, + "dump-rechecks-fence-after-rollback": 3, "observer-failure-hit": 0, "observer-failure-publication": 0, "observer-failure-source-error": 0, - "fixture:0": 79, - "fixture:1": 85, - "fixture:2": 97, - "fixture:3": 92, - "fixture:4": 78, - "fixture:5": 81, - "read-budget:10": 274, - "read-budget:20": 113, - "read-budget:30": 112, + "fixture:0": 84, + "fixture:1": 88, + "fixture:2": 82, + "fixture:3": 73, + "fixture:4": 87, + "fixture:5": 98, + "read-budget:10": 275, + "read-budget:20": 105, + "read-budget:30": 86, "read-budget:50": 105, - "follower-keeps-read-budget": 9, - "initial-budget:0": 77, - "initial-budget:1": 80, - "initial-budget:2": 93, - "initial-budget:3": 88, - "initial-budget:4": 75, - "late-resolve": 83, - "late-reject": 83, - "normalized-reply-allows-refill:1": 5, + "follower-keeps-read-budget": 10, + "initial-budget:0": 83, + "initial-budget:1": 85, + "initial-budget:2": 80, + "initial-budget:3": 72, + "initial-budget:4": 86, + "late-resolve": 75, + "late-reject": 71, + "normalized-reply-allows-refill:1": 4, "normalized-reply-allows-refill:2": 0, - "normalized-reply-allows-refill:4": 3, - "normalized-reply-allows-refill:8": 2, - "normalized-reply-allows-refill:9": 3, - "normalized-reply-allows-refill:10": 3, + "normalized-reply-allows-refill:4": 0, + "normalized-reply-allows-refill:8": 3, + "normalized-reply-allows-refill:9": 1, + "normalized-reply-allows-refill:10": 1, "normalized-reply-allows-refill:11": 2, - "normalized-reply-fences-refill:12": 1, - "normalized-reply-allows-refill:13": 1, - "successful-read-has-no-late-cancel": 33, + "normalized-reply-fences-refill:12": 2, + "normalized-reply-allows-refill:13": 3, + "successful-read-has-no-late-cancel": 30, "write-stamp-after-serialization": 0, - "late-rejection-does-not-repeat-error": 112, + "late-rejection-does-not-repeat-error": 96, "future-offset-observing-layer-positive-seconds": 0, - "shared-failure-preserves-leader-and-follower-trail": 2 + "shared-failure-preserves-leader-and-follower-trail": 0 } }, "independent": { "sampledHistories": 512, - "corpusSha256": "57134d035a0f317364338513d107f10d26765ca05b33f3af6b58c2836c236b6e", + "corpusSha256": "846dc3e49b3230a65e0c20be16aa80fba5fd638400a58b4142ac07d33f1ba0c3", "labels": { "independent-source-error-identities": 262, "independent-read-overlap": 465, @@ -187,7 +244,7 @@ }, "local-clock": { "sampledHistories": 128, - "corpusSha256": "dd5c3f7d92942a834c84ec7b4e2602e4208cff99b4e94c23f8bd3e259df439ff", + "corpusSha256": "b4ef22b82f6c00adb35f09ac54abb1db81ee4dba3b18f24c6f80295544759590", "labels": { "fractional-insertion-expiry": 0, "shared-instance-grid": 0 @@ -407,7 +464,7 @@ }, "shadow": { "sampledHistories": 1024, - "corpusSha256": "c0dceb6430cd99bcb85948585856e5cb5fed5fa88771a525697e8304511a0038", + "corpusSha256": "d2027e4824ab64f7c15c9d9aceee09d3924476f932ed2aeef440719ef831eca5", "labels": { "outcome:match": 53, "outcome:mismatch": 54, @@ -490,7 +547,7 @@ }, "shadow-layers": { "sampledHistories": 256, - "corpusSha256": "cf714d5332969b514f383c69ac1f2d5849f26058d6e8885a5a7e853c6b3394ab", + "corpusSha256": "6afffa10eb14b0fe44b31ed244d4c16a8ae448fc1999e8f9e20759043d8e814d", "labels": { "dark-present-c0-never-publishes-local": 0, "dark-local-source-publication-stops-later-shadow": 0, @@ -516,9 +573,21 @@ "ordinary-serving-miss-has-no-diagnostic-source": 0 } }, + "shadow-read-deadlines": { + "sampledHistories": 128, + "corpusSha256": "ea77c01153937b49307768d155de915cc8bc6fb7194ecc3df7fef34f0040a624", + "labels": { + "c0-read-timeout-retains-raw-capacity": 0, + "c1-read-timeout-retains-raw-capacity": 0, + "job-then-read-timeout-has-one-verdict": 0, + "confirmation-keeps-captured-read-budget": 0, + "confirmation-read-budget-starts-at-dispatch": 0, + "timely-shadow-read-cancels-deadline-and-fills": 0 + } + }, "source-budgets": { "sampledHistories": 256, - "corpusSha256": "66f0cf911392eb56088394edb53eb471c86d55378ec8ca0670214d3cf079b064", + "corpusSha256": "90b412b41f2fa15c6c78b06cef9f9f812bd5310dcc38b18b89dff7bff5081929", "labels": { "outside-calls-skip-source-deadlines-sharing-and-publication": 0, "default-source-budget-expires-at-sixty-seconds": 0, diff --git a/go/behavior_driver_test.go b/go/behavior_driver_test.go index 06098573..4adcca6e 100644 --- a/go/behavior_driver_test.go +++ b/go/behavior_driver_test.go @@ -842,6 +842,18 @@ func (d *behaviorDriver) apply(input obj) error { } } d.append("maintenance", status) + case "inspectCoalescing": + instance := bs(input["instance"]) + if instance == "" { + instance = "default" + } + state := d.instance(instance).GetCoalescingState().Process + var age any + if state.ActiveLeaders > 0 { + age = float64(state.OldestLeaderAge) / float64(time.Millisecond) + } + d.record("coalescingState", obj{"instance": instance, "activeLeaders": state.ActiveLeaders, + "activeFollowers": state.ActiveFollowers, "oldestLeaderAgeMs": age}) case "observeMarker": identity := d.identity(bs(input["key"]), "") identity.Tracked = true diff --git a/go/behavior_registry_test.go b/go/behavior_registry_test.go index d5aa9a6f..4a3ff9bf 100644 --- a/go/behavior_registry_test.go +++ b/go/behavior_registry_test.go @@ -6,7 +6,7 @@ import ( "testing" ) -var behaviorProfileVersions = map[string]int64{"recovery-read": 1, "local-failure": 1, "runtime-boundaries": 1, "shadow-layers": 1, "local-clock": 1, "source-budgets": 1, "effects": 2, "scope": 2, "policy": 3, "layers": 2, "recovery": 1, "independent": 2, "shadow": 3, "admission": 1} +var behaviorProfileVersions = map[string]int64{"shadow-read-deadlines": 1, "dark-layers": 2, "recovery-read": 1, "local-failure": 1, "runtime-boundaries": 1, "shadow-layers": 1, "local-clock": 1, "source-budgets": 1, "effects": 3, "scope": 2, "policy": 3, "layers": 2, "recovery": 1, "independent": 2, "shadow": 3, "admission": 1} func validateBehaviorProfileRegistry(raw []byte, name string, version int64) error { decoded, err := behaviorJSON(raw) diff --git a/go/causal_contract_test.go b/go/causal_contract_test.go index c7ed90e6..62696eb6 100644 --- a/go/causal_contract_test.go +++ b/go/causal_contract_test.go @@ -1,6 +1,7 @@ package dialcache import ( + "errors" "fmt" "strings" "testing" @@ -17,11 +18,15 @@ type behaviorCausalEvent struct { outcome string } +type behaviorPropertyAssertion struct{ message string } + +func (e *behaviorPropertyAssertion) Error() string { return e.message } + // The mutation runner accepts only explicit property assertions as causal // evidence. Invalid monitor input and missing ownership remain ordinary errors. func behaviorPropertyFailure(rule, condition string, event obj) error { event["condition"] = condition - return fmt.Errorf("CAUSAL_PROPERTY_FAILURE rule=%s event=%s", rule, bjson(event)) + return &behaviorPropertyAssertion{fmt.Sprintf("CAUSAL_PROPERTY_FAILURE rule=%s event=%s", rule, bjson(event))} } // This necessary C25/C26 condition links actual external invocation contexts, @@ -36,6 +41,7 @@ func assertBehaviorCausality(history []behaviorCausalEvent) error { sources := map[int]*source{} owners := map[int]bool{} previous := int64(-1) + var assertion error for index, e := range history { fail := func(reason string) error { return fmt.Errorf("C25/C26 causal event %d: %s", index, reason) } if e.at < previous { @@ -65,20 +71,27 @@ func assertBehaviorCausality(history []behaviorCausalEvent) error { return fail("write has no observed source ownership") } if s.owner != e.owner { - return behaviorPropertyFailure("C26", "write belongs to a different invocation's source", obj{"event": e.kind, "index": index, "atMs": e.at, "source": e.id, "owner": e.owner, "sourceOwner": s.owner}) + if assertion == nil { + assertion = behaviorPropertyFailure("C26", "write belongs to a different invocation's source", obj{"event": e.kind, "index": index, "atMs": e.at, "source": e.id, "owner": e.owner, "sourceOwner": s.owner}) + } } if s.outcome != "resolve" { - return behaviorPropertyFailure("C26", "write requires that exact source's successful settlement", obj{"event": e.kind, "index": index, "atMs": e.at, "source": e.id, "owner": e.owner, "outcome": s.outcome}) + if assertion == nil { + assertion = behaviorPropertyFailure("C26", "write requires that exact source's successful settlement", obj{"event": e.kind, "index": index, "atMs": e.at, "source": e.id, "owner": e.owner, "outcome": s.outcome}) + } } if s.budget >= 0 && s.settled-s.started >= s.budget { - return behaviorPropertyFailure("C25", "late raw settlement cannot authorize publication", obj{"event": e.kind, "index": index, "atMs": e.at, "source": e.id, "owner": e.owner, "startedAtMs": s.started, "settledAtMs": s.settled, "budgetMs": s.budget}) + if assertion == nil { + assertion = behaviorPropertyFailure("C25", "late raw settlement cannot authorize publication", obj{"event": e.kind, "index": index, "atMs": e.at, "source": e.id, "owner": e.owner, "startedAtMs": s.started, "settledAtMs": s.settled, "budgetMs": s.budget}) + } } default: return fail("unknown causal event") } } - return nil + return assertion } + func (d *behaviorDriver) assertPublicationCausality() error { d.mu.Lock() events := append([]behaviorCausalEvent(nil), d.causal...) @@ -141,3 +154,30 @@ func TestCausalEvidenceExcludesMalformedMonitorInputs(t *testing.T) { t.Fatalf("deadline violation lacks structured semantic evidence: %v", err) } } + +func TestBoundaryRecordingContinuesOnlyCompletePropertyChecks(t *testing.T) { + d := &behaviorDriver{history: []behaviorHistory{{event: "sourceStart", id: 0, at: 0}, {event: "sourceSettlement", id: 0, at: 10, outcome: "resolve"}, {event: "fallbackCompletion", at: 10, duration: 10}}} + property := d.assertEffectsHistory() + t.Setenv("DIALCACHE_REPLAY_DIVERGENCES", "") + if replayRecordingError(property) == nil { + t.Fatal("ordinary replay ignored a semantic assertion") + } + t.Setenv("DIALCACHE_REPLAY_DIVERGENCES", "recording.jsonl") + if replayRecordingError(property) != nil { + t.Fatal("recording stopped before comparing the public observation") + } + for _, malformed := range []error{errors.New("driver command failed"), fmt.Errorf("CAUSAL_PROPERTY_FAILURE rule=C25 event={}")} { + if replayRecordingError(malformed) == nil { + t.Fatal("untagged driver failure was ignored") + } + } + // An earlier semantic failure cannot conceal an unvalidated later event. + d.history = append(d.history, behaviorHistory{event: "sourceSettlement", id: 99, at: 10, outcome: "resolve"}) + if replayRecordingError(d.assertEffectsHistory()) == nil { + t.Fatal("earlier semantic failure concealed malformed effects history") + } + history := []behaviorCausalEvent{{kind: "sourceStart", id: 0, owner: 0, at: 0, budget: 10}, {kind: "sourceSettlement", id: 0, at: 10, outcome: "resolve"}, {kind: "writeDispatch", id: 0, owner: 0, at: 10}, {kind: "sourceSettlement", id: 99, at: 10, outcome: "resolve"}} + if replayRecordingError(assertBehaviorCausality(history)) == nil { + t.Fatal("earlier semantic failure concealed malformed publication history") + } +} diff --git a/go/effects_replay_test.go b/go/effects_replay_test.go index 28ad28fa..92fbb0b5 100644 --- a/go/effects_replay_test.go +++ b/go/effects_replay_test.go @@ -24,6 +24,7 @@ func (d *behaviorDriver) assertEffectsHistory() error { var active *source authorized := false previous := int64(-1) + var assertion error for index, e := range history { fail := func(reason string) error { return fmt.Errorf("effects contract event %d %s: %s", index, e.event, reason) @@ -55,25 +56,33 @@ func (d *behaviorDriver) assertEffectsHistory() error { } elapsed := float64(e.at - active.at) if math.Abs(e.duration-elapsed) > 1e-7 { - return behaviorPropertyFailure("C23", "duration includes lookup or omits source time", obj{"event": e.event, "index": index, "atMs": e.at, "elapsedMs": elapsed, "durationMs": e.duration}) + if assertion == nil { + assertion = behaviorPropertyFailure("C23", "duration includes lookup or omits source time", obj{"event": e.event, "index": index, "atMs": e.at, "elapsedMs": elapsed, "durationMs": e.duration}) + } } if !e.failed && (elapsed >= 10 || active.settled != "resolve") { - return behaviorPropertyFailure("C25", "success must be accepted before its source deadline", obj{"event": e.event, "index": index, "atMs": e.at, "elapsedMs": elapsed, "budgetMs": 10, "settlement": active.settled, "failed": e.failed}) + if assertion == nil { + assertion = behaviorPropertyFailure("C25", "success must be accepted before its source deadline", obj{"event": e.event, "index": index, "atMs": e.at, "elapsedMs": elapsed, "budgetMs": 10, "settlement": active.settled, "failed": e.failed}) + } } if e.failed && elapsed < 10 && active.settled != "reject" { - return behaviorPropertyFailure("C23", "source lost its full source-relative budget", obj{"event": e.event, "index": index, "atMs": e.at, "elapsedMs": elapsed, "budgetMs": 10, "settlement": active.settled, "failed": e.failed}) + if assertion == nil { + assertion = behaviorPropertyFailure("C23", "source lost its full source-relative budget", obj{"event": e.event, "index": index, "atMs": e.at, "elapsedMs": elapsed, "budgetMs": 10, "settlement": active.settled, "failed": e.failed}) + } } active = nil authorized = !e.failed case "writeDispatch": if !authorized { - return behaviorPropertyFailure("C26", "publication without accepted source success", obj{"event": e.event, "index": index, "atMs": e.at, "authorized": authorized}) + if assertion == nil { + assertion = behaviorPropertyFailure("C26", "publication without accepted source success", obj{"event": e.event, "index": index, "atMs": e.at, "authorized": authorized}) + } } default: return fail("unknown monitor event") } } - return nil + return assertion } // Both public replay and witness validation consume this exact inventory. @@ -152,7 +161,7 @@ func TestEffectsConformance(t *testing.T) { } } } - t.Logf("specification=0.1.0 effectsProfile=2 traces=%d", len(paths)) + t.Logf("specification=0.1.0 effectsProfile=3 traces=%d", len(paths)) } func TestEffectsParserRejectsMissingDiagnostics(t *testing.T) { coordinator := newReplayCoordinator(t) diff --git a/go/feature_replay_test.go b/go/feature_replay_test.go index 0a92fa4d..4b3ab31e 100644 --- a/go/feature_replay_test.go +++ b/go/feature_replay_test.go @@ -65,8 +65,22 @@ func TestFeatureConformance(t *testing.T) { t.Fatalf("unknown selected feature profile: %s", selected) } } - count := 0 - executedProfiles := map[string]bool{} + // Each profile owns its counter. Cleanup runs after all parallel children, + // so the summary never races with replay or accepts an empty -run selection. + counts := map[string]*int{} + t.Cleanup(func() { + count, executedProfiles := 0, 0 + for _, replayed := range counts { + count += *replayed + if *replayed > 0 { + executedProfiles++ + } + } + if count == 0 { + t.Fatal("no feature traces selected") + } + t.Logf("specification=0.1.0 featureProfiles=%d replayed=%d", executedProfiles, count) + }) names := []string{} for name := range profiles { if name != "core" && name != "effects" && name != "local-clock" { @@ -82,42 +96,48 @@ func TestFeatureConformance(t *testing.T) { if err != nil { t.Fatal(err) } - if len(paths) > 0 { - requireBehaviorProfile(t, name) + if len(paths) == 0 { + continue } - actions := map[string]bool{} - for _, path := range paths { - prepared, err := coordinator.prepare(name, path, nil) - if err != nil { - t.Fatal(err) + count := 0 + counts[name] = &count + t.Run(name, func(t *testing.T) { + // Go's -parallel flag bounds the number of profile coordinators. + // Recordings retain their single-writer, ordered JSONL output. + if os.Getenv("DIALCACHE_REPLAY_DIVERGENCES") == "" { + t.Parallel() } - for _, action := range ba(prepared["actions"]) { - actions[bs(action)] = true + coordinator := newReplayCoordinator(t) + requireBehaviorProfile(t, name) + actions := map[string]bool{} + for _, path := range paths { + prepared, err := coordinator.prepare(name, path, nil) + if err != nil { + t.Fatal(err) + } + for _, action := range ba(prepared["actions"]) { + actions[bs(action)] = true + } + t.Run(filepath.Base(path), func(t *testing.T) { + count++ + synctest.Test(t, func(t *testing.T) { + d := newBehaviorDriver(t, bm(prepared["fixture"])) + defer d.close() + if err := coordinator.replay(d, prepared); err != nil { + t.Error(err) + } + }) + }) } - t.Run(name+"/"+filepath.Base(path), func(t *testing.T) { - count++ - executedProfiles[name] = true - synctest.Test(t, func(t *testing.T) { - d := newBehaviorDriver(t, bm(prepared["fixture"])) - defer d.close() - if err := coordinator.replay(d, prepared); err != nil { - t.Error(err) + if os.Getenv("DIALCACHE_FEATURE_TRACE_DIR") != "" && os.Getenv("DIALCACHE_FEATURE_TRACE_FILE") == "" { + for _, action := range ba(profiles[name]) { + if !actions[bs(action)] { + t.Errorf("%s corpus omits action %s", name, action) } - }) - }) - } - if os.Getenv("DIALCACHE_FEATURE_TRACE_DIR") != "" && os.Getenv("DIALCACHE_FEATURE_TRACE_FILE") == "" { - for _, action := range ba(profiles[name]) { - if !actions[bs(action)] { - t.Errorf("%s corpus omits action %s", name, action) } } - } - } - if count == 0 { - t.Fatal("no feature traces selected") + }) } - t.Logf("specification=0.1.0 featureProfiles=%d replayed=%d", len(executedProfiles), count) } func TestFeatureParserRejectsMissingAndUnsafeInputs(t *testing.T) { diff --git a/go/invalidation_vector_driver_test.go b/go/invalidation_vector_driver_test.go new file mode 100644 index 00000000..af1491b6 --- /dev/null +++ b/go/invalidation_vector_driver_test.go @@ -0,0 +1,194 @@ +package dialcache + +import ( + "context" + "crypto/sha256" + "encoding/json" + "fmt" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + "time" + + "github.com/redis/go-redis/v9" +) + +type vectorInvalidationState struct { + Kind string `json:"kind"` + Value string `json:"value,omitempty"` + Values []string `json:"values,omitempty"` + TTLMS int64 `json:"ttlMs"` +} +type vectorInvalidationInput struct { + Existing vectorInvalidationState `json:"existing"` + FutureBufferMS string `json:"futureBufferMs"` + InvalidatedAtMS string `json:"invalidatedAtMs"` +} +type vectorInvalidationActual struct { + Outcome string `json:"outcome"` + Kind string `json:"kind"` + Content any `json:"content"` + TTLMS int64 `json:"ttlMs"` + ElapsedMS int64 `json:"elapsedMs"` +} + +// Setup and observation bracket the actual production Lua in one atomic +// execution. The worker knows only inputs; no expected row enters this path. +const vectorInvalidationScript = `redis.replicate_commands() +local function now_ms() + local now=redis.call("TIME") + return tonumber(now[1])*1000+math.floor(tonumber(now[2])/1000) +end +local started_at=now_ms() +redis.call("DEL",KEYS[1]) +if ARGV[3]=="string" then redis.call("SET",KEYS[1],ARGV[4]) end +if ARGV[3]=="list" then for _,value in ipairs(cjson.decode(ARGV[4])) do redis.call("RPUSH",KEYS[1],value) end end +if tonumber(ARGV[5])>0 then redis.call("PEXPIRE",KEYS[1],ARGV[5]) end +local result=(function() +` + InvalidationScript + ` +end)() +local status=result==1 and "success" or (type(result)=="table" and (result.err=="ERR invalid DialCache future buffer" or result.err=="ERR invalid DialCache invalidatedAtMs") and "rejected" or "unexpected_reply") +local kind=redis.call("TYPE",KEYS[1]).ok +local content={} +if kind=="string" then content=redis.call("GET",KEYS[1]) end +if kind=="list" then content=redis.call("LRANGE",KEYS[1],0,-1) end +if kind=="none" then kind="absent" end +local ttl=redis.call("PTTL",KEYS[1]) +return {status,kind,content,ttl,now_ms()-started_at}` + +func vectorInvalidationClient(t *testing.T) (*redis.Client, string) { + t.Helper() + endpoint := os.Getenv("DIALCACHE_VECTOR_REDIS_URL") + if endpoint == "" { + t.Fatal("INVALIDATION_INFRASTRUCTURE: Redis vector endpoint required") + } + options, err := redis.ParseURL(endpoint) + if err != nil { + t.Fatal("INVALIDATION_INFRASTRUCTURE:", err) + } + options.MaxRetries = -1 + client := redis.NewClient(options) + key := fmt.Sprintf("{formal-invalidation-%d-%d}#watermark", os.Getpid(), time.Now().UnixNano()) + t.Cleanup(func() { client.Del(context.Background(), key); client.Close() }) + return client, key +} +func recordVectorInvalidation(t *testing.T, client *redis.Client, key string, input vectorInvalidationInput) vectorInvalidationActual { + t.Helper() + content := input.Existing.Value + if input.Existing.Kind == "list" { + encoded, err := json.Marshal(input.Existing.Values) + if err != nil { + t.Fatal("INVALIDATION_INFRASTRUCTURE:", err) + } + content = string(encoded) + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + raw, err := client.Eval(ctx, vectorInvalidationScript, []string{key}, input.FutureBufferMS, input.InvalidatedAtMS, input.Existing.Kind, content, input.Existing.TTLMS).Slice() + if err != nil || len(raw) != 5 { + t.Fatal("INVALIDATION_INFRASTRUCTURE: invalid native reply", raw, err) + } + outcome, ok1 := raw[0].(string) + kind, ok2 := raw[1].(string) + ttl, ok3 := raw[3].(int64) + elapsed, ok4 := raw[4].(int64) + if !ok1 || !ok2 || !ok3 || !ok4 || (outcome != "success" && outcome != "rejected") || ttl < -2 || elapsed < 0 { + t.Fatal("INVALIDATION_INFRASTRUCTURE: malformed native result", raw) + } + switch kind { + case "string": + if _, ok := raw[2].(string); !ok { + t.Fatal("INVALIDATION_INFRASTRUCTURE: malformed string", raw) + } + case "list", "absent": + items, ok := raw[2].([]any) + if !ok || (kind == "absent" && len(items) != 0) || (kind == "list" && len(items) == 0) { + t.Fatal("INVALIDATION_INFRASTRUCTURE: malformed content", raw) + } + for _, item := range items { + if _, ok := item.(string); !ok { + t.Fatal("INVALIDATION_INFRASTRUCTURE: malformed list", raw) + } + } + default: + t.Fatal("INVALIDATION_INFRASTRUCTURE: unexpected key type", kind) + } + return vectorInvalidationActual{Outcome: outcome, Kind: kind, Content: raw[2], TTLMS: ttl, ElapsedMS: elapsed} +} + +// One connection replays the complete generated set. Process/transport errors +// are explicitly rejected by the campaign evaluator as infrastructure. +func TestGeneratedInvalidationVectors(t *testing.T) { + if os.Getenv("DIALCACHE_VECTOR_REDIS_URL") == "" { + t.Skip("this native vector lane runs in mutation campaigns") + } + raw, err := os.ReadFile("../formal/quint-invalidation-vectors.json") + if err != nil { + t.Fatal("INVALIDATION_INFRASTRUCTURE:", err) + } + var corpus struct { + SchemaVersion int `json:"schemaVersion"` + Provenance struct { + Model string + SourceSHA256 map[string]string `json:"sourceSha256"` + } + Vectors []struct { + Name string + vectorInvalidationInput + Expected struct { + Error bool + State vectorInvalidationState + } + } + } + if err = json.Unmarshal(raw, &corpus); err != nil || corpus.SchemaVersion != 2 || len(corpus.Vectors) != 288 { + t.Fatal("INVALIDATION_INFRASTRUCTURE: invalid corpus", err) + } + const model = "formal/dialcache-invalidation-transition.qnt" + if corpus.Provenance.Model != model || len(corpus.Provenance.SourceSHA256) != 2 { + t.Fatal("INVALIDATION_INFRASTRUCTURE: invalid provenance") + } + for _, path := range []string{model, "formal/generate-invalidation-vectors.mjs"} { + source, err := os.ReadFile(filepath.Join("..", path)) + if err != nil || fmt.Sprintf("%x", sha256.Sum256(source)) != corpus.Provenance.SourceSHA256[path] { + t.Fatal("INVALIDATION_INFRASTRUCTURE: stale provenance", path, err) + } + } + client, key := vectorInvalidationClient(t) + for i, row := range corpus.Vectors { + if !strings.HasPrefix(row.Name, fmt.Sprintf("Quint %03d: ", i)) { + t.Fatal("INVALIDATION_INFRASTRUCTURE: incomplete or reordered vectors") + } + t.Run(row.Name, func(t *testing.T) { + actual := recordVectorInvalidation(t, client, key, row.vectorInvalidationInput) + outcome := "success" + if row.Expected.Error { + outcome = "rejected" + } + state := row.Expected.State + var content any = state.Value + if state.Kind != "string" { + items := make([]any, len(state.Values)) + for i, v := range state.Values { + items[i] = v + } + content = items + } + if actual.Outcome != outcome || actual.Kind != state.Kind || !reflect.DeepEqual(actual.Content, content) { + t.Errorf("invalidation result: got=%+v want outcome=%s kind=%s content=%v", actual, outcome, state.Kind, content) + } + if state.TTLMS < 0 { + if actual.TTLMS != state.TTLMS { + t.Errorf("TTL %d want %d", actual.TTLMS, state.TTLMS) + } + } else { + minimum := max(int64(0), state.TTLMS-actual.ElapsedMS) + if actual.TTLMS < minimum || actual.TTLMS > state.TTLMS { + t.Errorf("TTL %d outside [%d,%d] with server elapsed %d", actual.TTLMS, minimum, state.TTLMS, actual.ElapsedMS) + } + } + }) + } +} diff --git a/go/replay_coordinator_test.go b/go/replay_coordinator_test.go index 487b5c46..26b63a38 100644 --- a/go/replay_coordinator_test.go +++ b/go/replay_coordinator_test.go @@ -5,6 +5,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "math" @@ -248,6 +249,19 @@ func (c *replayCoordinator) replay(d *behaviorDriver, prepared obj, monitors ... func (c *replayCoordinator) execute(prepared obj, apply func(obj) error, observation func() obj, wallMS func() int64, monitors ...func() error) error { return c.executeWithReceipt(prepared, apply, observation, wallMS, nil, monitors...) } + +func replayRecordingError(err error) error { + // Full monitor input validation precedes this typed semantic assertion. + // Recording still requires the coordinator's complete observation comparison; + // this diagnostic alone never establishes boundary evidence. + var property *behaviorPropertyAssertion + if os.Getenv("DIALCACHE_REPLAY_DIVERGENCES") != "" && errors.As(err, &property) { + fmt.Fprintln(os.Stderr, property) + return nil + } + return err +} + func (c *replayCoordinator) executeWithReceipt(prepared obj, apply func(obj) error, observation func() obj, wallMS func() int64, receipt func() obj, monitors ...func() error) error { session := bs(prepared["session"]) complete := false @@ -264,13 +278,13 @@ func (c *replayCoordinator) executeWithReceipt(prepared obj, apply func(obj) err return fmt.Errorf("replay session requires a %s receipt the driver does not report", receiptDefinition) } for _, input := range ba(prepared["setup"]) { - if err := apply(bm(input)); err != nil { + if err := replayRecordingError(apply(bm(input))); err != nil { return err } } for index := int64(0); index < bn(prepared["steps"]); index++ { for _, monitor := range monitors { - if err := monitor(); err != nil { + if err := replayRecordingError(monitor()); err != nil { return err } } @@ -303,7 +317,7 @@ func (c *replayCoordinator) executeWithReceipt(prepared obj, apply func(obj) err return fmt.Errorf("malformed next replay command") } for _, input := range ba(result["inputs"]) { - if err := apply(bm(input)); err != nil { + if err := replayRecordingError(apply(bm(input))); err != nil { return err } } diff --git a/go/vector_boundary_driver_test.go b/go/vector_boundary_driver_test.go new file mode 100644 index 00000000..345dafab --- /dev/null +++ b/go/vector_boundary_driver_test.go @@ -0,0 +1,125 @@ +package dialcache + +import ( + "encoding/hex" + "encoding/json" + "os" + "testing" +) + +// Expected values never enter this driver. Known API failures are observations; +// request, process and serialization failures fail the worker itself. +func TestVectorBoundaryDriver(t *testing.T) { + path := os.Getenv("DIALCACHE_VECTOR_REQUEST") + if path == "" { + t.Skip("no selected vector") + } + out := os.Getenv("DIALCACHE_VECTOR_OUT") + if out == "" { + t.Fatal("DIALCACHE_VECTOR_OUT is required") + } + raw, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + var request struct { + Operation string + Input json.RawMessage + } + if err := json.Unmarshal(raw, &request); err != nil { + t.Fatal(err) + } + var actual any + switch request.Operation { + case "invalidation": + var input vectorInvalidationInput + if err := json.Unmarshal(request.Input, &input); err != nil { + t.Fatal(err) + } + client, key := vectorInvalidationClient(t) + actual = recordVectorInvalidation(t, client, key, input) + case "key": + var input Identity + if err := json.Unmarshal(request.Input, &input); err != nil { + t.Fatal(err) + } + logical, value, watermark, err := input.Keys() + if err != nil { + if err.Error() != "identity contains a reserved hash-tag delimiter" { + t.Fatal(err) + } + actual = map[string]any{"kind": "key_error"} + } else { + var observedWatermark any + if input.Tracked { + observedWatermark = watermark + } + actual = map[string]any{"kind": "key", "logicalKey": logical, "valueKey": value, "watermarkKey": observedWatermark} + } + case "trackedDecode": + var input struct{ FrameHex, WatermarkUTF8 *string } + if err := json.Unmarshal(request.Input, &input); err != nil { + t.Fatal(err) + } + var frame []byte + if input.FrameHex != nil { + frame = unhex(t, *input.FrameHex) + } + result := DecodeFrame(frame, true, input.WatermarkUTF8) + value := map[string]any{"kind": result.Kind} + if result.Kind == "miss" { + value["reason"] = result.Reason + if result.ObservedWatermarkMS != nil { + value["observedWatermarkMs"] = *result.ObservedWatermarkMS + } + } + if result.Kind == "hit" { + value["createdAtMs"] = result.Frame.CreatedAtMS + if result.Frame.Binary { + value["payloadType"] = "binary" + value["payloadHex"] = hex.EncodeToString(result.Frame.Payload) + } else { + value["payloadType"] = "string" + value["payloadUtf8"] = string(result.Frame.Payload) + } + } + actual = value + case "envelope": + var input struct{ InputHex string } + if err := json.Unmarshal(request.Input, &input); err != nil { + t.Fatal(err) + } + result := DecompressPayload(Payload{Bytes: unhex(t, input.InputHex), Binary: true}) + actual = map[string]any{"decodedHex": hex.EncodeToString(result.Payload.Bytes), "outcome": result.Outcome} + case "compression": + var input struct { + PayloadType, PayloadUTF8, PayloadHex string + ThresholdBytes, MaxDecompressedBytes int + } + if err := json.Unmarshal(request.Input, &input); err != nil { + t.Fatal(err) + } + payload := Payload{Bytes: []byte(input.PayloadUTF8), Binary: input.PayloadType == "binary"} + if payload.Binary { + payload.Bytes = unhex(t, input.PayloadHex) + } + result, err := CompressPayload(payload, CompressionConfig{ThresholdBytes: input.ThresholdBytes, Level: 3}, input.MaxDecompressedBytes) + if err != nil { + t.Fatal(err) + } + marker := -1 + if result.Outcome == "compressed" { + marker = int(result.Payload.Bytes[0]) + } + actual = map[string]any{"outcome": result.Outcome, "storedBytes": result.StoredBytes, "marker": marker} + default: + t.Fatal("unknown vector operation", request.Operation) + } + encoded, err := json.Marshal(map[string]any{"actual": actual}) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(out, append(encoded, '\n'), 0600); err != nil { + t.Fatal(err) + } +} diff --git a/test/dialcache-get-or-load.test.ts b/test/dialcache-get-or-load.test.ts index 7be2e51a..1dde123e 100644 --- a/test/dialcache-get-or-load.test.ts +++ b/test/dialcache-get-or-load.test.ts @@ -241,6 +241,7 @@ describe("DialCache getOrLoad", () => { }); it("reads and writes Redis values with the per-invocation serializer", async () => { + vi.useFakeTimers(); const redis = new FakeRedis(); const dialcache = new DialCache({ redis: { client: redis } }); const serializer: Serializer = { diff --git a/test/fixtures/admission-budget-witnesses.json b/test/fixtures/admission-budget-witnesses.json new file mode 100644 index 00000000..d5c9ea07 --- /dev/null +++ b/test/fixtures/admission-budget-witnesses.json @@ -0,0 +1,20 @@ +[ + { + "regression": "laterServedShadowKeepsItsWholeBudgetTest", + "trace": { + "source": { + "model": "formal/dialcache-admission-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/admission-budget-witnesses.json/laterServedShadowKeepsItsWholeBudgetTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}}}}, + {"input":{"choice":{"#bigint":"10"},"name":"advance"},"mbt::actionTaken":"advance","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"10"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseLoad"},"mbt::actionTaken":"releaseLoad","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[false],"writeTtls":[],"writes":{"#bigint":"0"}}}}, + {"input":{"choice":{"#bigint":"1"},"name":"advance"},"mbt::actionTaken":"advance","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[false],"writeTtls":[],"writes":{"#bigint":"0"}}}}, + {"input":{"choice":{"#bigint":"0"},"name":"rejectLoader"},"mbt::actionTaken":"rejectLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["source_error"],"sourceScopes":[false],"writeTtls":[],"writes":{"#bigint":"0"}}}} + ] + } + } +] diff --git a/test/fixtures/dark-layers-witnesses.json b/test/fixtures/dark-layers-witnesses.json new file mode 100644 index 00000000..9237ad22 --- /dev/null +++ b/test/fixtures/dark-layers-witnesses.json @@ -0,0 +1,560 @@ +[ + { + "regression": "rejectedDarkSourceSeedsNoLayerTest", + "trace": { + "source": { + "model": "formal/dialcache-dark-layers-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/dark-layers-witnesses.json/rejectedDarkSourceSeedsNoLayerTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"rejectLoader"},"mbt::actionTaken":"rejectLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"3"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["source_error"],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":["local"],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"3"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"2"},"recovery":[],"shadow":["source_error"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":["local"],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}} + ] + } + }, + { + "regression": "uncoalescedPublicationDoesNotPreemptTheRegisteredLeaderTest", + "trace": { + "source": { + "model": "formal/dialcache-dark-layers-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/dark-layers-witnesses.json/uncoalescedPublicationDoesNotPreemptTheRegisteredLeaderTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"5"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"5"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"2"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"2"}}},"s":{"o":{"calls":[{"#bigint":"0"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"4"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"4"}}},"s":{"o":{"calls":[{"#bigint":"0"},{"#bigint":"2"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"},{"#bigint":"2"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"6"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"6"}}},"s":{"o":{"calls":[{"#bigint":"0"},{"#bigint":"2"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"3"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":["process"],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"2"},{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"3"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":["process"],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}} + ] + } + }, + { + "regression": "darkFillCarriesCapturedRetentionOnTheHeldPathTest", + "trace": { + "source": { + "model": "formal/dialcache-dark-layers-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/dark-layers-witnesses.json/darkFillCarriesCapturedRetentionOnTheHeldPathTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"3"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"3"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"7"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"7"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseDump"},"mbt::actionTaken":"releaseDump","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[{"#bigint":"120000"}],"writes":{"#bigint":"1"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseWrite"},"mbt::actionTaken":"releaseWrite","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["filled"],"sourceScopes":[true],"writeTtls":[{"#bigint":"120000"}],"writes":{"#bigint":"1"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}} + ] + } + }, + { + "regression": "fillAtTheWatermarkInstantIsFencedTest", + "trace": { + "source": { + "model": "formal/dialcache-dark-layers-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/dark-layers-witnesses.json/fillAtTheWatermarkInstantIsFencedTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"invalidate"},"mbt::actionTaken":"invalidate","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"1"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":["ok"],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"3"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"3"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"1"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":["ok"],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"1"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":["ok"],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"1"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":["ok"],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"1"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":["ok"],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["fill_fenced"],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}} + ] + } + }, + { + "regression": "frameAtTheWatermarkIsFencedFromTheDarkReadTest", + "trace": { + "source": { + "model": "formal/dialcache-dark-layers-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/dark-layers-witnesses.json/frameAtTheWatermarkIsFencedFromTheDarkReadTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"invalidate"},"mbt::actionTaken":"invalidate","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"1"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":["ok"],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"seed"},"mbt::actionTaken":"seed","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"1"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":["ok"],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"3"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"3"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"1"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":["ok"],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"1"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":["ok"],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"1"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":["ok"],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"advance"},"mbt::actionTaken":"advance","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"1"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":["ok"],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"2"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"2"}}},"s":{"o":{"calls":[{"#bigint":"2"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"1"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":["ok"],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}} + ] + } + }, + { + "regression": "fillFenceIsRejudgedAfterAWallRollbackTest", + "trace": { + "source": { + "model": "formal/dialcache-dark-layers-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/dark-layers-witnesses.json/fillFenceIsRejudgedAfterAWallRollbackTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"invalidate"},"mbt::actionTaken":"invalidate","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"1"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":["ok"],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"advance"},"mbt::actionTaken":"advance","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"1"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":["ok"],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"3"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"3"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"1"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":["ok"],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"1"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":["ok"],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"1"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":["ok"],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"1"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":["ok"],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"-1"},"name":"rollbackWall"},"mbt::actionTaken":"rollbackWall","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"1"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":["ok"],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseDump"},"mbt::actionTaken":"releaseDump","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"1"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":["ok"],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["fill_fenced"],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}} + ] + } + }, + { + "regression": "staleVisibleFrameDeclinedByTheDarkReadFillsUnfencedTest", + "trace": { + "source": { + "model": "formal/dialcache-dark-layers-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/dark-layers-witnesses.json/staleVisibleFrameDeclinedByTheDarkReadFillsUnfencedTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"3"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"3"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"3"},"name":"seed"},"mbt::actionTaken":"seed","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"3"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"invalidate"},"mbt::actionTaken":"invalidate","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"1"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":["ok"],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"2"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"2"}}},"s":{"o":{"calls":[{"#bigint":"2"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"1"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":["ok"],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseDump"},"mbt::actionTaken":"releaseDump","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"2"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"1"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":["ok"],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[{"#bigint":"120000"}],"writes":{"#bigint":"1"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseWrite"},"mbt::actionTaken":"releaseWrite","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"2"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"1"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":["ok"],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["filled"],"sourceScopes":[true],"writeTtls":[{"#bigint":"120000"}],"writes":{"#bigint":"1"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"2"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"1"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":["ok"],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"2"},"recovery":[],"shadow":["filled"],"sourceScopes":[true,true],"writeTtls":[{"#bigint":"120000"}],"writes":{"#bigint":"1"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"2"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"1"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":["ok"],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"2"},"recovery":[],"shadow":["filled"],"sourceScopes":[true,true],"writeTtls":[{"#bigint":"120000"}],"writes":{"#bigint":"1"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"3"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"3"}}},"s":{"o":{"calls":[{"#bigint":"2"},{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"1"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":["ok"],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"2"},"recovery":[],"shadow":["filled","fill_fenced"],"sourceScopes":[true,true],"writeTtls":[{"#bigint":"120000"}],"writes":{"#bigint":"1"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}} + ] + } + }, + { + "regression": "undecodableDarkC0IsNeverRepairedTest", + "trace": { + "source": { + "model": "formal/dialcache-dark-layers-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/dark-layers-witnesses.json/undecodableDarkC0IsNeverRepairedTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"3"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"3"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"4"},"name":"seed"},"mbt::actionTaken":"seed","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"4"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"2"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"2"}}},"s":{"o":{"calls":[{"#bigint":"2"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseLoad"},"mbt::actionTaken":"releaseLoad","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"2"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["deserialization_error"],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}} + ] + } + }, + { + "regression": "unsupportedEncodingFailsTheDarkReadTest", + "trace": { + "source": { + "model": "formal/dialcache-dark-layers-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/dark-layers-witnesses.json/unsupportedEncodingFailsTheDarkReadTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"3"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"3"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"5"},"name":"seed"},"mbt::actionTaken":"seed","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"5"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["redis_error"],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["redis_error"],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}} + ] + } + }, + { + "regression": "sourceDeadlineExpiresTheDarkJobAndAttributesToTheLocalLayerTest", + "trace": { + "source": { + "model": "formal/dialcache-dark-layers-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/dark-layers-witnesses.json/sourceDeadlineExpiresTheDarkJobAndAttributesToTheLocalLayerTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"10"},"name":"advance"},"mbt::actionTaken":"advance","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"10"}}},"s":{"o":{"calls":[{"#bigint":"4"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["timeout"],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":["local"],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"4"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["timeout"],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":["local"],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"4"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"2"},"recovery":[],"shadow":["timeout"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":["local"],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}} + ] + } + }, + { + "regression": "sameKeyCallersOnTwoInstancesKeepSeparateFlightsTest", + "trace": { + "source": { + "model": "formal/dialcache-dark-layers-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/dark-layers-witnesses.json/sameKeyCallersOnTwoInstancesKeepSeparateFlightsTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"4"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"4"}}},"s":{"o":{"calls":[{"#bigint":"0"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"2"},"recovery":[],"shadow":[],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"2"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"2"}}},"s":{"o":{"calls":[{"#bigint":"2"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"2"},"recovery":[],"shadow":[],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}} + ] + } + }, + { + "regression": "closedScopeCallerBypassesLayersAndAdmitsNoJobTest", + "trace": { + "source": { + "model": "formal/dialcache-dark-layers-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/dark-layers-witnesses.json/closedScopeCallerBypassesLayersAndAdmitsNoJobTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"closeScope"},"mbt::actionTaken":"closeScope","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[false],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"10"},"name":"advance"},"mbt::actionTaken":"advance","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"10"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[false],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}} + ] + } + }, + { + "regression": "fullInstanceDropsAnotherKeyWhileTheC0ReadIsHeldTest", + "trace": { + "source": { + "model": "formal/dialcache-dark-layers-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/dark-layers-witnesses.json/fullInstanceDropsAnotherKeyWhileTheC0ReadIsHeldTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"3"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"3"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"0"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"4"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"4"}}},"s":{"o":{"calls":[{"#bigint":"0"},{"#bigint":"0"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"3"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"3"},"reads":{"#bigint":"2"},"recovery":[],"shadow":["dropped"],"sourceScopes":[true,true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}} + ] + } + }, + { + "regression": "dumpFaultEndsTheFillWithoutReplacingTheCallerResultTest", + "trace": { + "source": { + "model": "formal/dialcache-dark-layers-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/dark-layers-witnesses.json/dumpFaultEndsTheFillWithoutReplacingTheCallerResultTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"3"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"3"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"dumpFault"},"mbt::actionTaken":"dumpFault","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseDump"},"mbt::actionTaken":"releaseDump","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["fill_error"],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}} + ] + } + }, + { + "regression": "localPublicationStopsLaterDarkWorkTest", + "trace": { + "source": { + "model": "formal/dialcache-dark-layers-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/dark-layers-witnesses.json/localPublicationStopsLaterDarkWorkTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"2"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"2"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}} + ] + } + }, + { + "regression": "retainedC0AgeIsSampledAtTheVerdictTest", + "trace": { + "source": { + "model": "formal/dialcache-dark-layers-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/dark-layers-witnesses.json/retainedC0AgeIsSampledAtTheVerdictTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"3"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"3"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"2"},"name":"seed"},"mbt::actionTaken":"seed","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"2"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"advance"},"mbt::actionTaken":"advance","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"2"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"2"}}},"s":{"o":{"calls":[{"#bigint":"2"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseLoad"},"mbt::actionTaken":"releaseLoad","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"2"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"2"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"2"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"2"},"recovery":[],"shadow":["mismatch"],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[{"#bigint":"60000"}],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}} + ] + } + }, + { + "regression": "rollbackMakesTheSeededFrameFutureAndReportsItsOffsetTest", + "trace": { + "source": { + "model": "formal/dialcache-dark-layers-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/dark-layers-witnesses.json/rollbackMakesTheSeededFrameFutureAndReportsItsOffsetTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"3"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"3"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"seed"},"mbt::actionTaken":"seed","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"-1"},"name":"rollbackWall"},"mbt::actionTaken":"rollbackWall","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[{"layer":"remote_shadow","offsetMs":{"#bigint":"1000"}}],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"2"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"2"}}},"s":{"o":{"calls":[{"#bigint":"2"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[{"layer":"remote_shadow","offsetMs":{"#bigint":"1000"}}],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseDump"},"mbt::actionTaken":"releaseDump","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"2"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[{"#bigint":"120000"}],"writes":{"#bigint":"1"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[{"layer":"remote_shadow","offsetMs":{"#bigint":"1000"}}],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseWrite"},"mbt::actionTaken":"releaseWrite","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"2"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["filled"],"sourceScopes":[true],"writeTtls":[{"#bigint":"120000"}],"writes":{"#bigint":"1"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[{"layer":"remote_shadow","offsetMs":{"#bigint":"1000"}}],"inspections":[]}}} + ] + } + }, + { + "regression": "transientRequestOnlySourceFailureIsAttributedToRequestLayerTest", + "trace": { + "source": { + "model": "formal/dialcache-dark-layers-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/dark-layers-witnesses.json/transientRequestOnlySourceFailureIsAttributedToRequestLayerTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"6"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"6"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"rejectLoader"},"mbt::actionTaken":"rejectLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"3"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["source_error"],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":["request_local"],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}} + ] + } + }, + { + "regression": "laterDarkSourceKeepsItsWholeBudgetTest", + "trace": { + "source": { + "model": "formal/dialcache-dark-layers-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/dark-layers-witnesses.json/laterDarkSourceKeepsItsWholeBudgetTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"10"},"name":"advance"},"mbt::actionTaken":"advance","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"10"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"2"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"2"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}} + ] + } + }, + { + "regression": "timedOutDarkReadKeepsCapacityUntilRawReleaseTest", + "trace": { + "source": { + "model": "formal/dialcache-dark-layers-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/dark-layers-witnesses.json/timedOutDarkReadKeepsCapacityUntilRawReleaseTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"3"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"3"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"10"},"name":"advance"},"mbt::actionTaken":"advance","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"10"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["timeout"],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["timeout","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"3"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"3"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["timeout","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["timeout","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"3"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"3"},"reads":{"#bigint":"2"},"recovery":[],"shadow":["timeout","dropped"],"sourceScopes":[true,true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}} + ] + } + }, + { + "regression": "timedOutDarkDecodeKeepsCapacityUntilRawReleaseTest", + "trace": { + "source": { + "model": "formal/dialcache-dark-layers-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/dark-layers-witnesses.json/timedOutDarkDecodeKeepsCapacityUntilRawReleaseTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"3"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"3"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"seed"},"mbt::actionTaken":"seed","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"2"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"2"}}},"s":{"o":{"calls":[{"#bigint":"2"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"10"},"name":"advance"},"mbt::actionTaken":"advance","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"10"}}},"s":{"o":{"calls":[{"#bigint":"2"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["timeout"],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"2"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["timeout","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"3"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"3"}}},"s":{"o":{"calls":[{"#bigint":"2"},{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["timeout","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseLoad"},"mbt::actionTaken":"releaseLoad","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"2"},{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["timeout","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"2"},{"#bigint":"1"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"3"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"3"},"reads":{"#bigint":"2"},"recovery":[],"shadow":["timeout","dropped"],"sourceScopes":[true,true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}} + ] + } + }, + { + "regression": "timedOutDarkConfirmationKeepsCapacityUntilRawReleaseTest", + "trace": { + "source": { + "model": "formal/dialcache-dark-layers-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/dark-layers-witnesses.json/timedOutDarkConfirmationKeepsCapacityUntilRawReleaseTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"3"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"3"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"seed"},"mbt::actionTaken":"seed","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"2"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"2"}}},"s":{"o":{"calls":[{"#bigint":"2"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseLoad"},"mbt::actionTaken":"releaseLoad","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"2"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"2"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"10"},"name":"advance"},"mbt::actionTaken":"advance","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"10"}}},"s":{"o":{"calls":[{"#bigint":"2"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"2"},"recovery":[],"shadow":["timeout"],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"2"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"2"},"recovery":[],"shadow":["timeout","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"3"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"3"}}},"s":{"o":{"calls":[{"#bigint":"2"},{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"2"},"recovery":[],"shadow":["timeout","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"2"},{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"2"},"recovery":[],"shadow":["timeout","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"2"},{"#bigint":"1"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"3"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"3"},"reads":{"#bigint":"3"},"recovery":[],"shadow":["timeout","dropped"],"sourceScopes":[true,true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}} + ] + } + }, + { + "regression": "timedOutDarkDumpKeepsCapacityUntilRawReleaseTest", + "trace": { + "source": { + "model": "formal/dialcache-dark-layers-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/dark-layers-witnesses.json/timedOutDarkDumpKeepsCapacityUntilRawReleaseTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"3"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"3"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"10"},"name":"advance"},"mbt::actionTaken":"advance","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"10"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["timeout"],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["timeout","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"3"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"3"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["timeout","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseDump"},"mbt::actionTaken":"releaseDump","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["timeout","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"3"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"3"},"reads":{"#bigint":"2"},"recovery":[],"shadow":["timeout","dropped"],"sourceScopes":[true,true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}} + ] + } + }, + { + "regression": "timedOutDarkWriteKeepsCapacityUntilRawReleaseTest", + "trace": { + "source": { + "model": "formal/dialcache-dark-layers-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/dark-layers-witnesses.json/timedOutDarkWriteKeepsCapacityUntilRawReleaseTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"3"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"3"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseDump"},"mbt::actionTaken":"releaseDump","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[{"#bigint":"120000"}],"writes":{"#bigint":"1"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"10"},"name":"advance"},"mbt::actionTaken":"advance","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"10"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["timeout"],"sourceScopes":[true],"writeTtls":[{"#bigint":"120000"}],"writes":{"#bigint":"1"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["timeout","dropped"],"sourceScopes":[true,true],"writeTtls":[{"#bigint":"120000"}],"writes":{"#bigint":"1"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"3"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"3"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["timeout","dropped"],"sourceScopes":[true,true],"writeTtls":[{"#bigint":"120000"}],"writes":{"#bigint":"1"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseWrite"},"mbt::actionTaken":"releaseWrite","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["timeout","dropped"],"sourceScopes":[true,true],"writeTtls":[{"#bigint":"120000"}],"writes":{"#bigint":"1"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"3"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"3"},"reads":{"#bigint":"2"},"recovery":[],"shadow":["timeout","dropped"],"sourceScopes":[true,true,true],"writeTtls":[{"#bigint":"120000"}],"writes":{"#bigint":"1"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}} + ] + } + }, + { + "regression": "inspectionTracksKeysFollowersAndOldestLeaderTest", + "trace": { + "source": { + "model": "formal/dialcache-dark-layers-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/dark-layers-witnesses.json/inspectionTracksKeysFollowersAndOldestLeaderTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"6"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"6"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":["request_local"],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"2"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"2"}}},"s":{"o":{"calls":[{"#bigint":"0"},{"#bigint":"0"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"3"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":["request_local","process"],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"2"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"2"}}},"s":{"o":{"calls":[{"#bigint":"0"},{"#bigint":"0"},{"#bigint":"0"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"4"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":["request_local","process","request_local"],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"advance"},"mbt::actionTaken":"advance","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"0"},{"#bigint":"0"},{"#bigint":"0"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"4"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":["request_local","process","request_local"],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"0"},{"#bigint":"0"},{"#bigint":"0"},{"#bigint":"0"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"5"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":["request_local","process","request_local"],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"inspect"},"mbt::actionTaken":"inspect","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"},{"#bigint":"0"},{"#bigint":"0"},{"#bigint":"0"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"5"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":["request_local","process","request_local"],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[{"activeFollowers":{"#bigint":"1"},"activeLeaders":{"#bigint":"2"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"1"}}]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"},{"#bigint":"1"},{"#bigint":"1"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"5"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":["request_local","process","request_local"],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[{"activeFollowers":{"#bigint":"1"},"activeLeaders":{"#bigint":"2"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"1"}}]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"inspect"},"mbt::actionTaken":"inspect","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"},{"#bigint":"1"},{"#bigint":"1"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"5"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":["request_local","process","request_local"],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[{"activeFollowers":{"#bigint":"1"},"activeLeaders":{"#bigint":"2"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"1"}},{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"1"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"0"}}]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"advance"},"mbt::actionTaken":"advance","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"},{"#bigint":"1"},{"#bigint":"1"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"5"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":["request_local","process","request_local"],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[{"activeFollowers":{"#bigint":"1"},"activeLeaders":{"#bigint":"2"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"1"}},{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"1"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"0"}}]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"rejectLoader"},"mbt::actionTaken":"rejectLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"},{"#bigint":"1"},{"#bigint":"1"},{"#bigint":"3"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"5"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":["request_local","process","request_local"],"configErrors":{"#bigint":"0"},"fallbackErrors":["local"],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[{"activeFollowers":{"#bigint":"1"},"activeLeaders":{"#bigint":"2"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"1"}},{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"1"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"0"}}]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"inspect"},"mbt::actionTaken":"inspect","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"},{"#bigint":"1"},{"#bigint":"1"},{"#bigint":"3"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"5"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":["request_local","process","request_local"],"configErrors":{"#bigint":"0"},"fallbackErrors":["local"],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[{"activeFollowers":{"#bigint":"1"},"activeLeaders":{"#bigint":"2"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"1"}},{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"1"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"0"}},{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"0"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"-1"}}]}}} + ] + } + }, + { + "regression": "inspectionIsPerInstanceAndUsesMonotonicTimeTest", + "trace": { + "source": { + "model": "formal/dialcache-dark-layers-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/dark-layers-witnesses.json/inspectionIsPerInstanceAndUsesMonotonicTimeTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"6"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"6"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"advance"},"mbt::actionTaken":"advance","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"4"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"4"}}},"s":{"o":{"calls":[{"#bigint":"0"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"-1"},"name":"rollbackWall"},"mbt::actionTaken":"rollbackWall","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[{"#bigint":"0"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"inspect"},"mbt::actionTaken":"inspect","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"1"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"1"}}]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"inspect"},"mbt::actionTaken":"inspect","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"0"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"1"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"1"}},{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"1"},"instance":{"#bigint":"1"},"oldestLeaderAgeMs":{"#bigint":"0"}}]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"1"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"1"}},{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"1"},"instance":{"#bigint":"1"},"oldestLeaderAgeMs":{"#bigint":"0"}}]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"inspect"},"mbt::actionTaken":"inspect","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"1"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"1"}},{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"1"},"instance":{"#bigint":"1"},"oldestLeaderAgeMs":{"#bigint":"0"}},{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"0"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"-1"}}]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"inspect"},"mbt::actionTaken":"inspect","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"1"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"1"}},{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"1"},"instance":{"#bigint":"1"},"oldestLeaderAgeMs":{"#bigint":"0"}},{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"0"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"-1"}},{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"1"},"instance":{"#bigint":"1"},"oldestLeaderAgeMs":{"#bigint":"0"}}]}}} + ] + } + }, + { + "regression": "inspectionExcludesRequestOnlyAndUncoalescedWorkTest", + "trace": { + "source": { + "model": "formal/dialcache-dark-layers-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/dark-layers-witnesses.json/inspectionExcludesRequestOnlyAndUncoalescedWorkTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":["request_local"],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"inspect"},"mbt::actionTaken":"inspect","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":["request_local"],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"0"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"-1"}}]}}}, + {"input":{"choice":{"#bigint":"5"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"5"}}},"s":{"o":{"calls":[{"#bigint":"0"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":["request_local"],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"0"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"-1"}}]}}}, + {"input":{"choice":{"#bigint":"2"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"2"}}},"s":{"o":{"calls":[{"#bigint":"0"},{"#bigint":"0"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"3"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":["request_local"],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"0"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"-1"}}]}}}, + {"input":{"choice":{"#bigint":"2"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"2"}}},"s":{"o":{"calls":[{"#bigint":"0"},{"#bigint":"0"},{"#bigint":"0"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"3"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"4"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["dropped","dropped"],"sourceScopes":[true,true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":["request_local"],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"0"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"-1"}}]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"inspect"},"mbt::actionTaken":"inspect","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"},{"#bigint":"0"},{"#bigint":"0"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"3"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"4"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["dropped","dropped"],"sourceScopes":[true,true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":["request_local"],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"0"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"-1"}},{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"0"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"-1"}}]}}} + ] + } + }, + { + "regression": "inspectionClearsTimedOutCallersWhileRawWorkRemainsTest", + "trace": { + "source": { + "model": "formal/dialcache-dark-layers-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/dark-layers-witnesses.json/inspectionClearsTimedOutCallersWhileRawWorkRemainsTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"2"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"2"}}},"s":{"o":{"calls":[{"#bigint":"0"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":["process"],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"inspect"},"mbt::actionTaken":"inspect","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":["process"],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[{"activeFollowers":{"#bigint":"1"},"activeLeaders":{"#bigint":"1"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"0"}}]}}}, + {"input":{"choice":{"#bigint":"10"},"name":"advance"},"mbt::actionTaken":"advance","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"10"}}},"s":{"o":{"calls":[{"#bigint":"4"},{"#bigint":"4"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["timeout"],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":["process"],"configErrors":{"#bigint":"0"},"fallbackErrors":["local"],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[{"activeFollowers":{"#bigint":"1"},"activeLeaders":{"#bigint":"1"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"0"}}]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"inspect"},"mbt::actionTaken":"inspect","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"4"},{"#bigint":"4"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["timeout"],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":["process"],"configErrors":{"#bigint":"0"},"fallbackErrors":["local"],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[{"activeFollowers":{"#bigint":"1"},"activeLeaders":{"#bigint":"1"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"0"}},{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"0"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"-1"}}]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"4"},{"#bigint":"4"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"3"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["timeout","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":["process"],"configErrors":{"#bigint":"0"},"fallbackErrors":["local"],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[{"activeFollowers":{"#bigint":"1"},"activeLeaders":{"#bigint":"1"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"0"}},{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"0"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"-1"}}]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"inspect"},"mbt::actionTaken":"inspect","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"4"},{"#bigint":"4"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"3"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["timeout","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":["process"],"configErrors":{"#bigint":"0"},"fallbackErrors":["local"],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[{"activeFollowers":{"#bigint":"1"},"activeLeaders":{"#bigint":"1"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"0"}},{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"0"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"-1"}},{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"1"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"0"}}]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"4"},{"#bigint":"4"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"3"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["timeout","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":["process"],"configErrors":{"#bigint":"0"},"fallbackErrors":["local"],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[{"activeFollowers":{"#bigint":"1"},"activeLeaders":{"#bigint":"1"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"0"}},{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"0"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"-1"}},{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"1"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"0"}}]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"4"},{"#bigint":"4"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"3"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["timeout","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":["process"],"configErrors":{"#bigint":"0"},"fallbackErrors":["local"],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[{"activeFollowers":{"#bigint":"1"},"activeLeaders":{"#bigint":"1"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"0"}},{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"0"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"-1"}},{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"1"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"0"}}]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"inspect"},"mbt::actionTaken":"inspect","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"4"},{"#bigint":"4"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"3"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["timeout","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":["process"],"configErrors":{"#bigint":"0"},"fallbackErrors":["local"],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[{"activeFollowers":{"#bigint":"1"},"activeLeaders":{"#bigint":"1"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"0"}},{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"0"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"-1"}},{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"1"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"0"}},{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"1"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"0"}}]}}} + ] + } + }, + { + "regression": "inspectionExcludesUnfinishedShadowAfterCallerSuccessTest", + "trace": { + "source": { + "model": "formal/dialcache-dark-layers-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/dark-layers-witnesses.json/inspectionExcludesUnfinishedShadowAfterCallerSuccessTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"inspect"},"mbt::actionTaken":"inspect","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"0"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"-1"}}]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseDump"},"mbt::actionTaken":"releaseDump","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[{"#bigint":"120000"}],"writes":{"#bigint":"1"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"0"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"-1"}}]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"inspect"},"mbt::actionTaken":"inspect","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[{"#bigint":"120000"}],"writes":{"#bigint":"1"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[],"inspections":[{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"0"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"-1"}},{"activeFollowers":{"#bigint":"0"},"activeLeaders":{"#bigint":"0"},"instance":{"#bigint":"0"},"oldestLeaderAgeMs":{"#bigint":"-1"}}]}}} + ] + } + } +] diff --git a/test/fixtures/effects-authority-witnesses.json b/test/fixtures/effects-authority-witnesses.json index 9d7a8d9b..e1dbc552 100644 --- a/test/fixtures/effects-authority-witnesses.json +++ b/test/fixtures/effects-authority-witnesses.json @@ -107,5 +107,44 @@ {"input":{"choice":{"#bigint":"0"},"name":"rejectLoader"},"mbt::actionTaken":"rejectLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"3"},{"#bigint":"3"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"io":{"aborted":[],"budgets":[{"#bigint":"10"}],"sourceErrors":[{"#bigint":"1"},{"#bigint":"1"}]},"events":[{"amount":{"#bigint":"0"},"detail":"policy_disabled","event":"disabled","location":"local"},{"amount":{"#bigint":"0"},"detail":"","event":"request","location":"remote"},{"amount":{"#bigint":"0"},"detail":"policy_disabled","event":"disabled","location":"local"},{"amount":{"#bigint":"0"},"detail":"","event":"coalesced","location":"process"},{"amount":{"#bigint":"0"},"detail":"cache_read","event":"error","location":"remote"},{"amount":{"#bigint":"0"},"detail":"","event":"get","location":"remote"},{"amount":{"#bigint":"0"},"detail":"fallback","event":"error","location":"remote"},{"amount":{"#bigint":"0"},"detail":"","event":"fallback","location":"remote"}]}} ] } + }, + { + "regression": "frameReplyAtLastFreshMillisecondHitsTest", + "trace": { + "source": { + "model": "formal/dialcache-effects-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/effects-authority-witnesses.json/frameReplyAtLastFreshMillisecondHitsTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"2"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"2"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"io":{"aborted":[],"budgets":[],"sourceErrors":[]},"events":[]}}, + {"input":{"choice":{"#bigint":"15"},"name":"adapterReply"},"mbt::actionTaken":"adapterReply","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"15"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"io":{"aborted":[],"budgets":[],"sourceErrors":[]},"events":[]}}, + {"input":{"choice":{"#bigint":"59999"},"name":"advanceWall"},"mbt::actionTaken":"advanceWall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"59999"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"io":{"aborted":[],"budgets":[],"sourceErrors":[]},"events":[]}}, + {"input":{"choice":{"#bigint":"-1"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"io":{"aborted":[],"budgets":[{"#bigint":"10"}],"sourceErrors":[{"#bigint":"0"}]},"events":[{"amount":{"#bigint":"0"},"detail":"policy_disabled","event":"disabled","location":"local"},{"amount":{"#bigint":"0"},"detail":"","event":"request","location":"remote"}]}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"io":{"aborted":[],"budgets":[{"#bigint":"10"}],"sourceErrors":[{"#bigint":"0"}]},"events":[{"amount":{"#bigint":"0"},"detail":"policy_disabled","event":"disabled","location":"local"},{"amount":{"#bigint":"0"},"detail":"","event":"request","location":"remote"}]}}, + {"input":{"choice":{"#bigint":"-1"},"name":"releaseLoad"},"mbt::actionTaken":"releaseLoad","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"io":{"aborted":[],"budgets":[{"#bigint":"10"}],"sourceErrors":[{"#bigint":"0"}]},"events":[{"amount":{"#bigint":"0"},"detail":"policy_disabled","event":"disabled","location":"local"},{"amount":{"#bigint":"0"},"detail":"","event":"request","location":"remote"},{"amount":{"#bigint":"0"},"detail":"load","event":"serialization","location":"remote"},{"amount":{"#bigint":"0"},"detail":"","event":"get","location":"remote"}]}} + ] + } + }, + { + "regression": "staleFrameReplyExpiresRefillsAndIsReadableTest", + "trace": { + "source": { + "model": "formal/dialcache-effects-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/effects-authority-witnesses.json/staleFrameReplyExpiresRefillsAndIsReadableTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"2"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"2"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"io":{"aborted":[],"budgets":[],"sourceErrors":[]},"events":[]}}, + {"input":{"choice":{"#bigint":"15"},"name":"adapterReply"},"mbt::actionTaken":"adapterReply","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"15"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"io":{"aborted":[],"budgets":[],"sourceErrors":[]},"events":[]}}, + {"input":{"choice":{"#bigint":"60000"},"name":"advanceWall"},"mbt::actionTaken":"advanceWall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"60000"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"io":{"aborted":[],"budgets":[],"sourceErrors":[]},"events":[]}}, + {"input":{"choice":{"#bigint":"-1"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"io":{"aborted":[],"budgets":[{"#bigint":"10"}],"sourceErrors":[{"#bigint":"0"}]},"events":[{"amount":{"#bigint":"0"},"detail":"policy_disabled","event":"disabled","location":"local"},{"amount":{"#bigint":"0"},"detail":"","event":"request","location":"remote"}]}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"io":{"aborted":[],"budgets":[{"#bigint":"10"}],"sourceErrors":[{"#bigint":"0"}]},"events":[{"amount":{"#bigint":"0"},"detail":"policy_disabled","event":"disabled","location":"local"},{"amount":{"#bigint":"0"},"detail":"","event":"request","location":"remote"},{"amount":{"#bigint":"0"},"detail":"expired","event":"miss","location":"remote"},{"amount":{"#bigint":"0"},"detail":"","event":"get","location":"remote"}]}}, + {"input":{"choice":{"#bigint":"0"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"io":{"aborted":[],"budgets":[{"#bigint":"10"}],"sourceErrors":[{"#bigint":"0"}]},"events":[{"amount":{"#bigint":"0"},"detail":"policy_disabled","event":"disabled","location":"local"},{"amount":{"#bigint":"0"},"detail":"","event":"request","location":"remote"},{"amount":{"#bigint":"0"},"detail":"expired","event":"miss","location":"remote"},{"amount":{"#bigint":"0"},"detail":"","event":"get","location":"remote"},{"amount":{"#bigint":"0"},"detail":"","event":"fallback","location":"remote"}]}}, + {"input":{"choice":{"#bigint":"-1"},"name":"releaseDump"},"mbt::actionTaken":"releaseDump","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[{"#bigint":"60000"}],"writes":{"#bigint":"1"}},"io":{"aborted":[],"budgets":[{"#bigint":"10"}],"sourceErrors":[{"#bigint":"0"}]},"events":[{"amount":{"#bigint":"0"},"detail":"policy_disabled","event":"disabled","location":"local"},{"amount":{"#bigint":"0"},"detail":"","event":"request","location":"remote"},{"amount":{"#bigint":"0"},"detail":"expired","event":"miss","location":"remote"},{"amount":{"#bigint":"0"},"detail":"","event":"get","location":"remote"},{"amount":{"#bigint":"0"},"detail":"","event":"fallback","location":"remote"},{"amount":{"#bigint":"0"},"detail":"dump","event":"serialization","location":"remote"},{"amount":{"#bigint":"1"},"detail":"","event":"size","location":"remote"},{"amount":{"#bigint":"1"},"detail":"","event":"storedSize","location":"remote"},{"amount":{"#bigint":"0"},"detail":"","event":"writeDispatch","location":"remote"}]}}, + {"input":{"choice":{"#bigint":"-1"},"name":"releaseWrite"},"mbt::actionTaken":"releaseWrite","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[{"#bigint":"60000"}],"writes":{"#bigint":"1"}},"io":{"aborted":[],"budgets":[{"#bigint":"10"}],"sourceErrors":[{"#bigint":"0"}]},"events":[{"amount":{"#bigint":"0"},"detail":"policy_disabled","event":"disabled","location":"local"},{"amount":{"#bigint":"0"},"detail":"","event":"request","location":"remote"},{"amount":{"#bigint":"0"},"detail":"expired","event":"miss","location":"remote"},{"amount":{"#bigint":"0"},"detail":"","event":"get","location":"remote"},{"amount":{"#bigint":"0"},"detail":"","event":"fallback","location":"remote"},{"amount":{"#bigint":"0"},"detail":"dump","event":"serialization","location":"remote"},{"amount":{"#bigint":"1"},"detail":"","event":"size","location":"remote"},{"amount":{"#bigint":"1"},"detail":"","event":"storedSize","location":"remote"},{"amount":{"#bigint":"0"},"detail":"","event":"writeDispatch","location":"remote"}]}}, + {"input":{"choice":{"#bigint":"-1"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"2"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[{"#bigint":"60000"}],"writes":{"#bigint":"1"}},"io":{"aborted":[],"budgets":[{"#bigint":"10"},{"#bigint":"10"}],"sourceErrors":[{"#bigint":"0"},{"#bigint":"0"}]},"events":[{"amount":{"#bigint":"0"},"detail":"policy_disabled","event":"disabled","location":"local"},{"amount":{"#bigint":"0"},"detail":"","event":"request","location":"remote"},{"amount":{"#bigint":"0"},"detail":"expired","event":"miss","location":"remote"},{"amount":{"#bigint":"0"},"detail":"","event":"get","location":"remote"},{"amount":{"#bigint":"0"},"detail":"","event":"fallback","location":"remote"},{"amount":{"#bigint":"0"},"detail":"dump","event":"serialization","location":"remote"},{"amount":{"#bigint":"1"},"detail":"","event":"size","location":"remote"},{"amount":{"#bigint":"1"},"detail":"","event":"storedSize","location":"remote"},{"amount":{"#bigint":"0"},"detail":"","event":"writeDispatch","location":"remote"},{"amount":{"#bigint":"0"},"detail":"policy_disabled","event":"disabled","location":"local"},{"amount":{"#bigint":"0"},"detail":"","event":"request","location":"remote"}]}}, + {"input":{"choice":{"#bigint":"1"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"2"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[{"#bigint":"60000"}],"writes":{"#bigint":"1"}},"io":{"aborted":[],"budgets":[{"#bigint":"10"},{"#bigint":"10"}],"sourceErrors":[{"#bigint":"0"},{"#bigint":"0"}]},"events":[{"amount":{"#bigint":"0"},"detail":"policy_disabled","event":"disabled","location":"local"},{"amount":{"#bigint":"0"},"detail":"","event":"request","location":"remote"},{"amount":{"#bigint":"0"},"detail":"expired","event":"miss","location":"remote"},{"amount":{"#bigint":"0"},"detail":"","event":"get","location":"remote"},{"amount":{"#bigint":"0"},"detail":"","event":"fallback","location":"remote"},{"amount":{"#bigint":"0"},"detail":"dump","event":"serialization","location":"remote"},{"amount":{"#bigint":"1"},"detail":"","event":"size","location":"remote"},{"amount":{"#bigint":"1"},"detail":"","event":"storedSize","location":"remote"},{"amount":{"#bigint":"0"},"detail":"","event":"writeDispatch","location":"remote"},{"amount":{"#bigint":"0"},"detail":"policy_disabled","event":"disabled","location":"local"},{"amount":{"#bigint":"0"},"detail":"","event":"request","location":"remote"}]}}, + {"input":{"choice":{"#bigint":"-1"},"name":"releaseLoad"},"mbt::actionTaken":"releaseLoad","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"2"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[{"#bigint":"60000"}],"writes":{"#bigint":"1"}},"io":{"aborted":[],"budgets":[{"#bigint":"10"},{"#bigint":"10"}],"sourceErrors":[{"#bigint":"0"},{"#bigint":"0"}]},"events":[{"amount":{"#bigint":"0"},"detail":"policy_disabled","event":"disabled","location":"local"},{"amount":{"#bigint":"0"},"detail":"","event":"request","location":"remote"},{"amount":{"#bigint":"0"},"detail":"expired","event":"miss","location":"remote"},{"amount":{"#bigint":"0"},"detail":"","event":"get","location":"remote"},{"amount":{"#bigint":"0"},"detail":"","event":"fallback","location":"remote"},{"amount":{"#bigint":"0"},"detail":"dump","event":"serialization","location":"remote"},{"amount":{"#bigint":"1"},"detail":"","event":"size","location":"remote"},{"amount":{"#bigint":"1"},"detail":"","event":"storedSize","location":"remote"},{"amount":{"#bigint":"0"},"detail":"","event":"writeDispatch","location":"remote"},{"amount":{"#bigint":"0"},"detail":"policy_disabled","event":"disabled","location":"local"},{"amount":{"#bigint":"0"},"detail":"","event":"request","location":"remote"},{"amount":{"#bigint":"0"},"detail":"load","event":"serialization","location":"remote"},{"amount":{"#bigint":"0"},"detail":"","event":"get","location":"remote"}]}} + ] + } } ] diff --git a/test/fixtures/kernel/budgeted-local.qnt b/test/fixtures/kernel/budgeted-local.qnt index fbd36bd7..f57bf75b 100644 --- a/test/fixtures/kernel/budgeted-local.qnt +++ b/test/fixtures/kernel/budgeted-local.qnt @@ -26,15 +26,18 @@ module budgeted_local { // Admission kinds: an enabled caller in persistent context 0 or 1, a caller // in a disabled context, a caller whose key failed to construct, and an - // enabled caller for key 1 (the two-key layout only). + // enabled caller for key 1 (the two-key layout only). A failed key can also + // retain the enabled flag: failure still bypasses policy and every layer. pure val ENABLED = 0 pure val ENABLED_OTHER = 1 pure val DISABLED = 2 pure val FAILED_KEY = 3 pure val ENABLED_KEY_ONE = 4 + pure val ENABLED_FAILED_KEY = 5 pure def call(kind: int): Call = { instance: 0, key: if (kind == ENABLED_KEY_ONE) 1 else 0, context: if (kind == ENABLED_OTHER) 1 else 0, - enabled: kind == ENABLED or kind == ENABLED_OTHER or kind == ENABLED_KEY_ONE, keyFailed: kind == FAILED_KEY + enabled: kind == ENABLED or kind == ENABLED_OTHER or kind == ENABLED_KEY_ONE or kind == ENABLED_FAILED_KEY, + keyFailed: kind == FAILED_KEY or kind == ENABLED_FAILED_KEY } type State = { @@ -111,6 +114,20 @@ module budgeted_local { .then(admit(ENABLED_OTHER)).then(release) .expect(s.o.loaders == 4 and s.o.policyCalls == 1 and s.budgets.nth(3) == BUDGET and s.sources.nth(3).shared) + // Keeping enabled true does not let a failed key read a live local entry or + // share source work. Both bypass callers retain their configured deadlines. + run enabledFailedKeyBypassesCachingWithItsDeadlineTest = init + .then(admit(ENABLED)).then(release).then(settle(0, VALUE_ONE)) + .expect(s.o.calls == List(VALUE_ONE) and s.localValues == List(VALUE_ONE)) + .then(admit(ENABLED_FAILED_KEY)).then(admit(ENABLED_FAILED_KEY)) + .expect(s.o.calls == List(VALUE_ONE, CALL_PENDING, CALL_PENDING) + and s.o.loaders == 3 and s.o.policyCalls == 1 + and s.o.reads == 0 and s.o.loads == 0 and s.o.dumps == 0 and s.o.writes == 0 + and s.budgets == List(BUDGET, BUDGET, BUDGET)) + .then(advance(BUDGET)) + .expect(s.o.calls == List(VALUE_ONE, DEADLINE_ERROR, DEADLINE_ERROR) + and s.localValues == List(VALUE_ONE) and s.o.policyCalls == 1 and s.o.loaders == 3) + // A caller whose scope closed between admission and release starts its // source at release, outside the cache, with the configured budget: it // was enabled when admitted. diff --git a/test/fixtures/kernel/held-diagnostics.qnt b/test/fixtures/kernel/held-diagnostics.qnt index 2e29bb9d..68439834 100644 --- a/test/fixtures/kernel/held-diagnostics.qnt +++ b/test/fixtures/kernel/held-diagnostics.qnt @@ -31,6 +31,7 @@ module held_diagnostics { pure val LAYOUT: Serving::Layout = { keysPerInstance: 1, keysPerScope: 1, persistentContexts: 2, operationsPerEntity: 1, probeSourceScope: false, policyProvider: true } pure val SHARED: Resolution = { layers: { request: true, local: false, remote: true }, coalesce: true } pure val PROCESS_ONLY: Resolution = { layers: { request: false, local: false, remote: true }, coalesce: true } + pure val REQUEST_ALONE: Resolution = { layers: { request: true, local: false, remote: false }, coalesce: true } pure val LOCAL_ALONE: Resolution = { layers: { request: true, local: true, remote: false }, coalesce: true } pure val TTLS: Ttls = { localMs: 0, freshMs: 1000, retentionMs: 5000 } pure val LOCAL_TTLS: Ttls = { localMs: 1000, freshMs: 1000, retentionMs: 5000 } @@ -139,9 +140,16 @@ module held_diagnostics { // An accepted settlement records no fallback error. run acceptedSettlementRecordsNoErrorTest = init.then(begin(0)).then(settle(0, VALUE_TWO)) .expect(s.o.calls == List(VALUE_TWO) and s.o.writes == 1 and s.d.fallbackErrors == List()) - // A source a release starts without the remote layer is attributed to the - // request memo when the call memoizes; its failure names that layer. - run sourceWithoutRemoteLayerIsAttributedToTheRequestMemoTest = init.then(beginWith(0, LOCAL_ALONE, LOCAL_TTLS, Recovery::ALLOW)) + // TypeScript's active local traversal owns this fallback even inside a + // request memo. Without a shared layer, the request owns it, including a + // transient request with no persistent memo slot (dark-layers native runs). + run activeLocalSourceIsAttributedToLocalInsideTheRequestMemoTest = init.then(beginWith(0, LOCAL_ALONE, LOCAL_TTLS, Recovery::ALLOW)) + .expect(s.o.reads == 0 and s.o.loaders == 1 and s.sourceLayers == List("local")) + .then(settle(0, SOURCE_ERROR)).expect(s.d.fallbackErrors == List("local") and s.o.classifications == 0) + run requestOnlySourceIsAttributedToTheRequestMemoTest = init.then(beginWith(0, REQUEST_ALONE, TTLS, Recovery::ALLOW)) + .expect(s.o.reads == 0 and s.o.loaders == 1 and s.sourceLayers == List("request_local")) + .then(settle(0, SOURCE_ERROR)).expect(s.d.fallbackErrors == List("request_local") and s.o.classifications == 0) + run transientRequestOnlySourceKeepsItsAttributionTest = init.then(beginWith(2, REQUEST_ALONE, TTLS, Recovery::ALLOW)) .expect(s.o.reads == 0 and s.o.loaders == 1 and s.sourceLayers == List("request_local")) .then(settle(0, SOURCE_ERROR)).expect(s.d.fallbackErrors == List("request_local") and s.o.classifications == 0) // A bypass source started at admission has no layer and its failure records none. diff --git a/test/fixtures/shadow-read-deadlines-witnesses.json b/test/fixtures/shadow-read-deadlines-witnesses.json new file mode 100644 index 00000000..7bb6e34a --- /dev/null +++ b/test/fixtures/shadow-read-deadlines-witnesses.json @@ -0,0 +1,126 @@ +[ + { + "regression": "c0ReadDeadlineKeepsRawCapacityTest", + "trace": { + "source": { + "model": "formal/dialcache-shadow-read-deadlines-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/shadow-read-deadlines-witnesses.json/c0ReadDeadlineKeepsRawCapacityTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[],"sourceErrors":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"5"},"name":"advance"},"mbt::actionTaken":"advance","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"5"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["redis_error"],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[{"#bigint":"0"}],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["redis_error","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[{"#bigint":"0"}],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"},{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"3"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"3"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["redis_error","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[{"#bigint":"0"}],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"},{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"10"},"name":"advance"},"mbt::actionTaken":"advance","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"10"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["redis_error","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[{"#bigint":"0"}],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"},{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["redis_error","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[{"#bigint":"0"}],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"},{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"3"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"3"},"reads":{"#bigint":"2"},"recovery":[],"shadow":["redis_error","dropped"],"sourceScopes":[true,true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[{"#bigint":"0"}],"budgets":[{"#bigint":"5"},{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"},{"#bigint":"0"},{"#bigint":"0"}]}}} + ] + } + }, + { + "regression": "c1ReadDeadlineKeepsRawCapacityTest", + "trace": { + "source": { + "model": "formal/dialcache-shadow-read-deadlines-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/shadow-read-deadlines-witnesses.json/c1ReadDeadlineKeepsRawCapacityTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[],"sourceErrors":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"seed"},"mbt::actionTaken":"seed","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[],"sourceErrors":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"2"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"2"}}},"s":{"o":{"calls":[{"#bigint":"2"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseLoad"},"mbt::actionTaken":"releaseLoad","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"2"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"2"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[{"#bigint":"5"},{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"5"},"name":"advance"},"mbt::actionTaken":"advance","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"5"}}},"s":{"o":{"calls":[{"#bigint":"2"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"2"},"recovery":[],"shadow":["confirmation_error"],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[{"#bigint":"1"}],"budgets":[{"#bigint":"5"},{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"2"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"2"},"recovery":[],"shadow":["confirmation_error","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[{"#bigint":"1"}],"budgets":[{"#bigint":"5"},{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"},{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"3"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"3"}}},"s":{"o":{"calls":[{"#bigint":"2"},{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"2"},"recovery":[],"shadow":["confirmation_error","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[{"#bigint":"1"}],"budgets":[{"#bigint":"5"},{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"},{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"10"},"name":"advance"},"mbt::actionTaken":"advance","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"10"}}},"s":{"o":{"calls":[{"#bigint":"2"},{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"2"},"recovery":[],"shadow":["confirmation_error","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[{"#bigint":"1"}],"budgets":[{"#bigint":"5"},{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"},{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"2"},{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"2"},"recovery":[],"shadow":["confirmation_error","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[{"#bigint":"1"}],"budgets":[{"#bigint":"5"},{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"},{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"2"},{"#bigint":"1"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"3"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"3"},"reads":{"#bigint":"3"},"recovery":[],"shadow":["confirmation_error","dropped"],"sourceScopes":[true,true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[{"#bigint":"1"}],"budgets":[{"#bigint":"5"},{"#bigint":"5"},{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"},{"#bigint":"0"},{"#bigint":"0"}]}}} + ] + } + }, + { + "regression": "jobDeadlineThenReadDeadlineReportsOneVerdictTest", + "trace": { + "source": { + "model": "formal/dialcache-shadow-read-deadlines-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/shadow-read-deadlines-witnesses.json/jobDeadlineThenReadDeadlineReportsOneVerdictTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[],"sourceErrors":[]}}}, + {"input":{"choice":{"#bigint":"20"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"20"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[],"sourceErrors":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[{"#bigint":"20"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[{"#bigint":"20"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"10"},"name":"advance"},"mbt::actionTaken":"advance","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"10"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["timeout"],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[{"#bigint":"20"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["timeout","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[{"#bigint":"20"}],"sourceErrors":[{"#bigint":"0"},{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"3"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"3"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["timeout","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[{"#bigint":"20"}],"sourceErrors":[{"#bigint":"0"},{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"10"},"name":"advance"},"mbt::actionTaken":"advance","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"10"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["timeout","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[{"#bigint":"0"}],"budgets":[{"#bigint":"20"}],"sourceErrors":[{"#bigint":"0"},{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["timeout","dropped"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[{"#bigint":"0"}],"budgets":[{"#bigint":"20"}],"sourceErrors":[{"#bigint":"0"},{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"},{"#bigint":"1"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"3"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"3"},"reads":{"#bigint":"2"},"recovery":[],"shadow":["timeout","dropped"],"sourceScopes":[true,true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[{"#bigint":"0"}],"budgets":[{"#bigint":"20"},{"#bigint":"20"}],"sourceErrors":[{"#bigint":"0"},{"#bigint":"0"},{"#bigint":"0"}]}}} + ] + } + }, + { + "regression": "confirmationKeepsCapturedReadBudgetTest", + "trace": { + "source": { + "model": "formal/dialcache-shadow-read-deadlines-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/shadow-read-deadlines-witnesses.json/confirmationKeepsCapturedReadBudgetTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[],"sourceErrors":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"seed"},"mbt::actionTaken":"seed","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[],"sourceErrors":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"20"},"name":"policy"},"mbt::actionTaken":"policy","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"20"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"2"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"2"}}},"s":{"o":{"calls":[{"#bigint":"2"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseLoad"},"mbt::actionTaken":"releaseLoad","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"2"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"2"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[{"#bigint":"5"},{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"5"},"name":"advance"},"mbt::actionTaken":"advance","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"5"}}},"s":{"o":{"calls":[{"#bigint":"2"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"2"},"recovery":[],"shadow":["confirmation_error"],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[{"#bigint":"1"}],"budgets":[{"#bigint":"5"},{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"2"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"2"},"recovery":[],"shadow":["confirmation_error"],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[{"#bigint":"1"}],"budgets":[{"#bigint":"5"},{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"2"},{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"2"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"2"},"reads":{"#bigint":"3"},"recovery":[],"shadow":["confirmation_error"],"sourceScopes":[true,true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[{"#bigint":"1"}],"budgets":[{"#bigint":"5"},{"#bigint":"5"},{"#bigint":"20"}],"sourceErrors":[{"#bigint":"0"},{"#bigint":"0"}]}}} + ] + } + }, + { + "regression": "confirmationReadBudgetStartsAtItsOwnDispatchTest", + "trace": { + "source": { + "model": "formal/dialcache-shadow-read-deadlines-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/shadow-read-deadlines-witnesses.json/confirmationReadBudgetStartsAtItsOwnDispatchTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[],"sourceErrors":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"seed"},"mbt::actionTaken":"seed","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[],"sourceErrors":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"4"},"name":"advance"},"mbt::actionTaken":"advance","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"4"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"2"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"2"}}},"s":{"o":{"calls":[{"#bigint":"2"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseLoad"},"mbt::actionTaken":"releaseLoad","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"2"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"2"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[{"#bigint":"5"},{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"4"},"name":"advance"},"mbt::actionTaken":"advance","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"4"}}},"s":{"o":{"calls":[{"#bigint":"2"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"2"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[{"#bigint":"5"},{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"advance"},"mbt::actionTaken":"advance","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"2"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"1"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"2"},"recovery":[],"shadow":["confirmation_error"],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[{"#bigint":"1"}],"budgets":[{"#bigint":"5"},{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}} + ] + } + }, + { + "regression": "readBeforeDeadlineCanFillAndCancelsItsTimerTest", + "trace": { + "source": { + "model": "formal/dialcache-shadow-read-deadlines-conformance.qnt", + "recipe": "formal/fixture-recipes.json#test/fixtures/shadow-read-deadlines-witnesses.json/readBeforeDeadlineCanFillAndCancelsItsTimerTest" + }, + "states": [ + {"input":{"choice":{"#bigint":"-1"},"name":"init"},"mbt::actionTaken":"init","mbt::nondetPicks":{"choice":{"tag":"None","value":{"#tup":[]}}},"s":{"o":{"calls":[],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"0"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"0"},"reads":{"#bigint":"0"},"recovery":[],"shadow":[],"sourceScopes":[],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[],"sourceErrors":[]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"beginCall"},"mbt::actionTaken":"beginCall","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"0"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"1"},"name":"resolveLoader"},"mbt::actionTaken":"resolveLoader","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"1"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"4"},"name":"advance"},"mbt::actionTaken":"advance","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"4"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"0"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseRead"},"mbt::actionTaken":"releaseRead","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[],"writes":{"#bigint":"0"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseDump"},"mbt::actionTaken":"releaseDump","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":[],"sourceScopes":[true],"writeTtls":[{"#bigint":"60000"}],"writes":{"#bigint":"1"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"0"},"name":"releaseWrite"},"mbt::actionTaken":"releaseWrite","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"0"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["filled"],"sourceScopes":[true],"writeTtls":[{"#bigint":"60000"}],"writes":{"#bigint":"1"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}}, + {"input":{"choice":{"#bigint":"10"},"name":"advance"},"mbt::actionTaken":"advance","mbt::nondetPicks":{"choice":{"tag":"Some","value":{"#bigint":"10"}}},"s":{"o":{"calls":[{"#bigint":"1"}],"classifications":{"#bigint":"0"},"comparisons":{"#bigint":"0"},"dumps":{"#bigint":"1"},"invalidations":{"#bigint":"0"},"loaders":{"#bigint":"1"},"loads":{"#bigint":"0"},"maintenance":[],"policyCalls":{"#bigint":"1"},"reads":{"#bigint":"1"},"recovery":[],"shadow":["filled"],"sourceScopes":[true],"writeTtls":[{"#bigint":"60000"}],"writes":{"#bigint":"1"}},"d":{"ages":[],"coalesced":[],"configErrors":{"#bigint":"0"},"fallbackErrors":[],"warnings":{"#bigint":"0"},"futureOffsets":[]},"io":{"aborted":[],"budgets":[{"#bigint":"5"}],"sourceErrors":[{"#bigint":"0"}]}}} + ] + } + } +] diff --git a/test/formal-admission-budget-witnesses.test.ts b/test/formal-admission-budget-witnesses.test.ts new file mode 100644 index 00000000..12a49784 --- /dev/null +++ b/test/formal-admission-budget-witnesses.test.ts @@ -0,0 +1,40 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; +import { admissionWitnesses } from "../formal/replay/witnesses/admission.mjs"; +import { parseTrace, profiles } from "../formal/replay/features.mjs"; +import { witnessStates } from "../formal/replay/witnesses/trace.mjs"; + +const fixtures = JSON.parse(readFileSync(new URL("./fixtures/admission-budget-witnesses.json", import.meta.url), "utf8")) as Array<{ + trace: { states: Array<{ input: { name: string; choice: unknown }; s: { o: Record } }> }; +}>; +const label = "later-served-shadow-keeps-own-job-budget"; +const integer = (value: number) => ({ "#bigint": String(value) }); +function trace() { + // Classify explicit commands; simulator metadata is not witness evidence. + const copy = structuredClone(fixtures[0]!.trace); + return { states: copy.states.map(({ input, s }) => ({ input, s })) }; +} +function classified(raw: ReturnType): boolean { + return admissionWitnesses([{ ...parseTrace(raw, "trace.itf.json", profiles.admission!), + ...witnessStates(raw, "trace.itf.json") }]).has(label); +} + +describe("later admitted shadow budget witness", () => { + it("requires the later start, preserved caller and source-error outcome", () => { + expect(classified(trace())).toBe(true); + const tooEarly = trace(); + tooEarly.states[1]!.input.choice = integer(1); + expect(classified(tooEarly)).toBe(false); + for (const step of [5, 6]) { + const wrongCaller = trace(); + wrongCaller.states[step]!.s.o.calls = [integer(4)]; + expect(classified(wrongCaller)).toBe(false); + } + const earlyTimeout = trace(); + earlyTimeout.states[5]!.s.o.shadow = ["timeout"]; + expect(classified(earlyTimeout)).toBe(false); + const wrongVerdict = trace(); + wrongVerdict.states[6]!.s.o.shadow = ["timeout"]; + expect(classified(wrongVerdict)).toBe(false); + }); +}); diff --git a/test/formal-boundary-evidence.test.ts b/test/formal-boundary-evidence.test.ts new file mode 100644 index 00000000..fe57e476 --- /dev/null +++ b/test/formal-boundary-evidence.test.ts @@ -0,0 +1,29 @@ +import { writeFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { it } from "vitest"; + +import { boundaryTrace } from "../formal/boundary-replay.mjs"; +import { ReplayCoordinator, type ReplayRecording } from "../formal/replay/coordinator.mjs"; +import { replayThroughCoordinator } from "./formal/coordinated-replay.js"; + +const history = process.env.DIALCACHE_BOUNDARY_HISTORY; + +// The mutation runner judges the recorded comparisons. A driver, settlement, +// or cleanup failure still fails this test and makes the evidence unreached. +it.runIf(history !== undefined)("records the selected boundary history", async () => { + const out = process.env.DIALCACHE_BOUNDARY_OUT; + if (!out) throw new Error("DIALCACHE_BOUNDARY_OUT is required for a boundary replay"); + const traces = process.env.DIALCACHE_FEATURE_TRACE_DIR + ? resolve(process.env.DIALCACHE_FEATURE_TRACE_DIR, "..") : resolve(".formal-traces"); + const { profile, path } = boundaryTrace(history!, traces); + let recording: ReplayRecording = { path, divergences: [], completed: false, lastStep: -1 }; + const coordinator = new ReplayCoordinator({ record: true, onRecord: record => { recording = record; } }); + try { + await replayThroughCoordinator(profile, path, coordinator); + } catch (error) { + recording = { ...recording, completed: false, error: String(error) }; + throw error; + } finally { + writeFileSync(out, JSON.stringify({ history, via: "coordinator", ...recording }) + "\n"); + } +}, 30_000); diff --git a/test/formal-boundary-replay.test.ts b/test/formal-boundary-replay.test.ts new file mode 100644 index 00000000..6f4dd6fc --- /dev/null +++ b/test/formal-boundary-replay.test.ts @@ -0,0 +1,70 @@ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { resolve } from "node:path"; +import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; + +import { boundaryBaselines, boundaryTrace, goBoundarySelection, mutationBoundaries, readBoundaryRecording, type BoundaryRecording } from "../formal/boundary-replay.mjs"; + +const directory = mkdtempSync(resolve(tmpdir(), "dialcache-boundary-test-")); +const file = resolve(directory, "recordings.jsonl"); +const history = "shadow-layers/capturedRetentionTest"; +const path = boundaryTrace(history, directory).path; +const packet = { path, completed: true, lastStep: 7, divergences: [{ step: 7, action: "releaseWrite", paths: ["o.writeTtls.0"] }] }; +afterEach(() => rmSync(file, { force: true })); +afterAll(() => rmSync(directory, { recursive: true, force: true })); + +describe("boundary replay evidence", () => { + it.each([ + ["core", "TestCoreConformance", "DIALCACHE_MBT_TRACE_FILE"], + ["effects", "TestEffectsConformance", "DIALCACHE_EFFECTS_TRACE_FILE"], + ["local-clock", "TestLocalClockConformance", "DIALCACHE_FEATURE_TRACE_FILE"], + ["shadow-layers", "TestFeatureConformance", "DIALCACHE_FEATURE_TRACE_FILE"], + ])("selects only the %s history through its native Go driver", (profile, name, selector) => { + const selection = goBoundarySelection(profile, path); + expect(selection.test).toBe(name); + expect(selection.env[selector]).toBe(path); + expect(Object.entries(selection.env).filter(([key]) => key.endsWith("_DIR")).every(([, value]) => value === "")).toBe(true); + expect(Object.values(selection.env).filter(Boolean)).toEqual([path]); + }); + + it("requires exactly one terminal recording of the selected file", () => { + for (const value of ["", "not json", JSON.stringify({ ...packet, path: "/another/history.itf.json" }), + JSON.stringify({ ...packet, lastStep: -1, divergences: [] }), JSON.stringify({ ...packet, divergences: [...packet.divergences, ...packet.divergences] }), + `${JSON.stringify(packet)}\n${JSON.stringify(packet)}`]) { + writeFileSync(file, value); + expect(readBoundaryRecording(file, history, path, { status: 0 })).toMatchObject({ completed: false, lastStep: -1, error: expect.stringContaining("recording unavailable") }); + } + }); + + it("does not credit an early divergence after driver or native cleanup failure", () => { + writeFileSync(file, JSON.stringify(packet)); + expect(readBoundaryRecording(file, history, path, { status: 0 })).toMatchObject({ completed: true, divergences: packet.divergences }); + const output = JSON.stringify({ Output: " feature_replay_test.go:84: No pending dump 0\n" }); + expect(readBoundaryRecording(file, history, path, { status: 1, stdout: output })).toMatchObject({ + completed: false, divergences: packet.divergences, error: expect.stringContaining("No pending dump 0"), + }); + writeFileSync(file, JSON.stringify({ ...packet, completed: false, error: "step 8 releaseDump: Settlement violation: runnable task" })); + expect(readBoundaryRecording(file, history, path, { status: 1 })).toMatchObject({ + completed: false, divergences: packet.divergences, error: expect.stringContaining("step 8 releaseDump: Settlement violation"), + }); + }); + + it("requires clean complete baselines and replays shared histories once per mutant", () => { + const entries = [{ challenge: "one", history }, { challenge: "two", history }, { challenge: "backlog" }]; + const baseline: BoundaryRecording = { ...packet, history, via: "coordinator", divergences: [] }; + const replay = vi.fn(() => baseline); + expect(boundaryBaselines(entries, replay)).toEqual({ [history]: baseline }); + expect(replay).toHaveBeenCalledOnce(); + const assess = vi.fn((entry: typeof entries[number], record?: BoundaryRecording) => ({ challenge: entry.challenge, record })); + replay.mockClear(); + expect(mutationBoundaries(entries, replay, assess)).toHaveLength(3); + expect(replay).toHaveBeenCalledOnce(); + expect(assess).toHaveBeenLastCalledWith(entries[2], undefined); + expect(() => boundaryBaselines(entries, () => ({ ...baseline, divergences: packet.divergences }))).toThrow(/zero divergences/); + expect(() => boundaryBaselines(entries, () => ({ ...baseline, completed: false, error: "driver failure" }))).toThrow(/driver failure/); + }); + + it("refuses histories that escape the regression tree", () => { + for (const value of ["../other", "scope/../test", "scope/run.json", "scope//run"]) expect(() => boundaryTrace(value)).toThrow(/Invalid boundary history/); + }); +}); diff --git a/test/formal-boundary-report.test.ts b/test/formal-boundary-report.test.ts new file mode 100644 index 00000000..86ff34d8 --- /dev/null +++ b/test/formal-boundary-report.test.ts @@ -0,0 +1,212 @@ +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { afterEach, describe, expect, it } from "vitest"; + +type Evidence = { challenge: string; mutant: string; history: string; step: number; fields: string[] }; +type Recording = { path: string; completed: boolean; lastStep: number; divergences: Array<{ step: number; paths: string[] }>; error?: string }; +type Verdict = { state: string; matched?: string[]; reason?: string; divergences?: unknown[] }; +const { assessBoundary, boundaryReview, validateBoundaryReportFreshness, fingerprintFiles, sha256, languages } = await import(new URL("../formal/mutation-reports.mjs", import.meta.url).href) as { + assessBoundary(evidence: Evidence | { challenge: string; mutant: string; state: string }, recording?: Recording): Verdict; + boundaryReview(report: unknown, evidence: unknown[], options?: { requireEntries?: boolean }): Verdict[]; + validateBoundaryReportFreshness(report: unknown, options?: { directory?: string }): void; + fingerprintFiles(directory: string, paths: string[]): { files: number; sha256: string }; + sha256(bytes: Buffer): string; + languages: Record<"ts" | "go", { inputs: string[] }>; +}; +const evidence: Evidence = { challenge: "captured-retention", mutant: "M29", history: "shadow-layers/capturedTest", step: 7, fields: ["o.writeTtls", "o.shadow"] }; +const record = (changes: Partial = {}): Recording => ({ + path: "/tmp/corpus/regressions/shadow-layers/capturedTest.itf.json", completed: true, lastStep: 11, + divergences: [{ step: 4, paths: ["o.policyCalls"] }, { step: 6, paths: ["o.policyCalls", "o.writeTtls.0"] }, { step: 7, paths: ["o.policyCalls", "o.writeTtls.0"] }], + ...changes, +}); + +const temporaryDirectories: string[] = []; +afterEach(() => { for (const directory of temporaryDirectories.splice(0)) rmSync(directory, { recursive: true, force: true }); }); + +function measuredSnapshot(port: "ts" | "go") { + const directory = mkdtempSync(join(tmpdir(), "dialcache-boundary-freshness-")); + temporaryDirectories.push(directory); + const corpusPaths = [".formal-traces/conformance", ".formal-traces/effects", ".formal-traces/features", ".formal-traces/regressions"]; + for (const path of ["src", "test", "formal", "go", ...corpusPaths]) mkdirSync(join(directory, path), { recursive: true }); + const files: Record = { + "src/cache.ts": "export const value = 1;", "test/replay.ts": "compare(actual, expected);", + "formal/model.qnt": "pure val accepted = true", "formal/mutations.json": '{"mutations":[]}', + "go/cache.go": "package cache", "go/go.mod": "module cache", "go/go.sum": "dependency checksum", + "package.json": '{"type":"module"}', "pnpm-lock.yaml": "lockfileVersion: 9.0", "tsconfig.json": "{}", "vitest.config.ts": "export default {};", + ".formal-traces/regressions/shadow-layers/capturedTest.itf.json": '{"states":[{"input":{"name":"init"}}]}', + }; + for (const [path, content] of Object.entries(files)) { + mkdirSync(dirname(join(directory, path)), { recursive: true }); + writeFileSync(join(directory, path), content); + } + const configuration = ["package.json", "pnpm-lock.yaml", "tsconfig.json", "vitest.config.ts"]; + const report: Record = { + complete: true, catalogSha256: sha256(readFileSync(join(directory, "formal/mutations.json"))), + inputs: fingerprintFiles(directory, languages[port].inputs), corpus: fingerprintFiles(directory, corpusPaths), + ...(port === "go" ? { go: "go version go1.27.1" } : { + configurationSha256: Object.fromEntries(configuration.map(path => [path, sha256(readFileSync(join(directory, path)))])), + }), + mutations: [{ id: "M29", cohorts: {}, boundary: [assessBoundary(evidence, record())] }], + boundaryBaselines: { [evidence.history]: { ...record({ divergences: [] }), history: evidence.history } }, + }; + return { directory, report }; +} + +describe("gated boundary report freshness", () => { + it("fails the actual gated command on stale inputs while historical inspection succeeds", async () => { + const { boundaryEvidence } = await import(new URL("../formal/execution.mjs", import.meta.url).href) as { + boundaryEvidence(): Array; + }; + const mutations = new Map(); + const baselines: Record = {}; + for (const entry of boundaryEvidence()) { + if (!mutations.has(entry.mutant)) mutations.set(entry.mutant, { id: entry.mutant, cohorts: {}, boundary: [] }); + if ("state" in entry) mutations.get(entry.mutant)!.boundary.push(entry); + else { + mutations.get(entry.mutant)!.boundary.push(assessBoundary(entry, record({ + path: `regressions/${entry.history}.itf.json`, lastStep: entry.step, divergences: [{ step: entry.step, paths: [entry.fields[0]!] }], + }))); + baselines[entry.history] = { history: entry.history, completed: true, lastStep: Math.max(baselines[entry.history]?.lastStep ?? 0, entry.step), divergences: [] }; + } + } + const { directory, report } = measuredSnapshot("go"); + Object.assign(report, { + catalogSha256: sha256(readFileSync(new URL("../formal/mutations.json", import.meta.url))), + inputs: { files: 0, sha256: "stale" }, mutations: [...mutations.values()], boundaryBaselines: baselines, + }); + const path = join(directory, "historical-report.json"); + writeFileSync(path, JSON.stringify(report)); + const args = [fileURLToPath(new URL("../formal/mutation-reports.mjs", import.meta.url)), "boundary", "--report", path]; + const historical = spawnSync(process.execPath, args, { encoding: "utf8" }); + expect(historical.status, historical.stderr).toBe(0); + const gated = spawnSync(process.execPath, [...args, "--gate"], { encoding: "utf8" }); + expect(gated.status).toBe(1); + expect(gated.stderr).toContain("source inputs differ from the measured report"); + }); + + it.each(["ts", "go"] as const)("accepts the original %s snapshot but rejects changed sources with unchanged boundary declarations", port => { + const { directory, report } = measuredSnapshot(port); + expect(() => validateBoundaryReportFreshness(report, { directory })).not.toThrow(); + for (const path of [port === "ts" ? "src/cache.ts" : "go/cache.go", "formal/model.qnt"]) { + const original = readFileSync(join(directory, path)); + writeFileSync(join(directory, path), "changed behavior at the same named checkpoint"); + // Historical inspection can still interpret the old recording, but its + // unchanged mapping cannot make it evidence for the modified checkout. + expect(boundaryReview(report, [evidence], { requireEntries: true })[0]?.state).toBe("confirmed"); + expect(() => validateBoundaryReportFreshness(report, { directory })).toThrow(/source inputs differ/); + writeFileSync(join(directory, path), original); + } + }); + + it.each(["ts", "go"] as const)("rejects a changed catalog or corpus for %s", port => { + const { directory, report } = measuredSnapshot(port); + const catalog = join(directory, "formal/mutations.json"), original = readFileSync(catalog); + writeFileSync(catalog, '{"mutations":[{"id":"M29","after":"different fault"}]}'); + expect(() => validateBoundaryReportFreshness(report, { directory })).toThrow(/mutation catalog differs/); + writeFileSync(catalog, original); + writeFileSync(join(directory, ".formal-traces/regressions/shadow-layers/capturedTest.itf.json"), '{"states":[]}'); + expect(() => validateBoundaryReportFreshness(report, { directory })).toThrow(/corpus differs/); + }); + + it("checks separately recorded TypeScript configuration and Go module configuration", () => { + const ts = measuredSnapshot("ts"), go = measuredSnapshot("go"); + writeFileSync(join(ts.directory, "pnpm-lock.yaml"), "changed dependency resolution"); + expect(() => validateBoundaryReportFreshness(ts.report, ts)).toThrow(/TypeScript configuration differs/); + writeFileSync(join(go.directory, "go/go.mod"), "module changed"); + expect(() => validateBoundaryReportFreshness(go.report, go)).toThrow(/source inputs differ/); + }); + + it("refuses missing fingerprints or ambiguous language metadata without blocking historical inspection", () => { + const { directory, report } = measuredSnapshot("ts"); + for (const field of ["catalogSha256", "inputs", "corpus", "configurationSha256"]) { + const incomplete = { ...report }; + delete incomplete[field]; + expect(() => validateBoundaryReportFreshness(incomplete, { directory })).toThrow(); + expect(boundaryReview(incomplete, [evidence])[0]?.state).toBe("confirmed"); + } + expect(() => validateBoundaryReportFreshness({ ...report, go: "go version go1.27.1" }, { directory })).toThrow(/unambiguous/); + }); +}); + +describe("native boundary evidence", () => { + it("credits the captured TTL at its checkpoint after an earlier unrelated provider call", () => { + expect(assessBoundary(evidence, record())).toMatchObject({ state: "confirmed", matched: ["o.writeTtls.0"] }); + expect(assessBoundary({ ...evidence, fields: ["o.policyCalls"] }, record()).state).toBe("side-effect-only"); + expect(assessBoundary({ ...evidence, step: 4, fields: ["o.policyCalls"] }, record()).state).toBe("confirmed"); + }); + + it("credits newly divergent decode counters but not a carried counter", () => { + const target = { ...evidence, fields: ["o.calls", "o.dumps", "o.loads"] }; + expect(assessBoundary(target, record({ divergences: [{ step: 7, paths: ["o.dumps", "o.loads"] }] })).state).toBe("confirmed"); + expect(assessBoundary(target, record({ divergences: [{ step: 6, paths: ["o.dumps"] }, { step: 7, paths: ["o.dumps"] }] })).state).toBe("side-effect-only"); + }); + + it("requires a core caller result instead of carrying an earlier missing-publication count", () => { + const history = "core/invalidationRefillsFromCurrentSourceTest"; + const target = { challenge: "conformance-remote-miss-skips-publication", mutant: "M38", history, step: 3, fields: ["lastResult"] }; + const recording = { + path: `/tmp/regressions/${history}.itf.json`, completed: true, lastStep: 5, + divergences: [{ step: 1, paths: ["redisWrites"] }, { step: 2, paths: ["redisWrites"] }, + { step: 3, paths: ["lastResult", "remoteLoaderCalls", "redisWrites"] }], + }; + expect(assessBoundary(target, recording)).toMatchObject({ state: "confirmed", matched: ["lastResult"] }); + expect(assessBoundary({ ...target, fields: ["redisWrites"] }, recording).state).toBe("side-effect-only"); + expect(assessBoundary(target, { ...recording, divergences: recording.divergences.map(entry => ({ + ...entry, paths: entry.paths.filter(path => path !== "lastResult"), + })) }).state).toBe("side-effect-only"); + expect(assessBoundary(target, { ...recording, completed: false, error: "driver failed after result" }).state).toBe("unreached"); + }); + + it("keeps failed or incomplete replays outside detection even after a matching divergence", () => { + expect(assessBoundary(evidence, record({ completed: false, lastStep: 7, error: "step 8 releaseDump: No pending dump 0" }))).toMatchObject({ state: "unreached", reason: expect.stringContaining("step 8 releaseDump") }); + expect(assessBoundary(evidence, record({ completed: false, error: "step 8: Settlement violation: read gates" }))).toMatchObject({ state: "unreached", reason: expect.stringContaining("Settlement violation") }); + expect(assessBoundary(evidence, record({ lastStep: 6 })).state).toBe("unreached"); + expect(assessBoundary(evidence, record({ path: "/tmp/regressions/shadow-layers/otherTest.itf.json" })).state).toBe("unreached"); + expect(assessBoundary(evidence).state).toBe("unreached"); + expect(assessBoundary(evidence, record({ divergences: [{ step: 7, paths: ["o.writeTtls.0"] }, { step: 7, paths: ["o.shadow"] }] })).state).toBe("unreached"); + }); + + it("reports clean mutants and unmapped execution boundaries explicitly", () => { + expect(assessBoundary(evidence, record({ divergences: [] })).state).toBe("not-divergent"); + for (const state of ["vector", "unreproduced"]) expect(assessBoundary({ challenge: "gap", mutant: "M06", state }).state).toBe(state); + }); + + it("requires a completed recording even when legacy assertions claim the target mismatch", () => { + const message = `/tmp/regressions/${evidence.history}.itf.json step 7 action releaseWrite\n` + + 'expected: {"o":{"writeTtls":[120000]}}\nactual: {"o":{"writeTtls":[180000]}}'; + for (const generated of [ + { assertionEvidence: { failure: message } }, + { divergences: [{ history: evidence.history, step: 7, paths: ["o.writeTtls.0"] }] }, + ]) { + const report = { mutations: [{ id: "M29", cohorts: { generated } }] }; + const original = structuredClone(report); + for (const requireEntries of [false, true]) { + expect(boundaryReview(report, [evidence], { requireEntries })) + .toEqual([expect.objectContaining({ state: "unreached", divergences: [] })]); + } + expect(report).toEqual(original); + } + }); + + it("requires an explicit entry for coverage limitations when gating a report", () => { + const limitation = { challenge: "not-yet-reproduced", mutant: "M29", state: "unreproduced" }; + const report = { mutations: [{ id: "M29", cohorts: {} }] }; + expect(boundaryReview(report, [limitation])[0]?.state).toBe("unreproduced"); + expect(boundaryReview(report, [limitation], { requireEntries: true })[0]).toMatchObject({ state: "unreached", reason: expect.stringContaining("per-challenge") }); + expect(boundaryReview({ mutations: [{ ...report.mutations[0], boundary: [limitation] }] }, [limitation], { requireEntries: true })[0]?.state).toBe("unreproduced"); + }); + + it("refuses missing mutants, stale pins and missing clean baselines when reading reports", () => { + expect(boundaryReview({ mutations: [] }, [evidence])[0]).toMatchObject({ state: "unreached", reason: expect.stringContaining("absent") }); + const confirmed = assessBoundary(evidence, record()); + const report = { mutations: [{ id: "M29", cohorts: {}, boundary: [confirmed] }], + boundaryBaselines: { [evidence.history]: { ...record({ divergences: [] }), history: evidence.history } } }; + expect(boundaryReview(report, [evidence])[0]?.state).toBe("confirmed"); + expect(boundaryReview(report, [{ ...evidence, step: 8 }])[0]).toMatchObject({ state: "unreached", reason: expect.stringContaining("current evidence") }); + expect(boundaryReview({ ...report, boundaryBaselines: {} }, [evidence])[0]).toMatchObject({ state: "unreached", reason: expect.stringContaining("baseline") }); + expect(boundaryReview({ ...report, mutations: [{ id: "M29", cohorts: {}, boundary: [{ ...confirmed, divergences: [] }] }] }, [evidence])[0]?.state).toBe("not-divergent"); + }); +}); diff --git a/test/formal-conformance.test.ts b/test/formal-conformance.test.ts index c4355d9d..227ab1fe 100644 --- a/test/formal-conformance.test.ts +++ b/test/formal-conformance.test.ts @@ -101,7 +101,7 @@ class ConformanceDriver { snapshot(): Observation { return { sourceVersion: this.sourceVersion, - lastResult: this.lastResult, + lastResult: this.lastResult === undefined ? { absent: true } : this.lastResult, ...this.counters, redisReads: this.redis.getCalls + this.redis.mGetCalls, redisWrites: this.redis.setCalls, diff --git a/test/formal-dark-layers-witnesses.test.ts b/test/formal-dark-layers-witnesses.test.ts new file mode 100644 index 00000000..0e3c08f0 --- /dev/null +++ b/test/formal-dark-layers-witnesses.test.ts @@ -0,0 +1,108 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; +import { darkLayersWitnesses, darkLayersWitnessRules } from "../formal/replay/witnesses/dark-layers.mjs"; +import { darkLayersProfile } from "../formal/replay/profiles/dark-layers.mjs"; +import { projectObservation } from "../formal/replay/features.mjs"; +import type { ObservedEvent } from "./formal/behavior-driver.js"; +import { emptyObservation } from "../formal/replay/observation.mjs"; + +const fixtures = JSON.parse(readFileSync(new URL("./fixtures/dark-layers-witnesses.json", import.meta.url), "utf8")) as Array<{ + regression: string; + trace: { states: Array<{ s: { o: Record; d: Record } }> }; +}>; +const integer = (value: number) => ({ "#bigint": String(value) }); +function classify(trace: { states: unknown[] }): Set { + return darkLayersWitnesses([{ path: "trace.itf.json", states: trace.states }]); +} +function traceFor(name: string) { + const rule = darkLayersWitnessRules.find(rule => rule.name === name)!; + return structuredClone(fixtures.find(fixture => fixture.regression === rule.regression)!.trace); +} + +describe("held dark-layer public witnesses", () => { + for (const rule of darkLayersWitnessRules) { + it(`requires each public checkpoint for ${rule.name}`, () => { + const original = traceFor(rule.name); + expect(classify(original).has(rule.name)).toBe(true); + for (const checkpoint of rule.checkpoints) { + const corrupted = structuredClone(original); + const fields = Object.keys(checkpoint.observation).length ? checkpoint.observation : checkpoint.diagnostics!; + const target = Object.keys(checkpoint.observation).length ? corrupted.states[checkpoint.step]!.s.o : corrupted.states[checkpoint.step]!.s.d; + const key = Object.keys(fields)[0]!, value = target[key]; + target[key] = Array.isArray(value) ? (value.length ? [] : [integer(999)]) : integer(999); + expect(classify(corrupted).has(rule.name)).toBe(false); + } + }); + } + it("requires each snapshot field, including ownership and the replacement oldest age", () => { + for (const rule of darkLayersWitnessRules.filter(rule => rule.name.startsWith("inspection-"))) { + for (const checkpoint of rule.checkpoints) { + const original = traceFor(rule.name); + const snapshots = original.states[checkpoint.step]!.s.d.inspections as Array>; + for (const field of ["instance", "activeLeaders", "activeFollowers", "oldestLeaderAgeMs"]) { + const corrupted = structuredClone(original); + const actual = (corrupted.states[checkpoint.step]!.s.d.inspections as Array>).at(-1)!; + actual[field] = integer(999); + expect(classify(corrupted).has(rule.name), `${rule.name} step ${checkpoint.step} ${field}`).toBe(false); + } + expect(snapshots.length).toBeGreaterThan(0); + } + } + }); + it("treats snapshot record member order as immaterial", () => { + const name = "inspection-counts-process-followers-and-oldest", trace = traceFor(name); + for (const state of trace.states) { + state.s.d.inspections = (state.s.d.inspections as Array>).map(snapshot => Object.fromEntries(Object.entries(snapshot).reverse())); + } + expect(classify(trace).has(name)).toBe(true); + }); + it("projects only the actual public snapshot and rejects malformed numeric fields", () => { + const snapshot: ObservedEvent = { event: "coalescingState", instance: "0", activeLeaders: 1, activeFollowers: 2, oldestLeaderAgeMs: 3 }; + const observed = { ...emptyObservation(), events: [snapshot] }; + expect(projectObservation(darkLayersProfile, observed)).toMatchObject({ d: { inspections: [{ instance: 0, activeLeaders: 1, activeFollowers: 2, oldestLeaderAgeMs: 3 }] } }); + snapshot.activeLeaders = 0; + snapshot.activeFollowers = 0; + snapshot.oldestLeaderAgeMs = null; + expect(projectObservation(darkLayersProfile, observed)).toMatchObject({ d: { inspections: [{ instance: 0, activeLeaders: 0, activeFollowers: 0, oldestLeaderAgeMs: null }] } }); + for (const [field, value] of [["activeLeaders", -1], ["activeFollowers", 0.5], ["oldestLeaderAgeMs", Number.NaN], ["instance", "other"]] as const) { + expect(() => projectObservation(darkLayersProfile, { ...emptyObservation(), events: [{ ...snapshot, [field]: value }] })).toThrow(/Malformed actual coalescing inspection/); + } + }); + it("does not credit the strict fill fence when serialization starts", () => { + const name = "dark-fill-at-watermark-is-fenced", trace = traceFor(name); + trace.states[5]!.s.o.dumps = integer(1); + expect(classify(trace).has(name)).toBe(false); + }); + it("does not credit source-error isolation when dark work decodes or fills", () => { + const name = "rejected-dark-source-seeds-no-layer"; + for (const field of ["loads", "dumps", "writes"]) { + const trace = traceFor(name); + trace.states[4]!.s.o[field] = integer(1); + expect(classify(trace).has(name)).toBe(false); + } + }); + it("requires the later dark read's captured watermark to stop its fill", () => { + const name = "stale-visible-dark-c0-fills-unfenced", trace = traceFor(name); + trace.states[11]!.s.o.shadow = ["filled", "filled"]; + expect(classify(trace).has(name)).toBe(false); + }); + it("requires both denied admission and later released capacity for every held effect", () => { + for (const rule of darkLayersWitnessRules.filter(rule => rule.name.startsWith("timed-out-dark-"))) { + const dropped = rule.checkpoints[1]!, readmitted = rule.checkpoints.at(-1)!; + const early = traceFor(rule.name); + early.states[dropped.step]!.s.o.reads = integer(Number(dropped.observation.reads) + 1); + expect(classify(early).has(rule.name)).toBe(false); + const leaked = traceFor(rule.name); + leaked.states[readmitted.step]!.s.o.reads = integer(Number(readmitted.observation.reads) - 1); + expect(classify(leaked).has(rule.name)).toBe(false); + } + }); + it("requires the profile's diagnostic identity and keeps the existing default", () => { + const observed = { ...emptyObservation(), events: [{ event: "coalesced" as const, cacheNamespace: "urn", keyType: "id", useCase: "DarkLayers", scope: "request_local" }] }; + expect(projectObservation(darkLayersProfile, observed)).toMatchObject({ d: { coalesced: ["request_local"] } }); + expect(() => projectObservation({ ...darkLayersProfile, diagnosticUseCase: "Behavior" }, observed)).toThrow(); + observed.events[0]!.useCase = "Behavior"; + const { diagnosticUseCase: _identity, ...defaultIdentity } = darkLayersProfile; + expect(projectObservation(defaultIdentity, observed)).toMatchObject({ d: { coalesced: ["request_local"] } }); + }); +}); diff --git a/test/formal-differential.test.ts b/test/formal-differential.test.ts index 66698600..a9dbf13a 100644 --- a/test/formal-differential.test.ts +++ b/test/formal-differential.test.ts @@ -27,6 +27,7 @@ const differential = await import(new URL("../formal/differential.mjs", import.m replayHistories(tree: string, model: Model, descriptor: unknown, histories: History[], options: { chunk: number; output: string; concurrency: number }): Promise; composedProfiles(manifest: { models: Array<{ path: string; profile?: string }> }, options?: { cwd?: string }): string[]; prepare(reference: string, options?: { cwd?: string; output?: string }): { reference: { revision: string; tree: string; manifests: Manifests }; candidate: { tree: string; manifests: Manifests } }; + runDifferential(profileId: string, prepared: ReturnType, options: { chunk: number; output: string; concurrency: number; log: (message: string) => void }): Promise; selectProfiles(prepared: { reference: { manifests: Manifests; tree: string }; candidate: { manifests: Manifests; tree: string } }): string[]; shardProfiles(names: string[], index: number, count: number): string[]; closureSkip(reference: Model, candidate: Model, referenceSources: Record, candidateSources: Record): string | null; @@ -221,7 +222,7 @@ describe("corpus differential comparison", () => { }, 30_000); it("selects the composed profiles by their kernel imports in either revision, following helper libraries, and lists every Quint source", () => { - expect(differential.composedProfiles(readExecution())).toEqual(["effects", "recovery", "policy", "shadow", "scope", "admission", "layers", "independent", "recovery-read", "local-failure", "runtime-boundaries", "shadow-layers", "local-clock", "source-budgets"]); + expect(differential.composedProfiles(readExecution())).toEqual(["effects", "recovery", "policy", "shadow", "scope", "admission", "layers", "independent", "recovery-read", "local-failure", "runtime-boundaries", "shadow-layers", "local-clock", "source-budgets", "dark-layers", "shadow-read-deadlines"]); // A profile composed only at the reference (a rewrite off the library) is still selected. const referenceTree = mkdtempSync(join(tmpdir(), "differential-reference-")); const candidateTree = mkdtempSync(join(tmpdir(), "differential-candidate-")); @@ -259,21 +260,20 @@ describe("corpus differential comparison", () => { expect([...sources].sort()).toEqual(sources); }); - it("deals the composed profiles round-robin into shards by sorted name, every profile in exactly one shard, and refuses an index outside 1..count", () => { - const names = ["shadow", "admission", "recovery-read", "effects", "layers", "recovery", "scope"]; - const sorted = [...names].sort(); + it("balances profiles deterministically, assigns every name once, and refuses an index outside 1..count", () => { + const names = ["shadow", "admission", "recovery-read", "effects", "layers", "recovery", "scope", "new-profile", "shadow"]; + const sorted = [...new Set(names)].sort(); for (const count of [1, 2, 3, 4, 7, 9]) { const shards = Array.from({ length: count }, (_, position) => differential.shardProfiles(names, position + 1, count)); // The union is the input with no name repeated, whatever the input's order. expect(shards.flat().sort(), `${count} shards`).toEqual(sorted); - expect(shards.flat(), `${count} shards`).toHaveLength(names.length); + expect(shards.flat(), `${count} shards`).toHaveLength(sorted.length); expect(shards, `${count} shards`).toEqual(Array.from({ length: count }, (_, position) => differential.shardProfiles(sorted, position + 1, count))); } - // Round-robin, not contiguous slices: alphabetical neighbours (recovery, recovery-read) part ways. - expect(differential.shardProfiles(names, 1, 4)).toEqual(["admission", "recovery-read"]); - expect(differential.shardProfiles(names, 2, 4)).toEqual(["effects", "scope"]); - expect(differential.shardProfiles(names, 3, 4)).toEqual(["layers", "shadow"]); - expect(differential.shardProfiles(names, 4, 4)).toEqual(["recovery"]); + // Keep the dominant shadow workload apart from the next-largest profiles. + const shards = Array.from({ length: 4 }, (_, position) => differential.shardProfiles(names, position + 1, 4)); + expect(shards.find(shard => shard.includes("shadow"))).toEqual(["shadow"]); + expect(shards.find(shard => shard.includes("effects"))).not.toContain("recovery"); expect(differential.shardProfiles(names, 1, 1)).toEqual(sorted); // A shard past the number of profiles is empty, not an error. expect(differential.shardProfiles(["a", "b"], 3, 3)).toEqual([]); @@ -284,6 +284,20 @@ describe("corpus differential comparison", () => { } }); + it("keeps the measured slow profiles apart for the full composed inventory, including currently skipped profiles", () => { + const names = differential.composedProfiles(readExecution()); + const shards = Array.from({ length: 4 }, (_, position) => differential.shardProfiles(names, position + 1, 4)); + expect(shards.flat().sort()).toEqual([...names].sort()); + const shadow = shards.find(shard => shard.includes("shadow"))!; + expect(shadow).not.toEqual(expect.arrayContaining(["layers"])); + expect(shadow).not.toEqual(expect.arrayContaining(["recovery"])); + expect(shadow).not.toEqual(expect.arrayContaining(["effects"])); + expect(shards.flat()).toEqual(expect.arrayContaining(["effects", "dark-layers", "shadow-read-deadlines"])); + // Names without timing history still distribute evenly and deterministically. + expect([1, 2, 3].map(index => differential.shardProfiles(["z", "a", "b", "c"], index, 3))) + .toEqual([["a", "z"], ["b"], ["c"]]); + }); + it("refuses --shard outside --composed and a malformed shard before preparing any tree", () => { const output = mkdtempSync(join(tmpdir(), "differential-shard-")); try { @@ -314,6 +328,46 @@ describe.skipIf(!quintAvailable)("corpus differential replay through a tree", () const smokeHistory = (descriptor: unknown) => parseTrace(JSON.parse(readFileSync(resolve(root, "formal/layers-smoke.itf.json"), "utf8")), "layers-smoke", descriptor); const copyTree = (into: string) => { copySources(root, into); return into; }; + it("reports every replay batch in both directions while preserving all history verdicts", async () => { + const reference = copyTree(join(output, "progress-reference")); + const candidate = copyTree(join(output, "progress-candidate")); + const smallCorpus = (traces: number) => { + const manifests = differential.readManifests(root); + const model = manifests.execution.models.find(model => model.profile === "layers")!; + Object.assign(model, { generate: { ...(model.generate as object), maxSamples: traces, maxSteps: 4, traces }, replayRegressions: [] }); + return manifests; + }; + const prepared = { + reference: { revision: "test-reference", tree: reference, manifests: smallCorpus(2) }, + candidate: { tree: candidate, manifests: smallCorpus(3) }, + }; + // Different corpus sizes prevent the unchanged-input skip and exercise an + // uneven final batch without changing either model's behavior. + const run = async (name: string) => { + const logs: string[] = []; + const report = await differential.runDifferential("layers", prepared, + { chunk: 2, output: join(output, name), concurrency: 2, log: message => logs.push(message) }); + expect(report.forward).toMatchObject({ sampled: 2, regressions: 0 }); + expect(report.reverse).toMatchObject({ sampled: 3, regressions: 0 }); + expect(logs).toContain("layers: 1 forward and 2 reverse replay batches, up to 2 concurrent processes."); + const batches = logs.filter(line => line.includes("batches complete")); + expect(batches).toHaveLength(3); + expect(batches.filter(line => line.startsWith("layers forward:"))).toEqual([ + expect.stringMatching(/^layers forward: 1\/1 batches complete; batch 1 replayed 2 histories in [\d.]+ s \([\d.]+ s elapsed\)\.$/), + ]); + const reverse = batches.filter(line => line.startsWith("layers reverse:")); + expect(reverse[0]).toContain("1/2 batches complete"); + expect(reverse[1]).toContain("2/2 batches complete"); + expect(reverse.filter(line => line.includes("batch 1 replayed 2 histories"))).toHaveLength(1); + expect(reverse.filter(line => line.includes("batch 2 replayed 1 histories"))).toHaveLength(1); + return report; + }; + const clean = await run("progress-clean"); + expect(clean.forward.disagreed).toBe(0); + expect(clean.reverse.disagreed).toBe(0); + expect(differential.verdict(clean).failed).toBe(false); + }, 180_000); + it("shares one constrained clone per input pair across the histories of a chunk", async () => { const { model } = layers(); const parse = spawnSync("quint", ["parse", model.path, `--out=${output}/parsed.json`, `--source-map=${output}/map.json`], { cwd: root, encoding: "utf8" }); diff --git a/test/formal-divergence.test.ts b/test/formal-divergence.test.ts new file mode 100644 index 00000000..12c7b6ea --- /dev/null +++ b/test/formal-divergence.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from "vitest"; +import assert, { AssertionError } from "node:assert/strict"; +import { assertObservation, countingPaths, diffPaths, isObservationComparison } from "../formal/replay/divergence.mjs"; + +describe("comparison divergence paths", () => { + it("tags only a complete comparison, preserving the original assertion and records", () => { + const capture = (run: () => void) => { try { run(); } catch (error) { return error; } throw new Error("Expected an assertion"); }; + const complete = capture(() => assertObservation({ o: { loads: 2 } }, { o: { loads: 1 } })); + expect(complete).toBeInstanceOf(AssertionError); + expect(isObservationComparison(complete)).toBe(true); + expect(complete).toMatchObject({ actual: { o: { loads: 2 } }, expected: { o: { loads: 1 } } }); + const project = () => { assert.equal("served", undefined); return {}; }; + const partial = capture(() => assertObservation(project(), {})); + expect(partial).toBeInstanceOf(AssertionError); + expect(isObservationComparison(partial)).toBe(false); + }); + + it("compares named fields, array elements and lengths without losing missing members", () => { + expect(diffPaths({ o: { calls: [1], loaders: 1 } }, { o: { calls: [2, 1], loaders: 1 } })) + .toEqual(["o.calls.0", "o.calls.1", "o.calls.length"]); + expect(diffPaths({ a: undefined }, {})).toEqual(["a"]); + expect(diffPaths([], [undefined])).toEqual(["0", "length"]); + expect(diffPaths({ o: { calls: [] } }, { o: { calls: [] } })).toEqual([]); + expect(diffPaths(1, 2)).toEqual(["$"]); + }); + + it("counts the carried write consequence at M29's checkpoint, never its earlier provider call", () => { + const divergences = [ + { step: 4, paths: ["o.policyCalls"] }, + { step: 6, paths: ["o.policyCalls", "o.writeTtls.0"] }, + { step: 7, paths: ["o.policyCalls", "o.writeTtls.0"] }, + ]; + expect(countingPaths(["o.writeTtls", "o.shadow"], divergences[2]!.paths, divergences[1]!.paths)).toEqual(["o.writeTtls.0"]); + expect(countingPaths(["o.policyCalls", "o.writeTtls"], divergences[2]!.paths, divergences[1]!.paths)).toEqual(["o.writeTtls.0"]); + }); + + it("counts new counters where that effect is the boundary, including flat projections", () => { + expect(countingPaths(["o.calls", "o.dumps", "o.loads"], ["o.dumps", "o.loads"], [])).toEqual(["o.dumps", "o.loads"]); + expect(countingPaths(["o.dumps"], ["o.dumps"], ["o.dumps"])).toEqual([]); + expect(countingPaths(["loaders", "calls"], ["loaders", "calls.0"], ["loaders", "calls.0"])).toEqual(["calls.0"]); + expect(countingPaths(["d.configErrors", "d.warnings"], ["d.configErrors", "d.warnings"], ["d.configErrors"])) + .toEqual(["d.warnings"]); + }); + + it("requires the declared field itself or a descendant and never credits a scalar assertion", () => { + expect(countingPaths(["o.calls"], ["o", "o.callsExtra", "$", "o.calls.0"], [])).toEqual(["o.calls.0"]); + // A counter repaired at k-1 can newly diverge at k; older differences do not suppress it. + expect(countingPaths(["o.reads"], ["o.reads"], [])).toEqual(["o.reads"]); + }); + + it("does not reuse core's earlier counter mismatch as evidence at a later result", () => { + const counters = ["outsideLoaderCalls", "requestLoaderCalls", "localLoaderCalls", "coalescedLoaderCalls", "remoteLoaderCalls", "redisReads", "redisWrites"]; + expect(countingPaths([...counters, "lastResult"], [...counters, "lastResult"], counters)).toEqual(["lastResult"]); + expect(countingPaths(["redisWrites", "remoteLoaderCalls"], ["redisWrites", "remoteLoaderCalls"], ["redisWrites"])) + .toEqual(["remoteLoaderCalls"]); + }); +}); diff --git a/test/formal-effects-authority-witnesses.test.ts b/test/formal-effects-authority-witnesses.test.ts index c4dd13d8..97446469 100644 --- a/test/formal-effects-authority-witnesses.test.ts +++ b/test/formal-effects-authority-witnesses.test.ts @@ -22,6 +22,36 @@ describe("effects authority witness controls", () => { trace.steps.at(-1)!.expected.calls = [0]; expect(classify(trace).has(rule.name)).toBe(false); }); + it("requires the exact expiry input instead of crediting an adjacent clock boundary", () => { + const name = "adapter-frame-exact-expiry-refills-and-reuses", trace = traceFor(name); + trace.steps[2]!.choice = 59999; + expect(classify(trace).has(name)).toBe(false); + }); + it("requires the expired category even when fallback, refill and reuse succeed", () => { + const name = "adapter-frame-exact-expiry-refills-and-reuses", trace = traceFor(name); + trace.steps.at(-1)!.expected.events.find(event => event.event === "miss")!.detail = "unclassified"; + expect(classify(trace).has(name)).toBe(false); + }); + it("rejects a second source as proof of reuse after the expired adapter reply", () => { + const name = "adapter-frame-exact-expiry-refills-and-reuses", trace = traceFor(name); + trace.steps.at(-1)!.expected.loaders = 2; + expect(classify(trace).has(name)).toBe(false); + }); + it("rejects a future-offset event as proof of an ordinary expired frame", () => { + const name = "adapter-frame-exact-expiry-refills-and-reuses", trace = traceFor(name); + trace.steps.at(-1)!.expected.events.push({ event: "futureOffset", location: "remote", detail: "", amount: 1 }); + expect(classify(trace).has(name)).toBe(false); + }); + it("requires the last-fresh control to hit without a source or miss", () => { + const name = "adapter-frame-last-fresh-millisecond-hits", trace = traceFor(name); + trace.steps.at(-1)!.expected.loaders = 1; + expect(classify(trace).has(name)).toBe(false); + }); + it("rejects wall advances outside the portable finite input domain", () => { + const fixture = structuredClone(fixtures.find(fixture => fixture.regression === "frameReplyAtLastFreshMillisecondHitsTest")!.trace) as { states: Array<{ input: { choice: unknown } }> }; + fixture.states[2]!.input.choice = { "#bigint": "60001" }; + expect(() => parseTrace(fixture, "trace.itf.json")).toThrow(/unsupported wall advance/); + }); it("requires a cache hit after the intervening fence, even when the initial source succeeds", () => { const name = "write-stamp-after-serialization", trace = traceFor(name); trace.steps.at(-1)!.expected.loaders = 2; diff --git a/test/formal-effects.test.ts b/test/formal-effects.test.ts index 003144c6..c59e6832 100644 --- a/test/formal-effects.test.ts +++ b/test/formal-effects.test.ts @@ -79,8 +79,13 @@ async function replay(trace: Trace, harness: { settle?: boolean } = {}) { // Check C23/C25/C26 directly on observed history, independently of // expected Quint phases, timestamps, and outcome predictions. assertEffectsHistory(driver.contractHistory()); - expect(project(observed), context).toEqual(expected); - } catch (cause) { throw mismatch(cause); } + const actual = project(observed); + try { expect(actual, context).toEqual(expected); } + catch (cause) { throw new Error(`${context}\ncomparison: projected-v1\nexpected: ${JSON.stringify(expected)}\nactual: ${JSON.stringify(actual)}`, { cause }); } + } catch (cause) { + if (cause instanceof Error && cause.message.includes('\ncomparison: projected-v1\n')) throw cause; + throw mismatch(cause); + } } } finally { await driver.dispose(); } } diff --git a/test/formal-execution.test.ts b/test/formal-execution.test.ts index 88211017..58d6ae55 100644 --- a/test/formal-execution.test.ts +++ b/test/formal-execution.test.ts @@ -4,7 +4,9 @@ import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; type Reproducer = { kind: string; run: string; model?: string; failure: string; family: string; profiles: string[]; exclusions: Record; scope?: string }; -type NativeMutants = { kind: string; text: string; mutant?: string; crossContract?: string }; +type WrittenEvidence = { history: string; step: number; fields: string[] }; +type BoundaryEvidence = { challenge: string; mutant: string; history?: string; step?: number; fields?: string[]; origin?: "derived" | "written" | "vector"; vector?: { samples: Record}> }; state?: "vector" | "unreproduced" }; +type NativeMutants = { kind: string; text: string; mutant?: string; crossContract?: string; evidence?: WrittenEvidence }; type Challenge = { id: string; contract: string; source: string; model: string; invariant: string; before: string; after: string; measures?: string; reproducer?: Reproducer; nativeMutants?: NativeMutants }; type Manifest = { check: { maxSamples: number; maxSteps: number; outputDirectory: string }; @@ -31,8 +33,8 @@ type MutantEntry = { id: string; case: string; description?: string; rationale?: type Catalog = { mutations: Map; caseContracts: Map }; type Summary = { [key: string]: unknown; nativeMutants: { mapped: number; unobservable: number; modelOnly: number; backlog: number }; unmappedMutants: number }; type Declarations = Map }>; -type Options = { readSource?(path: string): string; scanSource?(source: string): Declarations; grandfathered?: readonly string[]; grandfatheredNative?: readonly string[]; catalog?: Catalog; files?: string[] }; -const { root, scanDeclarations, scanDeclarationBodies, classifyRuns, reproducerCheckpoint, validateExecution, scheduleExecution, readMutantCatalog, checkMutantAnchors, mutantsForPort, challengesByMutant, nativeMutantKinds, grandfatheredReproducerBacklog, grandfatheredNativeMutantBacklog, quintSources } = await import(moduleUrl) as { +type Options = { readSource?(path: string): string; scanSource?(source: string): Declarations; catalog?: Catalog; files?: string[] }; +const { root, scanDeclarations, scanDeclarationBodies, classifyRuns, reproducerCheckpoint, checkpointStep, boundaryEvidence, evidenceOf, validateExecution, scheduleExecution, readMutantCatalog, checkMutantAnchors, mutantsForPort, challengesByMutant, nativeMutantKinds, quintSources } = await import(moduleUrl) as { root: string; quintSources(directory?: string): string[]; scheduleExecution(manifest?: Manifest, options?: Options): Scheduled; @@ -40,14 +42,15 @@ const { root, scanDeclarations, scanDeclarationBodies, classifyRuns, reproducerC scanDeclarationBodies(source: string): Declarations; classifyRuns(declarations: Map): { publicOnly: string[]; patching: string[] }; reproducerCheckpoint(source: string, run: string, failure: unknown): { before: string; through: string }; + checkpointStep(before: string, declarations: Declarations): number; + boundaryEvidence(manifest?: Manifest, options?: Options): BoundaryEvidence[]; + evidenceOf(challenge: Challenge, models: Map, publicOnly: Map, options?: Options): BoundaryEvidence | undefined; validateExecution(value: unknown, options?: Options): Summary; readMutantCatalog(readText?: (path: string) => string): Catalog; checkMutantAnchors(catalog: Catalog, readText?: (path: string) => string): Map; mutantsForPort(catalog: Catalog, port: "typescript" | "go"): Array<{ id: string; case: string; edits: unknown[]; requiredDetections: string[] }>; challengesByMutant(manifest: Manifest): Map; nativeMutantKinds: readonly string[]; - grandfatheredReproducerBacklog: readonly string[]; - grandfatheredNativeMutantBacklog: readonly string[]; }; const { checkSemanticCoverage } = await import(new URL("../formal/check-semantic-coverage.mjs", import.meta.url).href) as { checkSemanticCoverage(value: unknown): unknown; @@ -81,8 +84,8 @@ describe("formal execution schedule", () => { it("accounts for all models, selected invariants, regressions, generated traces and challenges without Quint", () => { // Every count is computed from the manifest and the Quint sources the validator reads, so a schedule change // needs no test edit. The sums are the invariants: the exported histories are the profiles' public-only - // runs, every model is challenged or waived, and every challenge carries a reproducer or sits in that - // backlog and maps to a native mutant or sits in that one. + // runs, every model is challenged or waived, and every challenge has both a + // reproducer and a native mapping or an explicit explanation of its boundary. const raw = manifest(), current = scheduled(), catalog = readMutantCatalog(); const registry = JSON.parse(readFileSync(new URL("../formal/profiles.json", import.meta.url), "utf8")) as { profiles: unknown[] }; const kinds = (kind: string) => raw.challenges.filter(challenge => challenge.nativeMutants?.kind === kind).length; @@ -96,12 +99,14 @@ describe("formal execution schedule", () => { challenges: raw.challenges.length, distinctFaults: new Set(raw.challenges.map(challenge => JSON.stringify([challenge.source, challenge.before, challenge.after]))).size, challengedModels: challenged, waivedModels: waived, reproducers: liveReproducers(), reproducerBacklog: raw.reproducerBacklog.length, nativeMutants: { mapped: kinds("mapped"), unobservable: kinds("unobservable"), modelOnly: kinds("model-only"), backlog: raw.nativeMutantBacklog.length }, + boundaryEvidence: Object.fromEntries(["derived", "written", "unreproduced", "vector"].map(kind => + [kind, boundaryEvidence(raw).filter(entry => (entry.origin ?? entry.state) === kind).length])), unmappedMutants: catalog.mutations.size - cited.size, }); expect(current.libraries.length + raw.models.length).toBe(quintSources().length); expect(challenged + waived).toBe(raw.models.length); - expect(liveReproducers() + raw.reproducerBacklog.length).toBe(raw.challenges.length); - expect(kinds("mapped") + kinds("unobservable") + kinds("model-only") + raw.nativeMutantBacklog.length).toBe(raw.challenges.length); + expect(liveReproducers()).toBe(raw.challenges.length); + expect(kinds("mapped") + kinds("unobservable") + kinds("model-only")).toBe(raw.challenges.length); }); it("reads the schedule from the Quint text and refuses a manifest that lists it again or leaves a model unscheduled", () => { @@ -302,12 +307,9 @@ describe("formal execution schedule", () => { const repeated = manifest(); const { measures: _ignored, ...first } = repeated.challenges[0]!; repeated.challenges.push({ ...first, id: "repeated-fault" }); - repeated.reproducerBacklog.push("repeated-fault"); - // A test-only grandfather list: production keeps the frozen constant. - const grandfathered = [...grandfatheredReproducerBacklog, "repeated-fault"]; - expect(() => validate(repeated, { grandfathered })).toThrow(/repeated-fault: repeats the fault of .* without a measures note/); + expect(() => validate(repeated)).toThrow(/repeated-fault: repeats the fault of .* without a measures note/); repeated.challenges.at(-1)!.measures = "Measures the same fault against a second invariant."; - expect(validate(repeated, { grandfathered }).distinctFaults).toBe(validate(manifest()).distinctFaults); + expect(validate(repeated).distinctFaults).toBe(validate(manifest()).distinctFaults); const strayNote = manifest(); strayNote.challenges.find(challenge => !challenge.measures)!.measures = "not a repeat"; expect(() => validate(strayNote)).toThrow(/a measures note is only for a repeated fault/); @@ -317,8 +319,6 @@ describe("formal execution schedule", () => { const uncovered = manifest(); const target = uncovered.models.find(model => model.path === "formal/dialcache-core.qnt")!; uncovered.challenges = uncovered.challenges.filter(challenge => challenge.model !== target.path); - uncovered.reproducerBacklog = uncovered.reproducerBacklog.filter(id => uncovered.challenges.some(challenge => challenge.id === id)); - uncovered.nativeMutantBacklog = uncovered.nativeMutantBacklog.filter(id => uncovered.challenges.some(challenge => challenge.id === id)); expect(() => validate(uncovered)).toThrow(/dialcache-core\.qnt: scheduled invariants have no model property challenge and no challengeWaiver/); target.challengeWaiver = " "; expect(() => validate(uncovered)).toThrow(/challenge waiver must explain an unchallenged model/); @@ -332,47 +332,38 @@ describe("formal execution schedule", () => { expect(() => validate(legacy)).toThrow(/property challenges live in the manifest challenges catalog/); }); - it("requires every challenge to carry a reproducer or sit in the reported backlog, and nothing else", () => { + it("requires a reproducer for every challenge and keeps the legacy backlogs empty", () => { const current = manifest(); - const withReproducer = current.challenges.filter(challenge => challenge.reproducer); - expect(withReproducer.map(challenge => challenge.id)).toEqual([ - "recovery-connection-inclusive-maximum", "policy-inclusive-local-expiry", "profile-recovery-wrong-snapshot", "recovery-read-wrong-admission-policy", "independent-wrong-admission-policy", "effects-wrong-acceptance-receipt", "recovery-strands-followers", "policy-inclusive-remote-freshness", "shadow-inclusive-c0-freshness", "shadow-fenced-fill-writes", "shadow-fill-before-source", "effects-fenced-source-publishes", "effects-late-source-accepted", "scope-nested-close-evicts-outer-memo", "admission-duplicate-key-admitted", "admission-capacity-off-by-one", "independent-deadline-settles-at-start", "recovery-read-inclusive-maximum", "recovery-read-recovery-warms-local", "recovery-read-tracked-retention-uncapped", "local-failure-write-fault-publishes", "local-failure-source-error-published", "shadow-layers-inclusive-c0-freshness", "shadow-layers-fill-uses-current-retention", "local-clock-precise-ttl", "local-clock-hit-renews-insertion", "stale-recovery-future-candidate", "envelope-strips-unknown-zero-prefix", "source-budgets-accepts-at-deadline-equality", "policy-hit-before-join", "policy-join-ignores-coalesce", "source-budgets-settlement-never-replaces-local-entry", "source-budgets-failed-settlement-clears-local-entry", - ]); - expect(withReproducer.map(challenge => challenge.reproducer!.kind)).toEqual([ - "exported-regression", "exported-regression", "exported-regression", "exported-regression", "exported-regression", "exported-regression", "exported-regression", "exported-regression", "exported-regression", "exported-regression", "exported-regression", "exported-regression", "exported-regression", "exported-regression", "exported-regression", "exported-regression", "exported-regression", "exported-regression", "exported-regression", "exported-regression", "exported-regression", "exported-regression", "exported-regression", "exported-regression", "exported-regression", "exported-regression", "exported-regression", "model-run", "exported-regression", "exported-regression", "exported-regression", "exported-regression", "exported-regression", - ]); - // The shared-rule fault of a verification model is pinned by a profile's exported regression. - expect(withReproducer.find(challenge => challenge.id === "stale-recovery-future-candidate")!.reproducer).toMatchObject({ - model: "formal/dialcache-policy-conformance.qnt", run: "wallRollbackRejectsFutureRemoteFrameTest", profiles: ["formal/dialcache-stale-recovery.qnt", "recovery", "policy", "shadow", "recovery-read", "effects"], + expect(current.reproducerBacklog).toEqual([]); + expect(current.nativeMutantBacklog).toEqual([]); + for (const challenge of current.challenges) expect(challenge.reproducer, challenge.id).toBeDefined(); + // A shared-rule fault of a verification model can be pinned by a profile's exported regression. + expect(current.challenges.find(challenge => challenge.id === "stale-recovery-future-candidate")!.reproducer).toMatchObject({ + model: "formal/dialcache-policy-conformance.qnt", run: "wallRollbackRejectsFutureRemoteFrameTest", profiles: ["formal/dialcache-stale-recovery.qnt", "recovery", "policy", "shadow", "recovery-read", "effects", "dark-layers"], }); - expect([...current.reproducerBacklog].sort()).toEqual(current.challenges.filter(challenge => !challenge.reproducer).map(challenge => challenge.id).sort()); const unlisted = manifest(); - const dropped = unlisted.reproducerBacklog.shift()!; - expect(() => validate(unlisted)).toThrow(new RegExp(`${dropped}: has no reproducer and is not listed in reproducerBacklog`)); - const both = manifest(); - both.reproducerBacklog.push("policy-inclusive-local-expiry"); - expect(() => validate(both)).toThrow(/policy-inclusive-local-expiry: has a reproducer and is listed in reproducerBacklog/); - const unknown = manifest(); - unknown.reproducerBacklog.push("invented-fault"); - expect(() => validate(unknown)).toThrow(/reproducerBacklog names an unknown challenge: invented-fault/); - // A new challenge cannot opt out by listing itself: only the frozen - // grandfather list may appear in the backlog, and it only shrinks. - const optedOut = manifest(); - const { measures: _note, reproducer: _reproducer, ...template } = optedOut.challenges.find(challenge => challenge.reproducer)!; - optedOut.challenges.push({ ...template, id: "new-fault-without-reproducer", after: template.after + " and true" }); - optedOut.reproducerBacklog.push("new-fault-without-reproducer"); - expect(() => validate(optedOut)).toThrow(/new-fault-without-reproducer: new challenges must carry a reproducer/); - expect([...grandfatheredReproducerBacklog].sort()).toEqual([...manifest().reproducerBacklog].sort()); - const duplicate = manifest(); - duplicate.reproducerBacklog.push(duplicate.reproducerBacklog[0]!); - expect(() => validate(duplicate)).toThrow(/Duplicate challenge ids in reproducerBacklog/); - const missing = manifest(); - delete (missing as Partial).reproducerBacklog; - expect(() => validate(missing)).toThrow(/reproducer backlog is missing/); + const dropped = "core-unhealthy-local-read-hits"; + delete unlisted.challenges.find(challenge => challenge.id === dropped)!.reproducer; + expect(() => validate(unlisted)).toThrow(new RegExp(`${dropped}: must carry a reproducer`)); + // Neither an existing nor a new challenge may opt out through a legacy backlog. + const newFault = manifest(); + const { measures: _note, reproducer: _reproducer, ...template } = newFault.challenges[0]!; + newFault.challenges.push({ ...template, id: "new-fault-without-reproducer", after: template.after + " and true" }); + expect(() => validate(newFault)).toThrow(/new-fault-without-reproducer: must carry a reproducer/); + for (const name of ["reproducerBacklog", "nativeMutantBacklog"] as const) { + for (const ids of [[dropped], ["invented-fault"], [dropped, dropped]]) { + const reopened = manifest(); + reopened[name] = ids; + expect(() => validate(reopened)).toThrow(new RegExp(`${name} must remain empty`)); + } + const missing = manifest(); + delete (missing as Partial)[name]; + expect(() => validate(missing)).toThrow(new RegExp(`${name} must remain empty`)); + } }); // Explicit budget: the hosted runner is about six times slower under coverage than a local run. - it("maps every challenge to a native mutant in the catalog or an enumerated explanation, and freezes the backlog", () => { + it("maps every challenge to a native mutant in the catalog or an enumerated explanation", () => { // A small catalog fixture: M01 and M11 require generated detection in both // ports, M40 sits on a C40 case and M20 requires generated detection only in // TypeScript. @@ -387,14 +378,11 @@ describe("formal execution schedule", () => { caseContracts: new Map([["C45.maximum-age-exclusive", ["C45"]], ["C09.fixed-local-ttl", ["C09"]], ["C11.distinct-keys", ["C11"]], ["C40.future-rejected", ["C40"]]]), }); // The rules are exercised against the fixture catalog, not the live mapping: - // start from the live manifest with every challenge explained and the frozen - // backlog, then vary one challenge at a time. + // start with every challenge explained, then vary one challenge at a time. const explained = () => { const m = manifest(); - m.nativeMutantBacklog = [...grandfatheredNativeMutantBacklog]; for (const challenge of m.challenges) { - if (m.nativeMutantBacklog.includes(challenge.id)) delete challenge.nativeMutants; - else challenge.nativeMutants = { kind: "model-only", text: "Fixture: no line in src/dialcache.ts carries this bookkeeping." }; + challenge.nativeMutants = { kind: "model-only", text: "Fixture: no line in src/dialcache.ts carries this bookkeeping." }; } return m; }; @@ -407,7 +395,6 @@ describe("formal execution schedule", () => { }; const mapped = (mutant: string, crossContract?: string): NativeMutants => ({ kind: "mapped", text: `Fixture mapping of ${mutant}.`, mutant, ...(crossContract === undefined ? {} : { crossContract }) }); expect(nativeMutantKinds).toEqual(["mapped", "unobservable", "model-only"]); - expect(grandfatheredNativeMutantBacklog).toEqual(["invalidation-transition-cutoff-moves-backwards", "invalidation-transition-inclusive-buffer-limit"]); // The mutant sits in the catalog. expect(withNative("policy-inclusive-local-expiry", mapped("M99"))).toThrow(/policy-inclusive-local-expiry: M99 is not in the mutant catalog/); expect(withNative("policy-inclusive-local-expiry", { kind: "mapped", text: "x" })).toThrow(/policy-inclusive-local-expiry: mapped nativeMutants need a mutant id/); @@ -441,36 +428,24 @@ describe("formal execution schedule", () => { m.challenges.find(challenge => challenge.id === "recovery-connection-inclusive-maximum")!.nativeMutants = mapped("M40", "The recovery monitor reaches it too."); }); expect(crossOnly().nativeMutants).toMatchObject({ mapped: 2 }); - // The backlog is the exact set of challenges without an entry, and only the grandfathered ones may sit in it. - expect(withNative("policy-inclusive-local-expiry", undefined)).toThrow(/policy-inclusive-local-expiry: has no nativeMutants and is not listed in nativeMutantBacklog/); - expect(withNative("policy-inclusive-local-expiry", undefined, m => { m.nativeMutantBacklog.push("policy-inclusive-local-expiry"); })) - .toThrow(/policy-inclusive-local-expiry: new challenges must map to a native mutant in both ports or explain why none exists; nativeMutantBacklog only grandfathers the challenges that predate the requirement/); - // A test-only grandfather list admits it: production keeps the frozen constant. - const optedIn = explained(); - delete optedIn.challenges.find(challenge => challenge.id === "policy-inclusive-local-expiry")!.nativeMutants; - optedIn.nativeMutantBacklog.push("policy-inclusive-local-expiry"); - expect(validate(optedIn, { catalog: catalog(), grandfatheredNative: [...grandfatheredNativeMutantBacklog, "policy-inclusive-local-expiry"] }).nativeMutants).toMatchObject({ backlog: grandfatheredNativeMutantBacklog.length + 1 }); - expect(withNative("policy-inclusive-local-expiry", mapped("M11"), m => { m.nativeMutantBacklog.push("policy-inclusive-local-expiry"); })).toThrow(/policy-inclusive-local-expiry: has nativeMutants and is listed in nativeMutantBacklog/); - expect(withNative("policy-inclusive-local-expiry", mapped("M11"), m => { m.nativeMutantBacklog.push("invented-fault"); })).toThrow(/nativeMutantBacklog names an unknown challenge: invented-fault/); - expect(withNative("policy-inclusive-local-expiry", mapped("M11"), m => { m.nativeMutantBacklog.push(m.nativeMutantBacklog[0]!); })).toThrow(/Duplicate challenge ids in nativeMutantBacklog/); - expect(withNative("policy-inclusive-local-expiry", mapped("M11"), m => { delete (m as Partial).nativeMutantBacklog; })).toThrow(/Challenge native-mutant backlog is missing/); - expect(withNative("invalidation-transition-cutoff-moves-backwards", { kind: "model-only", text: "Only src/internal/redis-scripts.ts carries it." })).toThrow(/invalidation-transition-cutoff-moves-backwards: has nativeMutants and is listed in nativeMutantBacklog/); + expect(withNative("policy-inclusive-local-expiry", undefined)) + .toThrow(/policy-inclusive-local-expiry: must map to a native mutant in both ports or explain why none exists/); // The summary counts every kind, the backlog and the catalog mutants no challenge cites. const summary = withNative("policy-inclusive-local-expiry", mapped("M11"), m => { m.challenges.find(challenge => challenge.id === twins[0])!.nativeMutants = unobservable("Scope."); m.challenges.find(challenge => challenge.id === twins[1])!.nativeMutants = unobservable("Layers."); })(); - expect(summary.nativeMutants).toEqual({ mapped: 1, unobservable: 2, modelOnly: manifest().challenges.length - grandfatheredNativeMutantBacklog.length - 3, backlog: grandfatheredNativeMutantBacklog.length }); + expect(summary.nativeMutants).toEqual({ mapped: 1, unobservable: 2, modelOnly: manifest().challenges.length - 3, backlog: 0 }); expect(summary.unmappedMutants).toBe(3); const fixture = explained(); expect(challengesByMutant({ ...fixture, challenges: [{ ...fixture.challenges[0]!, nativeMutants: mapped("M11") }, { ...fixture.challenges[1]!, nativeMutants: mapped("M11") }, { ...fixture.challenges[2]!, nativeMutants: mapped("M01") }, fixture.challenges[3]!] })) .toEqual(new Map([["M11", [fixture.challenges[0]!.id, fixture.challenges[1]!.id]], ["M01", [fixture.challenges[2]!.id]]])); }, 60_000); - it("maps every live challenge outside the frozen native-mutant backlog, and cites only catalog mutants", () => { + it("classifies every live challenge and cites only catalog mutants", () => { const live = manifest(); - expect([...live.nativeMutantBacklog].sort()).toEqual([...grandfatheredNativeMutantBacklog].sort()); - expect(live.challenges.filter(challenge => challenge.nativeMutants === undefined).map(challenge => challenge.id).sort()).toEqual([...grandfatheredNativeMutantBacklog].sort()); + expect(live.nativeMutantBacklog).toEqual([]); + for (const challenge of live.challenges) expect(challenge.nativeMutants, challenge.id).toBeDefined(); const catalog = readMutantCatalog(); const cited = challengesByMutant(live); for (const [mutant, ids] of cited) { @@ -541,7 +516,13 @@ describe("formal execution schedule", () => { it("validates reproducer kinds, cited models, declared checkpoints, profile partitions and model-only scope", () => { const exported = (edit: (reproducer: Reproducer) => void) => { const edited = manifest(); - edit(edited.challenges.find(challenge => challenge.id === "source-budgets-accepts-at-deadline-equality")!.reproducer!); + const reproducer = edited.challenges.find(challenge => challenge.id === "source-budgets-accepts-at-deadline-equality")!.reproducer!; + // This validation fixture deliberately uses the two-checkpoint timer + // history. The live native mapping uses effects' settlement-time history. + delete reproducer.model; + reproducer.run = "defaultSourceBudgetExpiresAtSixtySecondsTest"; + reproducer.failure = "s.o.calls == List(DEADLINE_ERROR, CALL_PENDING) and s.o.loaders == 2"; + edit(reproducer); return edited; }; const modelRun = (edit: (reproducer: Reproducer) => void) => { @@ -571,16 +552,16 @@ describe("formal execution schedule", () => { expect(() => validate(exported(r => { r.exclusions = { invented: "no such profile" }; }))).toThrow(/reproducer exclusion must name an unlisted known profile with a reason: invented/); expect(() => validate(exported(r => { r.exclusions = { "source-budgets": "listed and excluded" }; }))).toThrow(/reproducer exclusion must name an unlisted known profile/); expect(() => validate(exported(r => { r.exclusions = { effects: " " }; }))).toThrow(/reproducer exclusion must name an unlisted known profile with a reason: effects/); - // A shared-library fault partitions every profile between the listed and the excluded. + // Only profiles that import a shared rule need reviewed detection or exclusion claims. expect(() => validate(exported(r => { r.profiles = ["source-budgets", "effects"]; r.exclusions = { independent: "Its sources settle only through explicit deadlines." }; }))) - .toThrow(/a shared-library fault must list or exclude every profile; missing core/); - expect(validate(exported(r => { r.profiles = ["source-budgets", "effects", "shadow-layers", "recovery", "admission", "shadow"]; delete r.exclusions.effects; r.exclusions.independent = "Its sources settle only through explicit deadlines."; })).reproducers).toBe(liveReproducers()); + .toThrow(/a shared-library fault must list or exclude every importing profile; missing/); + expect(validate(exported(r => { r.profiles = ["source-budgets", "effects", "shadow-layers", "recovery", "admission", "shadow", "dark-layers", "shadow-read-deadlines"]; delete r.exclusions.effects; r.exclusions.independent = "Its sources settle only through explicit deadlines."; })).reproducers).toBe(liveReproducers()); expect(() => validate(exported(r => { r.scope = "not model-only"; }))).toThrow(/scope belongs only to a model-run reproducer/); // Another profile model's exported run may be cited only for a fault in a shared library: a fault in a // model's own file may not name one, whatever its reproducer kind. const budgetsRun = { model: "formal/dialcache-source-budgets-conformance.qnt", run: "defaultSourceBudgetExpiresAtSixtySecondsTest", failure: "s.o.calls == List(DEADLINE_ERROR, CALL_PENDING) and s.o.loaders == 2" }; expect(() => validate(modelRun(r => { Object.assign(r, budgetsRun); }))) - .toThrow(/reproducer model must name another profile model and is allowed only for an exported-regression of a shared-library fault: formal\/dialcache-source-budgets-conformance\.qnt/); + .toThrow(/reproducer model must name another profile model or vector model reached by a shared-library fault.*: formal\/dialcache-source-budgets-conformance\.qnt/); expect(() => validate(shared(r => { r.model = "formal/dialcache-stale-recovery.qnt"; }))).toThrow(/reproducer model must name another profile model/); expect(() => validate(shared(r => { r.model = "formal/dialcache-core.qnt"; }))).toThrow(/reproducer model must name another profile model .*: formal\/dialcache-core\.qnt/); expect(() => validate(shared(r => { r.model = "formal/invented.qnt"; }))).toThrow(/reproducer model must name another profile model .*: formal\/invented\.qnt/); @@ -588,8 +569,10 @@ describe("formal execution schedule", () => { expect(() => validate(shared(r => { r.run = "localHitDoesNotRenewInsertionTtlTest"; }))).toThrow(/has no top-level expect whose condition is the declared failure/); expect(() => validate(shared(r => { r.profiles = ["formal/dialcache-stale-recovery.qnt", "recovery", "shadow"]; r.exclusions.policy = "excluded anyway"; }))) .toThrow(/must name known profiles and include formal\/dialcache-stale-recovery\.qnt and policy/); - // A shared-library fault lists or excludes every known profile. - expect(() => validate(shared(r => { delete r.exclusions.core; delete r.exclusions.layers; }))).toThrow(/a shared-library fault must list or exclude every profile; missing core, layers/); + // Non-importers need no catalog entry; importing exclusions cannot disappear. + expect(validate(shared(r => { delete r.exclusions.core; })).reproducers).toBe(liveReproducers()); + expect(() => validate(shared(r => { delete r.exclusions.core; r.profiles.push("core"); }))).toThrow(/listed profile does not import/); + expect(() => validate(shared(r => { delete r.exclusions.layers; }))).toThrow(/a shared-library fault must list or exclude every importing profile; missing layers/); expect(validate(shared(r => { delete r.exclusions.layers; r.profiles.push("layers"); })).reproducers).toBe(liveReproducers()); // A state-patching run is a scheduled regression but never exported (the run is added to the effects text // through the source reader: no profile declares one), and an exported run may not be cited as a model-run. @@ -599,23 +582,39 @@ describe("formal execution schedule", () => { effects.reproducer = { ...effects.reproducer!, kind: "exported-regression", run: "patchedBudgetTest", failure: budget }; expect(() => validate(patching, { readSource: withPatchingRun })).toThrow(/exported-regression reproducer must cite an exported public-only run of formal\/dialcache-effects-conformance\.qnt: patchedBudgetTest/); effects.reproducer = { ...effects.reproducer!, kind: "model-run", run: "patchedBudgetTest", failure: budget, scope: "Patches the read budget directly." }; - expect(validate(patching, { readSource: withPatchingRun })).toMatchObject({ reproducers: liveReproducers(), reproducerBacklog: manifest().reproducerBacklog.length }); + // Supplying a deterministic model run must not downgrade a mapped native + // fault to an unreproduced boundary while leaving both backlogs unchanged. + expect(() => validate(patching, { readSource: withPatchingRun })) + .toThrow(/mapped native mutant requires an exported-regression or an exported-vector model-run reproducer/); effects.reproducer = { ...effects.reproducer!, kind: "model-run", run: "lateSourceResultIsADeadlineErrorTest", failure: manifest().challenges.find(challenge => challenge.id === "effects-late-source-accepted")!.reproducer!.failure, scope: "Not a model-only run." }; expect(() => validate(patching)).toThrow(/lateSourceResultIsADeadlineErrorTest is exported; cite it as an exported-regression reproducer/); expect(() => validate(modelRun(r => { delete r.scope; }))).toThrow(/model-run reproducer needs a scope/); expect(() => validate(modelRun(r => { r.profiles = ["recovery"]; }))).toThrow(/must name known profiles and include formal\/dialcache-envelope-vectors\.qnt/); - const exportedAsModelRun = manifest(); + const exportedAsModelRun = exported(() => {}); const budgets = exportedAsModelRun.challenges.find(challenge => challenge.id === "source-budgets-accepts-at-deadline-equality")!; budgets.reproducer = { ...budgets.reproducer!, kind: "model-run", scope: "Pretend it is model-only." }; expect(() => validate(exportedAsModelRun)).toThrow(/defaultSourceBudgetExpiresAtSixtySecondsTest is exported; cite it as an exported-regression reproducer/); const verification = manifest(); const core = verification.challenges.find(challenge => challenge.id === "core-unhealthy-local-read-hits")!; - verification.reproducerBacklog = verification.reproducerBacklog.filter(id => id !== core.id); core.reproducer = { kind: "exported-regression", run: "localReadFailureContinuesToRemoteTest", family: "unhealthy-read-served", profiles: ["formal/dialcache-core.qnt"], exclusions: {}, failure: "s.origin == RemoteValue and s.localReads == 1 and s.remoteReads == 1 and s.sourceCalls == 0 and s.localWrites == 0" }; expect(() => validate(verification)).toThrow(/exported-regression reproducer must cite an exported public-only run of formal\/dialcache-core\.qnt/); }, 60_000); + it("retains vector boundaries and native-free model reproducers", () => { + const current = manifest(); + const vector = current.challenges.find(challenge => challenge.id === "envelope-strips-unknown-zero-prefix")!; + expect(vector).toMatchObject({ reproducer: { kind: "model-run" }, nativeMutants: { kind: "mapped" } }); + expect(boundaryEvidence(current).find(entry => entry.challenge === vector.id)) + .toMatchObject({ origin: "vector", step: 0 }); + for (const kind of ["model-only", "unobservable"]) { + const challenge = current.challenges.find(item => item.nativeMutants?.kind === kind && item.reproducer)!; + expect(challenge, kind).toBeDefined(); + expect(boundaryEvidence(current).some(entry => entry.challenge === challenge.id), kind).toBe(false); + } + expect(validate(current)).toMatchObject({ reproducers: liveReproducers(), reproducerBacklog: current.reproducerBacklog.length }); + }); + it("keeps vector artifacts separate from profile histories and validates their provenance boundary", () => { for (const changed of [ (v: NonNullable) => { v.generator = "formal/../outside.mjs"; }, @@ -703,19 +702,19 @@ describe("formal execution schedule", () => { expect(() => checkSemanticCoverage(catalog)).toThrow(/not scheduled for execution/); }); - it("preserves ordered execution, per-profile budgets, seed override, and one closing challenge run", () => { + it("preserves ordered model execution, per-profile budgets and seed override", () => { const dryRun = (mode: string) => JSON.parse(execFileSync(process.execPath, [runner, mode, "--dry-run"], { env: { ...process.env, QUINT_SEED: "0x1234" }, stdio: ["pipe", "pipe", "pipe"], }).toString()) as Command[]; const check = dryRun("check"); expect(check.filter(job => job.args[0] === "typecheck").map(job => job.args[1])).toEqual(manifest().models.map(model => model.path)); + expect(check.filter(job => job.args[0] === "run").map(job => job.args[1])).toEqual(manifest().models.map(model => model.path)); expect(check.filter(job => job.args[0] === "test").map(job => job.args[1])).toEqual( scheduled().models.filter(model => model.regressions.length).map(model => model.path), ); - // The catalog mutates several models; it runs once after every model has - // been checked unmodified, never interleaved with a model's own schedule. - expect(check.filter(job => job.command === "node")).toEqual([{ command: "node", args: ["formal/check-model-properties.mjs"] }]); - expect(check.at(-1)!.args).toEqual(["formal/check-model-properties.mjs"]); + // The complete pinned fault campaign is a separate validation-plan step; + // this command retains every unmodified model check and regression. + expect(check.every(job => job.command === "quint")).toBe(true); for (const job of check.filter(job => job.args[0] === "run")) { expect(job.args).toEqual(expect.arrayContaining(["--backend=rust", "--n-threads=1", "--seed=0x1234", "--max-samples=2000", "--max-steps=40"])); } @@ -759,3 +758,191 @@ describe("formal execution schedule", () => { expect(sampled.find(job => job.outputDirectory === ".formal-traces/features/layers")!.args).toEqual(expect.arrayContaining(["--max-samples=2048", "--max-steps=80"])); }); }); + +describe("native boundary evidence", () => { + it("counts initializer aliases, nested chains and every repeated public transition", () => { + const declarations = scanDeclarationBodies(`module checkpoints { + var s: int + action initialized(mode: int): bool = all { s' = mode } + action init = initialized(0) + action advance(n: int): bool = all { s' = s + n } + action fixture = init.then(advance(1)) + action alias = fixture + action loop = 3.reps(_ => advance(1)) + action wrapped = all { alias } + action circular = circular + }`); + expect(checkpointStep("initialized(1).then(advance(2)).expect(s > 0)", declarations)).toBe(1); + expect(checkpointStep("alias.then(2.reps(_ => advance(1).then(advance(2))))", declarations)).toBe(5); + expect(checkpointStep("init.then(loop)", declarations)).toBe(3); + expect(() => checkpointStep("missing.then(advance(1))", declarations)).toThrow(/unknown action missing/); + expect(() => checkpointStep("circular", declarations)).toThrow(/cyclic action alias/); + expect(() => checkpointStep("init.then(wrapped)", declarations)).toThrow(/nested action sequence in wrapped/); + expect(() => checkpointStep("init.then(n.reps(_ => advance(1)))", declarations)).toThrow(/repetition count must be a safe integer literal/); + expect(() => checkpointStep("init.then(if (true) advance(1) else advance(2))", declarations)).toThrow(/unknown action if/); + expect(() => checkpointStep("init.then(advance(1)).unknown()", declarations)).toThrow(/unsupported action-chain suffix/); + }); + + it("derives reviewed checkpoints in init, parameterized initializer and repetition histories", () => { + const entries = new Map(boundaryEvidence().map(entry => [entry.challenge, entry])); + expect(entries.get("shadow-layers-fill-uses-current-retention")).toMatchObject({ + history: "shadow-layers/darkFillRetainsPolicyThroughSerializationTest", step: 7, fields: ["o.writeTtls", "o.shadow"], origin: "derived", + }); + expect(entries.get("recovery-connection-inclusive-maximum")).toMatchObject({ + history: "recovery/maximumAgeAfterDecodeFromStrictlyStaleFrameTest", step: 7, + }); + expect(entries.get("source-budgets-settlement-never-replaces-local-entry")).toMatchObject({ + history: "policy/independentLocalPublicationUsesLastCompletionTest", step: 9, fields: ["o.calls", "o.loaders"], + }); + expect(entries.get("source-budgets-failed-settlement-clears-local-entry")).toMatchObject({ + history: "policy/independentFailureKeepsLocalValueWhenRemoteFailsTest", step: 10, fields: ["o.calls", "o.loaders", "o.reads"], + }); + expect(entries.get("shadow-read-deadline-ignores-bounded-result")).toMatchObject({ + history: "shadow-read-deadlines/c0ReadDeadlineKeepsRawCapacityTest", step: 3, fields: ["o.calls", "o.shadow", "io.aborted"], origin: "derived", + }); + expect(entries.get("shadow-read-deadline-releases-raw-capacity")).toMatchObject({ + history: "shadow-read-deadlines/c0ReadDeadlineKeepsRawCapacityTest", step: 4, fields: ["o.reads", "o.shadow"], origin: "derived", + }); + expect(entries.get("effects-fenced-source-publishes")).toMatchObject({ fields: ["calls", "dumps", "writes"] }); + expect(entries.get("local-clock-precise-ttl")).toMatchObject({ fields: ["calls", "loaders"] }); + expect(entries.get("effects-wrong-acceptance-receipt")).toMatchObject({ step: 7, fields: ["events"], origin: "written" }); + const effectsSource = readFileSync(root + "formal/dialcache-effects-conformance.qnt", "utf8"); + expect(checkpointStep("tenMillisecondFixture", scanDeclarationBodies(effectsSource))).toBe(0); + const budgetsSource = readFileSync(root + "formal/dialcache-source-budgets-conformance.qnt", "utf8"); + const repeated = reproducerCheckpoint(budgetsSource, "localEntryExpiresAfterItsTtlTest", + "s.o.calls == List(VALUE_ONE, VALUE_ONE, VALUE_TWO, VALUE_TWO) and s.o.loaders == 2"); + expect(checkpointStep(repeated.before, scanDeclarationBodies(budgetsSource))).toBe(30); + }); + + it("reports every mapped challenge once and computes the inventory from its evidence", () => { + const current = manifest(), entries = boundaryEvidence(current); + expect(entries.map(entry => entry.challenge)).toEqual(current.challenges.filter(challenge => challenge.nativeMutants?.kind === "mapped").map(challenge => challenge.id)); + expect(validate(current).boundaryEvidence).toEqual(Object.fromEntries(["derived", "written", "unreproduced", "vector"].map(kind => + [kind, entries.filter(entry => (entry.origin ?? entry.state) === kind).length]))); + expect(entries.find(entry => entry.challenge === "envelope-strips-unknown-zero-prefix")).toMatchObject({ origin: "vector", step: 0 }); + expect(entries.find(entry => entry.challenge === "frame-vectors-inclusive-fence")).toMatchObject({ origin: "vector", step: 0 }); + expect(entries.find(entry => entry.challenge === "scope-source-error-memoized")).toMatchObject({ + history: "dark-layers/rejectedDarkSourceSeedsNoLayerTest", step: 5, origin: "derived", + }); + }); + + it("keeps private state and string literals out of derived public paths", () => { + const current = manifest(); + const entry = boundaryEvidence(current).find(item => item.challenge === "recovery-strands-followers")!; + expect(entry.fields).toEqual(["o.calls", "d.coalesced", "o.loads"]); + const challenge = structuredClone(current.challenges.find(item => item.id === "recovery-strands-followers")!); + const cited = current.models.find(item => item.path === (challenge.reproducer!.model ?? challenge.model))!; + const failure = 's.memo == 0 and s.o.calls == List("s.o.loaders")'; + challenge.reproducer = { ...challenge.reproducer!, run: "quotedTest", failure }; + expect(evidenceOf(challenge, new Map([[cited.path, cited]]), new Map([[cited.path, ["quotedTest"]]]), { + readSource: () => `module quotes { var s: int action init = all { s' = 0 } + action next = all { s' = 1 } run quotedTest = init.then(next).expect(${failure}) }`, + })).toMatchObject({ fields: ["o.calls"] }); + const effectsChallenge = structuredClone(current.challenges.find(item => item.id === "effects-fenced-source-publishes")!); + const effectsModel = current.models.find(item => item.path === effectsChallenge.model)!; + const privateFailure = 's.reads == List() and s.o.calls == List()'; + effectsChallenge.reproducer = { ...effectsChallenge.reproducer!, run: "privateTest", failure: privateFailure }; + expect(evidenceOf(effectsChallenge, new Map([[effectsModel.path, effectsModel]]), new Map([[effectsModel.path, ["privateTest"]]]), { + readSource: () => `module private_fields { var s: int action init = all { s' = 0 } + action next = all { s' = 1 } run privateTest = init.then(next).expect(${privateFailure}) }`, + })).toMatchObject({ fields: ["calls"] }); + const altered = structuredClone(current); + delete altered.challenges.find(challenge => challenge.id === "effects-wrong-acceptance-receipt")!.nativeMutants!.evidence; + expect(() => validate(altered)).toThrow(/checkpoint has no derived public fields; write nativeMutants.evidence/); + }); + + it("accepts only core's compared fields, never its stored-value predictions", () => { + const current = manifest(); + const challenge = structuredClone(current.challenges.find(item => item.id === "conformance-local-hit-returns-source")!); + const model = current.models.find(item => item.path === challenge.model)!; + delete challenge.nativeMutants!.evidence; + const failure = 's.localValue == 1 and s.localCached and s.lastResult == 1 and s.localLoaderCalls == 1'; + challenge.reproducer = { ...challenge.reproducer!, run: "coreBoundaryTest", failure }; + expect(evidenceOf(challenge, new Map([[model.path, model]]), new Map([[model.path, ["coreBoundaryTest"]]]), { + readSource: () => `module core_boundary { var s: int action init = all { s' = 0 } + action call = all { s' = 1 } run coreBoundaryTest = init.then(call).expect(${failure}) }`, + })).toMatchObject({ fields: ["lastResult", "localLoaderCalls"], step: 1 }); + + for (const field of ["localValue", "remoteValue", "localCached", "remoteReadable", "o.calls", "calls", "lastResultExtra"]) { + const altered = structuredClone(current), target = altered.challenges.find(item => item.id === challenge.id)!; + target.nativeMutants!.evidence = { history: "core/localValueSurvivesSourceChangeTest", step: 3, fields: [field] }; + expect(() => validate(altered), field).toThrow(/evidence fields/); + } + }); + + it.each([ + { history: "shadow-layers/anotherTest", step: 7, fields: ["o.shadow"] }, + { history: "shadow-layers/darkFillRetainsPolicyThroughSerializationTest", step: 0, fields: ["o.shadow"] }, + { history: "shadow-layers/darkFillRetainsPolicyThroughSerializationTest", step: 1.5, fields: ["o.shadow"] }, + { history: "shadow-layers/darkFillRetainsPolicyThroughSerializationTest", step: 7, fields: [] }, + { history: "shadow-layers/darkFillRetainsPolicyThroughSerializationTest", step: 7, fields: ["s.memo"] }, + { history: "shadow-layers/darkFillRetainsPolicyThroughSerializationTest", step: 7, fields: ["calls"] }, + { history: "shadow-layers/darkFillRetainsPolicyThroughSerializationTest", step: 7, fields: ["o.calls.0"] }, + { history: "shadow-layers/darkFillRetainsPolicyThroughSerializationTest", step: 7, fields: ["o.shadow", "o.shadow"] }, + ])("rejects malformed or unrelated written evidence: %j", evidence => { + const current = manifest(); + current.challenges.find(challenge => challenge.id === "shadow-layers-fill-uses-current-retention")!.nativeMutants!.evidence = evidence; + expect(() => validate(current)).toThrow(/evidence (history|step|fields)/); + }); + + it("allows written evidence to override a derivation but never to choose another history", () => { + const current = manifest(), challenge = current.challenges.find(item => item.id === "shadow-layers-fill-uses-current-retention")!; + challenge.nativeMutants!.evidence = { history: "shadow-layers/darkFillRetainsPolicyThroughSerializationTest", step: 6, fields: ["o.writeTtls"] }; + expect(boundaryEvidence(current).find(entry => entry.challenge === challenge.id)).toMatchObject({ step: 6, fields: ["o.writeTtls"], origin: "written" }); + expect(validate(current)).toHaveProperty("boundaryEvidence"); + const model = current.models.find(item => item.path === challenge.model)!; + expect(() => evidenceOf(challenge, new Map([[model.path, model]]), new Map([[model.path, []]]))).toThrow(/exported public-only history/); + // Written evidence cannot substitute for the required reproducer. + delete challenge.reproducer; + expect(() => validate(current)).toThrow(/must carry a reproducer/); + }); + + it("requires actual flat assertion fields for effects and local-clock", () => { + for (const [id, fields] of [["effects-wrong-acceptance-receipt", ["o.calls"]], ["local-clock-precise-ttl", ["events"]]] as const) { + const current = manifest(), challenge = current.challenges.find(item => item.id === id)!; + const original = boundaryEvidence(current).find(item => item.challenge === id)!; + challenge.nativeMutants!.evidence = { history: original.history!, step: original.step!, fields: [...fields] }; + expect(() => validate(current)).toThrow(/evidence fields/); + } + }); + + it("cross-checks every evidence field against the profile's actual comparison record", async () => { + const features = await import(new URL("../formal/replay/features.mjs", import.meta.url).href) as { + profiles: Record; + parseTrace(raw: unknown, path: string, profile: unknown): { steps: unknown[] }; + expectedObservation(step: unknown): Record; + }; + const effects = await import(new URL("../formal/replay/effects.mjs", import.meta.url).href) as { parseTrace(raw: unknown, path: string): { steps: Array<{ expected: Record }> } }; + const clock = await import(new URL("../formal/replay/local-clock.mjs", import.meta.url).href) as { parseLocalClockTrace(raw: unknown, path: string): { steps: Array<{ expected: Record }> } }; + const core = await import(new URL("../formal/replay/core.mjs", import.meta.url).href) as { + parseItfTrace(raw: unknown, path: string): { states: Array<{ state: unknown }> }; + expectedCoreObservation(state: unknown): Record; + }; + const registry = JSON.parse(readFileSync(root + "formal/profiles.json", "utf8")) as { profiles: Array<{ id: string; smoke: string }> }; + for (const entry of boundaryEvidence().filter(item => item.history !== undefined)) { + if (entry.vector) { + for (const sample of Object.values(entry.vector.samples)) for (const field of entry.fields!) { + expect(Object.hasOwn(sample.expected, field), `${entry.challenge}: ${field}`).toBe(true); + } + continue; + } + const profile = entry.history!.split("/")[0]!, path = registry.profiles.find(item => item.id === profile)!.smoke; + const raw = JSON.parse(readFileSync(root + path, "utf8")) as unknown; + const descriptor = features.profiles[profile]; + const expected = profile === "effects" ? effects.parseTrace(raw, path).steps[0]!.expected + : profile === "local-clock" ? clock.parseLocalClockTrace(raw, path).steps[0]!.expected + : profile === "core" ? core.expectedCoreObservation(core.parseItfTrace(raw, path).states[0]!.state) + : features.expectedObservation(features.parseTrace(raw, path, descriptor).steps[0]); + for (const field of entry.fields!) { + const flag = field.startsWith("d.") ? "diagnosticAge" : field.startsWith("io.") ? "readIO" + : field === "markers" ? "markerIO" : field === "compression" ? "compressionIO" : field === "policyErrors" ? "policyErrorIO" : undefined; + if (flag) expect(descriptor?.[flag], `${entry.challenge}: ${field}`).toBeTruthy(); + let observed: unknown = expected; + for (const part of field.split(".")) { + expect(observed !== null && typeof observed === "object" && Object.hasOwn(observed, part), `${entry.challenge}: ${field}`).toBe(true); + observed = (observed as Record)[part]; + } + } + } + }); +}); diff --git a/test/formal-exploration.test.ts b/test/formal-exploration.test.ts index dc6c861d..f1ba6ce5 100644 --- a/test/formal-exploration.test.ts +++ b/test/formal-exploration.test.ts @@ -130,6 +130,17 @@ describe("isolated exploratory validation", () => { ["formal/run-models.mjs", "check"], ["formal/run-models.mjs", "generate"], ["formal/witnesses.mjs", "evaluate"], ]); for (const step of seeded) expect(step.env?.QUINT_SEED).toBe("0x2a"); + // Only the identical pinned fault campaign is omitted. Clean model + // checks/regressions, lint, fixtures, generation and witnesses remain. + expect(plan.filter(step => step.args?.[0]?.startsWith("formal/")).map(step => step.args)).toEqual([ + ["formal/run-models.mjs", "check"], + ["formal/lint-profiles.mjs", "baseline", "--check"], + ["formal/check-kernel-fixtures.mjs"], + ["formal/run-models.mjs", "generate"], + ["formal/generated-fixtures.mjs", "--check"], + ["formal/witnesses.mjs", "evaluate", "--profile", "all"], + ["formal/check-go-parity.mjs"], + ]); const replays = plan.filter(step => step.nativeReport); expect(replays.map(step => step.nativeReport)).toEqual(["typescript", "go"]); for (const step of replays) expect(step.env?.DIALCACHE_FEATURE_TRACE_DIR).toBe(`${directory}/.formal-traces/features`); diff --git a/test/formal-features.test.ts b/test/formal-features.test.ts index ca79c38e..58f9f6a8 100644 --- a/test/formal-features.test.ts +++ b/test/formal-features.test.ts @@ -2,6 +2,7 @@ import { profiles, parseTrace, featureInput, assertFeatureObservation, type Trac import { SettlementLedger } from "../formal/replay/settlement.mjs"; import { checkCorpus, loadCorpus } from "../formal/replay/witnesses/index.mjs"; import { readFileSync, readdirSync } from "node:fs"; +import { isObservationComparison } from "../formal/replay/divergence.mjs"; import { resolve } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; @@ -40,7 +41,12 @@ async function replay(profile: Profile, trace: Trace, harness: { settle?: boolea // carry the expected/actual markers the mutation lanes credit. try { ledger.assert(driver.receipt(), observed, { wallMs: Date.now() }); } catch (cause) { throw new Error([`${context}: ${(cause as Error).message}`, driver.settlementDiagnostic()].filter(Boolean).join("\n"), { cause }); } - try { assertFeatureObservation(profile, step, observed); } catch (cause) { throw mismatch(cause); } + try { assertFeatureObservation(profile, step, observed); } catch (cause) { + // Mutation diagnostics must compare the same projection as the assertion, + // rather than the model's o/d/io records against the raw driver snapshot. + if (isObservationComparison(cause)) throw new Error(`${context}\ncomparison: projected-v1\nexpected: ${JSON.stringify(cause.expected)}\nactual: ${JSON.stringify(cause.actual)}`, { cause }); + throw mismatch(cause); + } } } finally { await driver.dispose(); } } diff --git a/test/formal-go-semantic-runner.test.ts b/test/formal-go-semantic-runner.test.ts index e88b725b..f8ceff58 100644 --- a/test/formal-go-semantic-runner.test.ts +++ b/test/formal-go-semantic-runner.test.ts @@ -25,6 +25,15 @@ describe("Go local-clock mutation assertion attribution", () => { "TestLocalClockConformance/trace.itf.json": "observation-mismatch", } }); }); + it("retains the raw failing assertion for diagnosis", () => { + const message = "/tmp/corpus/regressions/shadow-layers/capturedRetentionTest.itf.json step 4 action resolveLoader: Observation mismatch\n" + + "expected: {\"o\":{\"policyCalls\":1,\"writeTtls\":[]}}\nactual: {\"o\":{\"policyCalls\":2,\"writeTtls\":[]}}"; + const result = evaluateGoTestEvents(replayFailure("TestFeatureConformance", "feature_replay_test.go", + message), 1); + expect(result).toMatchObject({ assertionEvidence: { + "TestFeatureConformance/trace.itf.json": ` feature_replay_test.go:84: ${message}\n`, + } }); + }); it("credits the core replay's coalesced-pair assertion, which carries no expected/actual pair", () => { const pair = replayFailure("TestCoreConformance", "core_replay_test.go", "pair returned different values"); expect(evaluateGoTestEvents(pair, 1)).toMatchObject({ state: "detected", assertionKinds: { "TestCoreConformance/trace.itf.json": "pair-value-mismatch" } }); @@ -52,3 +61,11 @@ describe("Go local-clock mutation assertion attribution", () => { }, ); }); + + +it("does not count Redis transport or driver errors as native assertions", () => { + expect(() => evaluateGoTestEvents(replayFailure("TestGeneratedInvalidationVectors", "invalidation_vector_driver_test.go", + "INVALIDATION_INFRASTRUCTURE: connection refused"), 1)).toThrow(/Redis vector infrastructure failure/); + expect(evaluateGoTestEvents(replayFailure("TestGeneratedInvalidationVectors", "invalidation_vector_driver_test.go", + "invalidation result: got cutoff=1000 want cutoff=10000000"), 1)).toMatchObject({state: "detected"}); +}); diff --git a/test/formal-invalidation-native.test.ts b/test/formal-invalidation-native.test.ts new file mode 100644 index 00000000..19b4d8ac --- /dev/null +++ b/test/formal-invalidation-native.test.ts @@ -0,0 +1,37 @@ +import { randomUUID } from "node:crypto"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { readGeneratedInvalidationVectors } from "../formal/generate-invalidation-vectors.mjs"; +import { invalidationClient, recordInvalidation } from "./invalidation-native-driver.js"; +import { validVectorResult } from "../formal/vector-evidence.mjs"; + +// Mutation runners provision a private real server. Ordinary unit runs leave +// this lane disabled; make integration has its own vector replay. Mutation +// cohorts require the server endpoint and reject skipped selection. +describe.runIf(process.env.DIALCACHE_VECTOR_REDIS_URL !== undefined)("generated invalidation vectors", () => { + const vectors = readGeneratedInvalidationVectors().vectors; + let client: ReturnType; + const key = `{formal-invalidation-${randomUUID()}}#watermark`; + beforeAll(async () => { + client = invalidationClient(); + client.on("error", () => {}); + await client.connect(); + }); + afterAll(async () => { if (client?.isOpen) { await client.del(key); await client.quit(); } }); + for (const row of vectors) it(row.name, async () => { + let actual; + try { + actual = await recordInvalidation(client, key, { existing: row.existing, futureBufferMs: row.futureBufferMs, invalidatedAtMs: row.invalidatedAtMs }); + if (!validVectorResult("invalidation", actual)) throw new Error("Malformed native invalidation result"); + } catch (error) { throw new Error(`INVALIDATION_INFRASTRUCTURE: ${String(error)}`); } + const expected = row.expected.state; + expect({ outcome: actual.outcome, kind: actual.kind, content: actual.content }).toEqual({ + outcome: row.expected.error ? "rejected" : "success", kind: expected.kind, + content: expected.kind === "string" ? expected.value : expected.kind === "list" ? expected.values : [], + }); + if (expected.ttlMs < 0) expect(actual.ttlMs).toBe(expected.ttlMs); + else { + expect(actual.ttlMs).toBeGreaterThanOrEqual(Math.max(0, expected.ttlMs - actual.elapsedMs)); + expect(actual.ttlMs).toBeLessThanOrEqual(expected.ttlMs); + } + }); +}); diff --git a/test/formal-model-properties.test.ts b/test/formal-model-properties.test.ts index 28f48b37..d2a7bdf6 100644 --- a/test/formal-model-properties.test.ts +++ b/test/formal-model-properties.test.ts @@ -1,13 +1,31 @@ -import { readFileSync } from "node:fs"; -import { describe, expect, it } from "vitest"; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { resolve } from "node:path"; +import { describe, expect, it, vi } from "vitest"; type Reproducer = { kind: string; run: string; model?: string; failure: string; family: string; profiles: string[]; exclusions: Record; scope?: string }; type Challenge = { id: string; contract: string; source: string; model: string; invariant: string; before: string; after: string; measures?: string; reproducer?: Reproducer }; -const { validatePropertyResult, validateReproducerResult, probeNames, selectChallenges } = await import(new URL("../formal/check-model-properties.mjs", import.meta.url).href) as { +type ProfileModel = { path: string; profile: string; invariants: string[]; regressions: string[] }; +type PartitionPlan = Array<{ model: ProfileModel; mode: string }>; +type Inconclusive = { run: string; code: string; message: string }; +type ProfileResult = { status: string; failed?: string[]; inconclusive?: Inconclusive[] }; +type ProfileCheck = (model: ProfileModel, label: string) => Promise; +type MeasurementEntry = { id: string; baseline: string; mutant: string; error?: string }; +type MeasurementReport = { complete: boolean; partial: boolean; challenges: MeasurementEntry[] }; +const { validatePropertyResult, validateReproducerResult, validateProfileTests, challengePartitionPlan, checkChallengePartition, probeNames, selectChallenges, runChallengeMeasurements } = await import(new URL("../formal/check-model-properties.mjs", import.meta.url).href) as { validatePropertyResult(result: unknown, exitCode: number, expectation: string): void; validateReproducerResult(output: unknown, exitCode: number | null, run: unknown, expectation: string): { status: string; code?: string }; probeNames(run: string): { before: string; through: string }; selectChallenges(manifest: { challenges: Challenge[] }, only?: string): Challenge[]; + validateProfileTests(output: string, exitCode: number | null, runs: string[]): { status: string; failed: string[]; inconclusive?: Inconclusive[] }; + challengePartitionPlan(challenge: Challenge, manifest: { models: ProfileModel[]; libraries: string[] }, directory: string): PartitionPlan; + checkChallengePartition(challenge: Challenge, plan: PartitionPlan, callbacks: { + tests: ProfileCheck; invariants: ProfileCheck; proven?: Set; checkExclusions?: boolean; + onInconclusive?: (profile: string, histories: Inconclusive[]) => void; + }): Promise>; + runChallengeMeasurements(report: MeasurementReport, measure: (entry: MeasurementEntry, index: number) => Promise, options: { + concurrency: number; save: () => void; + }): Promise; }; const { reproducerCheckpoint } = await import(new URL("../formal/execution.mjs", import.meta.url).href) as { reproducerCheckpoint(source: string, run: string, failure: string): { before: string; through: string }; @@ -103,6 +121,21 @@ describe("model property challenge evidence", () => { expect(selectChallenges(manifest, `${second!.id},${first!.id}`)).toEqual([first, second]); expect(() => selectChallenges(manifest, `${first!.id},invented-fault`)).toThrow(/Unknown model property challenges: invented-fault/); }); + it("credits only completed assertion failures in a profile's scheduled runs", () => { + expect(validateProfileTests(report([run], []), 0, [run])).toEqual({ status: "passed", failed: [] }); + expect(validateProfileTests(report([], [{ name: run }]), 1, [run])).toEqual({ status: "failed", failed: [run] }); + expect(validateProfileTests(report([], [{ name: run, code: "QNT511", message: `Test ${run} returned false` }]), 1, [run])).toEqual({ status: "failed", failed: [run] }); + expect(validateProfileTests(report([], [{ name: run, ...disabled }]), 1, [run])).toEqual({ status: "inconclusive", failed: [], inconclusive: [{ run, ...disabled }] }); + expect(validateProfileTests(report([], [{ name: run }, { name: before, ...disabled }]), 1, [run, before])).toEqual({ + status: "failed", failed: [run], inconclusive: [{ run: before, ...disabled }], + }); + expect(() => validateProfileTests(compileError, 1, [run])).toThrow(/did not complete/); + expect(() => validateProfileTests(report([run], []), 0, [run, before])).toThrow(/did not complete/); + expect(() => validateProfileTests(report([], [{ name: run }]).split("Error [")[0]!, 1, [run])) + .toThrow(/failed without complete Quint diagnostics/); + expect(() => validateProfileTests(report([], [{ name: run }]), 2, [run])).toThrow(/exit code/); + expect(() => validateProfileTests(report([run], []), 1, [run])).toThrow(/exit code/); + }); it("keeps unique compiling-fault anchors and named independent target properties", () => { expect(new Set(manifest.challenges.map(challenge => challenge.id)).size).toBe(manifest.challenges.length); for (const challenge of manifest.challenges) { @@ -114,3 +147,167 @@ describe("model property challenge evidence", () => { } }); }); + +describe("independent model challenge measurements", () => { + const measurementReport = (partial = false): MeasurementReport => ({ complete: false, partial, + challenges: ["first", "second", "later"].map(id => ({ id, baseline: "pending", mutant: "pending" })), + }); + + it("collects independent failures and completes later work before rejecting the campaign", async () => { + const measured = measurementReport(); + const stalePartition = new Error("exclusion no longer holds: first/dark-layers"); + const evaluatorFailure = new Error("Quint execution failed: SIGTERM"); + const finished: string[] = []; + const saved: MeasurementReport[] = []; + const failure = await runChallengeMeasurements(measured, async entry => { + try { + entry.baseline = "passed"; + if (entry.id === "first") throw stalePartition; + if (entry.id === "second") throw evaluatorFailure; + entry.mutant = "detected"; + } finally { finished.push(entry.id); } + }, { concurrency: 1, save: () => saved.push(structuredClone(measured)) }).catch((error: unknown) => error); + expect(finished).toEqual(["first", "second", "later"]); + expect(failure).toBeInstanceOf(AggregateError); + expect((failure as AggregateError).errors).toEqual([stalePartition, evaluatorFailure]); + expect((failure as Error).message).toContain("first: Error: exclusion no longer holds"); + expect((failure as Error).message).toContain("second: Error: Quint execution failed: SIGTERM"); + expect(measured.challenges).toEqual([ + { id: "first", baseline: "passed", mutant: "pending", error: String(stalePartition) }, + { id: "second", baseline: "passed", mutant: "pending", error: String(evaluatorFailure) }, + { id: "later", baseline: "passed", mutant: "detected" }, + ]); + expect(measured.complete).toBe(false); + expect(saved).toHaveLength(3); + expect(saved.every(snapshot => !snapshot.complete)).toBe(true); + expect(saved.at(-1)).toEqual(measured); + }); + + it("reports concurrent failures in catalog order after all work settles", async () => { + const measured = measurementReport(); + const first = new Error("first challenge failed"); + const second = new Error("second challenge failed"); + let releaseFirst!: () => void; + const secondFinished = new Promise(resolve => { releaseFirst = resolve; }); + const finished: string[] = []; + const failure = await runChallengeMeasurements(measured, async entry => { + if (entry.id === "first") { + await secondFinished; + finished.push(entry.id); + throw first; + } + finished.push(entry.id); + if (entry.id === "second") { + releaseFirst(); + throw second; + } + entry.mutant = "detected"; + }, { concurrency: 2, save: () => {} }).catch((error: unknown) => error); + expect(finished[0]).toBe("second"); + expect(finished).toContain("later"); + expect((failure as AggregateError).errors).toEqual([first, second]); + expect(measured.complete).toBe(false); + }); + + it("completes only successful full campaigns and propagates report failures", async () => { + for (const partial of [false, true]) { + const measured = measurementReport(partial); + const save = vi.fn(); + await runChallengeMeasurements(measured, async entry => { entry.baseline = "passed"; entry.mutant = "detected"; }, { concurrency: 2, save }); + expect(measured.complete).toBe(!partial); + expect(measured.challenges.every(entry => entry.error === undefined)).toBe(true); + expect(save).toHaveBeenCalledTimes(4); + } + const measured = measurementReport(); + const writeFailure = new Error("cannot save report"); + await expect(runChallengeMeasurements(measured, async () => {}, { concurrency: 1, save: () => { throw writeFailure; } })).rejects.toBe(writeFailure); + expect(measured.complete).toBe(false); + await expect(runChallengeMeasurements(measured, async () => {}, { concurrency: 1, save: () => { if (measured.complete) throw writeFailure; } })).rejects.toBe(writeFailure); + expect(measured.complete).toBe(false); + }); +}); + +describe("shared-library challenge partitions", () => { + const models = ["listed", "cited", "excluded", "structural"].map(profile => ({ + path: `formal/${profile}.qnt`, profile, invariants: ["obligation"], regressions: ["behaviorTest"], + })); + const challenge: Challenge = { id: "shared-boundary", contract: "C01", source: "formal/kernel/shared.qnt", model: "formal/listed.qnt", + invariant: "obligation", before: "true", after: "false", reproducer: { + kind: "exported-regression", run: "behaviorTest", model: "formal/cited.qnt", failure: "s.o.calls == List(1)", family: "shared-boundary", + profiles: ["listed", "cited"], exclusions: { excluded: "Imports the rule but never exercises its boundary." }, + } }; + const plan: PartitionPlan = models.map(model => ({ model, mode: model.profile === "excluded" ? "excluded" : model.profile === "structural" ? "structural" : "listed" })); + const proven = new Set(["formal/cited.qnt"]); + const pass = async () => ({ status: "passed", failed: [] }); + + it("infers structural exclusions and requires decisions when imports reach the fault", () => { + const directory = mkdtempSync(resolve(tmpdir(), "dialcache-partition-test-")); + try { + mkdirSync(resolve(directory, "formal/kernel"), { recursive: true }); + writeFileSync(resolve(directory, "formal/kernel/shared.qnt"), "module shared { pure val allowed = true }"); + writeFileSync(resolve(directory, "formal/bridge.qnt"), 'module bridge { import shared.* from "./kernel/shared" }'); + for (const model of models) writeFileSync(resolve(directory, model.path), `module ${model.profile} { ${model.profile === "structural" ? "" : 'import bridge.* from "./bridge"'} }`); + const inventory = { models, libraries: [challenge.source, "formal/bridge.qnt"] }; + expect(challengePartitionPlan(challenge, inventory, directory)).toEqual(plan); + expect(() => challengePartitionPlan({ ...challenge, reproducer: { ...challenge.reproducer!, profiles: ["listed", "cited", "structural"] } }, inventory, directory)).toThrow(/listed profile does not import/); + expect(() => challengePartitionPlan({ ...challenge, reproducer: { ...challenge.reproducer!, exclusions: {} } }, inventory, directory)) + .toThrow(/shared-boundary\/excluded: profile is neither listed nor excluded/); + // An unrelated profile adds a structural result without editing every + // challenge. If it later imports the source, its exclusion needs review. + const added = { ...models[3]!, path: "formal/added.qnt", profile: "added" }; + writeFileSync(resolve(directory, added.path), "module added { pure val unrelated = true }"); + const expanded = { ...inventory, models: [...models, added] }; + expect(challengePartitionPlan(challenge, expanded, directory)).toEqual([...plan, { model: added, mode: "structural" }]); + writeFileSync(resolve(directory, added.path), 'module added { import bridge.* from "./bridge" }'); + expect(() => challengePartitionPlan(challenge, expanded, directory)).toThrow(/shared-boundary\/added: profile is neither listed nor excluded/); + const reviewed = { ...challenge, reproducer: { ...challenge.reproducer!, exclusions: { ...challenge.reproducer!.exclusions, added: "Imports the rule without reaching its boundary." } } }; + expect(challengePartitionPlan(reviewed, expanded, directory)).toEqual([...plan, { model: added, mode: "excluded" }]); + expect(challengePartitionPlan({ ...challenge, source: "formal/listed.qnt" }, inventory, directory)).toEqual([]); + } finally { rmSync(directory, { recursive: true, force: true }); } + }); + + it("measures reaching exclusions and listed profiles while reusing proven detections", async () => { + const tests = vi.fn(async (model: ProfileModel, label: string) => ({ status: model.profile === "listed" && label === "mutant" ? "failed" : "passed", failed: ["behaviorTest"] })); + const invariants = vi.fn(pass); + expect(await checkChallengePartition(challenge, plan, { tests, invariants, proven })).toEqual({ listed: "detects", cited: "detects", excluded: "holds", structural: "structural" }); + expect(tests.mock.calls.map(([model, label]) => [model.profile, label])).toEqual([ + ["listed", "baseline"], ["listed", "mutant"], ["excluded", "baseline"], ["excluded", "mutant"], + ]); + expect(invariants).not.toHaveBeenCalled(); + }); + + it("falls back to scheduled invariants and refuses a listed survivor", async () => { + const invariants = vi.fn(async (_model: ProfileModel, label: string) => ({ status: label === "mutant" ? "failed" : "passed" })); + expect(await checkChallengePartition(challenge, plan, { tests: pass, invariants, proven })).toMatchObject({ listed: "detects" }); + expect(invariants.mock.calls.map(([model, label]) => [model.profile, label])).toEqual([["listed", "baseline"], ["listed", "mutant"]]); + await expect(checkChallengePartition(challenge, plan, { tests: pass, invariants: pass, proven })).rejects.toThrow(/listed profile does not detect/); + }); + + it("rejects exclusions that fail and checks their invariant suites under --only", async () => { + const onlyExcluded = plan.filter(entry => entry.mode !== "listed"); + await expect(checkChallengePartition(challenge, onlyExcluded, { tests: async (_model, label) => ({ status: label === "mutant" ? "failed" : "passed", failed: ["behaviorTest"] }), invariants: pass })) + .rejects.toThrow(/exclusion no longer holds: shared-boundary\/excluded; list the profile in profiles/); + const invariants = vi.fn(async (_model: ProfileModel, label: string) => ({ status: label === "mutant" ? "failed" : "passed" })); + await expect(checkChallengePartition(challenge, onlyExcluded, { tests: pass, invariants, checkExclusions: true })).rejects.toThrow(/scheduled invariant violation/); + expect(invariants.mock.calls.map(([model, label]) => [model.profile, label])).toEqual([["excluded", "baseline"], ["excluded", "mutant"]]); + }); + + it("never credits broken baselines or evaluator and setup failures", async () => { + await expect(checkChallengePartition(challenge, plan, { tests: async () => ({ status: "failed" }), invariants: pass, proven })).rejects.toThrow(/unmodified profile must pass/); + await expect(checkChallengePartition(challenge, plan, { tests: async () => { throw new Error("typecheck failed"); }, invariants: pass, proven })).rejects.toThrow(/typecheck failed/); + await expect(checkChallengePartition(challenge, plan, { tests: pass, invariants: async () => { throw new Error("evaluator crashed"); }, proven })).rejects.toThrow(/evaluator crashed/); + }); + it("keeps inconclusive histories separate from independent assertion or invariant evidence", async () => { + const inconclusive = [{ run: "disabledHistoryTest", ...disabled }]; + const onInconclusive = vi.fn(); + const listed = plan.filter(entry => entry.model.profile === "listed"); + const tests = async (_model: ProfileModel, label: string) => label === "baseline" ? { status: "passed" } : { status: "failed", failed: ["behaviorTest"], inconclusive }; + expect(await checkChallengePartition(challenge, listed, { tests, invariants: pass, onInconclusive })).toEqual({ listed: "detects" }); + expect(onInconclusive).toHaveBeenCalledWith("listed", inconclusive); + const noAssertion = async (_model: ProfileModel, label: string) => label === "baseline" ? { status: "passed" } : { status: "inconclusive", failed: [], inconclusive }; + expect(await checkChallengePartition(challenge, listed, { tests: noAssertion, invariants: async (_model, label) => ({ status: label === "baseline" ? "passed" : "failed" }), onInconclusive })).toEqual({ listed: "detects" }); + await expect(checkChallengePartition(challenge, listed, { tests: noAssertion, invariants: pass })).rejects.toThrow(/does not detect/); + await expect(checkChallengePartition(challenge, plan.filter(entry => entry.mode === "excluded"), { tests: noAssertion, invariants: pass })).rejects.toThrow(/exclusion is inconclusive/); + await expect(checkChallengePartition(challenge, listed, { tests: async () => ({ status: "inconclusive", inconclusive }), invariants: pass })).rejects.toThrow(/unmodified profile must pass/); + }); +}); diff --git a/test/formal-mutation-shards.test.ts b/test/formal-mutation-shards.test.ts index ee1aa020..6fea28f3 100644 --- a/test/formal-mutation-shards.test.ts +++ b/test/formal-mutation-shards.test.ts @@ -1,3 +1,4 @@ +import type { VectorEvidence } from "../formal/vector-evidence.mjs"; import { createHash } from "node:crypto"; import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; @@ -7,7 +8,8 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; type Shard = { index: number; count: number }; type Selection = { shard: Shard; only?: string[] | undefined }; type Cohort = { state: "detected" | "survived" | "crashed"; passed: number; failed: number; failingTests: string[]; [key: string]: unknown }; -type Mutation = { id: string; case: string; description: string; cohorts: Record }; +type Boundary = { vector?: VectorEvidence; challenge: string; mutant: string; history?: string; step?: number; fields?: string[]; state?: string; [key: string]: unknown }; +type Mutation = { id: string; case: string; description: string; cohorts: Record; boundary?: Boundary[] }; type Report = Record & { shard?: Shard & { mutationIds: string[] }; baselines: Record; mutations: Mutation[] }; type CatalogEntry = { id: string; case: string; description: string; requiredDetections: string[] }; type Catalog = { mutations: CatalogEntry[] }; @@ -39,7 +41,8 @@ const merge = await import(new URL("../formal/merge-mutation-reports.mjs", impor readShardReports(directory: string): Report[]; mergeMutationReports(name: string, options?: { directory?: string; shardsDirectory?: string; outputDirectory?: string }): Report; }; -const { challengesByMutant } = await import(new URL("../formal/execution.mjs", import.meta.url).href) as { +const { boundaryEvidence, challengesByMutant } = await import(new URL("../formal/execution.mjs", import.meta.url).href) as { + boundaryEvidence(): Boundary[]; challengesByMutant(manifest: { challenges: Array<{ id: string; nativeMutants?: { mutant?: string } }> }): Map; }; const { parseShard, parseOnly, selectionFromArguments, partitionMutations, selectMutations, selectionDirectory, fingerprintFiles, requiredDetectionRegressions, goDetection, typescriptDetection, gateDetections, languages, classifyCohort, crashedCohort, noncompilingResult, portableCohort } = shared; @@ -65,6 +68,36 @@ const whole = { index: 1, count: 1 }; const slices = (count: number) => Array.from({ length: count }, (_, position) => partitionMutations(ids, { index: position + 1, count })); // The Challenges column names the model challenges each mutant is the native twin of. const challengesOf = (id: string) => (challengesByMutant(JSON.parse(readRepo("formal/execution.json").toString())).get(id) ?? []).join(", ") || "none"; +const evidence = boundaryEvidence(); +// Synthetic observations exercise the report merger; these are harness fixtures, +// never native measurement evidence. Every vector fixture has typed actual output. +function vectorRecording(entry: Boundary, port: "typescript" | "go", changed = false) { + const vector = entry.vector!, sample = vector.samples[port]; + let actual = structuredClone(sample.expected); + if (changed) switch (sample.request.operation) { + case "key": actual = {kind: "key_error"}; break; + case "trackedDecode": actual.reason = actual.reason === "expired" ? "watermark_fenced" : "expired"; break; + case "envelope": actual.decodedHex = `${actual.decodedHex}ff`; break; + case "compression": actual.outcome = actual.outcome === "compressed" ? "not_smaller" : "compressed"; break; + case "invalidation": + if (entry.fields!.includes("content")) actual.content = `${actual.content}0`; + else actual.outcome = actual.outcome === "success" ? "rejected" : "success"; + break; + default: throw new Error("Unknown vector fixture operation"); + } + return { history: entry.history!, completed: true, lastStep: 0, divergences: [], vectorResult: { + history: entry.history!, port, row: sample.row, artifactSha256: vector.artifactSha256, inputSha256: sample.inputSha256, actual, + } }; +} +const boundaries = (id: string, port: "typescript" | "go"): Boundary[] => evidence.filter(entry => entry.mutant === id).map(entry => + entry.vector ? { ...entry, ...vectorRecording(entry, port, true), state: "confirmed" } : entry.state ? { ...entry } : { + ...entry, state: "confirmed", completed: true, lastStep: entry.step! + 1, + divergences: [{ step: entry.step!, paths: [entry.fields![0]!] }], + }); +const boundaryBaselines = (port: "typescript" | "go") => Object.fromEntries(evidence.filter(entry => entry.history).map(entry => [entry.history!, + entry.vector ? vectorRecording(entry, port) : { + history: entry.history, completed: true, lastStep: Math.max(...evidence.filter(other => other.history === entry.history).map(other => other.step!)) + 1, divergences: [], + }])); // Shapes follow the hosted Go report (run 34669546872) and the TypeScript // runner's report object: the same keys, small values. @@ -90,7 +123,7 @@ const fixedSurvivors = new Set(["M09"]); function goMutation(entry: CatalogEntry): Mutation { const generated = goCohort(goTests.generated, [goTests.generated[0]!, goTests.generated[2]!]); const fixed = goCohort(goTests.fixed, fixedSurvivors.has(entry.id) ? [] : [goTests.fixed[1]!]); - return { id: entry.id, case: entry.case, description: entry.description, + return { id: entry.id, case: entry.case, description: entry.description, boundary: boundaries(entry.id, "go"), cohorts: { ordinary: goCohort(goTests.ordinary, ordinarySurvivors.has(entry.id) ? [] : [goTests.ordinary[0]!]), generated, fixed, portable: union(generated, fixed) } }; } function goBaselines(): Record { @@ -108,11 +141,12 @@ function goIdentity(catalogSha256: string, inputs: { files: number; sha256: stri // What measure-go-semantics.mjs writes for --shard=1/1. function goSingleReport(catalogSha256: string, inputs: { files: number; sha256: string }): Report { const mutations = goCatalog.mutations.map(goMutation); - return { schemaVersion: 1, complete: true, startedAt: "2026-09-12T00:27:22.662Z", baselines: goBaselines(), mutations, elapsedSeconds: 1507, + return { schemaVersion: 1, complete: true, startedAt: "2026-09-12T00:27:22.662Z", baselines: goBaselines(), boundaryBaselines: boundaryBaselines("go"), mutations, elapsedSeconds: 1507, ...goIdentity(catalogSha256, inputs), detection: goDetection(mutations), requiredDetectionRegressions: [] }; } // What each measure-go-semantics.mjs --shard=i/n run writes. function goShardReports(single: Report, count: number): Report[] { + single = structuredClone(single); return Array.from({ length: count }, (_, position) => { const shard = { index: position + 1, count }; const mutations = partitionMutations(single.mutations, shard); @@ -132,7 +166,7 @@ function tsCohort(executed: string[], failing: string[] = []): Cohort { function tsMutation(entry: CatalogEntry): Mutation { const generated = tsCohort(tsTests.generated, [tsTests.generated[0]!]); const fixed = tsCohort(tsTests.fixed, entry.id === "M09" ? [] : [tsTests.fixed[0]!]); - return { id: entry.id, case: entry.case, description: entry.description, + return { id: entry.id, case: entry.case, description: entry.description, boundary: boundaries(entry.id, "typescript"), cohorts: { ordinary: tsCohort(tsTests.ordinary, entry.id === "M12" ? [] : [tsTests.ordinary[0]!]), generated, fixed, portable: union(generated, fixed) } }; } function tsBaselines(): Record { @@ -146,7 +180,7 @@ function tsShardReports(catalogSha256: string, inputs: { files: number; sha256: const shard = { index: position + 1, count }; const slice = partitionMutations(mutations, shard); return { schemaVersion: 1, complete: false, shard: { ...shard, mutationIds: slice.map(mutation => mutation.id) }, startedAt: `2026-09-12T01:0${position}:00.000Z`, - revision: "ed9bd62", node: "v24.20.0", catalogSha256, sourceSha256: { "src/dialcache.ts": "aa".repeat(32) }, declaredCoverage, baselines: tsBaselines(), mutations: slice, + revision: "ed9bd62", node: "v24.20.0", catalogSha256, sourceSha256: { "src/dialcache.ts": "aa".repeat(32) }, declaredCoverage, baselines: tsBaselines(), boundaryBaselines: boundaryBaselines("typescript"), mutations: slice, elapsedSeconds: 300, inputs, configurationSha256: { "package.json": "bb".repeat(32) }, corpus: { files: 5514, sha256: "cc".repeat(32) }, reachedWitnesses: { effects: { required: 40, reached: 40, traces: 300 } } }; }); @@ -229,6 +263,32 @@ describe("mutation shard partition", () => { expect(() => gateDetections(languages.go, report, goCatalog.mutations, { summarize: false })).toThrow(/has no measured result/); expect(requiredDetectionRegressions(slice, report.mutations)).toEqual([`${slice[0]!.id}/generated`]); }); + + it("requires current completed boundary evidence independently of cohort detection", () => { + const entry = goCatalog.mutations.find(item => item.id === "M29")!; + const declaration = evidence.find(item => item.mutant === entry.id && item.history)!; + const expected = `${entry.id}/boundary:${declaration.challenge}`; + const fresh = (): Report => ({ ...goSingleReport("catalog", { files: 1, sha256: "x" }), complete: false, + mutations: [goMutation(entry)] }); + for (const change of [ + (report: Report) => { delete report.mutations[0]!.boundary; }, + (report: Report) => { report.mutations[0]!.boundary![0]!.step = declaration.step! + 1; }, + (report: Report) => { report.mutations[0]!.boundary![0]!.divergences = []; }, + (report: Report) => { report.mutations[0]!.boundary![0]!.completed = false; }, + (report: Report) => { report.boundaryBaselines = {}; }, + (report: Report) => { report.mutations[0]!.boundary![0]!.divergences = [{ step: declaration.step, paths: ["o.policyCalls"] }]; }, + ]) { + const report = fresh(); change(report); + expect(() => gateDetections(languages.go, report, [entry], { summarize: false })).toThrow(expected); + expect(report.requiredDetectionRegressions).toEqual([expected]); + expect(report.complete).toBe(false); + } + const report = fresh(); + gateDetections(languages.go, report, [entry], { summarize: false }); + expect(report.requiredDetectionRegressions).toEqual([]); + report.mutations[0]!.boundary!.push(structuredClone(report.mutations[0]!.boundary![0]!)); + expect(() => gateDetections(languages.go, report, [entry], { summarize: false })).toThrow(/repeats a challenge/); + }); }); describe("mutation shard merge", () => { @@ -274,6 +334,27 @@ describe("mutation shard merge", () => { expect((report.shards as unknown[]).length).toBe(ids.length + 1); }); + it("unions clean boundary baselines and preserves each mutant's boundary result", () => { + const reports = shards(); + for (const report of reports) for (const mutation of report.mutations) delete mutation.boundary; + const history = "shadow-layers/capturedTest"; + const boundary = { challenge: "captured-retention", history, step: 7, fields: ["o.writeTtls"], state: "confirmed" }; + for (const [index, report] of reports.entries()) report.boundaryBaselines = { + [history]: { history, path: `/runner-${index}/regressions/${history}.itf.json`, completed: true, lastStep: 11, divergences: [] }, + }; + Object.assign(reports[0]!.mutations[0]!, { boundary: [boundary] }); + // This test isolates the merge; the gate separately checks current pins. + const result = mergeShardReports(languages.go, reports, context); + expect(result.boundaryBaselines).toEqual({ [history]: { history, completed: true, lastStep: 11, divergences: [] } }); + expect(result.mutations[0]).toMatchObject({ boundary: [boundary] }); + const divergent = structuredClone(reports); + divergent[1]!.boundaryBaselines = { [history]: { history, completed: true, lastStep: 11, divergences: [{ step: 2, paths: ["o.calls.0"] }] } }; + refuse(divergent, /boundary baseline .*not a clean completed replay/); + const missing = structuredClone(reports); + for (const report of missing) report.boundaryBaselines = {}; + refuse(missing, /boundary history .*no clean baseline/); + }); + it("keeps a crashed Go cohort out of the detection total and lists it apart from survivors", () => { const generated = goCohort(goTests.generated, [goTests.generated[0]!]); const fixed = goCohort(goTests.fixed); @@ -425,6 +506,11 @@ describe("mutation shard merge over a shard directory", () => { directory = mkdtempSync(join(tmpdir(), "dialcache-mutation-shards-")); for (const path of ["formal", "src", "test", "go", "shards"]) mkdirSync(join(directory, path), { recursive: true }); for (const path of ["formal/mutations.json", "formal/semantic-cases.json", "formal/execution.json"]) copyFileSync(new URL(path, repo), join(directory, path)); + const manifest = JSON.parse(readRepo("formal/execution.json").toString()) as { models: { path: string; vectorExport?: {artifact: string} }[] }; + for (const model of manifest.models) { + copyFileSync(new URL(model.path, repo), join(directory, model.path)); + if (model.vectorExport) copyFileSync(new URL(model.vectorExport.artifact, repo), join(directory, model.vectorExport.artifact)); + } writeFileSync(join(directory, "src/index.ts"), "export {};\n"); writeFileSync(join(directory, "test/index.test.ts"), "export {};\n"); writeFileSync(join(directory, "go/cache.go"), "package dialcache\n"); diff --git a/test/formal-profile-lint.test.ts b/test/formal-profile-lint.test.ts index 5c1bbbc1..e13fd982 100644 --- a/test/formal-profile-lint.test.ts +++ b/test/formal-profile-lint.test.ts @@ -11,6 +11,7 @@ type Report = { main: string; modules: string[]; tableSize: number; + shapes: { states: Array<{ variable: string; fields: string[] }>; count: number; violations: Array<{ rule: string; variable: string; fields: string[]; detail: string }> }; composition: { kernelModules: string[]; actions: string[]; publicActions: string[]; reachableDefinitions: number; stateAssigningDefinitions: string[]; libraryTransitions: string[]; count: number; violations: CompositionViolation[] }; witnessIsolation: { witnessVariables: string[]; @@ -56,6 +57,111 @@ const quintAvailable = spawnSync("quint", ["--version"], { encoding: "utf8" }).s const quintTimeout = 60_000; const cli = (...args: string[]) => spawnSync(process.execPath, ["formal/lint-profiles.mjs", ...args], { cwd: root, encoding: "utf8" }); +describe("atomic profile restrictions", () => { + it("requires every atomic-release profile to schedule the payload safety property", () => { + const baseline = JSON.parse(readFileSync(root + "formal/profile-lint-baseline.json", "utf8")) as Baseline; + const manifest = JSON.parse(readFileSync(root + "formal/execution.json", "utf8")) as { models: Array<{ profile?: string; path: string; invariants: string[] }> }; + const atomic = new Set(["serving::begin", "serving::release", "receipts::release", "remote_writes::releaseJudged", "shadow::begin", "shadow::release", "local_faults::begin"]); + for (const profile of baseline.profiles.filter(profile => profile.libraryTransitions.some(transition => atomic.has(transition)))) { + const model = manifest.models.find(model => model.profile === profile.id)!; + expect(model.invariants, profile.id).toContain("atomicPathSeedsDecodableFrames"); + expect(readFileSync(root + model.path, "utf8"), profile.id).toMatch(/val atomicPathSeedsDecodableFrames\s*=\s*Payloads::atomicSafe\(s\)/); + } + }); +}); + +describe.skipIf(!quintAvailable)("kernel shape restrictions", () => { + const directory = mkdtempSync(join(tmpdir(), "dialcache-lint-shapes-")); + afterAll(() => rmSync(directory, { recursive: true, force: true })); + for (const [module, transitions] of Object.entries({ + shadow: ["beginDark", "settleHeld", "beginHeld", "begin", "settleJob"], metrics: ["begin", "settleLoader"], + local_faults: ["begin", "settle"], remote_io: ["begin"], + compression: ["settleLoader"], adapter_replies: ["settleRead"], remote_writes: ["settleLoader"], + })) writeFileSync(join(directory, `${module}.qnt`), `module ${module} {\n${transitions.map(name => + `pure def ${name}(state: { n: int | r }): { n: int | r } = state`).join("\n")}\n}`); + async function inspect(name: string, fields: string, initial: string, transition: string, extra = "", other = false) { + const path = join(directory, `${name}.qnt`); + writeFileSync(path, `module ${name} { + import shadow as Shadow from "./shadow" + import metrics as Metrics from "./metrics" + import local_faults as Faults from "./local_faults" + import remote_io as Remote from "./remote_io" + import compression as Compression from "./compression" + import adapter_replies as Replies from "./adapter_replies" + import remote_writes as Writes from "./remote_writes" + ${extra} + type State = ${fields} + var s: State + ${other ? "var other: { n: int }" : ""} + var input: int + action init = all { s' = ${initial}, input' = 0 ${other ? ", other' = { n: 0 }" : ""} } + action step = all { s' = ${transition}, input' = 1 ${other ? ", other' = Shadow::beginDark(other)" : ""} } + }`); + const checked = spawnSync("quint", ["typecheck", path], { cwd: root, encoding: "utf8" }); + expect(checked.status, checked.stdout + checked.stderr).toBe(0); + return lintModel(path, { kernelModules: ["shadow", "metrics", "local_faults", "remote_io", "compression", "adapter_replies", "remote_writes"] }); + } + + it("requires a lifecycle for jointly held reads and dumps, including type aliases", async () => { + const fields = "More[{ dumps: List[int], n: int }]", initial = "{ reads: List(), dumps: List(), n: 0 }"; + const alias = "type More[r] = { reads: List[int] | r }"; + for (const [name, transition] of [["dark", "Shadow::beginDark(s)"], ["effects", "Metrics::settleLoader(s)"], ["served", "Shadow::beginHeld(s)"]]) { + expect((await inspect(name!, fields, initial, transition!, alias)).shapes.count).toBe(0); + } + const bad = await inspect("mixed_held", fields, initial, "Remote::begin(s)", alias); + expect(bad.shapes.violations).toEqual([expect.objectContaining({ rule: "held-effects", fields: ["reads", "dumps"] })]); + expect(cli(join(directory, "mixed_held.qnt"), "--kernel=shadow,metrics,local_faults,remote_io").status).toBe(1); + }, quintTimeout); + + it("requires the dark lifecycle when jobs carry a caller source budget", async () => { + const fields = "{ jobs: List[int], sourceBudget: int, n: int }", initial = "{ jobs: List(), sourceBudget: 10, n: 0 }"; + expect((await inspect("dark_budget", fields, initial, "Shadow::settleHeld(s)")).shapes.count).toBe(0); + const bad = await inspect("served_budget", fields, initial, "Shadow::beginHeld(s)"); + expect(bad.shapes.violations).toEqual([ + expect.objectContaining({ rule: "held-effects", fields: ["jobs", "sourceBudget"], detail: expect.stringContaining("require one of") }), + expect.objectContaining({ rule: "held-effects", fields: ["jobs", "sourceBudget"], detail: expect.stringContaining("cannot compose shadow::beginHeld") }), + ]); + await expect(computeBaseline({ profiles: [{ id: "served-budget", model: join(directory, "served_budget.qnt") }] })).rejects.toThrow(/unsupported kernel shape/); + }, quintTimeout); + + it("rejects incompatible shadow lifecycles even beside valid dark transitions", async () => { + const fields = "{ jobs: List[int], sourceBudget: int, n: int }", initial = "{ jobs: List(), sourceBudget: 10, n: 0 }"; + for (const name of ["begin", "settleJob", "beginHeld"]) { + const bad = await inspect(`mixed_${name}`, fields, initial, `Shadow::${name}(Shadow::beginDark(s))`); + expect(bad.shapes.violations).toEqual([expect.objectContaining({ rule: "held-effects", detail: expect.stringContaining(`cannot compose shadow::${name}`) })]); + } + }, quintTimeout); + + it("does not use a transition assigned to another state as shape evidence", async () => { + for (const [name, fields, initial] of [ + ["isolated_budget", "{ jobs: List[int], sourceBudget: int, n: int }", "{ jobs: List(), sourceBudget: 10, n: 0 }"], + ["isolated_refill", "{ reads: List[int], dumps: List[int], n: int }", "{ reads: List(), dumps: List(), n: 0 }"], + ]) { + const bad = await inspect(name!, fields!, initial!, "Remote::begin(s)", "", true); + expect(bad.shapes.violations).toEqual([expect.objectContaining({ rule: "held-effects", variable: "s", detail: expect.stringContaining("require one of") })]); + } + }, quintTimeout); + + it("requires local-fault transitions and rejects healthy held work even alongside them", async () => { + const fields = "{ localFailed: bool, n: int }", initial = "{ localFailed: false, n: 0 }"; + expect((await inspect("local_fault", fields, initial, "Faults::begin(s)")).shapes.count).toBe(0); + expect((await inspect("ignored_fault", fields, initial, "s")).shapes.violations).toEqual([ + expect.objectContaining({ rule: "local-fault", detail: expect.stringContaining("require one of") }), + ]); + const bad = await inspect("faulted_held", fields, initial, "Remote::begin(Faults::settle(s))"); + expect(bad.shapes.violations).toEqual([expect.objectContaining({ rule: "local-fault", detail: expect.stringContaining("cannot compose remote_io::begin") })]); + for (const [name, transition, target] of [ + ["faulted_compression", "Compression::settleLoader", "compression::settleLoader"], + ["faulted_reply", "Replies::settleRead", "adapter_replies::settleRead"], + ["faulted_write", "Writes::settleLoader", "remote_writes::settleLoader"], + ["faulted_metrics", "Metrics::begin", "metrics::begin"], + ]) { + const wrapper = await inspect(name!, fields, initial, `${transition}(Faults::settle(s))`); + expect(wrapper.shapes.violations).toEqual([expect.objectContaining({ rule: "local-fault", detail: expect.stringContaining(`cannot compose ${target}`) })]); + } + }, quintTimeout); +}); + describe("profile lint baseline diff", () => { const baseline: Baseline = { schemaVersion: 3, quintVersion: "0.32.0", kernelModules: ["serving"], profiles: [ { id: "a", model: "formal/a.qnt", module: "a", libraryTransitions: ["serving::begin"], compositionViolations: 0 }, @@ -461,6 +567,6 @@ describe.skipIf(!quintAvailable)("profile lint baseline", () => { it("passes baseline --check from the CLI on the committed file", () => { const result = cli("baseline", "--check"); expect(result.status, result.stderr).toBe(0); - expect(result.stdout).toContain(`${baselinePath} matches 15 profiles`); + expect(result.stdout).toContain(`${baselinePath} matches ${JSON.parse(readFileSync(join(root, baselinePath), "utf8")).profiles.length} profiles`); }, 180_000); }); diff --git a/test/formal-quint-pool.test.ts b/test/formal-quint-pool.test.ts index 5691e662..da1b7e1e 100644 --- a/test/formal-quint-pool.test.ts +++ b/test/formal-quint-pool.test.ts @@ -120,6 +120,28 @@ describe("Quint process pool", () => { expect(failure.status).toBe(2); }); + it("retains complete, isolated diagnostics when concurrent children exit immediately after large writes", async () => { + const outputs = ["first λ🙂\n", "second λ🙂\n"].map(prefix => ({ + stdout: prefix.repeat(131_072) + "stdout-tail", + stderr: prefix.repeat(131_072) + "stderr-tail", + })); + const results = await Promise.all(["first λ🙂\n", "second λ🙂\n"].map(prefix => + spawnBuffered(process.execPath, ["-e", ` + const text = ${JSON.stringify(prefix)}.repeat(131_072); + process.stdout.write(text + 'stdout-tail'); + process.stderr.write(text + 'stderr-tail'); + process.exit(1); + `]))); + for (const [index, result] of results.entries()) { + expect(result).toMatchObject({ status: 1, signal: null }); + expect(result.error).toBeUndefined(); + for (const stream of ["stdout", "stderr"] as const) { + expect(result[stream]).toHaveLength(outputs[index]![stream].length); + expect(result[stream]).toBe(outputs[index]![stream]); + } + } + }); + it("prints a command's log as one closed group block", () => { expect(formatGroup("quint run model", "line one\n", "line two")).toBe("::group::quint run model\nline one\nline two\n::endgroup::"); expect(formatGroup("quint typecheck model", "", undefined as unknown as string)).toBe("::group::quint typecheck model\n::endgroup::"); @@ -139,23 +161,22 @@ describe("Quint process pool", () => { expect(planned.args).toEqual(["run", "formal/dialcache-layers-conformance.qnt", "--mbt", "--backend=rust", "--n-threads=1", "--seed=0xd1a1ca", "--max-samples=2048", "--max-steps=80", "--n-traces=512", "--out-itf=.formal-traces/features/layers/trace_{seq}.itf.json", "--verbosity=1", "--invariants", "sourceEffectsMatch", "capacityIsPerInstance", "closedScopesHaveNoMemo", "registeredSourcesArePending", "zeroCapacityHasNoLocalValues", "callsKeepSourceOutcome", - "localMembershipMatchesLru", "absentRemoteHasNoAdapterEffects", "sourceOwnershipNeverCrossesKeyOrInstance"]); + "localMembershipMatchesLru", "absentRemoteHasNoAdapterEffects", "sourceOwnershipNeverCrossesKeyOrInstance", "atomicPathSeedsDecodableFrames"]); const pilot = generationArguments("/differential/layers/candidate/formal/dialcache-layers-conformance.qnt", layers.generate!, ["a", "b"], { ...options, outputDirectory: "/scratch" }); const shared = planned.args.slice(0, planned.args.indexOf("--invariants") + 1) .map(arg => arg === layers.path ? "/differential/layers/candidate/formal/dialcache-layers-conformance.qnt" : arg.startsWith("--out-itf=") ? "--out-itf=/scratch/trace_{seq}.itf.json" : arg); expect(pilot).toEqual([...shared, "a", "b"]); }); - it("puts every job of the real plans into exactly one per-model chain, in plan order, without the challenge run", () => { + it("puts every job of the real plans into exactly one per-model chain, in plan order", () => { for (const mode of ["check", "generate"] as const) { const plan = executionPlan(mode); const chains = executionChains(plan); const chained = chains.flat(); const challengeRuns = plan.filter(job => job.command === "node" && job.args[0] === "formal/check-model-properties.mjs"); - expect(challengeRuns.length).toBe(mode === "check" ? 1 : 0); - expect(chained.length).toBe(plan.length - challengeRuns.length); + expect(challengeRuns).toEqual([]); + expect(chained.length).toBe(plan.length); expect(new Set(chained).size).toBe(chained.length); - expect(chained.filter(job => challengeRuns.includes(job))).toEqual([]); for (const chain of chains) { expect(new Set(chain.map(job => job.command === "quint" ? job.args[1] : job.args.join(" "))).size).toBe(1); const positions = chain.map(job => plan.indexOf(job)); @@ -164,7 +185,7 @@ describe("Quint process pool", () => { } }); - it("chains a model's jobs in plan order, isolates node exports and leaves the challenge run out", () => { + it("chains a model's jobs in plan order and isolates node exports", () => { const plan = [ { command: "quint", args: ["typecheck", "formal/a.qnt"] }, { command: "quint", args: ["run", "formal/a.qnt", "--seed=1"] }, @@ -172,7 +193,6 @@ describe("Quint process pool", () => { { command: "quint", args: ["typecheck", "formal/b.qnt"] }, { command: "node", args: ["formal/generate-key-vectors.mjs", "--check"] }, { command: "quint", args: ["run", "formal/b.qnt", "--mbt"] }, - { command: "node", args: ["formal/check-model-properties.mjs"] }, ]; expect(executionChains(plan)).toEqual([ [plan[0], plan[1], plan[2]], diff --git a/test/formal-replay-coordinator.test.ts b/test/formal-replay-coordinator.test.ts index 94d8deaa..1328cd43 100644 --- a/test/formal-replay-coordinator.test.ts +++ b/test/formal-replay-coordinator.test.ts @@ -1,4 +1,5 @@ import { AssertionError } from "node:assert"; +import { spawnSync } from "node:child_process"; import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { Readable } from "node:stream"; @@ -6,7 +7,8 @@ import { resolve } from "node:path"; import { describe, expect, it, vi } from "vitest"; import { bindTrace, profileActions } from "../formal/replay/bindings.mjs"; -import { ReplayCoordinator, settlement } from "../formal/replay/coordinator.mjs"; +import { ReplayCoordinator, settlement, type ReplayRecording } from "../formal/replay/coordinator.mjs"; +import { diffPaths, isObservationComparison } from "../formal/replay/divergence.mjs"; import { expectedCoreObservation, parseItfTrace } from "../formal/replay/core.mjs"; import { featureInput, profiles } from "../formal/replay/features.mjs"; import { inputsFor } from "../formal/replay/effects.mjs"; @@ -17,6 +19,7 @@ import { parseJSON, replayLines } from "../formal/replay/validation.mjs"; import { replaySources } from "../formal/replay/sources.mjs"; import { BehaviorDriver, type Fixture, type Input } from "./formal/behavior-driver.js"; import { replayThroughCoordinator, smokeTracePath } from "./formal/coordinated-replay.js"; +import { LocalCache } from "../src/internal/local-cache.js"; type Raw = { states: Array & { s: Record }> }; function smoke(profile: string): Raw { @@ -26,8 +29,7 @@ function smoke(profile: string): Raw { const environment = { wallMs: wallEpochMs }; const roundtrip = (value: unknown) => parseJSON(JSON.stringify(value)); -function replaySession(raw: Raw, profile = "core") { - const coordinator = new ReplayCoordinator(); +function replaySession(raw: Raw, profile = "core", coordinator = new ReplayCoordinator()) { let id = 0; const request = (fields: Record) => coordinator.dispatch(roundtrip({ version: 1, id: ++id, ...fields })); const prepared = request({ op: "prepare", profile, path: "control.itf.json", raw: JSON.stringify(raw) }); @@ -189,6 +191,9 @@ describe("observation encoding contract", () => { { profile: "shadow", definition: "behaviorObservation", path: "observed.events[0].seconds", observed: () => ({ ...behaviorObservation("shadow"), events: [{ event: "shadowAge", cacheNamespace: "urn", useCase: "Behavior", keyType: "id", outcome: "match", seconds: "1" }] }) }, { profile: "effects", definition: "behaviorObservation", path: "observed.writeTtls[0]", observed: () => ({ ...behaviorObservation("effects"), writeTtls: [60000.5] }) }, { profile: "core", definition: "coreObservation", path: "observed.redisReads", observed: () => ({ ...coreObservation(), redisReads: -1 }) }, + { profile: "core", definition: "coreObservation", path: "observed.lastResult", observed: () => ({ ...coreObservation(), lastResult: null }) }, + { profile: "core", definition: "coreObservation", path: "observed.lastResult", observed: () => ({ ...coreObservation(), lastResult: { absent: false } }) }, + { profile: "core", definition: "coreObservation", path: "observed.lastResult", observed: () => { const { lastResult: _value, ...rest } = coreObservation(); return rest; } }, { profile: "core", definition: "coreObservation", path: "observed.redisWrites", observed: () => { const { redisWrites: _writes, ...rest } = coreObservation(); return rest; } }, { profile: "local-clock", definition: "localClockObservation", path: "observed.calls[0]", observed: () => ({ ...emptyObservation(), calls: [{ status: "pending" }] }) }, { profile: "local-clock", definition: "localClockObservation", path: "observed.events", observed: () => ({ ...emptyObservation(), calls: [], events: [] }) }, @@ -501,9 +506,31 @@ describe("fixture work on the receipt clocks", () => { }); describe("coordinated end-to-end replay with the real drivers", () => { + it("records an absent actual core hit as a wrong result, preserving the complete history", async () => { + const raw = smoke("core"); + raw.states = raw.states.slice(0, 6); // Warm local, change source, then read the stored value. + const original = LocalCache.prototype.getWithResolvedConfig; + vi.spyOn(LocalCache.prototype, "getWithResolvedConfig").mockImplementation(function (this: LocalCache, ...args) { + const result = original.apply(this, args); + return result.status === "hit" ? { ...result, value: undefined } : result; + }); + const directory = mkdtempSync(resolve(tmpdir(), "dialcache-core-absent-")); + const records: ReplayRecording[] = []; + try { + const path = resolve(directory, "absent.itf.json"); + writeFileSync(path, JSON.stringify(raw)); + const result = await replayThroughCoordinator("core", path, new ReplayCoordinator({ record: true, onRecord: record => records.push(record) })); + expect(result.divergences).toEqual([{ step: 5, action: "localCall", paths: ["lastResult"] }]); + expect(records).toEqual([{ path, completed: true, lastStep: 5, divergences: result.divergences }]); + } finally { vi.restoreAllMocks(); rmSync(directory, { recursive: true, force: true }); } + }); + it.each(Object.keys(profileActions()))("replays the committed %s smoke trace through the coordinator", async profile => { - const result = await replayThroughCoordinator(profile, smokeTracePath(profile)); + const records: ReplayRecording[] = []; + const result = await replayThroughCoordinator(profile, smokeTracePath(profile), new ReplayCoordinator({ record: true, onRecord: record => records.push(record) })); expect(result.steps).toBe(smoke(profile).states.length); + expect(result.divergences).toEqual([]); + expect(records).toEqual([{ path: smokeTracePath(profile), completed: true, lastStep: result.steps - 1, divergences: [] }]); }); it("reports a driver observation the model did not predict as a mismatch, not a shape error", async () => { @@ -518,6 +545,115 @@ describe("coordinated end-to-end replay with the real drivers", () => { }); }); +describe("recording divergent histories", () => { + it("stops at a transient projection assertion instead of making a carried counter look new", () => { + const raw = smoke("recovery"); + (raw.states[0]!.s.o as Record).policyCalls = { "#bigint": "999" }; + const records: ReplayRecording[] = []; + const session = replaySession(raw, "recovery", new ReplayCoordinator({ record: true, onRecord: record => records.push(record) })); + const empty = emptyObservation(session.binding.fixture as unknown as Fixture); + session.observe(0, empty); + // The age event was emitted before the decode marked recovery served. Its + // scalar outcome assertion hides the already divergent load counter. + const early = { ...empty, loads: 1, events: [{ event: "recoveryAge", cacheNamespace: "urn", useCase: "Behavior", keyType: "id", outcome: "served", seconds: 0 }] }; + expect(() => session.observe(1, early)).toThrow(/Observation projection assertion did not compare a complete record/); + expect(records).toEqual([{ path: "control.itf.json", completed: false, lastStep: 0, + divergences: [{ step: 0, action: "init", paths: ["o.policyCalls"] }], error: expect.stringMatching(/step 1 action .*Observation projection assertion/) }]); + // Once recovery is marked served, projection can compare the load count. + // Continuing the previous recording would falsely call that counter new. + const repaired = { ...early, recovery: ["served"] }; + try { session.binding.assert(1, repaired); throw new Error("Expected comparison failure"); } + catch (cause) { + expect(isObservationComparison(cause)).toBe(true); + const compared = cause as AssertionError; + expect(diffPaths(compared.expected, compared.actual)).toContain("o.loads"); + } + expect(() => session.observe(1, repaired)).toThrow(/Unknown replay session/); + }); + + it("continues every comparison while preserving wire replies and independent command selection", () => { + const original = smoke("core"); + original.states = original.states.slice(0, 3); + const changed = structuredClone(original); + changed.states[1]!.s.redisReads = { "#bigint": "998" }; + changed.states[2]!.s.redisReads = { "#bigint": "999" }; + const observations = parseItfTrace(original, "original").states.map(step => expectedCoreObservation(step.state)); + const records: ReplayRecording[] = []; + const coordinator = new ReplayCoordinator({ record: true, onRecord: record => records.push(record) }); + const session = replaySession(changed, "core", coordinator); + const clean = replaySession(original); + for (const [index, observation] of observations.entries()) expect(session.observe(index, observation)).toEqual(clean.observe(index, observation)); + const recording = coordinator.recording(session.prepared.session as string)!; + expect(recording).toEqual({ path: "control.itf.json", completed: true, lastStep: 2, + divergences: [1, 2].map(step => ({ step, action: session.binding.trace.steps[step]!.action, paths: ["redisReads"] })) }); + expect(records).toEqual([recording]); + recording.divergences[0]!.paths.push("tampered"); + expect(coordinator.recording(session.prepared.session as string)!.divergences[0]!.paths).toEqual(["redisReads"]); + expect(() => session.observe(3, {})).toThrow(/Unknown replay session/); + }); + + it("retains an earlier divergence when settlement refuses the next observation", () => { + const raw = smoke("independent"); + (raw.states[0]!.s.o as Record).policyCalls = { "#bigint": "999" }; + const records: ReplayRecording[] = []; + const session = replaySession(raw, "independent", new ReplayCoordinator({ record: true, onRecord: record => records.push(record) })); + const observed = emptyObservation(session.binding.fixture as unknown as Fixture); + session.observe(0, observed); + const fields = session.settlementFields(observed) as { receipt: SettlementReceipt; environment: { wallMs: number } }; + expect(() => session.observe(1, observed, { ...fields, receipt: { ...fields.receipt, runnable: 1 } })) + .toThrow(/step 1 action .*Settlement violation: 1 runnable task/); + expect(records).toEqual([{ path: "control.itf.json", completed: false, lastStep: 0, + divergences: [{ step: 0, action: "init", paths: ["o.policyCalls"] }], error: expect.stringMatching(/step 1 action .*Settlement violation: 1 runnable task/) }]); + }); + + it("records a driver failure after a divergence without completing the history", async () => { + const raw = smoke("shadow-layers"); + raw.states = raw.states.slice(0, 2); + (raw.states[0]!.s.o as Record).reads = { "#bigint": "999" }; + Object.assign(raw.states[1]!, { input: { name: "releaseDump", choice: { "#bigint": "0" } }, + "mbt::actionTaken": "releaseDump", "mbt::nondetPicks": { choice: { tag: "Some", value: { "#bigint": "0" } } } }); + const directory = mkdtempSync(resolve(tmpdir(), "dialcache-recording-failure-")); + const records: ReplayRecording[] = []; + try { + const path = resolve(directory, "unreachable.itf.json"); + writeFileSync(path, JSON.stringify(raw)); + await expect(replayThroughCoordinator("shadow-layers", path, new ReplayCoordinator({ record: true, onRecord: record => records.push(record) }))) + .rejects.toThrow(/No pending dump 0/); + expect(records).toEqual([{ path, completed: false, lastStep: 0, + divergences: [{ step: 0, action: "init", paths: ["o.reads"] }], error: expect.stringContaining("step 1 action releaseDump: No pending dump 0") }]); + } finally { rmSync(directory, { recursive: true, force: true }); } + }); + + it.each([false, true])("writes terminal JSONL evidence out of band (failure=%s)", failed => { + const original = smoke("core"); + original.states = original.states.slice(0, 2); + const changed = structuredClone(original); + changed.states[0]!.s.redisReads = { "#bigint": "999" }; + const observations = parseItfTrace(original, "original").states.map(step => expectedCoreObservation(step.state)); + const directory = mkdtempSync(resolve(tmpdir(), "dialcache-recording-wire-")); + try { + const output = resolve(directory, "divergences.jsonl"); + const requests = [ + { version: 1, id: 1, op: "prepare", profile: "core", path: "wire.itf.json", raw: JSON.stringify(changed) }, + ...observations.map((observed, index) => ({ version: 1, id: index + 2, op: "observe", session: "1", index, settlement, + observed: failed && index === 1 ? {} : observed, environment: { wallMs: wallEpochMs + index } })), + ]; + const process = spawnSync(globalThis.process.execPath, [resolve("formal/replay/coordinator.mjs")], { + input: requests.map(request => JSON.stringify(request)).join("\n") + "\n", encoding: "utf8", + env: { ...globalThis.process.env, DIALCACHE_REPLAY_DIVERGENCES: output }, + }); + expect(process.status, process.stderr).toBe(0); + const replies = process.stdout.trim().split("\n").map(line => JSON.parse(line) as Record); + for (const reply of replies) expect(() => assertSchema(reply, "response")).not.toThrow(); + expect(replies.at(-1)).toMatchObject(failed ? { ok: false, error: expect.stringContaining("Malformed replay observation") } + : { ok: true, result: { complete: true, steps: 2 } }); + const records = readFileSync(output, "utf8").trim().split("\n").map(line => JSON.parse(line) as ReplayRecording); + expect(records).toEqual([{ path: "wire.itf.json", completed: !failed, lastStep: failed ? 0 : 1, + divergences: [{ step: 0, action: "init", paths: ["redisReads"] }], ...(failed ? { error: expect.stringContaining("step 1 action outsideCall: Malformed replay observation") } : {}) }]); + } finally { rmSync(directory, { recursive: true, force: true }); } + }); +}); + describe("versioned replay protocol", () => { it("preserves absence, strings, null, false, zero and empty strings across JSON", () => { const values = [undefined, "undefined", null, false, 0, ""]; diff --git a/test/formal-semantic-reporter.test.ts b/test/formal-semantic-reporter.test.ts index 68b90db6..28aec6a9 100644 --- a/test/formal-semantic-reporter.test.ts +++ b/test/formal-semantic-reporter.test.ts @@ -87,6 +87,10 @@ it("requires executed assertions and distinguishes detection from infrastructure // The first violated rule travels with the error, so a mutant's cohort can be recorded against it by name. const thrown = (() => { try { evaluateSemanticTestReport(violated, { reason: "failed", collectionErrors: [], unhandledErrors: [] }, 1); } catch (error) { return error as Error & { settlementViolation?: string }; } return undefined; })(); expect(thrown?.settlementViolation).toBe("step 1 action beginCall: Settlement violation: read gates held 0, schedule requires 1"); + const redisFailure = { testResults: [{ assertionResults: [{ status: "failed", fullName: "Redis vector", failureMessages: ["Error: INVALIDATION_INFRASTRUCTURE: connection refused"] }] }] }; + expect(() => evaluateSemanticTestReport(redisFailure, { reason: "failed", collectionErrors: [], unhandledErrors: [] }, 1)).toThrow(/infrastructure/); + const codeFrame = { testResults: [{ assertionResults: [{ status: "failed", fullName: "Redis vector", failureMessages: ['AssertionError: expected cutoff1000 to equal10000000\n 24| throw new Error("INVALIDATION_INFRASTRUCTURE: example")'] }] }] }; + expect(evaluateSemanticTestReport(codeFrame, { reason: "failed", collectionErrors: [], unhandledErrors: [] }, 1)).toMatchObject({state: "detected"}); const timedOut = { testResults: [{ assertionResults: [{ status: "failed", fullName: "slow", failureMessages: ["Error: Test timed out in 5000ms."] }] }] }; const slow = (() => { try { evaluateSemanticTestReport(timedOut, { reason: "failed", collectionErrors: [], unhandledErrors: [] }, 1); } catch (error) { return error as Error & { settlementViolation?: string }; } return undefined; })(); expect(slow?.settlementViolation).toBeUndefined(); diff --git a/test/formal-shadow-read-deadlines-witnesses.test.ts b/test/formal-shadow-read-deadlines-witnesses.test.ts new file mode 100644 index 00000000..07dabf74 --- /dev/null +++ b/test/formal-shadow-read-deadlines-witnesses.test.ts @@ -0,0 +1,50 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; +import { shadowReadDeadlinesWitnesses, shadowReadDeadlinesWitnessRules } from "../formal/replay/witnesses/shadow-read-deadlines.mjs"; + +type PublicState = { o: Record; d: Record; io: Record }; +type Trace = { states: Array<{ input: { name: string; choice: unknown }; s: PublicState }> }; +const fixtures = JSON.parse(readFileSync(new URL("./fixtures/shadow-read-deadlines-witnesses.json", import.meta.url), "utf8")) as Array<{ regression: string; trace: Trace }>; +const integer = (value: number) => ({ "#bigint": String(value) }); +function traceFor(name: string) { + const rule = shadowReadDeadlinesWitnessRules.find(rule => rule.name === name)!; + return structuredClone(fixtures.find(fixture => fixture.regression === rule.regression)!.trace); +} +function credited(name: string, trace: Trace) { + return shadowReadDeadlinesWitnesses([{ path: "renamed.itf.json", states: trace.states }]).has(name); +} + +describe("separate shadow read deadline witness boundaries", () => { + for (const rule of shadowReadDeadlinesWitnessRules) { + it(`requires every public consequence for ${rule.name}`, () => { + const original = traceFor(rule.name); + expect(credited(rule.name, original)).toBe(true); + // Corrupt every required output independently, including empty lists: + // cancellations and budgets must be evidence, not incidental metadata. + for (const checkpoint of rule.checkpoints) { + for (const [channel, fields] of [["o", checkpoint.observation], ["d", checkpoint.diagnostics], ["io", checkpoint.io]] as const) { + for (const field of Object.keys(fields ?? {})) { + const changed = structuredClone(original); + delete changed.states[checkpoint.step]!.s[channel][field]; + expect(credited(rule.name, changed), `${channel}.${field} at ${checkpoint.step}`).toBe(false); + } + } + } + }); + } + it("rejects C1 taking a new runtime read budget", () => { + const name = "confirmation-keeps-captured-read-budget", trace = traceFor(name); + trace.states[6]!.s.io.budgets = [5, 20].map(integer); + expect(credited(name, trace)).toBe(false); + }); + it("rejects cancellation before C1 has spent its own budget", () => { + const name = "confirmation-read-budget-starts-at-dispatch", trace = traceFor(name); + trace.states[7]!.s.io.aborted = [integer(1)]; + expect(credited(name, trace)).toBe(false); + }); + it("rejects a second verdict after the job deadline", () => { + const name = "job-then-read-timeout-has-one-verdict", trace = traceFor(name); + trace.states[7]!.s.o.shadow = ["timeout", "dropped", "redis_error"]; + expect(credited(name, trace)).toBe(false); + }); +}); diff --git a/test/formal-validation.test.ts b/test/formal-validation.test.ts index d3d44a17..6624e4da 100644 --- a/test/formal-validation.test.ts +++ b/test/formal-validation.test.ts @@ -86,6 +86,34 @@ process.exit(Number(process.argv[3] ?? 0));\n`); }); }); + it("excludes only opt-in Go workers from complete replay and exploration", async () => { + const { loadGoReplayInventory } = await import(new URL("../formal/check-go-replay.mjs", import.meta.url).href) as { + loadGoReplayInventory(): { required: Array<{ name: string }> }; + }; + const { explorationPlan } = await import(new URL("../formal/explore.mjs", import.meta.url).href) as { + explorationPlan(directory: string, seed: string): Step[]; + }; + const goTest = (step: Step) => step.command === "go" && step.args?.includes("test"); + const replay = validationPlan("formal-go", { directory }).find(goTest)!; + expect(replay.args).toContain("-skip"); + const skipped = new RegExp(replay.args![replay.args!.indexOf("-skip") + 1]!); + for (const worker of ["TestGeneratedInvalidationVectors", "TestVectorBoundaryDriver"]) { + expect(skipped.test(worker), worker).toBe(true); + expect(skipped.test(`${worker}Required`), worker).toBe(false); + expect(skipped.test(`Other${worker}`), worker).toBe(false); + } + // Check the real inventory so adding a required corpus root cannot + // silently inherit a worker exclusion. + const requiredRoots = [...new Set(loadGoReplayInventory().required.map(entry => entry.name.split("/")[0]!))]; + expect(requiredRoots.filter(name => skipped.test(name))).toEqual([]); + expect(explorationPlan(directory, "0x1").find(goTest)!.args) + .toEqual(replay.args); + for (const target of ["check-go", "smoke"]) { + expect(validationPlan(target, { directory }).find(goTest)!.args, target) + .not.toContain("-skip"); + } + }); + it("stops at a failing child and preserves its partial native report without running later steps", async () => { const steps: Step[] = [ { label: "first", command: process.execPath, args: [child, "first"] }, @@ -129,6 +157,7 @@ process.exit(Number(process.argv[3] ?? 0));\n`); // evidence about Quint; generation is the only producer downstream reads. expect(validationPlan("formal-check", { directory })).toEqual([ { label: "Check every scheduled Quint model", command: process.execPath, args: ["formal/run-models.mjs", "check"] }, + { label: "Measure every pinned model fault", command: process.execPath, args: ["formal/check-model-properties.mjs"] }, { label: "Check the profile lint baseline", command: process.execPath, args: ["formal/lint-profiles.mjs", "baseline", "--check"] }, { label: "Check the kernel library fixtures", command: process.execPath, args: ["formal/check-kernel-fixtures.mjs"] }, ]); @@ -138,8 +167,16 @@ process.exit(Number(process.argv[3] ?? 0));\n`); for (const target of ["formal-ts", "formal-go", "mutations"]) { expect(validationPlan(target, { directory }).some(step => step.args?.[0] === "formal/run-models.mjs")).toBe(false); } - // ci keeps requiring the check through the aggregate; nothing else adds a second one. - expect(validationPlan("ci", { directory }).filter(step => step.args?.[0] === "formal/run-models.mjs" && step.args[1] === "check")).toHaveLength(1); + // Every acceptance entry point keeps one complete campaign, after all + // unmodified model checks. No filtered --only run can replace that gate. + for (const target of ["formal-check", "formal", "ci"]) { + const plan = validationPlan(target, { directory }); + const checks = plan.filter(step => step.args?.[0] === "formal/run-models.mjs" && step.args[1] === "check"); + const campaigns = plan.filter(step => step.args?.[0] === "formal/check-model-properties.mjs"); + expect(checks, target).toHaveLength(1); + expect(campaigns.map(step => step.args), target).toEqual([["formal/check-model-properties.mjs"]]); + expect(plan.indexOf(checks[0]!), target).toBeLessThan(plan.indexOf(campaigns[0]!)); + } }); @@ -302,6 +339,16 @@ process.exit(Number(process.argv[3] ?? 0));\n`); } }); + it("requires Docker for mutation measurements but not report merging", () => { + fakeTool("docker", 'console.error("Docker not running"); process.exit(1)'); + for (const target of ["mutations-ts", "mutations-go"]) { + expect(() => checkPrerequisites(target, { directory, environment, nodeVersion: "v24.20.0" })).toThrow(/docker/); + } + for (const target of ["mutations-merge-ts", "mutations-merge-go"]) { + expect(() => checkPrerequisites(target, { directory, environment, nodeVersion: "v24.20.0" })).not.toThrow(); + } + }); + it("allows the standalone floor target on Node 22.15 and propagates its PATH without reintroducing selectors", async () => { const floor = fakeTool("node22", `if (process.argv[2] === '--version') console.log('v22.15.0'); else { @@ -439,7 +486,7 @@ describe("full formal workflow shape", () => { const job = jobs.differential!; expect(job.if).toBe("github.event_name == 'pull_request'"); // Every composed profile replaying both ways after a kernel change overran one 60-minute job - // (57 minutes, then a cancellation at the timeout); four round-robin shards keep each inside it. + // (57 minutes, then a cancellation at the timeout); retain the four required shard statuses. expect(job["timeout-minutes"]).toBe(60); expect(job.strategy).toEqual({ "fail-fast": false, matrix: { shard: [1, 2, 3, 4] } }); const shards = job.strategy!.matrix!.shard as number[]; diff --git a/test/formal-vector-boundary.test.ts b/test/formal-vector-boundary.test.ts new file mode 100644 index 00000000..0e0c9778 --- /dev/null +++ b/test/formal-vector-boundary.test.ts @@ -0,0 +1,76 @@ +import { randomUUID } from "node:crypto"; +import { invalidationClient, recordInvalidation, type InvalidationInput } from "./invalidation-native-driver.js"; +import { readFileSync, writeFileSync } from "node:fs"; +import { it } from "vitest"; +import { DialCacheKey } from "../src/index.js"; +import { decodeTrackedRedisReadResult, isRedisReadMiss } from "../src/redis-protocol.js"; +import { DialCacheRedisPayloadEncodingError } from "../src/redis-client.js"; +import { compressPayload, decompressPayload } from "../src/internal/compression.js"; + +const requestPath = process.env.DIALCACHE_VECTOR_REQUEST; +type Request = + | { operation: "invalidation"; input: InvalidationInput } + | { operation: "key"; input: ConstructorParameters[0] } + | { operation: "trackedDecode"; input: { frameHex: string | null; watermarkUtf8: string | null } } + | { operation: "envelope"; input: { inputHex: string } } + | { operation: "compression"; input: { payloadType: "string" | "binary"; payloadUtf8?: string; payloadHex?: string; thresholdBytes: number; maxDecompressedBytes: number } }; + +// This worker receives only the operation and its external inputs. Expected +// values and the generated corpus remain in the parent coordinator. +it.runIf(requestPath !== undefined)("records one native vector result", async () => { + const out = process.env.DIALCACHE_VECTOR_OUT; + if (!out) throw new Error("DIALCACHE_VECTOR_OUT is required"); + const request = JSON.parse(readFileSync(requestPath!, "utf8")) as Request; + if (Object.keys(request).sort().join() !== "input,operation") throw new Error("Invalid vector request"); + let actual: unknown; + switch (request.operation) { + case "invalidation": { + const client = invalidationClient(); + client.on("error", () => {}); + const key = `{formal-boundary-${randomUUID()}}#watermark`; + try { await client.connect(); actual = await recordInvalidation(client, key, request.input); } + finally { if (client.isOpen) { await client.del(key); await client.quit(); } } + break; + } + case "key": { + try { + const key = new DialCacheKey(request.input); + actual = { kind: "key", logicalKey: key.urn, valueKey: `${key.urn}:dialcache-frame-v1`, + watermarkKey: key.trackForInvalidation ? `${key.prefix}#watermark` : null }; + } catch (error) { + if (!(error instanceof Error) || !error.message.startsWith("Redis Cluster hash tag components must not contain braces:")) throw error; + actual = { kind: "key_error" }; + } + break; + } + case "trackedDecode": { + try { + const { frameHex, watermarkUtf8 } = request.input; + const result = decodeTrackedRedisReadResult(frameHex === null ? null : Buffer.from(frameHex, "hex"), + watermarkUtf8 === null ? null : Buffer.from(watermarkUtf8)); + actual = isRedisReadMiss(result) ? result : { kind: "hit", createdAtMs: result.createdAtMs, + ...(typeof result.payload === "string" ? { payloadType: "string", payloadUtf8: result.payload } + : { payloadType: "binary", payloadHex: result.payload.toString("hex") }) }; + } catch (error) { + if (!(error instanceof DialCacheRedisPayloadEncodingError)) throw error; + actual = { kind: "payload_encoding_error" }; + } + break; + } + case "envelope": { + const result = decompressPayload(Buffer.from(request.input.inputHex, "hex")); + actual = { decodedHex: Buffer.from(result.payload).toString("hex"), outcome: result.outcome }; + break; + } + case "compression": { + const input = request.input; + const raw = input.payloadType === "binary" ? Buffer.from(input.payloadHex!, "hex") : input.payloadUtf8!; + const result = compressPayload(raw, { thresholdBytes: input.thresholdBytes, level: 3 }, input.maxDecompressedBytes); + actual = { outcome: result.outcome, storedBytes: result.storedBytes, + marker: result.outcome === "compressed" ? Buffer.from(result.payload)[0] : -1 }; + break; + } + default: throw new Error("Unknown vector operation"); + } + writeFileSync(out, JSON.stringify({ actual }) + "\n"); +}); diff --git a/test/formal-vector-evidence.test.ts b/test/formal-vector-evidence.test.ts new file mode 100644 index 00000000..32f04fe3 --- /dev/null +++ b/test/formal-vector-evidence.test.ts @@ -0,0 +1,149 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; +import { assessVectorBoundary, resolveVectorEvidence, validVectorResult } from "../formal/vector-evidence.mjs"; + +const read = (path: string) => readFileSync(new URL(`../${path}`, import.meta.url), "utf8"); +const manifest = JSON.parse(read("formal/execution.json")); +const challenge = manifest.challenges.find((entry: { id: string }) => entry.id === "envelope-strips-unknown-zero-prefix"); +const model = manifest.models.find((entry: { path: string }) => entry.path === challenge.model); +const written = challenge.nativeMutants.evidence.vector; +const vector = resolveVectorEvidence(written, model, read); +const evidence = { challenge: challenge.id, mutant: "M54", vector, history: `vector/${challenge.reproducer.run}`, step: 0, + fields: vector.fields, origin: "vector" }; +function recording(port: "typescript" | "go" = "typescript", changed = false) { + const sample = vector.samples[port]; + return { history: evidence.history, completed: true, lastStep: 0, divergences: [], vectorResult: { + port, history: evidence.history, row: sample.row, artifactSha256: vector.artifactSha256, inputSha256: sample.inputSha256, + actual: changed ? { decodedHex: "03", outcome: "passthrough" } : structuredClone(sample.expected), + } }; +} + +describe("exact generated vector evidence", () => { + it("selects one owned row per binding and sends no expectations to the worker", () => { + expect(vector.samples.typescript.request).toEqual({ operation: "envelope", input: { inputHex: "0003" } }); + expect(() => resolveVectorEvidence({ ...written, artifact: "formal/protocol-vectors.json" }, model, read)).toThrow(); + expect(() => resolveVectorEvidence({ ...written, rows: { ...written.rows, go: "missing" } }, model, read)).toThrow(/exactly one/); + expect(() => resolveVectorEvidence(written, model, path => { + const corpus = JSON.parse(read(path)); corpus.envelopeVectors.push(corpus.envelopeVectors.find((row: { name: string }) => row.name === written.rows.go)); + return JSON.stringify(corpus); + })).toThrow(/exactly one/); + expect(() => resolveVectorEvidence({ ...written, fields: ["loaders"] }, model, read)).toThrow(); + }); + it("recomputes a semantic mismatch from the native bytes", () => { + for (const port of ["typescript", "go"] as const) { + expect(assessVectorBoundary(evidence, recording(port)).state).toBe("not-divergent"); + expect(assessVectorBoundary(evidence, recording(port, true))).toMatchObject({ state: "confirmed", + divergences: [{ step: 0, action: "envelope", paths: ["decodedHex"] }] }); + } + const fakeDiff = { ...recording(), divergences: [{ step: 0, action: "envelope", paths: ["decodedHex"] }] }; + expect(assessVectorBoundary(evidence, fakeDiff).state).toBe("not-divergent"); + }); + it("rejects missing output, failed processes and mismatched row fingerprints", () => { + expect(assessVectorBoundary(evidence).state).toBe("unreached"); + expect(assessVectorBoundary(evidence, { ...recording("go", true), completed: false }).state).toBe("unreached"); + expect(assessVectorBoundary(evidence, { ...recording("go", true), error: "process exited 1" }).state).toBe("unreached"); + for (const key of ["port", "history", "row", "artifactSha256", "inputSha256"] as const) { + const wrong = recording(); wrong.vectorResult[key] = "wrong" as never; + expect(assessVectorBoundary(evidence, wrong).state).toBe("unreached"); + } + const wrong = recording(); wrong.vectorResult.actual = { decodedHex: "03" }; + expect(assessVectorBoundary(evidence, wrong).state).toBe("unreached"); + }); + it("requires a clean baseline with the same port and vector declaration", async () => { + const url = new URL("../formal/mutation-reports.mjs", import.meta.url).href; + const { boundaryReview } = await import(url) as { boundaryReview(report: unknown, evidence: unknown[], options: unknown): Array<{state: string}> }; + const report: { configurationSha256?: object; go?: string; boundaryBaselines: Record>; + mutations: Array<{id: string; boundary: unknown[]}> } = { configurationSha256: {}, boundaryBaselines: { [evidence.history]: recording() }, mutations: [ + { id: "M54", boundary: [assessVectorBoundary(evidence, recording("typescript", true))] }, + ] }; + const assess = () => boundaryReview(report, [evidence], { requireEntries: true })[0]!.state; + expect(assess()).toBe("confirmed"); + report.boundaryBaselines[evidence.history] = recording("go"); + expect(assess()).toBe("unreached"); + report.boundaryBaselines[evidence.history] = recording("typescript", true); + expect(assess()).toBe("unreached"); + // Copying both matching TypeScript recordings cannot satisfy a Go report. + report.boundaryBaselines[evidence.history] = recording(); + delete report.configurationSha256; + report.go = "go test toolchain"; + expect(assess()).toBe("unreached"); + }); + it("keeps known API variants separate from malformed output", () => { + expect(validVectorResult("key", { kind: "key_error" })).toBe(true); + expect(validVectorResult("key", { error: "crashed" })).toBe(false); + expect(validVectorResult("trackedDecode", { kind: "miss", reason: "watermark_fenced", observedWatermarkMs: 1 })).toBe(true); + expect(validVectorResult("trackedDecode", { kind: "hit" })).toBe(false); + expect(validVectorResult("compression", { outcome: "compressed", storedBytes: 18, marker: 1 })).toBe(true); + expect(validVectorResult("compression", { outcome: "compressed", storedBytes: NaN, marker: 1 })).toBe(false); + for (const outcome of ["", "test crashed", "unknown"]) { + expect(validVectorResult("envelope", { outcome, decodedHex: "03" })).toBe(false); + expect(validVectorResult("compression", { outcome, storedBytes: 18, marker: 1 })).toBe(false); + expect(validVectorResult("trackedDecode", { kind: "miss", reason: outcome })).toBe(false); + const malformed = recording(); malformed.vectorResult.actual = { decodedHex: "03", outcome }; + expect(assessVectorBoundary(evidence, malformed).state).toBe("unreached"); + } + }); + it("permits another model's vector reproducer only for a shared rule", async () => { + const url = new URL("../formal/execution.mjs", import.meta.url).href; + const { validateExecution } = await import(url) as { validateExecution(value: unknown): unknown }; + const changed = structuredClone(manifest); + const originalFrame = changed.challenges.find((entry: { id: string }) => entry.id === "frame-vectors-inclusive-fence"); + const frame = structuredClone(originalFrame); + frame.id = "shared-vector-schema-fixture"; + changed.challenges.push(frame); + const rule = changed.challenges.find((entry: { id: string }) => entry.id === "fence-inclusive-timestamp"); + const cited = frame.model; + // Structural validation does not establish reachability. The actual + // challenge runner must still measure the primary property and both + // before/through probes against the shared edit. + Object.assign(frame, { source: rule.source, before: rule.before, after: rule.after, + model: "formal/dialcache-redis-protocol.qnt", invariant: "trackedZeroTimestampIsNotHit", measures: "Fixture for shared vector citation validation." }); + Object.assign(frame.reproducer, { model: cited, profiles: [frame.model, cited], + exclusions: Object.fromEntries(changed.models.filter((item: { profile?: string }) => item.profile) + .map((item: { profile: string }) => [item.profile, "Schema fixture; runtime partition measurement is a separate mandatory check."])) }); + expect(() => validateExecution(changed)).not.toThrow(); + frame.reproducer.profiles.pop(); + expect(() => validateExecution(changed)).toThrow(/must name known profiles/); + frame.reproducer.profiles.push(cited); + frame.source = cited; + frame.before = originalFrame.before; + frame.after = originalFrame.after; + expect(() => validateExecution(changed)).toThrow(/shared-library fault/); + }); +}); + + +describe("real Redis vector evidence", () => { + const challenge = manifest.challenges.find((item: {id: string}) => item.id === "invalidation-transition-cutoff-moves-backwards"); + const model = manifest.models.find((item: {path: string}) => item.path === challenge.model); + const vector = resolveVectorEvidence(challenge.nativeMutants.evidence.vector, model, read); + const evidence = { vector, history: `vector/${challenge.reproducer.run}`, fields: ["content", "ttlMs"] }; + const sample = vector.samples.typescript; + const result = (actual: Record) => ({ completed: true, lastStep: 0, vectorResult: { + port: "typescript", history: evidence.history, row: sample.row, artifactSha256: vector.artifactSha256, + inputSha256: sample.inputSha256, actual, + } }); + it("keeps expected state out of the native Redis request", () => { + expect(sample.request).toEqual({ operation: "invalidation", input: { + existing: {kind: "string", value: "10000000", ttlMs: 100}, futureBufferMs: "0", invalidatedAtMs: "1000", + } }); + expect(assessVectorBoundary(evidence, result(sample.expected)).state).toBe("not-divergent"); + expect(assessVectorBoundary(evidence, result({...sample.expected, content: "1000"}))).toMatchObject({state: "confirmed", divergences: [{paths: ["content"]}]}); + }); + it("uses only recorded server elapsed time to bound positive TTL drift", () => { + const ttl = sample.expected.ttlMs as number; + const assess = (ttlMs: number, elapsedMs: number) => assessVectorBoundary(evidence, result({...sample.expected, ttlMs, elapsedMs})).state; + expect(assess(ttl - 3, 3)).toBe("not-divergent"); + expect(assess(ttl - 4, 3)).toBe("confirmed"); + expect(assess(ttl + 1, 3)).toBe("confirmed"); + expect(assess(ttl, -1)).toBe("unreached"); + expect(assess(-1, 100)).toBe("confirmed"); + }); + it("rejects unknown Lua replies, malformed state and missing time", () => { + for (const actual of [ {...sample.expected, outcome: "unexpected_reply"}, {...sample.expected, elapsedMs: undefined}, + {...sample.expected, content: ["not a string"]}, {...sample.expected, ttlMs: -3} ]) { + expect(validVectorResult("invalidation", actual)).toBe(false); + expect(assessVectorBoundary(evidence, result(actual)).state).toBe("unreached"); + } + }); +}); diff --git a/test/formal/behavior-driver.ts b/test/formal/behavior-driver.ts index c50a0ec4..1bd852d7 100644 --- a/test/formal/behavior-driver.ts +++ b/test/formal/behavior-driver.ts @@ -23,7 +23,7 @@ export type Recovery = "allow" | "deny" | "error"; export type EventName = "readContext" | "readAbort" | "request" | "miss" | "disabled" | "error" | "coalesced" | "invalidation" | "shadowAge" | "recoveryAge" | "futureOffset" | "size" | "storedSize" | "compression" | "get" | "fallback" | "serialization" - | "mismatchWarning" | "writeDispatch" | "marker"; + | "mismatchWarning" | "writeDispatch" | "marker" | "coalescingState"; export interface ObservedEvent { event: EventName; [field: string]: string | number | boolean | null } // JSON-shaped adapter observations deliberately include malformed replies. A // strongly typed port can reject these at its adapter boundary instead. @@ -60,6 +60,7 @@ export type Input = | { op: "seed"; useCase?: string; key?: string; value?: Value; ageMs?: number; frameHex?: string; payloadText?: string; payloadHex?: string; ttlMs?: number } | { op: "invalidate"; key?: string; futureBufferMs?: number } | { op: "observeMarker"; key?: string } + | { op: "inspectCoalescing"; instance?: string } | { op: "adapterReply"; value: AdapterReply } | { op: "policy"; value: Policy | null } | { op: "faults"; value: Partial } @@ -380,6 +381,12 @@ export class BehaviorDriver { else throw error; } break; + case "inspectCoalescing": { + const instance = input.instance ?? "default"; + const state = this.instance(instance).getCoalescingState().process; + this.record("coalescingState", { instance, ...state }); + break; + } case "observeMarker": { // Observe the controlled Redis environment, never DialCache's internal // state. The fixed origin makes the timestamp portable across runtimes. diff --git a/test/formal/coordinated-replay.ts b/test/formal/coordinated-replay.ts index b6e48f71..377d3b49 100644 --- a/test/formal/coordinated-replay.ts +++ b/test/formal/coordinated-replay.ts @@ -5,6 +5,7 @@ import { performance } from "node:perf_hooks"; import { vi } from "vitest"; import { ReplayCoordinator, settlement } from "../../formal/replay/coordinator.mjs"; +import type { Divergence } from "../../formal/replay/divergence.mjs"; import type { CoreCommand } from "../../formal/replay/core.mjs"; import { wallEpochMs } from "../../formal/replay/settlement.mjs"; import { parseJSON } from "../../formal/replay/validation.mjs"; @@ -46,7 +47,7 @@ type Observed = { complete: false; index: number; inputs: Array { + options: ReplayOptions = {}): Promise<{ steps: number; divergences: Divergence[] }> { let id = 0; const request = (fields: Record): T => coordinator.dispatch(parseJSON(JSON.stringify({ version: 1, id: ++id, ...fields }))) as T; @@ -65,10 +66,13 @@ export async function replayThroughCoordinator(profile: string, path: string, co })); if (result.complete) { complete = true; - return { steps: result.steps }; + return { steps: result.steps, divergences: coordinator.recording(prepared.session)?.divergences ?? [] }; } for (const command of result.inputs) await driver.apply(command); } + } catch (cause) { + coordinator.abort(prepared.session, cause); + throw cause; } finally { // The coordinator drops a session on its own failure; release it after a // driver failure so the coordinator can be reused for another trace. @@ -114,7 +118,7 @@ class CoordinatedBehaviorDriver implements CoordinatedDriver { } } -// Core: the flat-integer conformance driver from test/formal-conformance.test.ts, +// Core: the flat conformance driver from test/formal-conformance.test.ts, // consuming the coordinator's explicit advanceWall/bumpSource/invalidate/call // commands instead of action names. class CoordinatedCoreDriver implements CoordinatedDriver { @@ -178,9 +182,13 @@ class CoordinatedCoreDriver implements CoordinatedDriver { this.redis.failGet = false; } } - observe(): Record { + observe(): Record { return { - sourceVersion: this.sourceVersion, lastResult: this.lastResult, ...this.counters, + sourceVersion: this.sourceVersion, + // An absent actual result must survive the JSON roundtrip as data. + // Otherwise a wrong-value fault impersonates a missing driver field. + lastResult: this.lastResult === undefined ? { absent: true } : this.lastResult, + ...this.counters, redisReads: this.redis.getCalls + this.redis.mGetCalls, redisWrites: this.redis.setCalls, }; } diff --git a/test/formal/feature-profile.ts b/test/formal/feature-profile.ts index c42b92bc..56d66da5 100644 --- a/test/formal/feature-profile.ts +++ b/test/formal/feature-profile.ts @@ -15,7 +15,9 @@ export interface Profile { policyErrorIO?: boolean; diagnosticConfigErrors?: boolean; diagnosticFutureOffsets?: boolean; + diagnosticInspections?: boolean; diagnosticAge?: "shadowAge" | "recoveryAge" | "none"; + diagnosticUseCase?: string; fixture: Fixture | ((choice: number) => Fixture); initChoices?: readonly number[]; setup: Input[]; diff --git a/test/invalidation-native-driver.ts b/test/invalidation-native-driver.ts new file mode 100644 index 00000000..2e3656c7 --- /dev/null +++ b/test/invalidation-native-driver.ts @@ -0,0 +1,47 @@ +import { createClient } from "redis"; +import { INVALIDATE_CACHE_SCRIPT } from "../src/redis-protocol.js"; +import type { InvalidationVectorState } from "../formal/generate-invalidation-vectors.mjs"; + +export type InvalidationInput = { existing: InvalidationVectorState; futureBufferMs: string; invalidatedAtMs: string }; +export type InvalidationActual = { outcome: "success" | "rejected"; kind: "absent" | "string" | "list"; content: string | string[]; ttlMs: number; elapsedMs: number }; + +// Only setup and observation are test-owned. The transition is the production +// Lua, executed atomically between them. Redis TIME bounds actual expiry drift. +const script = `redis.replicate_commands() +local function now_ms() + local now = redis.call("TIME") + return tonumber(now[1]) * 1000 + math.floor(tonumber(now[2]) / 1000) +end +local started_at = now_ms() +redis.call("DEL", KEYS[1]) +if ARGV[3] == "string" then redis.call("SET", KEYS[1], ARGV[4]) end +if ARGV[3] == "list" then + for _, value in ipairs(cjson.decode(ARGV[4])) do redis.call("RPUSH", KEYS[1], value) end +end +if tonumber(ARGV[5]) > 0 then redis.call("PEXPIRE", KEYS[1], ARGV[5]) end +local result = (function() +${INVALIDATE_CACHE_SCRIPT} +end)() +local status = result == 1 and "success" or (type(result) == "table" and (result.err == "ERR invalid DialCache future buffer" or result.err == "ERR invalid DialCache invalidatedAtMs") and "rejected" or "unexpected_reply") +local kind = redis.call("TYPE", KEYS[1]).ok +local content = {} +if kind == "string" then content = redis.call("GET", KEYS[1]) end +if kind == "list" then content = redis.call("LRANGE", KEYS[1], 0, -1) end +if kind == "none" then kind = "absent" end +local ttl_ms = redis.call("PTTL", KEYS[1]) +return {status, kind, content, ttl_ms, now_ms() - started_at}`; + +export function invalidationClient(): ReturnType { + const url = process.env.DIALCACHE_VECTOR_REDIS_URL; + if (!url) throw new Error("INVALIDATION_INFRASTRUCTURE: Redis vector endpoint is required"); + return createClient({ url, socket: { reconnectStrategy: false, connectTimeout: 10_000 } }); +} +export async function recordInvalidation(client: ReturnType, key: string, input: InvalidationInput): Promise { + const initial = input.existing; + const raw = await client.eval(script, { keys: [key], arguments: [input.futureBufferMs, input.invalidatedAtMs, initial.kind, + initial.kind === "string" ? initial.value : initial.kind === "list" ? JSON.stringify(initial.values) : "", String(initial.ttlMs)] }); + if (!Array.isArray(raw) || raw.length !== 5) throw new Error("Invalid native invalidation reply"); + const [outcome, kind, content, ttlMs, elapsedMs] = raw; + // The coordinator validates the complete result schema before comparison. + return { outcome, kind, content, ttlMs, elapsedMs } as InvalidationActual; +}