diff --git a/documentation/plans/semantic-fuzzing-cleanup.md b/documentation/plans/semantic-fuzzing-cleanup.md new file mode 100644 index 000000000..af39ec9ca --- /dev/null +++ b/documentation/plans/semantic-fuzzing-cleanup.md @@ -0,0 +1,388 @@ +# Semantic fuzzer cleanup: evidence before more search + +Implementation record and plan, 2026-09-14. This supersedes the implementation +queue in `semantic-fuzzing-reduction-audit.md`; that document remains a historical +experiment log. The user has authorized planning and reconsidering the design, +including reduction rules and semantic assertions. This document does not change +runtime behavior or executable contracts. + +**Objective.** Produce clear, small, replayable examples with bounded triage and +execution cost. A useful improvement removes distracting behavior, reaches a +known simpler witness, catches a real contract violation, or saves work without +losing those abilities. More accepted rewrites, a shorter JSON string, and fewer +reports by themselves are not evidence of success. + +**Implementation outcome (2026-09-14).** The initial cleanup is implemented and +validated. See the repository-owned [evidence audit](../../packages/signals/tests/semantics/EVIDENCE.md) +and [usage guide](../../packages/signals/tests/semantics/README.md). + +- Shared ordinary/paired traversal, actual execution accounting, persistent pass + sweeps and bounded expensive cleanup are active. Original/final paired runs are + separate costs; final verification remains mandatory. +- The 97-input fixture retains 1,720 memberships. All eight known simplifications + now replay at 6,765 search calls versus the previous 6,701. The resulting 59 + exact repro keys are not presented as 59 unique issues. +- Ablations justify the retained special cuts. Not every cheap overlapping helper + has independently proven marginal benefit; the evidence inventory says so. +- The proposed universal monotonic rank was **not adopted**: it lost canonical + convergence. Existing ordering protections remain. Substantive size descent + applies to the delivery escape transaction, with shared bounds. +- CDD and native fast-check shrinking were measured and **not adopted** in the + default pipeline. The optional experiments remain reproducible commands. +- A bounded report queue considers later simpler examples; it prioritizes distinct + symptoms before using spare slots for a second shape. It retains originals and + records exact selected-input mappings. No cross-case result cache was added. +- No semantic contract was weakened or runtime behavior changed. The audit labels + proposed optimistic laws and incomplete independent mutation coverage explicitly. +- Validation: typecheck; 327 tests including nine fault calibrations; 900 fixed + holdout cases with no harness errors/limits and all selected witnesses replayed. + +The following sections retain the rationale and conditional gates. They are not +an instruction to implement rejected experiments or broaden semantic scope. Any +future rule should earn its place through a useful witness or a measured cost +reduction. A claim of broad convergence needs fresh examples, not more tuning of +these 97 inputs. + +**A. Establish an executable quality baseline first.** + +Use three complementary sets, with exact runtime and oracle identities: + +- A compact quality suite built from manually verified reduction routes. Include + the earlier source/arithmetic, callback, delivery/order and visibility wins, plus + C85 -> C67, C16 -> C17, C34 -> C80, C88 -> C10 and C96 -> C74. Keep the original + input, a known simpler output, its semantic disposition, and a replay command. + A different, equally useful valid failing output is acceptable in discovery + mode; exact spelling is only required by canonicalization-specific tests. +- The saved 97 audited inputs and their current 64 distinct outputs, retaining all + 1,720 original memberships. Compare both reduction from the larger inputs and + further reduction of the already-small outputs. Separate the 51 standalone + semantic representatives, one paired comparison and 12 policy representatives. +- A fixed, separate set of generated seeds spanning ordinary graphs, branches, + observation, actions/optimism, readiness and attachment, plus the existing fault + calibrations and positive controls. Do not use only the branch cohort or tune + transformations against every evaluation seed. Include some larger supported + scenarios to expose repeated-prefix costs hidden by tiny final examples. + +Make a small repository-owned comparison command and fixture manifest, rather +than relying on one-off scripts under a personal evidence directory. Store compact +scenarios and provenance in the repository; bulky traces/results remain optional +artifacts. Historical integration tests must run against a reproducibly built +historical source target or controlled fault fixture. Never require a repaired +current Solid to preserve an old bug, and never replace an unavailable historical +execution with a fabricated claim of runtime verification. + +Record candidate generation, static-invalid rejection, runtime-invalid rejection, +cache hits, actual worker executions, comparison/control executions, accepted +edits, material complexity changes, budget exhaustion and independent final +replay. Keep timing collection outside the hot interpreter and optional in normal +campaigns. Compare equal worker-execution budgets; record candidate budgets +separately, since one paired candidate may run two or three workers' executions. + +Quality has two axes: the substantive contents of the final repro and the work +needed to obtain it. Track memo/source/reader counts, async mechanisms, action +segments, event/callback/flush operations, branch structure and reads. Keep exact +repro-key convergence as a useful secondary measurement, not a unique-bug count. + +Ablation must establish marginal usefulness: disable a family and check whether +another family reaches the same result anyway. Add-back checks are useful when +families overlap. Compare final outputs, not merely the number of accepted edits. +Use repeated sequential runs and medians for performance decisions; the previous +partially overlapping exploration timings are not a throughput benchmark. + +**B. Make one small reduction engine serve ordinary and paired cases.** + +Today `shrink.ts` and `equivalence.ts` duplicate restart loops, seen sets, ordering +limits and final cleanup. Consolidate traversal, budget charging and scheduling. +Keep ordinary and paired execution/acceptance as explicit adapters, not a general +plugin framework. Preserve the public ordinary/focused/control behavior and the +paired-to-standalone handoff, including clearing comparison metadata only when a +valid individual execution is actually the new witness. + +Move admission logic to one place: an admitted semantic failure can replace +another in discovery mode; an invalid case, unsupported scope, waived finding, +policy-only outcome, harness error or resource limit cannot. Focused reduction +and policy/error reduction retain their existing narrower predicates. Never +interpret a matching diagnostic alone as proof of the same bug. + +Put execution accounting at the shared run boundary. Charge actual runs for +variants, passing controls and accepted-result verification. Keep a candidate +limit too, so cheap invalid candidates cannot consume unbounded host CPU. Preserve +the last independently valid result when a budget runs out. Reserve/document a +final replay allowance and report its cost; do not silently exceed the search +budget or skip verification. Initial replay, search and final verification should +be separately visible in results. Clarify any CLI budget-unit change explicitly. + +Start with per-search canonical candidate caching. Use compact immutable outcome +records; do not share mutable runner traces between candidates. Measure overlap +across inputs before adding a bounded campaign cache. Any such cache must include +runtime/build identity, oracle revision and run options, allowances, comparison +mode and exact scenario/variant keys. Controls have their own execution keys. +Final replay bypasses cache. Do not cache worker errors or limits as stable facts. + +Keep native tasks, microtasks, synchronous event blocks and worker containment +unchanged. This refactor must not insert an await between setters or change the +existing failure-driven worker retirement policy. + +Acceptance gate: existing replay, isolation, focused/control, pair and budget tests +pass; calibration detection remains intact; the shared engine reaches baseline +quality before replacing either old loop. + +**C. Fix pass scheduling before broadening the search.** + +Adapt Hypothesis's pass discipline: continue productive work within a pass rather +than restarting all earlier passes after every accepted edit. Revisit earlier +passes after a sweep makes progress. A deletion cursor stays at the position of +the next surviving item. Graph rewrites must rebuild affected candidate targets +or use stable IDs; never keep iterating an obsolete graph snapshot. Add explicit +controls for deletion-shift skips and cross-pass changes that make an earlier +candidate useful again. + +Run cheap structural deletion and normalization first. Enable expensive rewrites +after that reaches a fixed point; keep their budget small and visible. A successful +expensive rewrite gets ordinary cleanup without launching independent full +shrinks from every neighboring program. Do not introduce beam search, a generic +passing-state frontier, random restarts or a solver. + +Use a single documented progress order rather than incidental JSON property order +plus family-specific exceptions. An initial candidate is a lexicographic ordering +of declaration count, async/action/callback mechanisms, executable operation count, +expression/reference complexity, and a stable normalized encoding. Validate this +ordering on the quality suite: it must prefer the useful cuts already observed, +including removal of an async boundary despite an additional source read. Adjust +that proposal if it contradicts those examples; JSON byte count is a tie-breaker, +not the objective. + +Ordinary accepted rewrites must descend in this order. Equal-size normalization +uses an explicit stable ordering of compatible operations/references. A bounded +escape rewrite is judged together with its cleanup: only retain it if the final +program descends relative to the entry point. Preserve the prior best throughout. +This should make `OrderingLimit` and recursive finishing-pass workarounds +unnecessary, but remove them only after termination and quality tests demonstrate +that their protections have been replaced. + +For chunk deletion, compare the current halving approach against a small +Counter-Based Delta Debugging implementation over the same valid deletion units. +Use it only if execution counts improve without unacceptable quality loss. It +will not solve graph equivalence or magically merge different minima. + +Acceptance gate: no loss of the verified small witnesses; reduced repeated-prefix +work in synthetic quality controls and real inputs; no cycling; measured costs +within the same execution budgets. If scheduling changes save no meaningful work, +retain the simpler design rather than claiming an architectural win. + +**D. Replace overlapping special cases with a small normalization vocabulary.** + +Use the API-sequence normalization work in *One Test to Rule Them All* as the +starting point. Our graph and event language needs an adaptation, not a wholesale +copy of a source-text reducer. + +There are two different operations to keep explicit: + +- Representation normalization: stable names, field/default representation and + request-owner renaming that do not change the represented execution. Preserve + declaration/read order and distinguished source roles. Do not normalize away + an observer, action boundary or scheduling operation. +- Replay-tested normalization: consistent value/reference substitutions, merging + compatible ordinary sources, replacing a memo with an upstream expression, + collapsing an owner/reader with its dependent operations, and ordering compatible + declarations or event steps. These change the program and need the oracle. + +Expose a few cohesive candidate passes over graph, observation/lifetime and event +structure. Share the low-level reference and operation rewriting helpers instead +of duplicating them. This is not a new graph-rewrite DSL. Keep flat numeric scenario +records and copy only the edited arrays/records in normal transformations. + +Generalize useful edits where evidence warrants it: + +- Apply a compatible substitution consistently across its references/uses, not + only one scalar field. Keep zero/equality relationships meaningful when the + edit claims to be a value renaming. Other arithmetic edits remain new programs. +- Delete whole structural units with their obsolete bookkeeping operations before + validation, rather than making the runtime reject predictable dangling handles. + Removing a shared memo does not implicitly authorize removing its other readers. +- Try legal action progress/visibility permutations within a callback; the current + swap filter excludes `resume-action` entirely. Start/resume prerequisites must + remain valid. No cross-task/flush change is treated as harmless sorting. +- Allow a concrete memo/async-boundary cut to be paired with a bounded related + read, arithmetic or delivery change. The completed candidate is replayed; its + intermediate edits need not fail. Restrict this to replay-proven kinds of cuts + and relevant source paths, not all pairs/triples of transformations. +- Treat delivery modes as alternative schedules, not universally ordered levels + of simplicity. A controlled delivery may permit removal of action/scheduling + scaffolding. Offer this only as a bounded escape with a real final reduction. + +Keep strict request replay. Deleting an async owner can remove its own resolutions; +retained owners keep their exact captured question/occurrence unless an explicitly +validated translation applies. Generic newest/oldest retargeting produced no useful +cuts in the latest experiment and is not part of this cleanup. + +For each current family, create a concise inventory row with purpose, cost class, +positive quality fixture, negative validity/control fixture, corpus contribution, +and disposition: retain, subsume, experimental-only, or delete. Cheap foundational +rewrites need not each eliminate a duplicate to justify their existence. Expensive +special cases need an independently reproduced material win that cheaper rules +cannot obtain, and an acceptable incremental cost. A rule's helpful name or a +unit test proving it emits a candidate is insufficient. + +Initial evidence-based disposition: + +| Current area | Starting decision | +| --- | --- | +| Core deletions, pruning, owner/reference repair | Retain; consolidate duplicate implementations. | +| Alpha-normalization and exact request translation | Retain correctness protections; measure simplification separately. | +| Callback-end and initial visibility rewrites | Fold into event/lifecycle normalization if their quality fixtures survive. | +| Source merge plus offset | Fold into consistent graph/value rewrites; retain the known C32 -> C17 route. | +| Delivery/declaration paired search | Expensive tier; preserve C76/C21 quality, remove the bespoke search loop if the common engine can achieve it. | +| Observation and conditional collapse | Consolidate structural edits; retain only evidence-backed extra combinations. | +| Unrestricted ordering/full-shrink pivots | Do not adopt broadly: one exact merge in 1,968 replay calls. Fix the missing action-order case directly. | +| Request retargeting | Do not adopt: 631 calls and no useful cuts in eight eligible examples. | +| Broad setting pivots with a full shrink each | Do not adopt as implemented: 31,641 calls for two exact merges. | +| Three alternate full pass orders | Do not adopt: 19,999 calls over 83 earlier semantic inputs, no better final output under the recorded comparison. | +| Removal-first compound candidates | Evaluate in the bounded expensive tier: 1,557 calls found two exact merges; expanding to async-boundary removal found a third at higher cost. | + +These are initial decisions, not completed ablations. The existing corpus is small +and heavily reused; holdout evidence is required before calling a family generally +effective. Keep proven witnesses when deleting their old implementations. + +**E. Evaluate generator-integrated shrinking as a bounded alternative.** + +We currently call `fc.sample` and then reduce the resulting scenario independently. +Before developing a new choice-sequence engine, build a small fast-check baseline +that retains generation/shrink context and runs the existing interpreter/oracle. +Use fresh ordinary and branch cohorts first. Compare native shrinking alone and +native shrinking followed by our cheap normalizer at equal execution budgets. + +The generator must produce supported graphs and valid references as choices +shrink. Merely wrapping existing JSON in an arbitrary does not achieve this. +Preserve both the generated input/context and the runtime's exact canonical replay; +the latter must not mutate the former. Measure rejection/misalignment, final +scenario quality and reproduction reliability, not only shrink iteration count. + +Do not route individual synchronous setters through `asyncModelRun` or a scheduler +wrapper that awaits each operation. Our event blocks must remain uninterrupted; +Solid's native scheduling is part of the test. The official command shrinker is a +useful model for execution-aware shrinking, not a replacement event loop. + +Decision gate: adopt integration only for scopes where the experiment gives a +clear benefit with a small implementation. It need not replace external reduction +of archived/manual JSON or paired cases. If the adaptation is large or results are +worse, keep fast-check for generation and document the measured reason. Do not +migrate all cohorts or add another runtime/language before this gate. + +**F. Audit semantic assertions separately from reduction heuristics.** + +For each executable law, record its behavioral statement, applicability predicate, +observable witness, contract basis (user ruling, specification/test, or explicit +hypothesis), positive control, deliberately broken control, and known issue/fix +replays where available. Reuse the existing rule catalog and tests; add only missing +information, not a second abstract rule language. + +Classify evidence precisely: + +- A seeded fault establishes that the checker can detect that violation within + its scope, not that a generated upstream failure is necessarily correct. +- A repaired runtime passing the original repro and a clean control strengthens + the diagnosis, but upstream behavior does not define the ideal contract. +- A law that currently finds no bugs is not useless if it protects a meaningful + guarantee and has calibrated detection. +- Safety, final completion, progress, policy review and inapplicability stay distinct. + A sufficient progress condition must not be reported as a complete liveness model. + +Check that scope changes under shrinking cannot manufacture a violation, silently +apply a waiver, or treat missing witnesses as published state. Preserve source +observers as actual modeled observers, scoped fallback permission, native event +batching, and separation of parent action work from generated optimistic work. +Revisit any inconsistent law or unnecessary restriction with a small failing and +passing example. Contract changes require an explicit explanation/ruling and a +rule-revision bump; never weaken a law to improve the deduplication numbers. + +Audit the interpreter/model boundary without rewriting the 1,500-line runner just +for aesthetics. Extract code only where there is a clear responsibility with its +own tests or harmful duplication. No new DOM dependency, stores, stateful `prev`, +streaming or generalized timer model in this cleanup. + +**G. Make campaign output support the one-issue-at-a-time workflow.** + +Today the CLI retains every finding but only shrinks the first example of each +symptom, up to 20 signatures. Keep retention; remove the assumption that this first +example is the best witness. Different bugs can share a symptom, and a later +example may be dramatically easier to understand. + +Use a small, deterministic, budgeted queue of reduction candidates. Prefer cheaper +raw scenarios and permit limited structural diversity within a symptom. Treat that +selection as a scheduling heuristic, never as proof of shared root cause. A bounded +comparison should establish whether this produces better first reports than the +current first-arrival rule; do not simply shrink every generated failure. + +Save exact reduced repro keys and map original memberships to them. Show the user +one or a few clear candidates, while keeping the rest replayable. State whether an +example is a semantic candidate, a policy question, paired-only, or budget-limited. +Do not label structural clusters as unique issues or discard originals because a +reduction reached a different bug. After a fix, replay originals and run fresh +cases; unsupported or still-unreduced findings remain in the queue. + +Keep generation, reduction and reporting time separate in campaign summaries. +Retain the smallest observed valid witness even if a later search uses up its +budget. No automatic TypeScript reproduction generation in this plan. + +**H. Implementation sequence and completion criteria.** + +1. Add the quality manifest/comparison command and audit the current families and + contract coverage. Capture the current branch/build and frozen historical target + separately. Start missing positive/negative controls before moving code. +2. Consolidate search execution, accounting and adapters without changing search + policy. Verify ordinary, paired, control and isolation behavior. +3. Implement persistent pass sweeps, common progress ordering and cheap/expensive + tiers. Run the quality suite and equal-budget corpus comparison. +4. Consolidate normalization and remove redundant/unsupported expensive rules one + family at a time. Add the small replay-proven generalizations. Run ablations; + keep only measured improvements or essential low-cost correctness machinery. +5. Run the fast-check integration and CDD experiments as explicit decision gates, + not mandatory migrations. Record useful negative results too. +6. Improve bounded candidate selection/reporting and complete the contract audit. + Document unresolved rulings with minimal examples, not invented semantics. +7. Run the full semantic tests and type check, all calibrations, exact replay of + retained representatives and the fixed holdout campaigns. Check current upstream + separately if desired; never mix a runtime rebase into a reducer comparison. + +Success means a smaller, more cohesive implementation with measured reduction +quality/cost, clear law evidence and honest artifacts. Require no unexplained loss +of manually verified simplifications or calibration coverage, no artifact/replay +regressions, bounded search, and no material generation-only slowdown. Evaluate +performance changes against measurement noise; do not promise an arbitrary speedup +or that 64 programs must become five bugs. Expensive additions that do not earn +their cost stay outside the default pipeline. + +Update the README into a concise usage/architecture guide and link the evidence +catalog rather than appending each historical patch. Keep one current plan; older +reduction and extension documents should point here and remain historical references. +Do not add pooling, a generic rewrite framework, a beam-search frontier, automatic +GitHub reporting or a new dependency solely to organize this cleanup. + +**Research basis.** + +- [Groce et al., One Test to Rule Them All, ISSTA 2017](https://agroce.github.io/issta17.pdf): + API-sequence normalization/generalization, replay-tested consistent substitutions + and operation ordering; normalization can improve already delta-minimized tests. + Its empirical convergence is motivation, not a promised result for our DAGs. +- [Hypothesis internals](https://github.com/HypothesisWorks/hypothesis/blob/master/guides/internals.rst): + continue within passes, postpone expensive passes, cache decisions and use + execution-guided repair. Greedy reduction itself is not the architectural defect. +- [MacIver and Donaldson, ECOOP 2020](https://drops.dagstuhl.de/entities/document/10.4230/LIPIcs.ECOOP.2020.13): + shrinking generator choices helps retain validity; adopting this requires keeping + generation context and a shrink-friendly generator. +- [Perses, ICSE 2018](https://web.cs.ucdavis.edu/~su/publications/perses.pdf): + use structure to avoid invalid candidates; syntax alone cannot enforce our + request, observation and action semantics. +- [Counter-Based Delta Debugging](https://arxiv.org/html/2408.04735v4): + simple accounting can avoid inefficient repeated queries; evaluate over our + deletion units rather than assuming published benchmark gains transfer. +- [fast-check arbitraries](https://fast-check.dev/docs/core-blocks/arbitraries/) and + [model-based command shrinking](https://fast-check.dev/docs/advanced/model-based-testing/): + an available baseline; command execution tracking does not justify changing our + event-loop scheduling. + +Local experiment evidence: +`/home/gabriel/code/solid-effect-publication-evidence/semantic-fuzzing/search-barriers-2026-09-14/README.md` +and neighboring `paired-reduction-2026-09-14`, `coordinated-reduction-2026-09-14`, +`discovery-reduction-2026-09-14`, and `all-97-audit-2026-09-14` artifacts. diff --git a/documentation/plans/semantic-fuzzing-next.md b/documentation/plans/semantic-fuzzing-next.md new file mode 100644 index 000000000..d3aa71ffe --- /dev/null +++ b/documentation/plans/semantic-fuzzing-next.md @@ -0,0 +1,244 @@ +# Semantic fuzzer scope and extensions + +The [architecture](semantic-fuzzing.md) and [README](../../packages/signals/tests/semantics/README.md) +describe the implementation. The runner always uses the checked-out source; +corpus discovery revisions are historical provenance, not runtime pins. + +## Implemented scope + +- Bounded pure scalar DAGs with synchronous, promise, manual and await delivery. +- Native event blocks, microtasks, promise continuations and controlled host tasks. +- Multiple sources, optional publication anchors and branch-selecting memos. +- Changing observation, disposal, owned mounts and flat/nested Loading regions. +- Exact request identities, current obligations and scoped fallback history. +- One action lifetime with optimistic proposals or a shared latest channel. +- Tracked readiness, guarded event reads and synchronous verdict derivations. +- Scoped waiting policies, final consistency, completion and residual-work laws. +- After-drain ordinary progress checks and explicit evidence-scoped exceptions, + retained through reporting, shrinking and replay. +- Paired delivery, latest warming and ordinary/optimistic comparisons. +- Canonical replay, bounded reduction, fault calibration and worker containment. +- Independent host attachment for fixed output regions, nested render-effect + replacement, scheduled portal destinations and visible-cleanup continuity. + +The interpreter rejects unsupported combinations. The README gives the detailed +preconditions of each cohort and equivalence relation. + +## Extend only with an independent oracle + +Prioritize a small contract slice with a clear positive control and a deliberately +broken control. State the invariant without relying on Solid's current ownership +or pending implementation, then add only the language features needed to reach +it. Check replay and reduction before expanding campaign size. + +Useful follow-ups include independent-reveal controls, multiple action lifetimes, +async derivations of readiness and latest-input readiness. These require explicit +publication/lifetime contracts before generation. Custom equality, stateful `prev`, +user errors, stores, streaming and generalized timers are separate extensions. +Automatic TypeScript reproduction generation remains deferred; translate reduced +artifacts manually for reporting. + +The current [cleanup plan](semantic-fuzzing-cleanup.md) covers reduction quality, +execution cost, contract evidence and reporting. The earlier +[reduction audit](semantic-fuzzing-reduction-audit.md) retains its measurements. +The working loop prioritizes one +actionable issue at a time. Discovery shrinking may expose a different semantic +bug as long as the resulting scenario, oracle scope and request selections are +valid. Preserve originals for replay after a fix, then fuzz again to find what +remains. Same-failure preservation is a focused option, not the default discovery +goal. This supersedes stricter failure-identity guidance below for discovery +reduction; it does not relax the semantic rules themselves. + +Performance work should begin with measurements of complete campaigns and their +actual hot paths. Preserve predictable scalar representations and independently +owned evidence. Avoid speculative pooling or complex caching. + +## Next implementation: completion of individual update groups + +Status: ordinary group checkpoint implemented. Group history, identified +authoritative scripts, G1/G2, targeted generation, replay and runtime-fault +calibration are in place. Rule revision 16 requires batching within a synchronous +callback, but permits either independent or joint completion across adjacent +microtasks in one host-task drain. The old async-landing adoption question no +longer blocks this scope. Temporal permissions are evidence-bearing, affect +only progress deadlines and are retained separately from bug findings. + +Changing-observer and optimistic **group** integration remain pending. Existing +observer/optimistic rules still run; the stronger per-group deadline is simply +out of scope. This is a usable checkpoint, not completion of every extension below. + +Extend P1 beyond the current sufficient test +that no visible reader has unresolved demands. The objective is to assert that +one update must publish while another update or action still has legitimate work. + +### A. Establish grouping controls before expanding generation + +Write small, explicit controls for: + +- Two ordinary source writes in one synchronous batch versus separate batches. +- Explicit flush between writes, and microtask writes queued before/after the + scheduled flush. A host task is a delivery mechanism, not an assumed update ID. +- Independent async derivations read through separate effects or one shared + effect, resolving in both orders. +- Overlapping writes to the same source and through a shared derivation. +- An action's writes before/after yields, two actions starting together or in + separate batches, and ordinary writes alongside an action. + +Read existing decision/test evidence but write down the intended contract +separately. Runtime behavior is not the oracle. Ambiguous controls get a reduced +example and a user ruling before becoming assertions. Start generated strict +grouping with synchronous batches and explicit flush/drain separators; incorporate +queued microtask patterns using the revision-16 temporal permission. + +### B. Add a small independent update history + +Use integer write/group/action IDs, existing source masks, and small group records. +Record intended write values, batching membership, action holds and successive +writes to each source. Preserve exact request occurrences in the existing ledger. +Record why groups are related, not Solid's internal transition identity. + +Keep two kinds of relationship distinct: + +- Proven grouping supports atomicity and no-early-publication checks. +- Possible grouping conservatively extends completion deadlines only. It cannot + require joint publication or make early independent publication a failure. + +Static memo source masks provide conservative potential joins between overlapping +unfinished updates. Shared effects do not join groups. Never infer actual temporal +entanglement solely because two sources occur somewhere in one memo's ancestry. +Retire write generations after the applicable publication obligation is met; +old shared derivations must not permanently couple later updates. Uncertain +retirement/ownership prevents the affected proof, not every rule in the scenario. + +No runtime pending sets, transaction IDs or onSettled callbacks may establish that +an update is ready. Those remain diagnostic observations or checked outputs. + +### C. Check ordinary groups independently + +Begin with initialized flat pure DAGs, complete anchors and continuous data +readers. Use both separate and shared effects. Compute each group's candidate +publication from its intended writes and the published values of other groups; +do not require an unrelated group's latest requested data to be ready. + +Derive blocking demands from the pure graph and exact physical request ledger. +After a host drain, a group whose conservative possible blockers are all finished +must publish. An independently open action or request elsewhere must not prevent +this assertion. Preserve existing safety checks at every publication. + +Then apply the existing witnessed hiding/disposal/fallback rules to these groups. +Fallback can release source publication while retaining obligations for its own +content. Branches, readiness, unsettled mounts and nested boundaries initially +retain existing checks without the stronger per-group deadline. + +### D. Represent authoritative actions explicitly + +The current action grammar is specialized to one optimistic script. Add bounded +identified action scripts with ordinary writes and controlled yield gates, +initially at most two concurrent actions. Preserve old replay formats through a +normalization step or optional backwards-compatible fields. + +An action continuation retains its action identity across host tasks. Distinct +actions are not automatically distinct update groups: same-batch starts or later +entanglement can join their authoritative work. All action holds in such a group +must end before it is ready. Finishing the action body is necessary, not sufficient; +observed downstream work may remain. A later unrelated callback does not inherit +an action merely because the action is still open. Nested actions are deferred. + +### E. Connect optimistic lanes without inheriting parent holds + +Represent the optimistic publication group separately from its parent lifetime. +Its own observed work determines when it can publish; an open parent action does +not hold it. Parent completion triggers correction without waiting for unfinished +optimistic work. Observed correction remains an obligation under the proposed +parent-completion contract, with its authority still marked experimental. + +Reuse the current one-parent optimistic/latest grammar first. Add only combinations +whose correction and shared-derivation obligations are expressible independently; +do not infer parallel graph evaluation. If the single-graph interaction creates +an ambiguous obligation, pause on a concrete example rather than inventing a rule. + +### F. Calibrate, preserve evidence and measure coverage + +For each new law, add passing controls and a disposable runtime mutation that +violates it: premature action completion, lost ready-group wakeup or accidental +coupling through a shared effect. Require a semantic finding rather than a crash. +Verify same-batch/shared-derivation controls prevent false independence assertions. + +Generate schedules that leave unrelated gates unresolved at the checkpoint, vary +graph shape and completion order, and replay minimized failures. Shrinking must +preserve the violated relation and any exception identity; removing a batching +boundary cannot silently turn an independence finding into a different issue. + +Extend the existing ideal-rule/explicit-allowance mechanism. Preserve waived +evidence and scope in artifacts. Report checks made and skipped, including the +reason (possible entanglement, unsettled observation, unsupported lifetime), so +conservative modeling is visible coverage loss rather than a claimed proof. + +Reuse precomputed masks and request indices, bound live groups/actions, and prefer +short loops over graph cloning or per-read bookkeeping. Use simple group merges +over the small active set; no speculative pooling or generic solver. Run targeted +self-tests, fault calibration, and bounded campaigns with measured throughput. + +Completion criterion: the fuzzer distinguishes batching, authoritative action +holds, derivation entanglement and shared effects, and detects late publication +of an independently ready group while other finite work remains deliberately +unresolved. Existing whole-view completion and consistency laws remain active. + +## Observable attachment and cleanup: first slice implemented + +Model a small output tree independently of Solid's owner tree. Default render +effects may prepare detached nodes early; scheduled root/portal effects attach +them. Numeric node/slot handles represent creation, writes, attachment and +cleanup removal. Only actual reachability from visible roots determines output; +never hide a mutation merely because its reactive owner is uncommitted. + +Check consistency after drains and at external observations, allowing intermediate +host mutations inside an uninterrupted flush. Generate valid construction +patterns: fresh detached preparation, scheduled external attachment, nested effect +replacement, retained/fallback content, and independent portal destinations. +First calibrate a safe detached preparation, premature attachment and #3404's +premature visible cleanup (including schedule: true). Existing tree.ts only models +logical nesting and cannot substitute for this independent attachment model. +The `attachment` cohort now exercises fixed single-source output regions with a +parent compute and nested effect. Local writes prepare fresh detached containers; +portal writes are scheduled against a separate visible root. Host nodes use +numeric handles and linked child lists; creation allocates one record, reads walk +only the output subtree without cloning or following Solid's dependency graph. +There is a 4096-node per-case limit, alongside existing worker/work/frame limits. + +A1 requires an initialized region to retain exactly one contribution until its +replacement or explicit removal. Existing S1 checks the attached values against +published inputs. Root reachability is independent of reactive ownership, and +checks run at completed flushes/drains, allowing private preparation and +remove-then-attach within a flush. Unit controls inject premature attachment, +visible cleanup and duplicate contributions; detached cleanup and preparation +are positive controls. The #3404-shaped integration control can pass after a +runtime fix; the test does not require Solid to remain broken. + +Revision 17 keeps per-group progress and paired equivalence out of this new +scope. Generated changing visibility, Loading/fallback replacement, multi-source +regions and detached portal targets are still follow-ups. The host model already +tests a detached portal destination becoming reachable, but that shape is not yet +part of random generation. Existing logical tree.ts remains separate. + +Verification against `next` 14ded248 and #3405 323f9ad: seed 3404, 1000 attachment +cases found 507 visible-output gaps on next; all 1000 passed on the PR. Replaying +all 507 saved canonical failures preserved every schedule and passed on the PR. +No runtime source changes are included. The failed effect-reentry experiment was +discarded before starting this work. + +## Ordinary-group checkpoint verification (2026-09-14) + +- Full signals suite: 1,940 passed, one skipped; fuzzer typecheck passed. +- Runtime fault calibration remains covered by the passing suite, including + unwanted effect entanglement and removed action holds. +- 1,000 update-group cases, seed 3289: 985 pass, 15 G2 candidates in one symptom + category; no worker errors or limits. The candidates concern independent groups + across separate host turns and are not validated as 15 distinct runtime bugs. +- Temporal permission extended 114 checks across 79 executions. Accepted timing + schedules are retained in batching.jsonl; a recorded case replayed with identical + canonical schedule and group history. +- Campaign artifacts and logs: ../solid-effect-publication-evidence/semantic-fuzzing/ + batching-checkpoint-2026-09-14 (sibling evidence workspace). + +No Solid runtime source changes are part of this checkpoint. diff --git a/documentation/plans/semantic-fuzzing-reduction-audit.md b/documentation/plans/semantic-fuzzing-reduction-audit.md new file mode 100644 index 000000000..3475bb3fa --- /dev/null +++ b/documentation/plans/semantic-fuzzing-reduction-audit.md @@ -0,0 +1,432 @@ +# Implementation plan: get to the next actionable issue + +The current implementation plan is [semantic-fuzzing-cleanup.md](semantic-fuzzing-cleanup.md). +The decisions and measurements below are retained as historical evidence. + +Status: A, the bounded B transformations, and E are implemented and verified +(2026-09-14). C now includes implemented branch, delivery, observation and multi-proposal +experiments described below; other C opportunities and D remain deferred. This revision replaces the earlier plan that prioritized +preserving each original failure identity during reduction. Solid runtime semantics +and the fuzzer's rule definitions remain unchanged. + +## Goal and working loop + +Help the user find, understand, and fix **one real issue at a time**. A smaller +representative count, exhaustive root-cause classification, and preserving every +variation as a separate triage item are not objectives in themselves. + +1. Run the fuzzer against the selected runtime and retain its generated failures. +2. Reduce a promising case aggressively toward a simple, valid semantic failure. + It may expose a different bug from the original. That is useful, not a reason + to reject the reduction. +3. Independently replay the result, check the rule's applicability, and manually + translate/reduce it into a clear example. Present one actionable issue. +4. When a fix is available, replay the saved corpus against it and run fresh + fuzzing. Use surviving or newly generated failures to select the next issue. + +Saved originals provide cheap deterministic regression coverage; fresh fuzzing +provides new combinations. Neither requires an exhaustive classification before +reporting the first issue. Never claim that fixing a reduced example fixes every +original that reached it. Nothing in this plan automatically posts reports. + +The 97-case audit remains useful evidence, not a target number to optimize: +`../solid-effect-publication-evidence/semantic-fuzzing/all-97-audit-2026-09-14/`. +Keep all 1,720 original memberships. The audit's previous warnings about losing +failure identity now identify possible bug changes, not automatic rejection rules. + +## What changes from the previous plan + +- Default discovery reduction may change the rule, diagnostic, source kind, + lifecycle phase, observation shape, or failing value, provided the candidate + independently constitutes a valid failure under an enabled semantic rule. +- Do not build structured witness matching, projection proofs, diagnostic alias + registries, or default multi-runtime preservation checks. +- Preserve originals and the final reduced result with their actual diagnostics. + No exhaustive bug-equivalence graph or full rewrite history is required. +- Keep strict, focused reduction available through the existing fingerprint/control + mechanism when the user explicitly asks to minimize a particular issue or + comparison. It is not the default discovery bottleneck. +- A reduction that becomes a clean pass is still rejected. A malformed scenario, + unsupported oracle scope, harness error, timeout, or exhausted budget is not a + substitute for a semantic bug. Policy/allowance-only findings remain separate. +- Prioritize removing distracting graph/schedule complexity over maximizing merges. + Delay speculative grammar and state-history machinery until there is a specific + useful repro it would simplify. + +## A. Implement simple discovery acceptance first + +Files: `shrink.ts`, `equivalence.ts`, and CLI replay/artifact handling. + +- Add an explicit discovery acceptance mode and use it for ordinary bug discovery. + Reuse the existing exact-fingerprint behavior for focused reduction. +- Centralize the small acceptance predicate. In discovery mode, require a valid, + supported execution with an actual admitted semantic violation; it need not + have the original fingerprint. Consult result disposition/allowances, rather + than assuming every non-pass result is a bug. +- Do not accept a W1 review finding or a waived progress observation as a + replacement for an actionable failure. Review-only cases may be minimized in + their own existing workflow and must remain labeled review-only. +- Do not weaken the invariants or introduce special-case waivers to make a + candidate fail or pass. The new candidate is checked against its own graph, + dependencies, update history and observation obligations. +- On acceptance, update the selected result and its reported fingerprint. Final + replay must reproduce that reduced result, not the original diagnostic. + Audit the CLI's current fingerprint comparisons and artifact keys accordingly. +- Record the original case reference, original diagnostic, reduced scenario and + final diagnostic. Keep original corpus entries even when several reduce to the + same result. Report a reduction relationship, not proven shared root cause. +- Reuse the current budget and seen-set machinery. Additional confirmation only + needs to verify the final candidate on the runtime being investigated. + +Examples now allowed when they remain valid failures: C14 → C05, C17 → C20, +C18 → C16, C03 → C01, and reductions exposing a simpler optimistic lifetime +problem. C02 → C11 must not silently replace a bug report with a policy review. +C56/C83 may reduce to ordinary failures on a runtime where those failures exist; +they do not need to keep failing on every other historical PR branch. + +Tests: cross-rule semantic failure accepted; clean pass, invalid, inapplicable, +policy-only, waived, worker-error and budget-limit outcomes rejected; final replay +and artifact labeling follow the reduced diagnostic. Exact mode and explicitly +requested passing controls still work. Use fabricated result records for these +unit tests so upstream fixes do not invalidate reducer correctness tests. + +## B. Implement the replay-proven transformations + +Keep these as ordinary candidate generators. They do not need semantic +identity metadata under discovery acceptance. Replay decides whether the simpler +program still exposes a useful violation. + +### Tuple-source projection + +Files: observation candidates in `reduce.ts` and tests. + +Extend fixed-reader-to-anchor conversion to a flat, always-observed data reader +containing sources and derivations. Extract its source fields into publication +anchors, adding each once, and retain its remaining fields in order. Start with +unaddressed readers without nesting, boundaries, pending mode or attachment +features. One extraction per reader is sufficient initially; no subset explosion. + +This changes effect structure intentionally. Positives: C26 → C22, C28 → C48, +C36 → C21, C41 → C40, C83 → C56, C92 → C76. The tested C04/C20/C74/C84 +projections pass and must be rejected. No tuple-versus-anchor witness matcher. + +### Optimistic/action-to-signal substitution + +Files: optimistic candidates and reference helpers in `reduce.ts`. + +Start with simple single-proposal scripts. Replace the proposal with an ordinary +write, remapping dependencies, branch selectors, reader refs, anchors and supported +imperative reads. Ensure no dangling references or invalid action controls remain. +Initially skip complex queued scripts and explicit request selections that cannot +be translated cheaply. + +Removing action lifetime or a later authority write is permitted as an intentional +simplification if the resulting program is valid and still fails. It must not be +presented as equivalent lifecycle behavior. Similarly, a tested latest-to-explicit +optimistic substitution may be offered without asserting those APIs equivalent. + +C19 → C35, C56 → C21 and C83 → C36 are useful paths. C43/C93 become passes +under the tested substitution. Other authority-driven cases need candidates that +retain or remove the later write explicitly, rather than mislabeling an accidental +omission as a faithful translation. + +### Coordinated proposal/authority rewrite + +Recognize no-op proposal plus immediate action completion; replace the pair with +one live proposal of the authoritative value, editing the script and values +jointly. Keep the first implementation restricted to direct, adjacent operations +with no intervening queued work. It may change the eventual lifetime: accept only +if the rewritten scenario independently fails. C10 → C13 and C30 → C19 are +positive examples; C25/C88 pass after the attempted rewrite. + +### Arithmetic cone plus exact request translation + +Extend `affineQuestions` and existing reference helpers instead of adding another +parser. Replace a single-source pure synchronous arithmetic cone feeding an async +memo with a simple async identity. Translate explicit resolve keys together with +the graph: owner, input tuple and occurrence. Keep each retained resolution exact +and valid for the rewritten schedule; reject ambiguous mappings or try deletion +of that operation as a separate explicit candidate. Never silently resolve an +arbitrary different request as a fallback. + +Start with cones whose removed nodes have no other consumers or imperative reads. +Keep safe numeric representations. C60/C94 → C12 are positives; the C42 identity +rewrite passes. Test repeated equal inputs, occurrences, cancellation and remapped +owners. Reuse existing source merging in small coordinated cone/branch candidates; +a merge may change the type of bug, but must leave a valid modeled graph. + +### Schedule rewrite that unlocks pruning + +Add a small late-running candidate family alongside the existing within-callback +ordering rules. First support moving an uncanceled visibility microtask into a +new host turn immediately after its enclosing turn. Preserve unrelated steps and +queue IDs. Then rerun cheap deletion/pruning: this lets C31/C38 shed a latest read +and reach C02. + +Changing batching is allowed because this is a new replayed candidate, not a +normalization equivalence. Reject malformed queue/cancel/task references. Use a +fixed representation preference and the existing seen set to prevent cycles; +charge the enabling move and subsequent reduction to the same budget. C52/C59/ +C61/C77/C88/C95 are negative controls for the tested host-turn rewrite. + +## C. Try inexpensive extensions after the first useful results + +Each item is a bounded experiment. Land it if it materially simplifies a candidate +worth understanding, not merely because it reduces the number of groups. + +| Opportunity | Implementation approach | Cases / stopping condition | +| --- | --- | --- | +| Broader diagnostic/verdict projection | With A in place, let existing reader/pending/anchor deletion candidates expose other admitted rules. Keep the final rule and disposition honest; no alias framework. | C03/C12/C19/C37/C43/C50/C56/C60/C83/C94. Do not accept a mere policy review or harness failure. | +| Source merging | Reuse the existing merge helper with a bounded accompanying branch/key rewrite. Validate the resulting source/update history. Independent sources may become one source if the new program still has a real failure. | C32/C33/C39/C49/C65/C86. Do not claim the original G2 relationship survived. | +| Legacy v1 to v2 | Encode the actual v1 source/anchor defaults in a valid v2 candidate, then run normal reduction. Keep legacy loading supported. Treat it as replay-tested until output and oracle-scope equivalence are established. | C59/C95. Stop if this requires unrelated runner-semantic changes. | +| Branch rewriting | First bypass whole branch cones, collapse repeated selectors, and simplify both arms using the existing grammar. Condition reads and async edges may change intentionally, but the independent model must describe the new graph correctly. | C16–C20/C62/C66/C78/C84/C86. A simpler C84 that only fails the selected runtime is useful; no historical multi-target requirement. | +| Alternative async delivery | From a local minimum, try one bounded delivery change and a finishing pass. Accept only a smaller or equally small preferred result that independently fails. Share budgets and avoid rewrite cycles. | C34/C68, C21/C76/C36/C92; negatives around C43/C93/C74/C96. No unbounded beam search. | +| Boundary/ownership projection | Try flat boundary-to-pending substitutions and removal of one nesting level. Rewrite reader ownership consistently and replay under the resulting graph's rules. A different genuine bug is acceptable. | C08/C23, C44/C81/C90, C54/C71/C79/C87. Do not alter visibility rules or excuse leaks using Solid owner state. | +| Mount/gate projection | Extend isolated reader conversion with coordinated initial flag, control writes and anchors. Produce a valid new observation model; hidden/absent/detached remain distinct in its oracle. | C24/C25/C27/C46/C65/C70/C72/C77. C74/C96's passing Show translations are rejected as candidates, not reasons to weaken the checks. | +| General action simplification | Try inlining synchronous actions, deleting empty segments, or jointly removing holds/resumes and keeping resulting ordinary writes. Simplifying away the original unfinished action is allowed if the new program still independently fails. | C04/C06/C80. Retain valid same-callback batching for whatever script remains; no invented async lifetime surrogate. | +| Broader timing / latest-read erasure | Try a few explicit callback-hoisting or flush/turn templates; retry existing read deletion afterward. Validate IDs and request selections and keep the search bounded. | C50/C61/C67/C68/C73/C77/C85/C88/C95. No automatic assumption that operations commute. | + +A failed candidate is ordinary negative evidence, not a reason to keep a whole +family forever or to construct machinery that forces it to work. + +## D. Defer machinery with uncertain return + +### Grammar extensions + +Do not start by adding a general expression AST. If an otherwise useful issue +remains needlessly complicated because a constant branch cannot be represented, +prototype the smallest bounded extension for that example. Update the independent +model, runner, normalization and request capture together. Land it only if the +benefit outweighs the extra language/model surface. Otherwise retain the larger +repro and move on to fixing the issue it already demonstrates. + +### Initial-state and prefix reduction + +First try existing prefix deletion and coordinated value changes. Do not build +reactive-history reconstruction. If several useful cases would become much clearer +with nonzero source initializers, consider a small optional initial-values array. +The resulting graph can be a different program: it does not need to recreate +original owner/prev/lane history. It does need a correctly modeled initialization +and valid suffix operations/request keys. A real failure in that new program is +useful. No graph snapshots, pooling or generalized state serialization. + +C51/C64/C67/C72/C73/C85/C87/C89 are exploratory examples, not a commitment to +support every prefix. Preserve the originals for replay after a fix. + +### Cross-runtime preservation + +Do not add it to ordinary reduction. Select the runtime being investigated, verify +the final repro there, and replay/fuzz against its fix afterward. Existing focused +controls can serve an explicit request to retain a particular regression boundary; +that exception should not drive the normal architecture or replay cost. + +## E. Keep paired findings honest without locking discovery to them + +Reuse `equivalence.ts` and its existing two-run semantics. An E4 claim still needs +two valid comparable executions, eligible warmup, and correct variant-specific +request keys. Never report an invalid comparison as a Solid bug. + +In discovery mode, if reduction exposes an ordinary admitted semantic failure in +one valid variant, that can become the selected issue. Export the actual variant +as an ordinary scenario, clear paired-only fields/comparison labels, and retain +the original paired case as provenance. Do not keep calling it E4. If a reduction +still relies on an E4 difference, retain and replay both variants normally. + +Focused equivalence reduction remains available when specifically requested. Test +single-variant conversion, variant-key cleanup, one-sided inapplicability and +warmup that starts async work. C91 remains in the saved corpus regardless of what +simpler issue discovery selects. + +## F. Validation and the fix/replay/fuzz cycle + +For each implemented slice: + +- Add transformation/acceptance tests with passing and failing controls. Keep + reducer unit tests independent of upstream bugs remaining unfixed. +- Run relevant semantic tests and typecheck. Compare old/new reduction on the + same frozen corpus and budgets, including another old-rule finishing pass. +- Independently replay the final candidate with its actual rule and graph. Check + exact request selection and artifact correctness. Preserve all originals. +- Manually inspect a few before/after examples: did the change remove meaningful + explanatory complexity and make an issue easier to reproduce and understand? +- Record replay cost, time, budget exhaustion and candidate complexity as supporting + metrics. Representative count is diagnostic, not an acceptance target. +- Check the ordinary budget on a separate generated sample. Avoid building a + reducer specialized only for the 97 audited shapes. + +The operational success criterion is reaching a small, credible repro promptly, +then being able to tell what remains after its fix. Present one issue, not a dump +of every surviving category. Keep supporting variants in artifacts unless they +materially change the user's understanding of that issue. + +After a fix, replay saved originals and reduced examples, then run fresh fuzzing +on the patched runtime. A formerly merged case can now surface as its own issue. +When a known unfixed bug dominates a run, retain its examples and test a candidate +fix when available; do not blacklist an entire rule or broadly waive related +failures just to make the run look clean. + +Implementation style: small helpers, loops, predictable numeric IDs and cheap +applicability checks before copying. Reuse request/key helpers. Keep accounting +and provenance off the graph hot path. No pooling, generic proof machinery, +always-on multi-version replay, arbitrary permutation search or exhaustive root- +cause bookkeeping before the next issue can be fixed. + +## Coverage of the original audit opportunities + +| Opportunity | Revised location | +| --- | --- | +| diagnostic / verdict-projection | A; C | +| tuple-projection | B | +| source-kind | B | +| lifecycle / action-lowering | B; C | +| graph-quotient / request-translation | B; C | +| source-merge | B; C | +| schedule / latest-read-erasure | B; C | +| version-upgrade | C | +| branch-rewrite / delivery-substitution | C; grammar extensions in D only if justified | +| boundary-substitution / ownership-projection | C | +| control-projection | C | +| initial-state | D, deferred | +| multi-target | D, removed from default scope | +| paired-preservation | E, correct reporting rather than mandatory same-bug preservation | + + +## First implementation results (2026-09-14) + +Discovery is now the default for admitted semantic failures; `--shrink-mode focused` +retains fingerprint matching. W1 review and waived P1 observations cannot replace a +bug. Final replay and artifact diagnostics follow the reduced result. Paired +findings may export an independently failing variant with ordinary replay metadata. + +Implemented candidates: mixed source/derived reader projection, single-proposal +optimistic lowering, latest-to-override substitution, adjacent no-op proposal and +completion rewriting, an isolated affine cone with exact request translation, and +an uncanceled visibility microtask moved to the next host turn. These deliberately +have narrow applicability checks; broader grammar and schedule searches are deferred. + +On all 97 saved representatives, using the same frozen PR #3425 worker and a +500-attempt budget, old/focused/discovery reduction produced 97/88/72 distinct +repro keys. Those are not counts of unique bugs. All final examples replayed; +none exhausted the budget; all 1,720 original memberships remain saved. +Discovery changed the diagnostic of 29 examples. Runtime calls were +2,187/2,453/2,714; single-run wall times were 11.4/13.2/13.0 seconds. These timings +are supporting measurements, not a statistically established speed comparison. + +Useful concrete simplifications: + +- C31/C38 lost their imperative latest read and queued callback, leaving an ordinary + write followed by hiding the reader in the next event. +- C60/C94 lost their arithmetic helper and explicit request-resolution operation, + leaving one async memo and one optimistic proposal. The result reports S4 instead + of O2; that is a different independently witnessed failure, not proven same cause. +- C91 still requires a paired E4 comparison and remains labeled as such. + +A separate 1,000-case ordinary campaign at seed 91427, on checked-out target +`e69d4769b25e695c010f34c592fb75be4e33cc8f`, retained 13 semantic failures and +five policy findings with no worker errors. The four selected examples were +independently replayed after shrinking; two reached the normal 150-attempt budget. +The campaign including shrinking took 7.2 seconds. These findings are validation +of the discovery workflow, not newly triaged upstream issue claims. + +A same-runtime, same-budget old/new comparison on all 18 retained fresh findings +produced identical scenarios and diagnostics (1,895 attempts each; three cases hit +150). This sample establishes replay compatibility, not a general reduction win. +The demonstrated improvements are in the audited optimistic/observation examples. +Validation: 41 semantic test files / 304 tests pass; semantic TypeScript checking +and `git diff --check` pass. + +Evidence, replay scripts and complete results: +`../solid-effect-publication-evidence/semantic-fuzzing/discovery-reduction-2026-09-14/`. +The next useful step is selecting a clear repro and replaying its originals against +its fix, rather than expanding all optional reducers before reporting anything. + + +## Coordinated reduction results (2026-09-14) + +Implemented and retained after real replay experiments: + +- Conditional-to-consumer folding and simultaneous shortening of both branches, + with immediate disconnected-node pruning. +- Uniform async delivery plus explicit-flush-to-host-turn rewriting. +- One memo's delivery changed together with bypassing another memo, preserving + mixed async paths. These are bounded two-edit templates, not a general search + through passing intermediate programs. +- Nested/gated/pending observation scaffolding collapsed into one fixed reader, + with obsolete reader-control commands removed together. Render regions are excluded. +- Multiple direct optimistic proposals lowered into ordinary writes, including + explicit authority. This is an intentional new program, not preserved action timing. + +More elaborate async modes require removing a node or an explicit flush in the same +candidate. The first unrestricted experiment kept switching async forms and cost +6,395 runtime calls. It was replaced with the bounded version; its results remain +in evidence as `unrestricted-*`, not as the chosen implementation. + +At equal 500-attempt budgets, starting from the prior 97 saved outputs, old/new +reduction produced 72/67 distinct repro keys. All final outputs independently +replayed; no case hit its budget; all 1,720 original memberships remain. The final +67 contain 55 semantic failures and 12 policy-review findings, not 67 proven bugs. +The extra pass used 2,914 runtime calls versus 2,037; single-run wall times were +9.8 versus 8.9 seconds. This is no claim of a stable throughput benchmark. + +Concrete wins: C68 now matches C34 despite the original delivery/event difference; +C84's conditional graph loses a memo; C89 loses a memo and preparation turn; C93 +reaches the existing one-memo S4 failure; C62 loses a memo and reaches the existing +three-memo L1 example. C28/C48 also lose a separate source reader. Ordinary source +role swaps were probed but produced no further matches, so were not implemented. + +A fresh 1,000-case branch campaign (seed 91428, checked-out runtime, ordinary +150-attempt budget) retained seven semantic failures and six policy findings, with +zero worker errors. It took 5.9 seconds including shrinking. This is validation, +not a claim of seven new upstream issues. These rules improve concrete repros but +have not reduced the report queue to a handful of established issue families. + +Scripts, pinned runtime comparisons, raw findings and reduced examples: +`../solid-effect-publication-evidence/semantic-fuzzing/coordinated-reduction-2026-09-14/`. + +Validation of the final coordinated slice: 42 test files / 313 tests pass, semantic +TypeScript checking passes, and `git diff --check` is clean. The prior separate +18-finding sample reached the same final repros with 1,895 vs 1,900 attempts and +three budget exhaustions each. Of the fresh branch campaign's five selected +examples, three reached 150 attempts; those are reduced witnesses, not claimed +local minima. + + +## Further paired edits implemented (2026-09-14) + +All four follow-up ideas are implemented: source merging plus one small offset +adjustment; queued callback payload moved to the containing callback's end; +first direct visibility setter absorbed from a mixed callback; and one memo's +delivery changed together with a valid adjacent declaration swap. Surviving exact +request identities remain intact; invalid or policy-only outcomes cannot replace +a semantic failure. These are new replayed programs, not assumed equivalences. + +Delivery/declaration candidates prefer a canonical direction or combine with cone +removal. A small per-run ordering limit prevents another family from resetting the +ordering search. A final ordinary reduction pass disables delivery/declaration +moves and retries prior simplifications, sharing the remaining budget and passing +control. This makes shared repros converge instead of ending on different visited +async forms. Both ordinary and paired reducers implement this finishing pass. +Read-order reversals alone showed no direct reduction benefit and were left out. + +Final frozen-runtime comparison: 67 -> 64 repro keys, comprising 52 semantic +failures and 12 policy findings. All 97 outputs replayed; all 1,720 original +memberships remain; none exhausted 500 attempts. Runtime calls increased from +2,649 to 5,865; single-run wall times were 10.6 and 21.5 seconds. This is a real +search-cost increase for more aggressive reduction, not a performance improvement. +The first unrestricted ordering experiment took 8,256 calls and hit one budget; +those intermediate artifacts are retained separately. + +Confirmed wins: C32 reaches C17 by merging its sources and repairing an equality; +C61 reaches C29 by appending the queued write after the visibility setter; C76 and +C21 now converge through delivery/declaration changes and cleanup. C42 loses its +arithmetic memo and initial hide operation. The total count is not a count of +independent bugs and is still larger than the desired handful of issue reports. + +Validation: 43 test files / 320 tests, semantic TypeScript checking and diff checks +pass. A fresh 1,000-case branch campaign at seed 91429 on the checked-out runtime +retained 11 semantic failures and five policy findings, with no worker errors. +It took 8.0 seconds including shrinking. The five selected results all replayed; +two used the full default 150-attempt budget and are not claimed local minima. + +Evidence and reproducible scripts: +`../solid-effect-publication-evidence/semantic-fuzzing/paired-reduction-2026-09-14/`. diff --git a/documentation/plans/semantic-fuzzing.md b/documentation/plans/semantic-fuzzing.md new file mode 100644 index 000000000..663687806 --- /dev/null +++ b/documentation/plans/semantic-fuzzing.md @@ -0,0 +1,80 @@ +# Core semantic fuzzing + +Build a reusable tool for finding contradictions in reactive publication across +graph shapes and schedules. The deliverable is the generator, independent rules, +replay and reduction machinery, not a fixed list of discovered bugs. + +Implementation and commands: [semantics README](../../packages/signals/tests/semantics/README.md). +Executable contract catalog: [rules.ts](../../packages/signals/tests/semantics/rules.ts). + +## Architecture + +A versioned JSON scenario describes a bounded pure scalar DAG, readers and +external events. Stable node IDs and request occurrence keys survive replay and +reduction. `fast-check` supplies seeded structural generation; a project-owned +interpreter invokes the real signals primitives directly. No per-case source +compilation, DOM, solver or replacement event loop is required. + +A separate numeric model evaluates expected answers and dependencies. It does +not copy Solid's scheduler or consult its pending lists to decide which work is +required. Publication anchors are explicit observers; absent anchors cannot be +used as evidence of published state. Intermediate checks use witnessed values, +while final checks also detect missing publication. + +Native promises, microtasks and await continuations keep their JavaScript +ordering. MessageChannel tasks deliver synchronous event blocks and establish +microtask drain barriers. Generated promises and action gates are controlled by +separate ledgers. Task callbacks are explicit external input, not residual memo +work. General application timers and wall-clock deadlines are outside the model. + +The completion suffix delivers remaining external input, settles exact current +and witnessed fallback requirements, then releases residual requests individually. +An earlier request with identical inputs is still a different occurrence. +After host drains, an independent sufficient progress check also runs before +fallback or residual requests finish. Ideal violations are evaluated first; +optional named allowances classify exact evidence without erasing it. Exceptions +cannot excuse corrupted or missing final data. Once a correct final view is reached, +residual completion cannot change it without new input. + +Workers isolate nontermination and out-of-band exceptions. Watchdogs and heap +limits are inconclusive outcomes rather than semantic failures. Lifecycle error +handlers remain installed between cases. Failed cases retain their pre-cleanup +ledger; physical teardown still releases outstanding gates. A worker failing +while idle is attributed to its previous input and stops the campaign. + +## Contracts and calibration + +Rules have explicit preconditions and authority. Confirmed contracts and proposed +optimistic semantics remain distinguishable in artifacts and documentation. +A counterexample is a candidate violation of a scoped law, not automatically a +confirmed upstream bug. In particular, do not silently settle the open contract +for imperative latest reads or broaden accepted disposal waiting by inference. + +Self-tests prove validation, scheduling, model checks, identity, reduction, +reporting and containment. Disposable runtime mutations calibrate detection; +reduced cases must pass against unmodified source. Tests must not require real +upstream bugs to remain present. Candidate corpus replay is a separate command; +fixed, accepted cases can become ordinary correctness regressions. + +Paired runs compare synchronous and asynchronous delivery, early/latest companion +creation, and a held optimistic proposal versus an ordinary write. Each relation +has narrower preconditions than the full scenario language. Both runs retain +independent safety checks; paired reduction preserves which side failed and the +exact request choices on each side. + +## Retention, reduction and cost + +Every finding retains its canonical schedule in a compact JSONL record. Symptom +grouping limits full traces and shrinking, not candidate retention. Group counts +are not estimates of unique root causes. Source/build hashes, rule revision and +execution policy identify the target; seeds alone are not replay authority. + +Reduction tries valid local structural simplifications under a finite attempt +budget. Exact scheduled request identities cannot silently retarget after an +earlier operation is removed. The result is replayed, but is not claimed to be a +global minimum or proof of a shared root cause with its larger input. + +Use small numeric values, dense slots, short loops and bounded graphs. Allocate +independent snapshots where retaining evidence requires ownership. Avoid pooling, +shared mutable buffers or added caches without profiling. Build each worker once +per campaign; all added instrumentation and allocation live in test tooling. diff --git a/packages/signals/package.json b/packages/signals/package.json index 410c17506..7174fdb4c 100644 --- a/packages/signals/package.json +++ b/packages/signals/package.json @@ -50,6 +50,8 @@ "build:js": "rollup -c && node ./scripts/mangle-props.mjs dist/prod dist/observe && node ./scripts/check-pure.mjs dist/prod && node ./scripts/check-pure.mjs dist/observe", "types": "tsc -p tsconfig.build.json", "test": "vitest run", + "fuzz": "node tests/semantics/cli.mjs", + "fuzz:types": "tsc -p tests/semantics/tsconfig.json", "test:watch": "vitest watch tests", "test:gc": "node --expose-gc ./vitest.js", "test:gc:watch": "node --expose-gc ./vitest.js --watch", @@ -62,6 +64,7 @@ "@rollup/plugin-replace": "^6.0.3", "@rollup/plugin-typescript": "^12.3.0", "@types/node": "^25.0.8", + "fast-check": "4.9.0", "rimraf": "^5.0.1", "rollup": "^4.53.4", "rollup-plugin-prettier": "^4.1.2", diff --git a/packages/signals/tests/semantics/CONTRACTS.md b/packages/signals/tests/semantics/CONTRACTS.md new file mode 100644 index 000000000..5071c32d8 --- /dev/null +++ b/packages/signals/tests/semantics/CONTRACTS.md @@ -0,0 +1,559 @@ +# Contract rationale and reference + +Current commands and reduction behavior are in [README.md](README.md). +The executable catalog is `rules.ts`; [EVIDENCE.md](EVIDENCE.md) distinguishes +calibrated detection, proposed contracts and remaining scope limits. Historical +measurements below describe their recorded revisions, not current throughput. + +## Language and execution + +- The ordinary cohort has one numeric source; the multi cohort has two. + Both have one visibility signal, 1–5 generated pure memos and 1–3 readers; + version 2 replay permits up to four sources and larger bounded graphs. Memos form a DAG with stable + IDs, arbitrary sharing, affine computations, and sync, fulfilled-promise, + manually resolved promise, or `async`/`await` delivery. +- Readers publish tuples through the two halves of `createRenderEffect`. They + can conditionally read their inputs or each sit inside a real Loading boundary, + either retaining content or resetting on source changes. Roots can be disposed. + The mounts cohort creates and cleans up owned reader roots through a parent + render effect. Memos remain outside that subtree. Prepared child values become + visible only when the parent publishes attachment; desired mount state is + tracked independently so a missed mount cannot erase the final obligation. +- The boundaries cohort gives each reader its own retaining or resetting Loading + boundary. Multiple regions may share requests, but fallback permission stays + local to the reader that actually displays it. The nested cohort adds a + static region tree with inner boundaries and unbounded children. A child can + prepare content while covered; it only becomes visible when its enclosing + region publishes content. Reset boundaries use the first source as their key. + Nested regions currently exclude observation toggles, dynamic mounts, pending + readers, optimistic actions and branch-selecting memos. +- The readiness cohort renders `isPending(() => refs.map(read))` without an extra + data observer. Explicit event-block clicks read that expression only when its + published verdict is false. Context-free pending probes are separate operations. + Generation combines these with conditional readers, owned mounting, and optional + publication anchors. The derived-readiness cohort passes the verdict through + 1–3 synchronous numeric memos; small integer arithmetic exercises propagation + without wrapping graph values in objects. The optimistic-readiness cohort adds + guarded clicks and those derivations to one-parent overrides. Their own slot + is verdict-inert while the action is held (A24). Pending readers over latest + action inputs and async derivations of the verdict itself remain unsupported. +- The optimistic cohort has one real action, one optimistic scalar, 1–4 controlled + yield gates, and a final authoritative write. Readers are initialized, + unconditional and have no boundary. Action gates and memo requests have + separate ledgers. Multi-parent actions are unsupported. +- The reads cohort adds explicit ordinary/latest reads inside event blocks. + Latest equivalence warms companions before the same schedule without adding a + reader. Warming that starts extra async work or suspends is inapplicable to the + allocation-only law. The latest cohort uses a real action writing authority + while memos/readers consume a separate latest channel from that signal or a + shared identity memo. Downstream work can be async. `viaMemo` changes the + upstream owner; all reads still share that owner's companion. +- The branches cohort adds two-source branch-selecting memos. Async request keys + include the selector and selected inputs; required work follows that chosen + path. The branch-boundaries cohort combines this with flat Loading regions. + Requested and published paths are recorded separately; a published path is + unknown when source anchors cannot witness it. This combination currently + excludes dynamic mounts, nested boundaries and optimistic inputs. +- Source and visibility effects are explicit publication anchors in + ordinary graphs. Version 2 can remove source anchors with `anchors` and the + visibility anchor with `anchorShow: false`; the observation, latest and branches + cohorts exercise these. Rules never read hidden runtime accessors to inspect + state. Without an anchor, intermediate derivation checks require an actual + tuple witness; final desired-value checks still apply. Only `frame.inputs` + entries corresponding to configured anchors are published observations; legacy + `input`/`show` fields are not witnesses when their anchors are disabled. +- A turn is an uninterrupted synchronous block. Queued microtasks, promise + continuations, and async continuations use native JavaScript scheduling. + Callback delivery through a host task uses `MessageChannel`; a subsequent + real host task establishes that finite native microtask chains drained. + `flush` is an explicit generated operation, not silently inserted after each + stimulus. Initialization and disposal do explicitly flush. +- Tracked one-shot task callbacks model additional external inputs and can be + canceled. Their delivery order is explicit in the scenario. This is not a + general virtual `setTimeout` implementation: no deadlines, equal-deadline + timer races, intervals, elapsed-time code, or arbitrary application timers are + generated. All async work in the language goes through the ledger. + +`fast-check` supplies seeded structural generation. The interpreter, work +accounting, rules, and domain-aware shrinking remain small project-owned code. +There is no generic model-command executor that awaits each synchronous step. + +## Laws + +The executable rule catalog is in `rules.ts`, with the pure specification in +`scenario.ts`. It refers to values, observation and publication, not flags, +reporter sets, lane IDs or internal graph layout. + +| Rule | Assertion and limits | +| ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| S1 | A delivered tuple is internally coherent wherever it contains a derivation’s complete source set. Stable derivations agree with their own published sources; unrelated sources need not publish together. | +| S2 | Published visibility agrees with hidden/ready content, allowing an actual published fallback. | +| S3 | Once all generated writes' scoped `onSettled` callbacks have run, their inputs must have published at the completed flush. A raw internal transition-settled hook is only diagnostic. | +| S4 | After a correct final view, separately resolving residual work cannot change that view without new external input. | +| S5 | A disposed reader generation receives no further publication callback, including after remount. | +| L1 | All finite work, including residual requests, settling reaches the final pure view. A later repair cannot erase an earlier consistency violation. | +| P1 | After a host drain, ordinary writes publish once no visible reader requires unresolved work. Scoped to flat pure DAGs with complete anchors and settled observation controls; no actions, branches or readiness readers. Published fallback does not hold source publication. | +| W1 | Completion beyond modeled requirements remains a timing finding. Default `review` leaves unknown timing open; P1 independently rejects provable delays. Historical disposal permission is opt-in. `required-only` and `retain-disposed` remain alternative W1 investigation policies, not switches disabling P1. | +| O1 | Ready continuous optimistic data can publish while its one parent action remains open. O1 excludes gates, mounts, boundaries and readiness readers, and does not demand completion of ordinary controls. Experimental expectation. | +| O2 | Authoritative correction does not require obsolete optimistic requests to complete. Experimental expectation, distinct from eventual final correctness. | +| R1 | Ordinary imperative reads match an existing publication witness. For memo reads this requires a direct ready reader; an unobserved async memo is not assumed current merely because its source published. | +| R2 | A rendered ready control can read its guarded expression without suspending; where source anchors exist, its read agrees with them. This does not assert that every stale-but-readable value must have a true verdict. | +| R4 | After a flush, a continuously observed ordinary expression with a distinct desired answer and complete source anchors must report pending while that answer is held (SPEC A24). Equal-answer motion, partial witnesses, boundaries, changing observation and actions are excluded. | +| R3 | Explicit context-free isPending probes do not throw NotReadyError, per SPEC A16. No false-implies-readable assertion applies to these probes. | +| E5 | One held optimistic proposal and the same ordinary write have the same stutter-normalized data trace in a settled pure graph with unconditional readers and no boundary. The parent stays open until cleanup; reversion is outside this comparison. | +| E4 | Early latest creation without extra work or observation leaves later publication and imperative-read traces unchanged. | +| E3 | For generated pure graphs with settled initial state, continuously observed readers, one write, no readiness verdict readers, and no fallback, sync, fulfilled-promise and delayed delivery have the same data-publication trace after consecutive duplicates are removed. Every intermediate frame also runs the safety checks. | + +Async requests are recorded **before Solid receives their promises**, including +the memo ID, captured inputs, occurrence, and physical completion. Required +answers are derived from the scenario's pure graph and desired inputs, independent +of Solid's reported pending records. A hook marks which promises Solid registered +for diagnostics; that registration cannot make the oracle consider work ready. + +Requirements belong to individual readers. Current obligations are derived from +all declared refs and desired inputs, independently of whether Solid actually +ran the reader. A separate history records exact waiting requests on requested +paths after the reader attempted that prefix. Published fallback may retain +those witnessed requests; another flight at the same memo, or a newly spawned +descendant, gets no inherited permission. Once current content publishes, the +old waiting interval closes. Nested requirements name the enclosing fallback. + +This history is a sufficient scoped witness, not a complete reconstruction of +Solid's subscriptions. Unwitnessed old work stays in the residual phase; if it +is needed to finish, that is an open W1 timing finding by default. It cannot +suppress final correctness or publication checks. Partial source observation +never becomes a made-up published branch. Final footprint diagnostics use stable +replay IDs; internal selections use byte arrays and small source masks. + +The completion suffix stops external input, drains queued task callbacks, settles +required current/fallback-covered work, then resolves residual work one request +at a time. Ordinary unobserved cache changes are allowed. A later setter in a +queued callback is an external stimulus, not an orphaned memo completion. + +A recovered waiting finding outside the accepted exception has status `policy`, not a hang. A narrowly +allowed finding remains in the artifact even when status is `pass`. `inapplicable` +means a paired law's preconditions do not hold. None of these outcomes suppresses +applicable final-state or safety checks. Corpus labels likewise never disable laws. + +`onSettled` is a post-commit callback: it can start a new pending update but cannot +extend the committed one. S3 includes observed optimistic correction in the parent +update under the proposed correction contract. That stronger expectation is marked separately +from a confirmed upstream contract; the current checkout may fail it. + +Readiness has an explicit upstream exception: SPEC A16 requires context-free +isPending probes to remain nonthrowing, including false for initial uninitialized +data. A19 classifies that as loading, with no observable value to be non-final. +Inside a surrounding reactive context, initial NotReady propagates to boundaries; +untrack() by itself does not remove that context (#2928). Preserve this distinction +in the readiness laws; false is not a universal context-free readability +assertion. `pending-contract.test.ts` records the accepted behavior. + +## Reduction and calibration + +Reduction removes schedule chunks, operations, readers, unused nodes, and +unnecessary arithmetic. It preserves topological references and exact request +identities. Once a generated resolve selects a request, replay saves its captured +inputs and occurrence; deleting an earlier write cannot silently redirect it to +another request. Non-applicable generated resolutions become explicit no-ops. +Candidates must expose an admitted semantic failure, within `--budget` attempts +(default 150), and the resulting minimum is replayed again. Focused mode additionally +requires the original rule and symptom. This is a local +reduction, not a claim of a globally minimal program. + +The reducer also tries coordinated changes learned from retained failures: +initial hide-to-hidden-state, joint arithmetic/dependency simplification, removing +multiple offsets together, and moving synchronous arithmetic across memo edges. +It can remove a mounting mechanism, read a synchronous sum's inputs directly, +or join adjacent event bodies without dropping their operations. These edits can +change ownership, entanglement and batching. They require replay, rather than +being declared semantic equivalences; keep original witnesses when grouping them. + +Single-use bypasses can replace one reader or memo input with an upstream input +while retaining the intermediate computation for other consumers. Surviving +requests stay exact. Setter ordering also tries visibility and mounting setters +on different targets within one callback, always descending in the canonical +ordering. It never swaps setters across reads, flushes, callbacks or host turns. + +Late observation reductions can replace a mounting control with a visibility +gate only when the global visibility signal has no anchor, readers or setters. +A flat fixed reader can project its source fields into publication anchors, +retaining its derived fields in order, if no operation addresses that reader. +These changes require replay because they alter observation machinery. Original +cases remain in the corpus even when the reduced scenario exposes a different bug. + +Captured request inputs normally remain unchanged. Identity-value renaming uses +a bijection; folding a synchronous `a*x+b` input also offers an exact safe-integer +inverse for the affected request keys. Both retain the request owner and occurrence, +including paired keys. Invalid or inexact inverses reject that candidate; neither +rule falls back to selecting another pending request. Empty latest warmup lists +normalize to omission; nonempty warmups remain meaningful. + +Further bounded candidates lower a simple optimistic action into ordinary writes, +replace a no-op proposal plus immediate completion with a live proposal, and move +an uncanceled visibility microtask into its own host turn. A synchronous arithmetic +cone feeding an async memo can collapse to an async identity when its exact request +tuples translate unambiguously, retaining their occurrence numbers. These are +intentional program changes, not equivalences. Cheap pruning resumes after each +accepted edit; the same seen set and budget cover the whole search. + +Coordinated branch candidates fold a conditional into a consuming memo and shorten +both arms together, immediately pruning disconnected nodes. Delivery candidates +try a uniform async mechanism or change one memo while bypassing another. They +can also replace an explicit flush with a host-turn boundary in the same trial. +Thus two edits can expose a failure even when either edit alone would pass. +This is a fixed menu of two-edit templates, not an unbounded lookahead search. + +Manual gates or extra await boundaries are offered only together with removing a +memo or explicit flush. This avoids repeatedly switching between equally complex +async forms. Surviving manual request keys remain exact; resolutions of removed +async mechanisms become noops. Strict replay rejects requests the new graph no +longer reaches. No invalid run is accepted as a substitute failure. + +Observation collapse replaces non-render reader scaffolding with one fixed data +reader and removes its obsolete mount/dispose/click commands together. Direct +multi-proposal action scripts can lower to ordinary writes through their explicit +proposal/authority sequence. These deliberately change observation, batching and +lifetime, and require replay under the rewritten program's laws. + +Source-merging candidates also try a small offset adjustment in the same trial. +This can retain an old/new answer equality that either edit alone would lose. +Callback candidates can append an uncanceled queued payload to the end of its +containing callback. Initial visibility can absorb the first direct visibility +setter even when that callback contains other work. None assumes that the rewritten +schedule is equivalent; each is replayed as a new program. + +Delivery/declaration candidates change one memo's async delivery and swap adjacent +valid declarations together. They descend in canonical order, or remove an arithmetic +cone at the same time. A per-run ordering limit retains prior progress across other +families' delivery changes; a smaller graph starts a fresh ordering search. When +these moves succeeded and budget remains, a final pass disables them and retries +ordinary simplifications with a fresh seen set. This prevents an already-tried +simplification from leaving the output in an unnecessarily elaborate async form. +The finishing pass shares the attempt budget, counters and optional passing control. +Paired shrinking uses the same bounded finishing behavior. + +Minimized artifacts record `originalSignature`, `shrinkMode` and the final result's +actual diagnostic. Replay checks the final diagnostic, not the original. The +operational loop is to understand and fix one clear repro, then replay originals +and run fresh fuzzing against the fix. Shared reduced examples do not prove shared +root causes or justify dropping the originals. + +`shrink(..., { stats })` optionally collects attempts, accepted edits and candidate +replay milliseconds by reduction family. Control checks remain separately counted +in `controlAttempts`. With no stats object, shrinking makes no profiling clock +reads. An explicit `order` can test a deterministic family ordering; it does not +learn priorities across cases. Judge orders by resulting repro size, deduplication +and total replay cost, not acceptance rate alone: high acceptance can mean many +small edits that use up the budget before larger useful reductions. + +Calibration intentionally changes a disposable worker bundle, never source files: + +```sh +pnpm --filter @solidjs/signals fuzz --calibrate +pnpm --filter @solidjs/signals fuzz --calibrate --fault drop-wake --shrink +pnpm --filter @solidjs/signals fuzz --calibrate --fault stale-result --shrink +pnpm --filter @solidjs/signals fuzz --calibrate --fault lost-blocker --shrink +pnpm --filter @solidjs/signals fuzz --calibrate --fault false-ready --shrink +pnpm --filter @solidjs/signals fuzz --calibrate --fault false-verdict --shrink +``` + +These remove the scheduled flush, stale-flight identity guard, or pending reporter +registration, or replace isPending with a false verdict that never observes its +expression. The false-ready mutation calibrates guarded clicks and initial readiness; +it does not establish complete question-scoped pending coverage. A separate +false-verdict mutation runs the original isPending probe, preserving tracking +and initial suspension, but discards its result. R4 catches its false verdict +while an ordinary data observer holds the update. Exact mutation anchors must match once or the build +fails. Integration tests verify semantic detection, reduction, replay, and that +the reduced case passes against unmodified Solid. This tests detection of actual +runtime mistakes rather than merely feeding fabricated bad outputs to the checker. +Calibration exits successfully only when it detects a semantic failure and has no +runtime errors, invalid cases, or exhausted budgets. A worker crash is not evidence +that a calibration fault was detected. + +Tests also exercise native nested microtasks, chained fulfilled promises during +initialization, fallback retention, obsolete completions, reverse-order replay, +completion-probe neutrality, fresh versus reused workers, and watchdog termination. + +## Cost and next extensions + +The default worker is reused after complete disposal/draining; failures retire it. +`--fresh` isolates every case as a control. The parent watchdog can terminate a +worker stuck in synchronous or microtask work. Graph/work/trace/round budgets cap +individual cases. Worker V8 heaps are capped at 128 MiB old generation and +32 MiB young generation; heap exhaustion is `limit`, not a semantic failure. +Worker stdout/stderr are retained as a bounded 4 KiB diagnostic tail. The CLI +also reports the largest after-cleanup heap sample, explicitly not a peak or RSS +measurement. A campaign is capped at 10,000 generated cases; use separate +seeds/processes for larger runs. Each worker owns a separate Solid module instance. + +The hot checks prepare each scenario's graph once: stable replay IDs map to dense +numeric slots and source coverage uses four-bit integer masks. Evaluations and +path selections return independently owned arrays, with no shared scratch state +or invalidation caches. Requests are indexed by +node and matched by numeric tuple loops, never by JSON equality. Reader lookup +is prepared once, pending probes do not allocate discarded arrays, and inactive +readiness laws skip evaluation. Attribution hooks use a typed object instead of +a Proxy in the runtime's read/recompute path. The generated data remains small +integers; there is no bitwise coercion that would truncate a valid replay value. + +Captured request inputs and publication snapshots remain independently owned: +reusing those mutable buffers would destroy evidence. JSON remains appropriate +for canonical artifact keys, persisted reports and cold paired-trace formatting. +Performance profiling and benchmarks are deferred; this +cleanup claims fewer unnecessary operations, not a measured speedup. Prefer +straightforward loops and predictable data shapes; pooling, buffer reuse and +additional caches need a measured hotspot to justify their complexity. + +The CLI bundles once, interprets data for each case, and reports total campaign +time, operations, requests, and throughput. Timing includes generation, worker +startup, reports and requested shrinking, but excludes bundling. Equivalence's +`total` counts graph comparisons; `executions` counts its two or three runs per graph. +These are diagnostic measurements, not a production benchmark. Production pays +zero new bookkeeping: all changes are test files and development tooling. The +runner enables existing `__DEV__`, `__OBSERVE__`, and `__TEST__` checks. + +The [next implementation plan](../../../../documentation/plans/semantic-fuzzing-next.md) +records the staged scope. Implemented: scoped waiting policies, corpus and paired +reduction, multiple sources, one action with optimistic proposals, explicit reads, +latest warming and tracked derivations, ordinary/optimistic paired equivalence, +owned reader mounting, optional publication anchors, branch-selecting memos, +independent/nested boundary regions, scoped request/branch history across fallback, +readiness through synchronous numeric memos, one-parent optimistic readiness, +and A24 held-answer verdict checks. Unsupported combinations are listed above; +async derivations of readiness, latest-input readiness, and full question-scoped +pending semantics need further contract slices. Automatic TypeScript repro generation is skipped +for now; reporting repros are adapted manually from the artifacts. Custom equality, `prev`, errors, stores, streaming, +`loadingValue`, and generalized time remain deferred. + +Campaign results are tied to their recorded source and worker hashes. The original +baseline for this expansion was `005a623864ffd7cc3c76628369588ddcc4df26a5`; +those findings are not claims about current next or Ryan's current PR heads. + +On 2026-09-12, the working branch was rebased onto next at +`344ed054a932c7508efe57908eb175ed67d54940`. The `stale-reader` and `stuck-hidden` +corpus scenarios now pass under every waiting policy; their regression tests +expect correct publication. That rebase did not change the semantic laws. Revision 13 preserves exact request +occurrences through completion and snapshots work before cleanup. + +## Rule revision 13: decided semantics and provisional read scope + +Revision 13 treated the narrow #3347 disposal permission as accepted by default. +Revision 14 below replaces that default with an explicit compatibility allowance. +Other revision 13 contracts remain as described here. + +O1 now checks only the optimistic source anchor and continuous unconditional data +readers. It does not run the global final-state checker during an open action: +ordinary source, visibility and mount updates need not adopt optimistic timing. +Dynamic readers remain checked by S1/S2 and final completion; extending their +optimistic progress contract requires separately specified preconditions. + +R1 scopes by declared data flow, not merely the spelling of a read operation. +A plain getter or memo may forward the designated latest source (-2); its samples +remain recorded for replay and E4, but R1 does not choose staged versus published +outside reads. The existing source masks conservatively include both branch arms. +Independent ordinary sources in the same scenario retain R1 checks. No read-path +or graph work was added to Solid; this is pure per-scenario metadata. + +A15 independent-reveal controls and multi-action provenance generation are still +follow-up work. O2 and S3 have not been broadened or reclassified by this revision. + +## Self-tests and candidate corpus + +The default Vitest suite checks the harness, accepted runtime controls, and +injected mistakes. It must not require an unfixed Solid bug to remain present. +Candidate scenarios live in `corpus.ts`; `--corpus` evaluates them against the +current checkout and can report failures without making those failures a +requirement of the self-tests. A later runtime fix can turn a candidate into a +passing regression without changing its rule. + +`ruleContracts` records the scope and authority of each rule. O1/O2/E5 and the +optimistic extension of S3 are experimental proposals, not asserted upstream +agreement. Campaign failures need triage before becoming correctness tests or +CI gates; the self-test command is separate from those exploratory campaigns. + +Summary counts `failureSymptomGroups` and `findingSymptomGroups` measure grouping, +`retainedCases` counts compact replay records, and `workerErrors` records failures +outside an active case. JSONL replay entries carry the comparison, fault and +waiting policy required to reproduce their interpretation. + +## Rule revision 14: ideal progress and explicit exceptions + +Rebased onto PR #3392 at `bc54629bf58f55f47e0f4ae369919d643e85594a`, including +#3381 (`55847dd7`, `a82d2e3c`), on top of next `344ed054`. #3337 is a separate +unmerged stack and is not included. This records the tested foundation, not a +moving runtime pin. + +P1 is a sufficient progress proof, not a copy of the scheduler's blocker test. +After a host drain, inspect declared readers, their published visibility, and the +independent request ledger. Settled hidden/absent readers and published fallback +cannot hold ordinary source publication. Any remaining visible reader with a +missing or unresolved desired answer prevents the proof. All source anchors must +be present. Actions, branch selection, readiness readers and nested regions are +outside this slice; unsettled visibility/mount controls also prevent the proof. +We do not infer which separate transactions entangle or demand earliest per-source +publication while another live reader still has work. + +This check happens while fallback requests are deliberately unresolved. The old +completion check settled those requests first, so it could not expose unnecessary +source blocking behind a fallback. Boundary content must still finish afterwards; +P1 does not abandon its completion obligations. + +Evaluate the ideal law first, then classify a violation against named exceptions +in `policy.ts`. No exception is enabled by default. The historical optional +`legacy-disposal-wait` permission requires a v1 single-write graph, unconditional +readers without boundaries or visibility writes, and a pending observation +followed by explicit disposal for **every exact remaining request** in that write. +It cannot cover hiding a conditional reader, a fallback reset, a later write or +new unwitnessed descendants. It cannot waive S1–S5, L1, or another rule. Missing +final publication replaces an earlier progress failure as the primary finding. + +```sh +# Ideal progress contract (default) +pnpm --filter @solidjs/signals fuzz --seed 3289 --cases 1000 +# Historical comparison; ideal violations still appear in artifacts/counts +pnpm --filter @solidjs/signals fuzz --allow legacy-disposal-wait --seed 3289 --cases 1000 +# Replay a waived finding against the ideal contract +pnpm --filter @solidjs/signals fuzz --replay findings.jsonl --index 17 --allow none +``` + +`result.progress` retains the failed ideal check, immutable outstanding-request +witnesses and any matching allowance ID. Replay restores the recorded allowances +unless explicitly overridden. Shrinking a waived case preserves its exception +identity. `progressFindings` counts cases with ideal violations and `waivedProgress` +counts matches by allowance. These remain retained even when the run passes, so +compatibility does not erase evidence. Recovered timing beyond P1's proven scope +remains an open W1 finding, not an invented obligation or a hang. + +Calibration faults `lost-disposal-wake` and `lost-fallback-wake` remove only the +relevant wakeup in a disposable worker bundle. Each is caught by P1, reduced and +replayed against unmodified Solid. An integration test also checks opt-in waiver, +JSONL retention/replay, strict replay, and rejection of the same waiver for +fallback waiting. Correct early disposal/release and a second live reader serve +as positive controls. The harness no longer requires the old disposal delay to +remain a runtime behavior. + +The progress checker adds work only at host-drain checkpoints, using the existing +bounded graph/ledger. It adds no runtime node fields, subscription traversal, +virtual scheduler, per-case compilation or pooled buffers. Its limited sufficient +condition deliberately leaves room to expand contracts without reconstructing +Solid's implementation in the oracle. + +## Rule revision 16: ordinary update groups, action scripts and batching uncertainty + +The stronger experimental G1/G2 model is implemented in `groups.ts`. It derives +batch identity from executed setters within a callback and explicit flushes, +not runtime transition IDs. No extra microtask markers are scheduled. Identified `actions` can contain up to five +synchronous segments separated by controlled yield gates; at most two scripts +are declared. `start-action` and `resume-action` name the script with `action`. +Old optimistic scripts and replay formats remain supported. + +G1 checks atomic publication of proven groups and prohibits authoritative changes +while a group has action holds. Same-source overlapping writes merge their groups; +completed publication retires their identity. A resumed action retains its group. +G2 checks readiness per group, allowing unrelated groups to remain pending. +Potential joins through memo ancestry extend the deadline only, not the required +atomicity. Shared effects never introduce a join in the model. Reader demand +masks are compiled separately per ref and propagated through memo edges, so a +changed shared derivation still waits for necessary foreign async ancestors. + +The scope is initialized ordinary flat graphs with complete source anchors and +fixed data readers. Changing observation, boundaries, branches, readiness, latest +reads and optimistic lifetimes keep their existing rules but currently skip G1/G2. +Group checks can be disabled through the internal `groupChecks: false` runner +option for noninterference tests; campaign defaults enable applicable checks. +Summary fields `groupScopes`, `groupChecks`, `groupHeld`, `batchingHeld`, +`batchingCases` and `possibleGroupJoins` +make scope and conservative withholding visible. Counts are checks, not distinct +semantic cases. Failure artifacts include group history and action progress. + +```sh +pnpm --filter @solidjs/signals fuzz --cohort update-groups --seed 3289 --cases 1000 +pnpm --filter @solidjs/signals fuzz --calibrate --fault entangle-effect --shrink +pnpm --filter @solidjs/signals fuzz --calibrate --fault drop-action-hold --shrink +``` + +Calibration inserts an unwanted effect join (with commit replay so detection is +about the delay), or removes the action completion hold. Controls cover batching, +FIFO microtasks, shared effects, same-source reuse and multiple action lifetimes. +Focused shrinking preserves G2's independent/possible-entanglement/possible-batching/ +single-group relation. Discovery may select a different admitted violation; both +modes can simplify action segments. Candidate runtime delays remain in corpus, +never as tests that require them to persist. + +**Batching permission (2026-09-14):** writes in one synchronous callback form a +proven batch unless an explicit flush separates them. Separate callbacks within +one host task and its microtask drain may entangle, but need not. The generator's +queued callbacks and resumed action steps close proven batches; actual host-task +boundaries and explicit flush separators close the proximity window. Internal +flushes do not themselves end this permission window. + +A generated memo landing can bring its still-live source group into proximity +with another group active in that window. The hook supplies the generated memo's +source mask, never a Solid transition ID or an assertion that it is ready. The +permission is attached to the particular live groups, retained while they are +unfinished, and never inherited by later generations merely reusing a source. +Possible edges are closed transitively with possible derivation joins, so a +permitted join may encounter another update's genuine blockers. No edge adds a +mandatory publication constraint. Even permitted groups must publish once their +conservative blockers are gone; safety and final completion remain unchanged. + +`batchingHeld` counts checks that would have reported G2 without the temporal +permission. `batchingCases` counts executions using that permission. For those +executions, `batching.jsonl` preserves canonical schedules, group history and +metadata for replay. These are accepted uncertain timings, not failures or +confirmed runtime entanglements. A passing run without any such checks pays no +artifact-writing cost. Both independent and joint publication have oracle tests; +separated-task independence and fault calibration ensure this permission does +not disable progress checking generally. + +The unused fulfilled memo that absorbs a neighboring write is now covered by +this permission. Existing shared-effect delays across separate host turns remain +G2 candidates. Optimistic group integration and changing-observer group deadlines +are explicitly outside this checkpoint. Output attachment/cleanup modeling is the +next independent extension; see the implementation plan. + +### Attached output and cleanup (rule revision 17) + +```sh +pnpm --filter @solidjs/signals fuzz --cohort attachment --seed 3404 --cases 1000 --shrink +``` + +A reader can declare `render: { on, target, scheduled }`. Its parent render-effect +compute reads `on`, creates a fresh detached container, and creates a nested effect +that reads the reader's usual `refs`. The parent's scheduled apply attaches the +container. With `target: "portal"`, the nested effect instead attaches its payload +to an independently visible destination; this requires `scheduled: true`. +Local effects may run inline while preparing their fresh detached container. + +`output.ts` models only host creation, writes, attachment and removal. Numeric +handles and linked child lists give one record per created host node, without +per-frame subtree cloning, reactive graph traversal or pooling. The 4096-node cap reports +a harness limit, not a semantic failure. A root's actual reachability determines +visibility; ownership disposal does not automatically hide its output. Effect +cleanup explicitly detaches the affected node, so a cleanup that removes visible +content before replacement is observable. + +**A1** requires exactly one contribution in an initialized region until replacement +or explicit disposal. It also detects duplicate contributions and output left +behind after removal. The existing S1 rule checks the attached values against the +published source anchors. These checks run after complete flushes/drains, allowing +detached preparation and intermediate removal/attachment inside a flush. An +uninitialized region may still be empty; final completion remains checked by L1. +`output.test.ts` calibrates premature attachment, early visible removal and +extra contributions, alongside safe detached work and a real default-effect +preparation/scheduled-parent control. + +This slice supports ordinary single-source DAGs and fixed output regions. `on` +must be an ancestor of the payload, keeping parent work in the existing request +ledger. Actions, optimism, dynamic branches, nested boundaries and paired +comparisons are not composed with this renderer yet. G1/G2/P1 remain out of scope; +existing consistency and final-completion laws still apply. Detached portal +visibility is covered by host controls, but the generator currently uses visible +portal destinations. Output node counts, private writes and checks are retained +in results and campaign summaries. + +The #3404 integration case accepts either a valid runtime or an A1 finding and +checks replay/shrinking when failing. It does not require the runtime to preserve +the bug. On next 14ded248, 507/1000 generated schedules found a visible-output gap; +all 1000 passed on #3405 323f9ad, including exact replay of all 507 failing schedules. diff --git a/packages/signals/tests/semantics/EVIDENCE.md b/packages/signals/tests/semantics/EVIDENCE.md new file mode 100644 index 000000000..f9e4919b1 --- /dev/null +++ b/packages/signals/tests/semantics/EVIDENCE.md @@ -0,0 +1,124 @@ +# Evidence and limits + +Cleanup audit, 2026-09-14. Reduction changes do not change law revision 17 or +Solid's runtime. The checked-in catalog in `rules.ts` carries each law's scope, +authority, checkpoints and control references into artifacts. This document +records evidence quality, not another executable rule language. + +## Semantic law audit + +| Laws | Observable claim and applicability | Passing / deliberately broken evidence | Remaining limit | +| --- | --- | --- | --- | +| A1 | Attached initialized output stays present until replacement/removal. Detached preparation is private. | `output.test.ts`: private writes pass; premature attachment, cleanup and duplicate contribution fail. `attachment.test.ts` exercises the interpreter shape motivated by #3404. | In-memory host model, not a browser DOM or arbitrary portal topology. The fake-host negative controls test the oracle, not mutations of every Solid attachment optimization. | +| S1, S2 | Pure visible data and observation controls agree with witnessed published sources. | `runner.test.ts`, `mounts.test.ts`, `output.test.ts`; `lost-blocker` mutation and stale/obsolete controls. | Missing source observations are not invented. Explicit fallback/covered regions are allowed. | +| S3 | Scoped completion implies publication at the next completed flush. | `settled-writes.test.ts`, `optimistic.test.ts`; callback-started work is a new update. | Including corrective optimistic work remains an experimental contract; no dedicated runtime mutation isolates every optimistic completion path. | +| S4, S5 | Obsolete work does not corrupt settled output; disposed generations receive no later callbacks. | `runner.test.ts`, `mounts.test.ts`, `completion.test.ts`; `stale-result` mutation. | Stateful previous values, stores and streams are not modeled. | +| L1 | Once all finite generated work settles, the final pure view appears. | `completion.test.ts`; `drop-wake` mutation. | Finite generated promises only; a watchdog is inconclusive, never a liveness proof. | +| P1, W1 | Proven ordinary release should occur; uncertain wait timing stays a policy question. | `progress.test.ts`, `completion.test.ts`; `lost-disposal-wake` / `lost-fallback-wake` mutations. Passing held-work and fallback controls; exact historical disposal allowance cannot waive unrelated or final failures. | Sufficient conditions, not complete liveness. No actions/branches/readiness in P1. #3375/#3372 inform controls, not the definition of the ideal law. | +| G1, G2 | Known ordinary groups publish atomically, action holds prevent early commit, unrelated completed groups may progress. | `groups.test.ts`, `group-controls.test.ts`; `entangle-effect` / `drop-action-hold` mutations. | Flat initialized ordinary DAGs with complete anchors. Adjacent microtasks permit entanglement; same event/microtask writes require batching. Possible memo joins conservatively extend deadlines. | +| R2, R3, R4 | A published ready control can be used; context-free pending probes do not throw; a held new answer has tracked pending state. | `readiness.test.ts`, `pending-contract.test.ts`; `false-ready` / `false-verdict` mutations. | R2 uses an explicit modeled click; R3 follows A16, R4 follows A24. No blanket context-free false-implies-readable rule, no user errors generated. | +| R1, E4 | Ordinary outside reads match witnessed publication; allocation-only warming should not change observations. | `reads.test.ts`: passing scalar reads, latest exclusions, exact paired request choices and controlled differing paired results. | Reads through latest remain outside R1; E4 excludes warmups adding async work. No independent runtime mutation dedicated to every E4 violation. | +| E3 | A pure single write's data trace agrees across sync/promise/await, modulo waiting. | `equivalence.test.ts`, `paired-reduce.test.ts`; passing variants and synthetic differing traces. | No action lifetimes, readiness, fallback resets or intervening writes. Pair controls establish oracle/adapter behavior, not broad scheduler equivalence. | +| O1, O2, E5 | Scoped optimistic progress, prompt corrective work, and held single-proposal ordinary equivalence. | `optimistic.test.ts`, `optimistic-scope.test.ts`, `optimistic-equivalence.test.ts`, conditional-scope controls. | Experimental user-directed contracts. One parent, narrow observation/authoritative-write scope; less independent mutation coverage than ordinary laws. Upstream agreement is not assumed. | + +All nine seeded runtime faults remain separate from real findings. Detection proves +that a checker can notice the fault within its test scope. Passing upstream alone +does not prove the oracle complete, and a deliberately corrupted frame tests the +oracle, not the full interpreter. These distinctions are why no law was weakened +or deleted during reduction cleanup. + +## Reduction quality and cost + +The manifest pins worker SHA-256 +`f688dcadb8319ed9db1a9f95bc0520b1d65121154f8810f0f9de5f10da1227ec`, containing +historical runtime `9fa294fb58e6e0ff8003efdd6fcf9a5e6dba3ef3` and oracle revision +17. The comparison tool verifies the exact worker hash and independently replays +each final witness. The executable fixture file retains all 97 inputs and 1,720 +memberships. Eight manually identified targets are an improvement floor, not a +claim of eight bugs or a completeness benchmark. + +Measured with 500 search executions allowed per input: + +| Comparison | Search executions | Exact repro keys | Known simpler routes | +| --- | ---: | ---: | ---: | +| Old reducer, 97 larger originals | 6,701 | 64 | 3/8 | +| Shared engine + sweep scheduling, before new cuts | 5,379 | 64 | 3/8 | +| Cleanup including bounded cuts, 97 larger originals | 6,765 | 59 | 8/8 | +| Old reducer, 97 already-small inputs | 5,377 | 64 | 3/8 | +| Sweep scheduling alone, already-small inputs | 3,967 | 64 | 3/8 | +| Cleanup including bounded cuts, already-small inputs | 5,173 | 59 | 8/8 | + +Each full run additionally used 98 initial and 98 final verification executions. +Earlier experimental logs charged the two initial paired runs to search; the table +corrects that classification. Execution counts are the primary cost evidence. +Single-run wall times, especially overlapping exploratory runs, are not a reliable +throughput speedup claim. The added cuts spend most of the scheduler savings on +five known simplifications, rather than making the whole reducer 26% faster. + +### Family inventory and ablations + +| Families | Purpose / cost | Evidence and disposition | +| --- | --- | --- | +| normalize | Stable names/defaults and exact request translation; cheap | `normalize.test.ts`, `quality.test.ts`; retain as representation/correctness infrastructure. Not independently credited with a bug merge. | +| pruning, anchors, sources, turns, steps, readers, nodes | Remove unused structure and schedule; cheap | `shrink.test.ts`, `reduce.test.ts`, `corpus-reduce.test.ts`; strict reference/request and disposal controls. Retain core deletion vocabulary. | +| optimistic, actions, lifecycle, initialVisibility | Remove unnecessary lifetime/visibility scaffolding | `coordinated-reduce.test.ts`, `paired-reduce.test.ts`. Removing initialVisibility costs C42 an extra operation (78 calls saved across 97 inputs). Retain small rewrite. | +| focused, structural, edges, cones, observation, observationCollapse | Collapse concrete graph/observer units | `focused-reduce.test.ts`, `edge-reduce.test.ts`, `observation-reduce.test.ts`; replay tested, no assumed graph equivalence. Retain; per-family marginal utility is not proven for every overlapping helper. | +| arithmetic, branches, schedule, deliverySchedule, ordering | Simplify expressions and explicit ordering | `algebra-reduce.test.ts`, `reduce.test.ts`, `coordinated-reduce.test.ts`; exact owner/occurrence checks. Resume-action/visibility permutations recover C85. Retain, no arbitrary retargeting. | +| callbackEnd | Remove needless callback scheduling | C61 route is lost when disabled; `paired-reduce.test.ts` negative order controls. Retain. | +| mergeArithmetic | Source substitution plus arithmetic compensation; expensive | C32 route is lost when disabled. Retain in expensive tier. | +| deliveryOrdering | Coupled declaration/delivery normalization; expensive | Full-corpus removal saves 1,797 calls but loses C42's declaration cut, plus some canonical convergence. Retain in expensive tier; not claimed cheap. | +| cuts | Concrete node/async removal with related visible read, offset or delivery | Disabling loses C16, C34, C96 routes. Retain only removal-first bounded combinations; no general pairs/triples search. | +| deliveryEscape | Promise→controlled delivery followed by cheap cleanup | Disabling loses C88. `search.test.ts` rejects a pivot without a substantive final cut; shares execution and candidate bounds. Retain as a bounded transaction. | + +The targeted ablations use nine quality cases; the deliveryOrdering and +initialVisibility checks also cover all 97 larger originals. They establish useful +specific witnesses, not general effectiveness on arbitrary future graphs. No +family is justified merely by its number of accepted edits. Helpers without +independent marginal evidence are explicitly labeled above rather than given +fabricated wins. + +### Decisions against added machinery + +- Native fast-check generation-context shrinking was compared against the external + reducer at 150 executions on ordinary/branch seeds 91500–91504. Nine seeds found semantic failures: external shrinking produced smaller size vectors in all nine, using 796 calls versus 1,150 native calls; the hybrid was never better than external. Keep the external + reducer: native choice shrinking retains too much graph/lifetime machinery on + this generator. `quality-experiments.mjs --experiment native` reproduces the gate; + it does not await individual graph operations or change event semantics. +- CDD was evaluated on the same turn-deletion units, followed by the same cleanup: + 83 semantic inputs, identical final size vectors, 5,606 executions versus 5,566 + for halving. Keep halving. This tests our short schedules, not a general claim + against CDD. The implementation follows [Algorithm 2](https://arxiv.org/html/2408.04735v4), + with p0=.25 and bounded execution. +- Imposing the proposed global complexity order on every edit saved calls but lost + exact convergence on C76, C85 and C96 (61 rather than 59 keys). Keep the existing + ordering guard pending a better-supported replacement. Substantive size descent + is enforced for delivery escapes; a global monotonicity guarantee is not claimed. +- No cross-case result cache, pooling, beam search, solver, graph-rewrite DSL or new + runtime dependency. Original request identities and final uncached verification + matter more than cosmetic convergence. + +The small queue ranks later inputs by machinery and permits limited shape variety; +its choice is never recorded as a root-cause grouping. Original JSONL retention and +selected-input→exact-output mappings remain the way to revisit work after a fix. + +## Campaign/report validation + +The final suite passes 327 tests across 46 files, including all nine runtime fault +calibrations and unmodified-runtime controls. Type checking passes. Six fixed +holdout campaigns ran 900 cases against the checked-out source: 791 pass, 106 +semantic candidates, three policy findings; no invalid, error or limit outcomes. +All 11 selected reductions independently replayed (744 search and 11 final +verification executions). These are untriaged candidate findings, not 106 new bugs. + +A separate eight-candidate selection comparison improved the S1 report from C07 +(size vector 4/5/2/3) to C24 (3/4/2/1), while retaining eight distinct symptoms. +It cost 161 calls versus 142 for first-arrival selection: a clarity improvement, +not a demonstrated search speedup. The queue reserves breadth before a second +shape when full. Original memberships are retained regardless of queue admission. + +Compact measured results are in [quality/results.json](quality/results.json). +Large full outputs and the pre-cleanup reducer snapshot were retained separately +under `/home/gabriel/code/solid-effect-publication-evidence/semantic-fuzzing/cleanup-2026-09-14`. +The historical worker is an external benchmark artifact, not vendored Solid code; +`--check` requires its recorded hash. Portable CI controls test current source and +synthetic violations and do not require that artifact or an old bug to persist. diff --git a/packages/signals/tests/semantics/README.md b/packages/signals/tests/semantics/README.md new file mode 100644 index 000000000..2d98cd873 --- /dev/null +++ b/packages/signals/tests/semantics/README.md @@ -0,0 +1,797 @@ +# Solid 2 semantic fuzzer: an AI handoff + +## For Ryan (and other humans) + +This is an experimental debugging tool, not a proposed permanent testing framework. +You do not need to learn its commands or JSON format. Give your AI this README and +ask it to do the work. Useful requests include: + +- **“Check my fix against the saved cases, then run fresh cases looking for related mistakes.”** +- **“Find the next distinct bug. Verify it on my current branch and give me one simple playground.”** +- **“Model this semantic guarantee across different graph shapes. Ask me about ambiguous cases.”** +- **“Investigate whether this finding is a real bug, a harness mistake, or a rule we should reconsider.”** +- **“Improve shrinking where it produces simpler repros or saves useful investigation time.”** + +The objective is a small stream of actionable issues, not impressive failure counts. +Rules express intended behavior; existing implementation behavior is evidence, not +an automatic definition of correctness. Narrow exceptions are possible without +abandoning stronger checks elsewhere. The AI should explain semantic decisions +before changing them and preserve cases when a fix makes their reductions pass. + +Everything below is context for the AI to continue this work without the original +conversation. The runtime can change, and this tool can change with it. Read the +code, use judgment, and create small local helpers when useful; do not turn this +handoff into a tooling project before using it. + +## Start here, AI + +This directory generates small numeric reactive graphs and schedules, executes +**real Solid signals primitives**, and checks observable semantic rules. It is an +interpreter, not a TypeScript source generator. `fast-check` provides seeded graph +generation; execution, the reference model, work accounting and shrinking are ours. + +The ordinary campaign needs no browser, JSX compilation or jsdom. A lightweight +host-output model also covers private preparation versus visible attachment. +Separate manually written playgrounds establish that a candidate has real UI +consequences. Do not confuse interpreter failures with verified application bugs. + +This handoff replaces reliance on the long development conversation and on personal +scripts. Prefer this README for the workflow, [CONTRACTS.md](CONTRACTS.md) for longer +rationale, [EVIDENCE.md](EVIDENCE.md) for calibration and reduction evidence, and +[the executable catalog](rules.ts) for the current scoped assertions. Documents +under `documentation/plans/semantic-fuzzing*.md` contain history, including rejected +experiments; they are not an instruction to implement every old proposal. + +The branch was rebased onto `next` at `63560a11` on 2026-09-15. It adds test tooling, +not the abandoned optimistic-lane runtime prototype from the earlier investigation. +The executable rule revision is **17** at handoff. Check actual source/issue heads +before giving a new status report; historical results below are explicitly dated. + +### A productive first session + +1. Inspect the checkout and pending user changes. Establish the runtime revision. +2. Run the harness tests/typecheck and a small campaign with an explicit output directory. +3. Replay the relevant saved input, preserving its policy and comparison mode. +4. Examine the first wrong frame and its work ledger, not just the final signature. +5. Reduce, then translate a promising result to ordinary Solid code and verify it. +6. Check related open issues, recent fixes and the actual heads of relevant PRs. +7. Present one issue or one semantic question. Do not flood the maintainer with + dozens of structural variants of a likely common problem. +8. After a fix, replay **originals as well as reductions**, then generate fresh cases. + +When asked to improve the fuzzer, a useful result is better bug detection, a simpler +witness, a better-scoped rule, or less wasted execution. A lower group count alone +is not a success criterion. Do not silently weaken a rule to make a campaign green. + +## Running it + +Use the repository's Node/pnpm versions. The signals package requires Node +`>=22.12.0`; recent manual verification used Node 24.18.0. Install the checkout's +locked dependencies normally (`pnpm install --frozen-lockfile`). `fast-check@4.9.0` +is a signals development dependency, and esbuild is available from the workspace. + +From the repository root: + +```sh +pnpm --filter @solidjs/signals exec vitest run tests/semantics +pnpm --filter @solidjs/signals fuzz:types +pnpm --filter @solidjs/signals fuzz --help + +# A useful ordinary starting run. Use a NEW output directory for each invocation. +pnpm --filter @solidjs/signals fuzz --seed 3289 --cases 1000 --shrink --out /tmp/solid-fuzz-run-1 + +# Replay one reduced artifact, independently of other worker executions. +pnpm --filter @solidjs/signals fuzz --replay /tmp/solid-fuzz-run-1/case-17-min.json --fresh --out /tmp/solid-fuzz-replay-1 + +# Index is the recorded campaign case index, not necessarily a JSONL line number. +pnpm --filter @solidjs/signals fuzz --replay /tmp/solid-fuzz-run-1/findings.jsonl --index 17 --out /tmp/solid-fuzz-replay-2 + +# Investigate one case more deeply, preserving its original failure fingerprint. +pnpm --filter @solidjs/signals fuzz --replay /tmp/solid-fuzz-run-1/case-17-min.json --shrink --shrink-mode focused --budget 500 --out /tmp/solid-fuzz-focused-1 + +# The small named regression corpus, NOT the 97-input reduction-quality fixture. +pnpm --filter @solidjs/signals fuzz --corpus --out /tmp/solid-fuzz-corpus-1 +``` + +`case-17-min.json` is an example path: use an actual selected index from your run's +`reductions.json`. Not every retained finding gets a separate report or reduction. +`--cases` currently accepts at most 10,000 per invocation. Split longer campaigns +into separately recorded seeds/cohorts rather than removing containment limits. + +Without `--out`, the CLI prints a newly created directory under the OS temporary +directory, named `solid-semantic-fuzz--`. Outputs are not saved next +to the tests automatically. An explicit output directory makes follow-up easier. +Reusing a directory can overwrite evidence, including `findings.jsonl`. +On Windows, replace `/tmp/...` with an appropriate local path. + +The CLI bundles the current source into a temporary worker, runs the campaign, +then removes its temporary build. **The worker used by an ordinary invocation is +not a persistent artifact.** If you need one for programmatic comparisons or the +quality tool, build it separately as described below. + +### Reading exit codes and results + +- Exit **0**: successful run under its selected mode. Fault calibration succeeds + when the deliberately introduced fault was detected, not when all cases pass. +- Exit **1**: an ordinary campaign reported a non-pass. Inspect statuses; this does + not by itself establish a Solid regression or a broken CLI. +- Exit **2**: setup/CLI failure, including incompatible calibration anchors. + +Result categories matter: + +| Result | Interpretation | +| -------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `pass` | Applicable checks passed. An explicitly waived progress finding may still be recorded. | +| `fail` | A semantic assertion produced a candidate counterexample. Verify the oracle and scope. | +| `policy` | A waiting behavior exceeded modeled requirements but recovered; this is not a demonstrated permanent hang. | +| `inapplicable` | An equivalence comparison's preconditions do not hold. | +| `invalid` | Bad scenario or an exact recorded request no longer exists in this execution. Not a fix or a bug. | +| `limit` | Resource/watchdog budget exhausted; inconclusive. Could warrant investigation, but not automatic liveness attribution. | +| `error` | Unexpected runtime/harness exception; preserve it and diagnose separately. | + +Worker failure while idle is attributed to the previous case and saved as +`worker-error.json`; it must not contaminate the next case. Failing workers are +retired. `--fresh` uses a new worker for every case; it is useful for checking a +suspected isolation problem but is slower than ordinary reuse. + +### Where the evidence lives + +| Artifact | Use | +| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------- | +| `summary.json` | Target/build/rule metadata, statuses, coverage, timing and execution counts. | +| `findings.jsonl` | Compact retained findings, including cases outside the report/reduction queue. | +| `case-N.json` and associated report files | Detailed examples selected for reporting; inspect directory contents rather than assuming every index exists. | +| `case-N-min.json` | Independently verified reduced scenario, comparison information and reduction accounting. | +| `reductions.json` | Selected original indices, reduced keys and costs. Not a root-cause classification. | +| `worker-error.json` | Previous input when a worker fails after reporting a case. | + +Artifacts record Git HEAD, source/worker hashes, rule revision, seed, generator +version, Node version, compile defines, calibration fault, policy, allowances and +comparison mode. The source hash covers the built sources, so HEAD alone is not +sufficient when the worktree is dirty. A replay against changed sources warns; +that drift is expected when checking a fix, but must be recorded in conclusions. + +**Canonical JSON scenarios, not seeds, are replay authority.** Seeds regenerate +inputs only for the corresponding generator. Keep paired metadata: replaying a +paired artifact requires its recorded `--equivalence`, `--latest-equivalence` or +`--optimistic-equivalence` flag. Discovery shrinking may legitimately convert a +paired finding into a standalone failure and remove that metadata. + +## What to generate + +Start with a small run in a relevant cohort. Expand coverage after understanding +the current failures; a large overnight campaign often produces mostly duplicates. + +| Cohort | Main purpose | +| -------------------------------------------------------- | ------------------------------------------------------------------------------- | +| `ordinary`, `multi` | Pure sync/async DAGs, one or several ordinary sources. | +| `update-groups` | Same-event writes, action lifetimes, overlap and independent update completion. | +| `observation`, `mounts` | Changing readers, publication anchors, owned mounting and disposal. | +| `branches`, `branch-boundaries` | Conditional dependencies and changes to their required work. | +| `boundaries`, `nested` | Retaining/resetting Loading regions and local fallback permission. | +| `attachment` | Private output preparation versus attached/portal-visible output. | +| `reads` | Imperative ordinary/latest/pending probes within event blocks. | +| `latest` | An action's staged source exposed through latest and downstream derivations. | +| `optimistic` | One-parent optimistic proposals and authoritative correction. | +| `readiness`, `derived-readiness`, `optimistic-readiness` | Tracked pending verdicts, guarded reads and derived verdict propagation. | + +```sh +pnpm --filter @solidjs/signals fuzz --cohort branches --seed 91501 --cases 150 --shrink --out /tmp/solid-fuzz-branches-1 +pnpm --filter @solidjs/signals fuzz --cohort attachment --seed 91505 --cases 150 --out /tmp/solid-fuzz-attachment-1 +pnpm --filter @solidjs/signals fuzz --equivalence --seed 3289 --cases 150 --shrink --out /tmp/solid-fuzz-delivery-1 +pnpm --filter @solidjs/signals fuzz --latest-equivalence --cohort latest --seed 3289 --cases 150 --out /tmp/solid-fuzz-latest-1 +pnpm --filter @solidjs/signals fuzz --optimistic-equivalence --seed 3289 --cases 150 --out /tmp/solid-fuzz-optimistic-1 +``` + +The three equivalence modes compare restricted executions, not arbitrary programs: +sync versus async data delivery; early versus lazy latest creation without new +work/observation; and an optimistic proposal versus an ordinary update while its +parent remains held. Read the preconditions in `equivalence.ts`. An unsupported +comparison should be inapplicable, not forced into a false equivalence. + +## The scenario language and its execution + +Read [scenario.ts](scenario.ts) before editing a saved scenario. Version 2 supports +explicit source/anchor sets and more combinations; version 1 remains replayable. +The validator bounds graph size and rejects malformed references. + +- Sources have stable negative IDs; `-1` is the ordinary source. `-2` can be an + independent source, or the optimistic/latest channel when that model is enabled. +- Memos have nonnegative IDs and earlier dependencies. Their pure answer is a + small integer affine computation over captured inputs. A branch selects which + dependency list contributes; the selector participates in request identity. +- `sync` returns directly. `promise` returns an already-resolved promise. + `manual` waits on a recorded resolver. `await` adds a native async continuation + around a controlled answer. These distinctions affect scheduling. +- Readers declare refs and publish through real `createRenderEffect` callbacks. + Optional gates, owner mounts, boundaries, pending probes and host-output regions + alter observation. They are not decorative test options. +- `anchors` and `anchorShow` explicitly create publication observers. Removing one + changes the program. A missing anchor does not authorize reading a hidden getter + and pretending it is published state. +- A turn's `steps` are one uninterrupted synchronous event block. Separate turns + have a native drain between them. Queued callbacks can use microtasks, promise + continuations, async continuations or host tasks. Task operations and cancellation + are recorded rather than inferred from elapsed wall time. +- `start-action`/`resume-action` advance actual action generators through controlled + yield gates. Action-body gates and async memo requests have different ownership. + +This is a small example, suitable for saving as JSON and passing to `--replay`: + +```json +{ + "version": 2, + "sources": [-1], + "anchors": [-1], + "show": true, + "nodes": [{ "id": 0, "deps": [-1], "factor": 1, "offset": 0, "delivery": "manual" }], + "readers": [{ "id": 0, "refs": [0], "gated": false, "boundary": "none" }], + "turns": [ + { "steps": [{ "op": "write", "source": -1, "value": 1 }] }, + { "steps": [{ "op": "resolve", "node": 0, "which": "newest" }] } + ] +} +``` + +It constructs roughly a signal, a delayed identity memo and a visible reader; +sets the source, lets Solid process that event, then resolves the latest request. +The runner also initializes and performs a completion suffix. Generated resolution +selection becomes an exact key in canonical artifacts, e.g. memo + captured inputs + +- occurrence. Once strict replay records that key, do not silently substitute a + new request merely because the graph changed. An invalid replay needs explanation. + +### Native scheduling is part of the test + +We do not replace JavaScript's promise/microtask scheduler. `HostTasks` uses +`MessageChannel` to deliver real host tasks and establish drain barriers. The +continuation of one resolved promise is not proof that all chained microtasks have +run. Conversely, calling `await` after every setter would split a user event and +invalidate the batching test. + +Controlled promises make long logical waits cheap. A thousand generated cases do +not each sleep for the playground's one-second delays. The ledger records every +modeled request before Solid sees its promise, its captured answer and whether it +physically completed. Action gates and queued external callbacks are also recorded. +No asynchronous work in the language should escape that accounting. + +The worker's wall-clock timeout (default 5 seconds per case) is containment for a +stuck synchronous loop, microtask loop or other failure to return. Graph/work/trace +budgets also bound cases. Neither kind of limit is an assertion that an application +request took too many milliseconds. General timer deadlines, equal-deadline races, +intervals and arbitrary application timer code are not modeled yet. + +`flush` is an explicit operation the generator can request. Ordinary event steps do +not inject an implicit flush between writes; setup and teardown explicitly flush. +Do not confuse an immediate-after-flush sample with a sample after a native drain. +That distinction affected a real candidate during this handoff (C09 below). + +### Completion, residual work, and “orphaned memos” + +An async memo still running is not inherently a bug. It may be unobserved, or a +visible Loading fallback may correctly permit its parent update to publish. +The useful question is whether work required by a visible answer has become +unaccounted for, or whether obsolete completion can later corrupt the visible view. + +The completion suffix stops external stimuli, drains generated callbacks, resolves +work required by current desired reader paths, then resolves residual requests +individually. It checks intermediate frames as well as final convergence. This +lets the fuzzer distinguish “still waiting unnecessarily” from “never reaches the +answer even when everything finishes” and “a late completion tears a settled view.” + +Requirements are independently calculated from the declared graph, desired inputs +and reader state. A missed runtime read cannot erase the model's obligation to +show the requested content. Fallback permission belongs to the actual enclosing +boundary and witnessed waiting interval; it is not a global waiver for all async +work. Request registration hooks are diagnostics, not the authority for readiness. + +The model is intentionally incomplete. Some overlap/branch/lifetime scenarios do +not provide enough independent evidence to require a particular earliest commit. +Keep that uncertainty local; final consistency and final completion can still be +checked. A later successful recovery must not erase an earlier torn frame. + +## Semantic intent and current rules + +The original optimistic-lane prototype was abandoned in favor of finding +counterexamples in upstream. Its broad direction remains useful: consistent +publication across ordinary and optimistic updates, actual observation determining +blocking, and parent work not waiting for obsolete optimistic work just to discard +it. Not every part of that ideal model is an accepted upstream contract or a +currently executable invariant. + +Use `ruleContracts` in [rules.ts](rules.ts) for authority, scope and evidence. The +following is a map, not permission to apply each rule outside that scope: + +| Rules | Intended guarantee | +| ---------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| S1, S2 | Published derivations agree with witnessed published inputs; visible/hidden content agrees with published controls. | +| A1 | An attached region keeps its output contribution until a replacement or explicit removal; detached preparation is not already visible. | +| S3 | Scoped write completion must not precede the publication it claims is complete. | +| S4, S5 | Residual work cannot corrupt a settled view; disposed reader generations cannot publish again. | +| L1 | Finite generated work, including residual work, eventually reaches the final pure view. | +| G1, G2 | Proven update groups publish atomically and conservative blockers bound independently provable progress. | +| P1, W1 | Require ordinary release where the model can prove it; keep uncertain timing separately reviewable. | +| R1 | Ordinary imperative reads agree with an actual publication witness. | +| R2, R3, R4 | Guarded ready reads, the context-free pending exception, and held-answer verdicts within their distinct scopes. | +| O1, O2 | Experimental optimistic publication and correction expectations under one parent. | +| E3, E4, E5 | Restricted data-delivery, lazy-latest and ordinary/optimistic equivalences. | + +### Publication, batching and entanglement + +Signals and derived values are stateful. Do not propose speculative reruns of the +whole graph as a harmless repair; `prev`, manually overridden derived state and +user callbacks can make reruns observable. Stateful `prev` is not currently modeled. + +One update is a unit of atomic publication. Independent updates need not commit +together just because an effect reads both. Derivations can require entanglement +because Solid maintains a shared graph: they compute from the newest incoming +information, and overlapping work can prevent either update completing on its own. +Do not make every historical shared memo edge a permanent entanglement rule. + +The current group model tracks definite relationships separately from possible +joins that only extend conservative completion deadlines. Same-source overlap, +action lifetime and writes in the same external event are important. Our working +batching compromise requires batching for writes in the same modeled synchronous +microtask/event block, permits it for adjacent microtasks ahead of a flush, and +does not arbitrarily require independent internal completions to merge. Broader +React comparisons motivated discussion but are not an oracle for Solid. + +A running action is unfinished authoritative work: it may refresh more state later. +Its initial ordinary writes cannot reveal merely because current derivations are +ready. Optimistic publication has independent commit timing; the parent owns when +it is reverted. Deferred state has a different relationship and must not silently +inherit optimistic rules. The modeled optimistic scope remains one parent. + +### Optimism, correction and latest + +The desired direction is to attempt authoritative correction as soon as the +parent's own work permits it, without first waiting for an obsolete optimistic +request. The correction can itself discover observed async work and wait for it. +How new overrides interact with an already-progressing correction is not broadly +settled by this fuzzer; do not invent multi-parent/forked-graph semantics to extend +coverage. O1/O2 and parts of the stronger correction contract are explicitly marked +experimental in the evidence catalog. + +All `latest` reads of the same upstream owner share its companion. Creating +separate upstream memos can create distinct owners. We did not attempt to change +that ownership choice. Lazy companion creation should not change observations in +the allocation-only situations covered by E4, but warming can itself start work; +such a case is not a free initialization-only equivalence. + +The untracked latest contract was disputed during the prototype. A provisional +`readStaged` separation was explored there, but it is **not an API added by this +branch**. #3337 separately explores writes becoming visible at flush. Do not treat +context-free latest reads as ordinary committed-state reads just because that +would make a nicer model. R1 excludes getters/derivations that may read latest; +paired allocation-only E4 and tracked publication checks remain useful. + +### Readiness is contextual + +A tracked `isPending` reader matters to observation; it is not a passive diagnostic. +R2 checks explicit user-style reads when a rendered control says ready. R4 covers +an initialized, continuously observed ordinary answer held away from a distinct +desired answer with enough source witnesses. These are narrower than “false always +means any getter anywhere is ready and current.” + +Context-free `isPending` has an intentional upstream exception: it is nonthrowing, +and initial uninitialized data can produce false (SPEC A16/A19 in the investigated +revision). `untrack` alone does not necessarily remove the surrounding reactive +context. Check `pending-contract.test.ts` before changing these distinctions. +Equal-answer motion is also not automatically a pending-verdict violation. + +We repeatedly found that adding a diagnostic `isPending`, `latest` or data reader +changed the bug. Preserve the original reader set, and treat an added observer as +a separate control rather than invisible instrumentation. + +### Private computation versus visible output + +Render-effect compute work may run before attachment. Creating a detached DOM +subtree and preparing its text can be unobservable while the parent's commit is +held. Requiring every private write to be atomically ordered would report compiler +optimizations as bugs. Conversely, a portal/external target is already visible; +calling that work “mounting” does not make a leaked write private. + +`output.ts` models host attachment independently of reactive ownership. The +attachment cohort constructs compiler-like parent/child shapes with local and +portal targets, including scheduled effects. A1/S1/S2 check visible contributions, +not every internal callback. Root disposal/mount generation identity also matters. +This is a limited abstract host, not a claim to model every DOM/hydration behavior. +#3404 helped calibrate it; that particular bug is not its definition of correctness. + +### Narrow allowances and rule changes + +Default waiting policy is `review`; no historical allowance is enabled by default. +`--waiting-policy required-only|retain-disposed` explores alternate waiting policies. +`--allow legacy-disposal-wait` is a narrow historical exception, not a switch to +turn off final-state or all progress checks. A waived finding is still retained. + +If the maintainer decides a delayed release is acceptable, encode the smallest +applicability predicate justified by that ruling, with a matching permitted case +and a nearby case that must still fail. Keep the ideal expectation and rationale +visible. Do not downgrade a permanent hang to a policy question because a related +late-but-eventually-correct example was accepted. + +Before altering semantics: show a concrete small scenario, identify which prior +rule/test would change, and ask if ambiguous. A counterexample showing that the +proposed model is contradictory is a valuable outcome. Changes to executable laws +or completion policy need a `ruleRevision` bump and corresponding evidence/tests. +Generator/reducer improvements should not quietly change the oracle. + +## Testing a different branch or commit + +There is deliberately no new multi-revision command in this handoff. The CLI tests +the source it imports from its checkout; it has **no `--ref` or `--runtime-dir` flag**. +Create a small helper if needed. Two approaches have been useful: + +1. Put the fuzzer on a disposable checkout of the target. Copy/cherry-pick only the + tooling, install its development dependency if absent, and run from that root. + Preserve local work and verify that no runtime fix accidentally came along. +2. Keep the fuzzer fixed and bundle it against an isolated target source archive. + We used this extensively to replay the same oracle against latest `next` and + pending PRs without repeatedly rebasing the tool. + +For the second approach, resolve the target to a full SHA and archive its +`packages/signals/src`. Use an esbuild resolver to redirect **every** signals source +import from the fuzzer to that archive, resolving relative paths before matching. +Keep imports within the target archive there. Fail the build if an old checkout's +signals source leaks in. Do not mix two Solid module instances or redirect only +`index.ts` while attribution imports still reach the old runtime. The current calibration matcher in `build-paths.mjs` expects `src/core/`. +Older flat layouts need an explicit adapter; do not assume a file-name match alone +proves the intended source was redirected or mutated. + +Bundle `worker.ts` with the normal `__DEV__`, `__OBSERVE__`, `__TEST__` defines all +true. Bundle `Client` and any needed generation/reduction functions from the +unchanged harness. Record target SHA, actual imported source paths, worker hash, +rule revision and options in the helper's report. Standard CLI metadata's Git HEAD +belongs to the process checkout; do not present it as a redirected target's SHA. + +The attribution hook API is a real compatibility dependency. Source layout or +hook changes can require a small adapter; that is a harness compatibility task, +not automatically a semantic failure. If a target cannot run, say so. Calibration +uses exact source-text anchors and should fail loudly when a targeted implementation +changed; never pretend an unapplied fault established checker sensitivity. + +For a persistent worker against **this checkout**, for example: + +```sh +mkdir -p /tmp/solid-fuzz-worker +pnpm --filter @solidjs/signals exec esbuild tests/semantics/worker.ts --bundle --platform=node --format=esm --target=node22 --define:__DEV__=true --define:__OBSERVE__=true --define:__TEST__=true --outfile=/tmp/solid-fuzz-worker/worker.mjs +node packages/signals/tests/semantics/quality.mjs --worker /tmp/solid-fuzz-worker/worker.mjs --inputs original --ids C19,C24,C34,C46,C53 --budget 150 --out /tmp/solid-fuzz-worker/quality.json +``` + +This plain worker build is not the CLI's calibration builder and its hash need not +match a historical worker. `quality.mjs` labels unmatched hashes as a custom target. +Use a new Client/worker instance to independently verify a final witness. Do not +carry a cached “pass” across target, rule, policy, compile-define or scenario changes. + +When comparing PRs, check their **bases as well as heads**. A repro passing on an +older PR does not prove the PR fixes it. We encountered exactly this with #3442: +the reduced JSX passed on #3337 and on #3337's base, but failed on newer `next`. +That is evidence of a later difference, not attribution to the pending change. + +## Saved cases and the current issue queue + +The compact, repository-owned reduction fixture is [quality/corpus.json](quality/corpus.json). +It contains **97 audited inputs**, previous reductions, **1,720 memberships**, eight +known useful reduction routes and fixed holdout seeds. `corpus.ts` is a different, +small named regression set used by `--corpus`. Do not confuse the two. + +To replay a quality fixture through the ordinary CLI, extract its scenario first: + +```sh +node --input-type=module -e 'import fs from "node:fs"; const m=JSON.parse(fs.readFileSync("packages/signals/tests/semantics/quality/corpus.json","utf8")); const c=m.cases.find(c=>c.id==="C53"); fs.writeFileSync("/tmp/solid-fuzz-C53.json",JSON.stringify(c.input,null,2));' +pnpm --filter @solidjs/signals fuzz --replay /tmp/solid-fuzz-C53.json --fresh --out /tmp/solid-fuzz-C53-replay +``` + +Use `.reduced` for the previous minimum. For a fixture with `comparison`, preserve +the corresponding comparison mode; the raw extraction above is suitable for C53, +which is standalone. A raw scenario has no historical metadata: retain provenance +in your comparison report. The fixture's old signature is not a required failure +on a repaired current runtime. + +### Recent manually verified examples (2026-09-15 snapshot) + +The TSX files below are **standalone playground sources**, not automated test files +or generated interpreter output. Paste one into a Solid 2 playground or compile it +against the desired target. They deliberately include their own `render` call. +The manual translation can differ structurally from the fixture; each version was +verified independently. IDs link evidence, not exact program equivalence. + +| Fixture | Playground | Issue and disposition at handoff | +| --------------- | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| C46 | [Held conditional JSX](handoff/held-conditional-view.tsx) | [#3438](https://github.com/solidjs/solid/issues/3438), fixed by merged [#3439](https://github.com/solidjs/solid/pull/3439). Keep as a passing control. | +| C19 / C30 / C35 | [Combined pending probe](handoff/combined-pending.tsx) | [#3442](https://github.com/solidjs/solid/issues/3442). [#3445](https://github.com/solidjs/solid/pull/3445) opened during handoff; not yet verified here. | +| C34 / C68 | [Memoized sum](handoff/stale-sum.tsx) | [#3443](https://github.com/solidjs/solid/issues/3443), open at snapshot. | +| C24 | [Latest inside held Show](handoff/latest-held-show.tsx) | [#3444](https://github.com/solidjs/solid/issues/3444), still reproduced after #3439 merged. | +| C53 | [Cancelled async branch](handoff/cancelled-branch-hidden.tsx) | Draft candidate, not filed at handoff; related to [#3371](https://github.com/solidjs/solid/pull/3371). | +| C02 / C31 / C38 | Extract fixture | Hidden last reader delays source release until its request finishes. Treat as a progress/ruling question, not proven permanent corruption. | + +Specific lessons from this batch: + +- **C46:** `show() ? count() : "hidden"` froze while `count` published and `show` + still displayed true. The memo-wrapped variant was already fixed by #3431; + #3439 fixed the effect variant. A compiler may memoize only the condition, + not the whole ternary. Inspect emitted code when this distinction matters. +- **C19:** adding a combined pending probe made sibling data publish partially. + Removing the synchronous copy of the slow result, making fast synchronous, + probing only one result, or removing the probe avoided it. Adding a raw-slow + pending indicator changed the bug. The final JSX displays data directly; + older variants used imperative reads and had different branch results. +- **C34:** two separate source writes, overlapping async identities and a shared + sum could show A=1, B=0, Sum=0. Plain getter sum remained consistent; same-event + writes avoided it. No readiness probes or optimistic state were needed. +- **C24:** one authoritative source can drive Show's condition while both children + read its latest companion. Outside showed 1 while the still-visible inside + paragraph stayed 0. A JSX ternary instead of Show worked. Do not merge this into + C46 solely because both involve held removal; it survived C46's actual fix. +- **C53:** opening a gated reader, entering an async conditional branch, then + returning to its previously established answer left the panel hidden after all + requests completed. Removing `async` from the conditional, removing its branch, + collapsing to one async memo, or changing the final answer to a new value avoided + it. Replacing the gate with Show produced a blank value rather than literal + `hidden`; that is another manifestation, not a semantic pass. +- **C09:** an immediate-after-`flush()` pending snapshot was false, but substituting + a long request and sampling after a native drain produced true. Do not present + this as a sustained false readiness indicator without resolving that distinction. + +The earlier big replay used #3431 `dee461be` on `next` `25c5064b`: all 79,500 old +generated cases yielded 1,001 semantic failures and 312 policy findings, compared +with 1,627 and 93 in the historical run. Runtime and harness both differed from +the old campaign, so the difference alone is not a regression attribution. +Combining retained failures gave 1,035 exact inputs and 92 reduced repro keys; +47 passed separate #3337, 44 failed, one was invalid. These are **not 44 bugs**, +not current counts after later fixes, and not evidence of a combined PR merge. + +Seven audited originals (C19, C30, C43, C45, C56, C73, C93) still failed on that +runtime even though their earlier minima passed. Conversely, C14's old minimum +failed while its original passed. This is why originals and reductions both stay. + +The full 79,500-case run, bundled old generator/workers and bulky traces were local +investigation artifacts and are **not included**. Some historical documents name +personal paths to them; those are provenance, not prerequisites. Do not claim you +replayed that exact campaign unless those artifacts are actually available. The +97-input fixture and the five playgrounds above are the portable starting point. + +Other retained leads include C10/C25/C42/C69/C74 (new or returning observers), C63 +(conditional held-input recovery), and readiness variants C43/C82/C93. Compare them +with current fixes and the listed issues before filing; the remaining corpus was +not exhaustively mapped to independent root causes. The current human priority is +sharing this tool so it can be used while fixing issues, rather than filing every +remaining candidate first. + +## Manual triage and playground reduction + +Automatic output is a starting point. Identify the first violated observable claim +and ask whether the model has enough witnesses to justify it. Read `frames`, +`requirements`, `work`, `events`, requested/published paths and diagnostic fields. +Distinguish desired values, committed values and speculative computation. A final +matching answer does not absolve an earlier visible mismatch. + +For a candidate worth reporting: + +1. Replay its canonical input on the actual current target, preferably fresh. +2. Reduce with the default budget; increase locally only when a useful simplification + seems close. If a rewrite changes which bug is found, retain both inputs. +3. Translate it to a tiny core reproduction with controlled resolvers. This checks + behavior without the fuzzer's oracle, completion probes or attribution hooks. +4. Try removing sources, memo hops, observers, branches, boundaries, action steps, + async delivery, and scheduling gaps. Change one causal feature at a time and + keep controls. If a seemingly simpler translation fixes it, inspect why. +5. Write a small TSX playground with one button, inline component state and obvious + top-to-bottom data flow. Prefer `async`/`await` where it preserves the behavior; + actions require generator or async-generator functions in this API. +6. Verify the **exact final code**, ideally with compiled JSX and a real click. + Check development and production when relevant. Do not claim browser testing + when you used jsdom, or reuse a result from an earlier non-equivalent reduction. +7. Check issue history and PR heads. Draft a terse report with reproduction steps, + observed/expected behavior, target SHA and meaningful controls. Leave posting + to the user unless explicitly authorized. + +Use delays around 500ms–1000ms for readable progression. Let initial values settle +rather than adding `if (value === 0)` timing shortcuts unless that condition is +actually the bug. A shared helper is fine: + +```ts +const delay = (ms: number, value?: T) => new Promise(r => setTimeout(r, ms, value)); +``` + +For a single delay, inline `await new Promise(r => setTimeout(r, 500))` is fine. +Avoid generic run/mode controllers, IIFEs without a behavioral reason, imperative +`render` calls in click handlers and extra disabled-button state. A disabled button +is appropriate when the readiness contract itself is the subject. Prefer showing +comparisons together when useful, but avoid plumbing that obscures the graph. + +Preserve important distinctions during reduction: + +- A getter, memo, effect compute and effect run have different tracking/commit roles. +- Separate JSX expressions are separate readers; a template string combines reads. +- Show, inline ternary and manually nested render effects need not create the same + ownership graph. Replacing one is an experimental control, not guaranteed cleanup. +- An async function can turn a synchronous NotReady throw into a rejected promise. + `Promise.resolve(read())` evaluates `read` synchronously; `async () => read()` does + not have identical suspension timing. Test the actual simplification. +- An extra displayed source/pending flag is an observer and can create a hold. +- Changing 0→1→0 to 0→1→2 removes equality recovery; do not do it for aesthetics. +- Two adjacent microtasks are not necessarily one event, and `await` is not `flush`. +- A late correct result is a progress question; a wrong stable view after all work + settles is a correctness/liveness failure, not merely a missed optimization. + +### How we verified JSX without relying on a browser tool + +We used an isolated source archive with signals, solid and web source, including +web's server-functions support needed by its exports. Babel's Solid plugin plus +the TypeScript preset compiled the TSX; esbuild bundled the target's `solid-js`, +`@solidjs/signals` and `@solidjs/web` source aliases. jsdom supplied document/window, +DOM constructors and MutationObserver. A native button click started the example; +real timers separated observations of paragraph text. + +For development/production, mirror the package's build replacements. In particular +web's string tokens **`"_SOLID_DEV_"` and `"_SOLID_OBSERVE_"`** must both be replaced, +in addition to signals' `__DEV__`, `__OBSERVE__`, `__TEST__` defines. We once forgot +the observe token in a production helper; event dispatch accessed nonexistent +attribution and the click never ran. That was a helper error, not a Solid finding. +Record errors and assert the expected intermediate/final states rather than only +printing snapshots. Root-mount diagnostics about no Loading can be expected in +these minimal examples; a runtime exception is a different matter. + +These verification helpers are not a supported bundled framework. The AI can +recreate them when useful. Use the current JSX compiler/build setup and inspect +its output if ownership differs. Keep the core campaign free of DOM compilation. + +## Improving rules, generation and shrinking + +### Find the right layer first + +| Files | Responsibility | +| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| `scenario.ts`, `model.ts` | Replay language, pure numeric answers and references. | +| `generate.ts` | Cohorts and supported structural/scheduling combinations. | +| `runner.ts` | Real graph construction, operation execution, publication/work ledger and completion. | +| `host.ts`, `client.ts`, `worker.ts` | Native tasks, isolation, containment and transport. | +| `rules.ts`, `groups.ts`, `progress.ts`, `footprints.ts`, `policy.ts` | Observable assertions, scoped requirements and explicit waiting policies. | +| `output.ts`, `tree.ts` | Visible attachment/region model and nested host output. | +| `admission.ts`, `search.ts`, `shrink.ts`, `equivalence.ts` | Acceptance predicates, shared search and ordinary/paired adapters. | +| `candidates.ts`, `reduce.ts`, `order.ts`, `normalize.ts` | Concrete graph/schedule rewrites and ordering. | +| `selection.ts`, `complexity.ts`, `cli.ts`, `cli.mjs` | Report queue, costs, artifacts, build and execution entry points. | +| `quality.mjs`, `quality-experiments.mjs`, `quality/` | Retained reduction evidence and optional comparisons. | + +A finding may reveal a runner bug, a pure-model error, a law's excessive scope, +missing generation coverage, ineffective shrinking, or Solid behavior. Determine +which before patching. Do not make the pure model depend on Solid's internal lane +IDs or flags merely to match the current implementation. Internal hooks can help +explain a result without deciding whether it is legal. + +For a new rule, write a behavioral statement, applicability predicate, observable +witness and authority/rationale. Add a passing near-neighbor and a deliberately +wrong case it detects. State unsupported combinations. For a new generator shape, +keep synchronous event execution uninterrupted, request identity exact, and every +new external operation/work source in the ledger. Stores, stateful `prev`, streams, +general timers, multi-parent optimistic lifetimes and full DOM semantics remain +future work requiring their own contracts. + +### What the shrinker does and does not promise + +All findings stay in the JSONL. The first 20 symptoms get detailed reports. With +`--shrink`, a bounded candidate queue selects up to eight cases, allowing up to two +structural shapes per symptom when space permits and preferring simpler candidates. +Distinct symptoms take priority over extra shapes. This bounds attention and cost; +it is not root-cause deduplication. Replay/calibration reduces explicit selections. + +Default discovery mode accepts any independently demonstrated semantic failure, +including a different one uncovered while reducing. Focused mode preserves the +fingerprint. Invalid, inapplicable, waived, policy-only, error or limit outcomes +cannot replace a semantic failure. Policy/diagnostic inputs retain narrower +predicates. Paired discovery can hand off to an independently failing single side. + +The shared search runs productive passes, then revisits earlier passes on another +sweep. Cheap deletion/normalization precedes bounded structural cuts and delivery +escapes. Every behavioral rewrite is replay-tested. Representation normalization +must not quietly reorder dependency reads, declarations or events that affect +semantics. “These graphs look alike” is not proof that creation order is irrelevant. + +`--budget 150` counts actual search interpreter executions per chosen candidate. +A paired trial costs multiple executions; its controls count too. Initial replay +and mandatory fresh final verification are separate reported costs. Candidate +enumeration has its own bound. A budget-limited witness is not globally minimal. +The last independently valid result is retained when the budget runs out. + +A fuzzer does not need to sort all failures into a perfect handful of root causes +before it is useful. Once you have one small actionable report, fixing it and +rerunning the originals/fresh generator often removes many variants more cheaply +than exhaustive clustering. Keep unresolved cases; don't discard them based on a +shared symptom or because they reduced to another known issue. + +### Lessons from the reduction experiments + +We spent substantial effort on apparent duplicate minima. Some work was useful; +a lot of increasingly elaborate escape searches had poor marginal returns. The +retained implementation has evidence, but is not a claim of optimal shrinking. + +- Cheap deletion, pruning, consistent substitutions and exact request renaming are + fundamental even when each doesn't eliminate a unique duplicate by itself. +- Structural cuts can remove machinery that isolated field deletion cannot: a + source plus offsets, a mount plus dependent operations, a memo hop plus related + reads. Admit the completed program through replay; do not demand intermediate + partial rewrites preserve the failure. +- Keep expensive rewrites bounded and justify them with a useful witness that the + cheaper passes fail to reach. A test that only proves a candidate was emitted + does not establish value. +- Persistent sweeps reduced restart waste. A universal new monotonic rank was + tested and rejected because it lost useful convergence; don't assume an old plan + asking for it describes today's implementation. +- Generic request retargeting, broad passing-state/full-shrink pivots and repeated + alternate full pass orders were expensive for little benefit. Don't reintroduce + them based on names alone. +- CDD and native fast-check shrinking were evaluated, not adopted as defaults. + Optional experiments remain available. This doesn't mean prior art is useless; + it means this graph/request/observation language needs evidence for adaptation. +- Performance preference: small integers, predictable records, straightforward + loops and few unnecessary copies. Avoid repeated filter/map/spread chains in hot + paths. Do not add pooling, complex caches or a new framework without a measured + hotspot and a demonstrated win. + +See [EVIDENCE.md](EVIDENCE.md) for the family inventory and measured limits. The +historical 97-case fixture was heavily reused; evaluate new ideas on fresh seeds +and the fixed holdout too. Compare equal execution budgets with the same oracle +and runtime. Separate generation, execution/checking, reduction and serialization +costs; concurrent runs are not clean throughput comparisons. + +### Calibration and quality checks + +```sh +# Ordinary controls, then deliberately remove an expected wake in a disposable bundle. +pnpm --filter @solidjs/signals fuzz --calibrate --out /tmp/solid-fuzz-calibration-clean +pnpm --filter @solidjs/signals fuzz --calibrate --fault drop-wake --out /tmp/solid-fuzz-calibration-fault + +# These require a separately built worker (see above). +node packages/signals/tests/semantics/quality.mjs --worker /tmp/solid-fuzz-worker/worker.mjs --inputs reduced --budget 150 --out /tmp/solid-fuzz-quality-current.json +node packages/signals/tests/semantics/quality.mjs --worker /tmp/solid-fuzz-worker/worker.mjs --inputs reduced --disable cuts --budget 150 --out /tmp/solid-fuzz-quality-without-cuts.json +node packages/signals/tests/semantics/quality-experiments.mjs --worker /tmp/solid-fuzz-worker/worker.mjs --experiment native --out /tmp/solid-fuzz-quality-native.json +node packages/signals/tests/semantics/quality-experiments.mjs --worker /tmp/solid-fuzz-worker/worker.mjs --experiment chunks --out /tmp/solid-fuzz-quality-chunks.json +``` + +`quality.mjs` supports `--ids`, `--baseline` (a bundled earlier reducer), `--schedule +restart|sweep`, and `--check`. The latter asserts historical quality routes only +with the pinned historical worker hash and selected route coverage. Do not use it +to demand that fixed upstream bugs keep failing. Historical `quality/results.json` +is evidence from its recorded target, not an up-to-date current-runtime score. + +A successful fault calibration establishes sensitivity to the seeded fault in its +scope. It does not prove every upstream finding is a bug, or that all laws have +independent mutation coverage. `EVIDENCE.md` distinguishes those claims. Anchors can +drift when upstream internals change; diagnose and update the mutation deliberately. + +## What to leave for the next session + +Keep a small durable note alongside campaign outputs with target SHAs, commands, +artifact paths, rule revision, candidate-to-issue mappings, confirmed controls and +remaining questions. Include whether verification was core-only, compiled JSX in +jsdom or a real browser. Record why a candidate was grouped, deferred or rejected. +Don't make the next AI reconstruct those decisions from thousands of tool messages. + +Do not promise perfect coverage, global minima, a unique issue count, or a formal +proof that orphaned work is impossible. The practical objective is a trustworthy, +explainable counterexample loop that helps the maintainer improve Solid and helps +us improve the semantic model. This iteration can remain experimental while being +useful for that work. + +## Handoff validation (2026-09-15) + +On the rebased source based on `next` `63560a11`: + +- Frozen-lockfile installation succeeded. +- All 327 semantic harness tests passed; `fuzz:types` passed. +- The README's small JSON scenario replayed successfully in a fresh worker. +- A 1,000-case ordinary campaign (`--seed 3289 --shrink`) completed with 966 passes, + 22 semantic candidates and 12 policy findings; four candidates were reduced. + No worker errors, invalid cases or limits were reported. This is an execution + check, not a claim of 22 new bugs or a clean runtime. +- `--calibrate --fault drop-wake` detected the injected fault successfully. +- The documented persistent-worker build and selected-original quality command + ran successfully; C46 passed and the other four selected inputs still failed. +- The saved C53 original reproduced in a fresh CLI replay. + +These checks validate this handoff's operating path. They are not a full new +79,500-case campaign or verification of the still-open #3445 fix. Temporary run +outputs are not part of the shareable repository; rerun the documented commands +for current evidence rather than relying on this snapshot. diff --git a/packages/signals/tests/semantics/admission.ts b/packages/signals/tests/semantics/admission.ts new file mode 100644 index 000000000..874feceb4 --- /dev/null +++ b/packages/signals/tests/semantics/admission.ts @@ -0,0 +1,38 @@ +import type { RunResult } from "./runner.js"; +import { waitingSignature } from "./policy.js"; +import { rules } from "./rules.js"; + +const semanticRules = new Set(rules.map(rule => rule.id)); + +/** Discovery may find a different bug, but not an invalid run or a waived wait. */ +export function isSemanticFailure(result: RunResult): boolean { + if ( + result.status !== "fail" || + !result.failure || + result.error || + result.cleanupError || + !semanticRules.has(result.failure.rule) + ) + return false; + if (result.failure.rule === "W1") return result.waiting?.disposition === "violation"; + if (result.failure.rule === "P1" && result.progress?.length) + return result.progress.some(finding => !finding.allowance); + return true; +} + +export function fingerprint(result: RunResult): string | undefined { + if (result.status === "error" && result.error) return `runtime: ${result.error.split("\n")[0]}`; + if (result.status === "fail" && result.failure) + return `${result.failure.rule}: ${result.failure.message}${ + result.failure.rule === "G2" + ? `: ${(result.failure.expected as { relation?: string })?.relation}` + : result.failure.rule === "W1" && result.waiting + ? `: ${waitingSignature(result.waiting)}` + : "" + }`; + const progress = result.progress?.[0]; + if ((result.status === "pass" || result.status === "policy") && progress) + return `P1: ${progress.failure.message}: allowance=${progress.allowance ?? "none"}`; + if ((result.status === "policy" || result.status === "pass") && result.waiting) + return waitingSignature(result.waiting); +} diff --git a/packages/signals/tests/semantics/algebra-reduce.test.ts b/packages/signals/tests/semantics/algebra-reduce.test.ts new file mode 100644 index 000000000..68800c343 --- /dev/null +++ b/packages/signals/tests/semantics/algebra-reduce.test.ts @@ -0,0 +1,179 @@ +import { chain } from "./fixtures.js"; +import { focusedReductions, identityValues } from "./reduce.js"; +import { reductions, fingerprint } from "./shrink.js"; +import { normalize } from "./normalize.js"; +import { runScenario } from "./runner.js"; +import { evaluate, validate, type Scenario } from "./scenario.js"; + +function identity(): Scenario { + const s = chain(); + for (const n of s.nodes) { + n.factor = 1; + n.offset = 0; + } + s.turns = [ + { steps: [{ op: "write", value: 7 }] }, + { steps: [{ op: "resolve", node: 0, which: "newest", key: "0:[7]#1" }] }, + { steps: [{ op: "write", value: 9 }] }, + { steps: [{ op: "resolve", node: 0, which: "newest", key: "0:[9]#1" }] }, + { steps: [{ op: "write", value: 7 }] }, + { steps: [{ op: "resolve", node: 0, which: "newest", key: "0:[7]#2" }] } + ]; + s.strict = true; + return s; +} + +test("identity renaming preserves equality, exact request occurrences and published values", async () => { + const s = identity(), + before = structuredClone(s), + c = identityValues(s)!; + expect(validate(c)).toBeUndefined(); + expect(c.turns[5]).toEqual({ + steps: [{ op: "resolve", node: 0, which: "newest", key: "0:[1]#2" }] + }); + const a = await runScenario(s), + b = await runScenario(c); + expect(a.status).toBe("pass"); + expect(b.status).toBe("pass"); + expect(b.frames.length).toBe(a.frames.length); + const rename = (n: number) => (n === 7 ? 1 : n === 9 ? 2 : n); + for (let i = 0; i < a.frames.length; i++) { + expect(b.frames[i].at).toBe(a.frames[i].at); + expect(b.frames[i].input).toBe(rename(a.frames[i].input)); + for (const reader in a.frames[i].outputs) { + const value = a.frames[i].outputs[reader]; + expect(b.frames[i].outputs[reader]).toEqual(Array.isArray(value) ? value.map(rename) : value); + } + } + expect(identityValues(c)).toBeUndefined(); + expect(s).toEqual(before); +}); + +test("identity renaming handles optimistic proposals and paired keys with the same bijection", () => { + const s = identity(); + s.version = 2; + s.sources = [-1, -2]; + s.optimistic = { proposals: [2, 1], authoritative: 0 }; + s.nodes[0].deps = [-2]; + s.turns = [ + { steps: [{ op: "start-action" }] }, + { + steps: [ + { + op: "resolve", + node: 0, + which: "oldest", + key: "0:[2]#1", + variantKeys: ["0:[1]#2", null] + } + ] + } + ]; + const c = identityValues(s)!; + expect(c.optimistic).toEqual({ proposals: [1, 2], authoritative: 0 }); + expect(c.turns[1]).toEqual({ + steps: [ + { op: "resolve", node: 0, which: "oldest", key: "0:[1]#1", variantKeys: ["0:[2]#2", null] } + ] + }); +}); + +test("identity renaming declines arithmetic, branches, sums, negative zero and unknown questions", () => { + const variants = [identity(), identity(), identity(), identity()]; + variants[0].nodes[0].offset = 1; + variants[1].nodes[0].branch = { condition: -1, otherwise: [-1] }; + variants[2].nodes[0].deps = [-1, -1]; + variants[3].turns.unshift({ steps: [{ op: "write", value: -0 }] }); + for (const s of variants) expect(identityValues(s)).toBeUndefined(); + for (const key of [ + "0:[99]#1", + "00:[7]#1", + "1:[7]#1", + "0:[7,9]#1", + "0:[7.0]#1", + "0:[x]#1", + "0:[7]#01" + ]) { + const s = identity(); + s.turns[1] = { steps: [{ op: "resolve", node: 0, which: "newest", key }] }; + expect(identityValues(s), key).toBeUndefined(); + } +}); + +test("affine composition retains answers while bypassing a synchronous dependency", async () => { + const s = chain("sync"); + s.nodes[1].factor = 3; + s.nodes[1].offset = 4; + s.nodes[1].delivery = "promise"; + const c = [...focusedReductions(s)].find(c => c.nodes[1].deps[0] === -1)!; + expect(c.nodes[1]).toEqual({ id: 1, deps: [-1], factor: 6, offset: 7, delivery: "promise" }); + for (const input of [0, 1, 2]) expect(evaluate(c, input).get(1)).toBe(evaluate(s, input).get(1)); + const a = await runScenario(s), + b = await runScenario(c); + expect(a.status).toBe("pass"); + expect(b.frames).toEqual(a.frames); + s.nodes[0].delivery = "manual"; + expect([...focusedReductions(s)].some(c => c.nodes[1].deps[0] === -1)).toBe(false); +}); + +test("a leading proposal and its resume can be removed together", () => { + const s = identity(); + s.version = 2; + s.sources = [-1, -2]; + s.optimistic = { proposals: [0, 1, 3], authoritative: 0 }; + s.nodes[0].deps = [-2]; + s.turns = [ + { steps: [{ op: "start-action" }] }, + { steps: [{ op: "resume-action" }] }, + { steps: [{ op: "resolve", node: 0, which: "newest", key: "0:[1]#1" }] } + ]; + const c = [...focusedReductions(s)].find(c => c.optimistic?.proposals.length === 2)!; + expect(c.optimistic?.proposals).toEqual([1, 3]); + expect(c.turns[0]).toEqual(s.turns[0]); + expect(c.turns[1]).toEqual({ steps: [{ op: "noop" }] }); + expect(c.turns[2]).toEqual(s.turns[2]); +}); + +test("default observers remain reducible after syntax normalization", () => { + const s: Scenario = { ...chain(), version: 2, sources: [-1], anchors: [-1], anchorShow: true }; + for (const input of [s, normalize(s)]) { + const cs = [...reductions(input)]; + expect(cs.some(c => c.anchors?.length === 0)).toBe(true); + expect(cs.some(c => c.anchorShow === false)).toBe(true); + } +}); + +test("moving a first unmount into initial state retains the later remount", () => { + const s: Scenario = { + ...chain(), + version: 2, + sources: [-1], + turns: [ + { steps: [{ op: "write", value: 1 }] }, + { steps: [{ op: "mount", reader: 0, value: false }] }, + { steps: [{ op: "mount", reader: 0, value: true }] } + ] + }; + s.readers[0].mounted = true; + const c = [...focusedReductions(s)].find(c => c.readers[0].mounted === false)!; + expect(c.turns[0]).toEqual(s.turns[0]); + expect(c.turns[1]).toEqual({ steps: [{ op: "noop" }] }); + expect(c.turns[2]).toEqual(s.turns[2]); +}); + +test("format upgrade excludes the legacy disposal allowance scope", async () => { + const s = chain(); + expect([...focusedReductions(s)].some(c => c.version === 2)).toBe(false); + s.readers[0].gated = true; + s.turns.push({ steps: [{ op: "show", value: false }] }); + const c = [...focusedReductions(s)].find(c => c.version === 2)!; + expect(c.sources).toEqual([-1]); + const a = await runScenario(s), + b = await runScenario(c); + expect(fingerprint(a)).toBeDefined(); + expect(fingerprint(b)).toBe(fingerprint(a)); + for (let i = 0; i < a.frames.length; i++) { + expect(b.frames[i].input).toBe(a.frames[i].input); + expect(b.frames[i].outputs).toEqual(a.frames[i].outputs); + } +}); diff --git a/packages/signals/tests/semantics/attachment.test.ts b/packages/signals/tests/semantics/attachment.test.ts new file mode 100644 index 000000000..edbe52974 --- /dev/null +++ b/packages/signals/tests/semantics/attachment.test.ts @@ -0,0 +1,91 @@ +import { generate } from "./generate.js"; +import { runScenario, type RunResult } from "./runner.js"; +import { validate, type Scenario } from "./scenario.js"; +import { fingerprint, shrink } from "./shrink.js"; + +function chain(): Scenario { + return { + version: 2, + sources: [-1], + show: true, + nodes: [ + { id: 0, deps: [-1], factor: 1, offset: 0, delivery: "manual" }, + { id: 1, deps: [0], factor: 1, offset: 0, delivery: "manual" } + ], + readers: [ + { + id: 0, + refs: [1], + gated: false, + boundary: "none", + render: { on: 0, target: "portal", scheduled: true } + } + ], + turns: [ + { steps: [{ op: "write", value: 1 }] }, + { steps: [{ op: "resolve", node: 0, which: "newest" }] }, + { steps: [{ op: "resolve", node: 1, which: "newest" }] } + ] + }; +} + +test("attachment replay rejects unsupported renderer patterns", () => { + const s = chain(); + expect(validate(s)).toBeUndefined(); + s.readers[0].render!.scheduled = false; + expect(validate(s)).toContain("scheduled portals"); + s.readers[0].render!.scheduled = true; + s.readers[0].render!.on = 2; + expect(validate(s)).toBeDefined(); +}); + +test("#3404 shape measures output removal, and remains replayable after the runtime fixes it", async () => { + const result = await runScenario(chain()); + expect(["pass", "fail"], result.error).toContain(result.status); + expect(result.attachment!.checks).toBeGreaterThan(0); + expect(result.frames[0].outputs[0]).toEqual([0]); + expect(result.frames.at(-1)!.outputs[0]).toEqual([1]); + if (result.failure) { + expect(result.failure.rule).toBe("A1"); + expect(result.failure.message).toBe("Visible output disappeared before replacement or removal"); + expect(result.failure.frame!.outputs[0]).toBe("absent"); + const reduced = await shrink(result, runScenario, 40); + expect(fingerprint(reduced.result)).toBe(fingerprint(result)); + expect(reduced.result.scenario.readers.some(r => r.render)).toBe(true); + expect((await runScenario(reduced.result.scenario)).failure).toEqual(reduced.result.failure); + } + const replay = await runScenario(result.scenario); + expect(replay.frames).toEqual(result.frames); + expect(replay.failure).toEqual(result.failure); +}); + +test("synchronous detached preparation and explicit removal preserve visible output", async () => { + for (const target of ["local", "portal"] as const) { + const s = chain(); + for (const node of s.nodes) node.delivery = "sync"; + s.readers[0].render!.target = target; + s.readers[0].render!.scheduled = target === "portal"; + s.turns = [{ steps: [{ op: "write", value: 1 }] }, { steps: [{ op: "dispose", reader: 0 }] }]; + const result = await runScenario(s); + expect(result.status, JSON.stringify(result)).toBe("pass"); + expect(result.frames.at(-1)!.outputs[0]).toBe("absent"); + } +}); + +test("attachment generation is valid, reproducible and independent of cleanup order", async () => { + const cases = generate(3404, 30, "attachment"); + const results: RunResult[] = []; + expect(generate(3404, 30, "attachment")).toEqual(cases); + for (const s of cases) { + expect(validate(s)).toBeUndefined(); + const result = await runScenario(s); + expect(["pass", "fail"], result.error).toContain(result.status); + results.push(result); + } + for (let i = results.length - 1; i >= 0; i--) { + const result = results[i]; + const replay = await runScenario(result.scenario); + expect(replay.frames).toEqual(result.frames); + expect(fingerprint(replay)).toEqual(fingerprint(result)); + } +}); diff --git a/packages/signals/tests/semantics/boundaries.test.ts b/packages/signals/tests/semantics/boundaries.test.ts new file mode 100644 index 000000000..e0f5f803b --- /dev/null +++ b/packages/signals/tests/semantics/boundaries.test.ts @@ -0,0 +1,150 @@ +import { runScenario } from "./runner.js"; +import { checkFrame, type Frame } from "./rules.js"; +import { generate } from "./generate.js"; +import { validate, type Scenario } from "./scenario.js"; +import { reductions } from "./shrink.js"; +import { withWorker } from "./worker-fixture.js"; + +function disjoint(): Scenario { + return { + version: 2, + sources: [-1, -2], + show: true, + nodes: [ + { id: 0, deps: [-1], factor: 1, offset: 0, delivery: "manual" }, + { id: 1, deps: [-2], factor: 1, offset: 0, delivery: "manual" } + ], + readers: [ + { id: 0, refs: [0], gated: false, boundary: "reset" }, + { id: 1, refs: [1], gated: false, boundary: "reset" } + ], + turns: [ + { + steps: [ + { op: "write", source: -1, value: 1 }, + { op: "write", source: -2, value: 1 } + ] + }, + { steps: [{ op: "resolve", node: 0, which: "newest" }] }, + { steps: [{ op: "resolve", node: 1, which: "newest" }] } + ] + }; +} + +test("separate Loading regions publish fallbacks and resolve independently", async () => { + const result = await runScenario(disjoint()); + expect(result.status, JSON.stringify(result)).toBe("pass"); + expect(result.coverage).toContain("multiple-published-fallbacks"); + expect(result.coverage).toContain("fallback-beside-content"); + expect( + result.frames.some(f => f.outputs[1] === "loading" && JSON.stringify(f.outputs[0]) === "[1]") + ).toBe(true); + expect((await runScenario(result.scenario)).frames).toEqual(result.frames); +}); + +test("a fallback never excuses another region's incoherent content", () => { + const s = disjoint(); + const frame: Frame = { + at: "control", + input: 1, + inputs: { [-1]: 1, [-2]: 1 }, + show: true, + outputs: { 0: "loading", 1: [1] } + }; + expect(checkFrame(s, frame)).toBeUndefined(); + frame.outputs[1] = [0]; + expect(checkFrame(s, frame)).toMatchObject({ rule: "S1", reader: 1 }); +}); + +test("boundary removal is a valid structural reduction", () => { + expect( + [...reductions(disjoint())].some( + s => !validate(s) && s.readers.length === 2 && s.readers[0].boundary === "none" + ) + ).toBe(true); +}); + +test("generated boundary regions retain canonical replay", async () => { + await withWorker(async client => { + for (const s of generate(3347, 50, "boundaries")) { + expect(validate(s)).toBeUndefined(); + const a = await client.run(s); + const b = await client.run(a.scenario); + expect(["pass", "fail", "policy", "error"], JSON.stringify(a)).toContain(a.status); + expect(b.status).toBe(a.status); + expect(b.frames).toEqual(a.frames); + expect(b.requirements).toEqual(a.requirements); + } + }); +}); + +test("an inner boundary can load while its outer region keeps showing content", async () => { + const s = disjoint(); + s.readers[1].parent = 0; + s.turns = [ + { + steps: [ + { op: "write", source: -1, value: 1 }, + { op: "write", source: -2, value: 1 } + ] + }, + { steps: [{ op: "resolve", node: 0, which: "newest" }] }, + { steps: [{ op: "resolve", node: 1, which: "newest" }] } + ]; + const r = await runScenario(s); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect(r.coverage).toContain("nested-content-covered"); + expect(r.coverage).toContain("inner-fallback-beside-parent-content"); + expect(r.frames.some(f => f.outputs[0] === "loading" && f.outputs[1] === "covered")).toBe(true); + expect((await runScenario(r.scenario)).frames).toEqual(r.frames); +}); + +test("an unbounded child sends its loading to the outer boundary", async () => { + const s = disjoint(); + s.readers[1].parent = 0; + s.readers[1].boundary = "none"; + const r = await runScenario(s); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect(r.coverage).toContain("nested-content-covered"); + expect(r.coverage).not.toContain("inner-fallback-beside-parent-content"); + expect(r.frames.at(-1)?.outputs).toEqual({ 0: [1], 1: [1] }); +}); + +test("nested-region checks distinguish covered content from a stale visible child", () => { + const s = disjoint(); + s.readers[1].parent = 0; + const frame: Frame = { + at: "control", + input: 1, + inputs: { [-1]: 1, [-2]: 1 }, + show: true, + outputs: { 0: "loading", 1: "covered" } + }; + expect(checkFrame(s, frame)).toBeUndefined(); + frame.outputs[0] = [1]; + expect(checkFrame(s, frame)).toMatchObject({ rule: "S2", reader: 1 }); + frame.outputs[1] = [0]; + expect(checkFrame(s, frame)).toMatchObject({ rule: "S1", reader: 1 }); +}); + +test("nested generation and flattening reduction preserve exact replay", async () => { + await withWorker(async client => { + for (const s of generate(3347, 60, "nested")) { + expect(validate(s)).toBeUndefined(); + expect( + [...reductions(s)].some( + r => + !validate(r) && + r.readers.length === s.readers.length && + r.readers.some((reader, i) => reader.parent !== s.readers[i].parent) + ) + ).toBe(true); + const a = await client.run(s); + const b = await client.run(a.scenario); + expect(["pass", "fail", "policy", "error"], JSON.stringify(a)).toContain(a.status); + expect(b.status).toBe(a.status); + expect(b.frames).toEqual(a.frames); + expect(b.requirements).toEqual(a.requirements); + } + }); +}); diff --git a/packages/signals/tests/semantics/branches.test.ts b/packages/signals/tests/semantics/branches.test.ts new file mode 100644 index 000000000..5fadde7bb --- /dev/null +++ b/packages/signals/tests/semantics/branches.test.ts @@ -0,0 +1,72 @@ +import { generate } from "./generate.js"; +import { runScenario } from "./runner.js"; +import { ancestors, evaluate, validate, type Scenario } from "./scenario.js"; +import { reductions } from "./shrink.js"; + +function branching(): Scenario { + return { + version: 2, + sources: [-1, -2], + show: true, + nodes: [ + { id: 0, deps: [-1], factor: 2, offset: 0, delivery: "sync" }, + { id: 1, deps: [-2], factor: 1, offset: 10, delivery: "sync" }, + { + id: 2, + deps: [0], + branch: { condition: -2, otherwise: [1] }, + factor: 1, + offset: 0, + delivery: "sync" + } + ], + readers: [{ id: 0, refs: [-1, -2, 2], gated: false, boundary: "none" }], + turns: [ + { steps: [{ op: "write", source: -1, value: 2 }] }, + { steps: [{ op: "write", source: -2, value: 1 }] }, + { steps: [{ op: "write", source: -2, value: 0 }] } + ] + }; +} + +test("pure branch requirements include the selector and only the chosen path", () => { + const s = branching(); + const off = evaluate(s, 2, { [-1]: 2, [-2]: 0 }); + const on = evaluate(s, 2, { [-1]: 2, [-2]: 1 }); + expect(off.get(2)).toBe(10); + expect(on.get(2)).toBe(4); + expect(ancestors(s, [2], off)).toEqual(new Set([2, -2, 1])); + expect(ancestors(s, [2], on)).toEqual(new Set([2, -2, 0, -1])); +}); + +test("the runtime follows both branches and can switch back", async () => { + const r = await runScenario(branching()); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect(r.frames.findLast(f => f.at === "turn 0")?.outputs[0]).toEqual([2, 0, 10]); + expect(r.frames.findLast(f => f.at === "turn 1")?.outputs[0]).toEqual([2, 1, 4]); + expect(r.frames.at(-1)?.outputs[0]).toEqual([2, 0, 10]); + expect(r.coverage).toEqual(expect.arrayContaining(["branch-true", "branch-false"])); +}); + +test("branch reductions preserve valid references or remove the branch explicitly", () => { + const s = branching(); + const valid = [...reductions(s)].filter(c => !validate(c)); + expect(valid.some(c => c.nodes.find(n => n.id === 2)?.branch === undefined)).toBe(true); + expect( + valid.every( + c => !c.nodes.some(n => n.branch?.otherwise.includes(1)) || c.nodes.some(n => n.id === 1) + ) + ).toBe(true); +}); + +test("generated branch selection and async completion replay deterministically", async () => { + for (const s of generate(3350, 40, "branches")) { + expect(validate(s)).toBeUndefined(); + const a = await runScenario(s); + const b = await runScenario(a.scenario); + expect(["pass", "fail", "policy", "error"], JSON.stringify(a)).toContain(a.status); + expect(b.status).toBe(a.status); + expect(b.frames).toEqual(a.frames); + expect(b.requirements).toEqual(a.requirements); + } +}); diff --git a/packages/signals/tests/semantics/build-paths.mjs b/packages/signals/tests/semantics/build-paths.mjs new file mode 100644 index 000000000..a84a6da89 --- /dev/null +++ b/packages/signals/tests/semantics/build-paths.mjs @@ -0,0 +1,10 @@ +/** esbuild uses native paths; artifact labels and calibration use portable ones. */ +export const portablePath = path => path.replaceAll("\\", "/"); +export function calibrationTarget(path, file) { + return portablePath(path).endsWith(`/src/core/${file}`); +} +export function sourceLabel(path, root) { + const file = portablePath(path); + const prefix = portablePath(root) + "/"; + return file.startsWith(prefix) ? "SIGNALS/" + file.slice(prefix.length) : file; +} diff --git a/packages/signals/tests/semantics/build-paths.test.ts b/packages/signals/tests/semantics/build-paths.test.ts new file mode 100644 index 000000000..f9addc08a --- /dev/null +++ b/packages/signals/tests/semantics/build-paths.test.ts @@ -0,0 +1,13 @@ +// The adapter runs directly in Node before the TypeScript worker is bundled. +import { calibrationTarget, sourceLabel } from "./build-paths.mjs"; + +test.each([ + ["/repo/signals/src/core/async.ts", "/repo/signals"], + [String.raw`C:\repo\signals\src\core\async.ts`, String.raw`C:\repo\signals`] +])("calibration and source labels accept native path %s", (file, root) => { + expect(calibrationTarget(file, "async.ts")).toBe(true); + expect(calibrationTarget(file, "scheduler.ts")).toBe(false); + expect(sourceLabel(file, root)).toBe("SIGNALS/src/core/async.ts"); + expect(sourceLabel(file + ".backup", root)).toBe("SIGNALS/src/core/async.ts.backup"); + expect(calibrationTarget(file + ".backup", "async.ts")).toBe(false); +}); diff --git a/packages/signals/tests/semantics/calibration.test.ts b/packages/signals/tests/semantics/calibration.test.ts new file mode 100644 index 000000000..a56728481 --- /dev/null +++ b/packages/signals/tests/semantics/calibration.test.ts @@ -0,0 +1,226 @@ +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { Client } from "./client.js"; +import { chain } from "./fixtures.js"; + +const exec = promisify(execFile); +const cli = fileURLToPath(new URL("./cli.mjs", import.meta.url)); + +test.each([ + "drop-wake", + "stale-result", + "lost-blocker", + "false-ready", + "false-verdict", + "lost-disposal-wake", + "lost-fallback-wake", + "entangle-effect", + "drop-action-hold" +])( + "detects and reduces the %s runtime fault", + async fault => { + const out = await mkdtemp(join(tmpdir(), "solid-fuzz-test-")); + try { + await exec(process.execPath, [ + cli, + "--calibrate", + "--fault", + fault, + "--shrink", + "--out", + out + ]); + const summary = JSON.parse(await readFile(join(out, "summary.json"), "utf8")); + expect(summary.statuses.fail).toBeGreaterThan(0); + expect(summary.statuses.error ?? 0).toBe(0); + const index = { + "drop-wake": 0, + "stale-result": 2, + "lost-blocker": 1, + "false-ready": 5, + "false-verdict": 6, + "lost-disposal-wake": 7, + "lost-fallback-wake": 8, + "entangle-effect": 9, + "drop-action-hold": 10 + }[fault]!; + const reductions = await Promise.all( + (await readdir(out)) + .filter(file => file.endsWith("-min.json")) + .map(async file => JSON.parse(await readFile(join(out, file), "utf8"))) + ); + const reduced = + fault === "false-verdict" + ? reductions.find(r => r.result.failure?.rule === "R4") + : JSON.parse(await readFile(join(out, `case-${index}-min.json`), "utf8")); + expect(reduced, JSON.stringify(reductions.map(r => r.result.failure))).toBeDefined(); + // The two-source fallback witness is already locally minimal. + expect(reduced.attempts).toBeGreaterThan(0); + if (fault !== "lost-fallback-wake") expect(reduced.accepted).toBeGreaterThan(0); + expect(reduced.result.status).toBe("fail"); + if (fault.startsWith("lost-") && fault.endsWith("-wake")) + expect(reduced.result.failure.rule).toBe("P1"); + if (fault === "entangle-effect") { + expect(reduced.result.failure.rule).toBe("G2"); + expect(reduced.result.failure.expected.relation).toBe("independent"); + } + if (fault === "drop-action-hold") expect(reduced.result.failure.rule).toBe("G1"); + if (fault === "false-ready") expect(reduced.result.failure.rule).toBe("R2"); + if (fault === "false-verdict") expect(reduced.result.failure.rule).toBe("R4"); + // The same portable case must pass against the unmodified runtime. Remove + // build metadata because this is explicitly a cross-target comparison. + const replay = join(out, "scenario.json"); + await writeFile(replay, JSON.stringify(reduced.scenario)); + await exec(process.execPath, [cli, "--replay", replay, "--out", join(out, "control")]); + const control = JSON.parse(await readFile(join(out, "control", "summary.json"), "utf8")); + expect(control.statuses).toEqual({ pass: 1 }); + } finally { + await rm(out, { recursive: true, force: true }); + } + }, + 20000 +); + +test("watchdog terminates a worker that cannot yield", async () => { + const out = await mkdtemp(join(tmpdir(), "solid-fuzz-watchdog-")); + const file = join(out, "worker.mjs"); + await writeFile( + file, + "import { parentPort } from 'node:worker_threads'; parentPort.on('message', () => { while (true) {} });" + ); + const client = new Client(file, 200); + try { + const result = await client.run(chain()); + expect(result.status).toBe("limit"); + expect(result.error).toContain("watchdog"); + } finally { + await client.close(); + await rm(out, { recursive: true, force: true }); + } +}); + +test("worker heap exhaustion is an inconclusive resource limit", async () => { + const out = await mkdtemp(join(tmpdir(), "solid-fuzz-memory-")); + const file = join(out, "worker.mjs"); + await writeFile( + file, + `import { parentPort } from 'node:worker_threads'; + parentPort.on('message', () => { + const arrays = []; + for (let i = 0; i < 64; i++) arrays.push(new Array(2_000_000).fill(i)); + parentPort.postMessage(arrays.length); + });` + ); + const client = new Client(file); + try { + const result = await client.run(chain()); + expect(result.status).toBe("limit"); + expect(result.error).toMatch(/memory limit/i); + expect(result.diagnostics?.length ?? 0).toBeLessThanOrEqual(4096); + } finally { + await client.close(); + await rm(out, { recursive: true, force: true }); + } +}); + +test("fresh and reused workers agree on generated cases", async () => { + const out = await mkdtemp(join(tmpdir(), "solid-fuzz-isolation-")); + try { + // Campaigns may find semantic counterexamples. A nonzero semantic exit is + // expected and differs from a missing/malformed summary or process error. + for (const fresh of [false, true]) { + const args = [cli, "--seed", "3305", "--cases", "40", "--out", join(out, String(fresh))]; + if (fresh) args.push("--fresh"); + await exec(process.execPath, args).catch(e => { + if (e.code !== 1) throw e; + }); + } + const a = JSON.parse(await readFile(join(out, "false", "summary.json"), "utf8")); + const b = JSON.parse(await readFile(join(out, "true", "summary.json"), "utf8")); + expect(a.statuses).toEqual(b.statuses); + expect(a.coverage).toEqual(b.coverage); + expect(a.failureSymptomGroups).toEqual(b.failureSymptomGroups); + expect(a.metadata.workerHash).toEqual(b.metadata.workerHash); + expect(a.metadata.sourceHash).toEqual(b.metadata.sourceHash); + } finally { + await rm(out, { recursive: true, force: true }); + } +}, 20000); + +test("a progress allowance survives reporting, shrinking and replay but cannot cover fallback waiting", async () => { + const out = await mkdtemp(join(tmpdir(), "solid-fuzz-allowance-")); + try { + await exec(process.execPath, [ + cli, + "--calibrate", + "--fault", + "lost-disposal-wake", + "--out", + join(out, "ideal") + ]); + const ideal = JSON.parse(await readFile(join(out, "ideal", "case-7.json"), "utf8")); + const file = join(out, "scenario.json"); + await writeFile(file, JSON.stringify(ideal.scenario)); + await exec(process.execPath, [ + cli, + "--replay", + file, + "--fault", + "lost-disposal-wake", + "--allow", + "legacy-disposal-wait", + "--shrink", + "--out", + join(out, "allowed") + ]); + const allowed = JSON.parse(await readFile(join(out, "allowed", "summary.json"), "utf8")); + expect(allowed.statuses).toEqual({ pass: 1 }); + expect(allowed.progressFindings).toBe(1); + expect(allowed.waivedProgress).toEqual({ "legacy-disposal-wait": 1 }); + expect(allowed.retainedCases).toBe(1); + const reduced = JSON.parse(await readFile(join(out, "allowed", "case-0-min.json"), "utf8")); + expect(reduced.result.progress[0].allowance).toBe("legacy-disposal-wait"); + expect(reduced.accepted).toBeGreaterThan(0); + const recorded = join(out, "allowed", "findings.jsonl"); + for (const strict of [false, true]) { + const args = [ + cli, + "--replay", + recorded, + "--index", + "0", + "--fault", + "lost-disposal-wake", + "--out", + join(out, String(strict)) + ]; + if (strict) args.push("--allow", "none"); + await exec(process.execPath, args).catch(e => { + if (!strict || e.code !== 1) throw e; + }); + const result = JSON.parse(await readFile(join(out, String(strict), "case-0.json"), "utf8")); + expect(result.result.status).toBe(strict ? "fail" : "pass"); + expect(result.result.progress[0].allowance).toBe(strict ? undefined : "legacy-disposal-wait"); + expect(result.result.frames).toEqual(ideal.result.frames); + } + await exec(process.execPath, [ + cli, + "--calibrate", + "--fault", + "lost-fallback-wake", + "--allow", + "legacy-disposal-wait", + "--out", + join(out, "outside") + ]); + const outside = JSON.parse(await readFile(join(out, "outside", "case-8.json"), "utf8")); + expect(outside.result.failure.rule).toBe("P1"); + expect(outside.result.progress[0].allowance).toBeUndefined(); + } finally { + await rm(out, { recursive: true, force: true }); + } +}, 20000); diff --git a/packages/signals/tests/semantics/candidates.ts b/packages/signals/tests/semantics/candidates.ts new file mode 100644 index 000000000..93f6bfeca --- /dev/null +++ b/packages/signals/tests/semantics/candidates.ts @@ -0,0 +1,335 @@ +import { anchorIds, dependencies, queuedSteps, validate, type Scenario } from "./scenario.js"; +import { + canonicalize, + structuralReductions, + focusedReductions, + pruningReductions, + arithmeticReductions, + edgeReductions, + observationReductions, + lifecycleReductions, + scheduleReductions, + coneReductions, + branchReductions, + deliveryScheduleReductions, + observationCollapseReductions, + mergeArithmeticReductions, + deliveryOrderingReductions, + callbackEndReductions, + initialVisibilityReductions, + cutReductions, + deliveryEscapeReductions +} from "./reduce.js"; +import { normalize, scenarioKey } from "./normalize.js"; +import { orderingReductions } from "./order.js"; + +/** Structural edits retain exact questions. Identity-value renaming and bounded + * affine folding translate inputs explicitly, retaining owner and occurrence. */ +export const reductionFamilies = { + normalize: normalizeCandidates, + anchors: anchorsCandidates, + optimistic: optimisticCandidates, + actions: actionsCandidates, + sources: sourcesCandidates, + turns: turnsCandidates, + focused: focusedCandidates, + arithmetic: arithmeticReductions, + pruning: pruningReductions, + structural: structuralCandidates, + edges: edgeReductions, + observation: observationReductions, + lifecycle: lifecycleReductions, + schedule: scheduleReductions, + cones: coneReductions, + branches: branchReductions, + deliverySchedule: deliveryScheduleReductions, + observationCollapse: observationCollapseReductions, + mergeArithmetic: mergeArithmeticReductions, + deliveryOrdering: deliveryOrderingReductions, + callbackEnd: callbackEndReductions, + initialVisibility: initialVisibilityReductions, + cuts: cutReductions, + deliveryEscape: deliveryEscapeReductions, + steps: stepsCandidates, + readers: readersCandidates, + nodes: nodesCandidates, + ordering: orderingCandidates +}; + +export type ReductionFamily = keyof typeof reductionFamilies; +// Prune disconnected work before replaying detailed edits on a larger graph. +// Single-use edge trials finish cases well but compete with cheaper edits in +// short searches. Keep them last; higher acceptance alone is not a priority rule. +export const reductionOrder: readonly ReductionFamily[] = [ + "normalize", + "pruning", + "anchors", + "optimistic", + "actions", + "sources", + "turns", + "focused", + "arithmetic", + "structural", + "steps", + "readers", + "nodes", + "ordering", + "edges", + "observation", + "lifecycle", + "cones", + "schedule", + "branches", + "deliverySchedule", + "observationCollapse", + "initialVisibility", + "callbackEnd", + "mergeArithmetic", + "deliveryOrdering", + "cuts", + "deliveryEscape" +]; + +export function* reductions( + s: Scenario, + order: readonly ReductionFamily[] = reductionOrder, + enter?: (family: ReductionFamily) => void +): Generator { + for (const family of order) { + enter?.(family); + yield* reductionFamilies[family](s); + } +} + +function* normalizeCandidates(s: Scenario): Generator { + const named = normalize(s); + if (JSON.stringify(named) !== JSON.stringify(s)) yield named; +} + +function* anchorsCandidates(s: Scenario): Generator { + if (s.version === 2) { + const observed = anchorIds(s); + for (let i = 0; i < observed.length; i++) { + const anchors = observed.slice(); + anchors.splice(i, 1); + yield { ...s, anchors }; + } + if (s.anchorShow !== false) yield { ...s, anchorShow: false }; + } +} + +function* optimisticCandidates(s: Scenario): Generator { + if (s.optimistic) { + for (let i = 0; i < s.optimistic.proposals.length; i++) { + if (s.optimistic.proposals.length > 1) { + const c = structuredClone(s); + c.optimistic!.proposals.splice(i, 1); + yield c; + } + if (s.optimistic.proposals[i] !== 0) { + const c = structuredClone(s); + c.optimistic!.proposals[i] = 0; + yield c; + } + } + if (s.optimistic.authoritative !== 0) { + const c = structuredClone(s); + c.optimistic!.authoritative = 0; + yield c; + } + } +} + +function* actionsCandidates(s: Scenario): Generator { + if (s.actions) { + for (let i = 0; i < s.actions.length; i++) { + const script = s.actions[i]; + for (let j = 0; j < script.segments.length; j++) { + for (let k = 0; k < script.segments[j].length; k++) { + const c = structuredClone(s); + c.actions![i].segments[j].splice(k, 1); + yield c; + } + if (script.segments.length > 2) { + const c = structuredClone(s); + c.actions![i].segments.splice(j, 1); + yield c; + } + } + } + } +} + +function* sourcesCandidates(s: Scenario): Generator { + if (s.version === 2) { + const used = new Set([ + ...s.nodes.flatMap(n => dependencies(n)), + ...s.readers.flatMap(r => r.refs), + -1 + ]); + for (const turn of s.turns) + if ("steps" in turn) + for (const step of turn.steps) { + const leaves = step.op === "queue" ? queuedSteps(step) : [step]; + for (const leaf of leaves) if (leaf.op === "write") used.add(leaf.source ?? -1); + } + for (const a of s.actions ?? []) + for (const segment of a.segments) for (const w of segment) used.add(w.source); + for (const source of s.sources!) + if (!used.has(source)) { + const c = structuredClone(s); + c.sources = c.sources!.filter(id => id !== source); + yield c; + } + } +} + +function* turnsCandidates(s: Scenario): Generator { + for ( + let width = Math.max(1, Math.ceil(s.turns.length / 2)); + width >= 1; + width = Math.floor(width / 2) + ) { + for (let i = 0; i < s.turns.length; i += width) { + const c = structuredClone(s); + c.turns.splice(i, width); + yield c; + } + } + // Removing a turn also removes its work. Joining two event bodies tests + // whether only their intervening scheduler drain was unnecessary. This is + // a batching change, never a normalization or an equivalence assertion. + for (let i = 1; i < s.turns.length; i++) { + const a = s.turns[i - 1], + b = s.turns[i]; + if (!("steps" in a) || !("steps" in b)) continue; + const turns = s.turns.slice(); + turns.splice(i - 1, 2, { steps: a.steps.concat(b.steps) }); + yield { ...s, turns }; + } +} + +function* focusedCandidates(s: Scenario): Generator { + yield* focusedReductions(s); +} + +function* structuralCandidates(s: Scenario): Generator { + yield* structuralReductions(s, false); +} + +function* stepsCandidates(s: Scenario): Generator { + for (let i = 0; i < s.turns.length; i++) { + const turn = s.turns[i]; + if (!("steps" in turn)) continue; + for (let j = 0; j < turn.steps.length; j++) { + const c = structuredClone(s); + const t = c.turns[i]; + if ("steps" in t) t.steps.splice(j, 1); + yield c; + const step = turn.steps[j]; + if (step.op === "queue" && step.steps) + for (let k = 0; k < step.steps.length; k++) { + const c = structuredClone(s); + const t = c.turns[i]; + if ("steps" in t) { + const q = t.steps[j]; + if (q.op === "queue") queuedSteps(q).splice(k, 1); + } + yield c; + } + } + } +} + +function* readersCandidates(s: Scenario): Generator { + for (let i = 0; i < s.readers.length; i++) { + const c = structuredClone(s); + c.readers.splice(i, 1); + yield c; + if (s.readers[i].render) { + const c = structuredClone(s); + delete c.readers[i].render; + yield c; + if (s.readers[i].render!.on !== -1) { + const c = structuredClone(s); + c.readers[i].render!.on = -1; + yield c; + } + } + if (s.readers[i].parent !== undefined) { + const c = structuredClone(s); + delete c.readers[i].parent; + yield c; + } + if (s.readers[i].pendingDepth) { + const c = structuredClone(s); + if (c.readers[i].pendingDepth === 1) delete c.readers[i].pendingDepth; + else c.readers[i].pendingDepth!--; + yield c; + } + if (s.readers[i].pending) { + const c = structuredClone(s); + delete c.readers[i].pending; + delete c.readers[i].pendingDepth; + // Remove only operations attached to this feature; keep other event work. + const remove = (x: { op: string; reader?: number }) => + x.op !== "click" || x.reader !== s.readers[i].id; + for (const turn of c.turns) + if ("steps" in turn) { + turn.steps = turn.steps.filter(remove); + for (const step of turn.steps) + if (step.op === "queue") { + const steps = queuedSteps(step).filter(remove); + delete step.step; + step.steps = steps; + } + } + yield c; + } + if (s.readers[i].boundary !== "none") { + const c = structuredClone(s); + c.readers[i].boundary = "none"; + yield c; + } + } +} + +function* nodesCandidates(s: Scenario): Generator { + for (const node of s.nodes) { + if (node.branch) + for (const deps of [node.deps, node.branch.otherwise]) { + const c = structuredClone(s); + const n = c.nodes.find(n => n.id === node.id)!; + n.deps = [...deps]; + delete n.branch; + yield c; + } + if ( + !s.nodes.some(n => dependencies(n).includes(node.id)) && + !s.readers.some(r => r.refs.includes(node.id) || r.render?.on === node.id) + ) { + const c = structuredClone(s); + c.nodes = c.nodes.filter(n => n.id !== node.id); + yield c; + } + for (const field of ["factor", "offset"] as const) { + const value = field === "factor" ? 1 : 0; + if (node[field] === value) continue; + const c = structuredClone(s); + c.nodes.find(n => n.id === node.id)![field] = value; + yield c; + } + } +} + +function* orderingCandidates(s: Scenario): Generator { + yield* orderingReductions(s); +} + +export const expensiveFamilies = new Set([ + "mergeArithmetic", + "deliveryOrdering", + "cuts", + "deliveryEscape" +]); diff --git a/packages/signals/tests/semantics/cli.mjs b/packages/signals/tests/semantics/cli.mjs new file mode 100644 index 000000000..2b425234a --- /dev/null +++ b/packages/signals/tests/semantics/cli.mjs @@ -0,0 +1,111 @@ +import { build } from "esbuild"; +import { calibrationTarget, sourceLabel } from "./build-paths.mjs"; +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, dirname } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { createHash } from "node:crypto"; + +const dir = dirname(fileURLToPath(import.meta.url)); +const temp = await mkdtemp(join(tmpdir(), "solid-fuzz-build-")); +const workerFile = join(temp, "worker.mjs"); +const args = process.argv.slice(2).filter(x => x !== "--"); +const fault = args.includes("--fault") ? args[args.indexOf("--fault") + 1] : undefined; +const faults = { + "entangle-effect": [ + "core.ts", + "let prev: Transition | null = (el as any)._valueTransition;", + "let prev: Transition | null = (el as any)._valueTransition; if (prev && !currentTransition(prev)._done) { globalQueue.initTransition(prev); (activeTransition!._contested ??= []).push(el); }" + ], + "drop-action-hold": [ + "scheduler.ts", + "if (transition._actions.length) {", + "if (false && transition._actions.length) {" + ], + "lost-disposal-wake": [ + "owner.ts", + "if (t && n._statusFlags & STATUS_PENDING && !wokenTransitions.includes(t))", + "if (false && t && n._statusFlags & STATUS_PENDING && !wokenTransitions.includes(t))" + ], + "lost-fallback-wake": [ + "scheduler.ts", + "export function wakeParked(): void {", + "export function wakeParked(): void { return;" + ], + "false-verdict": [ + "verdict.ts", + "export function isPending(fn: () => any): boolean {", + "export function isPending(fn: () => any): boolean { actualPending(fn); return false; }\nfunction actualPending(fn: () => any): boolean {" + ], + "false-ready": [ + "verdict.ts", + "export function isPending(fn: () => any): boolean {", + "export function isPending(fn: () => any): boolean { return false;" + ], + "drop-wake": [ + "scheduler.ts", + "if (!syncDepth && !globalQueue._running && !projectionWriteActive) queueMicrotask(flush);", + "if (!syncDepth && !globalQueue._running && !projectionWriteActive) void 0;" + ], + "stale-result": [ + "async.ts", + "const asyncWrite = (value: T, then?: () => void) => {\n if (el._x?._inFlight !== result) return;", + "const asyncWrite = (value: T, then?: () => void) => {" + ], + "lost-blocker": [ + "scheduler.ts", + "if (!reporters) activeTransition._asyncReporters.set(source, (reporters = new Set()));", + "if (!reporters) reporters = new Set();" + ] +}; +const sources = new Map(); +let mutated = false; +const adapter = { + name: "record-and-calibrate-runtime", + setup(build) { + build.onLoad({ filter: /\.(ts|js)$/ }, async ({ path }) => { + let contents = await readFile(path, "utf8"); + sources.set(sourceLabel(path, dirname(dirname(dir))), contents); + if (fault && calibrationTarget(path, faults[fault][0])) { + const [, before, after] = faults[fault]; + if (contents.split(before).length !== 2) + throw new Error(`Calibration anchor drift: ${fault}`); + contents = contents.replace(before, after); + mutated = true; + } + return { contents, loader: path.endsWith(".ts") ? "ts" : "js" }; + }); + } +}; +try { + if (fault && !faults[fault]) throw new Error(`Unknown calibration fault: ${fault}`); + const common = { + bundle: true, + platform: "node", + format: "esm", + target: "node22", + logLevel: "warning" + }; + await build({ + ...common, + entryPoints: [join(dir, "worker.ts")], + outfile: workerFile, + define: { __DEV__: "true", __OBSERVE__: "true", __TEST__: "true" }, + plugins: [adapter] + }); + if (fault && !mutated) throw new Error("Requested calibration mutation was not applied"); + const workerHash = createHash("sha256") + .update(await readFile(workerFile)) + .digest("hex"); + await build({ ...common, entryPoints: [join(dir, "cli.ts")], outfile: join(temp, "cli.mjs") }); + const { main } = await import(pathToFileURL(join(temp, "cli.mjs")).href); + const digest = createHash("sha256"); + for (const [path, contents] of [...sources].sort(([a], [b]) => a.localeCompare(b))) + digest.update(path).update(contents); + await main({ args, workerFile, workerHash, fault, sourceHash: digest.digest("hex") }); +} catch (error) { + console.error(error); + process.exitCode = 2; +} finally { + await rm(temp, { recursive: true, force: true }); +} diff --git a/packages/signals/tests/semantics/cli.test.ts b/packages/signals/tests/semantics/cli.test.ts new file mode 100644 index 000000000..232c3914b --- /dev/null +++ b/packages/signals/tests/semantics/cli.test.ts @@ -0,0 +1,25 @@ +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { main } from "./cli.js"; + +test("calibration cannot succeed merely because the worker crashes", async () => { + const out = await mkdtemp(join(tmpdir(), "solid-fuzz-broken-worker-")); + const workerFile = join(out, "broken.mjs"); + await writeFile(workerFile, 'throw new Error("Worker setup failed");'); + const log = vi.spyOn(console, "log").mockImplementation(() => {}); + try { + await expect( + main({ + args: ["--calibrate", "--out", out], + workerFile, + fault: "drop-wake", + sourceHash: "test", + workerHash: "test" + }) + ).rejects.toThrow(/Calibration.*(error|inconclusive)/); + } finally { + log.mockRestore(); + await rm(out, { recursive: true, force: true }); + } +}); diff --git a/packages/signals/tests/semantics/cli.ts b/packages/signals/tests/semantics/cli.ts new file mode 100644 index 000000000..4021ca5cc --- /dev/null +++ b/packages/signals/tests/semantics/cli.ts @@ -0,0 +1,560 @@ +import { mkdir, open, readFile, writeFile, type FileHandle } from "node:fs/promises"; +import { execFileSync } from "node:child_process"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { parseArgs } from "node:util"; +import { Client, WorkerFailure, workerLimits } from "./client.js"; +import { generate } from "./generate.js"; +import { + chain, + obsolete, + reveal, + readyControl, + heldPending, + disposedReader, + releasedFallback, + separateUpdates, + heldAction +} from "./fixtures.js"; +import { CandidateQueue, type Candidate } from "./selection.js"; +import { scenarioKey } from "./normalize.js"; +import { fingerprint, shrink, isSemanticFailure } from "./shrink.js"; +import type { RunResult } from "./runner.js"; +import type { Scenario } from "./scenario.js"; +import { runEquivalence, shrinkEquivalence, variants } from "./equivalence.js"; +import { + allowanceIds, + allowanceContracts, + type Allowance, + waitingPolicies, + type WaitingPolicy +} from "./policy.js"; +import { ruleRevision, ruleContracts } from "./rules.js"; +import { corpus } from "./corpus.js"; + +interface Context { + args: string[]; + workerFile: string; + workerHash: string; + sourceHash: string; + fault?: string; +} + +export async function main(context: Context) { + const { values: v } = parseArgs({ + args: context.args, + options: { + seed: { type: "string", default: "3289" }, + cases: { type: "string", default: "1000" }, + replay: { type: "string" }, + index: { type: "string" }, + out: { type: "string" }, + fault: { type: "string" }, + timeout: { type: "string", default: "5000" }, + budget: { type: "string", default: "150" }, + shrink: { type: "boolean" }, + "shrink-mode": { type: "string", default: "discovery" }, + fresh: { type: "boolean" }, + calibrate: { type: "boolean" }, + equivalence: { type: "boolean" }, + "latest-equivalence": { type: "boolean" }, + "optimistic-equivalence": { type: "boolean" }, + corpus: { type: "boolean" }, + cohort: { type: "string" }, + "waiting-policy": { type: "string" }, + allow: { type: "string", multiple: true }, + help: { type: "boolean" } + } + }); + if (v.help) { + console.log( + "Solid semantic fuzzer\n --seed N --cases N [--out DIR] [--fresh] [--shrink] [--budget N] (actual search executions per selected case)\n --replay FILE [--index N for findings.jsonl] [--shrink]\n --shrink-mode discovery|focused (default discovery)\n --corpus\n --cohort attachment|update-groups|ordinary|multi|optimistic|reads|mounts|latest|observation|branches|readiness|boundaries|nested|derived-readiness|optimistic-readiness|branch-boundaries\n --waiting-policy review|required-only|retain-disposed (default review)\n --allow legacy-disposal-wait|none (optional historical exception; default none)\n --equivalence --seed N --cases N [--shrink]\n --latest-equivalence --seed N --cases N [--shrink]\n --optimistic-equivalence --seed N --cases N [--shrink]\n --calibrate [--fault drop-wake|stale-result|lost-blocker|false-ready|false-verdict|lost-disposal-wake|lost-fallback-wake|entangle-effect|drop-action-hold]\n --timeout MS (per-case worker watchdog; default 5000)\nArtifacts contain target hashes, canonical schedules, traces and work ledgers." + ); + return; + } + const integer = (key: "seed" | "cases" | "timeout" | "budget", min: number, max: number) => { + const n = Number(v[key]); + if (!Number.isInteger(n) || n < min || n > max) throw new Error(`Invalid --${key}`); + return n; + }; + const seed = integer("seed", -2147483648, 2147483647); + if ( + [v.equivalence, v["latest-equivalence"], v["optimistic-equivalence"]].filter(Boolean).length > 1 + ) + throw new Error("Choose one equivalence comparison"); + const paired = !!v.equivalence || !!v["latest-equivalence"] || !!v["optimistic-equivalence"]; + const comparison = v["latest-equivalence"] + ? "latest" + : v["optimistic-equivalence"] + ? "optimistic" + : "delivery"; + const count = integer("cases", 1, 10000); + const budget = integer("budget", 0, 10000); + const shrinkMode = v["shrink-mode"]; + if (shrinkMode !== "discovery" && shrinkMode !== "focused") + throw new Error("Invalid --shrink-mode: choose discovery or focused"); + const timeout = integer("timeout", 1, 60000); + let waitingPolicy = (v["waiting-policy"] ?? "review") as WaitingPolicy; + if (!waitingPolicies.includes(waitingPolicy)) throw new Error("Invalid --waiting-policy"); + let allowances = ( + v.allow?.length === 1 && v.allow[0] === "none" ? [] : (v.allow ?? []) + ) as Allowance[]; + if (allowances.some(id => !allowanceIds.includes(id))) throw new Error("Invalid --allow"); + const cohort = v.cohort ?? (v["latest-equivalence"] ? "reads" : "ordinary"); + if ( + ![ + "attachment", + "update-groups", + "ordinary", + "multi", + "optimistic", + "reads", + "mounts", + "latest", + "observation", + "branches", + "readiness", + "boundaries", + "nested", + "derived-readiness", + "optimistic-readiness", + "branch-boundaries" + ].includes(cohort) + ) + throw new Error("Invalid --cohort"); + if (cohort === "optimistic" && v.equivalence) + throw new Error("Ordinary delivery equivalence does not cover action lifetimes"); + const out = resolve(v.out ?? join(tmpdir(), `solid-semantic-fuzz-${Date.now()}-${seed}`)); + await mkdir(out, { recursive: true }); + let target = "unknown"; + try { + target = execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim(); + } catch {} + const metadata = { + target, + sourceHash: context.sourceHash, + workerHash: context.workerHash, + workerLimits, + node: process.version, + generator: "fast-check@4.9.0", + seed, + fault: context.fault ?? null, + defines: { __DEV__: true, __OBSERVE__: true, __TEST__: true }, + fresh: !!v.fresh, + equivalence: paired, + comparison, + ruleRevision, + waitingPolicy, + allowances, + allowanceContracts, + ruleContracts, + cohort + }; + const client = new Client(context.workerFile, timeout, !!v.fresh); + const statuses: Record = {}; + const coverage: Record = {}; + const signatures = new Set(); + const failureSignatures = new Set(); + let waitingFindings = 0; + let progressFindings = 0; + const waivedProgress: Record = {}; + const groupScopes: Record = {}; + let groupChecks = 0, + groupHeld = 0, + batchingHeld = 0, + possibleGroupJoins = 0; + let attachmentCases = 0, + attachmentChecks = 0, + outputNodes = 0, + privateOutputWrites = 0; + let total = 0; + let executions = 0; + let operations = 0; + let requests = 0; + let maxReportedHeapBytes = 0; + const started = performance.now(); + let findings: FileHandle | undefined; + let batching: FileHandle | undefined; + let batchingCases = 0; + let retainedCases = 0; + let workerErrors = 0; + const candidates = new CandidateQueue(); + const reductionExecutions = { initial: 0, search: 0, verification: 0 }; + const searchStatuses: Record = {}; + const reducedReports: { + index: number; + signature: string; + reproKey: string; + exhausted: boolean; + }[] = []; + let generationMs = 0, + campaignMs = 0, + reductionMs = 0, + reportingMs = 0; + const save = async (name: string, data: unknown) => { + const start = performance.now(); + await writeFile(join(out, `${name}.json`), JSON.stringify(data, null, 2) + "\n"); + reportingMs += performance.now() - start; + }; + const reduceCandidate = async ({ + index, + signature, + scenario: canonical, + result, + pair + }: Candidate) => { + const start = performance.now(), + reportStart = reportingMs; + const before = { ...reductionExecutions }; + let initialRemaining = paired ? variants(canonical, comparison).length : 0; + const run = async (s: Scenario) => { + const initial = initialRemaining > 0; + if (initial) { + initialRemaining--; + reductionExecutions.initial++; + } else reductionExecutions.search++; + const r = await client.run(s, { waitingPolicy, allowances }); + if (!initial) searchStatuses[r.status] = (searchStatuses[r.status] ?? 0) + 1; + return r; + }; + const verify = (s: Scenario) => { + reductionExecutions.verification++; + return client.run(s, { waitingPolicy, allowances }); + }; + // Later, simpler candidates get full artifacts too. Originals remain in JSONL. + await save(`case-${index}`, { metadata, index, scenario: canonical, result, pair }); + if (!paired) { + const reduced = await shrink(result, run, budget, { mode: shrinkMode }); + const replay = await verify(reduced.result.scenario); + const signature = fingerprint(reduced.result)!; + if ( + fingerprint(replay) !== signature || + (isSemanticFailure(reduced.result) && !isSemanticFailure(replay)) + ) + throw new Error("Reduced failure did not replay cleanly"); + reducedReports.push({ + index, + signature, + reproKey: reduced.reproKey, + exhausted: reduced.exhausted + }); + await save(`case-${index}-min`, { + metadata, + index, + originalSignature: fingerprint(result), + shrinkMode, + scenario: reduced.result.scenario, + ...reduced, + executionCounts: { initial: 0, search: reduced.attempts, verification: 1 } + }); + } else { + const reduced = await shrinkEquivalence(canonical, run, budget, comparison, { + mode: shrinkMode + }); + let replayResult: RunResult, replaySignature: string | undefined; + if (reduced.single) { + replayResult = await verify(reduced.scenario); + replaySignature = fingerprint(replayResult); + } else { + const replay = await runEquivalence(reduced.scenario, verify, comparison); + replayResult = replay.result; + replaySignature = replay.signature; + } + if ( + replaySignature !== reduced.signature || + (isSemanticFailure(reduced.result) && !isSemanticFailure(replayResult)) + ) + throw new Error("Reduced pair did not replay cleanly"); + const reproKey = `${reduced.single ? "single" : comparison}:${reduced.signature}\n${scenarioKey(reduced.scenario)}`; + reducedReports.push({ + index, + signature: reduced.signature!, + reproKey, + exhausted: reduced.exhausted + }); + await save(`case-${index}-min`, { + metadata: { + ...metadata, + equivalence: !reduced.single, + comparison: reduced.single ? undefined : comparison + }, + index, + originalSignature: signature, + shrinkMode, + ...reduced, + reproKey, + executionCounts: { + initial: reduced.initialExecutions, + search: reduced.attempts, + verification: reductionExecutions.verification - before.verification + } + }); + } + reductionMs += performance.now() - start - (reportingMs - reportStart); + console.log( + ` Case ${index}: reduced with ${reductionExecutions.search - before.search} search executions; independently replayed` + ); + }; + try { + const generationStart = performance.now(); + let scenarios: Scenario[]; + if (v.replay) { + const contents = await readFile(resolve(v.replay), "utf8"); + let artifact; + if (v.replay.endsWith(".jsonl")) { + const index = Number(v.index); + if (v.index === undefined || !Number.isInteger(index) || index < 0) + throw new Error("JSONL replay requires --index N"); + for (const line of contents.split("\n")) { + if (!line) continue; + const entry = JSON.parse(line); + if (entry.index === index) { + artifact = entry; + break; + } + } + if (!artifact) throw new Error(`No retained case at index ${index}`); + } else artifact = JSON.parse(contents); + if ( + artifact.metadata?.equivalence && + (!paired || (artifact.metadata.comparison ?? "delivery") !== comparison) + ) + throw new Error( + "Use the recorded comparison: --equivalence, --latest-equivalence or --optimistic-equivalence" + ); + if (artifact.metadata && artifact.metadata.fault !== (context.fault ?? null)) + throw new Error("Replay fault differs from artifact; pass the recorded --fault explicitly"); + if (artifact.metadata?.workerHash && artifact.metadata.workerHash !== context.workerHash) + console.warn( + "Replay uses a different worker bundle than the artifact; results may differ." + ); + const recordedPolicy = artifact.metadata?.waitingPolicy; + if (recordedPolicy && !waitingPolicies.includes(recordedPolicy)) + throw new Error("Unsupported replay waiting policy"); + if (recordedPolicy && !v["waiting-policy"]) waitingPolicy = recordedPolicy; + metadata.waitingPolicy = waitingPolicy; + const recordedAllowances = artifact.metadata?.allowances; + if ( + recordedAllowances !== undefined && + (!Array.isArray(recordedAllowances) || + recordedAllowances.some(id => !allowanceIds.includes(id))) + ) + throw new Error("Unsupported replay allowances"); + if (recordedAllowances && !v.allow) allowances = recordedAllowances; + metadata.allowances = allowances; + scenarios = [artifact.scenario ?? artifact.result?.scenario ?? artifact]; + } else if (v.calibrate) + scenarios = [ + chain("sync"), + chain(), + obsolete(), + reveal(), + reveal("reset"), + readyControl(), + heldPending(), + disposedReader(), + releasedFallback(), + separateUpdates(), + heldAction() + ]; + else if (v.corpus) scenarios = corpus.map(c => c.scenario); + else scenarios = generate(seed, count, cohort as import("./generate.js").Cohort); + generationMs = performance.now() - generationStart; + const campaignStart = performance.now(), + reportStart = reportingMs, + reduceStart = reductionMs; + findings = await open(join(out, "findings.jsonl"), "w"); + try { + for (const scenario of scenarios) { + const pair: RunResult[] = []; + let canonical = scenario; + let pairSignature: string | undefined; + let result: RunResult; + if (paired) { + const compared = await runEquivalence( + scenario, + s => client.run(s, { waitingPolicy, allowances }), + comparison + ); + pair.push(...compared.pair); + result = compared.result; + canonical = compared.scenario; + pairSignature = compared.signature; + } else result = await client.run(scenario, { waitingPolicy, allowances }); + const index = total++; + statuses[result.status] = (statuses[result.status] ?? 0) + 1; + if (result.waiting) waitingFindings++; + if (result.progress?.length) { + progressFindings++; + for (const finding of result.progress) + if (finding.allowance) + waivedProgress[finding.allowance] = (waivedProgress[finding.allowance] ?? 0) + 1; + } + const runs = paired ? pair : [result]; + executions += runs.length; + for (const tag of new Set(runs.flatMap(r => r.coverage))) + coverage[tag] = (coverage[tag] ?? 0) + 1; + for (const r of runs) { + if (r.attachment) { + attachmentCases++; + attachmentChecks += r.attachment.checks; + outputNodes += r.attachment.nodes; + privateOutputWrites += r.attachment.privateWrites; + } + if (r.groups) { + const scope = r.groups.scope ?? "checked"; + groupScopes[scope] = (groupScopes[scope] ?? 0) + 1; + groupChecks += r.groups.checks; + groupHeld += r.groups.held; + batchingHeld += r.groups.batchingHeld; + possibleGroupJoins += r.groups.possibleJoins; + if (r.groups.batchingHeld) { + batching ??= await open(join(out, "batching.jsonl"), "w"); + const reportStart = performance.now(); + await batching.write( + JSON.stringify({ index, scenario: r.scenario, metadata, groups: r.groups }) + "\n" + ); + reportingMs += performance.now() - reportStart; + batchingCases++; + } + } + operations += r.metrics.operations; + requests += r.metrics.requests; + maxReportedHeapBytes = Math.max(maxReportedHeapBytes, r.metrics.heapUsedBytes ?? 0); + } + const signature = + pairSignature ?? + fingerprint(result) ?? + `${result.status}: ${result.error?.split("\n")[0]}`; + if (result.status === "fail") failureSignatures.add(signature); + if (result.status !== "pass" || result.waiting || result.progress?.length) { + const reportStart = performance.now(); + await findings.write( + JSON.stringify({ + index, + signature, + scenario: paired ? canonical : result.scenario, + metadata: { + workerHash: context.workerHash, + sourceHash: context.sourceHash, + fault: context.fault ?? null, + equivalence: paired, + comparison, + waitingPolicy, + allowances, + ruleRevision + }, + status: result.status, + failure: result.failure, + error: result.error, + waiting: result.waiting?.disposition, + progress: result.progress + }) + "\n" + ); + reportingMs += performance.now() - reportStart; + retainedCases++; + } + if (v.shrink && !v.calibrate && !v.replay && fingerprint(result)) + candidates.offer({ + index, + signature, + scenario: paired ? canonical : result.scenario, + result, + pair + }); + if ( + (result.status !== "pass" || result.waiting || result.progress?.length) && + !signatures.has(signature) + ) { + signatures.add(signature); + if (signatures.size > 20) continue; + await save(`case-${index}`, { + metadata, + index, + scenario: paired ? canonical : result.scenario, + result, + ...(v.corpus ? { corpus: corpus[index] } : {}), + ...(paired ? { pair } : {}) + }); + console.log(`Case ${index}: ${signature}`); + if (v.shrink && fingerprint(result) && (v.calibrate || v.replay)) + await reduceCandidate({ index, signature, scenario: canonical, result, pair }); + } + if (total % 1000 === 0) console.log(`${total}/${scenarios.length} cases`); + } + campaignMs = + performance.now() - + campaignStart - + (reportingMs - reportStart) - + (reductionMs - reduceStart); + for (const candidate of candidates.entries) await reduceCandidate(candidate); + if (v.shrink) await save("reductions", reducedReports); + await client.close(); + client.checkHealth(); + } catch (error) { + if (!(error instanceof WorkerFailure)) throw error; + workerErrors++; + await save("worker-error", { + metadata, + scenario: error.result.scenario, + result: error.result + }); + console.error( + "Worker failed after reporting a case; original input saved in worker-error.json" + ); + } + const elapsedMs = performance.now() - started; + const summary = { + metadata, + total, + executions, + statuses, + coverage, + operations, + requests, + maxReportedHeapBytes, + elapsedMs, + generationMs, + campaignMs, + reductionMs, + reportingMs, + reductionExecutions, + searchStatuses, + reducedCandidates: reducedReports.length, + reducedReproKeys: new Set(reducedReports.map(r => r.reproKey)).size, + casesPerSecond: total / (elapsedMs / 1000), + failureSymptomGroups: failureSignatures.size, + findingSymptomGroups: signatures.size, + retainedCases, + workerErrors, + waitingFindings, + progressFindings, + waivedProgress, + groupScopes, + groupChecks, + groupHeld, + batchingHeld, + batchingCases, + possibleGroupJoins, + attachmentCases, + attachmentChecks, + outputNodes, + privateOutputWrites + }; + await save("summary", summary); + console.log(JSON.stringify(summary, null, 2)); + console.log(`Artifacts: ${out}`); + if (v.calibrate && context.fault) { + if (workerErrors || statuses.error || statuses.limit || statuses.invalid) + throw new Error( + "Calibration had runtime errors or inconclusive cases; detection is not established" + ); + if (!statuses.fail) throw new Error("Calibration fault was not detected"); + } else if (workerErrors || Object.keys(statuses).some(status => status !== "pass")) + process.exitCode = 1; + } finally { + await findings?.close(); + await batching?.close(); + await client.close(); + } +} diff --git a/packages/signals/tests/semantics/client.test.ts b/packages/signals/tests/semantics/client.test.ts new file mode 100644 index 000000000..df19b18a6 --- /dev/null +++ b/packages/signals/tests/semantics/client.test.ts @@ -0,0 +1,49 @@ +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { Client, WorkerFailure } from "./client.js"; +import { chain } from "./fixtures.js"; + +// A control port lets the fixture fail only after run() has returned. +test.each(["throw", "exit"])( + "an idle worker %s stays attributed to its previous case", + async mode => { + const dir = await mkdtemp(join(tmpdir(), "solid-fuzz-idle-")); + const file = join(dir, "worker.mjs"); + await writeFile( + file, + `import { parentPort, BroadcastChannel } from 'node:worker_threads'; + parentPort.once('message', ({ scenario }) => { + const channel = new BroadcastChannel(scenario.control); + channel.onmessage = () => { + if (${JSON.stringify(mode)} === 'throw') throw new Error('late fixture failure'); + process.exit(7); + }; + parentPort.postMessage({ status: 'pass' }); + });` + ); + const { BroadcastChannel } = await import("node:worker_threads"); + const control = new BroadcastChannel(dir); + const client = new Client(file); + const previous = Object.assign(chain(), { control: dir }); + try { + expect((await client.run(previous)).status).toBe("pass"); + control.postMessage("fail now"); + await vi.waitFor(() => expect(() => client.checkHealth()).toThrow(WorkerFailure)); + try { + await client.run(chain("sync")); + throw new Error("Expected idle failure"); + } catch (error) { + expect(error).toBeInstanceOf(WorkerFailure); + expect((error as WorkerFailure).result.scenario).toBe(previous); + expect((error as WorkerFailure).result.error).toContain( + mode === "throw" ? "late fixture failure" : "(7)" + ); + } + } finally { + control.close(); + await client.close(); + await rm(dir, { recursive: true, force: true }); + } + } +); diff --git a/packages/signals/tests/semantics/client.ts b/packages/signals/tests/semantics/client.ts new file mode 100644 index 000000000..17410a989 --- /dev/null +++ b/packages/signals/tests/semantics/client.ts @@ -0,0 +1,124 @@ +import { Worker } from "node:worker_threads"; +import type { RunOptions, RunResult } from "./runner.js"; +import type { Scenario } from "./scenario.js"; + +export const workerLimits = { maxOldGenerationSizeMb: 128, maxYoungGenerationSizeMb: 32 }; + +/** An idle failure belongs to the last submitted case, never the next input. */ +export class WorkerFailure extends Error { + constructor(readonly result: RunResult) { + super(result.error); + } +} +interface Slot { + worker: Worker; + scenario?: Scenario; + started: number; + diagnostics: string; + closing: boolean; + failed: boolean; + finish?: (result: RunResult) => void; +} + +/** One outstanding case per worker; the watchdog lives outside its event loop. */ +export class Client { + private slot?: Slot; + private idleFailure?: RunResult; + constructor( + private file: string, + private timeout = 5000, + private fresh = false + ) {} + checkHealth() { + if (this.idleFailure) throw new WorkerFailure(this.idleFailure); + } + private failure(slot: Slot, status: "error" | "limit", error: string): RunResult { + return { + status, + error, + scenario: slot.scenario!, + frames: [], + requirements: [], + work: [], + coverage: [], + events: [], + diagnostics: slot.diagnostics || undefined, + metrics: { + operations: 0, + skipped: 0, + requests: 0, + frames: 0, + elapsedMs: performance.now() - slot.started + } + }; + } + private create(): Slot { + const slot: Slot = { + worker: new Worker(this.file, { stderr: true, stdout: true, resourceLimits: workerLimits }), + started: 0, + diagnostics: "", + closing: false, + failed: false + }; + const capture = (chunk: Buffer) => { + slot.diagnostics = (slot.diagnostics + chunk.toString()).slice(-4096); + }; + slot.worker.stderr.on("data", capture); + slot.worker.stdout.on("data", capture); + const fail = (status: "error" | "limit", error: string) => { + if (slot.failed) return; + slot.failed = true; + const result = this.failure(slot, status, error); + if (slot.finish) slot.finish(result); + else this.idleFailure ??= result; + }; + // Lifecycle listeners stay installed while idle and during termination. + slot.worker.on("error", (e: Error & { code?: string }) => { + fail(e.code === "ERR_WORKER_OUT_OF_MEMORY" ? "limit" : "error", e.stack ?? e.message); + }); + slot.worker.on("exit", code => { + if (!slot.closing) fail("error", `Worker exited unexpectedly (${code})`); + }); + return slot; + } + async run(scenario: Scenario, options?: RunOptions): Promise { + this.checkHealth(); + const slot = (this.slot ??= this.create()); + if (slot.finish) throw new Error("Only one case may run per client"); + slot.scenario = scenario; + slot.diagnostics = ""; + slot.started = performance.now(); + const result = await new Promise(resolve => { + const finish = (r: RunResult) => { + clearTimeout(timer); + slot.worker.off("message", finish); + slot.finish = undefined; + slot.scenario = r.scenario ?? slot.scenario; + if (slot.diagnostics) r.diagnostics = slot.diagnostics; + resolve(r); + }; + const timer = setTimeout( + () => finish(this.failure(slot, "limit", `Host watchdog exceeded ${this.timeout}ms`)), + this.timeout + ); + slot.finish = finish; + slot.worker.once("message", finish); + try { + slot.worker.postMessage({ scenario, options }); + } catch (error) { + finish(this.failure(slot, "error", String(error))); + } + }); + if (this.fresh || result.status !== "pass") await this.close(); + this.checkHealth(); + return result; + } + async close() { + const slot = this.slot; + this.slot = undefined; + if (slot) { + slot.closing = true; + await slot.worker.terminate(); + } + } +} diff --git a/packages/signals/tests/semantics/completion.test.ts b/packages/signals/tests/semantics/completion.test.ts new file mode 100644 index 000000000..853c3ba16 --- /dev/null +++ b/packages/signals/tests/semantics/completion.test.ts @@ -0,0 +1,78 @@ +import { corpus } from "./corpus.js"; +import { runScenario } from "./runner.js"; +import { chain } from "./fixtures.js"; +import { checkFinal } from "./rules.js"; +import { retainedObservations } from "./policy.js"; + +const disposed = () => structuredClone(corpus.find(c => c.name === "disposed-reader")!.scenario); + +test("disposal publishes without waiting under every timing policy", async () => { + for (const waitingPolicy of ["review", "required-only", "retain-disposed"] as const) { + const r = await runScenario(disposed(), { waitingPolicy }); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect(r.waiting).toBeUndefined(); + expect(r.frames.findLast(f => f.at === "turn 1")?.input).toBe(2); + } +}); + +test.each(["stale-reader", "stuck-hidden"])( + "%s publishes its final view under every waiting policy", + async name => { + const s = corpus.find(c => c.name === name)!.scenario; + for (const waitingPolicy of ["review", "required-only", "retain-disposed"] as const) { + const r = await runScenario(s, { waitingPolicy }); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect(r.failure).toBeUndefined(); + expect( + checkFinal(s, r.frames.at(-1)!, name === "stale-reader" ? 2 : 0, true) + ).toBeUndefined(); + } + } +); + +test("a waiting policy cannot turn missing final publication into success", () => { + const s = chain("sync"); + expect( + checkFinal(s, { at: "final completion", input: 0, show: true, outputs: { 0: [0, 4] } }, 1, true) + ).toMatchObject({ rule: "L1" }); +}); + +test("a retained permission requires a pending observation and matching write", () => { + expect(retainedObservations([], 1)).toEqual([]); + expect(retainedObservations([{ reader: 0, write: 1, at: "observed" }], 1)).toEqual([]); + expect( + retainedObservations([{ reader: 0, write: 1, at: "observed", disposedAt: "removed" }], 2) + ).toEqual([]); +}); + +test("disposing after a request settles creates no retained permission", async () => { + const s = disposed(); + s.turns.splice(1, 0, { steps: [{ op: "resolve", node: 0, which: "newest" }] }); + const r = await runScenario(s, { waitingPolicy: "retain-disposed" }); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect(r.waiting).toBeUndefined(); + expect(r.work.flatMap(w => w.observations).some(o => o.disposedAt)).toBe(false); +}); + +test("a later write is not covered by an earlier write's disposed observer", async () => { + const s = disposed(); + s.turns.push({ steps: [{ op: "write", value: 3 }] }); + const r = await runScenario(s, { waitingPolicy: "retain-disposed" }); + if (r.waiting) { + expect(r.waiting.retained).toEqual([]); + expect(r.waiting.disposition).toBe("out-of-scope"); + expect(r.status).not.toBe("pass"); + } + expect(r.work.flatMap(w => w.observations).some(o => o.write === 2 && o.disposedAt)).toBe(false); +}); + +test("returning to the same inputs leaves the obsolete occurrence for residual completion", async () => { + const s = chain(); + s.turns = [1, 2, 1].map(value => ({ steps: [{ op: "write", value }] })); + const result = await runScenario(s, { completionProbes: false }); + expect(result.status, JSON.stringify(result)).toBe("pass"); + expect(result.events).toContain("required completion: required 0:[1]#2"); + expect(result.events).not.toContain("required completion: required 0:[1]#1"); + expect(result.events).toContain("residual completion: residual 0:[1]#1"); + expect(result.frames.at(-1)?.outputs[0]).toEqual([1, 6]); +}); diff --git a/packages/signals/tests/semantics/complexity.ts b/packages/signals/tests/semantics/complexity.ts new file mode 100644 index 000000000..fff5e5c8c --- /dev/null +++ b/packages/signals/tests/semantics/complexity.ts @@ -0,0 +1,58 @@ +import { queuedSteps, sourceIds, type Scenario } from "./scenario.js"; +import { scenarioKey } from "./normalize.js"; + +/** A stable preference for less machinery, not shorter JSON or a bug identity. */ +export function complexity(s: Scenario): number[] { + let mechanisms = 0, + operations = 0, + expressions = 0; + for (const n of s.nodes) { + if (n.delivery !== "sync") mechanisms++; + expressions += n.deps.length + (n.branch ? n.branch.otherwise.length + 1 : 0); + if (n.factor !== 1) expressions++; + if (n.offset !== 0) expressions++; + } + for (const r of s.readers) { + expressions += r.refs.length; + mechanisms += Number(r.gated) + Number(r.boundary !== "none") + Number(r.mounted !== undefined); + mechanisms += Number(!!r.pending) + (r.pendingDepth ?? 0) + Number(!!r.render); + } + if (s.optimistic) mechanisms += 1 + s.optimistic.proposals.length; + for (const a of s.actions ?? []) { + mechanisms += a.segments.length; + for (const segment of a.segments) operations += segment.length; + } + for (const t of s.turns) { + mechanisms++; + if ("steps" in t) + for (const x of t.steps) { + operations++; + if (x.op === "queue") { + mechanisms++; + operations += queuedSteps(x).length; + } + if (x.op === "flush") mechanisms++; + } + } + return [ + sourceIds(s).length + s.nodes.length + s.readers.length, + mechanisms, + operations, + expressions + ]; +} + +export function compareSize(a: Scenario, b: Scenario): number { + const left = complexity(a), + right = complexity(b); + for (let i = 0; i < left.length; i++) if (left[i] !== right[i]) return left[i] - right[i]; + return 0; +} + +export function compareComplexity(a: Scenario, b: Scenario): number { + const size = compareSize(a, b); + if (size) return size; + const x = scenarioKey(a), + y = scenarioKey(b); + return x < y ? -1 : x > y ? 1 : 0; +} diff --git a/packages/signals/tests/semantics/coordinated-reduce.test.ts b/packages/signals/tests/semantics/coordinated-reduce.test.ts new file mode 100644 index 000000000..bee13890f --- /dev/null +++ b/packages/signals/tests/semantics/coordinated-reduce.test.ts @@ -0,0 +1,243 @@ +import { + branchReductions, + deliveryScheduleReductions, + lifecycleReductions, + observationCollapseReductions +} from "./reduce.js"; +import { evaluate, validate, type Scenario } from "./scenario.js"; +import { runScenario } from "./runner.js"; +import { shrink } from "./shrink.js"; + +function conditional(): Scenario { + return { + version: 2, + sources: [-1], + anchors: [], + anchorShow: false, + show: true, + nodes: [ + { id: 0, deps: [-1], factor: 1, offset: 2, delivery: "promise" }, + { + id: 1, + deps: [-1], + branch: { condition: -1, otherwise: [0] }, + factor: 1, + offset: 0, + delivery: "sync" + }, + { id: 2, deps: [-1, 1], factor: 2, offset: 3, delivery: "manual" } + ], + readers: [{ id: 0, refs: [2], gated: false, boundary: "none" }], + turns: [{ steps: [{ op: "write", value: 1 }] }], + strict: true + }; +} + +test("conditional folding rewrites both consumer paths and prunes the unused parent", () => { + const s = conditional(), + before = JSON.stringify(s); + const c = [...branchReductions(s)][0]; + expect(c.nodes.map(n => n.id)).toEqual([0, 2]); + expect(c.nodes[1].deps).toEqual([-1, -1]); + expect(c.nodes[1].branch).toEqual({ condition: -1, otherwise: [-1, 0] }); + expect(c.nodes[1].delivery).toBe("manual"); + for (const input of [0, 1, 2]) expect(evaluate(c, input).get(2)).toBe(evaluate(s, input).get(2)); + expect(validate(c)).toBeUndefined(); + expect(JSON.stringify(s)).toBe(before); + s.readers[0].refs.push(1); + expect([...branchReductions(s)][0].nodes.map(n => n.id)).toEqual([0, 1, 2]); +}); + +test("folding keeps surviving request identities, while removed owners become noops", () => { + const s = conditional(); + s.nodes[1].delivery = "manual"; + s.turns.push({ + steps: [ + { op: "resolve", node: 1, which: "newest", key: "1:[1]#1" }, + { + op: "resolve", + node: 2, + which: "newest", + key: "2:[1,1]#2", + variantKeys: ["2:[1,1]#2", null] + } + ] + }); + const c = [...branchReductions(s)][0]; + expect(c.turns[1]).toEqual({ + steps: [{ op: "noop" }, (s.turns[1] as { steps: unknown[] }).steps[1]] + }); +}); + +test("branch paths can be shortened together without changing the input scenario", () => { + const s = conditional(); + s.nodes[1] = { id: 1, deps: [-1], factor: 1, offset: 0, delivery: "promise" }; + s.nodes[2] = { + id: 2, + deps: [0], + branch: { condition: -1, otherwise: [1] }, + factor: 1, + offset: 0, + delivery: "promise" + }; + const before = JSON.stringify(s), + c = [...branchReductions(s)][0]; + expect(c.nodes).toEqual([ + { ...s.nodes[2], deps: [-1], branch: { condition: -1, otherwise: [-1] } } + ]); + expect(validate(c)).toBeUndefined(); + expect(JSON.stringify(s)).toBe(before); +}); + +function joined(): Scenario { + return { + version: 2, + sources: [-1, -2], + show: true, + nodes: [ + { id: 0, deps: [-1], factor: 1, offset: 0, delivery: "promise" }, + { id: 1, deps: [-2], factor: 1, offset: 0, delivery: "promise" }, + { id: 2, deps: [0, 1], factor: 1, offset: 0, delivery: "sync" } + ], + readers: [{ id: 0, refs: [2], gated: false, boundary: "none" }], + turns: [ + { + steps: [ + { op: "write", source: -1, value: 1 }, + { op: "flush" }, + { op: "write", source: -2, value: 1 } + ] + } + ] + }; +} + +test("delivery and event boundaries change together, keeping node identities and unrelated work", () => { + const s = joined(), + before = JSON.stringify(s); + const c = [...deliveryScheduleReductions(s)].find( + c => c.nodes[0].delivery === "manual" && c.turns.length === 2 + )!; + expect(c.nodes.map(n => n.delivery)).toEqual(["manual", "manual", "sync"]); + expect(c.turns).toEqual([ + { steps: [{ op: "write", source: -1, value: 1 }] }, + { steps: [{ op: "write", source: -2, value: 1 }] } + ]); + expect(validate(c)).toBeUndefined(); + expect(JSON.stringify(s)).toBe(before); +}); + +test("coordinated search crosses a passing intermediate without accepting that intermediate", async () => { + const s = joined(), + baseline = await runScenario(s); + const run = async (scenario: Scenario) => ({ + ...baseline, + scenario, + status: (scenario.nodes.length === 3 && + scenario.nodes[0]?.delivery === "manual" && + scenario.nodes[1]?.delivery === "manual" && + scenario.turns.length === 2 + ? "fail" + : "pass") as "fail" | "pass", + failure: undefined as undefined | { rule: string; message: string } + }); + const result = { + ...baseline, + status: "fail" as const, + failure: { rule: "S1", message: "synthetic" } + }; + const reduced = await shrink( + result, + async scenario => { + const r = await run(scenario); + if (r.status === "fail") r.failure = { rule: "L1", message: "synthetic smaller failure" }; + return r; + }, + 100, + { order: ["deliverySchedule"] } + ); + expect(reduced.accepted).toBe(1); + expect(reduced.result.scenario.nodes[0].delivery).toBe("manual"); + expect(reduced.result.scenario.turns).toHaveLength(2); + expect(reduced.result.failure?.rule).toBe("L1"); + expect(reduced.attempts).toBeLessThan(100); +}); + +test("observation collapse removes nested controls and their commands together", () => { + const s = joined(); + s.readers = [ + { id: 0, refs: [-1, 0], gated: false, boundary: "reset" }, + { id: 1, refs: [0, 2], gated: true, boundary: "none", parent: 0, pending: true } + ]; + s.turns.push({ + steps: [ + { op: "dispose", reader: 0 }, + { op: "click", reader: 1 } + ] + }); + const before = JSON.stringify(s), + c = [...observationCollapseReductions(s)][0]; + expect(c.readers).toEqual([{ id: 0, refs: [-1, 0, 2], gated: false, boundary: "none" }]); + expect(c.turns.at(-1)).toEqual({ steps: [{ op: "noop" }, { op: "noop" }] }); + expect(validate(c)).toBeUndefined(); + expect(JSON.stringify(s)).toBe(before); + s.readers[0].render = { on: -1, target: "portal", scheduled: true }; + expect([...observationCollapseReductions(s)]).toHaveLength(0); +}); + +test("multiple optimistic proposals lower to a direct write sequence including authority", () => { + const s: Scenario = { + version: 2, + sources: [-1, -2], + anchors: [-2], + anchorShow: false, + show: true, + optimistic: { proposals: [1, 2], authoritative: 3 }, + nodes: [{ id: 0, deps: [-2], factor: 1, offset: 0, delivery: "manual" }], + readers: [{ id: 0, refs: [0], gated: false, boundary: "none" }], + turns: [ + { steps: [{ op: "start-action" }] }, + { steps: [{ op: "resume-action" }] }, + { steps: [{ op: "resume-action" }] }, + { steps: [{ op: "resume-action" }, { op: "start-action" }] } + ] + }; + const before = JSON.stringify(s), + c = [...lifecycleReductions(s)].find(c => !c.optimistic)!; + expect(c.turns).toEqual([ + { steps: [{ op: "write", source: -1, value: 1 }] }, + { steps: [{ op: "write", source: -1, value: 2 }] }, + { steps: [{ op: "write", source: -1, value: 3 }] }, + { steps: [{ op: "noop" }, { op: "noop" }] } + ]); + expect(validate(c)).toBeUndefined(); + expect(JSON.stringify(s)).toBe(before); +}); + +test("more elaborate delivery forms always remove a node or explicit flush", () => { + const s = joined(); + s.turns = [{ steps: [{ op: "write", value: 1 }] }]; + for (const c of deliveryScheduleReductions(s)) { + if (c.nodes.some(n => n.delivery === "manual" || n.delivery === "await")) + expect(c.nodes.length).toBeLessThan(s.nodes.length); + expect(validate(c)).toBeUndefined(); + } +}); + +test("local delivery pairs preserve mixed async paths and surviving exact requests", () => { + const s = joined(); + s.nodes[0].delivery = "manual"; + s.nodes[1].delivery = "manual"; + s.readers[0].refs = [0, 1, 2]; + const flight = { op: "resolve" as const, node: 1, which: "newest" as const, key: "1:[1]#2" }; + s.turns.push({ steps: [{ op: "resolve", node: 0, which: "newest", key: "0:[1]#1" }, flight] }); + const before = JSON.stringify(s); + const c = [...deliveryScheduleReductions(s)].find( + c => + c.nodes.length === 2 && c.nodes[0].delivery === "promise" && c.nodes[1].delivery === "manual" + )!; + expect(c).toBeDefined(); + expect(c.turns.at(-1)).toEqual({ steps: [{ op: "noop" }, flight] }); + expect(validate(c)).toBeUndefined(); + expect(JSON.stringify(s)).toBe(before); +}); diff --git a/packages/signals/tests/semantics/corpus-reduce.test.ts b/packages/signals/tests/semantics/corpus-reduce.test.ts new file mode 100644 index 000000000..4d1fc930e --- /dev/null +++ b/packages/signals/tests/semantics/corpus-reduce.test.ts @@ -0,0 +1,202 @@ +import { chain } from "./fixtures.js"; +import { arithmeticReductions, focusedReductions, structuralReductions } from "./reduce.js"; +import { reductionFamilies } from "./shrink.js"; +import { runScenario } from "./runner.js"; +import { evaluate, validate, type Scenario } from "./scenario.js"; + +function affine(): Scenario { + const s = chain("sync"); + s.nodes[1].delivery = "manual"; + s.turns = [ + { steps: [{ op: "write", value: 2 }] }, + { steps: [{ op: "resolve", node: 1, which: "newest", key: "1:[5]#1" }] }, + { steps: [{ op: "write", value: 3 }] }, + { steps: [{ op: "resolve", node: 1, which: "newest", key: "1:[7]#1" }] }, + { steps: [{ op: "write", value: 2 }] }, + { steps: [{ op: "resolve", node: 1, which: "newest", key: "1:[5]#2" }] } + ]; + s.strict = true; + return s; +} + +function translated(s: Scenario): Scenario | undefined { + return [...focusedReductions(s)].find( + c => c.nodes[1].deps[0] === -1 && JSON.stringify(c.turns) !== JSON.stringify(s.turns) + ); +} + +test("affine input folding resolves the corresponding requests, including repeated inputs", async () => { + const s = affine(), + before = structuredClone(s), + c = translated(s)!; + expect(validate(c)).toBeUndefined(); + expect(c.turns[5]).toEqual({ + steps: [{ op: "resolve", node: 1, which: "newest", key: "1:[2]#2" }] + }); + for (const input of [0, 2, 3]) expect(evaluate(c, input)).toEqual(evaluate(s, input)); + const a = await runScenario(s), + b = await runScenario(c); + expect(a.status).toBe("pass"); + expect(b.status).toBe("pass"); + expect(b.frames).toEqual(a.frames); + expect(s).toEqual(before); +}); + +test("affine translation preserves queued boundaries, paired nulls and request occurrences", () => { + const s = affine(); + s.turns = [ + { + steps: [ + { + op: "queue", + id: 0, + via: "task", + steps: [ + { + op: "resolve", + node: 1, + which: "newest", + key: "1:[5]#2", + variantKeys: [null, "1:[7]#3"] + }, + { op: "resolve", node: 0, which: "oldest", key: "0:[2]#1" } + ] + } + ] + }, + { task: 0 } + ]; + const c = translated(s)!; + expect(c.turns).toEqual([ + { + steps: [ + { + op: "queue", + id: 0, + via: "task", + steps: [ + { + op: "resolve", + node: 1, + which: "newest", + key: "1:[2]#2", + variantKeys: [null, "1:[3]#3"] + }, + { op: "resolve", node: 0, which: "oldest", key: "0:[2]#1" } + ] + } + ] + }, + { task: 0 } + ]); +}); + +test("affine translation rejects malformed, noninvertible and inexact questions", () => { + for (const key of [ + "1:[4]#1", + "0:[5]#1", + "01:[5]#1", + "1:[5.0]#1", + "1:[5,7]#1", + "1:[5]#01", + "1:[x]#1", + "1:[9007199254740992]#1", + "1:[-0]#1" + ]) { + const s = affine(); + s.turns = [{ steps: [{ op: "resolve", node: 1, which: "newest", key }] }]; + expect(translated(s), key).toBeUndefined(); + } + for (const factor of [0, 0.5, Number.MAX_SAFE_INTEGER + 1]) { + const s = affine(); + s.nodes[0].factor = factor; + expect(translated(s)).toBeUndefined(); + } + const asyncParent = affine(); + asyncParent.nodes[0].delivery = "manual"; + expect(translated(asyncParent)).toBeUndefined(); +}); + +test("removing a mounting mechanism retains unrelated callbacks and controls", () => { + const s: Scenario = { ...chain(), version: 2, sources: [-1] }; + s.readers[0].mounted = true; + s.turns = [ + { + steps: [ + { + op: "queue", + id: 0, + via: "microtask", + steps: [ + { op: "mount", reader: 0, value: false }, + { op: "show", value: false } + ] + } + ] + } + ]; + const before = structuredClone(s); + const c = [...structuralReductions(s)].find( + c => c.readers.length === 1 && c.readers[0].mounted === undefined + )!; + expect(c.turns).toEqual([ + { + steps: [ + { + op: "queue", + id: 0, + via: "microtask", + steps: [{ op: "noop" }, { op: "show", value: false }] + } + ] + } + ]); + expect(validate(c)).toBeUndefined(); + expect(s).toEqual(before); +}); + +test("direct tuple reads preserve input order and do not flatten async or branch computations", () => { + const s = chain("sync"); + s.nodes[1].deps = [0, -1]; + const c = [...structuralReductions(s)].find(c => c.readers[0]?.refs.length === 3)!; + expect(c.readers[0].refs).toEqual([-1, 0, -1]); + expect(c.nodes).toEqual(s.nodes); + s.nodes[1].delivery = "promise"; + expect([...structuralReductions(s)].some(c => c.readers[0]?.refs.length === 3)).toBe(false); + s.nodes[1].delivery = "sync"; + s.nodes[1].branch = { condition: -1, otherwise: [0] }; + expect([...structuralReductions(s)].some(c => c.readers[0]?.refs.length === 3)).toBe(false); +}); + +test("joining event bodies retains work order, callback boundaries and explicit flushes", () => { + const s = chain(); + s.turns = [ + { steps: [{ op: "write", value: 1 }, { op: "flush" }] }, + { steps: [{ op: "queue", id: 0, via: "task", step: { op: "write", value: 2 } }] }, + { task: 0 }, + { steps: [{ op: "write", value: 3 }] } + ]; + const before = structuredClone(s); + const candidates = [...reductionFamilies.turns(s)]; + const c = candidates.find(c => "steps" in c.turns[0] && c.turns[0].steps.length === 3)!; + expect(c.turns).toEqual([ + { + steps: [ + { op: "write", value: 1 }, + { op: "flush" }, + { op: "queue", id: 0, via: "task", step: { op: "write", value: 2 } } + ] + }, + { task: 0 }, + { steps: [{ op: "write", value: 3 }] } + ]); + expect(s).toEqual(before); +}); + +test("offsets can be removed together without translating captured questions", () => { + const s = affine(); + const c = [...arithmeticReductions(s)].find(c => c.nodes.every(n => n.offset === 0))!; + expect(c).toBeDefined(); + expect(c.turns).toEqual(s.turns); + expect(s.nodes[0].offset).toBe(1); +}); diff --git a/packages/signals/tests/semantics/corpus.ts b/packages/signals/tests/semantics/corpus.ts new file mode 100644 index 000000000..746514b80 --- /dev/null +++ b/packages/signals/tests/semantics/corpus.ts @@ -0,0 +1,447 @@ +import type { Scenario } from "./scenario.js"; + +/** Reduced generated witnesses; classifications never suppress executable rules. */ +export const corpus: Array<{ + name: string; + classification: string; + seed?: number; + index?: number; + foundOn: string; + scenario: Scenario; +}> = [ + { + name: "disposed-reader", + classification: "historical-disposal-waiting; released by #3392", + seed: 3289, + index: 17, + foundOn: "005a623864ffd7cc3c76628369588ddcc4df26a5", + scenario: { + version: 1, + show: false, + nodes: [ + { + id: 0, + deps: [-1], + factor: 1, + offset: 0, + delivery: "manual" + }, + { + id: 1, + deps: [0], + factor: 1, + offset: 0, + delivery: "sync" + } + ], + readers: [ + { + refs: [1], + gated: false, + id: 0, + boundary: "none" + } + ], + turns: [ + { + steps: [ + { + op: "write", + value: 2 + } + ] + }, + { + steps: [ + { + op: "dispose", + reader: 0 + } + ] + } + ], + strict: true + } + }, + { + name: "stale-reader", + classification: "candidate-failure", + seed: 3289, + index: 73, + foundOn: "005a623864ffd7cc3c76628369588ddcc4df26a5", + scenario: { + version: 1, + show: false, + nodes: [ + { + id: 0, + deps: [-1], + factor: 1, + offset: 0, + delivery: "sync" + }, + { + id: 1, + deps: [0], + factor: 1, + offset: 0, + delivery: "manual" + }, + { + id: 2, + deps: [0], + factor: 1, + offset: 0, + delivery: "promise" + } + ], + readers: [ + { + refs: [-1, 1], + gated: false, + id: 1, + boundary: "none" + }, + { + refs: [-1, 2], + gated: true, + id: 2, + boundary: "none" + } + ], + turns: [ + { + steps: [ + { + op: "queue", + id: 0, + via: "task", + step: { + op: "show", + value: true + } + } + ] + }, + { + steps: [ + { + op: "write", + value: 2 + } + ] + } + ], + strict: true + } + }, + { + name: "stuck-hidden", + classification: "candidate-failure", + seed: 3289, + index: 168, + foundOn: "005a623864ffd7cc3c76628369588ddcc4df26a5", + scenario: { + version: 1, + show: true, + nodes: [ + { + id: 0, + deps: [-1], + factor: 1, + offset: 0, + delivery: "manual" + } + ], + readers: [ + { + refs: [-1], + gated: true, + id: 0, + boundary: "none" + }, + { + refs: [0], + gated: true, + id: 2, + boundary: "none" + } + ], + turns: [ + { + steps: [ + { + op: "queue", + id: 1, + via: "promise", + step: { + op: "show", + value: false + } + }, + { + op: "write", + value: 3 + } + ] + }, + { + steps: [ + { + op: "show", + value: true + } + ] + }, + { + steps: [ + { + op: "write", + value: 0 + } + ] + } + ], + strict: true + } + }, + { + name: "late-pending-read", + classification: "fixed-on-pr-3347", + seed: 3289, + index: 7045, + foundOn: "005a623864ffd7cc3c76628369588ddcc4df26a5", + scenario: { + version: 1, + show: false, + nodes: [ + { + id: 0, + deps: [-1], + factor: 1, + offset: 0, + delivery: "await" + }, + { + id: 1, + deps: [0], + factor: 1, + offset: 0, + delivery: "manual" + } + ], + readers: [ + { + refs: [0], + gated: false, + id: 0, + boundary: "retain" + }, + { + refs: [-1, 0, 1], + gated: true, + id: 2, + boundary: "none" + } + ], + turns: [ + { + steps: [ + { + op: "write", + value: 2 + } + ] + }, + { + steps: [ + { + op: "resolve", + node: 0, + which: "newest", + key: "0:[2]#1" + } + ] + }, + { + steps: [ + { + op: "write", + value: 0 + } + ] + }, + { + steps: [ + { + op: "queue", + id: 4, + via: "task", + step: { + op: "show", + value: true + } + } + ] + } + ], + strict: true + } + }, + { + name: "disposed-pending-control", + classification: "candidate-failure", + seed: 3289, + index: 524, + foundOn: "005a623864ffd7cc3c76628369588ddcc4df26a5", + scenario: { + version: 1, + show: false, + nodes: [ + { + id: 0, + deps: [-1], + factor: 1, + offset: 0, + delivery: "sync" + }, + { + id: 1, + deps: [-1], + factor: 1, + offset: 0, + delivery: "sync" + }, + { + id: 2, + deps: [0, 1], + factor: 1, + offset: 0, + delivery: "manual" + } + ], + readers: [ + { + refs: [2, 0], + gated: true, + id: 0, + boundary: "none" + } + ], + turns: [ + { + steps: [ + { + op: "write", + value: 1 + } + ] + }, + { + steps: [ + { + op: "show", + value: true + } + ] + }, + { + steps: [ + { + op: "dispose", + reader: 0 + } + ] + } + ], + strict: true + } + }, + { + name: "optimistic-correction-settled", + classification: "experimental-completion-candidate", + foundOn: "344ed054a932c7508efe57908eb175ed67d54940", + scenario: { + version: 2, + sources: [-1, -2], + show: true, + optimistic: { proposals: [1], authoritative: 0 }, + nodes: [{ id: 0, deps: [-2], factor: 2, offset: 0, delivery: "manual" }], + readers: [{ id: 0, refs: [-2, 0], gated: false, boundary: "none" }], + turns: [ + { steps: [{ op: "start-action" }] }, + { steps: [{ op: "resolve", node: 0, which: "newest" }] } + ] + } + }, + { + name: "latest-late-mount", + classification: "final-publication-candidate", + foundOn: "344ed054a932c7508efe57908eb175ed67d54940", + scenario: { + version: 2, + sources: [-1, -2], + anchors: [-1], + show: true, + optimistic: { kind: "latest", proposals: [1], authoritative: 1, hold: true }, + nodes: [], + readers: [{ id: 0, refs: [-2], gated: false, boundary: "none", mounted: false }], + turns: [ + { steps: [{ op: "start-action" }] }, + { steps: [{ op: "mount", reader: 0, value: true }] } + ] + } + }, + { + name: "held-sync-verdict", + classification: "readiness-candidate", + foundOn: "344ed054a932c7508efe57908eb175ed67d54940", + scenario: { + version: 2, + sources: [-1], + show: true, + nodes: [ + { id: 0, deps: [-1], factor: 1, offset: 0, delivery: "sync" }, + { id: 1, deps: [-1], factor: 1, offset: 0, delivery: "manual" } + ], + readers: [ + { id: 0, refs: [0], gated: false, boundary: "none", pending: true }, + { id: 1, refs: [1], gated: false, boundary: "none" } + ], + turns: [{ steps: [{ op: "write", value: 1 }] }] + } + }, + { + name: "hidden-reader-release", + classification: "ideal-progress-candidate; distinct from explicit disposal", + foundOn: "bc54629bf58f55f47e0f4ae369919d643e85594a", + scenario: { + version: 1, + show: true, + nodes: [{ id: 0, deps: [-1], factor: 1, offset: 0, delivery: "manual" }], + readers: [{ id: 0, refs: [0], gated: true, boundary: "none" }], + turns: [{ steps: [{ op: "write", value: 1 }] }, { steps: [{ op: "show", value: false }] }] + } + }, + { + name: "mixed-effect-entanglement", + classification: + "ideal-independent-publication-candidate; shared effect reads async answers and their sources", + foundOn: "bc54629bf58f55f47e0f4ae369919d643e85594a", + scenario: { + version: 2, + sources: [-1, -2], + show: true, + nodes: [ + { id: 0, deps: [-1], factor: 1, offset: 0, delivery: "manual" }, + { id: 1, deps: [-2], factor: 1, offset: 0, delivery: "manual" } + ], + readers: [{ id: 0, refs: [0, 1, -1, -2], gated: false, boundary: "none" }], + turns: [ + { steps: [{ op: "write", source: -1, value: 1 }] }, + { steps: [{ op: "write", source: -2, value: 1 }] }, + { steps: [{ op: "resolve", node: 0, which: "newest" }] } + ] + } + } +]; diff --git a/packages/signals/tests/semantics/discovery-reduce.test.ts b/packages/signals/tests/semantics/discovery-reduce.test.ts new file mode 100644 index 000000000..4febb894a --- /dev/null +++ b/packages/signals/tests/semantics/discovery-reduce.test.ts @@ -0,0 +1,228 @@ +import { chain } from "./fixtures.js"; +import { runScenario, type RunResult } from "./runner.js"; +import { fingerprint, isSemanticFailure, shrink } from "./shrink.js"; +import { + coneReductions, + lifecycleReductions, + observationReductions, + scheduleReductions +} from "./reduce.js"; +import { validate, type Scenario } from "./scenario.js"; +const fail = (r: RunResult, rule = "S1"): RunResult => ({ + ...r, + status: "fail", + failure: { rule, message: `synthetic ${rule}` } +}); + +test("discovery accepts another semantic bug and reports the final fingerprint", async () => { + const baseline = await runScenario(chain("sync")); + const original = fail(baseline); + const run = async (scenario: Scenario) => fail({ ...baseline, scenario }, "L1"); + const reduced = await shrink(original, run, 10); + expect(reduced.accepted).toBeGreaterThan(0); + expect(fingerprint(reduced.result)).toBe("L1: synthetic L1"); + expect(reduced.reproKey).toMatch(/^L1: synthetic L1\n/); + const focused = await shrink(original, run, 10, { mode: "focused" }); + expect(focused.accepted).toBe(0); + expect(focused.result).toBe(original); +}); + +test("discovery rejects invalid, unsupported, waived and harness-failure outcomes", async () => { + const baseline = await runScenario(chain("sync")); + const original = fail(baseline); + const rejected: RunResult[] = [ + ...(["pass", "policy", "invalid", "inapplicable", "limit", "error"] as const).map(status => ({ + ...original, + status + })), + { ...original, error: "worker error" }, + { ...original, cleanupError: "cleanup error" }, + fail(baseline, "unregistered-rule"), + fail(baseline, "W1"), + { + ...fail(baseline, "P1"), + progress: [ + { + failure: { rule: "P1", message: "waived" }, + outstanding: [], + allowance: "legacy-disposal-wait" + } + ] + } + ]; + for (const result of rejected) { + expect(isSemanticFailure(result)).toBe(false); + expect((await shrink(original, async scenario => ({ ...result, scenario }), 1)).accepted).toBe( + 0 + ); + } + expect(isSemanticFailure({ ...original, progress: rejected.at(-1)!.progress })).toBe(true); +}); + +test("waiting review cannot substitute for a bug but an explicit violation can", async () => { + const baseline = await runScenario(chain("sync")); + const frame = baseline.frames.at(-1)!; + const result: RunResult = { + ...fail(baseline, "W1"), + waiting: { + rule: "W1", + message: "synthetic wait", + policy: "required-only", + disposition: "violation", + scope: "other", + before: frame, + afterRetained: frame, + after: frame, + pending: [], + retained: [], + releases: [], + recovered: true + } + }; + expect(isSemanticFailure(result)).toBe(true); + for (const disposition of ["open", "allowed", "out-of-scope"] as const) { + result.waiting!.disposition = disposition; + expect(isSemanticFailure(result)).toBe(false); + } +}); + +test("passing controls still constrain discovery and share its budget", async () => { + const baseline = await runScenario(chain("sync")), + original = fail(baseline); + const run = vi.fn(async (scenario: Scenario) => fail({ ...baseline, scenario }, "L1")); + const control = vi.fn(async (scenario: Scenario) => + scenario === original.scenario ? baseline : fail({ ...baseline, scenario }) + ); + const reduced = await shrink(original, run, 10, { control }); + expect(reduced.accepted).toBe(0); + expect(reduced.attempts).toBe(run.mock.calls.length + control.mock.calls.length); + expect(reduced.attempts).toBeLessThanOrEqual(10); +}); + +function optimistic(): Scenario { + return { + version: 2, + sources: [-1, -2], + anchors: [-2], + anchorShow: false, + optimistic: { proposals: [1], authoritative: 0 }, + nodes: [{ id: 0, deps: [-2], factor: 1, offset: 0, delivery: "manual" }], + readers: [{ id: 0, refs: [-2, 0], gated: false, boundary: "none" }], + show: true, + turns: [{ steps: [{ op: "start-action" }] }], + strict: true + }; +} + +test("tuple projection keeps the data reader, order and unique anchors", () => { + const s = optimistic(); + s.readers[0].refs = [-2, 0, -2]; + const original = JSON.stringify(s); + const c = [...observationReductions(s)].find(c => c.readers[0]?.refs.length === 1)!; + expect(c.anchors).toEqual([-2]); + expect(c.readers[0].refs).toEqual([0]); + expect(c.readers[0].id).toBe(0); + expect(validate(c)).toBeUndefined(); + expect(JSON.stringify(s)).toBe(original); + s.turns.push({ steps: [{ op: "click", reader: 0 }] }); + expect([...observationReductions(s)]).toHaveLength(0); +}); + +test("ordinary substitution remaps source roles and explicit authority without editing the input", () => { + const s = optimistic(); + s.nodes.push({ + id: 1, + deps: [-2], + branch: { condition: -2, otherwise: [0] }, + factor: 1, + offset: 0, + delivery: "sync" + }); + s.turns.push({ steps: [{ op: "read", ref: -2, mode: "plain" }, { op: "resume-action" }] }); + const original = JSON.stringify(s), + c = [...lifecycleReductions(s)].find(c => !c.optimistic)!; + expect(c.sources).toEqual([-1]); + expect(c.anchors).toEqual([-1]); + expect(c.nodes[1].branch).toEqual({ condition: -1, otherwise: [0] }); + expect(c.readers[0].refs).toEqual([-1, 0]); + expect(c.turns).toEqual([ + { steps: [{ op: "write", source: -1, value: 1 }] }, + { + steps: [ + { op: "read", ref: -1, mode: "plain" }, + { op: "write", source: -1, value: 0 } + ] + } + ]); + expect(validate(c)).toBeUndefined(); + expect(JSON.stringify(s)).toBe(original); + s.readers[0].refs.push(-1); + expect([...lifecycleReductions(s)]).toHaveLength(0); +}); + +test("proposal and authority change together when immediate completion is removed", () => { + const s = optimistic(); + s.optimistic = { proposals: [0], authoritative: 2 }; + s.turns[0] = { steps: [{ op: "start-action" }, { op: "resume-action" }] }; + const c = [...lifecycleReductions(s)].find(c => c.optimistic?.proposals[0] === 2)!; + expect(c.optimistic?.authoritative).toBe(0); + expect(c.turns).toEqual([{ steps: [{ op: "start-action" }] }]); + expect(validate(c)).toBeUndefined(); +}); + +test("queue movement preserves other work and refuses addressed queues", () => { + const s = optimistic(); + s.turns = [ + { + steps: [ + { op: "queue", id: 0, via: "microtask", step: { op: "show", value: false } }, + { op: "start-action" } + ] + } + ]; + const original = JSON.stringify(s), + c = [...scheduleReductions(s)][0]; + expect(c.turns).toEqual([ + { steps: [{ op: "start-action" }] }, + { steps: [{ op: "show", value: false }] } + ]); + expect(validate(c)).toBeUndefined(); + expect(JSON.stringify(s)).toBe(original); + s.turns.push({ steps: [{ op: "cancel", id: 0 }] }); + expect([...scheduleReductions(s)]).toHaveLength(0); +}); + +test("cone projection translates exact and paired occurrences and rejects mismatched inputs", () => { + const s = optimistic(); + s.nodes = [ + { id: 0, deps: [-2, -2], factor: 2, offset: 0, delivery: "sync" }, + { id: 1, deps: [-2, 0], factor: 1, offset: 0, delivery: "manual" } + ]; + s.readers[0].refs = [1]; + s.turns.push({ + steps: [ + { + op: "resolve", + node: 1, + which: "oldest", + key: "1:[3,12]#2", + variantKeys: ["1:[3,12]#2", null] + } + ] + }); + const original = JSON.stringify(s), + c = [...coneReductions(s)][0]; + expect(c.nodes).toEqual([{ id: 1, deps: [-2], factor: 1, offset: 0, delivery: "manual" }]); + expect(c.turns[1]).toEqual({ + steps: [ + { op: "resolve", node: 1, which: "oldest", key: "1:[3]#2", variantKeys: ["1:[3]#2", null] } + ] + }); + expect(validate(c)).toBeUndefined(); + expect(JSON.stringify(s)).toBe(original); + s.turns[1] = { steps: [{ op: "resolve", node: 1, which: "newest", key: "1:[3,8]#2" }] }; + expect([...coneReductions(s)]).toHaveLength(0); + s.turns.pop(); + s.readers[0].refs.push(0); + expect([...coneReductions(s)]).toHaveLength(0); +}); diff --git a/packages/signals/tests/semantics/edge-reduce.test.ts b/packages/signals/tests/semantics/edge-reduce.test.ts new file mode 100644 index 000000000..d4a606ae3 --- /dev/null +++ b/packages/signals/tests/semantics/edge-reduce.test.ts @@ -0,0 +1,136 @@ +import { chain } from "./fixtures.js"; +import { edgeReductions } from "./reduce.js"; +import { orderingReductions } from "./order.js"; +import { scenarioKey } from "./normalize.js"; +import { validate, type Scenario, type Step } from "./scenario.js"; + +test("single-use bypass retains other consumers, warmups and exact flights", () => { + const s: Scenario = { ...chain("manual"), version: 2, sources: [-1], warmLatest: [0] }; + s.nodes[1].delivery = "manual"; + s.readers.push({ id: 1, refs: [0], gated: false, boundary: "none" }); + s.turns.push({ + steps: [ + { op: "resolve", node: 1, which: "newest", key: "1:[3]#2", variantKeys: [null, "1:[3]#1"] } + ] + }); + const before = structuredClone(s); + const candidates = [...edgeReductions(s)]; + const memo = candidates.find(c => c.nodes[1].deps[0] === -1)!; + expect(memo.nodes[0]).toBe(s.nodes[0]); + expect(memo.readers).toBe(s.readers); + expect(memo.turns).toBe(s.turns); + expect(memo.warmLatest).toBe(s.warmLatest); + const reader = candidates.find(c => c.readers[0].refs[1] === 0)!; + expect(reader.readers[1]).toBe(s.readers[1]); + expect(reader.nodes).toBe(s.nodes); + expect(reader.turns).toBe(s.turns); + expect(s).toEqual(before); + for (const c of candidates) expect(validate(c)).toBeUndefined(); +}); + +test("branch-edge bypass changes only the chosen arm and keeps its condition", () => { + const s: Scenario = { ...chain("manual"), version: 2, sources: [-1, -2] }; + s.nodes[0].deps = [-1, -1]; + s.nodes[0].branch = { condition: -2, otherwise: [-1, -2] }; + s.nodes[1].branch = { condition: 0, otherwise: [0] }; + const cs = [...edgeReductions(s)]; + const c = cs.find(c => c.nodes[1].branch?.otherwise[0] === -2)!; + expect(c.nodes[1]).toEqual({ ...s.nodes[1], branch: { condition: 0, otherwise: [-2] } }); + expect(c.nodes[0]).toBe(s.nodes[0]); + expect(new Set(cs.map(c => JSON.stringify(c))).size).toBe(cs.length); + for (const c of cs) expect(validate(c)).toBeUndefined(); +}); + +test("visibility and mounting setters can sort within their own callback", () => { + const s: Scenario = { ...chain(), version: 2, sources: [-1] }; + s.readers[0].mounted = false; + s.turns = [ + { + steps: [ + { op: "write", value: 1 }, + { op: "show", value: true } + ] + }, + { + steps: [ + { + op: "queue", + id: 0, + via: "microtask", + steps: [ + { op: "write", value: 2 }, + { op: "mount", reader: 0, value: true } + ] + } + ] + } + ]; + const before = structuredClone(s); + const cs = [...orderingReductions(s)]; + expect( + cs.some( + c => + JSON.stringify(c.turns[0]) === + JSON.stringify({ + steps: [ + { op: "show", value: true }, + { op: "write", source: -1, value: 1 } + ] + }) + ) + ).toBe(true); + expect( + cs.some( + c => + JSON.stringify(c.turns[1]) === + JSON.stringify({ + steps: [ + { + op: "queue", + id: 0, + via: "microtask", + steps: [ + { op: "mount", reader: 0, value: true }, + { op: "write", source: -1, value: 2 } + ] + } + ] + }) + ) + ).toBe(true); + for (const c of cs) { + expect(scenarioKey(c) < scenarioKey(s)).toBe(true); + expect(c.turns.length).toBe(s.turns.length); + } + expect(s).toEqual(before); +}); + +test("setter ordering does not swap same-target writes or cross reads, flushes and turns", () => { + const s: Scenario = { ...chain("sync"), version: 2, sources: [-1] }; + s.readers[0].mounted = true; + const cases: Step[][] = [ + [ + { op: "write", value: 2 }, + { op: "write", value: 1 } + ], + [ + { op: "show", value: true }, + { op: "show", value: false } + ], + [ + { op: "mount", reader: 0, value: true }, + { op: "mount", reader: 0, value: false } + ], + [{ op: "write", value: 2 }, { op: "flush" }, { op: "show", value: false }], + [ + { op: "write", value: 2 }, + { op: "read", ref: 0, mode: "latest" }, + { op: "show", value: false } + ] + ]; + for (const steps of cases) { + s.turns = [{ steps }, { steps: [{ op: "show", value: false }] }]; + for (const c of orderingReductions(s)) + expect(c.turns).toEqual(JSON.parse(scenarioKey(s)).turns); + } +}); diff --git a/packages/signals/tests/semantics/equivalence.test.ts b/packages/signals/tests/semantics/equivalence.test.ts new file mode 100644 index 000000000..70c12645c --- /dev/null +++ b/packages/signals/tests/semantics/equivalence.test.ts @@ -0,0 +1,119 @@ +import { chain } from "./fixtures.js"; +import { runScenario } from "./runner.js"; +import { runEquivalence, shrinkEquivalence } from "./equivalence.js"; +import type { Scenario } from "./scenario.js"; + +test("paired reduction preserves the failing delivery and removes unused graph structure", async () => { + const s = chain(); + s.nodes.push({ id: 2, deps: [-1], factor: 1, offset: 0, delivery: "sync" }); + // A fake lost publication calibrates the reducer, not Solid. Actual runtime + // mutation detection is independently covered in calibration.test.ts. + const run = async (scenario: Scenario) => { + const r = await runScenario(scenario); + if (scenario.nodes.some(n => n.id === 0 && n.delivery === "await")) + r.frames = r.frames.slice(0, 1); + return r; + }; + const original = await runEquivalence(s, run); + expect(original.signature).toContain("sync-await: E3"); + const reduced = await shrinkEquivalence(s, run); + expect(reduced.signature).toBe(original.signature); + expect(reduced.accepted).toBeGreaterThan(0); + expect(reduced.scenario.nodes.some(n => n.id === 2)).toBe(false); + expect((await runEquivalence(reduced.scenario, run)).signature).toBe(original.signature); + expect((await runEquivalence(reduced.scenario, runScenario)).result.status).toBe("pass"); +}); + +test("paired reduction budgets interpreter executions and never runs a partial comparison", async () => { + const s = chain(); + const run = async (scenario: Scenario) => { + const r = await runScenario(scenario); + return scenario.nodes[0]?.delivery === "await" + ? { ...r, status: "fail" as const, failure: { rule: "test", message: "await failure" } } + : r; + }; + const reduced = await shrinkEquivalence(s, run, 1); + expect(reduced.attempts).toBe(0); + expect(reduced.initialExecutions).toBe(3); + expect(reduced.signature).toContain("variant-2"); + const complete = await shrinkEquivalence(s, run, 3); + expect(complete.attempts).toBe(3); + expect(complete.signature).toContain("variant-2"); +}); + +test("paired comparison retains a waived finding from an otherwise passing variant", async () => { + const run = async (scenario: Scenario) => { + const r = await runScenario(scenario); + if (scenario.nodes[0]?.delivery === "sync") + r.progress = [ + { + failure: { rule: "P1", message: "synthetic progress finding" }, + outstanding: [], + allowance: "legacy-disposal-wait" + } + ]; + return r; + }; + const paired = await runEquivalence(chain(), run); + expect(paired.result.status).toBe("pass"); + expect(paired.signature).toContain("variant-0: P1"); + expect(paired.result.progress?.[0].allowance).toBe("legacy-disposal-wait"); + const failing = await runEquivalence(chain(), async s => { + const r = await run(s); + if (s.nodes[0]?.delivery === "await") { + r.status = "fail"; + r.failure = { rule: "S1", message: "synthetic inconsistency" }; + } + return r; + }); + expect(failing.signature).toContain("variant-2: S1"); +}); + +test("discovery can export a real variant failure as an ordinary reduced scenario", async () => { + const run = async (s: Scenario) => { + const result = await runScenario(s); + return s.nodes.some(n => n.delivery === "await") + ? { + ...result, + status: "fail" as const, + failure: { rule: "L1", message: "lost final answer" } + } + : result; + }; + const reduced = await shrinkEquivalence(chain(), run, 30); + expect(reduced.single).toBe(true); + expect(reduced.signature).toBe("L1: lost final answer"); + expect(reduced.pair).toEqual([]); + expect((await run(reduced.scenario)).failure?.rule).toBe("L1"); + expect(reduced.attempts).toBeLessThanOrEqual(30); + const focused = await shrinkEquivalence(chain(), run, 10, "delivery", { mode: "focused" }); + expect(focused.single).toBe(false); + expect(focused.signature).toContain("variant-2: L1"); +}); + +test("standalone latest failure retains only its side's exact request choices", async () => { + const s = chain("manual"); + s.turns.push({ + steps: [ + { op: "resolve", node: 0, which: "newest", variantKeys: [null, null] }, + { op: "read", ref: -1, mode: "latest" } + ] + }); + const run = async (scenario: Scenario) => { + const r = await runScenario(scenario); + return scenario.warmLatest?.length + ? { + ...r, + status: "fail" as const, + failure: { rule: "L1", message: "synthetic warm failure" } + } + : r; + }; + // Zero budget isolates paired-to-standalone export from later pruning. + const reduced = await shrinkEquivalence(s, run, 0, "latest"); + expect(reduced.single).toBe(true); + expect(reduced.scenario.warmLatest).toEqual([-1]); + expect(JSON.stringify(reduced.scenario)).not.toContain("variantKeys"); + expect(reduced.scenario.turns.at(-1)).toMatchObject({ steps: [{ op: "noop" }, { op: "read" }] }); + expect((await run(reduced.scenario)).failure).toEqual(reduced.result.failure); +}); diff --git a/packages/signals/tests/semantics/equivalence.ts b/packages/signals/tests/semantics/equivalence.ts new file mode 100644 index 000000000..9afea92ba --- /dev/null +++ b/packages/signals/tests/semantics/equivalence.ts @@ -0,0 +1,250 @@ +import { queuedSteps, validate, type Leaf, type Scenario } from "./scenario.js"; +import { search, SearchBudget } from "./search.js"; +import type { RunResult } from "./runner.js"; +import { dataTrace } from "./rules.js"; +import { fingerprint, isSemanticFailure, shrink, type ShrinkOptions } from "./shrink.js"; + +/** E3 has deliberately narrow preconditions: settled initial state, one write, + * pure derivations and continuously observed readers with no fallback. Timing + * may add stuttering but must not change the sequence of data publications. */ +export type Comparison = "delivery" | "latest" | "optimistic"; +function leaves(s: Scenario): Leaf[] { + return s.turns.flatMap(turn => + "steps" in turn + ? turn.steps.flatMap(step => + step.op === "queue" ? queuedSteps(step) : step.op === "cancel" ? [] : [step] + ) + : [] + ); +} + +export function variants(s: Scenario, comparison: Comparison = "delivery"): Scenario[] { + if (s.readers.some(r => r.render)) + throw new Error("Output attachment equivalence needs a visible-output contract"); + if (s.actions) throw new Error("Action-script equivalence needs a separate lifetime contract"); + if (s.readers.some(r => r.parent !== undefined)) + throw new Error("Nested-region equivalence is outside the current paired contracts"); + if (comparison === "optimistic") { + if (s.version !== 1 || s.optimistic) + throw new Error("Optimistic equivalence currently requires a single ordinary source graph"); + const base: Scenario = { + version: 2, + sources: [-1, -2], + show: true, + nodes: s.nodes.map(n => ({ ...n, deps: n.deps.map(id => (id === -1 ? -2 : id)) })), + readers: s.readers.map(r => ({ + ...r, + refs: r.refs.map(id => (id === -1 ? -2 : id)), + gated: false, + boundary: "none" + })), + turns: [{ steps: [{ op: "write", source: -2, value: 1 }] }] + }; + return [ + base, + { + ...structuredClone(base), + optimistic: { proposals: [1], authoritative: 0, hold: true }, + turns: [{ steps: [{ op: "start-action" }] }] + } + ]; + } + if (comparison === "latest") { + const refs = new Set(); + if (s.optimistic?.kind === "latest") refs.add(-2); + for (const turn of s.turns) + if ("steps" in turn) + for (const step of turn.steps) + for (const leaf of step.op === "queue" ? queuedSteps(step) : [step]) + if (leaf.op === "read" && leaf.mode === "latest") refs.add(leaf.ref); + return [[], [...refs]].map((warmLatest, index) => { + const variant = structuredClone(s); + variant.warmLatest = warmLatest; + for (const step of leaves(variant)) + if (step.op === "resolve" && step.variantKeys) { + const key = step.variantKeys[index]; + if (key === undefined) throw new Error("Missing paired request identity"); + delete step.variantKeys; + if (key === null) Object.assign(step, { op: "noop" }); + else { + step.key = key; + variant.strict = true; + } + } + return variant; + }); + } + if (s.optimistic) + throw new Error("Ordinary delivery equivalence does not cover action lifetimes"); + if (s.readers.some(r => r.pending)) + throw new Error("Data-trace delivery equivalence excludes observable readiness verdicts"); + return (["sync", "promise", "await"] as const).map(delivery => ({ + ...s, + strict: false, + show: true, + nodes: s.nodes.map(n => ({ ...n, delivery })), + readers: s.readers.map(r => ({ + ...r, + gated: false, + boundary: "none" as const, + ...(r.mounted !== undefined ? { mounted: true } : {}) + })), + turns: [{ steps: [{ op: "write" as const, value: 1 }] }] + })); +} + +export function compare( + a: RunResult, + b: RunResult, + comparison: Comparison = "delivery" +): RunResult { + if (b.status !== "pass" || a.status !== "pass") return b.status !== "pass" ? b : a; + if ( + comparison === "latest" && + (!b.scenario.warmLatest?.length || b.warmup?.requests || b.warmup?.pending) + ) + return { + ...b, + status: "inapplicable", + error: "Latest comparison needs an allocation-only warmup with no additional async work" + }; + if ( + JSON.stringify(dataTrace(a.frames)) === JSON.stringify(dataTrace(b.frames)) && + (comparison !== "latest" || + (JSON.stringify(a.reads ?? []) === JSON.stringify(b.reads ?? []) && + JSON.stringify(a.clicks ?? []) === JSON.stringify(b.clicks ?? []))) + ) + return b; + return { + ...b, + status: "fail", + failure: { + rule: comparison === "latest" ? "E4" : comparison === "optimistic" ? "E5" : "E3", + message: + comparison === "optimistic" + ? "A held optimistic proposal differs from the same ordinary single-write publication" + : comparison === "latest" + ? "Early latest creation changes later observations" + : "Pure single-write data trace differs between sync and async delivery", + expected: + comparison === "latest" + ? { frames: dataTrace(a.frames), reads: a.reads, clicks: a.clicks } + : dataTrace(a.frames) + } + }; +} + +export async function runEquivalence( + s: Scenario, + run: (s: Scenario) => Promise, + comparison: Comparison = "delivery" +) { + const pair: RunResult[] = []; + for (const variant of variants(s, comparison)) pair.push(await run(variant)); + const failed = pair.findIndex(r => r.status !== "pass"); + let relation = + failed === -1 + ? comparison === "latest" + ? "late-early" + : comparison === "optimistic" + ? "ordinary-optimistic" + : "sync-promise" + : `variant-${failed}`; + let result = failed === -1 ? compare(pair[0], pair[1], comparison) : pair[failed]; + if (result.status === "pass" && comparison === "delivery") { + relation = "sync-await"; + result = compare(pair[0], pair[2]); + } + // A waived finding must not disappear just because another passing variant + // was chosen as the comparison result. Hard pair/variant failures still win. + if (result.status === "pass") { + const witnessed = pair.findIndex(r => r.progress?.length); + if (witnessed !== -1) { + result = pair[witnessed]; + relation = `variant-${witnessed}`; + } + } + // Record which transformation failed. Focused shrinking preserves this relation; + // discovery can export an independently failing variant as an ordinary case. + const signature = fingerprint(result); + const scenario = structuredClone(s); + if (comparison === "latest") { + const sides = pair.map(p => leaves(p.scenario)); + leaves(scenario).forEach((step, i) => { + if (step.op !== "resolve" || step.variantKeys) return; + const choices = sides.map(side => side[i]); + // A crashed worker may not have executed every command. Do not invent + // request identities for that unfinished prefix. + if (choices.every(c => c?.op === "noop" || (c?.op === "resolve" && c.key))) { + step.variantKeys = choices.map(c => (c.op === "resolve" ? c.key! : null)); + delete step.key; + } + }); + } + return { pair, result, scenario, signature: signature ? `${relation}: ${signature}` : undefined }; +} + +export async function shrinkEquivalence( + scenario: Scenario, + run: (s: Scenario) => Promise, + budget = 150, + comparison: Comparison = "delivery", + options: Pick = {} +) { + const original = await runEquivalence(scenario, run, comparison); + const target = original.signature; + if (!target) throw new Error("Only an equivalence counterexample can be shrunk"); + const limit = new SearchBudget(budget, original.pair.length); + const discovery = options.mode !== "focused" && isSemanticFailure(original.result); + const singleFailure = (r: typeof original) => + discovery ? r.pair.find(isSemanticFailure) : undefined; + const reduceSingle = async (result: RunResult, priorAccepted = 0) => { + const reduced = await shrink(result, run, budget - limit.executions, options); + return { + ...reduced, + pair: [] as RunResult[], + result: reduced.result, + scenario: reduced.result.scenario, + signature: fingerprint(reduced.result), + attempts: limit.executions + reduced.attempts, + accepted: priorAccepted + reduced.accepted, + initialExecutions: original.pair.length, + single: true + }; + }; + const initialSingle = singleFailure(original); + if (initialSingle) return reduceSingle(initialSingle); + const reduced = await search( + original, + limit, + async candidate => { + if (!limit.available(variants(candidate, comparison).length)) return; + const trial = await runEquivalence(candidate, s => limit.run(() => run(s)), comparison); + if (discovery ? !isSemanticFailure(trial.result) : trial.signature !== target) return; + return trial; + }, + s => JSON.stringify(variants(s, comparison)), + options, + r => !!singleFailure(r) + ); + const single = singleFailure(reduced.result); + if (single) { + const final = await reduceSingle(single, reduced.accepted); + return { + ...final, + candidates: reduced.candidates + final.candidates, + invalid: reduced.invalid + final.invalid, + duplicates: reduced.duplicates + final.duplicates, + exhausted: reduced.exhausted || final.exhausted + }; + } + return { + ...reduced, + ...reduced.result, + accepted: reduced.accepted, + exhausted: reduced.exhausted || !limit.available(original.pair.length), + attempts: limit.executions, + initialExecutions: original.pair.length, + single: false + }; +} diff --git a/packages/signals/tests/semantics/fixtures.ts b/packages/signals/tests/semantics/fixtures.ts new file mode 100644 index 000000000..61d20b589 --- /dev/null +++ b/packages/signals/tests/semantics/fixtures.ts @@ -0,0 +1,124 @@ +import type { Scenario } from "./scenario.js"; + +export function chain(delivery: "sync" | "promise" | "manual" | "await" = "manual"): Scenario { + return { + version: 1, + show: true, + nodes: [ + { id: 0, deps: [-1], factor: 2, offset: 1, delivery }, + { id: 1, deps: [0], factor: 1, offset: 3, delivery: "sync" } + ], + readers: [{ id: 0, refs: [-1, 1], gated: false, boundary: "none" }], + turns: [{ steps: [{ op: "write", value: 1 }] }] + }; +} + +export function obsolete(): Scenario { + const s = chain(); + s.turns = [ + { steps: [{ op: "write", value: 1 }] }, + { steps: [{ op: "write", value: 2 }] }, + { steps: [{ op: "resolve", node: 0, which: "newest" }] } + ]; + return s; +} + +export function reveal(boundary: "none" | "retain" | "reset" = "none"): Scenario { + const s = chain(); + s.show = false; + s.readers = [{ id: 0, refs: [1], gated: true, boundary }]; + s.turns.push({ steps: [{ op: "show", value: true }] }); + return s; +} + +export function readyControl(): Scenario { + const s = chain(); + s.version = 2; + s.sources = [-1]; + s.readers = [{ id: 0, refs: [1], gated: false, boundary: "none", pending: true }]; + s.turns = [ + { + steps: [ + { op: "click", reader: 0 }, + { op: "write", value: 1 } + ] + }, + { + steps: [ + { op: "click", reader: 0 }, + { op: "resolve", node: 0, which: "newest" } + ] + }, + { + steps: [ + { op: "click", reader: 0 }, + { op: "read", ref: 1, mode: "pending" } + ] + } + ]; + return s; +} + +export function heldPending(): Scenario { + const s = readyControl(); + s.readers.push({ id: 1, refs: [1], gated: false, boundary: "none" }); + s.turns = [{ steps: [{ op: "write", value: 1 }] }]; + return s; +} + +export function disposedReader(): Scenario { + const s = chain(); + s.turns.push({ steps: [{ op: "dispose", reader: 0 }] }); + return s; +} + +/** A held details write loses its only blocker when a page reset publishes fallback. */ +export function releasedFallback(): Scenario { + return { + version: 2, + sources: [-1, -2], + show: true, + nodes: [ + { id: 0, deps: [-1], factor: 1, offset: 0, delivery: "manual" }, + { id: 1, deps: [0, -2], factor: 1, offset: 0, delivery: "manual" } + ], + readers: [{ id: 0, refs: [1], gated: false, boundary: "reset" }], + turns: [ + { steps: [{ op: "write", source: -2, value: 1 }] }, + { steps: [{ op: "write", source: -1, value: 1 }] } + ] + }; +} + +export function separateUpdates(): Scenario { + return { + version: 2, + sources: [-1, -2], + show: true, + nodes: [ + { id: 0, deps: [-1], factor: 1, offset: 0, delivery: "manual" }, + { id: 1, deps: [-2], factor: 1, offset: 0, delivery: "manual" } + ], + readers: [ + { id: 0, refs: [0], gated: false, boundary: "none" }, + { id: 1, refs: [1], gated: false, boundary: "none" }, + { id: 2, refs: [-1, -2], gated: false, boundary: "none" } + ], + turns: [ + { steps: [{ op: "write", source: -1, value: 1 }] }, + { steps: [{ op: "write", source: -2, value: 1 }] }, + { steps: [{ op: "resolve", node: 0, which: "newest" }] } + ] + }; +} + +export function heldAction(): Scenario { + const s = separateUpdates(); + s.actions = [{ id: 0, segments: [[{ source: -1, value: 1 }], []] }]; + s.turns = [ + { steps: [{ op: "start-action", action: 0 }] }, + { steps: [{ op: "resolve", node: 0, which: "newest" }] }, + { steps: [{ op: "resume-action", action: 0 }] } + ]; + return s; +} diff --git a/packages/signals/tests/semantics/focused-reduce.test.ts b/packages/signals/tests/semantics/focused-reduce.test.ts new file mode 100644 index 000000000..56fd83044 --- /dev/null +++ b/packages/signals/tests/semantics/focused-reduce.test.ts @@ -0,0 +1,221 @@ +import { chain } from "./fixtures.js"; +import { focusedReductions } from "./reduce.js"; +import { runScenario } from "./runner.js"; +import { validate, type Scenario } from "./scenario.js"; + +const candidates = (s: Scenario) => [...focusedReductions(s)].filter(c => !validate(c)); + +test("optimistic values try one as well as the existing zero reduction", () => { + const s: Scenario = { + ...chain(), + version: 2, + sources: [-1, -2], + optimistic: { proposals: [2, 3], authoritative: 4 }, + turns: [] + }; + const before = structuredClone(s); + const cs = candidates(s); + expect(cs.some(c => c.optimistic?.proposals.join() === "1,3")).toBe(true); + expect(cs.some(c => c.optimistic?.authoritative === 1)).toBe(true); + expect(cs.some(c => c.sources?.length !== 2)).toBe(false); + expect(s).toEqual(before); +}); + +test("action write simplification retains segment boundaries and other writes", () => { + const s: Scenario = { + ...chain(), + version: 2, + sources: [-1], + actions: [{ id: 7, segments: [[{ source: -1, value: 3 }], [{ source: -1, value: 2 }]] }], + turns: [{ steps: [{ op: "start-action", action: 7 }] }] + }; + expect( + candidates(s).some( + c => + JSON.stringify(c.actions) === + JSON.stringify([ + { id: 7, segments: [[{ source: -1, value: 1 }], [{ source: -1, value: 2 }]] } + ]) + ) + ).toBe(true); +}); + +test("source merging rewrites references, actions and anchors, retaining request questions", () => { + const s: Scenario = { + ...chain(), + version: 2, + sources: [-1, -2], + anchors: [-1, -2], + warmLatest: [-2], + actions: [{ id: 0, segments: [[{ source: -2, value: 1 }], []] }], + turns: [ + { + steps: [ + { op: "read", ref: -2, mode: "plain" }, + { op: "resolve", node: 0, which: "oldest", key: "0:[1]#1" } + ] + } + ] + }; + s.nodes[0].deps = [-2]; + s.nodes[1].branch = { condition: -2, otherwise: [-2] }; + const before = structuredClone(s); + const c = candidates(s).find(c => c.sources?.length === 1)!; + expect(c.nodes[0].deps).toEqual([-1]); + expect(c.nodes[1].branch).toEqual({ condition: -1, otherwise: [-1] }); + expect(c.anchors).toEqual([-1]); + expect(c.warmLatest).toEqual([-1]); + expect(c.actions![0].segments[0][0].source).toBe(-1); + expect(c.turns[0]).toEqual({ + steps: [ + { op: "read", ref: -1, mode: "plain" }, + { op: "resolve", node: 0, which: "oldest", key: "0:[1]#1" } + ] + }); + expect(s).toEqual(before); +}); + +test("merging which changes a resolved question is invalid rather than redirected", async () => { + const s: Scenario = { + ...chain(), + version: 2, + sources: [-1, -2], + strict: true, + turns: [ + { + steps: [ + { op: "write", source: -2, value: 1 }, + { op: "write", source: -1, value: 2 } + ] + }, + { steps: [{ op: "resolve", node: 0, which: "newest", key: "0:[1]#1" }] } + ] + }; + s.nodes[0].deps = [-2]; + s.readers[0].refs = [-2, 1]; + expect((await runScenario(s)).status).toBe("pass"); + const c = candidates(s).find(c => c.sources?.length === 1)!; + expect((await runScenario(c)).status).toBe("invalid"); +}); + +test("task materialization puts work at invocation, not registration", async () => { + const s = chain("sync"); + s.turns = [ + { steps: [{ op: "queue", id: 7, via: "task", step: { op: "write", value: 1 } }] }, + { steps: [{ op: "write", value: 2 }] }, + { task: 7 } + ]; + const before = structuredClone(s); + const c = candidates(s).find(c => "steps" in c.turns[2])!; + expect(c.turns).toEqual([ + { steps: [{ op: "noop" }] }, + { steps: [{ op: "write", value: 2 }] }, + { steps: [{ op: "write", value: 1 }] } + ]); + const a = await runScenario(s), + b = await runScenario(c); + expect(a.status).toBe("pass"); + expect(b.frames).toEqual(a.frames); + expect(s).toEqual(before); +}); + +test("implicit tasks move to a final host turn; canceled or repeated tasks are left alone", () => { + const s = chain("sync"); + s.turns = [{ steps: [{ op: "queue", id: 0, via: "task", step: { op: "write", value: 1 } }] }]; + expect( + candidates(s).some( + c => + c.turns.length === 2 && + JSON.stringify(c.turns[1]) === JSON.stringify({ steps: [{ op: "write", value: 1 }] }) + ) + ).toBe(true); + s.turns.push({ steps: [{ op: "cancel", id: 0 }] }); + expect(candidates(s).some(c => JSON.stringify(c.turns).includes('"noop"'))).toBe(false); + s.turns.splice(1, 1, { task: 0 }, { task: 0 }); + expect(candidates(s).some(c => JSON.stringify(c.turns).includes('"noop"'))).toBe(false); +}); + +test("setup, flush separation and grouped probe removal are separate candidates", () => { + const s = chain("sync"); + s.show = false; + s.turns = [ + { + steps: [ + { op: "read", ref: -1, mode: "plain" }, + { op: "write", value: 1 }, + { op: "flush" }, + { op: "show", value: true }, + { op: "read", ref: -1, mode: "latest" } + ] + } + ]; + const cs = candidates(s); + expect(cs.some(c => c.show && c.turns === s.turns)).toBe(true); + const split = cs.find(c => c.turns.length === 2)!; + expect(split.show).toBe(false); + const probes = cs.find(c => JSON.stringify(c.turns).includes('"noop"'))!; + expect(probes.turns).toEqual([ + { + steps: [ + { op: "noop" }, + { op: "write", value: 1 }, + { op: "flush" }, + { op: "show", value: true }, + { op: "noop" } + ] + } + ]); +}); + +test("dropping a setup prefix can retain a changing write without altering surviving requests", () => { + const s = chain(); + s.turns = [ + { steps: [{ op: "write", value: 1 }] }, + { steps: [{ op: "resolve", node: 0, which: "newest", key: "0:[1]#1" }] }, + { steps: [{ op: "write", value: 0 }] }, + { steps: [{ op: "show", value: true }] } + ]; + const before = structuredClone(s); + expect( + candidates(s).some( + c => + JSON.stringify(c.turns) === + JSON.stringify([ + { steps: [{ op: "write", value: 1 }] }, + { steps: [{ op: "show", value: true }] } + ]) + ) + ).toBe(true); + expect(s).toEqual(before); +}); + +test("a microtask can move to a new event only when it is last in its callback", () => { + const s = chain("sync"); + s.turns = [ + { + steps: [ + { op: "write", value: 1 }, + { op: "queue", id: 0, via: "microtask", step: { op: "show", value: true } } + ] + } + ]; + expect(candidates(s).some(c => c.turns.length === 2)).toBe(true); + const t = s.turns[0]; + if ("steps" in t) t.steps.push({ op: "write", value: 2 }); + expect(candidates(s).some(c => c.turns.length === 2)).toBe(false); +}); + +test("matching offsets and writes can shrink together without rewriting request inputs", () => { + const s = chain(); + s.nodes[0].offset = 2; + s.nodes[1].offset = 2; + s.turns = [ + { steps: [{ op: "write", value: 2 }] }, + { steps: [{ op: "resolve", node: 0, which: "newest", key: "0:[2]#1" }] } + ]; + const before = structuredClone(s); + const c = candidates(s).find(c => c.nodes[0].offset === 1 && c.nodes[1].offset === 1)!; + expect(c.turns[0]).toEqual({ steps: [{ op: "write", value: 1 }] }); + expect(c.turns[1]).toEqual(s.turns[1]); + expect(s).toEqual(before); +}); diff --git a/packages/signals/tests/semantics/footprints.test.ts b/packages/signals/tests/semantics/footprints.test.ts new file mode 100644 index 000000000..9df7304b6 --- /dev/null +++ b/packages/signals/tests/semantics/footprints.test.ts @@ -0,0 +1,127 @@ +import { Footprint, type PathWork } from "./footprints.js"; +import { Model } from "./model.js"; +import type { Scenario } from "./scenario.js"; +import { validate } from "./scenario.js"; +import { runScenario } from "./runner.js"; +import { generate } from "./generate.js"; +import { withWorker } from "./worker-fixture.js"; + +function branching(): Scenario { + return { + version: 2, + sources: [-1, -2], + show: true, + nodes: [ + { id: 0, deps: [-1], delivery: "manual", factor: 1, offset: 0 }, + { id: 1, deps: [-1], delivery: "manual", factor: 2, offset: 0 }, + { + id: 2, + deps: [1], + branch: { condition: -2, otherwise: [0] }, + delivery: "sync", + factor: 1, + offset: 0 + } + ], + readers: [{ id: 0, refs: [2], gated: false, boundary: "reset" }], + turns: [ + { steps: [{ op: "write", source: -1, value: 1 }] }, + { steps: [{ op: "write", source: -2, value: 1 }] } + ] + }; +} + +test("branch history retains the exact witnessed flight without granting another flight permission", () => { + const s = branching(); + const model = new Model(s); + const path = new Footprint(model, s.readers[0]); + const a: PathWork = { key: "0:[1]#1", node: 0, inputs: [1], state: "waiting" }; + const b: PathWork = { key: "1:[1]#1", node: 1, inputs: [1], state: "waiting" }; + const ids = [0, 1, 2]; + path.attempted = 1; + const published = model.evaluate(0, { [-1]: 0, [-2]: 0 }); + path.update( + model.evaluate(1, { [-1]: 1, [-2]: 0 }), + published, + true, + true, + id => (id === 0 ? a : undefined), + ids, + "A", + 1 + ); + expect(path.retained.has(a)).toBe(true); + expect(path.retained.has(b)).toBe(false); + path.update( + model.evaluate(1, { [-1]: 1, [-2]: 1 }), + published, + false, + true, + id => (id === 1 ? b : undefined), + ids, + "B", + 2 + ); + expect(path.retained.has(a)).toBe(true); + expect(path.retained.has(b)).toBe(true); + expect(path.published[model.slots.get(0)!]).toBe(1); + expect(path.requested[model.slots.get(0)!]).toBe(0); + const replacement = { ...a, key: "0:[1]#2" }; + a.state = "resolved"; + path.update( + model.evaluate(1, { [-1]: 1, [-2]: 1 }), + published, + false, + true, + id => (id === 0 ? replacement : id === 1 ? b : undefined), + ids, + "B again", + 2 + ); + expect(path.retained.has(a)).toBe(false); + expect(path.retained.has(replacement)).toBe(false); + path.clear(); + expect(path.retained.size).toBe(0); +}); + +test("a never-attempted reader gets no historical waiting permission", () => { + const s = branching(); + const model = new Model(s); + const path = new Footprint(model, s.readers[0]); + const work: PathWork = { key: "0:[1]#1", node: 0, inputs: [1], state: "waiting" }; + path.update( + model.evaluate(1), + model.evaluate(0), + false, + true, + () => work, + [0, 1, 2], + "unread", + 1 + ); + expect(path.retained.size).toBe(0); +}); + +test("branch changes under a Loading region finish with the selected answer", async () => { + const result = await runScenario(branching()); + expect(["pass", "policy"], JSON.stringify(result)).toContain(result.status); + expect(result.frames.at(-1)?.outputs[0]).toEqual([2]); + expect(result.coverage).toContain("witnessed-fallback-work"); + expect(result.footprints?.[0].published).toContain(1); + expect(result.footprints?.[0].published).not.toContain(0); +}); + +test("branch/fallback generation preserves scoped witnesses on replay", async () => { + await withWorker(async client => { + for (const s of generate(3350, 60, "branch-boundaries")) { + expect(validate(s)).toBeUndefined(); + const a = await client.run(s); + const b = await client.run(a.scenario); + expect(["pass", "fail", "policy", "error"], JSON.stringify(a)).toContain(a.status); + expect(b.status).toBe(a.status); + expect(b.frames).toEqual(a.frames); + expect(b.requirements).toEqual(a.requirements); + expect(b.footprints).toEqual(a.footprints); + } + }); +}); diff --git a/packages/signals/tests/semantics/footprints.ts b/packages/signals/tests/semantics/footprints.ts new file mode 100644 index 000000000..c2822cd87 --- /dev/null +++ b/packages/signals/tests/semantics/footprints.ts @@ -0,0 +1,67 @@ +import { Model, type Values } from "./model.js"; +import type { ReaderSpec } from "./scenario.js"; + +export interface PathWork { + key: string; + node: number; + inputs: number[]; + state: "waiting" | "resolved"; +} +export interface Witness { + at: string; + write: number; +} + +/** History is allocated only for boundary regions. Permission records an + * exact generated request on a declared requested path, after a real reader + * attempted that prefix. It never derives an obligation from Solid's blocker + * list, and never grants permission to a new request merely sharing a node. */ +export class Footprint { + requested: Uint8Array; + published: Uint8Array; + readonly retained = new Map(); + attempted = 0; + publishedKnown = false; + readonly anchored: boolean; + readonly sources: number; + constructor( + private model: Model, + private reader: ReaderSpec + ) { + let sources = 0; + for (const ref of reader.refs) sources |= model.sourceMask(ref); + this.sources = sources; + this.anchored = (sources & model.anchorMask) === sources; + this.requested = new Uint8Array(model.size); + this.published = new Uint8Array(model.size); + } + update( + values: Values, + published: Values, + showingContent: boolean, + active: boolean, + find: (node: number, inputs: number[]) => PathWork | undefined, + nodeIds: readonly number[], + at: string, + write: number + ): void { + for (const [work] of this.retained) if (work.state === "resolved") this.retained.delete(work); + this.requested = this.model.select(this.reader.refs, values, active ? this.attempted : 0); + if (showingContent && this.anchored) { + this.published = this.model.select(this.reader.refs, published); + this.publishedKnown = true; + } + for (let i = 0; i < this.model.nodes.length; i++) { + const node = this.model.nodes[i]; + if (!this.requested[node.slot]) continue; + const work = find(nodeIds[i], values.inputs(i)); + if (work?.state === "waiting" && !this.retained.has(work)) + this.retained.set(work, { at, write }); + } + } + /** A region that has published the current complete answer closes its old + * waiting interval; those requests cannot gain permission on a later reload. */ + clear(): void { + this.retained.clear(); + } +} diff --git a/packages/signals/tests/semantics/generate.test.ts b/packages/signals/tests/semantics/generate.test.ts new file mode 100644 index 000000000..9b4faee56 --- /dev/null +++ b/packages/signals/tests/semantics/generate.test.ts @@ -0,0 +1,48 @@ +import { generate } from "./generate.js"; +import { runScenario, type RunResult } from "./runner.js"; +import { validate } from "./scenario.js"; +import { chain } from "./fixtures.js"; +import { dataTrace } from "./rules.js"; +import { variants, compare } from "./equivalence.js"; + +test("seeded generation is reproducible and well formed", () => { + const cases = generate(3289, 200); + expect(generate(3289, 200)).toEqual(cases); + for (const s of cases) expect(validate(s)).toBeUndefined(); +}); + +test("sync, fulfilled promise and delayed answers preserve the single-write data trace", async () => { + const traces: string[][] = []; + for (const delivery of ["sync", "promise", "manual", "await"] as const) { + const r = await runScenario(chain(delivery)); + expect(r.status).toBe("pass"); + traces.push(dataTrace(r.frames)); + } + for (const trace of traces) expect(trace).toEqual(traces[0]); +}); + +test("generated runs are replayable and cleanup is order independent", async () => { + const cases = generate(3305, 30); + const first: RunResult[] = []; + for (const s of cases) first.push(await runScenario(s)); + for (let i = cases.length - 1; i >= 0; i--) { + const a = first[i]; + expect(["pass", "fail", "policy"], a.error).toContain(a.status); + const b = await runScenario(a.scenario); + expect(b.status).toBe(a.status); + expect(b.failure).toEqual(a.failure); + expect(b.waiting).toEqual(a.waiting); + expect(b.frames).toEqual(a.frames); + } +}); + +test("sync/async equivalence holds across generated graph shapes", async () => { + for (const s of generate(3347, 100)) { + const [sync, promise, delayed] = variants(s); + const a = await runScenario(sync); + for (const variant of [promise, delayed]) { + const result = compare(a, await runScenario(variant)); + expect(result.status, JSON.stringify(result)).toBe("pass"); + } + } +}); diff --git a/packages/signals/tests/semantics/generate.ts b/packages/signals/tests/semantics/generate.ts new file mode 100644 index 000000000..ddea2f0dc --- /dev/null +++ b/packages/signals/tests/semantics/generate.ts @@ -0,0 +1,592 @@ +import * as fc from "fast-check"; +import { + queuedSteps, + type Delivery, + type Leaf, + type MemoSpec, + type ReaderSpec, + type Scenario, + type Turn +} from "./scenario.js"; + +// Small bounded graphs are cheap enough to explore thousands of schedules. +// References are constructed topologically, never repaired after generation. +export const scenarioArbitrary: fc.Arbitrary = fc + .integer({ min: 1, max: 5 }) + .chain(size => { + const nodes = Array.from({ length: size }, (_, id) => + fc.record({ + id: fc.constant(id), + deps: fc.uniqueArray(fc.integer({ min: -1, max: id - 1 }), { + minLength: 1, + maxLength: Math.min(2, id + 1) + }), + factor: fc.integer({ min: 1, max: 2 }), + offset: fc.integer({ min: 0, max: 2 }), + delivery: fc.constantFrom("sync", "sync", "manual", "await", "promise") + }) + ); + const reader = fc.record({ + refs: fc.uniqueArray(fc.integer({ min: -1, max: size - 1 }), { minLength: 1, maxLength: 3 }), + gated: fc.boolean() + }); + const leaf: fc.Arbitrary = fc.oneof( + { + weight: 5, + arbitrary: fc.integer({ min: 0, max: 3 }).map(value => ({ op: "write" as const, value })) + }, + { + weight: 3, + arbitrary: fc.record({ + op: fc.constant("resolve" as const), + node: fc.integer({ min: 0, max: size - 1 }), + which: fc.constantFrom("oldest" as const, "newest" as const) + }) + }, + { weight: 2, arbitrary: fc.boolean().map(value => ({ op: "show" as const, value })) }, + { weight: 1, arbitrary: fc.constant({ op: "flush" as const }) } + ); + return fc + .record({ + nodes: fc.tuple(...nodes), + readers: fc.array(reader, { minLength: 1, maxLength: 3 }), + boundary: fc.constantFrom( + "none" as const, + "none" as const, + "retain" as const, + "reset" as const + ), + show: fc.boolean(), + blocks: fc.array( + fc.record({ + first: leaf, + second: leaf, + kind: fc.integer({ min: 0, max: 8 }), + queueFirst: fc.boolean() + }), + { minLength: 3, maxLength: 10 } + ), + dispose: fc.boolean() + }) + .map(spec => { + const turns: Turn[] = []; + let id = 0; + for (const b of spec.blocks) { + if (b.kind < 3) turns.push({ steps: b.kind === 0 ? [b.first, b.second] : [b.first] }); + else { + const via = (["microtask", "promise", "await", "task", "task", "task"] as const)[ + b.kind - 3 + ]; + const callback = id++; + const queue = { op: "queue" as const, id: callback, via, step: b.second }; + turns.push({ steps: b.queueFirst ? [queue, b.first] : [b.first, queue] }); + if (b.kind === 7) turns.push({ steps: [{ op: "cancel", id: callback }] }); + else if (b.kind === 6) turns.push({ task: callback }); + } + } + if (spec.dispose) turns.push({ steps: [{ op: "dispose", reader: 0 }] }); + return { + version: 1, + show: spec.show, + nodes: spec.nodes as MemoSpec[], + readers: spec.readers.map( + (r, id): ReaderSpec => ({ ...r, id, boundary: id === 0 ? spec.boundary : "none" }) + ), + turns + }; + }); + }); + +export const multiSourceArbitrary: fc.Arbitrary = scenarioArbitrary.chain(base => + fc + .record({ + nodes: fc.array(fc.integer({ min: 0, max: 3 }), { + minLength: base.nodes.length, + maxLength: base.nodes.length + }), + readers: fc.array(fc.boolean(), { + minLength: base.readers.length, + maxLength: base.readers.length + }), + assignments: fc.integer({ min: 0, max: 0x7fffffff }), + continuationBlock: fc.boolean() + }) + .map(options => { + const s = structuredClone(base); + s.version = 2; + s.sources = [-1, -2]; + s.nodes.forEach((node, i) => { + if (options.nodes[i] & 1) node.deps = node.deps.map(id => (id === -1 ? -2 : id)); + if (options.nodes[i] & 2) node.deps = [...new Set([...node.deps, -2])]; + }); + s.readers.forEach((reader, i) => { + if (options.readers[i]) reader.refs = [...new Set([...reader.refs, -2])]; + }); + let next = 0; + const assign = (step: Leaf) => { + if (step.op === "write") step.source = options.assignments & (1 << next++) ? -1 : -2; + }; + for (const turn of s.turns) { + if (!("steps" in turn)) continue; + for (const step of turn.steps) { + if (step.op === "queue") { + queuedSteps(step).forEach(assign); + if (options.continuationBlock && step.step?.op === "write") { + const first = step.step; + delete step.step; + step.steps = [first, { ...first, source: first.source === -1 ? -2 : -1 }]; + } + } else if (step.op !== "cancel") assign(step); + } + } + return s; + }) +); + +export const optimisticArbitrary: fc.Arbitrary = scenarioArbitrary.chain(base => + fc + .record({ + proposals: fc.array(fc.integer({ min: 0, max: 3 }), { minLength: 1, maxLength: 3 }), + authoritative: fc.integer({ min: 0, max: 3 }) + }) + .map(optimistic => { + const s = structuredClone(base); + s.version = 2; + s.sources = [-1, -2]; + s.optimistic = optimistic; + s.show = true; + s.nodes.forEach(n => { + n.deps = n.deps.map(id => (id === -1 ? -2 : id)); + }); + s.readers.forEach(r => { + r.refs = r.refs.map(id => (id === -1 ? -2 : id)); + r.gated = false; + r.boundary = "none"; + }); + const convert = (step: Leaf): Leaf => + step.op === "write" + ? { op: "resume-action" } + : step.op === "show" || step.op === "dispose" || step.op === "flush" + ? { op: "noop" } + : step; + for (const turn of s.turns) + if ("steps" in turn) + turn.steps = turn.steps.map(step => + step.op === "queue" + ? { op: "queue", id: step.id, via: step.via, steps: queuedSteps(step).map(convert) } + : step.op === "cancel" + ? step + : convert(step) + ); + s.turns.unshift({ steps: [{ op: "start-action" }] }); + return s; + }) +); + +export const readsArbitrary: fc.Arbitrary = scenarioArbitrary.chain(base => + fc + .record({ + refs: fc.array(fc.integer({ min: -1, max: base.nodes.length - 1 }), { + minLength: base.turns.length, + maxLength: base.turns.length + }), + modes: fc.array(fc.boolean(), { minLength: base.turns.length, maxLength: base.turns.length }) + }) + .map(options => { + const s = structuredClone(base); + s.turns.forEach((turn, i) => { + if ("steps" in turn) { + const read: Leaf = { + op: "read", + ref: options.refs[i], + mode: options.modes[i] ? "plain" : "latest" + }; + turn.steps.unshift(read); + turn.steps.push({ ...read }); + } + }); + s.turns.push({ steps: [{ op: "read", ref: -1, mode: "latest" }] }); + return s; + }) +); + +export const mountsArbitrary: fc.Arbitrary = scenarioArbitrary.map(base => { + const s = structuredClone(base); + s.version = 2; + s.sources = [-1]; + s.readers[0].mounted = s.show; + s.readers[0].gated = false; + const convert = (step: Leaf): Leaf => + step.op === "show" + ? { op: "mount", reader: 0, value: step.value } + : step.op === "dispose" + ? { op: "mount", reader: 0, value: false } + : step; + for (const turn of s.turns) + if ("steps" in turn) + turn.steps = turn.steps.map(step => + step.op === "queue" + ? { op: "queue", id: step.id, via: step.via, steps: queuedSteps(step).map(convert) } + : step.op === "cancel" + ? step + : convert(step) + ); + return s; +}); + +export const latestArbitrary: fc.Arbitrary = mountsArbitrary.chain(base => + fc + .record({ + proposals: fc.array(fc.integer({ min: 0, max: 3 }), { minLength: 1, maxLength: 3 }), + authoritative: fc.integer({ min: 0, max: 3 }), + anchors: fc.subarray([-1, -2]), + viaMemo: fc.boolean(), + anchorShow: fc.boolean() + }) + .map(({ anchors, anchorShow, ...action }) => { + const s = structuredClone(base); + s.sources = [-1, -2]; + s.anchors = anchors; + s.anchorShow = anchorShow; + s.optimistic = { ...action, kind: "latest" }; + s.nodes.forEach(n => (n.deps = n.deps.map(id => (id === -1 ? -2 : id)))); + s.readers.forEach(r => (r.refs = r.refs.map(id => (id === -1 ? -2 : id)))); + const convert = (step: Leaf): Leaf => (step.op === "write" ? { op: "resume-action" } : step); + for (const turn of s.turns) + if ("steps" in turn) + turn.steps = turn.steps.map(step => + step.op === "queue" + ? { op: "queue", id: step.id, via: step.via, steps: queuedSteps(step).map(convert) } + : step.op === "cancel" + ? step + : convert(step) + ); + s.turns.unshift({ steps: [{ op: "start-action" }] }); + return s; + }) +); + +export const observationArbitrary: fc.Arbitrary = mountsArbitrary.chain(base => + fc + .record({ anchored: fc.boolean(), anchorShow: fc.boolean() }) + .map(({ anchored, anchorShow }) => ({ + ...base, + anchors: anchored ? [-1] : [], + anchorShow + })) +); + +export const branchesArbitrary: fc.Arbitrary = multiSourceArbitrary.chain(base => + fc + .record({ + choices: fc.array(fc.boolean(), { + minLength: base.nodes.length, + maxLength: base.nodes.length + }), + anchored: fc.boolean() + }) + .map(({ choices, anchored }) => { + const s = structuredClone(base); + s.anchors = anchored ? [-1, -2] : []; + s.readers.forEach(r => (r.boundary = "none")); + s.nodes.forEach((n, i) => { + if (choices[i]) n.branch = { condition: -2, otherwise: [i === 0 ? -1 : i - 1] }; + }); + return s; + }) +); + +// Independent boundary regions can share data but never share fallback permission. +export const boundariesArbitrary: fc.Arbitrary = multiSourceArbitrary.chain(base => + fc + .array(fc.constantFrom("none" as const, "retain" as const, "reset" as const), { + minLength: base.readers.length, + maxLength: base.readers.length + }) + .map(boundaries => ({ + ...base, + readers: base.readers.map((reader, i) => ({ ...reader, boundary: boundaries[i] })) + })) +); + +export const nestedArbitrary: fc.Arbitrary = boundariesArbitrary.map(base => { + const s = structuredClone(base); + if (s.readers.length === 1) + s.readers.push({ ...s.readers[0], refs: [s.nodes.at(-1)!.id], id: 1 }); + for (let i = 0; i < s.readers.length; i++) { + const reader = s.readers[i]; + reader.gated = false; + if (i === 0 && reader.boundary === "none") reader.boundary = "retain"; + if (i > 0) reader.parent = i > 1 && s.readers[i - 1].boundary !== "none" ? i - 1 : 0; + } + for (const turn of s.turns) + if ("steps" in turn) + for (let i = 0; i < turn.steps.length; i++) { + const step = turn.steps[i]; + if (step.op === "dispose") turn.steps[i] = { op: "noop" }; + else if (step.op === "queue") { + const leaves = queuedSteps(step); + for (let j = 0; j < leaves.length; j++) + if (leaves[j].op === "dispose") leaves[j] = { op: "noop" }; + delete step.step; + step.steps = leaves; + } + } + return s; +}); + +export const branchBoundariesArbitrary: fc.Arbitrary = branchesArbitrary.chain(base => + fc.constantFrom("retain" as const, "reset" as const).map(boundary => { + const s = structuredClone(base); + // Ensure this cohort exercises history rather than occasionally becoming + // an ordinary boundary scenario with no branch at all. + if (!s.nodes.some(n => n.branch)) s.nodes[0].branch = { condition: -2, otherwise: [-1] }; + s.readers[0].boundary = boundary; + return s; + }) +); + +export const readinessArbitrary: fc.Arbitrary = fc + .oneof( + observationArbitrary, + observationArbitrary.map(base => ({ + ...base, + anchors: [-1], + readers: base.readers.map(reader => ({ + ...reader, + gated: false, + boundary: "none" as const, + mounted: undefined + })), + turns: base.turns.map(turn => + "steps" in turn + ? { + steps: turn.steps.map(step => + step.op === "queue" + ? { + op: "queue" as const, + id: step.id, + via: step.via, + steps: queuedSteps(step).map(leaf => + leaf.op === "mount" ? { op: "noop" as const } : leaf + ) + } + : step.op === "mount" + ? { op: "noop" as const } + : step + ) + } + : turn + ) + })) + ) + .map(base => { + const s = structuredClone(base); + s.readers[0].pending = true; + const ref = s.readers[0].refs[0]; + s.turns = s.turns.flatMap(turn => [ + turn, + { + steps: [ + { op: "click" as const, reader: 0 }, + { op: "read" as const, ref, mode: "pending" as const } + ] + } + ]); + return s; + }); + +export const derivedReadinessArbitrary: fc.Arbitrary = readinessArbitrary.chain(base => + fc.integer({ min: 1, max: 3 }).map(pendingDepth => { + const s = structuredClone(base); + s.readers[0].pendingDepth = pendingDepth; + return s; + }) +); + +export const optimisticReadinessArbitrary: fc.Arbitrary = optimisticArbitrary.chain( + base => + fc.integer({ min: 0, max: 3 }).map(depth => { + const s = structuredClone(base); + s.readers[0].pending = true; + if (depth) s.readers[0].pendingDepth = depth; + s.turns = s.turns.flatMap(turn => [turn, { steps: [{ op: "click" as const, reader: 0 }] }]); + return s; + }) +); + +/** Focused update schedules over varied DAGs, with foreign requests/actions + * deliberately left open. No generator-provided grouping is trusted by the oracle. */ +export const updateGroupsArbitrary: fc.Arbitrary = multiSourceArbitrary.chain(base => + fc + .record({ + mode: fc.integer({ min: 0, max: 4 }), + actions: fc.boolean(), + secondSegment: fc.boolean(), + mixed: fc.boolean(), + reverse: fc.boolean() + }) + .map(options => { + const nodes = base.nodes; + const readers: ReaderSpec[] = []; + for (const node of nodes) + readers.push({ id: readers.length, refs: [node.id], gated: false, boundary: "none" }); + readers.push({ + id: readers.length, + refs: options.mixed ? [-1, -2, nodes[nodes.length - 1].id] : [-1, -2], + gated: false, + boundary: "none" + }); + const s: Scenario = { version: 2, sources: [-1, -2], show: true, nodes, readers, turns: [] }; + const first: Leaf = options.actions + ? { op: "start-action", action: 0 } + : { op: "write", source: -1, value: 1 }; + const second: Leaf = options.actions + ? { op: "start-action", action: 1 } + : { op: "write", source: -2, value: 1 }; + if (options.actions) + s.actions = [ + { + id: 0, + segments: [ + [{ source: -1, value: 1 }], + ...(options.secondSegment ? [[{ source: -1, value: 2 }]] : []), + [] + ] + }, + { id: 1, segments: [[{ source: -2, value: 1 }], []] } + ]; + if (options.mode === 0) s.turns.push({ steps: [first, second] }); + else if (options.mode === 1) s.turns.push({ steps: [first, { op: "flush" }, second] }); + else if (options.mode === 2) s.turns.push({ steps: [first] }, { steps: [second] }); + else { + const queued = { op: "queue" as const, id: 0, via: "microtask" as const, step: second }; + s.turns.push({ steps: options.mode === 3 ? [queued, first] : [first, queued] }); + } + if (options.actions && options.secondSegment) + s.turns.push({ steps: [{ op: "resume-action", action: 0 }] }); + // Resolve nodes one at a time. Chains may spawn later occurrences, which + // the existing completion suffix handles after the measured prefix. + for (let i = 0; i < nodes.length; i++) { + const n = nodes[options.reverse ? nodes.length - i - 1 : i]; + if (n.delivery === "manual" || n.delivery === "await") + s.turns.push({ steps: [{ op: "resolve", node: n.id, which: "newest" }] }); + } + if (options.actions) + s.turns.push({ steps: [{ op: "resume-action", action: options.reverse ? 1 : 0 }] }); + return s; + }) +); + +export const attachmentArbitrary: fc.Arbitrary = fc + .integer({ min: 2, max: 4 }) + .chain(size => + fc + .record({ + deliveries: fc.array(fc.constantFrom("sync", "promise", "manual", "await"), { + minLength: size, + maxLength: size + }), + factors: fc.array(fc.integer({ min: 1, max: 2 }), { minLength: size, maxLength: size }), + offsets: fc.array(fc.integer({ min: 0, max: 2 }), { minLength: size, maxLength: size }), + on: fc.integer({ min: -1, max: size - 2 }), + portal: fc.boolean(), + scheduled: fc.boolean(), + writes: fc.integer({ min: 1, max: 3 }), + reverse: fc.boolean(), + dispose: fc.boolean() + }) + .map(spec => { + const nodes: MemoSpec[] = []; + for (let id = 0; id < size; id++) + nodes.push({ + id, + deps: [id - 1], + factor: spec.factors[id], + offset: spec.offsets[id], + delivery: spec.deliveries[id] + }); + const turns: Turn[] = []; + for (let value = 1; value <= spec.writes; value++) { + turns.push({ steps: [{ op: "write", value }] }); + for (let i = 0; i < size; i++) + turns.push({ + steps: [{ op: "resolve", node: spec.reverse ? size - 1 - i : i, which: "newest" }] + }); + } + if (spec.dispose) turns.push({ steps: [{ op: "dispose", reader: 0 }] }); + return { + version: 2, + sources: [-1], + show: true, + nodes, + readers: [ + { + id: 0, + refs: [size - 1], + gated: false, + boundary: "none", + render: { + on: spec.on, + target: spec.portal ? "portal" : "local", + scheduled: spec.portal || spec.scheduled + } + } + ], + turns + }; + }) + ); + +export type Cohort = + | "attachment" + | "update-groups" + | "branch-boundaries" + | "optimistic-readiness" + | "derived-readiness" + | "nested" + | "boundaries" + | "readiness" + | "ordinary" + | "multi" + | "optimistic" + | "reads" + | "mounts" + | "latest" + | "observation" + | "branches"; +export function generate(seed: number, count: number, cohort: Cohort = "ordinary"): Scenario[] { + const arbitrary = + cohort === "attachment" + ? attachmentArbitrary + : cohort === "update-groups" + ? updateGroupsArbitrary + : cohort === "branch-boundaries" + ? branchBoundariesArbitrary + : cohort === "optimistic-readiness" + ? optimisticReadinessArbitrary + : cohort === "derived-readiness" + ? derivedReadinessArbitrary + : cohort === "nested" + ? nestedArbitrary + : cohort === "boundaries" + ? boundariesArbitrary + : cohort === "readiness" + ? readinessArbitrary + : cohort === "branches" + ? branchesArbitrary + : cohort === "latest" + ? latestArbitrary + : cohort === "observation" + ? observationArbitrary + : cohort === "mounts" + ? mountsArbitrary + : cohort === "reads" + ? readsArbitrary + : cohort === "optimistic" + ? optimisticArbitrary + : cohort === "multi" + ? multiSourceArbitrary + : scenarioArbitrary; + return fc.sample(arbitrary, { seed, numRuns: count }); +} diff --git a/packages/signals/tests/semantics/group-controls.test.ts b/packages/signals/tests/semantics/group-controls.test.ts new file mode 100644 index 000000000..0791c988d --- /dev/null +++ b/packages/signals/tests/semantics/group-controls.test.ts @@ -0,0 +1,159 @@ +import { + action, + createMemo, + createRenderEffect, + createRoot, + createSignal, + flush +} from "../../src/index.js"; +import { HostTasks } from "./host.js"; + +function setup(sharedMemo = false, asyncPair = false, separateAsync = true) { + const host = new HostTasks(); + const gates: Array void>> = [[], []]; + const shown = [0, 0]; + let pair = [0, 0]; + let dispose!: () => void; + const state = createRoot(d => { + dispose = d; + const [a, setA] = createSignal(0); + const [b, setB] = createSignal(0); + const values = [a, b]; + const details = values.map((read, i) => + createMemo(() => { + const value = read(); + return new Promise(resolve => gates[i].push(() => resolve(value))); + }) + ); + for (let i = 0; i < 2; i++) { + createRenderEffect(values[i], value => { + shown[i] = value; + }); + if (separateAsync) createRenderEffect(details[i], () => {}); + } + const combined = sharedMemo + ? createMemo(() => [a(), b()]) + : asyncPair + ? () => [details[0](), details[1]()] + : () => [a(), b()]; + createRenderEffect(combined, value => { + pair = value; + }); + return { setA, setB }; + }); + flush(); + const resolve = (i: number) => gates[i].shift()!(); + const ready = async () => { + resolve(0); + resolve(1); + await host.drain(); + }; + const close = async () => { + dispose(); + for (const queue of gates) for (const resolve of queue) resolve(); + flush(); + await host.drain(); + host.close(); + }; + return { ...state, host, resolve, ready, close, shown, pair: () => pair }; +} + +// Controls specify the grouping contract without reading runtime transaction IDs. +// Both async branches are initialized; neither reader needs initial suspension. +test.each([ + ["same block", true], + ["flush separator", false], + ["task separator", false], + ["microtask before flush", true], + ["microtask after flush", false], + ["shared memo", true] +] as const)("grouping: %s", async (mode, together) => { + const h = setup(mode === "shared memo"); + try { + await h.ready(); + if (mode === "task separator" || mode === "shared memo") { + await h.host.run(() => h.setA(1)); + await h.host.drain(); + await h.host.run(() => h.setB(1)); + } else + await h.host.run(() => { + if (mode === "microtask before flush") queueMicrotask(() => h.setB(1)); + h.setA(1); + if (mode === "flush separator") flush(); + if (mode === "microtask after flush") queueMicrotask(() => h.setB(1)); + else if (mode !== "microtask before flush") h.setB(1); + }); + await h.host.drain(); + await h.host.run(() => h.resolve(0)); + await h.host.drain(); + expect(h.shown, mode).toEqual(together ? [0, 0] : [1, 0]); + expect(h.pair(), mode).toEqual(h.shown); + await h.host.run(() => h.resolve(1)); + await h.host.drain(); + expect(h.shown).toEqual([1, 1]); + expect(h.pair()).toEqual([1, 1]); + } finally { + await h.close(); + } +}); + +test.each([true, false])("authoritative action holds, shared batch=%s", async together => { + const h = setup(); + let finish!: () => void; + const gate = new Promise(resolve => { + finish = resolve; + }); + let done!: Promise; + try { + await h.ready(); + await h.host.run(() => { + h.setA(1); + if (together) + done = action(function* () { + h.setB(1); + yield gate; + })(); + }); + await h.host.drain(); + if (!together) { + await h.host.run(() => { + done = action(function* () { + h.setB(1); + yield gate; + })(); + }); + await h.host.drain(); + } + await h.host.run(() => { + h.resolve(0); + h.resolve(1); + }); + await h.host.drain(); + expect(h.shown).toEqual(together ? [0, 0] : [1, 0]); + finish(); + await done; + await h.host.drain(); + expect(h.shown).toEqual([1, 1]); + } finally { + finish(); + await h.close(); + } +}); + +// Reading initialized async results alone also permits independent publication. +test.each([true, false])("shared async effect, separate async=%s", async separate => { + const h = setup(false, true, separate); + try { + await h.ready(); + await h.host.run(() => h.setA(1)); + await h.host.drain(); + await h.host.run(() => h.setB(1)); + await h.host.drain(); + await h.host.run(() => h.resolve(0)); + await h.host.drain(); + expect(h.shown).toEqual([1, 0]); + expect(h.pair()).toEqual([1, 0]); + } finally { + await h.close(); + } +}); diff --git a/packages/signals/tests/semantics/groups.test.ts b/packages/signals/tests/semantics/groups.test.ts new file mode 100644 index 000000000..fbeb321cc --- /dev/null +++ b/packages/signals/tests/semantics/groups.test.ts @@ -0,0 +1,342 @@ +import { separateUpdates } from "./fixtures.js"; +import { runScenario } from "./runner.js"; +import type { Scenario } from "./scenario.js"; +import { Groups } from "./groups.js"; +import { Model } from "./model.js"; + +const independent = separateUpdates; + +test.each([0, 1])("one shared effect permits independent publication, first=%s", async first => { + const s = independent(); + s.turns[2] = { steps: [{ op: "resolve", node: first, which: "newest" }] }; + const r = await runScenario(s); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect(r.frames.findLast(f => f.at === "turn 2")?.inputs).toEqual({ + [-1]: first === 0 ? 1 : 0, + [-2]: first === 1 ? 1 : 0 + }); + expect(r.groups?.events).toContain(`group ${first} published`); +}); + +test("same-block writes cannot be mistaken for independent updates", async () => { + const s = independent(); + s.turns.splice(0, 2, { + steps: [ + { op: "write", source: -1, value: 1 }, + { op: "write", source: -2, value: 1 } + ] + }); + const r = await runScenario(s); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect(r.frames.findLast(f => f.at === "turn 1")?.inputs).toEqual({ [-1]: 0, [-2]: 0 }); +}); + +test("same-source overlap joins, but a published update is retired before the next write", async () => { + const s = independent(); + s.turns = [ + { steps: [{ op: "write", value: 1 }] }, + { steps: [{ op: "write", value: 2 }] }, + { steps: [{ op: "resolve", node: 0, which: "newest" }] }, + { steps: [{ op: "write", value: 3 }] } + ]; + const r = await runScenario(s); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect(r.groups?.events).toContain("group 1 joins 0: same source"); + expect(r.groups?.events).toContain("group 0 published"); + expect(r.groups?.events).toContain("group 2 published"); +}); + +test.each([true, false])("two authoritative actions, together=%s", async together => { + const s = independent(); + s.actions = [ + { id: 0, segments: [[{ source: -1, value: 1 }], []] }, + { id: 1, segments: [[{ source: -2, value: 1 }], []] } + ]; + s.turns = together + ? [ + { + steps: [ + { op: "start-action", action: 0 }, + { op: "start-action", action: 1 } + ] + } + ] + : [ + { steps: [{ op: "start-action", action: 0 }] }, + { steps: [{ op: "start-action", action: 1 }] } + ]; + s.turns.push( + { + steps: [ + { op: "resolve", node: 0, which: "newest" }, + { op: "resolve", node: 1, which: "newest" } + ] + }, + { steps: [{ op: "resume-action", action: 0 }] } + ); + const r = await runScenario(s); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect(r.frames.findLast(f => f.at === `turn ${s.turns.length - 1}`)?.inputs).toEqual({ + [-1]: together ? 0 : 1, + [-2]: 0 + }); + expect(r.actionScripts?.every(a => a.ended)).toBe(true); +}); + +test("an action continuation keeps its group across yields", async () => { + const s = independent(); + s.actions = [{ id: 4, segments: [[{ source: -1, value: 1 }], [{ source: -1, value: 2 }], []] }]; + s.turns = [ + { steps: [{ op: "start-action", action: 4 }] }, + { steps: [{ op: "write", source: -2, value: 1 }] }, + { steps: [{ op: "resume-action", action: 4 }] }, + { + steps: [ + { op: "resolve", node: 0, which: "newest" }, + { op: "resolve", node: 1, which: "newest" } + ] + } + ]; + const r = await runScenario(s); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect(r.frames.findLast(f => f.at === "turn 3")?.inputs).toEqual({ [-1]: 0, [-2]: 1 }); + expect(r.frames.at(-1)?.inputs).toEqual({ [-1]: 2, [-2]: 1 }); +}); + +test("possible memo grouping affects deadlines, never mandatory joint publication", () => { + const s = independent(); + s.nodes.push({ id: 2, deps: [-1, -2], factor: 1, offset: 0, delivery: "sync" }); + const model = new Model(s); + const groups = new Groups(model, s.readers); + const frame = { at: "test", input: 0, inputs: { [-1]: 0, [-2]: 0 }, show: true, outputs: {} }; + groups.write(-1, 1, frame); + groups.closeBatch(); + groups.write(-2, 1, frame); + groups.closeBatch(); + expect( + groups.progress(frame, [ + { reader: 1, node: 1, inputs: [1], gate: "waiting", fallback: false, basis: "desired" } + ]) + ).toBeUndefined(); + expect(groups.possibleJoins).toBeGreaterThan(0); + expect(groups.publication({ ...frame, inputs: { [-1]: 1, [-2]: 0 } })).toBeUndefined(); +}); + +test("update-group generation is valid, deterministic and replayable", async () => { + const { generate } = await import("./generate.js"); + const { validate } = await import("./scenario.js"); + const scenarios = generate(3392, 40, "update-groups"); + expect(generate(3392, 40, "update-groups")).toEqual(scenarios); + for (const s of scenarios) { + expect(validate(s)).toBeUndefined(); + const r = await runScenario(s); + expect(["pass", "fail", "policy"], JSON.stringify(r)).toContain(r.status); + const disabled = await runScenario(r.scenario, { groupChecks: false }); + expect(disabled.frames).toEqual(r.frames); + const replay = await runScenario(r.scenario); + expect(replay.status).toBe(r.status); + expect(replay.frames).toEqual(r.frames); + expect(replay.groups).toEqual(r.groups); + expect(replay.actionScripts).toEqual(r.actionScripts); + } +}); + +test("action replay rejects invalid identities, sources and unbounded scripts", async () => { + const { validate } = await import("./scenario.js"); + const s = independent(); + s.actions = [{ id: 0, segments: [[{ source: -1, value: 1 }], []] }]; + s.turns = [{ steps: [{ op: "start-action", action: 9 }] }]; + expect(validate(s)).toBe("Invalid operation"); + s.turns = [{ steps: [{ op: "start-action", action: 0 }] }]; + s.actions[0].segments[0][0].source = -3; + expect(validate(s)).toBe("Invalid action segment"); + s.actions[0].segments = [[], [], [], [], [], []]; + expect(validate(s)).toBe("Invalid action script"); +}); + +test("a changed shared derivation still needs foreign async ancestors after their source published", () => { + const s = independent(); + s.nodes.push({ id: 2, deps: [0, -2], factor: 1, offset: 0, delivery: "sync" }); + s.readers = [{ id: 0, refs: [2], gated: false, boundary: "none" }]; + const model = new Model(s); + const groups = new Groups(model, s.readers); + const frame = { at: "test", input: 0, inputs: { [-1]: 0, [-2]: 0 }, show: true, outputs: {} }; + groups.write(-2, 1, frame); + groups.closeBatch(); + expect( + groups.progress(frame, [ + { reader: 0, node: 0, inputs: [0], gate: "waiting", fallback: false, basis: "desired" } + ]) + ).toBeUndefined(); + expect( + groups.progress(frame, [ + { reader: 0, node: 0, inputs: [0], gate: "resolved", fallback: false, basis: "desired" } + ])?.rule + ).toBe("G2"); +}); + +function groupFrame(a = 0, b = 0) { + return { at: "test", input: a, inputs: { [-1]: a, [-2]: b }, show: true, outputs: {} }; +} +const waitingB = [ + { + reader: 1, + node: 1, + inputs: [1], + gate: "waiting" as const, + fallback: false, + basis: "desired" as const + } +]; + +test.each([false, true])( + "adjacent callbacks permit either publication choice, independent=%s", + independent => { + const s = separateUpdates(); + const groups = new Groups(new Model(s), s.readers); + groups.write(-1, 1, groupFrame()); + groups.closeBatch(); + groups.write(-2, 1, groupFrame()); + groups.closeBatch(); + const frame = independent ? groupFrame(1, 0) : groupFrame(); + expect(groups.publication(frame)).toBeUndefined(); + expect(groups.progress(frame, waitingB)).toBeUndefined(); + expect(groups.batchingHeld).toBe(independent ? 0 : 1); + expect(groups.publication(groupFrame(1, 1))).toBeUndefined(); + expect(groups.pending).toBe(false); + } +); + +test("same-callback writes still require joint publication", () => { + const s = separateUpdates(); + const groups = new Groups(new Model(s), s.readers); + groups.write(-1, 1, groupFrame()); + groups.write(-2, 1, groupFrame()); + expect(groups.publication(groupFrame(1, 0))?.rule).toBe("G1"); +}); + +test.each(["task", "flush"])( + "a %s separator does not introduce temporal entanglement", + separator => { + const s = separateUpdates(); + const groups = new Groups(new Model(s), s.readers); + groups.write(-1, 1, groupFrame()); + if (separator === "task") groups.beginTask(); + else groups.separator(); + groups.write(-2, 1, groupFrame()); + expect(groups.progress(groupFrame(), waitingB)?.rule).toBe("G2"); + expect(groups.batchingHeld).toBe(0); + } +); + +test("a landing can introduce proximity to an older update, without adopting its identity", () => { + const s = separateUpdates(); + const groups = new Groups(new Model(s), s.readers); + groups.write(-1, 1, groupFrame()); + groups.beginTask(); + groups.write(-2, 1, groupFrame()); + groups.landing(1); + expect(groups.progress(groupFrame(), waitingB)).toBeUndefined(); + expect(groups.batchingHeld).toBe(1); + expect(groups.publication(groupFrame(1, 0))).toBeUndefined(); +}); + +test("temporal permission ends with the relevant groups, not future source generations", () => { + const s = separateUpdates(); + const groups = new Groups(new Model(s), s.readers); + groups.write(-1, 1, groupFrame()); + groups.closeBatch(); + groups.write(-2, 1, groupFrame()); + expect(groups.publication(groupFrame(0, 1))).toBeUndefined(); + groups.beginTask(); + groups.write(-2, 2, groupFrame(0, 1)); + expect(groups.progress(groupFrame(0, 1), waitingB)?.rule).toBe("G2"); +}); + +test("temporal permission never excuses failure to publish after all work resolves", () => { + const s = separateUpdates(); + const groups = new Groups(new Model(s), s.readers); + groups.write(-1, 1, groupFrame()); + groups.closeBatch(); + groups.write(-2, 1, groupFrame()); + expect(groups.progress(groupFrame(), waitingB)).toBeUndefined(); + groups.beginTask(); // the possible join persists while both groups remain live + expect(groups.progress(groupFrame(), waitingB)).toBeUndefined(); + expect(groups.progress(groupFrame(), [])?.rule).toBe("G2"); +}); + +test("adjacency to an action with no writes yet can extend a deadline", () => { + const s = separateUpdates(); + const groups = new Groups(new Model(s), s.readers); + groups.startAction(0, groupFrame()); + groups.closeBatch(); + groups.write(-1, 1, groupFrame()); + expect(groups.progress(groupFrame(), [])).toBeUndefined(); + expect(groups.batchingHeld).toBe(1); + groups.finishAction(0); + expect(groups.progress(groupFrame(), [])?.rule).toBe("G2"); +}); + +test("both orderings around the scheduled flush use possible, not proven, batching", async () => { + for (const before of [false, true]) { + const s = separateUpdates(); + const write = { op: "write" as const, source: -1, value: 1 }; + const queued = { + op: "queue" as const, + id: 0, + via: "microtask" as const, + step: { op: "write" as const, source: -2, value: 1 } + }; + s.turns.splice(0, 2, { steps: before ? [queued, write] : [write, queued] }); + const r = await runScenario(s); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect(r.groups?.events).toContain("groups 1 and 0 may join: adjacent microtasks"); + expect(r.groups?.events.some(e => e.includes("joins"))).toBe(false); + } +}); + +test("the unused fulfilled memo's batch adoption is permitted and recorded", async () => { + const s = separateUpdates(); + s.nodes.push({ id: 2, deps: [-1], factor: 1, offset: 0, delivery: "promise" }); + // B has no async dependency; A's unused memo can nevertheless absorb its write. + s.nodes.splice(1, 1); + s.readers = [{ id: 0, refs: [0], gated: false, boundary: "none" }]; + s.turns = [ + { + steps: [ + { op: "write", source: -1, value: 1 }, + { op: "queue", id: 0, via: "microtask", step: { op: "write", source: -2, value: 1 } } + ] + } + ]; + const r = await runScenario(s); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect(r.groups?.batchingHeld).toBeGreaterThan(0); + const replay = await runScenario(r.scenario); + expect(replay.groups).toEqual(r.groups); + expect(replay.frames).toEqual(r.frames); +}); + +test("a result from an older task records its new neighboring write through the landing hook", async () => { + const s = separateUpdates(); + s.nodes = [ + { id: 0, deps: [-1], factor: 1, offset: 0, delivery: "manual" }, + { id: 2, deps: [-1], factor: 1, offset: 0, delivery: "manual" } + ]; + s.readers = [{ id: 0, refs: [0], gated: false, boundary: "none" }]; + s.turns = [ + { steps: [{ op: "write", source: -1, value: 1 }] }, + { + steps: [ + { op: "queue", id: 0, via: "microtask", step: { op: "write", source: -2, value: 1 } }, + { op: "resolve", node: 2, which: "newest" } + ] + } + ]; + const r = await runScenario(s); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect(r.groups?.events).toContain("groups 0 and 1 may join: adjacent microtasks"); + expect(r.groups?.batchingHeld).toBeGreaterThan(0); + const disabled = await runScenario(r.scenario, { groupChecks: false }); + expect(disabled.frames).toEqual(r.frames); +}); diff --git a/packages/signals/tests/semantics/groups.ts b/packages/signals/tests/semantics/groups.ts new file mode 100644 index 000000000..34b48316a --- /dev/null +++ b/packages/signals/tests/semantics/groups.ts @@ -0,0 +1,333 @@ +import type { Model } from "./model.js"; +import { queuedSteps, type Scenario, type ReaderSpec } from "./scenario.js"; +import type { Failure, Frame } from "./rules.js"; +import type { Requirement } from "./runner.js"; + +interface Group { + id: number; + root: number; + mask: number; + holds: number; + before: number[]; + target: number[]; + adjacent: number[]; + published: boolean; + visit: number; + notedMask: number; +} + +/** Ordinary, initialized flat graphs first. Skipping this stronger contract + * does not disable the existing consistency or whole-view progress checks. */ +export function groupScope(s: Scenario, model: Model): string | undefined { + if (s.readers.some(r => r.render)) return "output-attachment"; + if (s.warmLatest?.length) return "latest-read"; + for (const turn of s.turns) + if ("steps" in turn) + for (const step of turn.steps) + for (const leaf of step.op === "queue" ? queuedSteps(step) : [step]) { + if (leaf.op === "show" || leaf.op === "mount" || leaf.op === "dispose") + return "changing-observation"; + if (leaf.op === "read" && leaf.mode === "latest") return "latest-read"; + } + if (s.optimistic) return "optimistic-lifetime"; + if (s.nodes.some(n => n.branch)) return "branch-history"; + if ( + s.readers.some( + r => + r.gated || + r.mounted !== undefined || + r.parent !== undefined || + r.pending || + r.boundary !== "none" + ) + ) + return "changing-observation"; + if (model.anchorMask !== (1 << model.sources.length) - 1) return "partial-anchors"; +} + +/** Update identity comes from executed input, not runtime transition IDs. + * One callback is a proven batch; explicit flush closes it synchronously. + * Activity in the same host task and its microtask drain may entangle, but + * neither that permission nor possible memo joins ever assert atomicity. */ +export class Groups { + private groups: Group[] = []; + private live: number[] = []; + private owners: number[]; + private batch = -1; + private window: number[] = []; + private visit = 0; + private closureHeld = false; + private demandMasks = new Map(); + private actionGroups = new Map(); + readonly events: string[] = []; + checks = 0; + held = 0; + possibleJoins = 0; + batchingHeld = 0; + constructor( + private model: Model, + readers: ReaderSpec[] + ) { + this.owners = model.sources.map(() => -1); + // A changed derivation may need async ancestors driven by other sources. + // Seed EACH declared ref separately so sharing an effect never connects its + // independent refs. Propagate the demand only through memo dependency edges. + for (const reader of readers) { + const demands = new Array(model.size).fill(0); + for (const ref of reader.refs) { + const slot = model.slots.get(ref)!; + demands[slot] |= model.masks[slot]; + } + for (let i = model.nodes.length - 1; i >= 0; i--) { + const node = model.nodes[i]; + for (const dep of node.deps) demands[dep] |= demands[node.slot]; + } + this.demandMasks.set(reader.id, demands); + } + } + get pending(): boolean { + return this.live.length > 0; + } + private root(id: number): number { + while (this.groups[id].root !== id) id = this.groups[id].root; + return id; + } + private current(frame: Frame, i: number): number { + return frame.inputs?.[this.model.sources[i]] ?? frame.input; + } + private create(frame: Frame): number { + const before = this.model.sources.map((_, i) => this.current(frame, i)); + const id = this.groups.length; + this.groups.push({ + id, + root: id, + mask: 0, + holds: 0, + before, + target: before.slice(), + adjacent: [], + published: false, + visit: 0, + notedMask: -1 + }); + this.live.push(id); + this.events.push(`group ${id} starts`); + return id; + } + private batchGroup(frame: Frame): number { + if (this.batch === -1) this.batch = this.create(frame); + return this.root(this.batch); + } + private merge(a: number, b: number, why: string): number { + a = this.root(a); + b = this.root(b); + if (a === b) return a; + // Keep older history as the root, including its pre-update published values. + if (a > b) [a, b] = [b, a]; + const x = this.groups[a], + y = this.groups[b]; + for (let i = 0; i < this.owners.length; i++) + if (y.mask & (1 << i)) { + if (!(x.mask & (1 << i))) x.before[i] = y.before[i]; + x.target[i] = y.target[i]; + } + x.mask |= y.mask; + x.holds += y.holds; + for (const other of y.adjacent) if (!x.adjacent.includes(other)) x.adjacent.push(other); + y.root = a; + this.live.splice(this.live.indexOf(b), 1); + this.events.push(`group ${b} joins ${a}: ${why}`); + return a; + } + write(source: number, value: number, frame: Frame, action?: number): void { + const slot = this.model.slots.get(source)!; + let id = + action === undefined ? this.batchGroup(frame) : this.root(this.actionGroups.get(action)!); + const owner = this.owners[slot]; + if (owner !== -1) id = this.merge(id, owner, "same source"); + const group = this.groups[id]; + if (!(group.mask & (1 << slot))) group.before[slot] = this.current(frame, slot); + group.mask |= 1 << slot; + group.target[slot] = value; + this.owners[slot] = id; + this.touch(id); + } + startAction(action: number, frame: Frame): void { + const id = this.batchGroup(frame); + this.groups[id].holds++; + this.actionGroups.set(action, id); + this.touch(id); + this.events.push(`action ${action} holds group ${id}`); + } + finishAction(action: number): void { + const id = this.root(this.actionGroups.get(action)!); + this.groups[id].holds--; + this.events.push(`action ${action} releases group ${id}`); + } + /** A real host-task boundary ends temporal proximity, not existing edges. */ + beginTask(): void { + this.closeBatch(); + this.window.length = 0; + } + closeBatch(): void { + this.batch = -1; + } + /** Explicit user flush is a deliberate separator, even within one callback. */ + separator(): void { + this.beginTask(); + } + resumeAction(action: number): void { + this.closeBatch(); + this.touch(this.root(this.actionGroups.get(action)!)); + } + /** Only generated memo identities/source masks are used, never Solid's owner IDs. */ + landing(mask: number): void { + this.closeBatch(); + for (const id of this.live) if (this.groups[id].mask & mask) this.touch(id); + } + private touch(id: number): void { + id = this.root(id); + const group = this.groups[id]; + let present = false; + for (const previous of this.window) { + const other = this.root(previous); + if (other === id) { + present = true; + continue; + } + const peer = this.groups[other]; + if (peer.published || group.adjacent.includes(other)) continue; + group.adjacent.push(other); + peer.adjacent.push(id); + this.events.push(`groups ${id} and ${other} may join: adjacent microtasks`); + } + if (!present) this.window.push(id); + } + /** Run only at complete publication checkpoints, never between effect runners. */ + publication(frame: Frame): Failure | undefined { + for (let j = this.live.length - 1; j >= 0; j--) { + const group = this.groups[this.live[j]]; + let changed = false, + complete = true; + for (let i = 0; i < this.owners.length; i++) { + if (!(group.mask & (1 << i))) continue; + const value = this.current(frame, i); + if (value !== group.before[i]) changed = true; + if (value !== group.target[i]) complete = false; + } + if (changed && (group.holds || !complete)) + return { + rule: "G1", + message: group.holds + ? "Authoritative writes published while their action group is open" + : "One proven update group published only part of its writes", + frame, + expected: { + group: group.id, + mask: group.mask, + holds: group.holds, + before: group.before.slice(), + target: group.target.slice() + } + }; + if (complete && !group.holds) { + group.published = true; + for (let i = 0; i < this.owners.length; i++) + if (this.owners[i] !== -1 && this.root(this.owners[i]) === group.id) this.owners[i] = -1; + this.live.splice(j, 1); + if (this.batch !== -1 && this.root(this.batch) === group.id) this.closeBatch(); + this.events.push(`group ${group.id} published`); + } + } + } + /** Close over shared memo ancestry and, optionally, temporal permissions. + * Visit stamps avoid allocating a set for each group at each checkpoint. */ + private closure(id: number, adjacent: boolean): number { + const visit = ++this.visit; + this.groups[id].visit = visit; + let mask = this.groups[id].mask; + let changed: boolean; + this.closureHeld = false; + do { + changed = false; + for (const other of this.live) { + const group = this.groups[other]; + if (group.visit !== visit) continue; + if (group.holds) this.closureHeld = true; + if (adjacent) + for (const peer of group.adjacent) { + const next = this.groups[this.root(peer)]; + if (next.published || next.visit === visit) continue; + next.visit = visit; + mask |= next.mask; + changed = true; + } + } + for (const node of this.model.nodes) { + const deps = this.model.masks[node.slot]; + if (!(deps & mask)) continue; + for (const other of this.live) { + const group = this.groups[other]; + if (group.visit === visit || !(group.mask & deps)) continue; + group.visit = visit; + mask |= group.mask; + changed = true; + } + } + } while (changed); + return mask; + } + private blocked(mask: number, requirements: Requirement[]): boolean { + if (this.closureHeld) return true; + for (const r of requirements) + if ( + r.gate !== "resolved" && + this.demandMasks.get(r.reader)![this.model.slots.get(r.node)!] & mask + ) + return true; + return false; + } + /** Permissions extend deadlines only; publication still checks proven groups. */ + progress(frame: Frame, requirements: Requirement[]): Failure | undefined { + for (const id of this.live) { + const group = this.groups[id]; + this.checks++; + if (!group.mask) continue; + const baseMask = this.closure(id, false); + if (baseMask !== group.mask) this.possibleJoins++; + if (this.blocked(baseMask, requirements)) { + this.held++; + continue; + } + const mask = this.closure(id, true); + if (this.blocked(mask, requirements)) { + this.held++; + this.batchingHeld++; + if (group.notedMask !== mask) { + group.notedMask = mask; + this.events.push(`group ${id} deadline extended by adjacent microtasks: mask ${mask}`); + } + continue; + } + return { + rule: "G2", + message: "A ready update group remains unpublished", + frame, + expected: { + group: id, + mask: group.mask, + possibleMask: mask, + relation: + mask !== baseMask + ? "possible-batching" + : mask !== group.mask + ? "possible-entanglement" + : this.live.length > 1 + ? "independent" + : "single-group", + target: group.target.slice() + } + }; + } + } +} diff --git a/packages/signals/tests/semantics/handoff/cancelled-branch-hidden.tsx b/packages/signals/tests/semantics/handoff/cancelled-branch-hidden.tsx new file mode 100644 index 000000000..9a9b9b6a8 --- /dev/null +++ b/packages/signals/tests/semantics/handoff/cancelled-branch-hidden.tsx @@ -0,0 +1,30 @@ +import { render } from "@solidjs/web"; +import { createMemo, createSignal } from "solid-js"; + +const delay = (ms: number, value?: T) => new Promise(r => setTimeout(r, ms, value)); + +export default function App() { + const [count, setCount] = createSignal(0); + const [show, setShow] = createSignal(false); + const details = createMemo(() => delay(1000, count())); + const selected = createMemo(async () => (count() ? details() : 0)); + + return ( + <> + +

Show: {String(show())}

+

Panel: {show() ? selected() : "hidden"}

+ + ); +} + +render(() => , document.getElementById("root")!); diff --git a/packages/signals/tests/semantics/handoff/combined-pending.tsx b/packages/signals/tests/semantics/handoff/combined-pending.tsx new file mode 100644 index 000000000..17e408200 --- /dev/null +++ b/packages/signals/tests/semantics/handoff/combined-pending.tsx @@ -0,0 +1,22 @@ +import { render } from "@solidjs/web"; +import { createMemo, createSignal, isPending } from "solid-js"; + +const delay = (ms: number, value?: T) => new Promise(r => setTimeout(r, ms, value)); + +export default function App() { + const [count, setCount] = createSignal(0); + const slow = createMemo(() => delay(1000, count())); + const fast = createMemo(async () => count()); + const copy = createMemo(() => slow()); + + return ( + <> + +

Pending: {String(isPending(() => [fast(), copy()]))}

+

Fast: {fast()}

+

Slow: {copy()}

+ + ); +} + +render(() => , document.getElementById("root")!); diff --git a/packages/signals/tests/semantics/handoff/held-conditional-view.tsx b/packages/signals/tests/semantics/handoff/held-conditional-view.tsx new file mode 100644 index 000000000..f5cfa275b --- /dev/null +++ b/packages/signals/tests/semantics/handoff/held-conditional-view.tsx @@ -0,0 +1,30 @@ +import { render } from "@solidjs/web"; +import { createMemo, createSignal } from "solid-js"; + +const delay = (ms: number, value?: T) => new Promise(r => setTimeout(r, ms, value)); + +export default function App() { + const [count, setCount] = createSignal(0); + const [show, setShow] = createSignal(true); + const delayedShow = createMemo(() => delay(1000, show())); + + return ( + <> + +

Count: {count()}

+

Show: {String(show())}

+

Panel: {show() ? count() : "hidden"}

+

Delayed show: {String(delayedShow())}

+ + ); +} + +render(() => , document.getElementById("root")!); diff --git a/packages/signals/tests/semantics/handoff/latest-held-show.tsx b/packages/signals/tests/semantics/handoff/latest-held-show.tsx new file mode 100644 index 000000000..1039c7c7e --- /dev/null +++ b/packages/signals/tests/semantics/handoff/latest-held-show.tsx @@ -0,0 +1,22 @@ +import { render } from "@solidjs/web"; +import { action, createSignal, latest, Show } from "solid-js"; + +export default function App() { + const [count, setCount] = createSignal(0); + const save = action(function* () { + setCount(1); + yield new Promise(r => setTimeout(r, 1000)); + }); + + return ( + <> + +

Outside: {latest(count)}

+ +

Inside: {latest(count)}

+
+ + ); +} + +render(() => , document.getElementById("root")!); diff --git a/packages/signals/tests/semantics/handoff/stale-sum.tsx b/packages/signals/tests/semantics/handoff/stale-sum.tsx new file mode 100644 index 000000000..5c4617b05 --- /dev/null +++ b/packages/signals/tests/semantics/handoff/stale-sum.tsx @@ -0,0 +1,31 @@ +import { render } from "@solidjs/web"; +import { createMemo, createSignal } from "solid-js"; + +const delay = (ms: number, value?: T) => new Promise(r => setTimeout(r, ms, value)); + +export default function App() { + const [inputA, setA] = createSignal(0); + const [inputB, setB] = createSignal(0); + const a = createMemo(() => delay(1000, inputA())); + const b = createMemo(() => delay(1000, inputB())); + const sum = createMemo(() => a() + b()); + + return ( + <> + +

A: {a()}

+

B: {b()}

+

Sum: {sum()}

+ + ); +} + +render(() => , document.getElementById("root")!); diff --git a/packages/signals/tests/semantics/host.ts b/packages/signals/tests/semantics/host.ts new file mode 100644 index 000000000..7078f8056 --- /dev/null +++ b/packages/signals/tests/semantics/host.ts @@ -0,0 +1,31 @@ +import { MessageChannel } from "node:worker_threads"; + +/** Each callback runs in its own host task, without replacing native jobs. */ +export class HostTasks { + private channel = new MessageChannel(); + private callbacks: Array<() => void> = []; + constructor(private begin?: () => void) { + this.channel.port1.on("message", () => this.callbacks.shift()!()); + } + run(fn: () => T): Promise { + return new Promise((resolve, reject) => { + this.callbacks.push(() => { + try { + this.begin?.(); + resolve(fn()); + } catch (e) { + reject(e); + } + }); + this.channel.port2.postMessage(0); + }); + } + async drain(): Promise { + // The continuation of run() alone is a microtask, not a drain barrier. + await this.run(() => {}); + } + close() { + this.channel.port1.close(); + this.channel.port2.close(); + } +} diff --git a/packages/signals/tests/semantics/isolation.test.ts b/packages/signals/tests/semantics/isolation.test.ts new file mode 100644 index 000000000..634121201 --- /dev/null +++ b/packages/signals/tests/semantics/isolation.test.ts @@ -0,0 +1,50 @@ +import { build } from "esbuild"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { Client } from "./client.js"; +import { generate } from "./generate.js"; +import type { RunResult } from "./runner.js"; + +function semanticResult({ metrics, error, ...result }: RunResult) { + // Worker heap, timings and stack locations are diagnostic, not semantics. + return { + ...result, + error: error?.split("\n")[0], + metrics: { + operations: metrics.operations, + skipped: metrics.skipped, + requests: metrics.requests, + frames: metrics.frames + } + }; +} +test("fresh and reused workers agree per case across observation and action cohorts", async () => { + const dir = await mkdtemp(join(tmpdir(), "solid-fuzz-isolation-")); + const file = join(dir, "worker.mjs"); + const reused = new Client(file); + const fresh = new Client(file, 5000, true); + try { + await build({ + entryPoints: [fileURLToPath(new URL("./worker.ts", import.meta.url))], + outfile: file, + bundle: true, + platform: "node", + format: "esm", + define: { __DEV__: "true", __OBSERVE__: "true", __TEST__: "true" } + }); + for (const cohort of ["ordinary", "optimistic", "mounts", "branch-boundaries"] as const) + for (const scenario of generate(3289, 12, cohort)) { + const a = await reused.run(scenario); + const b = await fresh.run(scenario); + expect(semanticResult(a)).toEqual(semanticResult(b)); + } + } finally { + await reused.close(); + await fresh.close(); + await rm(dir, { recursive: true, force: true }); + reused.checkHealth(); + fresh.checkHealth(); + } +}, 20000); diff --git a/packages/signals/tests/semantics/latest.test.ts b/packages/signals/tests/semantics/latest.test.ts new file mode 100644 index 000000000..97843dc08 --- /dev/null +++ b/packages/signals/tests/semantics/latest.test.ts @@ -0,0 +1,76 @@ +import { chain } from "./fixtures.js"; +import { generate } from "./generate.js"; +import { runScenario } from "./runner.js"; +import { runEquivalence } from "./equivalence.js"; +import { checkFinal, checkFrame } from "./rules.js"; +import { validate, type Scenario } from "./scenario.js"; +import { withWorker } from "./worker-fixture.js"; + +function held(delivery: "sync" | "manual"): Scenario { + return { + version: 2, + sources: [-1, -2], + show: true, + optimistic: { kind: "latest", proposals: [1], authoritative: 1, hold: true }, + nodes: [{ id: 0, deps: [-2], factor: 2, offset: 0, delivery }], + readers: [{ id: 0, refs: [-2, 0], gated: false, boundary: "none" }], + turns: [{ steps: [{ op: "start-action" }] }] + }; +} + +test.each(["sync", "manual"] as const)( + "tracked latest %s derives while authority remains held", + async delivery => { + const r = await runScenario(held(delivery)); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect(r.frames.at(-1)?.inputs).toEqual({ [-1]: 0, [-2]: 1 }); + expect(r.frames.at(-1)?.outputs[0]).toEqual([1, 2]); + } +); + +test.each(["sync", "manual"] as const)( + "latest of a shared upstream memo supports %s derivations", + async delivery => { + const s = held(delivery); + s.optimistic!.viaMemo = true; + const pair = await runEquivalence(s, runScenario, "latest"); + expect(pair.result.status, JSON.stringify(pair)).toBe("pass"); + expect(pair.pair.every(r => r.frames.at(-1)?.outputs[0]?.[1] === 2)).toBe(true); + } +); + +test("removing source anchors narrows intermediate checks but preserves final and tuple checks", async () => { + const s = chain("sync"); + s.version = 2; + s.sources = [-1]; + s.anchors = []; + s.anchorShow = false; + const r = await runScenario(s); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect(r.frames.at(-1)?.inputs).toEqual({}); + expect(r.frames.at(-1)?.outputs[0]).toEqual([1, 6]); + const frame = r.frames.at(-1)!; + expect(checkFrame(s, frame)).toBeUndefined(); + expect(checkFinal(s, { ...frame, outputs: { 0: [0, 4] } }, 1, true, { [-1]: 1 })?.rule).toBe( + "L1" + ); + expect(r.coverage).toContain("partial-source-observation"); +}); + +test.each(["latest", "observation"] as const)( + "generated %s cases have deterministic canonical replays", + async cohort => { + await withWorker(async client => { + for (const s of generate(3347, 40, cohort)) { + expect(validate(s)).toBeUndefined(); + const a = await client.run(s); + const b = await client.run(a.scenario); + expect(["pass", "fail", "policy", "error"], JSON.stringify(a)).toContain(a.status); + expect(b.status).toBe(a.status); + expect(b.failure).toEqual(a.failure); + expect(b.frames).toEqual(a.frames); + expect(b.error?.split("\n")[0]).toBe(a.error?.split("\n")[0]); + } + }); + } +); diff --git a/packages/signals/tests/semantics/limits.test.ts b/packages/signals/tests/semantics/limits.test.ts new file mode 100644 index 000000000..7ac03b76b --- /dev/null +++ b/packages/signals/tests/semantics/limits.test.ts @@ -0,0 +1,102 @@ +import { build } from "esbuild"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { Client } from "./client.js"; +import { chain } from "./fixtures.js"; + +let directory: string; +let client: Client; +beforeAll(async () => { + directory = await mkdtemp(join(tmpdir(), "solid-fuzz-limits-")); + const outfile = join(directory, "worker.mjs"); + await build({ + entryPoints: [fileURLToPath(new URL("./worker.ts", import.meta.url))], + outfile, + bundle: true, + platform: "node", + format: "esm", + define: { __DEV__: "true", __OBSERVE__: "true", __TEST__: "true" } + }); + client = new Client(outfile); +}); +afterAll(async () => { + await client?.close(); + if (directory) await rm(directory, { recursive: true, force: true }); +}); + +test("exactly the allowed number of completion rounds is sufficient", async () => { + const r = await client.run(chain(), { maxRounds: 1 }); + expect(r.status, JSON.stringify(r)).toBe("pass"); +}); + +test("exactly the allowed number of residual completions is sufficient", async () => { + const s = chain(); + s.turns = [1, 2, 3].map(value => ({ steps: [{ op: "write", value }] })); + s.turns.push({ steps: [{ op: "resolve", node: 0, which: "newest" }] }); + const r = await client.run(s, { maxRounds: 2 }); + expect(r.status, JSON.stringify(r)).toBe("pass"); +}); + +test.each(["microtask", "promise", "await"] as const)( + "invalid %s resolve keeps its status and trace", + async via => { + const s = chain(); + s.strict = true; + s.turns = [ + { + steps: [ + { + op: "queue", + id: 0, + via, + step: { + op: "resolve", + node: 0, + which: "newest", + key: "nonexistent" + } + } + ] + } + ]; + const r = await client.run(s); + expect(r.status, JSON.stringify(r)).toBe("invalid"); + expect(r.frames.length).toBeGreaterThan(0); + expect(r.error).toContain("nonexistent"); + } +); + +test("work exhaustion during a scheduled flush stays a limit, not a runtime crash", async () => { + const r = await client.run(chain(), { maxWork: 1 }); + expect(r.status, JSON.stringify(r)).toBe("limit"); + expect(r.frames.length).toBeGreaterThan(0); + expect(r.error).toContain("async work"); +}); + +test("exhaustion at the final checkpoint cannot escape as a pass", async () => { + const s = chain("sync"); + s.turns = []; + const r = await client.run(s, { maxFrames: 1 }); + expect(r.status, JSON.stringify(r)).toBe("limit"); + expect(r.frames).toHaveLength(1); +}); + +test("insufficient rounds are still a limit and cleanup does not overwrite it", async () => { + const s = chain(); + s.nodes[1].delivery = "manual"; + const r = await client.run(s, { maxRounds: 1 }); + expect(r.status, JSON.stringify(r)).toBe("limit"); + expect(r.work.some(w => w.state === "waiting")).toBe(true); + expect((await client.run(chain())).status).toBe("pass"); +}); + +test("teardown leaves the stopped work ledger intact", async () => { + const result = await client.run(chain(), { maxRounds: 0 }); + expect(result.status).toBe("limit"); + expect(result.work).toHaveLength(1); + expect(result.work[0].state).toBe("waiting"); + expect(result.events).toEqual(["setup: start 0:[0]#1"]); + expect((await client.run(chain())).status).toBe("pass"); +}); diff --git a/packages/signals/tests/semantics/model.test.ts b/packages/signals/tests/semantics/model.test.ts new file mode 100644 index 000000000..acd79f379 --- /dev/null +++ b/packages/signals/tests/semantics/model.test.ts @@ -0,0 +1,70 @@ +import { Model, sameNumbers } from "./model.js"; +import { ancestors, capture, evaluate } from "./scenario.js"; +import { generate } from "./generate.js"; + +test("dense model agrees with the pure specification on branching graphs and sparse IDs", () => { + for (const s of generate(3350, 60, "branches")) { + const rename = (id: number) => (id < 0 ? id : id * 5 + 7); + for (const node of s.nodes) { + node.id = rename(node.id); + node.deps = node.deps.map(rename); + if (node.branch) { + node.branch.condition = rename(node.branch.condition); + node.branch.otherwise = node.branch.otherwise.map(rename); + } + } + for (const reader of s.readers) reader.refs = reader.refs.map(rename); + const model = new Model(s); + for (let a = 0; a < 3; a++) + for (let b = 0; b < 3; b++) { + const inputs = { [-1]: a, [-2]: b }; + const expected = evaluate(s, a, inputs); + const values = model.evaluate(a, inputs); + for (const [id, value] of expected) expect(values.get(id)).toBe(value); + for (let i = 0; i < s.nodes.length; i++) + expect(values.inputs(i)).toEqual(capture(s.nodes[i], id => expected.get(id)!)); + for (const reader of s.readers) { + const needed = model.select(reader.refs, values); + const expected = ancestors(s, reader.refs, evaluate(s, a, inputs)); + for (const [id, slot] of model.slots) expect(!!needed[slot]).toBe(expected.has(id)); + } + } + } +}); + +test("later evaluations and selections preserve earlier values and paths", () => { + const s = generate(33, 1)[0]; + const model = new Model(s); + const first = model.evaluate(0); + const inputs = first.inputs(0); + const path = model.select(s.readers[0].refs, first); + const savedPath = path.slice(); + const next = model.evaluate(1); + model.select([], next); + expect(first.get(-1)).toBe(0); + expect(next.get(-1)).toBe(1); + expect(first.inputs(0)).toEqual(inputs); + expect(path).toEqual(savedPath); + expect(sameNumbers(inputs, first.inputs(0))).toBe(true); + expect(sameNumbers([1], [1, 2])).toBe(false); + expect(sameNumbers([1], [2])).toBe(false); +}); + +test("latest read scope follows dependency masks without excluding independent ordinary nodes", () => { + const model = new Model({ + version: 2, + sources: [-1, -2], + show: true, + optimistic: { kind: "latest", proposals: [1], authoritative: 1 }, + nodes: [ + { id: 7, deps: [-2], factor: 1, offset: 0, delivery: "sync" }, + { id: 12, deps: [7], factor: 1, offset: 0, delivery: "manual" }, + { id: 20, deps: [-1], factor: 1, offset: 0, delivery: "sync" } + ], + readers: [], + turns: [] + }); + for (const ref of [-2, 7, 12]) expect(model.readsLatest(ref)).toBe(true); + for (const ref of [-1, 20]) expect(model.readsLatest(ref)).toBe(false); + expect(new Model(generate(33, 1)[0]).readsLatest(-1)).toBe(false); +}); diff --git a/packages/signals/tests/semantics/model.ts b/packages/signals/tests/semantics/model.ts new file mode 100644 index 000000000..67e724cee --- /dev/null +++ b/packages/signals/tests/semantics/model.ts @@ -0,0 +1,135 @@ +import { anchorIds, sourceIds, type Scenario } from "./scenario.js"; + +interface Node { + slot: number; + deps: number[]; + condition: number; + otherwise: number[]; + factor: number; + offset: number; +} + +/** An independently owned evaluation of the pure specification. Slots are dense + * even when shrinking leaves holes in the replay's stable node IDs. */ +export class Values { + readonly data: number[] = []; + constructor( + private model: Model, + input: number, + inputs?: Record + ) { + for (const id of model.sources) this.data.push(inputs?.[id] ?? (id === -1 ? input : 0)); + for (const node of model.nodes) { + const deps = node.condition < 0 || this.data[node.condition] ? node.deps : node.otherwise; + let sum = 0; + for (const dep of deps) sum += this.data[dep]; + this.data.push(sum * node.factor + node.offset); + } + } + get(ref: number): number { + return this.data[this.model.slots.get(ref)!]; + } + inputs(nodeIndex: number): number[] { + const node = this.model.nodes[nodeIndex]; + const deps = node.condition < 0 || this.data[node.condition] ? node.deps : node.otherwise; + const inputs: number[] = []; + if (node.condition >= 0) inputs.push(this.data[node.condition]); + for (const dep of deps) inputs.push(this.data[dep]); + return inputs; + } +} + +/** Compile graph shape once per case. Four source bits fit in a small integer; + * values remain ordinary small numbers, with no provenance wrappers. This is + * pure scenario metadata, never a model of Solid's lane or pending internals. */ +export class Model { + readonly sources: number[]; + readonly anchors: number[]; + readonly slots = new Map(); + readonly nodes: Node[] = []; + readonly masks: number[] = []; + readonly tupleMasks = new Map(); + readonly size: number; + readonly anchorMask: number; + readonly hasPending: boolean; + readonly latestMask: number; + constructor(s: Scenario) { + this.sources = sourceIds(s); + this.anchors = anchorIds(s); + let anchorMask = 0; + for (let i = 0; i < this.sources.length; i++) { + const id = this.sources[i]; + this.slots.set(id, i); + this.masks.push(1 << i); + if (this.anchors.includes(id)) anchorMask |= 1 << i; + } + this.anchorMask = anchorMask; + for (const spec of s.nodes) { + const slot = this.slots.size; + const deps: number[] = []; + for (const id of spec.deps) deps.push(this.slots.get(id)!); + const otherwise: number[] = spec.branch ? [] : deps; + if (spec.branch) for (const id of spec.branch.otherwise) otherwise.push(this.slots.get(id)!); + const condition = spec.branch ? this.slots.get(spec.branch.condition)! : -1; + let mask = condition < 0 ? 0 : this.masks[condition]; + for (const dep of deps) mask |= this.masks[dep]; + if (spec.branch) for (const dep of otherwise) mask |= this.masks[dep]; + this.masks.push(mask); + this.nodes.push({ + slot, + deps, + otherwise, + condition, + factor: spec.factor, + offset: spec.offset + }); + this.slots.set(spec.id, slot); + } + for (const reader of s.readers) { + let mask = 0; + for (const ref of reader.refs) if (ref < 0) mask |= this.sourceMask(ref); + this.tupleMasks.set(reader.id, mask); + } + this.size = this.slots.size; + this.latestMask = s.optimistic?.kind === "latest" ? this.sourceMask(-2) : 0; + this.hasPending = + !s.optimistic && + s.readers.some( + r => r.pending && !r.gated && r.mounted === undefined && r.boundary === "none" + ); + } + evaluate(input: number, inputs?: Record): Values { + return new Values(this, input, inputs); + } + sourceMask(ref: number): number { + return this.masks[this.slots.get(ref)!]; + } + readsLatest(ref: number): boolean { + return (this.sourceMask(ref) & this.latestMask) !== 0; + } + witnessed(ref: number, mask = this.anchorMask): boolean { + const sources = this.sourceMask(ref); + return (sources & mask) === sources; + } + select(refs: number[], values: Values, count = refs.length): Uint8Array { + const needed = new Uint8Array(this.size); + for (let i = 0; i < count; i++) needed[this.slots.get(refs[i])!] = 1; + for (let i = this.nodes.length - 1; i >= 0; i--) { + const node = this.nodes[i]; + if (!needed[node.slot]) continue; + let deps = node.deps; + if (node.condition >= 0) { + needed[node.condition] = 1; + if (!values.data[node.condition]) deps = node.otherwise; + } + for (let j = 0; j < deps.length; j++) needed[deps[j]] = 1; + } + return needed; + } +} + +export function sameNumbers(a: readonly number[], b: readonly number[]): boolean { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; + return true; +} diff --git a/packages/signals/tests/semantics/mounts.test.ts b/packages/signals/tests/semantics/mounts.test.ts new file mode 100644 index 000000000..f1610f6e9 --- /dev/null +++ b/packages/signals/tests/semantics/mounts.test.ts @@ -0,0 +1,154 @@ +import { chain } from "./fixtures.js"; +import { generate } from "./generate.js"; +import { runScenario } from "./runner.js"; +import { checkFinal, checkFrame } from "./rules.js"; +import { validate, type Scenario } from "./scenario.js"; + +test("an owned reader can mount, unmount and remount with the current value", async () => { + const s = chain("sync"); + s.version = 2; + s.sources = [-1]; + s.readers[0].mounted = false; + s.turns = [ + { steps: [{ op: "mount", reader: 0, value: true }] }, + { steps: [{ op: "mount", reader: 0, value: false }] }, + { steps: [{ op: "write", value: 2 }] }, + { steps: [{ op: "mount", reader: 0, value: true }] } + ]; + const r = await runScenario(s); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect(r.frames.findLast(f => f.at === "turn 1")?.outputs[0]).toBe("absent"); + expect(r.frames.at(-1)?.outputs[0]).toEqual([2, 8]); + expect(r.coverage).toContain("owned-reader-created"); +}); + +test("mounting an async reader with already settled inputs succeeds", async () => { + const s = chain(); + s.version = 2; + s.sources = [-1]; + s.readers[0].mounted = false; + s.turns = [ + { steps: [{ op: "write", value: 2 }] }, + { steps: [{ op: "resolve", node: 0, which: "newest" }] }, + { steps: [{ op: "mount", reader: 0, value: true }] } + ]; + const r = await runScenario(s); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect(r.frames.at(-1)?.outputs[0]).toEqual([2, 8]); +}); + +test("a missed owned mount cannot remove the final-state obligation", () => { + const s = chain("sync"); + s.version = 2; + s.sources = [-1]; + s.readers[0].mounted = false; + const frame = { + at: "final", + input: 0, + show: true, + mounts: { 0: false }, + outputs: { 0: "absent" as const } + }; + expect(checkFinal(s, frame, 0, true, undefined, { 0: true })?.rule).toBe("L1"); + expect(checkFrame(s, { ...frame, mounts: { 0: true } })?.rule).toBe("S2"); +}); + +test("generated mounting scenarios validate and replay", async () => { + for (const s of generate(3347, 30, "mounts")) { + expect(validate(s)).toBeUndefined(); + const a = await runScenario(s); + const b = await runScenario(a.scenario); + expect(["pass", "fail", "policy"]).toContain(a.status); + expect(b.status).toBe(a.status); + expect(b.frames).toEqual(a.frames); + expect(b.failure).toEqual(a.failure); + } +}); + +test("a detached child fallback is not publication of its parent mount", async () => { + const s: Scenario = { + version: 2, + show: false, + nodes: [ + { + id: 0, + deps: [-1], + factor: 1, + offset: 0, + delivery: "manual" + }, + { + id: 1, + deps: [0], + factor: 1, + offset: 0, + delivery: "manual" + }, + { + id: 2, + deps: [0], + factor: 1, + offset: 0, + delivery: "promise" + } + ], + readers: [ + { + refs: [0, 1, 2], + gated: false, + id: 0, + boundary: "retain", + mounted: false + }, + { + refs: [1, -1, 2], + gated: false, + id: 1, + boundary: "none" + } + ], + turns: [ + { + steps: [ + { + op: "queue", + id: 0, + via: "promise", + steps: [ + { + op: "write", + value: 2 + } + ] + } + ] + }, + { + steps: [ + { + op: "queue", + id: 3, + via: "await", + steps: [ + { + op: "mount", + reader: 0, + value: true + } + ] + }, + { + op: "write", + value: 0 + } + ] + } + ], + sources: [-1], + strict: true + }; + const r = await runScenario(s); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect(r.events.some(e => e.includes('prepared reader 0: "loading"'))).toBe(true); + expect(r.frames.every(f => f.mounts?.[0] || f.outputs[0] === "absent")).toBe(true); +}); diff --git a/packages/signals/tests/semantics/multi.test.ts b/packages/signals/tests/semantics/multi.test.ts new file mode 100644 index 000000000..a89aa2d8d --- /dev/null +++ b/packages/signals/tests/semantics/multi.test.ts @@ -0,0 +1,123 @@ +import { generate } from "./generate.js"; +import { runScenario } from "./runner.js"; +import { checkFrame } from "./rules.js"; +import { queuedSteps, validate, type Scenario } from "./scenario.js"; + +function independent(): Scenario { + return { + version: 2, + sources: [-1, -2], + show: true, + nodes: [ + { id: 0, deps: [-1], factor: 1, offset: 0, delivery: "manual" }, + { id: 1, deps: [-2], factor: 1, offset: 0, delivery: "manual" } + ], + readers: [ + { id: 0, refs: [-1, 0], gated: false, boundary: "none" }, + { id: 1, refs: [-2, 1], gated: false, boundary: "none" } + ], + turns: [ + { steps: [{ op: "write", source: -1, value: 1 }] }, + { steps: [{ op: "write", source: -2, value: 2 }] }, + { steps: [{ op: "resolve", node: 1, which: "newest" }] } + ] + }; +} + +test.each([0, 1])( + "disjoint branches can publish independently, resolving node %i first", + async node => { + const s = independent(); + s.turns[2] = { steps: [{ op: "resolve", node, which: "newest" }] }; + const r = await runScenario(s); + expect(r.status, JSON.stringify(r)).toBe("pass"); + const frame = r.frames.filter(f => f.at === "turn 2").at(-1)!; + expect(frame.inputs).toEqual(node === 0 ? { [-1]: 1, [-2]: 0 } : { [-1]: 0, [-2]: 2 }); + expect(r.frames.at(-1)?.inputs).toEqual({ [-1]: 1, [-2]: 2 }); + } +); + +test("dependency checks allow mixed independent generations but reject a wrong dependent value", () => { + const s = independent(); + const frame = { + at: "published", + input: 0, + inputs: { [-1]: 0, [-2]: 2 }, + show: true, + outputs: { 0: [0, 0], 1: [2, 2] } + }; + expect(checkFrame(s, frame)).toBeUndefined(); + expect(checkFrame(s, { ...frame, outputs: { ...frame.outputs, 1: [2, 0] } })?.rule).toBe("S1"); +}); + +test("a joining memo is checked against both published sources", async () => { + const s = independent(); + s.nodes.push({ id: 2, deps: [0, 1], factor: 1, offset: 0, delivery: "sync" }); + s.readers = [{ id: 2, refs: [2], gated: false, boundary: "none" }]; + const r = await runScenario(s); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect(r.frames.at(-1)?.outputs[2]).toEqual([3]); +}); + +test.each(["microtask", "promise", "await", "task"] as const)( + "a %s continuation executes its writes in one call stack", + async via => { + const s = independent(); + s.nodes.forEach(n => (n.delivery = "sync")); + s.turns = [ + { + steps: [ + { + op: "queue", + id: 0, + via, + steps: [ + { op: "write", source: -1, value: 1 }, + { op: "write", source: -2, value: 2 } + ] + } + ] + } + ]; + const r = await runScenario(s); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect( + r.frames.every( + f => + (f.inputs?.[-1] === 0 && f.inputs?.[-2] === 0) || + (f.inputs?.[-1] === 1 && f.inputs?.[-2] === 2) + ) + ).toBe(true); + } +); + +test("multi-source generation is valid, exercises both sources and continuation blocks, and replays", async () => { + const scenarios = generate(3337, 100, "multi"); + expect(generate(3337, 100, "multi")).toEqual(scenarios); + const writes = new Set(); + let blocks = 0; + for (const s of scenarios) { + expect(validate(s)).toBeUndefined(); + for (const t of s.turns) + if ("steps" in t) + for (const step of t.steps) { + if (step.op === "write") writes.add(step.source!); + if (step.op === "queue" && queuedSteps(step).length > 1) blocks++; + } + } + expect(writes).toEqual(new Set([-1, -2])); + expect(blocks).toBeGreaterThan(0); + for (const s of scenarios.slice(0, 20)) { + const a = await runScenario(s); + const b = await runScenario(a.scenario); + expect(["pass", "fail", "policy"]).toContain(a.status); + expect(b.status).toBe(a.status); + expect(b.frames).toEqual(a.frames); + } +}); + +test("source IDs cannot be mistaken for resolvable memo requests", () => { + const s = independent(); + s.turns = [{ steps: [{ op: "resolve", node: -2, which: "newest" }] }]; + expect(validate(s)).toBeDefined(); +}); diff --git a/packages/signals/tests/semantics/normalize.test.ts b/packages/signals/tests/semantics/normalize.test.ts new file mode 100644 index 000000000..40ae8b214 --- /dev/null +++ b/packages/signals/tests/semantics/normalize.test.ts @@ -0,0 +1,188 @@ +import { chain } from "./fixtures.js"; +import { normalize, scenarioKey } from "./normalize.js"; +import { orderingReductions } from "./order.js"; +import { validate, type Scenario } from "./scenario.js"; +import { runScenario } from "./runner.js"; + +function renamed(): Scenario { + const s = chain("manual"); + s.nodes[0].id = 8; + s.nodes[1].id = 23; + s.nodes[1].deps = [8]; + s.readers[0].id = 9; + s.readers[0].refs = [-1, 23]; + s.turns = [ + { steps: [{ op: "write", value: 1 }] }, + { + steps: [ + { + op: "queue", + id: 42, + via: "task", + step: { + op: "resolve", + node: 8, + which: "oldest", + key: "8:[1]#1", + variantKeys: ["8:[1]#1", null] + } + } + ] + }, + { task: 42 } + ]; + return s; +} + +test("normalization renames owners without changing exact request questions or callback boundaries", () => { + const s = renamed(); + const before = JSON.stringify(s); + const n = normalize(s); + expect(validate(n)).toBeUndefined(); + expect(n.nodes[1].deps).toEqual([0]); + expect(n.readers[0].refs).toEqual([-1, 1]); + expect(n.turns[1]).toEqual({ + steps: [ + { + op: "queue", + id: 0, + via: "task", + steps: [ + { + op: "resolve", + node: 0, + which: "newest", + key: "0:[1]#1", + variantKeys: ["0:[1]#1", null] + } + ] + } + ] + }); + expect(n.turns[2]).toEqual({ task: 0 }); + expect(scenarioKey(n)).toBe(scenarioKey(s)); + expect(JSON.stringify(s)).toBe(before); +}); + +test("alpha-renamed manual requests replay with the same values and completion state", async () => { + const s = renamed(); + const turn = s.turns[1]; + if ("steps" in turn) { + const q = turn.steps[0]; + if (q.op === "queue" && q.step?.op === "resolve") delete q.step.variantKeys; + } + const a = await runScenario(s); + const b = await runScenario(normalize(s)); + expect(a.status, a.error).not.toBe("invalid"); + expect(a.status).not.toBe("error"); + expect(b.status).toBe(a.status); + expect(b.failure?.message).toBe(a.failure?.message); + expect(b.frames.length).toBe(a.frames.length); + for (let i = 0; i < a.frames.length; i++) + expect(Object.values(b.frames[i].outputs)).toEqual(Object.values(a.frames[i].outputs)); +}); + +function siblings(): Scenario { + return { + version: 2, + sources: [-1, -2], + show: true, + nodes: [ + { id: 4, deps: [-1], factor: 1, offset: 9, delivery: "sync" }, + { id: 8, deps: [-2], factor: 1, offset: 2, delivery: "sync" } + ], + readers: [ + { id: 6, refs: [4], gated: false, boundary: "none" }, + { id: 7, refs: [8], gated: false, boundary: "none" } + ], + turns: [ + { + steps: [ + { op: "write", source: -1, value: 1 }, + { op: "write", source: -2, value: 2 } + ] + } + ] + }; +} + +function ordered(s: Scenario): Scenario { + for (let i = 0; i < 100; i++) { + const candidate = orderingReductions(s).next().value; + if (!candidate) return normalize(s); + expect(scenarioKey(candidate) < scenarioKey(s)).toBe(true); + expect(validate(candidate)).toBeUndefined(); + s = candidate; + } + throw Error("Ordering did not terminate"); +} + +test("different sibling creation orders converge using graph references rather than original names", () => { + const a = siblings(); + const b = siblings(); + b.nodes.reverse(); + b.readers.reverse(); + if ("steps" in b.turns[0]) b.turns[0].steps.reverse(); + expect(scenarioKey(a)).not.toBe(scenarioKey(b)); + expect(scenarioKey(ordered(a))).toBe(scenarioKey(ordered(b))); +}); + +test("ordering keeps parents before children, dependencies before users, and event boundaries intact", () => { + const s = chain(); + s.turns = [{ steps: [{ op: "write", value: 2 }, { op: "flush" }, { op: "write", value: 1 }] }]; + for (const c of orderingReductions(s)) { + expect(validate(c)).toBeUndefined(); + expect(c.turns).toEqual(s.turns); + expect(c.nodes[0].deps).toEqual([-1]); + } + const a = siblings(); + a.readers[0].boundary = "retain"; + a.readers[1].parent = a.readers[0].id; + for (const c of orderingReductions(a)) expect(validate(c)).toBeUndefined(); +}); + +test("action and reader references normalize while distinguished source roles stay fixed", () => { + const s = siblings(); + s.readers[0].boundary = "retain"; + s.readers[1].parent = 6; + s.actions = [{ id: 99, segments: [[{ source: -2, value: 3 }], []] }]; + s.turns = [ + { steps: [{ op: "start-action", action: 99 }] }, + { steps: [{ op: "resume-action", action: 99 }] } + ]; + const n = normalize(s); + expect(validate(n)).toBeUndefined(); + expect(n.readers[1].parent).toBe(0); + expect(n.actions![0]).toEqual({ id: 0, segments: [[{ source: -2, value: 3 }], []] }); + expect(n.turns[0]).toEqual({ steps: [{ op: "start-action", action: 0 }] }); + expect(n.sources).toEqual([-1, -2]); +}); + +test("renaming does not repair malformed exact request keys", () => { + const s = renamed(); + const turn = s.turns[1]; + if (!("steps" in turn)) throw Error("Missing callback"); + const q = turn.steps[0]; + if (q.op !== "queue" || q.step?.op !== "resolve") throw Error("Missing resolve"); + q.step.key = "08:[1]#1"; + expect(scenarioKey(s)).toContain('"key":"08:[1]#1"'); +}); + +test("default publication anchors normalize without losing absent anchors or creation order", async () => { + const s: Scenario = { ...chain("sync"), version: 2, sources: [-1, -2] }; + const explicit = { ...s, anchors: [-1, -2], anchorShow: true }; + expect(scenarioKey(s)).toBe(scenarioKey(explicit)); + expect(scenarioKey({ ...s, anchors: [] })).not.toBe(scenarioKey(s)); + expect(scenarioKey({ ...s, anchors: [-2, -1] })).not.toBe(scenarioKey(s)); + expect(scenarioKey({ ...s, anchorShow: false })).not.toBe(scenarioKey(s)); + const a = await runScenario(explicit), + b = await runScenario(normalize(explicit)); + expect(a.status).toBe("pass"); + expect(b.frames).toEqual(a.frames); +}); + +test("empty latest warmups normalize to omission without discarding real warmups", () => { + const s = chain(); + expect(scenarioKey({ ...s, warmLatest: [] })).toBe(scenarioKey(s)); + expect(normalize({ ...s, warmLatest: [-1] }).warmLatest).toEqual([-1]); +}); diff --git a/packages/signals/tests/semantics/normalize.ts b/packages/signals/tests/semantics/normalize.ts new file mode 100644 index 000000000..bc1e5893c --- /dev/null +++ b/packages/signals/tests/semantics/normalize.ts @@ -0,0 +1,152 @@ +import { queuedSteps, sourceIds, type Leaf, type Scenario, type Step } from "./scenario.js"; + +/** Alpha-renaming preserves creation/read/event order. Request inputs and + * occurrence numbers are opaque: only their owning node's ID is renamed. + * This runs during triage, never in the generated graph's hot path. */ +export function normalize(s: Scenario): Scenario { + const nodes = new Map(); + const readers = new Map(); + const actions = new Map(); + const callbacks = new Map(); + for (let i = 0; i < s.nodes.length; i++) nodes.set(s.nodes[i].id, i); + for (let i = 0; i < s.readers.length; i++) readers.set(s.readers[i].id, i); + for (let i = 0; i < (s.actions?.length ?? 0); i++) actions.set(s.actions![i].id, i); + for (const turn of s.turns) + if ("steps" in turn) + for (const step of turn.steps) + if (step.op === "queue") callbacks.set(step.id, callbacks.size); + // Negative source IDs have distinguished roles (-1 truth, -2 optimistic). + // Keep them fixed; graph ordering must not exchange those roles. + const ref = (id: number) => (id < 0 ? id : nodes.get(id)!); + const refs = (ids: number[]) => { + const result: number[] = []; + for (const id of ids) result.push(ref(id)); + return result; + }; + const request = (key: string): string => { + const colon = key.indexOf(":"); + const id = Number(key.slice(0, colon)); + // Preserve malformed keys rather than making an invalid replay runnable. + return colon > 0 && String(id) === key.slice(0, colon) && nodes.has(id) + ? `${ref(id)}${key.slice(colon)}` + : key; + }; + const requestKeys = (keys: Array | undefined) => { + if (!keys) return undefined; + const result: Array = []; + for (const key of keys) result.push(key === null ? null : request(key)); + return result; + }; + const leaf = (x: Leaf): Leaf => { + switch (x.op) { + case "write": + return { op: x.op, source: x.source ?? -1, value: x.value }; + case "show": + return { op: x.op, value: x.value }; + case "resolve": + return { + op: x.op, + node: ref(x.node), + which: x.key ? "newest" : x.which, + key: x.key === undefined ? undefined : request(x.key), + variantKeys: requestKeys(x.variantKeys) + }; + case "read": + return { op: x.op, ref: ref(x.ref), mode: x.mode }; + case "mount": + return { op: x.op, reader: readers.get(x.reader)!, value: x.value }; + case "dispose": + case "click": + return { op: x.op, reader: readers.get(x.reader)! }; + case "start-action": + case "resume-action": + return { op: x.op, action: x.action === undefined ? undefined : actions.get(x.action)! }; + default: + return { op: x.op }; + } + }; + const step = (x: Step): Step => { + if (x.op === "cancel") return { op: x.op, id: callbacks.get(x.id)! }; + if (x.op !== "queue") return leaf(x); + const steps: Leaf[] = []; + for (const item of queuedSteps(x)) steps.push(leaf(item)); + return { op: x.op, id: callbacks.get(x.id)!, via: x.via, steps }; + }; + const renamedNodes: Scenario["nodes"] = []; + for (const n of s.nodes) + renamedNodes.push({ + id: ref(n.id), + deps: refs(n.deps), + factor: n.factor, + offset: n.offset, + delivery: n.delivery, + branch: n.branch && { + condition: ref(n.branch.condition), + otherwise: refs(n.branch.otherwise) + } + }); + const renamedReaders: Scenario["readers"] = []; + for (const r of s.readers) + renamedReaders.push({ + id: readers.get(r.id)!, + refs: refs(r.refs), + gated: r.gated, + boundary: r.boundary, + parent: r.parent === undefined ? undefined : readers.get(r.parent), + mounted: r.mounted, + pending: r.pending, + pendingDepth: r.pendingDepth, + render: r.render && { + on: ref(r.render.on), + target: r.render.target, + scheduled: r.render.scheduled + } + }); + let renamedActions: Scenario["actions"]; + if (s.actions) { + renamedActions = []; + for (const a of s.actions) + renamedActions.push({ id: actions.get(a.id)!, segments: a.segments }); + } + const turns: Scenario["turns"] = []; + for (const t of s.turns) { + if ("task" in t) turns.push({ task: callbacks.get(t.task)! }); + else { + const steps: Step[] = []; + for (const x of t.steps) steps.push(step(x)); + turns.push({ steps }); + } + } + let anchors = s.anchors; + const sources = sourceIds(s); + if (anchors && anchors.length === sources.length) { + let same = true; + for (let i = 0; i < anchors.length; i++) if (anchors[i] !== sources[i]) same = false; + if (same) anchors = undefined; + } + return { + version: s.version, + sources: s.sources, + anchors, + anchorShow: s.anchorShow === false ? false : undefined, + optimistic: s.optimistic && { + kind: s.optimistic.kind, + proposals: s.optimistic.proposals, + authoritative: s.optimistic.authoritative, + hold: s.optimistic.hold, + viaMemo: s.optimistic.viaMemo + }, + actions: renamedActions, + warmLatest: s.warmLatest?.length ? refs(s.warmLatest) : undefined, + nodes: renamedNodes, + readers: renamedReaders, + show: s.show, + turns, + strict: s.strict + }; +} + +/** Exact normalized execution, not a graph-isomorphism or symptom hash. */ +export function scenarioKey(s: Scenario): string { + return JSON.stringify(normalize(s)); +} diff --git a/packages/signals/tests/semantics/observation-reduce.test.ts b/packages/signals/tests/semantics/observation-reduce.test.ts new file mode 100644 index 000000000..36a7e843a --- /dev/null +++ b/packages/signals/tests/semantics/observation-reduce.test.ts @@ -0,0 +1,98 @@ +import { chain } from "./fixtures.js"; +import { observationReductions } from "./reduce.js"; +import { validate, type Scenario } from "./scenario.js"; + +function mounted(): Scenario { + const s: Scenario = { ...chain(), version: 2, sources: [-1], anchorShow: false }; + s.readers[0].mounted = false; + s.readers.push({ id: 1, refs: [0], gated: false, boundary: "none", mounted: true }); + s.turns = [ + { + steps: [ + { + op: "queue", + id: 0, + via: "task", + steps: [ + { op: "mount", reader: 0, value: true }, + { op: "mount", reader: 1, value: false }, + { op: "resolve", node: 0, which: "newest", key: "0:[1]#2" } + ] + } + ] + }, + { task: 0 } + ]; + return s; +} + +test("mount-to-gate preserves initial visibility, request keys and other mounting controls", () => { + const s = mounted(), + before = structuredClone(s); + const c = [...observationReductions(s)].find(c => c.readers[0].gated)!; + expect(c.show).toBe(false); + expect(c.readers[0].mounted).toBeUndefined(); + expect(c.readers[1]).toBe(s.readers[1]); + expect(c.turns).toEqual([ + { + steps: [ + { + op: "queue", + id: 0, + via: "task", + steps: [ + { op: "show", value: true }, + { op: "mount", reader: 1, value: false }, + { op: "resolve", node: 0, which: "newest", key: "0:[1]#2" } + ] + } + ] + }, + { task: 0 } + ]); + expect(validate(c)).toBeUndefined(); + expect(s).toEqual(before); +}); + +test("mount conversion does not reuse an observed or explicitly set visibility signal", () => { + for (const mode of ["anchor", "default-anchor", "reader", "setter"] as const) { + const s = mounted(); + if (mode === "anchor") s.anchorShow = true; + if (mode === "default-anchor") delete s.anchorShow; + if (mode === "reader") s.readers[1].gated = true; + if (mode === "setter") s.turns.push({ steps: [{ op: "show", value: false }] }); + expect([...observationReductions(s)], mode).toEqual([]); + } +}); + +test("a fixed source reader can become an anchor without changing other readers or work", () => { + const s: Scenario = { ...chain(), version: 2, sources: [-1, -2], anchors: [-2] }; + s.readers[0].refs = [-1]; + s.readers.push({ id: 1, refs: [1], gated: false, boundary: "none" }); + const before = structuredClone(s), + [c] = [...observationReductions(s)]; + expect(c.anchors).toEqual([-2, -1]); + expect(c.readers).toEqual([s.readers[1]]); + expect(c.nodes).toBe(s.nodes); + expect(c.turns).toBe(s.turns); + expect(validate(c)).toBeUndefined(); + expect(s).toEqual(before); + s.anchors = [-1, -2]; + expect([...observationReductions(s)]).toEqual([]); + delete s.anchors; + expect([...observationReductions(s)]).toEqual([]); +}); + +test("anchor substitution retains source readers addressed by queued disposal", () => { + const s: Scenario = { ...chain(), version: 2, sources: [-1], anchors: [] }; + s.readers[0].refs = [-1]; + s.turns = [ + { steps: [{ op: "queue", id: 0, via: "microtask", steps: [{ op: "dispose", reader: 0 }] }] } + ]; + expect([...observationReductions(s)]).toEqual([]); + s.turns = []; + for (const change of [{ gated: true }, { boundary: "retain" as const }, { pending: true }]) { + const c: Scenario = { ...s, readers: [{ ...s.readers[0], ...change }] }; + expect([...observationReductions(c)]).toEqual([]); + } +}); diff --git a/packages/signals/tests/semantics/optimistic-equivalence.test.ts b/packages/signals/tests/semantics/optimistic-equivalence.test.ts new file mode 100644 index 000000000..c2247d8be --- /dev/null +++ b/packages/signals/tests/semantics/optimistic-equivalence.test.ts @@ -0,0 +1,47 @@ +import { chain } from "./fixtures.js"; +import { generate } from "./generate.js"; +import { runEquivalence, shrinkEquivalence, variants } from "./equivalence.js"; +import { runScenario } from "./runner.js"; + +test.each(["sync", "manual", "promise", "await"] as const)( + "a %s derivation has the same publication for a held optimistic proposal", + async delivery => { + const pair = await runEquivalence(chain(delivery), runScenario, "optimistic"); + expect(pair.result.status, JSON.stringify(pair)).toBe("pass"); + expect(pair.pair[1].action?.bodyCompleted).toBe(false); + expect(pair.pair[1].action?.gates).toEqual([{ step: 0, state: "waiting" }]); + expect(pair.pair[1].frames.at(-1)?.inputs?.[-2]).toBe(1); + expect(pair.pair[1].coverage).toContain("completion-with-parent-held"); + } +); + +test("generated ordinary/optimistic pairs validate, reduce and replay", async () => { + for (const s of generate(3289, 30)) { + const a = await runEquivalence(s, runScenario, "optimistic"); + const b = await runEquivalence(a.scenario, runScenario, "optimistic"); + expect(["pass", "fail", "policy"], JSON.stringify(a)).toContain(a.result.status); + expect(b.signature).toBe(a.signature); + expect(b.pair.map(r => r.frames)).toEqual(a.pair.map(r => r.frames)); + } +}); + +test("paired reduction preserves an optimistic-only publication difference", async () => { + const s = chain("sync"); + const run: typeof runScenario = async s => { + const r = await runScenario(s); + // Calibrate the comparison/reducer independently of the runtime assertions. + if (s.optimistic && r.frames.length) r.frames.at(-1)!.outputs[0] = [99]; + return r; + }; + const original = await runEquivalence(s, run, "optimistic"); + expect(original.result.failure?.rule).toBe("E5"); + const reduced = await shrinkEquivalence(s, run, 30, "optimistic"); + expect(reduced.accepted).toBeGreaterThan(0); + expect((await runEquivalence(reduced.scenario, run, "optimistic")).signature).toBe( + original.signature + ); + expect((await runEquivalence(reduced.scenario, runScenario, "optimistic")).result.status).toBe( + "pass" + ); + expect(variants(reduced.scenario, "optimistic")[1].optimistic?.hold).toBe(true); +}); diff --git a/packages/signals/tests/semantics/optimistic-scope.test.ts b/packages/signals/tests/semantics/optimistic-scope.test.ts new file mode 100644 index 000000000..21012e4ff --- /dev/null +++ b/packages/signals/tests/semantics/optimistic-scope.test.ts @@ -0,0 +1,54 @@ +import { checkFinal, checkOptimistic, type Frame } from "./rules.js"; +import type { Scenario } from "./scenario.js"; +import { runScenario } from "./runner.js"; + +const scenario: Scenario = { + version: 2, + sources: [-1, -2], + show: true, + optimistic: { kind: "latest", viaMemo: true, proposals: [0, 0], authoritative: 0 }, + nodes: [{ id: 0, deps: [-2], factor: 1, offset: 0, delivery: "promise" }], + readers: [{ id: 0, refs: [0], gated: false, boundary: "retain", mounted: true }], + anchors: [], + anchorShow: false, + turns: [ + { steps: [{ op: "start-action" }] }, + { steps: [{ op: "resume-action" }, { op: "mount", reader: 0, value: false }] } + ] +}; + +test("an ordinary unmount may remain held until the action ends without failing O1", async () => { + const r = await runScenario(scenario); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect(r.frames.at(-1)?.outputs[0]).toBe("absent"); +}); + +test("O1 checks optimistic data without imposing ordinary source or mount completion", () => { + const s: Scenario = { + ...scenario, + anchors: [-1, -2], + readers: [...scenario.readers, { id: 1, refs: [-2, 0], gated: false, boundary: "none" }] + }; + const frame: Frame = { + at: "action open", + input: 0, + inputs: { [-1]: 0, [-2]: 1 }, + show: true, + mounts: { 0: true }, + outputs: { 0: [0], 1: [1, 1] } + }; + const desired = { [-1]: 2, [-2]: 1 }; + expect(checkOptimistic(s, frame, desired)).toBeUndefined(); + expect(checkOptimistic(s, { ...frame, outputs: { 0: [0], 1: [1, 0] } }, desired)).toMatchObject({ + rule: "O1", + reader: 1 + }); + expect(checkOptimistic(s, { ...frame, inputs: { [-1]: 0, [-2]: 0 } }, desired)).toMatchObject({ + rule: "O1" + }); + // The unmount still must complete once all work settles. + expect(checkFinal(s, frame, 0, true, frame.inputs, { 0: false })).toMatchObject({ + rule: "L1", + reader: 0 + }); +}); diff --git a/packages/signals/tests/semantics/optimistic.test.ts b/packages/signals/tests/semantics/optimistic.test.ts new file mode 100644 index 000000000..1162c29ed --- /dev/null +++ b/packages/signals/tests/semantics/optimistic.test.ts @@ -0,0 +1,61 @@ +import { generate } from "./generate.js"; +import { runScenario } from "./runner.js"; +import { validate, type Scenario } from "./scenario.js"; + +function optimistic(delivery: "sync" | "manual" = "sync"): Scenario { + return { + version: 2, + sources: [-1, -2], + optimistic: { proposals: [1], authoritative: 0 }, + show: true, + nodes: [{ id: 0, deps: [-2], factor: 2, offset: 0, delivery }], + readers: [{ id: 0, refs: [-2, 0], gated: false, boundary: "none" }], + turns: [{ steps: [{ op: "start-action" }] }] + }; +} + +test("an open action allows a synchronous optimistic publication then reverts", async () => { + const r = await runScenario(optimistic()); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect(r.frames.some(f => f.inputs?.[-2] === 1 && f.outputs[0]?.[1] === 2)).toBe(true); + expect(r.frames.at(-1)?.inputs).toEqual({ [-1]: 0, [-2]: 0 }); + expect(r.action).toEqual({ + started: true, + bodyCompleted: true, + gates: [{ step: 0, state: "resolved" }] + }); + expect(r.coverage).toContain("optimistic-ready-with-parent-open"); +}); + +test("an observed async optimistic answer may remain pending while its parent is held", async () => { + const s = optimistic("manual"); + s.turns.push({ steps: [{ op: "resolve", node: 0, which: "newest" }] }); + // The completion callback's scope during corrective async is a separate + // contract question. This control isolates publication and action lifetime. + const r = await runScenario(s, { completionProbes: false }); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect(r.frames.some(f => f.inputs?.[-2] === 1)).toBe(true); + expect(r.coverage).toContain("optimistic-ready-with-parent-open"); +}); + +test("finite action scripts finish on cleanup even if a work limit stops their first proposal", async () => { + const s = optimistic("manual"); + s.optimistic!.proposals = [1, 2, 3]; + const r = await runScenario(s, { maxWork: 1 }); + expect(r.status).toBe("limit"); + expect(r.action?.bodyCompleted).toBe(false); + expect((await runScenario(optimistic())).status).toBe("pass"); +}); + +test("optimistic generation uses valid bounded actions and deterministic replay", async () => { + for (const s of generate(3330, 30, "optimistic")) { + expect(validate(s)).toBeUndefined(); + const a = await runScenario(s); + expect(["pass", "fail", "policy"], JSON.stringify(a)).toContain(a.status); + const b = await runScenario(a.scenario); + expect(b.status).toBe(a.status); + expect(b.failure).toEqual(a.failure); + expect(b.frames).toEqual(a.frames); + expect(b.action).toEqual(a.action); + } +}); diff --git a/packages/signals/tests/semantics/order.ts b/packages/signals/tests/semantics/order.ts new file mode 100644 index 000000000..71069df08 --- /dev/null +++ b/packages/signals/tests/semantics/order.ts @@ -0,0 +1,103 @@ +import { normalize, scenarioKey } from "./normalize.js"; +import { queuedSteps, validate, type Leaf, type Scenario, type Step } from "./scenario.js"; + +function swap(items: T[], i: number, j: number): T[] { + const result = items.slice(); + result[i] = items[j]; + result[j] = items[i]; + return result; +} + +/** Each move descends in the same ordering, so sorting cannot oscillate. + * Even independent declarations may change notification order: these are + * replay-tested candidates, never assumed equivalences. */ +export function* orderingReductions(s: Scenario): Generator { + const key = scenarioKey(s); + function* offer(candidate: Scenario): Generator { + if (validate(candidate)) return; + const named = normalize(candidate); + if (JSON.stringify(named) < key) yield named; + } + for (let i = 0; i < s.nodes.length; i++) { + for (let j = i + 1; j < s.nodes.length; j++) yield* offer({ ...s, nodes: swap(s.nodes, i, j) }); + const n = s.nodes[i]; + for (let j = 1; j < n.deps.length; j++) { + const nodes = s.nodes.slice(); + nodes[i] = { ...n, deps: swap(n.deps, j - 1, j) }; + yield* offer({ ...s, nodes }); + } + } + for (let i = 0; i < s.readers.length; i++) { + for (let j = i + 1; j < s.readers.length; j++) + yield* offer({ ...s, readers: swap(s.readers, i, j) }); + const r = s.readers[i]; + for (let j = 1; j < r.refs.length; j++) { + const readers = s.readers.slice(); + readers[i] = { ...r, refs: swap(r.refs, j - 1, j) }; + yield* offer({ ...s, readers }); + } + } + // Do not move anything across a host turn, callback, or explicit flush. + // Reordering different setters is still tested: changing mount/visibility + // order can change observation even when both setters run in one callback. + function movable(a: Leaf | Step, b: Leaf | Step): boolean { + // Progress can change observation even within one callback. Keep start/resume + // order intact; these are replay-tested swaps with setters, not commutativity. + if (a.op === "resume-action") return b.op === "show" || b.op === "mount" || b.op === "write"; + if (b.op === "resume-action") return a.op === "show" || a.op === "mount" || a.op === "write"; + if (a.op !== "write" && a.op !== "show" && a.op !== "mount") return false; + if (b.op !== "write" && b.op !== "show" && b.op !== "mount") return false; + if (a.op !== b.op) return true; + if (a.op === "write" && b.op === "write") return (a.source ?? -1) !== (b.source ?? -1); + return a.op === "mount" && b.op === "mount" && a.reader !== b.reader; + } + for (let i = 0; i < s.turns.length; i++) { + const turn = s.turns[i]; + if (!("steps" in turn)) continue; + for (let j = 0; j < turn.steps.length; j++) { + if (j && movable(turn.steps[j - 1], turn.steps[j])) { + const turns = s.turns.slice(); + turns[i] = { steps: swap(turn.steps, j - 1, j) }; + yield* offer({ ...s, turns }); + } + const q = turn.steps[j]; + if (q.op !== "queue") continue; + const payload = queuedSteps(q); + for (let k = 1; k < payload.length; k++) { + if (!movable(payload[k - 1], payload[k])) continue; + const steps = turn.steps.slice(); + steps[j] = { op: "queue", id: q.id, via: q.via, steps: swap(payload, k - 1, k) }; + const turns = s.turns.slice(); + turns[i] = { steps }; + yield* offer({ ...s, turns }); + } + } + } +} + +/** Keep a delivery/order pivot from undoing earlier ordering progress after + * another family simplifies delivery. A smaller graph starts a new search. */ +export class OrderingLimit { + private size: number; + private key: string; + + constructor(s: Scenario) { + this.size = s.nodes.length; + this.key = scenarioKey(s); + } + + allows(s: Scenario): boolean { + return ( + s.nodes.length < this.size || (s.nodes.length === this.size && scenarioKey(s) < this.key) + ); + } + + accept(s: Scenario): void { + if (s.nodes.length > this.size) return; + const key = scenarioKey(s); + if (s.nodes.length < this.size || key < this.key) { + this.size = s.nodes.length; + this.key = key; + } + } +} diff --git a/packages/signals/tests/semantics/output.test.ts b/packages/signals/tests/semantics/output.test.ts new file mode 100644 index 000000000..38ab274c4 --- /dev/null +++ b/packages/signals/tests/semantics/output.test.ts @@ -0,0 +1,148 @@ +import { + createMemo, + createRenderEffect, + createRoot, + createSignal, + flush +} from "../../src/index.js"; +import { OutputTree, checkAttachment } from "./output.js"; +import { checkFrame } from "./rules.js"; +import { HostTasks } from "./host.js"; +import type { Scenario } from "./scenario.js"; + +const scalar: Scenario = { + version: 1, + show: true, + nodes: [], + turns: [], + readers: [{ id: 0, refs: [-1], gated: false, boundary: "none" }] +}; + +test("only attachment publishes prepared values; premature attachment violates consistency", () => { + const tree = new OutputTree(), + root = tree.create(true); + const old = tree.create(), + prepared = tree.create(); + tree.write(old, [0]); + tree.append(root, old); + tree.write(prepared, [1]); + const frame = () => ({ at: "probe", input: 0, show: true, outputs: { 0: tree.read(root) } }); + expect(checkFrame(scalar, frame())).toBeUndefined(); + expect(tree.privateWrites).toBe(2); + tree.replace(root, prepared); // deliberately broken renderer + expect(checkFrame(scalar, frame())?.rule).toBe("S1"); +}); + +test("visible cleanup is observable; detached cleanup and same-flush replacement are harmless", () => { + const tree = new OutputTree(), + root = tree.create(true); + const old = tree.create(), + discarded = tree.create(), + replacement = tree.create(); + tree.write(old, [0]); + tree.append(root, old); + tree.write(discarded, [1]); + tree.append(tree.create(), discarded); + tree.detach(discarded); + tree.read(root); + expect(checkAttachment(0, tree.count, true, true)).toBeUndefined(); + + tree.detach(old); // deliberately early cleanup, sampled at a host checkpoint + tree.read(root); + expect(checkAttachment(0, tree.count, true, true)?.rule).toBe("A1"); + tree.write(replacement, [1]); + tree.append(root, replacement); + tree.read(root); // cleanup + replacement inside one flush is allowed + expect(checkAttachment(0, tree.count, true, true)).toBeUndefined(); + expect(checkAttachment(0, tree.count, true, false)?.rule).toBe("A1"); + tree.clear(root); + tree.read(root); + expect(checkAttachment(0, tree.count, true, false)).toBeUndefined(); + expect(checkAttachment(0, tree.count, false, true)).toBeUndefined(); +}); + +test("portal visibility follows its destination, independently of the detached logical owner", () => { + const tree = new OutputTree(), + root = tree.create(true), + logical = tree.create(); + const target = tree.create(), + child = tree.create(); + tree.write(child, [1]); + tree.append(target, child); + expect(tree.read(target)).toBe("absent"); + tree.append(root, target); + expect(tree.visible(logical)).toBe(false); + expect(tree.read(target)).toEqual([1]); + tree.detach(child); + tree.read(target); + expect(checkAttachment(0, tree.count, true, true)?.rule).toBe("A1"); +}); + +test("duplicate contributions, moves and stale cleanup keep exact host identity", () => { + const tree = new OutputTree(), + root = tree.create(true), + other = tree.create(true); + const a = tree.create(), + b = tree.create(); + tree.write(a, [0]); + tree.write(b, [0]); + tree.append(root, a); + tree.append(root, b); + tree.read(root); + expect(checkAttachment(0, tree.count, true, true)?.message).toContain("duplicate"); + tree.append(other, a); + tree.detach(a); + tree.detach(a); + expect(tree.read(root)).toEqual([0]); + expect(tree.count).toBe(1); + expect(tree.read(other)).toBe("absent"); + expect(() => tree.append(b, b)).toThrow("Cyclic"); +}); + +test("default nested effects can prepare fresh content while a scheduled parent is held", async () => { + const host = new HostTasks(), + tree = new OutputTree(), + root = tree.create(true); + const requests: Array<() => void> = []; + let set!: (v: number) => void, dispose!: () => void; + createRoot(d => { + dispose = d; + const [value, write] = createSignal(0); + set = write; + const slow = createMemo(() => { + const v = value(); + return new Promise(resolve => requests.push(() => resolve(v))); + }); + createRenderEffect( + () => { + const v = value(), + cell = tree.create(); + createRenderEffect( + () => v, + next => tree.write(cell, [next]) + ); + createRenderEffect(slow, () => {}); + return cell; + }, + cell => tree.replace(root, cell), + { schedule: true } + ); + }); + try { + flush(); + requests.shift()!(); + await host.drain(); + expect(tree.read(root)).toEqual([0]); + set(1); + flush(); + expect(tree.privateWrites).toBe(2); + expect(tree.read(root)).toEqual([0]); + requests.shift()!(); + await host.drain(); + expect(tree.read(root)).toEqual([1]); + } finally { + dispose(); + flush(); + host.close(); + } +}); diff --git a/packages/signals/tests/semantics/output.ts b/packages/signals/tests/semantics/output.ts new file mode 100644 index 000000000..de5e2342e --- /dev/null +++ b/packages/signals/tests/semantics/output.ts @@ -0,0 +1,159 @@ +import { createRenderEffect, onCleanup } from "../../src/index.js"; +import type { Failure, Output } from "./rules.js"; +import type { RenderSpec } from "./scenario.js"; + +interface Node { + parent: number; + first: number; + last: number; + previous: number; + next: number; + root: boolean; + value?: Output; +} + +/** Host attachment is independent of reactive ownership. One record per created + * host node; disposal has no effect unless renderer cleanup actually detaches it. */ +export class OutputTree { + private nodes: Node[] = []; + count = 0; + privateWrites = 0; + visibleWrites = 0; + get size(): number { + return this.nodes.length; + } + constructor( + private limit: () => never = () => { + throw new Error("Output node limit"); + } + ) {} + create(root = false): number { + if (this.nodes.length === 4096) this.limit(); + const id = this.nodes.length; + this.nodes.push({ + parent: -1, + first: -1, + last: -1, + previous: -1, + next: -1, + root, + value: undefined + }); + return id; + } + visible(id: number): boolean { + while (this.nodes[id].parent !== -1) id = this.nodes[id].parent; + return this.nodes[id].root; + } + write(id: number, value: Output): void { + if (this.visible(id)) this.visibleWrites++; + else this.privateWrites++; + this.nodes[id].value = value; + } + append(parent: number, id: number): void { + // Bad renderer operations are harness errors, not semantic findings. + for (let p = parent; p !== -1; p = this.nodes[p].parent) + if (p === id) throw new Error("Cyclic host attachment"); + if (this.nodes[id].root) throw new Error("Cannot attach a visible root"); + this.detach(id); + const node = this.nodes[id], + target = this.nodes[parent]; + node.parent = parent; + node.previous = target.last; + if (target.last === -1) target.first = id; + else this.nodes[target.last].next = id; + target.last = id; + } + detach(id: number): void { + const node = this.nodes[id]; + if (node.parent === -1) return; + const parent = this.nodes[node.parent]; + if (node.previous === -1) parent.first = node.next; + else this.nodes[node.previous].next = node.next; + if (node.next === -1) parent.last = node.previous; + else this.nodes[node.next].previous = node.previous; + node.parent = node.previous = node.next = -1; + } + clear(id: number): void { + while (this.nodes[id].first !== -1) this.detach(this.nodes[id].first); + } + replace(parent: number, id: number): void { + this.clear(parent); + this.append(parent, id); + } + /** Read the attached subtree without cloning it or consulting Solid state. + * count exposes duplicate contributions instead of silently picking one. */ + read(root: number): Output { + this.count = 0; + let value: Output = "absent"; + if (!this.visible(root)) return value; + let id = root; + while (id !== -1) { + const node = this.nodes[id]; + if (node.value !== undefined) { + if (this.count++ === 0) value = node.value; + } + if (node.first !== -1) id = node.first; + else { + while (id !== root && this.nodes[id].next === -1) id = this.nodes[id].parent; + id = id === root ? -1 : this.nodes[id].next; + } + } + return value; + } +} + +export function checkAttachment( + reader: number, + count: number, + initialized: boolean, + active: boolean +): Failure | undefined { + const message = + !active && count + ? "Removed region still has visible output" + : active && count > 1 + ? "Region has duplicate visible output" + : active && initialized && !count + ? "Visible output disappeared before replacement or removal" + : undefined; + if (message) + return { rule: "A1", message, reader, expected: { count: active ? 1 : 0, actual: count } }; +} + +/** A compiled-renderer shape: a parent compute creates private content, nested + * effects populate it, and the parent's scheduled apply attaches it. A portal + * instead attaches its payload to an independently visible destination. */ +export function mountOutput( + tree: OutputTree, + root: number, + spec: RenderSpec, + readParent: () => number, + read: () => Output +): void { + const markerRoot = spec.target === "portal" ? tree.create(true) : root; + createRenderEffect( + () => { + readParent(); + const container = tree.create(); + const text = tree.create(); + createRenderEffect( + read, + value => { + tree.write(text, value); + tree.append(spec.target === "portal" ? root : container, text); + return () => tree.detach(text); + }, + { schedule: spec.scheduled } + ); + return container; + }, + container => tree.replace(markerRoot, container), + { schedule: true } + ); + // Only explicit removal of the entire region authorizes this removal. + onCleanup(() => { + tree.clear(root); + if (markerRoot !== root) tree.clear(markerRoot); + }); +} diff --git a/packages/signals/tests/semantics/paired-reduce.test.ts b/packages/signals/tests/semantics/paired-reduce.test.ts new file mode 100644 index 000000000..0e9bcc3ec --- /dev/null +++ b/packages/signals/tests/semantics/paired-reduce.test.ts @@ -0,0 +1,244 @@ +import { chain } from "./fixtures.js"; +import { + mergeArithmeticReductions, + deliveryOrderingReductions, + callbackEndReductions, + initialVisibilityReductions +} from "./reduce.js"; +import { OrderingLimit } from "./order.js"; +import { scenarioKey } from "./normalize.js"; +import { validate, type Scenario } from "./scenario.js"; +import { runScenario } from "./runner.js"; +import { shrink, type ShrinkOptions } from "./shrink.js"; + +function branch(): Scenario { + return { + version: 2, + sources: [-1, -2], + anchors: [], + anchorShow: false, + show: true, + nodes: [ + { id: 0, deps: [-2], factor: 1, offset: 0, delivery: "promise" }, + { + id: 1, + deps: [-1], + branch: { condition: -2, otherwise: [0] }, + factor: 1, + offset: 0, + delivery: "promise" + } + ], + readers: [{ id: 0, refs: [-2, 1], gated: false, boundary: "none" }], + turns: [{ steps: [{ op: "write", source: -2, value: 1 }] }] + }; +} + +test("source merging repairs a literal in the same candidate and keeps exact flight keys", () => { + const s = branch(); + s.turns.push({ + steps: [ + { op: "resolve", node: 0, which: "newest", key: "0:[1]#2", variantKeys: ["0:[1]#2", null] } + ] + }); + const before = JSON.stringify(s); + const c = [...mergeArithmeticReductions(s)].find(c => c.nodes[0].offset === 1)!; + expect(c.sources).toEqual([-1]); + expect(c.nodes[0].deps).toEqual([-1]); + expect(c.nodes[1].branch?.condition).toBe(-1); + expect(c.turns[0]).toEqual({ steps: [{ op: "write", source: -1, value: 1 }] }); + expect(c.turns[1]).toEqual(s.turns[1]); + expect(validate(c)).toBeUndefined(); + expect(JSON.stringify(s)).toBe(before); + s.optimistic = { proposals: [1], authoritative: 0 }; + expect([...mergeArithmeticReductions(s)]).toHaveLength(0); +}); + +test("source/offset search can cross two passing single edits", async () => { + const s = branch(), + baseline = await runScenario(s); + const original = { + ...baseline, + status: "fail" as const, + failure: { rule: "S1", message: "original" } + }; + const run = async (scenario: Scenario) => { + const failed = scenario.sources?.length === 1 && scenario.nodes[0].offset === 1; + return { + ...baseline, + scenario, + status: failed ? ("fail" as const) : ("pass" as const), + failure: failed ? { rule: "L1", message: "smaller" } : undefined + }; + }; + expect((await run({ ...s, nodes: [{ ...s.nodes[0], offset: 1 }, s.nodes[1]] })).status).toBe( + "pass" + ); + const r = await shrink(original, run, 30, { order: ["mergeArithmetic"] }); + expect(r.accepted).toBe(1); + expect(r.result.failure?.rule).toBe("L1"); + expect(r.result.scenario.sources).toEqual([-1]); +}); + +test("queue-at-end preserves payload order and unrelated callback/task work", () => { + const s = chain(); + s.turns = [ + { + steps: [ + { + op: "queue", + id: 0, + via: "microtask", + steps: [ + { op: "write", value: 1 }, + { op: "show", value: false } + ] + }, + { op: "show", value: true }, + { op: "queue", id: 1, via: "task", step: { op: "write", value: 2 } } + ] + }, + { task: 1 } + ]; + const before = JSON.stringify(s), + c = [...callbackEndReductions(s)][0]; + expect(c.turns[0]).toEqual({ + steps: [ + { op: "show", value: true }, + { op: "queue", id: 1, via: "task", step: { op: "write", value: 2 } }, + { op: "write", value: 1 }, + { op: "show", value: false } + ] + }); + expect(c.turns[1]).toEqual({ task: 1 }); + expect(validate(c)).toBeUndefined(); + expect(JSON.stringify(s)).toBe(before); + s.turns.push({ steps: [{ op: "cancel", id: 0 }] }); + expect([...callbackEndReductions(s)]).toHaveLength(0); +}); + +test("initial visibility absorbs only the first direct setter in a mixed callback", () => { + const s = chain(); + s.turns = [ + { + steps: [ + { op: "show", value: false }, + { op: "write", value: 1 }, + { op: "show", value: true } + ] + } + ]; + const before = JSON.stringify(s), + candidates = [...initialVisibilityReductions(s)]; + expect(candidates).toHaveLength(1); + expect(candidates[0].show).toBe(false); + expect(candidates[0].turns).toEqual([ + { + steps: [ + { op: "write", value: 1 }, + { op: "show", value: true } + ] + } + ]); + expect(validate(candidates[0])).toBeUndefined(); + expect(JSON.stringify(s)).toBe(before); + s.turns = [ + { steps: [{ op: "queue", id: 0, via: "microtask", step: { op: "show", value: false } }] } + ]; + expect([...initialVisibilityReductions(s)]).toHaveLength(0); +}); + +test("delivery/order candidates descend or remove a node without changing captured inputs", () => { + const s: Scenario = { + version: 2, + sources: [-1], + show: true, + nodes: [ + { id: 0, deps: [-1], factor: 1, offset: 0, delivery: "promise" }, + { id: 1, deps: [-1], factor: 1, offset: 0, delivery: "promise" }, + { id: 2, deps: [1], factor: 1, offset: 0, delivery: "sync" } + ], + readers: [{ id: 0, refs: [0, 2], gated: false, boundary: "none", pending: true }], + turns: [ + { + steps: [ + { op: "write", value: 1 }, + { op: "resolve", node: 1, which: "newest", key: "1:[1]#2" } + ] + } + ] + }; + const before = JSON.stringify(s), + candidates = [...deliveryOrderingReductions(s)]; + expect(candidates.some(c => c.nodes[0].delivery === "manual" && c.nodes[2].deps[0] === 0)).toBe( + true + ); + for (const c of candidates) { + expect(validate(c)).toBeUndefined(); + expect(c.nodes.length < s.nodes.length || scenarioKey(c) < scenarioKey(s)).toBe(true); + expect(JSON.stringify(c.turns)).toContain(":[1]#2"); + } + expect(JSON.stringify(s)).toBe(before); +}); + +test("ordering progress survives another family's higher-key delivery edit", () => { + const s = chain("sync"), + limit = new OrderingLimit(s); + const lower: Scenario = { ...s, nodes: [{ ...s.nodes[0], delivery: "promise" }, s.nodes[1]] }; + expect(limit.allows(lower)).toBe(true); + limit.accept(lower); + limit.accept(s); + expect(limit.allows(lower)).toBe(false); + expect(limit.allows(s)).toBe(false); + const smaller: Scenario = { + ...s, + nodes: [s.nodes[0]], + readers: [{ ...s.readers[0], refs: [0] }] + }; + expect(limit.allows(smaller)).toBe(true); + limit.accept(smaller); + expect(limit.allows({ ...smaller, nodes: [{ ...smaller.nodes[0], delivery: "promise" }] })).toBe( + true + ); +}); + +test("delivery/order exploration finishes with simpler delivery within the shared budget", async () => { + const s: Scenario = { + version: 2, + sources: [-1], + show: true, + nodes: [ + { id: 0, deps: [-1], factor: 1, offset: 0, delivery: "sync" }, + { id: 1, deps: [-1], factor: 1, offset: 0, delivery: "sync" } + ], + readers: [{ id: 0, refs: [0, 1], gated: false, boundary: "none" }], + turns: [] + }; + const baseline = await runScenario(s); + const failure = { rule: "S1", message: "synthetic two-reader-value failure" }; + let calls = 0; + const stats: NonNullable = {}; + const reduced = await shrink( + { ...baseline, status: "fail", failure }, + async scenario => { + calls++; + const failed = + scenario.nodes.length === 2 && + scenario.readers.length === 1 && + scenario.readers[0].refs.length === 2 && + scenario.readers[0].refs.every(ref => ref >= 0); + return { + ...baseline, + scenario, + status: failed ? "fail" : "pass", + failure: failed ? failure : undefined + }; + }, + 200, + { order: ["structural", "deliveryOrdering"], stats } + ); + expect(stats!.deliveryOrdering!.accepted).toBeGreaterThan(0); + expect(reduced.result.scenario.nodes.every(n => n.delivery === "sync")).toBe(true); + expect(reduced.attempts).toBe(calls); + expect(reduced.attempts).toBeLessThan(200); +}); diff --git a/packages/signals/tests/semantics/pending-contract.test.ts b/packages/signals/tests/semantics/pending-contract.test.ts new file mode 100644 index 000000000..99b801141 --- /dev/null +++ b/packages/signals/tests/semantics/pending-contract.test.ts @@ -0,0 +1,53 @@ +import { + createMemo, + createRenderEffect, + createRoot, + flush, + isPending, + NotReadyError +} from "../../src/index.js"; +import { HostTasks } from "./host.js"; + +// Accepted upstream contract: SPEC A16 (maintainer keep, 2026-07-06). +// #2928 / 70e89bafa7de4b6cbdb47aa83ac37b59244647d0 explicitly preserves the +// fully context-free boolean result. An untrack() call inside a surrounding +// reactive context can still propagate NotReadyError; "untracked" alone is +// not a sufficient scope predicate for a general readiness invariant. +test("A16 suspends a tracked first-load pending probe but returns false without reactive context", async () => { + const host = new HostTasks(); + let resolve!: (value: number) => void; + let dispose!: () => void; + let data!: () => number; + const published: boolean[] = []; + createRoot(d => { + dispose = d; + data = createMemo( + () => + new Promise(r => { + resolve = r; + }) + ); + createRenderEffect( + () => isPending(data), + value => { + published.push(value); + } + ); + }); + try { + flush(); + expect(published).toEqual([]); + expect(isPending(data)).toBe(false); + expect(data).toThrow(NotReadyError); + await host.run(() => resolve(1)); + await host.drain(); + expect(published).toEqual([false]); + expect(data()).toBe(1); + } finally { + dispose(); + resolve(1); + await host.drain(); + flush(); + host.close(); + } +}); diff --git a/packages/signals/tests/semantics/policy.ts b/packages/signals/tests/semantics/policy.ts new file mode 100644 index 000000000..f0a97db42 --- /dev/null +++ b/packages/signals/tests/semantics/policy.ts @@ -0,0 +1,75 @@ +import type { Failure, Frame } from "./rules.js"; + +export const waitingPolicies = ["review", "required-only", "retain-disposed"] as const; +export type WaitingPolicy = (typeof waitingPolicies)[number]; + +/** Evidence from the scenario's declared readers, never Solid's blocker sets. */ +export interface Observation { + reader: number; + write: number; + at: string; + disposedAt?: string; +} + +export interface WaitingFinding { + rule: "W1"; + message: string; + policy: WaitingPolicy; + disposition: "open" | "allowed" | "violation" | "out-of-scope"; + scope: "single-write-disposal" | "other"; + before: Frame; + afterRetained: Frame; + after: Frame; + pending: string[]; + retained: Array<{ key: string; observations: Observation[] }>; + releases: Array<{ key: string; retained: boolean; frame: Frame }>; + recovered: boolean; +} + +/** Permission applies only to a witnessed pending request, not its descendants. */ +export function retainedObservations(observations: Observation[], write: number): Observation[] { + return observations.filter(o => o.write === write && o.disposedAt !== undefined); +} + +export function waitingSignature(finding: WaitingFinding): string { + return [ + finding.rule, + finding.policy, + finding.disposition, + finding.scope, + finding.retained.length ? "observed-then-disposed" : "no-retained-witness", + finding.recovered ? "recovered" : "unfinished", + finding.releases.some(r => !r.retained) ? "other-work-released" : "retained-only" + ].join(": "); +} + +// Exceptions classify evidence after evaluating the ideal rule. They never +// disable a checker or waive final correctness. Default: no exceptions. +export const allowanceIds = ["legacy-disposal-wait"] as const; +export type Allowance = (typeof allowanceIds)[number]; +export const allowanceContracts = { + "legacy-disposal-wait": { + rule: "P1", + reference: "https://github.com/solidjs/solid/pull/3347#issuecomment-5626413374", + scope: + "one ordinary v1 write, unconditional readers, no boundaries; all remaining flights were observed then disposed for that write", + reason: + "Historical permission to finish after exact abandoned requests; optional compatibility, not the ideal contract" + } +}; +export interface ProgressFinding { + failure: Failure; + outstanding: Array<{ key: string; observations: Observation[] }>; + allowance?: Allowance; +} +export function progressAllowance( + enabled: readonly Allowance[], + narrowDisposal: boolean, + write: number, + outstanding: ProgressFinding["outstanding"] +): Allowance | undefined { + if (!narrowDisposal || !outstanding.length || !enabled.includes("legacy-disposal-wait")) return; + for (const request of outstanding) + if (!request.observations.some(o => o.write === write && o.disposedAt !== undefined)) return; + return "legacy-disposal-wait"; +} diff --git a/packages/signals/tests/semantics/priority-reduce.test.ts b/packages/signals/tests/semantics/priority-reduce.test.ts new file mode 100644 index 000000000..c46ae5559 --- /dev/null +++ b/packages/signals/tests/semantics/priority-reduce.test.ts @@ -0,0 +1,104 @@ +import { chain } from "./fixtures.js"; +import { arithmeticReductions, focusedReductions } from "./reduce.js"; +import { + reductions, + reductionFamilies, + reductionOrder, + shrink, + type ReductionStats, + type ReductionFamily +} from "./shrink.js"; +import { runScenario } from "./runner.js"; +import { evaluate, type Scenario } from "./scenario.js"; + +test("coordinated arithmetic preserves dependency order and exact request questions", () => { + const s = chain(); + s.nodes[0].deps = [-1, -1, -2, -2, -3]; + s.nodes[0].offset = 2; + s.turns = [ + { + steps: [ + { op: "write", value: 2 }, + { op: "resolve", node: 0, which: "newest", key: "0:[2,2,0,0,0]#2" } + ] + } + ]; + const before = structuredClone(s); + const c = [...arithmeticReductions(s)][0]; + expect(c.nodes[0].deps).toEqual([-1, -2, -3]); + expect(c.nodes[0].offset).toBe(1); + expect(c.turns[0]).toEqual({ + steps: [ + { op: "write", value: 1 }, + { op: "resolve", node: 0, which: "newest", key: "0:[2,2,0,0,0]#2" } + ] + }); + expect(s).toEqual(before); +}); + +test("moving a sync child's arithmetic upstream preserves its settled expression", () => { + const s = chain("promise"); + s.nodes[0].factor = 2; + s.nodes[0].offset = 3; + s.nodes[1].delivery = "sync"; + s.nodes[1].factor = 4; + s.nodes[1].offset = 5; + const candidates = [...arithmeticReductions(s)]; + const c = candidates.find(c => c.nodes[0].offset === 17)!; + expect(c).toBeDefined(); + for (const input of [0, 1, 7]) + expect(evaluate(c, input).get(s.nodes[1].id)).toBe(evaluate(s, input).get(s.nodes[1].id)); + expect(c.nodes[1].factor).toBe(1); + expect(c.nodes[1].offset).toBe(0); + const asyncChild = structuredClone(s); + asyncChild.nodes[1].delivery = "promise"; + expect([...arithmeticReductions(asyncChild)].some(c => c.nodes[0].offset === 17)).toBe(false); +}); + +test("initial hide reduction removes only a standalone initial hide", () => { + const s = chain(); + s.show = true; + s.turns = [{ steps: [{ op: "show", value: false }] }, { steps: [{ op: "write", value: 1 }] }]; + const c = [...focusedReductions(s)].find(c => !c.show)!; + expect(c.turns).toEqual(s.turns.slice(1)); + const mixed: Scenario = { + ...s, + turns: [ + { + steps: [ + { op: "write", value: 1 }, + { op: "show", value: false } + ] + } + ] + }; + expect([...focusedReductions(mixed)].some(c => !c.show)).toBe(false); +}); + +test("family order retains all candidates and stats account for actual replays", async () => { + expect(new Set(reductionOrder).size).toBe(Object.keys(reductionFamilies).length); + const s = chain("sync"); + const reverse = [...reductionOrder].reverse(); + const keys = (items: Iterable) => new Set([...items].map(s => JSON.stringify(s))); + expect(keys(reductions(s, reverse))).toEqual(keys(reductions(s))); + const base = await runScenario(s); + const original = { + ...base, + status: "fail" as const, + failure: { rule: "test", message: "profile" } + }; + const stats: Partial> = {}; + let calls = 0; + const r = await shrink( + original, + async scenario => { + calls++; + return { ...base, scenario }; + }, + 10, + { stats, order: reverse } + ); + expect(Object.values(stats).reduce((n, s) => n + s.attempts, 0)).toBe(calls); + expect(calls).toBe(r.attempts); + expect(Object.values(stats).every(s => s.accepted === 0)).toBe(true); +}); diff --git a/packages/signals/tests/semantics/progress.test.ts b/packages/signals/tests/semantics/progress.test.ts new file mode 100644 index 000000000..dbbf0e7f5 --- /dev/null +++ b/packages/signals/tests/semantics/progress.test.ts @@ -0,0 +1,99 @@ +import { Model } from "./model.js"; +import { chain, releasedFallback, disposedReader } from "./fixtures.js"; +import { checkProgress, progressScope } from "./progress.js"; +import { progressAllowance, type Observation } from "./policy.js"; +import { runScenario } from "./runner.js"; + +const witnessed: Observation[] = [{ reader: 0, write: 1, at: "request", disposedAt: "dispose" }]; + +test("historical disposal allowance is explicit and confined to exact witnessed work", () => { + const outstanding = [{ key: "first", observations: witnessed }]; + expect(progressAllowance([], true, 1, outstanding)).toBeUndefined(); + expect(progressAllowance(["legacy-disposal-wait"], true, 1, outstanding)).toBe( + "legacy-disposal-wait" + ); + expect(progressAllowance(["legacy-disposal-wait"], false, 1, outstanding)).toBeUndefined(); + expect(progressAllowance(["legacy-disposal-wait"], true, 2, outstanding)).toBeUndefined(); + expect(progressAllowance(["legacy-disposal-wait"], true, 1, [])).toBeUndefined(); + outstanding.push({ key: "new-descendant", observations: [] }); + expect(progressAllowance(["legacy-disposal-wait"], true, 1, outstanding)).toBeUndefined(); +}); + +test("published fallback releases source progress but a live outside reader can still hold it", () => { + const s = releasedFallback(); + const model = new Model(s); + const frame = { + at: "drain", + input: 0, + inputs: { [-1]: 1, [-2]: 0 }, + show: true, + outputs: { 0: "loading" as const } + }; + const req = [ + { + reader: 0, + node: 1, + inputs: [1, 1], + gate: "waiting" as const, + fallback: true, + basis: "desired" as const + } + ]; + expect(checkProgress(s, frame, { [-1]: 1, [-2]: 1 }, true, {}, req, model)?.rule).toBe("P1"); + s.readers.push({ id: 1, refs: [1], gated: false, boundary: "none" }); + req.push({ ...req[0], reader: 1 }); + expect( + checkProgress( + s, + { ...frame, outputs: { ...frame.outputs, 1: [0] } }, + { [-1]: 1, [-2]: 1 }, + true, + {}, + req, + new Model(s) + ) + ).toBeUndefined(); +}); + +test("unsettled observation controls are outside the sufficient progress proof", () => { + const s = chain(); + s.readers[0].gated = true; + const frame = { at: "drain", input: 0, show: false, outputs: { 0: "hidden" as const } }; + expect(checkProgress(s, frame, { [-1]: 1 }, true, {}, [], new Model(s))).toBeUndefined(); +}); + +test("progress scope does not guess action, branch, readiness or partial-anchor ownership", () => { + for (const s of [ + { ...chain(), optimistic: { proposals: [1], authoritative: 2 } }, + { ...chain(), anchors: [] }, + { + ...chain(), + readers: [{ id: 0, refs: [0], gated: false, boundary: "none" as const, pending: true }] + } + ]) + expect(progressScope(s, new Model(s))).toBe(false); +}); + +test.each(["disposal", "fallback"])( + "%s publishes ordinary sources before abandoned/covered requests finish", + async kind => { + const result = await runScenario(kind === "disposal" ? disposedReader() : releasedFallback()); + expect(result.status, JSON.stringify(result)).toBe("pass"); + expect(result.progress).toBeUndefined(); + const frame = result.frames.findLast(f => f.at === "turn 1"); + if (kind === "disposal") expect(frame?.input).toBe(1); + else { + expect(frame?.inputs).toEqual({ [-1]: 1, [-2]: 1 }); + expect(frame?.outputs[0]).toBe("loading"); + } + } +); + +test.each(["disposal", "fallback"])("%s preserves another reader's blocker", async kind => { + const s = kind === "disposal" ? disposedReader() : releasedFallback(); + s.readers.push({ id: 1, refs: [1], gated: false, boundary: "none" }); + const result = await runScenario(s); + expect(result.status, JSON.stringify(result)).toBe("pass"); + const frame = result.frames.findLast(f => f.at === "turn 1"); + expect(kind === "disposal" ? frame?.input : frame?.inputs?.[-2]).toBe(0); +}); diff --git a/packages/signals/tests/semantics/progress.ts b/packages/signals/tests/semantics/progress.ts new file mode 100644 index 000000000..61141c7f6 --- /dev/null +++ b/packages/signals/tests/semantics/progress.ts @@ -0,0 +1,56 @@ +import { Model } from "./model.js"; +import type { Scenario } from "./scenario.js"; +import type { Failure, Frame } from "./rules.js"; +import type { Requirement } from "./runner.js"; + +/** No reconstruction of transaction ownership: this sufficient condition is + * deliberately limited to ordinary flat data readers with complete anchors. + * Readiness, actions, branches and unsettled mounting need separate contracts. */ +export function progressScope(s: Scenario, model: Model): boolean { + return ( + !s.optimistic && + !s.actions && + !s.nodes.some(n => n.branch) && + !s.readers.some(r => r.pending || r.parent !== undefined || r.render) && + model.anchorMask === (1 << model.sources.length) - 1 + ); +} + +/** Called after a host drain, NOT inside an individual flush callback. Fallback + * work still belongs to the boundary, but does not hold these ordinary writes. + * Unknown observation topology or a live unresolved demand prevents this proof. */ +export function checkProgress( + s: Scenario, + frame: Frame, + desired: Record, + wantedShow: boolean, + wantedMounts: Record, + requirements: Requirement[], + model: Model +): Failure | undefined { + for (const reader of s.readers) { + const output = frame.outputs[reader.id]; + if (reader.mounted !== undefined && frame.mounts?.[reader.id] !== wantedMounts[reader.id]) + return; + if (output === "absent") continue; + if (reader.gated && (s.anchorShow === false || frame.show !== wantedShow)) return; + if (output === "hidden") { + if (!reader.gated || wantedShow) return; + continue; + } + if (output === "loading" && reader.boundary !== "none") continue; + if (!Array.isArray(output)) return; + } + for (const r of requirements) + if (r.gate !== "resolved" && Array.isArray(frame.outputs[r.reader])) return; + let changed = s.anchorShow !== false && frame.show !== wantedShow; + for (const id of model.sources) + if ((frame.inputs?.[id] ?? frame.input) !== desired[id]) changed = true; + if (!changed) return; + return { + rule: "P1", + message: "Ordinary writes remain unpublished after all visible blocking demands have ended", + frame, + expected: { inputs: { ...desired }, show: wantedShow } + }; +} diff --git a/packages/signals/tests/semantics/quality-experiments.mjs b/packages/signals/tests/semantics/quality-experiments.mjs new file mode 100644 index 000000000..f45e12c95 --- /dev/null +++ b/packages/signals/tests/semantics/quality-experiments.mjs @@ -0,0 +1,248 @@ +// Optional decision gates; never used by the campaign runner. +import { build } from "esbuild"; +import { readFile, writeFile, mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { parseArgs } from "node:util"; +import { createHash } from "node:crypto"; +const { values: args } = parseArgs({ + options: { + worker: { type: "string" }, + out: { type: "string" }, + experiment: { type: "string" }, + budget: { type: "string", default: "150" } + } +}); +if (!args.worker || !["native", "chunks", "selection"].includes(args.experiment)) + throw Error("Provide --worker FILE --experiment native|chunks|selection [--out FILE]"); +const budget = Number(args.budget); +if (!Number.isInteger(budget) || budget < 1 || budget > 10000) throw Error("Invalid budget"); +const dir = fileURLToPath(new URL(".", import.meta.url)); +const temp = await mkdtemp(join(tmpdir(), "solid-fuzz-experiment-")); +try { + await build({ + stdin: { + contents: `export {scenarioArbitrary,branchesArbitrary} from './generate.ts';export {Client} from './client.ts';export * from './shrink.ts';export * from './complexity.ts';export {validate} from './scenario.ts';export {CandidateQueue} from './selection.ts';export * as fc from 'fast-check';`, + resolveDir: dir + }, + outfile: join(temp, "lib.mjs"), + bundle: true, + platform: "node", + format: "esm", + logLevel: "silent" + }); + const lib = await import(pathToFileURL(join(temp, "lib.mjs"))); + const worker = resolve(args.worker), + client = new lib.Client(worker), + rows = []; + const checked = async s => { + const r = await client.run(s); + if (r.status === "error" || r.status === "limit") + throw Error(`Experiment interrupted: ${r.status}`); + return r; + }; + const verify = async r => { + if (!lib.isSemanticFailure(await checked(r.scenario))) + throw Error("Final witness did not replay"); + }; + try { + if (args.experiment === "native") { + for (const [cohort, arb] of [ + ["ordinary", lib.scenarioArbitrary], + ["branches", lib.branchesArbitrary] + ]) { + for (const seed of [91500, 91501, 91502, 91503, 91504]) { + let calls = 0, + first, + best, + firstCalls = 0, + invalid = 0, + unexpected; + + const details = await lib.fc.check( + lib.fc.asyncProperty(arb, async s => { + if (first && calls - firstCalls >= budget) throw new lib.fc.PreconditionFailure(true); + calls++; + let r; + try { + r = await checked(s); + } catch (error) { + unexpected = error; + throw error; + } + if (r.status === "invalid" || r.status === "inapplicable") { + invalid++; + return true; + } + if (!lib.isSemanticFailure(r)) return true; + if (!first) { + first = r; + firstCalls = calls; + } + if (!best || lib.compareComplexity(r.scenario, best.scenario) < 0) best = r; + return false; + }), + { seed, numRuns: 150 } + ); + if (unexpected) throw unexpected; + if (!first) { + rows.push({ cohort, seed, found: false, calls }); + continue; + } + const external = await lib.shrink(first, checked, budget); + const hybrid = await lib.shrink( + best, + checked, + Math.max(0, budget - (calls - firstCalls)) + ); + for (const r of [best, external.result, hybrid.result]) await verify(r); + rows.push({ + cohort, + seed, + first: lib.complexity(first.scenario), + native: lib.complexity(best.scenario), + external: lib.complexity(external.result.scenario), + hybrid: lib.complexity(hybrid.result.scenario), + nativeCalls: calls - firstCalls, + externalCalls: external.attempts, + hybridCalls: hybrid.attempts, + invalid, + interrupted: details.interrupted + }); + } + } + } else if (args.experiment === "selection") { + const manifest = JSON.parse( + await readFile(new URL("./quality/corpus.json", import.meta.url), "utf8") + ); + const first = [], + signatures = new Set(), + queue = new lib.CandidateQueue(); + for (let index = 0; index < manifest.cases.length; index++) { + const item = manifest.cases[index]; + if (item.comparison) continue; + const result = await checked(item.input); + if (!lib.isSemanticFailure(result)) continue; + const signature = lib.fingerprint(result); + const candidate = { index, signature, scenario: result.scenario, result, pair: [] }; + if (!signatures.has(signature) && first.length < queue.capacity) first.push(candidate); + signatures.add(signature); + queue.offer(candidate); + } + for (const [mode, selected] of [ + ["first", first], + ["queue", queue.entries] + ]) { + for (const c of selected) { + const reduced = await lib.shrink(c.result, checked, budget); + await verify(reduced.result); + rows.push({ + mode, + id: manifest.cases[c.index].id, + signature: c.signature, + raw: lib.complexity(c.scenario), + final: lib.complexity(reduced.result.scenario), + calls: reduced.attempts + }); + } + } + } else { + const manifest = JSON.parse( + await readFile(new URL("./quality/corpus.json", import.meta.url), "utf8") + ); + // Same ordered turn deletion units for both algorithms. CDD: Algorithm 2, + // https://arxiv.org/html/2408.04735v4, p0=.25; no complement queries. + for (const item of manifest.cases) { + if (item.comparison) continue; + const first = await checked(item.input); + if (!lib.isSemanticFailure(first)) continue; + const row = { id: item.id }; + for (const mode of ["halving", "cdd"]) { + let result = first, + calls = 0, + invalid = 0; + const seen = new Set(); + const remove = async (start, count) => { + const turns = result.scenario.turns.slice(); + turns.splice(start, count); + const candidate = { ...result.scenario, turns }; + if (lib.validate(candidate)) { + invalid++; + return false; + } + const key = JSON.stringify(candidate); + if (seen.has(key) || calls >= budget) return false; + seen.add(key); + calls++; + const next = await checked(candidate); + if (next.status === "invalid" || next.status === "inapplicable") invalid++; + if (!lib.isSemanticFailure(next)) return false; + result = next; + return true; + }; + if (mode === "halving") { + let changed = true; + while (changed && calls < budget) { + changed = false; + outer: for ( + let width = Math.max(1, Math.ceil(result.scenario.turns.length / 2)); + width; + width = Math.floor(width / 2) + ) { + for (let i = 0; i < result.scenario.turns.length && calls < budget; i += width) { + if (await remove(i, width)) { + changed = true; + break outer; + } + } + } + } + } else { + for (let round = 0; calls < budget; round++) { + const p = 0.25 * 1.582 ** round; + let width = 1, + gain = 1 - p; + for (let s = 2; s <= result.scenario.turns.length && p < 1; s++) { + const next = s * (1 - p) ** s; + if (next <= gain) break; + width = s; + gain = next; + } + const length = result.scenario.turns.length; + let removed = 0; + for (let i = 0; i < length && calls < budget; i += width) { + const count = Math.min(width, length - i); + if (await remove(i - removed, count)) removed += count; + } + if (width === 1) break; + } + } + const finish = await lib.shrink(result, checked, budget - calls); + await verify(finish.result); + row[mode] = { + chunkCalls: calls, + calls: calls + finish.attempts, + invalid, + complexity: lib.complexity(finish.result.scenario) + }; + } + rows.push(row); + } + } + } finally { + await client.close(); + } + const report = { + experiment: args.experiment, + budget, + workerHash: createHash("sha256") + .update(await readFile(worker)) + .digest("hex"), + rows + }; + if (args.out) await writeFile(resolve(args.out), JSON.stringify(report, null, 2) + "\n"); + console.log(JSON.stringify(report)); +} finally { + await rm(temp, { recursive: true, force: true }); +} diff --git a/packages/signals/tests/semantics/quality.mjs b/packages/signals/tests/semantics/quality.mjs new file mode 100644 index 000000000..e72e6ab3d --- /dev/null +++ b/packages/signals/tests/semantics/quality.mjs @@ -0,0 +1,199 @@ +// Run with --worker FILE; historical scores require the manifest's worker hash. +import { build } from "esbuild"; +import { readFile, writeFile, mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { resolve, join } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { parseArgs } from "node:util"; +import { createHash } from "node:crypto"; +const { values: args } = parseArgs({ + options: { + worker: { type: "string" }, + baseline: { type: "string" }, + out: { type: "string" }, + budget: { type: "string", default: "500" }, + inputs: { type: "string", default: "reduced" }, + disable: { type: "string" }, + schedule: { type: "string" }, + ids: { type: "string" }, + check: { type: "boolean" } + } +}); +if (!args.worker) + throw new Error("Provide --worker FILE: use a built semantic worker, not a source module"); +const budget = Number(args.budget); +if (!Number.isInteger(budget) || budget < 0 || budget > 10000) throw new Error("Invalid budget"); +if (!["original", "reduced"].includes(args.inputs)) + throw new Error("Choose --inputs original|reduced"); +if (args.schedule && !["restart", "sweep"].includes(args.schedule)) + throw new Error("Invalid schedule"); +const dir = fileURLToPath(new URL(".", import.meta.url)), + temp = await mkdtemp(join(tmpdir(), "solid-fuzz-quality-")); +try { + await build({ + stdin: { + contents: `export * from './shrink.ts';export * from './equivalence.ts';export {Client} from './client.ts';export {scenarioKey} from './normalize.ts';export * from './complexity.ts';`, + resolveDir: dir + }, + outfile: join(temp, "lib.mjs"), + bundle: true, + platform: "node", + format: "esm", + logLevel: "silent" + }); + const lib = await import(pathToFileURL(join(temp, "lib.mjs"))); + const manifest = JSON.parse( + await readFile(new URL("./quality/corpus.json", import.meta.url), "utf8") + ); + const worker = resolve(args.worker), + workerHash = createHash("sha256") + .update(await readFile(worker)) + .digest("hex"); + const configurations = []; + if (args.baseline) + configurations.push(["baseline", await import(pathToFileURL(resolve(args.baseline)))]); + configurations.push(["current", lib]); + const report = { + workerHash, + historical: workerHash === manifest.workerSha256, + runtime: workerHash === manifest.workerSha256 ? manifest.runtime : "custom target", + budget, + inputs: args.inputs, + runs: {} + }; + for (const [label, reducer] of configurations) { + const client = new lib.Client(worker); + let initialExecutions = 0, + searchExecutions = 0, + verificationExecutions = 0; + const rows = [], + keys = new Set(); + const order = reducer.reductionOrder.filter(f => !args.disable?.split(",").includes(f)); + if (args.disable?.split(",").some(f => !reducer.reductionOrder.includes(f))) + throw new Error("Unknown reduction family"); + const start = performance.now(); + try { + for (const item of manifest.cases) { + if (args.ids && !args.ids.split(",").includes(item.id)) continue; + const scenario = args.inputs === "original" ? item.input : item.reduced; + const stats = {}, + options = { order, stats, schedule: args.schedule }; + let result, + output, + signature, + single = !item.comparison, + exhausted, + counted, + details, + runtimeInvalid = 0; + const initial = async s => { + initialExecutions++; + return client.run(s); + }; + const run = async s => { + searchExecutions++; + const result = await client.run(s); + if (result.status === "invalid" || result.status === "inapplicable") runtimeInvalid++; + return result; + }; + if (item.comparison) { + const reduced = await reducer.shrinkEquivalence( + scenario, + run, + budget, + item.comparison, + options + ); + const initialCount = + reduced.initialExecutions ?? (item.comparison === "delivery" ? 3 : 2); + initialExecutions += initialCount; + searchExecutions -= initialCount; + ({ result, scenario: output, signature, single, exhausted } = reduced); + counted = reduced.attempts; + details = reduced; + } else { + const original = await initial(scenario); + if (!reducer.fingerprint(original)) { + rows.push({ id: item.id, status: original.status, skipped: true }); + continue; + } + const reduced = await reducer.shrink(original, run, budget, options); + result = reduced.result; + output = result.scenario; + signature = reducer.fingerprint(result); + exhausted = reduced.exhausted; + counted = reduced.attempts; + details = reduced; + } + const verify = async s => { + verificationExecutions++; + return client.run(s); + }; + const replay = single + ? await verify(output) + : await reducer.runEquivalence(output, verify, item.comparison); + if ( + (single ? reducer.fingerprint(replay) : replay.signature) !== signature || + (lib.isSemanticFailure(result) && !lib.isSemanticFailure(single ? replay : replay.result)) + ) + throw new Error(`${item.id} failed final replay`); + const key = `${single ? "single" : item.comparison}:${signature}\n${lib.scenarioKey(output)}`; + keys.add(key); + rows.push({ + id: item.id, + single, + signature, + scenario: output, + key, + complexity: lib.complexity(output), + counted, + exhausted, + candidates: details.candidates, + duplicates: details.duplicates, + staticInvalid: details.invalid, + runtimeInvalid, + stats + }); + } + } finally { + await client.close(); + } + const routes = []; + for (const route of manifest.routes) { + const row = rows.find(r => r.id === route.source); + if (!row || row.skipped) continue; + const target = manifest.cases.find(c => c.id === route.target).reduced; + routes.push({ + ...route, + exact: lib.scenarioKey(row.scenario) === lib.scenarioKey(target), + noMoreComplex: lib.compareComplexity(row.scenario, target) <= 0 + }); + } + report.runs[label] = { + initialExecutions, + searchExecutions, + verificationExecutions, + elapsedMs: performance.now() - start, + representatives: keys.size, + routes, + rows + }; + console.log(label, JSON.stringify({ ...report.runs[label], rows: undefined })); + } + if (args.out) await writeFile(resolve(args.out), JSON.stringify(report, null, 2)); + const expectedRoutes = manifest.routes.filter( + r => !args.ids || args.ids.split(",").includes(r.source) + ); + if ( + args.check && + (!report.historical || + !expectedRoutes.length || + report.runs.current.routes.length !== expectedRoutes.length || + report.runs.current.routes.some(r => !r.noMoreComplex)) + ) + throw new Error( + "Quality gate requires the pinned historical worker and all selected useful routes" + ); +} finally { + await rm(temp, { recursive: true, force: true }); +} diff --git a/packages/signals/tests/semantics/quality.test.ts b/packages/signals/tests/semantics/quality.test.ts new file mode 100644 index 000000000..046b9265a --- /dev/null +++ b/packages/signals/tests/semantics/quality.test.ts @@ -0,0 +1,48 @@ +import { readFileSync } from "node:fs"; +import { validate, type Scenario } from "./scenario.js"; +import { complexity, compareComplexity } from "./complexity.js"; +import { chain } from "./fixtures.js"; + +const manifest = JSON.parse( + readFileSync(new URL("./quality/corpus.json", import.meta.url), "utf8") +) as { + cases: { + id: string; + input: Scenario; + reduced: Scenario; + members: { batch: string; index: number }[]; + }[]; + routes: { source: string; target: string }[]; +}; + +test("quality fixtures retain all audited inputs and original memberships", () => { + const ids = new Set(), + memberships = new Set(); + for (const c of manifest.cases) { + expect(ids.has(c.id)).toBe(false); + ids.add(c.id); + expect(validate(c.input), c.id).toBeUndefined(); + expect(validate(c.reduced), c.id).toBeUndefined(); + for (const m of c.members) { + const key = `${m.batch}:${m.index}`; + expect(memberships.has(key), c.id).toBe(false); + memberships.add(key); + } + } + expect(ids.size).toBe(97); + expect(memberships.size).toBe(1720); + for (const r of manifest.routes) { + expect(ids.has(r.source)).toBe(true); + expect(ids.has(r.target)).toBe(true); + } +}); + +test("complexity prefers removing async machinery even with another witnessed input", () => { + const s = chain("manual"); + s.nodes[1].delivery = "manual"; + const c = structuredClone(s); + c.nodes[1].delivery = "sync"; + c.readers[0].refs.push(0); + expect(compareComplexity(c, s)).toBeLessThan(0); + expect(complexity(s)).toEqual(complexity(structuredClone(s))); +}); diff --git a/packages/signals/tests/semantics/quality/corpus.json b/packages/signals/tests/semantics/quality/corpus.json new file mode 100644 index 000000000..03a12aebc --- /dev/null +++ b/packages/signals/tests/semantics/quality/corpus.json @@ -0,0 +1 @@ +{"runtime":"9fa294fb58e6e0ff8003efdd6fcf9a5e6dba3ef3","workerSha256":"f688dcadb8319ed9db1a9f95bc0520b1d65121154f8810f0f9de5f10da1227ec","ruleRevision":17,"cases":[{"id":"C00","input":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"proposals":[1],"authoritative":0},"nodes":[],"readers":[{"id":0,"refs":[-2],"gated":false,"boundary":"none","pending":true}],"show":true,"turns":[{"steps":[{"op":"start-action"},{"op":"click","reader":0}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"proposals":[1],"authoritative":0},"nodes":[],"readers":[{"id":0,"refs":[-2],"gated":false,"boundary":"none","pending":true}],"show":true,"turns":[{"steps":[{"op":"start-action"},{"op":"click","reader":0}]}],"strict":true},"signature":"R2: A ready control read data inconsistent with published inputs","comparison":null,"members":[{"batch":"014-optimistic-readiness","index":1},{"batch":"014-optimistic-readiness","index":5},{"batch":"014-optimistic-readiness","index":11},{"batch":"014-optimistic-readiness","index":13},{"batch":"014-optimistic-readiness","index":17},{"batch":"014-optimistic-readiness","index":25},{"batch":"014-optimistic-readiness","index":39},{"batch":"014-optimistic-readiness","index":76},{"batch":"014-optimistic-readiness","index":78},{"batch":"014-optimistic-readiness","index":112},{"batch":"014-optimistic-readiness","index":122},{"batch":"014-optimistic-readiness","index":130},{"batch":"014-optimistic-readiness","index":134},{"batch":"014-optimistic-readiness","index":137},{"batch":"014-optimistic-readiness","index":158},{"batch":"014-optimistic-readiness","index":203},{"batch":"034-optimistic-readiness","index":66},{"batch":"034-optimistic-readiness","index":122},{"batch":"074-optimistic-readiness","index":20},{"batch":"074-optimistic-readiness","index":42},{"batch":"074-optimistic-readiness","index":78},{"batch":"074-optimistic-readiness","index":143},{"batch":"094-optimistic-readiness","index":3},{"batch":"094-optimistic-readiness","index":160},{"batch":"114-optimistic-readiness","index":185},{"batch":"114-optimistic-readiness","index":213},{"batch":"134-optimistic-readiness","index":11},{"batch":"134-optimistic-readiness","index":124},{"batch":"134-optimistic-readiness","index":247},{"batch":"154-optimistic-readiness","index":80},{"batch":"154-optimistic-readiness","index":115},{"batch":"174-optimistic-readiness","index":36},{"batch":"174-optimistic-readiness","index":118},{"batch":"174-optimistic-readiness","index":154},{"batch":"174-optimistic-readiness","index":181},{"batch":"194-optimistic-readiness","index":107},{"batch":"194-optimistic-readiness","index":156},{"batch":"194-optimistic-readiness","index":213},{"batch":"214-optimistic-readiness","index":57},{"batch":"214-optimistic-readiness","index":164},{"batch":"234-optimistic-readiness","index":101},{"batch":"254-optimistic-readiness","index":52},{"batch":"254-optimistic-readiness","index":181},{"batch":"274-optimistic-readiness","index":48},{"batch":"274-optimistic-readiness","index":107},{"batch":"294-optimistic-readiness","index":39},{"batch":"294-optimistic-readiness","index":101},{"batch":"294-optimistic-readiness","index":157},{"batch":"294-optimistic-readiness","index":221},{"batch":"314-optimistic-readiness","index":22},{"batch":"314-optimistic-readiness","index":79},{"batch":"314-optimistic-readiness","index":137},{"batch":"014-optimistic-readiness","index":161},{"batch":"034-optimistic-readiness","index":123},{"batch":"054-optimistic-readiness","index":36},{"batch":"054-optimistic-readiness","index":243},{"batch":"074-optimistic-readiness","index":21},{"batch":"074-optimistic-readiness","index":82},{"batch":"074-optimistic-readiness","index":148},{"batch":"074-optimistic-readiness","index":182},{"batch":"094-optimistic-readiness","index":22},{"batch":"094-optimistic-readiness","index":222},{"batch":"114-optimistic-readiness","index":17},{"batch":"114-optimistic-readiness","index":187},{"batch":"114-optimistic-readiness","index":218},{"batch":"134-optimistic-readiness","index":84},{"batch":"134-optimistic-readiness","index":128},{"batch":"154-optimistic-readiness","index":6},{"batch":"154-optimistic-readiness","index":51},{"batch":"154-optimistic-readiness","index":118},{"batch":"174-optimistic-readiness","index":123},{"batch":"174-optimistic-readiness","index":155},{"batch":"174-optimistic-readiness","index":183},{"batch":"174-optimistic-readiness","index":233},{"batch":"194-optimistic-readiness","index":26},{"batch":"194-optimistic-readiness","index":110},{"batch":"214-optimistic-readiness","index":128},{"batch":"214-optimistic-readiness","index":198},{"batch":"234-optimistic-readiness","index":173},{"batch":"254-optimistic-readiness","index":54},{"batch":"254-optimistic-readiness","index":120},{"batch":"254-optimistic-readiness","index":183},{"batch":"254-optimistic-readiness","index":241},{"batch":"274-optimistic-readiness","index":63},{"batch":"274-optimistic-readiness","index":108},{"batch":"274-optimistic-readiness","index":161},{"batch":"274-optimistic-readiness","index":192},{"batch":"294-optimistic-readiness","index":163},{"batch":"314-optimistic-readiness","index":149},{"batch":"314-optimistic-readiness","index":229},{"batch":"014-optimistic-readiness","index":214},{"batch":"034-optimistic-readiness","index":35},{"batch":"034-optimistic-readiness","index":87},{"batch":"034-optimistic-readiness","index":124},{"batch":"034-optimistic-readiness","index":216},{"batch":"074-optimistic-readiness","index":22},{"batch":"074-optimistic-readiness","index":50},{"batch":"074-optimistic-readiness","index":85},{"batch":"094-optimistic-readiness","index":32},{"batch":"094-optimistic-readiness","index":100},{"batch":"094-optimistic-readiness","index":173},{"batch":"094-optimistic-readiness","index":234},{"batch":"114-optimistic-readiness","index":20},{"batch":"114-optimistic-readiness","index":88},{"batch":"114-optimistic-readiness","index":144},{"batch":"114-optimistic-readiness","index":189},{"batch":"114-optimistic-readiness","index":228},{"batch":"134-optimistic-readiness","index":29},{"batch":"134-optimistic-readiness","index":163},{"batch":"134-optimistic-readiness","index":218},{"batch":"154-optimistic-readiness","index":13},{"batch":"154-optimistic-readiness","index":119},{"batch":"154-optimistic-readiness","index":193},{"batch":"174-optimistic-readiness","index":50},{"batch":"174-optimistic-readiness","index":127},{"batch":"174-optimistic-readiness","index":157},{"batch":"174-optimistic-readiness","index":237},{"batch":"194-optimistic-readiness","index":37},{"batch":"194-optimistic-readiness","index":116},{"batch":"194-optimistic-readiness","index":221},{"batch":"214-optimistic-readiness","index":76},{"batch":"214-optimistic-readiness","index":133},{"batch":"214-optimistic-readiness","index":204},{"batch":"234-optimistic-readiness","index":23},{"batch":"234-optimistic-readiness","index":130},{"batch":"234-optimistic-readiness","index":182},{"batch":"234-optimistic-readiness","index":245},{"batch":"254-optimistic-readiness","index":128},{"batch":"254-optimistic-readiness","index":196},{"batch":"274-optimistic-readiness","index":73},{"batch":"274-optimistic-readiness","index":109},{"batch":"274-optimistic-readiness","index":162},{"batch":"274-optimistic-readiness","index":205},{"batch":"294-optimistic-readiness","index":60},{"batch":"294-optimistic-readiness","index":115},{"batch":"294-optimistic-readiness","index":171},{"batch":"294-optimistic-readiness","index":227},{"batch":"314-optimistic-readiness","index":28},{"batch":"314-optimistic-readiness","index":95},{"batch":"314-optimistic-readiness","index":155},{"batch":"314-optimistic-readiness","index":238},{"batch":"014-optimistic-readiness","index":218},{"batch":"034-optimistic-readiness","index":88},{"batch":"054-optimistic-readiness","index":7},{"batch":"054-optimistic-readiness","index":47},{"batch":"054-optimistic-readiness","index":105},{"batch":"074-optimistic-readiness","index":24},{"batch":"074-optimistic-readiness","index":53},{"batch":"074-optimistic-readiness","index":115},{"batch":"074-optimistic-readiness","index":153},{"batch":"074-optimistic-readiness","index":201},{"batch":"094-optimistic-readiness","index":105},{"batch":"094-optimistic-readiness","index":179},{"batch":"114-optimistic-readiness","index":24},{"batch":"114-optimistic-readiness","index":191},{"batch":"114-optimistic-readiness","index":232},{"batch":"134-optimistic-readiness","index":99},{"batch":"134-optimistic-readiness","index":222},{"batch":"154-optimistic-readiness","index":14},{"batch":"154-optimistic-readiness","index":62},{"batch":"154-optimistic-readiness","index":95},{"batch":"154-optimistic-readiness","index":130},{"batch":"174-optimistic-readiness","index":63},{"batch":"174-optimistic-readiness","index":158},{"batch":"174-optimistic-readiness","index":201},{"batch":"174-optimistic-readiness","index":240},{"batch":"194-optimistic-readiness","index":56},{"batch":"194-optimistic-readiness","index":117},{"batch":"194-optimistic-readiness","index":181},{"batch":"214-optimistic-readiness","index":16},{"batch":"214-optimistic-readiness","index":80},{"batch":"214-optimistic-readiness","index":205},{"batch":"234-optimistic-readiness","index":140},{"batch":"234-optimistic-readiness","index":193},{"batch":"254-optimistic-readiness","index":68},{"batch":"254-optimistic-readiness","index":131},{"batch":"254-optimistic-readiness","index":210},{"batch":"274-optimistic-readiness","index":19},{"batch":"274-optimistic-readiness","index":74},{"batch":"274-optimistic-readiness","index":115},{"batch":"274-optimistic-readiness","index":164},{"batch":"274-optimistic-readiness","index":211},{"batch":"294-optimistic-readiness","index":7},{"batch":"294-optimistic-readiness","index":121},{"batch":"294-optimistic-readiness","index":173},{"batch":"294-optimistic-readiness","index":231},{"batch":"314-optimistic-readiness","index":32},{"batch":"314-optimistic-readiness","index":180},{"batch":"314-optimistic-readiness","index":244},{"batch":"014-optimistic-readiness","index":243},{"batch":"034-optimistic-readiness","index":44},{"batch":"034-optimistic-readiness","index":106},{"batch":"054-optimistic-readiness","index":13},{"batch":"054-optimistic-readiness","index":108},{"batch":"074-optimistic-readiness","index":35},{"batch":"074-optimistic-readiness","index":117},{"batch":"074-optimistic-readiness","index":154},{"batch":"074-optimistic-readiness","index":203},{"batch":"074-optimistic-readiness","index":234},{"batch":"094-optimistic-readiness","index":37},{"batch":"094-optimistic-readiness","index":79},{"batch":"094-optimistic-readiness","index":182},{"batch":"094-optimistic-readiness","index":246},{"batch":"114-optimistic-readiness","index":155},{"batch":"134-optimistic-readiness","index":45},{"batch":"154-optimistic-readiness","index":19},{"batch":"154-optimistic-readiness","index":140},{"batch":"154-optimistic-readiness","index":201},{"batch":"174-optimistic-readiness","index":89},{"batch":"174-optimistic-readiness","index":131},{"batch":"174-optimistic-readiness","index":210},{"batch":"174-optimistic-readiness","index":248},{"batch":"194-optimistic-readiness","index":70},{"batch":"194-optimistic-readiness","index":230},{"batch":"214-optimistic-readiness","index":18},{"batch":"214-optimistic-readiness","index":87},{"batch":"214-optimistic-readiness","index":135},{"batch":"214-optimistic-readiness","index":233},{"batch":"234-optimistic-readiness","index":62},{"batch":"234-optimistic-readiness","index":142},{"batch":"234-optimistic-readiness","index":197},{"batch":"254-optimistic-readiness","index":29},{"batch":"254-optimistic-readiness","index":132},{"batch":"254-optimistic-readiness","index":225},{"batch":"274-optimistic-readiness","index":116},{"batch":"274-optimistic-readiness","index":167},{"batch":"274-optimistic-readiness","index":225},{"batch":"294-optimistic-readiness","index":21},{"batch":"294-optimistic-readiness","index":176},{"batch":"294-optimistic-readiness","index":234},{"batch":"314-optimistic-readiness","index":119},{"batch":"314-optimistic-readiness","index":184},{"batch":"014-optimistic-readiness","index":196},{"batch":"034-optimistic-readiness","index":15},{"batch":"034-optimistic-readiness","index":49},{"batch":"034-optimistic-readiness","index":140},{"batch":"054-optimistic-readiness","index":56},{"batch":"054-optimistic-readiness","index":130},{"batch":"054-optimistic-readiness","index":201},{"batch":"074-optimistic-readiness","index":59},{"batch":"074-optimistic-readiness","index":242},{"batch":"094-optimistic-readiness","index":38},{"batch":"094-optimistic-readiness","index":80},{"batch":"094-optimistic-readiness","index":109},{"batch":"094-optimistic-readiness","index":185},{"batch":"114-optimistic-readiness","index":33},{"batch":"114-optimistic-readiness","index":116},{"batch":"114-optimistic-readiness","index":198},{"batch":"114-optimistic-readiness","index":249},{"batch":"134-optimistic-readiness","index":47},{"batch":"134-optimistic-readiness","index":177},{"batch":"134-optimistic-readiness","index":228},{"batch":"154-optimistic-readiness","index":69},{"batch":"154-optimistic-readiness","index":143},{"batch":"154-optimistic-readiness","index":209},{"batch":"174-optimistic-readiness","index":91},{"batch":"174-optimistic-readiness","index":169},{"batch":"174-optimistic-readiness","index":212},{"batch":"194-optimistic-readiness","index":74},{"batch":"194-optimistic-readiness","index":201},{"batch":"194-optimistic-readiness","index":234},{"batch":"214-optimistic-readiness","index":236},{"batch":"234-optimistic-readiness","index":2},{"batch":"254-optimistic-readiness","index":35},{"batch":"254-optimistic-readiness","index":234},{"batch":"274-optimistic-readiness","index":31},{"batch":"274-optimistic-readiness","index":80},{"batch":"274-optimistic-readiness","index":124},{"batch":"274-optimistic-readiness","index":235},{"batch":"294-optimistic-readiness","index":85},{"batch":"294-optimistic-readiness","index":178},{"batch":"294-optimistic-readiness","index":240},{"batch":"314-optimistic-readiness","index":5},{"batch":"314-optimistic-readiness","index":54},{"batch":"314-optimistic-readiness","index":125},{"batch":"314-optimistic-readiness","index":188},{"batch":"014-optimistic-readiness","index":198},{"batch":"034-optimistic-readiness","index":21},{"batch":"034-optimistic-readiness","index":109},{"batch":"034-optimistic-readiness","index":176},{"batch":"054-optimistic-readiness","index":16},{"batch":"054-optimistic-readiness","index":59},{"batch":"074-optimistic-readiness","index":38},{"batch":"074-optimistic-readiness","index":66},{"batch":"074-optimistic-readiness","index":136},{"batch":"074-optimistic-readiness","index":164},{"batch":"074-optimistic-readiness","index":207},{"batch":"074-optimistic-readiness","index":248},{"batch":"094-optimistic-readiness","index":41},{"batch":"094-optimistic-readiness","index":81},{"batch":"094-optimistic-readiness","index":191},{"batch":"114-optimistic-readiness","index":175},{"batch":"114-optimistic-readiness","index":205},{"batch":"134-optimistic-readiness","index":60},{"batch":"134-optimistic-readiness","index":113},{"batch":"134-optimistic-readiness","index":192},{"batch":"134-optimistic-readiness","index":239},{"batch":"154-optimistic-readiness","index":70},{"batch":"154-optimistic-readiness","index":112},{"batch":"154-optimistic-readiness","index":216},{"batch":"174-optimistic-readiness","index":2},{"batch":"174-optimistic-readiness","index":175},{"batch":"174-optimistic-readiness","index":215},{"batch":"214-optimistic-readiness","index":33},{"batch":"214-optimistic-readiness","index":237},{"batch":"234-optimistic-readiness","index":8},{"batch":"234-optimistic-readiness","index":90},{"batch":"234-optimistic-readiness","index":164},{"batch":"234-optimistic-readiness","index":227},{"batch":"254-optimistic-readiness","index":104},{"batch":"254-optimistic-readiness","index":236},{"batch":"274-optimistic-readiness","index":38},{"batch":"274-optimistic-readiness","index":179},{"batch":"294-optimistic-readiness","index":34},{"batch":"294-optimistic-readiness","index":243},{"batch":"314-optimistic-readiness","index":9},{"batch":"314-optimistic-readiness","index":70},{"batch":"314-optimistic-readiness","index":126},{"batch":"314-optimistic-readiness","index":195},{"batch":"014-optimistic-readiness","index":200},{"batch":"034-optimistic-readiness","index":22},{"batch":"034-optimistic-readiness","index":62},{"batch":"054-optimistic-readiness","index":24},{"batch":"054-optimistic-readiness","index":60},{"batch":"054-optimistic-readiness","index":229},{"batch":"074-optimistic-readiness","index":0},{"batch":"074-optimistic-readiness","index":39},{"batch":"074-optimistic-readiness","index":168},{"batch":"074-optimistic-readiness","index":249},{"batch":"094-optimistic-readiness","index":212},{"batch":"114-optimistic-readiness","index":184},{"batch":"114-optimistic-readiness","index":207},{"batch":"134-optimistic-readiness","index":64},{"batch":"134-optimistic-readiness","index":195},{"batch":"134-optimistic-readiness","index":242},{"batch":"154-optimistic-readiness","index":41},{"batch":"154-optimistic-readiness","index":75},{"batch":"154-optimistic-readiness","index":161},{"batch":"174-optimistic-readiness","index":114},{"batch":"174-optimistic-readiness","index":150},{"batch":"174-optimistic-readiness","index":179},{"batch":"194-optimistic-readiness","index":5},{"batch":"194-optimistic-readiness","index":102},{"batch":"194-optimistic-readiness","index":211},{"batch":"214-optimistic-readiness","index":44},{"batch":"214-optimistic-readiness","index":110},{"batch":"214-optimistic-readiness","index":163},{"batch":"234-optimistic-readiness","index":15},{"batch":"234-optimistic-readiness","index":99},{"batch":"234-optimistic-readiness","index":166},{"batch":"254-optimistic-readiness","index":107},{"batch":"274-optimistic-readiness","index":39},{"batch":"274-optimistic-readiness","index":138},{"batch":"294-optimistic-readiness","index":37},{"batch":"294-optimistic-readiness","index":154},{"batch":"294-optimistic-readiness","index":211},{"batch":"294-optimistic-readiness","index":246},{"batch":"314-optimistic-readiness","index":17},{"batch":"314-optimistic-readiness","index":75},{"batch":"054-optimistic-readiness","index":75},{"batch":"294-optimistic-readiness","index":111},{"batch":"094-optimistic-readiness","index":56},{"batch":"134-optimistic-readiness","index":67},{"batch":"254-optimistic-readiness","index":240},{"batch":"034-optimistic-readiness","index":26},{"batch":"234-optimistic-readiness","index":29},{"batch":"154-optimistic-readiness","index":22},{"batch":"214-optimistic-readiness","index":27},{"batch":"054-optimistic-readiness","index":81},{"batch":"094-optimistic-readiness","index":84},{"batch":"254-optimistic-readiness","index":180},{"batch":"094-optimistic-readiness","index":57},{"batch":"034-optimistic-readiness","index":36},{"batch":"094-optimistic-readiness","index":240},{"batch":"114-optimistic-readiness","index":113},{"batch":"114-optimistic-readiness","index":121},{"batch":"174-optimistic-readiness","index":0},{"batch":"234-optimistic-readiness","index":148},{"batch":"174-optimistic-readiness","index":110},{"batch":"234-optimistic-readiness","index":18}]},{"id":"C01","input":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"optimistic":{"proposals":[1],"authoritative":0},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action"}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"optimistic":{"proposals":[1],"authoritative":0},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action"}]}],"strict":true},"signature":"S4: Residual work changed a completed view without new input","comparison":null,"members":[{"batch":"004-optimistic","index":19},{"batch":"004-optimistic","index":26},{"batch":"004-optimistic","index":41},{"batch":"004-optimistic","index":90},{"batch":"004-optimistic","index":95},{"batch":"004-optimistic","index":109},{"batch":"004-optimistic","index":135},{"batch":"004-optimistic","index":152},{"batch":"004-optimistic","index":165},{"batch":"004-optimistic","index":174},{"batch":"004-optimistic","index":187},{"batch":"004-optimistic","index":199},{"batch":"004-optimistic","index":217},{"batch":"004-optimistic","index":218},{"batch":"004-optimistic","index":219},{"batch":"004-optimistic","index":228},{"batch":"004-optimistic","index":244},{"batch":"014-optimistic-readiness","index":42},{"batch":"014-optimistic-readiness","index":99},{"batch":"024-optimistic","index":51},{"batch":"024-optimistic","index":173},{"batch":"044-optimistic","index":39},{"batch":"044-optimistic","index":157},{"batch":"044-optimistic","index":237},{"batch":"054-optimistic-readiness","index":32},{"batch":"064-optimistic","index":62},{"batch":"064-optimistic","index":146},{"batch":"084-optimistic","index":1},{"batch":"084-optimistic","index":96},{"batch":"084-optimistic","index":195},{"batch":"104-optimistic","index":34},{"batch":"104-optimistic","index":136},{"batch":"114-optimistic-readiness","index":10},{"batch":"124-optimistic","index":5},{"batch":"124-optimistic","index":83},{"batch":"124-optimistic","index":142},{"batch":"124-optimistic","index":204},{"batch":"144-optimistic","index":81},{"batch":"164-optimistic","index":71},{"batch":"164-optimistic","index":160},{"batch":"184-optimistic","index":25},{"batch":"184-optimistic","index":148},{"batch":"184-optimistic","index":230},{"batch":"204-optimistic","index":101},{"batch":"204-optimistic","index":194},{"batch":"224-optimistic","index":29},{"batch":"224-optimistic","index":117},{"batch":"224-optimistic","index":211},{"batch":"244-optimistic","index":6},{"batch":"244-optimistic","index":152},{"batch":"244-optimistic","index":232},{"batch":"264-optimistic","index":94},{"batch":"264-optimistic","index":179},{"batch":"284-optimistic","index":62},{"batch":"284-optimistic","index":146},{"batch":"284-optimistic","index":201},{"batch":"304-optimistic","index":119},{"batch":"304-optimistic","index":237},{"batch":"014-optimistic-readiness","index":213},{"batch":"024-optimistic","index":78},{"batch":"024-optimistic","index":180},{"batch":"044-optimistic","index":41},{"batch":"044-optimistic","index":171},{"batch":"064-optimistic","index":65},{"batch":"064-optimistic","index":160},{"batch":"084-optimistic","index":14},{"batch":"084-optimistic","index":102},{"batch":"084-optimistic","index":215},{"batch":"104-optimistic","index":44},{"batch":"124-optimistic","index":23},{"batch":"124-optimistic","index":84},{"batch":"124-optimistic","index":143},{"batch":"124-optimistic","index":231},{"batch":"134-optimistic-readiness","index":17},{"batch":"144-optimistic","index":10},{"batch":"144-optimistic","index":101},{"batch":"144-optimistic","index":205},{"batch":"164-optimistic","index":92},{"batch":"164-optimistic","index":162},{"batch":"184-optimistic","index":168},{"batch":"184-optimistic","index":242},{"batch":"194-optimistic-readiness","index":163},{"batch":"204-optimistic","index":117},{"batch":"204-optimistic","index":200},{"batch":"214-optimistic-readiness","index":65},{"batch":"224-optimistic","index":39},{"batch":"224-optimistic","index":229},{"batch":"234-optimistic-readiness","index":22},{"batch":"244-optimistic","index":41},{"batch":"244-optimistic","index":154},{"batch":"264-optimistic","index":118},{"batch":"264-optimistic","index":205},{"batch":"284-optimistic","index":4},{"batch":"284-optimistic","index":90},{"batch":"284-optimistic","index":215},{"batch":"294-optimistic-readiness","index":222},{"batch":"304-optimistic","index":144},{"batch":"024-optimistic","index":89},{"batch":"024-optimistic","index":188},{"batch":"044-optimistic","index":45},{"batch":"044-optimistic","index":178},{"batch":"064-optimistic","index":83},{"batch":"064-optimistic","index":180},{"batch":"084-optimistic","index":26},{"batch":"084-optimistic","index":106},{"batch":"084-optimistic","index":222},{"batch":"104-optimistic","index":2},{"batch":"104-optimistic","index":65},{"batch":"104-optimistic","index":163},{"batch":"124-optimistic","index":24},{"batch":"124-optimistic","index":87},{"batch":"144-optimistic","index":12},{"batch":"144-optimistic","index":121},{"batch":"144-optimistic","index":207},{"batch":"164-optimistic","index":3},{"batch":"164-optimistic","index":102},{"batch":"164-optimistic","index":165},{"batch":"184-optimistic","index":102},{"batch":"184-optimistic","index":169},{"batch":"204-optimistic","index":122},{"batch":"204-optimistic","index":207},{"batch":"224-optimistic","index":41},{"batch":"224-optimistic","index":167},{"batch":"224-optimistic","index":238},{"batch":"244-optimistic","index":42},{"batch":"244-optimistic","index":169},{"batch":"264-optimistic","index":119},{"batch":"264-optimistic","index":209},{"batch":"284-optimistic","index":5},{"batch":"284-optimistic","index":110},{"batch":"284-optimistic","index":157},{"batch":"284-optimistic","index":236},{"batch":"304-optimistic","index":18},{"batch":"304-optimistic","index":150},{"batch":"024-optimistic","index":93},{"batch":"024-optimistic","index":213},{"batch":"044-optimistic","index":186},{"batch":"064-optimistic","index":93},{"batch":"064-optimistic","index":182},{"batch":"084-optimistic","index":64},{"batch":"084-optimistic","index":124},{"batch":"104-optimistic","index":6},{"batch":"104-optimistic","index":68},{"batch":"104-optimistic","index":166},{"batch":"124-optimistic","index":27},{"batch":"124-optimistic","index":101},{"batch":"124-optimistic","index":158},{"batch":"124-optimistic","index":242},{"batch":"134-optimistic-readiness","index":44},{"batch":"144-optimistic","index":21},{"batch":"144-optimistic","index":134},{"batch":"144-optimistic","index":228},{"batch":"164-optimistic","index":7},{"batch":"164-optimistic","index":105},{"batch":"164-optimistic","index":174},{"batch":"184-optimistic","index":0},{"batch":"184-optimistic","index":118},{"batch":"184-optimistic","index":184},{"batch":"204-optimistic","index":61},{"batch":"204-optimistic","index":124},{"batch":"204-optimistic","index":235},{"batch":"224-optimistic","index":50},{"batch":"224-optimistic","index":168},{"batch":"224-optimistic","index":246},{"batch":"244-optimistic","index":104},{"batch":"244-optimistic","index":182},{"batch":"264-optimistic","index":120},{"batch":"264-optimistic","index":212},{"batch":"284-optimistic","index":9},{"batch":"284-optimistic","index":122},{"batch":"284-optimistic","index":160},{"batch":"284-optimistic","index":248},{"batch":"304-optimistic","index":19},{"batch":"304-optimistic","index":158},{"batch":"024-optimistic","index":11},{"batch":"024-optimistic","index":95},{"batch":"024-optimistic","index":218},{"batch":"044-optimistic","index":66},{"batch":"044-optimistic","index":199},{"batch":"064-optimistic","index":24},{"batch":"064-optimistic","index":101},{"batch":"064-optimistic","index":183},{"batch":"074-optimistic-readiness","index":58},{"batch":"084-optimistic","index":77},{"batch":"084-optimistic","index":164},{"batch":"104-optimistic","index":7},{"batch":"104-optimistic","index":92},{"batch":"104-optimistic","index":177},{"batch":"124-optimistic","index":56},{"batch":"124-optimistic","index":109},{"batch":"124-optimistic","index":163},{"batch":"124-optimistic","index":243},{"batch":"144-optimistic","index":24},{"batch":"144-optimistic","index":139},{"batch":"144-optimistic","index":248},{"batch":"164-optimistic","index":188},{"batch":"184-optimistic","index":4},{"batch":"184-optimistic","index":124},{"batch":"184-optimistic","index":199},{"batch":"194-optimistic-readiness","index":198},{"batch":"204-optimistic","index":69},{"batch":"204-optimistic","index":134},{"batch":"224-optimistic","index":58},{"batch":"224-optimistic","index":173},{"batch":"244-optimistic","index":109},{"batch":"244-optimistic","index":191},{"batch":"264-optimistic","index":4},{"batch":"264-optimistic","index":125},{"batch":"264-optimistic","index":223},{"batch":"274-optimistic-readiness","index":77},{"batch":"284-optimistic","index":17},{"batch":"284-optimistic","index":131},{"batch":"284-optimistic","index":161},{"batch":"294-optimistic-readiness","index":73},{"batch":"294-optimistic-readiness","index":123},{"batch":"304-optimistic","index":46},{"batch":"304-optimistic","index":165},{"batch":"024-optimistic","index":26},{"batch":"024-optimistic","index":232},{"batch":"044-optimistic","index":83},{"batch":"044-optimistic","index":225},{"batch":"064-optimistic","index":32},{"batch":"064-optimistic","index":107},{"batch":"064-optimistic","index":212},{"batch":"074-optimistic-readiness","index":125},{"batch":"084-optimistic","index":89},{"batch":"084-optimistic","index":175},{"batch":"104-optimistic","index":11},{"batch":"104-optimistic","index":96},{"batch":"104-optimistic","index":186},{"batch":"114-optimistic-readiness","index":169},{"batch":"124-optimistic","index":61},{"batch":"124-optimistic","index":124},{"batch":"124-optimistic","index":165},{"batch":"144-optimistic","index":31},{"batch":"144-optimistic","index":144},{"batch":"164-optimistic","index":24},{"batch":"164-optimistic","index":120},{"batch":"164-optimistic","index":201},{"batch":"184-optimistic","index":10},{"batch":"184-optimistic","index":126},{"batch":"184-optimistic","index":202},{"batch":"204-optimistic","index":86},{"batch":"204-optimistic","index":149},{"batch":"224-optimistic","index":78},{"batch":"224-optimistic","index":187},{"batch":"234-optimistic-readiness","index":71},{"batch":"244-optimistic","index":115},{"batch":"244-optimistic","index":196},{"batch":"264-optimistic","index":33},{"batch":"264-optimistic","index":129},{"batch":"264-optimistic","index":238},{"batch":"274-optimistic-readiness","index":170},{"batch":"284-optimistic","index":21},{"batch":"284-optimistic","index":133},{"batch":"284-optimistic","index":165},{"batch":"304-optimistic","index":85},{"batch":"304-optimistic","index":181},{"batch":"024-optimistic","index":34},{"batch":"024-optimistic","index":236},{"batch":"044-optimistic","index":4},{"batch":"044-optimistic","index":94},{"batch":"044-optimistic","index":229},{"batch":"064-optimistic","index":54},{"batch":"064-optimistic","index":108},{"batch":"064-optimistic","index":217},{"batch":"084-optimistic","index":92},{"batch":"084-optimistic","index":181},{"batch":"104-optimistic","index":12},{"batch":"104-optimistic","index":98},{"batch":"104-optimistic","index":197},{"batch":"124-optimistic","index":68},{"batch":"124-optimistic","index":132},{"batch":"124-optimistic","index":197},{"batch":"144-optimistic","index":32},{"batch":"144-optimistic","index":151},{"batch":"154-optimistic-readiness","index":159},{"batch":"164-optimistic","index":34},{"batch":"164-optimistic","index":139},{"batch":"164-optimistic","index":206},{"batch":"184-optimistic","index":12},{"batch":"184-optimistic","index":128},{"batch":"184-optimistic","index":206},{"batch":"204-optimistic","index":90},{"batch":"204-optimistic","index":175},{"batch":"214-optimistic-readiness","index":150},{"batch":"224-optimistic","index":15},{"batch":"224-optimistic","index":81},{"batch":"224-optimistic","index":189},{"batch":"244-optimistic","index":2},{"batch":"244-optimistic","index":126},{"batch":"244-optimistic","index":209},{"batch":"254-optimistic-readiness","index":44},{"batch":"264-optimistic","index":36},{"batch":"264-optimistic","index":136},{"batch":"284-optimistic","index":24},{"batch":"284-optimistic","index":135},{"batch":"284-optimistic","index":188},{"batch":"304-optimistic","index":105},{"batch":"304-optimistic","index":214},{"batch":"024-optimistic","index":41},{"batch":"024-optimistic","index":168},{"batch":"044-optimistic","index":27},{"batch":"044-optimistic","index":132},{"batch":"044-optimistic","index":232},{"batch":"064-optimistic","index":61},{"batch":"064-optimistic","index":140},{"batch":"084-optimistic","index":95},{"batch":"084-optimistic","index":182},{"batch":"104-optimistic","index":29},{"batch":"104-optimistic","index":135},{"batch":"104-optimistic","index":205},{"batch":"124-optimistic","index":4},{"batch":"124-optimistic","index":76},{"batch":"124-optimistic","index":140},{"batch":"124-optimistic","index":202},{"batch":"144-optimistic","index":35},{"batch":"144-optimistic","index":191},{"batch":"164-optimistic","index":36},{"batch":"164-optimistic","index":151},{"batch":"164-optimistic","index":207},{"batch":"184-optimistic","index":17},{"batch":"184-optimistic","index":138},{"batch":"184-optimistic","index":209},{"batch":"204-optimistic","index":96},{"batch":"204-optimistic","index":182},{"batch":"224-optimistic","index":27},{"batch":"224-optimistic","index":97},{"batch":"224-optimistic","index":208},{"batch":"244-optimistic","index":145},{"batch":"264-optimistic","index":63},{"batch":"264-optimistic","index":146},{"batch":"284-optimistic","index":43},{"batch":"284-optimistic","index":137},{"batch":"284-optimistic","index":199},{"batch":"304-optimistic","index":118},{"batch":"304-optimistic","index":233}]},{"id":"C02","input":{"version":2,"sources":[-1],"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":true,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"show","value":false}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":true,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"show","value":false}]}],"strict":true},"signature":"P1: Ordinary writes remain unpublished after all visible blocking demands have ended","comparison":null,"members":[{"batch":"002-ordinary","index":37},{"batch":"002-ordinary","index":43},{"batch":"002-ordinary","index":192},{"batch":"005-reads","index":98},{"batch":"005-reads","index":166},{"batch":"011-boundaries","index":222},{"batch":"022-ordinary","index":94},{"batch":"031-boundaries","index":48},{"batch":"049-branches","index":241},{"batch":"051-boundaries","index":209},{"batch":"063-multi","index":164},{"batch":"082-ordinary","index":88},{"batch":"089-branches","index":172},{"batch":"102-ordinary","index":223},{"batch":"105-reads","index":37},{"batch":"122-ordinary","index":191},{"batch":"142-ordinary","index":74},{"batch":"143-multi","index":208},{"batch":"145-reads","index":76},{"batch":"163-multi","index":154},{"batch":"183-multi","index":11},{"batch":"191-boundaries","index":59},{"batch":"202-ordinary","index":155},{"batch":"203-multi","index":173},{"batch":"231-boundaries","index":42},{"batch":"242-ordinary","index":109},{"batch":"245-reads","index":195},{"batch":"251-boundaries","index":237},{"batch":"263-multi","index":50},{"batch":"283-multi","index":168},{"batch":"285-reads","index":245},{"batch":"303-multi","index":211},{"batch":"031-boundaries","index":68},{"batch":"042-ordinary","index":222},{"batch":"045-reads","index":47},{"batch":"062-ordinary","index":223},{"batch":"082-ordinary","index":94},{"batch":"103-multi","index":64},{"batch":"105-reads","index":134},{"batch":"123-multi","index":59},{"batch":"145-reads","index":110},{"batch":"162-ordinary","index":52},{"batch":"171-boundaries","index":190},{"batch":"183-multi","index":189},{"batch":"203-multi","index":201},{"batch":"222-ordinary","index":238},{"batch":"225-reads","index":195},{"batch":"231-boundaries","index":119},{"batch":"242-ordinary","index":213},{"batch":"249-branches","index":247},{"batch":"263-multi","index":89},{"batch":"265-reads","index":121},{"batch":"302-ordinary","index":225},{"batch":"303-multi","index":232},{"batch":"305-reads","index":35},{"batch":"023-multi","index":101},{"batch":"043-multi","index":45},{"batch":"045-reads","index":115},{"batch":"051-boundaries","index":1},{"batch":"062-ordinary","index":243},{"batch":"063-multi","index":195},{"batch":"082-ordinary","index":149},{"batch":"105-reads","index":144},{"batch":"123-multi","index":68},{"batch":"142-ordinary","index":200},{"batch":"145-reads","index":130},{"batch":"162-ordinary","index":177},{"batch":"165-reads","index":91},{"batch":"182-ordinary","index":2},{"batch":"202-ordinary","index":178},{"batch":"203-multi","index":211},{"batch":"205-reads","index":244},{"batch":"222-ordinary","index":10},{"batch":"223-multi","index":26},{"batch":"265-reads","index":124},{"batch":"302-ordinary","index":246},{"batch":"305-reads","index":49},{"batch":"023-multi","index":106},{"batch":"043-multi","index":70},{"batch":"045-reads","index":182},{"batch":"051-boundaries","index":27},{"batch":"091-boundaries","index":93},{"batch":"102-ordinary","index":23},{"batch":"123-multi","index":70},{"batch":"142-ordinary","index":231},{"batch":"165-reads","index":113},{"batch":"169-branches","index":203},{"batch":"182-ordinary","index":35},{"batch":"185-reads","index":71},{"batch":"191-boundaries","index":95},{"batch":"202-ordinary","index":185},{"batch":"222-ordinary","index":27},{"batch":"223-multi","index":33},{"batch":"262-ordinary","index":84},{"batch":"263-multi","index":182},{"batch":"282-ordinary","index":140},{"batch":"303-multi","index":23},{"batch":"305-reads","index":84},{"batch":"063-multi","index":33},{"batch":"082-ordinary","index":15},{"batch":"082-ordinary","index":184},{"batch":"102-ordinary","index":63},{"batch":"122-ordinary","index":12},{"batch":"123-multi","index":96},{"batch":"143-multi","index":62},{"batch":"163-multi","index":5},{"batch":"182-ordinary","index":129},{"batch":"185-reads","index":143},{"batch":"191-boundaries","index":208},{"batch":"202-ordinary","index":196},{"batch":"205-reads","index":11},{"batch":"211-boundaries","index":27},{"batch":"222-ordinary","index":96},{"batch":"223-multi","index":58},{"batch":"225-reads","index":4},{"batch":"243-multi","index":125},{"batch":"245-reads","index":111},{"batch":"262-ordinary","index":126},{"batch":"282-ordinary","index":147},{"batch":"303-multi","index":63},{"batch":"305-reads","index":121},{"batch":"309-branches","index":93},{"batch":"043-multi","index":213},{"batch":"045-reads","index":217},{"batch":"051-boundaries","index":103},{"batch":"062-ordinary","index":112},{"batch":"082-ordinary","index":66},{"batch":"082-ordinary","index":214},{"batch":"091-boundaries","index":229},{"batch":"102-ordinary","index":83},{"batch":"111-boundaries","index":68},{"batch":"122-ordinary","index":108},{"batch":"123-multi","index":138},{"batch":"163-multi","index":24},{"batch":"182-ordinary","index":133},{"batch":"191-boundaries","index":24},{"batch":"202-ordinary","index":220},{"batch":"211-boundaries","index":36},{"batch":"222-ordinary","index":138},{"batch":"223-multi","index":244},{"batch":"225-reads","index":27},{"batch":"243-multi","index":188},{"batch":"262-ordinary","index":158},{"batch":"282-ordinary","index":182},{"batch":"291-boundaries","index":237},{"batch":"303-multi","index":71},{"batch":"305-reads","index":127},{"batch":"022-ordinary","index":20},{"batch":"051-boundaries","index":149},{"batch":"062-ordinary","index":135},{"batch":"063-multi","index":72},{"batch":"083-multi","index":122},{"batch":"102-ordinary","index":181},{"batch":"122-ordinary","index":133},{"batch":"143-multi","index":118},{"batch":"145-reads","index":34},{"batch":"163-multi","index":44},{"batch":"182-ordinary","index":134},{"batch":"191-boundaries","index":41},{"batch":"202-ordinary","index":249},{"batch":"205-reads","index":108},{"batch":"211-boundaries","index":102},{"batch":"222-ordinary","index":177},{"batch":"242-ordinary","index":13},{"batch":"251-boundaries","index":205},{"batch":"262-ordinary","index":219},{"batch":"265-reads","index":40},{"batch":"303-multi","index":93},{"batch":"022-ordinary","index":85},{"batch":"025-reads","index":121},{"batch":"042-ordinary","index":70},{"batch":"049-branches","index":232},{"batch":"063-multi","index":161},{"batch":"082-ordinary","index":85},{"batch":"083-multi","index":186},{"batch":"085-reads","index":75},{"batch":"102-ordinary","index":220},{"batch":"122-ordinary","index":158},{"batch":"129-branches","index":130},{"batch":"143-multi","index":123},{"batch":"171-boundaries","index":125},{"batch":"182-ordinary","index":197},{"batch":"191-boundaries","index":55},{"batch":"202-ordinary","index":89},{"batch":"225-reads","index":55},{"batch":"242-ordinary","index":88},{"batch":"262-ordinary","index":247},{"batch":"285-reads","index":176},{"batch":"302-ordinary","index":178},{"batch":"303-multi","index":151},{"batch":"005-reads","index":77},{"batch":"025-reads","index":154},{"batch":"125-reads","index":227},{"batch":"265-reads","index":102},{"batch":"285-reads","index":131},{"batch":"185-reads","index":224},{"batch":"205-reads","index":154},{"batch":"222-ordinary","index":232},{"batch":"225-reads","index":116},{"batch":"085-reads","index":110},{"batch":"125-reads","index":236},{"batch":"165-reads","index":17},{"batch":"085-reads","index":236},{"batch":"185-reads","index":148},{"batch":"185-reads","index":57},{"batch":"245-reads","index":49},{"batch":"263-multi","index":158},{"batch":"085-reads","index":1},{"batch":"245-reads","index":57},{"batch":"045-reads","index":213},{"batch":"085-reads","index":52},{"batch":"085-reads","index":11},{"batch":"285-reads","index":98},{"batch":"063-multi","index":51},{"batch":"085-reads","index":19},{"batch":"145-reads","index":29},{"batch":"082-ordinary","index":73},{"batch":"283-multi","index":30},{"batch":"145-reads","index":60},{"batch":"245-reads","index":184},{"batch":"017-reads--latest-equivalence","index":164},{"batch":"097-reads--latest-equivalence","index":183},{"batch":"117-reads--latest-equivalence","index":86},{"batch":"137-reads--latest-equivalence","index":201},{"batch":"177-reads--latest-equivalence","index":9},{"batch":"197-reads--latest-equivalence","index":231},{"batch":"217-reads--latest-equivalence","index":15},{"batch":"257-reads--latest-equivalence","index":196},{"batch":"277-reads--latest-equivalence","index":114},{"batch":"057-reads--latest-equivalence","index":182},{"batch":"117-reads--latest-equivalence","index":94},{"batch":"137-reads--latest-equivalence","index":59},{"batch":"157-reads--latest-equivalence","index":30},{"batch":"197-reads--latest-equivalence","index":246},{"batch":"277-reads--latest-equivalence","index":215},{"batch":"317-reads--latest-equivalence","index":3},{"batch":"057-reads--latest-equivalence","index":184},{"batch":"137-reads--latest-equivalence","index":81},{"batch":"157-reads--latest-equivalence","index":107},{"batch":"217-reads--latest-equivalence","index":52},{"batch":"017-reads--latest-equivalence","index":22},{"batch":"077-reads--latest-equivalence","index":9},{"batch":"117-reads--latest-equivalence","index":166},{"batch":"137-reads--latest-equivalence","index":98},{"batch":"157-reads--latest-equivalence","index":115},{"batch":"197-reads--latest-equivalence","index":5},{"batch":"217-reads--latest-equivalence","index":102},{"batch":"317-reads--latest-equivalence","index":72},{"batch":"017-reads--latest-equivalence","index":46},{"batch":"117-reads--latest-equivalence","index":185},{"batch":"177-reads--latest-equivalence","index":228},{"batch":"057-reads--latest-equivalence","index":244},{"batch":"077-reads--latest-equivalence","index":110},{"batch":"137-reads--latest-equivalence","index":104},{"batch":"197-reads--latest-equivalence","index":84},{"batch":"237-reads--latest-equivalence","index":149},{"batch":"297-reads--latest-equivalence","index":18},{"batch":"017-reads--latest-equivalence","index":79},{"batch":"057-reads--latest-equivalence","index":108},{"batch":"157-reads--latest-equivalence","index":220},{"batch":"197-reads--latest-equivalence","index":228},{"batch":"257-reads--latest-equivalence","index":45},{"batch":"297-reads--latest-equivalence","index":71},{"batch":"317-reads--latest-equivalence","index":246},{"batch":"057-reads--latest-equivalence","index":167},{"batch":"057-reads--latest-equivalence","index":208},{"batch":"057-reads--latest-equivalence","index":34},{"batch":"157-reads--latest-equivalence","index":116},{"batch":"017-reads--latest-equivalence","index":129},{"batch":"137-reads--latest-equivalence","index":155},{"batch":"077-reads--latest-equivalence","index":227},{"batch":"177-reads--latest-equivalence","index":74},{"batch":"017-reads--latest-equivalence","index":211},{"batch":"177-reads--latest-equivalence","index":113},{"batch":"277-reads--latest-equivalence","index":239},{"batch":"057-reads--latest-equivalence","index":236},{"batch":"077-reads--latest-equivalence","index":63},{"batch":"197-reads--latest-equivalence","index":32},{"batch":"217-reads--latest-equivalence","index":144},{"batch":"317-reads--latest-equivalence","index":207},{"batch":"057-reads--latest-equivalence","index":42},{"batch":"217-reads--latest-equivalence","index":152},{"batch":"037-reads--latest-equivalence","index":67},{"batch":"157-reads--latest-equivalence","index":238},{"batch":"197-reads--latest-equivalence","index":230}]},{"id":"C03","input":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"optimistic":{"proposals":[1],"authoritative":0},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none","pending":true}],"show":true,"turns":[{"steps":[{"op":"start-action"}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"optimistic":{"proposals":[1],"authoritative":0},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action"}]}],"strict":true},"signature":"S4: Residual work changed a completed view without new input","comparison":null,"members":[{"batch":"014-optimistic-readiness","index":57},{"batch":"014-optimistic-readiness","index":62},{"batch":"014-optimistic-readiness","index":94},{"batch":"014-optimistic-readiness","index":117},{"batch":"014-optimistic-readiness","index":153},{"batch":"054-optimistic-readiness","index":157},{"batch":"074-optimistic-readiness","index":169},{"batch":"074-optimistic-readiness","index":219},{"batch":"094-optimistic-readiness","index":219},{"batch":"114-optimistic-readiness","index":59},{"batch":"114-optimistic-readiness","index":126},{"batch":"134-optimistic-readiness","index":203},{"batch":"154-optimistic-readiness","index":49},{"batch":"154-optimistic-readiness","index":170},{"batch":"154-optimistic-readiness","index":235},{"batch":"174-optimistic-readiness","index":227},{"batch":"194-optimistic-readiness","index":14},{"batch":"214-optimistic-readiness","index":3},{"batch":"214-optimistic-readiness","index":116},{"batch":"234-optimistic-readiness","index":171},{"batch":"234-optimistic-readiness","index":236},{"batch":"254-optimistic-readiness","index":110},{"batch":"274-optimistic-readiness","index":191},{"batch":"294-optimistic-readiness","index":248},{"batch":"034-optimistic-readiness","index":77},{"batch":"034-optimistic-readiness","index":210},{"batch":"054-optimistic-readiness","index":168},{"batch":"074-optimistic-readiness","index":220},{"batch":"094-optimistic-readiness","index":98},{"batch":"094-optimistic-readiness","index":165},{"batch":"114-optimistic-readiness","index":87},{"batch":"114-optimistic-readiness","index":132},{"batch":"134-optimistic-readiness","index":217},{"batch":"154-optimistic-readiness","index":92},{"batch":"154-optimistic-readiness","index":179},{"batch":"154-optimistic-readiness","index":237},{"batch":"174-optimistic-readiness","index":37},{"batch":"194-optimistic-readiness","index":220},{"batch":"214-optimistic-readiness","index":8},{"batch":"234-optimistic-readiness","index":102},{"batch":"234-optimistic-readiness","index":240},{"batch":"294-optimistic-readiness","index":42},{"batch":"314-optimistic-readiness","index":27},{"batch":"314-optimistic-readiness","index":88},{"batch":"014-optimistic-readiness","index":172},{"batch":"054-optimistic-readiness","index":37},{"batch":"054-optimistic-readiness","index":88},{"batch":"054-optimistic-readiness","index":171},{"batch":"074-optimistic-readiness","index":149},{"batch":"074-optimistic-readiness","index":226},{"batch":"094-optimistic-readiness","index":65},{"batch":"134-optimistic-readiness","index":91},{"batch":"154-optimistic-readiness","index":53},{"batch":"154-optimistic-readiness","index":94},{"batch":"174-optimistic-readiness","index":194},{"batch":"214-optimistic-readiness","index":11},{"batch":"254-optimistic-readiness","index":26},{"batch":"254-optimistic-readiness","index":65},{"batch":"274-optimistic-readiness","index":0},{"batch":"294-optimistic-readiness","index":0},{"batch":"014-optimistic-readiness","index":186},{"batch":"034-optimistic-readiness","index":128},{"batch":"054-optimistic-readiness","index":178},{"batch":"074-optimistic-readiness","index":228},{"batch":"094-optimistic-readiness","index":34},{"batch":"094-optimistic-readiness","index":74},{"batch":"114-optimistic-readiness","index":105},{"batch":"114-optimistic-readiness","index":150},{"batch":"134-optimistic-readiness","index":166},{"batch":"154-optimistic-readiness","index":197},{"batch":"174-optimistic-readiness","index":129},{"batch":"194-optimistic-readiness","index":225},{"batch":"214-optimistic-readiness","index":134},{"batch":"254-optimistic-readiness","index":27},{"batch":"294-optimistic-readiness","index":69},{"batch":"314-optimistic-readiness","index":109},{"batch":"034-optimistic-readiness","index":135},{"batch":"054-optimistic-readiness","index":53},{"batch":"054-optimistic-readiness","index":179},{"batch":"094-optimistic-readiness","index":108},{"batch":"114-optimistic-readiness","index":25},{"batch":"114-optimistic-readiness","index":197},{"batch":"114-optimistic-readiness","index":242},{"batch":"134-optimistic-readiness","index":102},{"batch":"134-optimistic-readiness","index":170},{"batch":"134-optimistic-readiness","index":227},{"batch":"174-optimistic-readiness","index":167},{"batch":"254-optimistic-readiness","index":70},{"batch":"274-optimistic-readiness","index":29},{"batch":"314-optimistic-readiness","index":33},{"batch":"034-optimistic-readiness","index":107},{"batch":"054-optimistic-readiness","index":15},{"batch":"074-optimistic-readiness","index":37},{"batch":"074-optimistic-readiness","index":161},{"batch":"074-optimistic-readiness","index":205},{"batch":"134-optimistic-readiness","index":109},{"batch":"154-optimistic-readiness","index":109},{"batch":"174-optimistic-readiness","index":1},{"batch":"194-optimistic-readiness","index":122},{"batch":"214-optimistic-readiness","index":93},{"batch":"214-optimistic-readiness","index":138},{"batch":"234-optimistic-readiness","index":222},{"batch":"254-optimistic-readiness","index":79},{"batch":"254-optimistic-readiness","index":155},{"batch":"294-optimistic-readiness","index":29},{"batch":"294-optimistic-readiness","index":124},{"batch":"034-optimistic-readiness","index":53},{"batch":"054-optimistic-readiness","index":151},{"batch":"054-optimistic-readiness","index":206},{"batch":"094-optimistic-readiness","index":113},{"batch":"114-optimistic-readiness","index":39},{"batch":"114-optimistic-readiness","index":120},{"batch":"154-optimistic-readiness","index":38},{"batch":"174-optimistic-readiness","index":139},{"batch":"194-optimistic-readiness","index":78},{"batch":"194-optimistic-readiness","index":129},{"batch":"194-optimistic-readiness","index":203},{"batch":"194-optimistic-readiness","index":249},{"batch":"214-optimistic-readiness","index":94},{"batch":"254-optimistic-readiness","index":176},{"batch":"274-optimistic-readiness","index":85},{"batch":"274-optimistic-readiness","index":136},{"batch":"294-optimistic-readiness","index":87},{"batch":"294-optimistic-readiness","index":135},{"batch":"294-optimistic-readiness","index":210},{"batch":"034-optimistic-readiness","index":110},{"batch":"034-optimistic-readiness","index":182},{"batch":"054-optimistic-readiness","index":156},{"batch":"074-optimistic-readiness","index":73},{"batch":"074-optimistic-readiness","index":215},{"batch":"094-optimistic-readiness","index":128},{"batch":"114-optimistic-readiness","index":56},{"batch":"134-optimistic-readiness","index":117},{"batch":"154-optimistic-readiness","index":229},{"batch":"174-optimistic-readiness","index":28},{"batch":"174-optimistic-readiness","index":216},{"batch":"234-optimistic-readiness","index":232},{"batch":"254-optimistic-readiness","index":46},{"batch":"254-optimistic-readiness","index":239},{"batch":"274-optimistic-readiness","index":97},{"batch":"274-optimistic-readiness","index":186},{"batch":"294-optimistic-readiness","index":90},{"batch":"314-optimistic-readiness","index":201},{"batch":"034-optimistic-readiness","index":25}]},{"id":"C04","input":{"version":2,"sources":[-1,-2],"anchorShow":false,"actions":[{"id":0,"segments":[[],[{"source":-1,"value":1}]]},{"id":1,"segments":[[{"source":-2,"value":1}],[]]}],"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"promise"}],"readers":[{"id":0,"refs":[-1,0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action","action":0}]},{"steps":[{"op":"start-action","action":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchorShow":false,"actions":[{"id":0,"segments":[[],[{"source":-1,"value":1}]]},{"id":1,"segments":[[{"source":-2,"value":1}],[]]}],"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"promise"}],"readers":[{"id":0,"refs":[-1,0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action","action":0}]},{"steps":[{"op":"start-action","action":1}]}],"strict":true},"signature":"G2: A ready update group remains unpublished: independent","comparison":null,"members":[{"batch":"021-update-groups","index":82},{"batch":"061-update-groups","index":104},{"batch":"081-update-groups","index":44},{"batch":"101-update-groups","index":222},{"batch":"241-update-groups","index":84},{"batch":"261-update-groups","index":44},{"batch":"021-update-groups","index":101},{"batch":"121-update-groups","index":184},{"batch":"181-update-groups","index":3},{"batch":"241-update-groups","index":109},{"batch":"261-update-groups","index":176},{"batch":"281-update-groups","index":129},{"batch":"041-update-groups","index":3},{"batch":"061-update-groups","index":224},{"batch":"101-update-groups","index":247},{"batch":"121-update-groups","index":210},{"batch":"241-update-groups","index":115},{"batch":"261-update-groups","index":203},{"batch":"281-update-groups","index":243},{"batch":"081-update-groups","index":180},{"batch":"121-update-groups","index":233},{"batch":"141-update-groups","index":83},{"batch":"021-update-groups","index":218},{"batch":"041-update-groups","index":91},{"batch":"121-update-groups","index":48},{"batch":"161-update-groups","index":39},{"batch":"201-update-groups","index":181},{"batch":"041-update-groups","index":154},{"batch":"101-update-groups","index":56},{"batch":"141-update-groups","index":111},{"batch":"181-update-groups","index":117},{"batch":"201-update-groups","index":182},{"batch":"221-update-groups","index":8},{"batch":"241-update-groups","index":181},{"batch":"301-update-groups","index":165},{"batch":"021-update-groups","index":7},{"batch":"121-update-groups","index":54},{"batch":"201-update-groups","index":188},{"batch":"241-update-groups","index":56},{"batch":"061-update-groups","index":95},{"batch":"101-update-groups","index":125},{"batch":"141-update-groups","index":195},{"batch":"221-update-groups","index":111},{"batch":"281-update-groups","index":28},{"batch":"281-update-groups","index":78},{"batch":"101-update-groups","index":239},{"batch":"081-update-groups","index":148},{"batch":"061-update-groups","index":237},{"batch":"021-update-groups","index":219},{"batch":"241-update-groups","index":35},{"batch":"061-update-groups","index":23},{"batch":"301-update-groups","index":92},{"batch":"061-update-groups","index":242},{"batch":"241-update-groups","index":177},{"batch":"061-update-groups","index":14},{"batch":"161-update-groups","index":105},{"batch":"101-update-groups","index":123},{"batch":"141-update-groups","index":190},{"batch":"161-update-groups","index":124},{"batch":"181-update-groups","index":129},{"batch":"221-update-groups","index":45},{"batch":"021-update-groups","index":80},{"batch":"161-update-groups","index":133},{"batch":"181-update-groups","index":149},{"batch":"261-update-groups","index":27}]},{"id":"C05","input":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"kind":"latest","proposals":[1],"authoritative":0,"viaMemo":false},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":true,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action"},{"op":"show","value":false}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"kind":"latest","proposals":[1],"authoritative":0,"viaMemo":false},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"promise"}],"readers":[{"id":0,"refs":[0],"gated":true,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action"},{"op":"show","value":false}]}],"strict":true},"signature":"S1: Published derivation disagrees with published input","comparison":null,"members":[{"batch":"007-latest","index":39},{"batch":"007-latest","index":133},{"batch":"007-latest","index":173},{"batch":"027-latest","index":216},{"batch":"067-latest","index":132},{"batch":"187-latest","index":242},{"batch":"267-latest","index":182},{"batch":"307-latest","index":13},{"batch":"307-latest","index":239},{"batch":"067-latest","index":134},{"batch":"167-latest","index":230},{"batch":"187-latest","index":42},{"batch":"287-latest","index":103},{"batch":"067-latest","index":143},{"batch":"187-latest","index":98},{"batch":"247-latest","index":14},{"batch":"267-latest","index":217},{"batch":"287-latest","index":155},{"batch":"307-latest","index":74},{"batch":"267-latest","index":225},{"batch":"027-latest","index":122},{"batch":"047-latest","index":232},{"batch":"087-latest","index":148},{"batch":"107-latest","index":84},{"batch":"147-latest","index":29},{"batch":"247-latest","index":184},{"batch":"267-latest","index":66},{"batch":"267-latest","index":228},{"batch":"127-latest","index":193},{"batch":"147-latest","index":36},{"batch":"227-latest","index":153},{"batch":"027-latest","index":189},{"batch":"147-latest","index":74},{"batch":"167-latest","index":106},{"batch":"187-latest","index":204},{"batch":"267-latest","index":177},{"batch":"287-latest","index":226},{"batch":"147-latest","index":84},{"batch":"227-latest","index":7},{"batch":"247-latest","index":138},{"batch":"107-latest","index":245},{"batch":"027-latest","index":198},{"batch":"167-latest","index":112},{"batch":"038-latest--latest-equivalence","index":148},{"batch":"058-latest--latest-equivalence","index":90},{"batch":"158-latest--latest-equivalence","index":69},{"batch":"198-latest--latest-equivalence","index":145},{"batch":"218-latest--latest-equivalence","index":95},{"batch":"238-latest--latest-equivalence","index":98},{"batch":"038-latest--latest-equivalence","index":165},{"batch":"118-latest--latest-equivalence","index":185},{"batch":"138-latest--latest-equivalence","index":196},{"batch":"198-latest--latest-equivalence","index":175},{"batch":"218-latest--latest-equivalence","index":100},{"batch":"238-latest--latest-equivalence","index":109},{"batch":"258-latest--latest-equivalence","index":29},{"batch":"298-latest--latest-equivalence","index":2},{"batch":"298-latest--latest-equivalence","index":237},{"batch":"058-latest--latest-equivalence","index":193},{"batch":"078-latest--latest-equivalence","index":1},{"batch":"138-latest--latest-equivalence","index":215},{"batch":"158-latest--latest-equivalence","index":217},{"batch":"198-latest--latest-equivalence","index":231},{"batch":"238-latest--latest-equivalence","index":145},{"batch":"278-latest--latest-equivalence","index":17},{"batch":"078-latest--latest-equivalence","index":108},{"batch":"198-latest--latest-equivalence","index":39},{"batch":"258-latest--latest-equivalence","index":175},{"batch":"298-latest--latest-equivalence","index":21},{"batch":"118-latest--latest-equivalence","index":15},{"batch":"278-latest--latest-equivalence","index":130},{"batch":"078-latest--latest-equivalence","index":187},{"batch":"118-latest--latest-equivalence","index":79},{"batch":"178-latest--latest-equivalence","index":160},{"batch":"198-latest--latest-equivalence","index":69},{"batch":"058-latest--latest-equivalence","index":87},{"batch":"138-latest--latest-equivalence","index":118},{"batch":"198-latest--latest-equivalence","index":135},{"batch":"118-latest--latest-equivalence","index":234},{"batch":"018-latest--latest-equivalence","index":201},{"batch":"138-latest--latest-equivalence","index":111},{"batch":"218-latest--latest-equivalence","index":246},{"batch":"138-latest--latest-equivalence","index":106}]},{"id":"C06","input":{"version":2,"sources":[-1,-2],"anchorShow":false,"actions":[{"id":0,"segments":[[{"source":-1,"value":1}],[]]},{"id":1,"segments":[[{"source":-2,"value":1}],[]]}],"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"}],"readers":[{"id":0,"refs":[-2,0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action","action":0}]},{"steps":[{"op":"start-action","action":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchorShow":false,"actions":[{"id":0,"segments":[[{"source":-1,"value":1}],[]]},{"id":1,"segments":[[{"source":-2,"value":1}],[]]}],"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"}],"readers":[{"id":0,"refs":[-2,0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action","action":0}]},{"steps":[{"op":"start-action","action":1}]}],"strict":true},"signature":"G2: A ready update group remains unpublished: independent","comparison":null,"members":[{"batch":"001-update-groups","index":3},{"batch":"121-update-groups","index":137},{"batch":"161-update-groups","index":139},{"batch":"181-update-groups","index":227},{"batch":"221-update-groups","index":139},{"batch":"061-update-groups","index":168},{"batch":"081-update-groups","index":101},{"batch":"181-update-groups","index":248},{"batch":"021-update-groups","index":184},{"batch":"181-update-groups","index":34},{"batch":"041-update-groups","index":17},{"batch":"181-update-groups","index":39},{"batch":"241-update-groups","index":159},{"batch":"141-update-groups","index":104},{"batch":"181-update-groups","index":88},{"batch":"301-update-groups","index":105},{"batch":"121-update-groups","index":50},{"batch":"041-update-groups","index":194},{"batch":"301-update-groups","index":236},{"batch":"121-update-groups","index":87},{"batch":"241-update-groups","index":78},{"batch":"001-update-groups","index":142},{"batch":"221-update-groups","index":201},{"batch":"101-update-groups","index":4}]},{"id":"C07","input":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[-1],"gated":true,"boundary":"retain"},{"id":1,"refs":[0],"gated":false,"boundary":"none"}],"show":false,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"show","value":true}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[-1],"gated":true,"boundary":"retain"},{"id":1,"refs":[0],"gated":false,"boundary":"none"}],"show":false,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"show","value":true}]}],"strict":true},"signature":"S1: Published derivation disagrees with published input","comparison":null,"members":[{"batch":"003-multi","index":57},{"batch":"005-reads","index":239},{"batch":"042-ordinary","index":103},{"batch":"023-multi","index":69},{"batch":"063-multi","index":176},{"batch":"142-ordinary","index":176},{"batch":"025-reads","index":231},{"batch":"183-multi","index":203},{"batch":"251-boundaries","index":140},{"batch":"135-branch-boundaries","index":186},{"batch":"245-reads","index":118},{"batch":"225-reads","index":40},{"batch":"305-reads","index":155},{"batch":"205-reads","index":119},{"batch":"011-boundaries","index":92},{"batch":"302-ordinary","index":182},{"batch":"131-boundaries","index":227},{"batch":"231-boundaries","index":192},{"batch":"291-boundaries","index":147},{"batch":"251-boundaries","index":169},{"batch":"111-boundaries","index":85},{"batch":"271-boundaries","index":141},{"batch":"077-reads--latest-equivalence","index":168},{"batch":"217-reads--latest-equivalence","index":46},{"batch":"317-reads--latest-equivalence","index":57},{"batch":"157-reads--latest-equivalence","index":192},{"batch":"137-reads--latest-equivalence","index":154},{"batch":"097-reads--latest-equivalence","index":142},{"batch":"277-reads--latest-equivalence","index":73},{"batch":"055-branch-boundaries","index":134},{"batch":"125-reads","index":54},{"batch":"171-boundaries","index":46},{"batch":"125-reads","index":197},{"batch":"271-boundaries","index":237},{"batch":"057-reads--latest-equivalence","index":21},{"batch":"077-reads--latest-equivalence","index":140},{"batch":"057-reads--latest-equivalence","index":140}]},{"id":"C08","input":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":true,"boundary":"reset"}],"show":true,"turns":[{"steps":[{"op":"show","value":false},{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":true,"boundary":"reset"}],"show":true,"turns":[{"steps":[{"op":"show","value":false},{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"W1: review: open: other: no-retained-witness: recovered: other-work-released","comparison":null,"members":[{"batch":"011-boundaries","index":33},{"batch":"242-ordinary","index":244},{"batch":"091-boundaries","index":116},{"batch":"143-multi","index":94},{"batch":"151-boundaries","index":80},{"batch":"185-reads","index":201},{"batch":"285-reads","index":161},{"batch":"062-ordinary","index":146},{"batch":"025-reads","index":192},{"batch":"215-branch-boundaries","index":51},{"batch":"055-branch-boundaries","index":199},{"batch":"291-boundaries","index":103},{"batch":"051-boundaries","index":159},{"batch":"211-boundaries","index":21},{"batch":"035-branch-boundaries","index":68},{"batch":"043-multi","index":123},{"batch":"125-reads","index":12},{"batch":"205-reads","index":75},{"batch":"245-reads","index":141},{"batch":"151-boundaries","index":240},{"batch":"017-reads--latest-equivalence","index":198},{"batch":"137-reads--latest-equivalence","index":207},{"batch":"237-reads--latest-equivalence","index":193},{"batch":"117-reads--latest-equivalence","index":17},{"batch":"237-reads--latest-equivalence","index":210},{"batch":"317-reads--latest-equivalence","index":247},{"batch":"171-boundaries","index":147},{"batch":"191-boundaries","index":82},{"batch":"245-reads","index":19},{"batch":"131-boundaries","index":78},{"batch":"162-ordinary","index":234},{"batch":"155-branch-boundaries","index":127},{"batch":"251-boundaries","index":148},{"batch":"283-multi","index":128},{"batch":"315-branch-boundaries","index":116},{"batch":"257-reads--latest-equivalence","index":84},{"batch":"135-branch-boundaries","index":102},{"batch":"291-boundaries","index":213},{"batch":"251-boundaries","index":230},{"batch":"062-ordinary","index":170},{"batch":"137-reads--latest-equivalence","index":218},{"batch":"005-reads","index":198},{"batch":"311-boundaries","index":53},{"batch":"202-ordinary","index":157},{"batch":"035-branch-boundaries","index":35},{"batch":"071-boundaries","index":154},{"batch":"291-boundaries","index":239},{"batch":"222-ordinary","index":184},{"batch":"305-reads","index":208}]},{"id":"C09","input":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"},{"id":1,"refs":[1],"gated":false,"boundary":"none","pending":true,"pendingDepth":1}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"},{"id":1,"refs":[1],"gated":false,"boundary":"none","pending":true,"pendingDepth":1}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"R4: Tracked verdict is false while a distinct ordinary answer is held","comparison":null,"members":[{"batch":"273-derived-readiness","index":24},{"batch":"053-derived-readiness","index":134},{"batch":"293-derived-readiness","index":207},{"batch":"053-derived-readiness","index":165},{"batch":"313-derived-readiness","index":114},{"batch":"033-derived-readiness","index":166},{"batch":"193-derived-readiness","index":129},{"batch":"073-derived-readiness","index":140},{"batch":"093-derived-readiness","index":96},{"batch":"193-derived-readiness","index":157},{"batch":"113-derived-readiness","index":53},{"batch":"033-derived-readiness","index":114},{"batch":"233-derived-readiness","index":208},{"batch":"213-derived-readiness","index":101},{"batch":"133-derived-readiness","index":217},{"batch":"273-derived-readiness","index":137}]},{"id":"C10","input":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"kind":"latest","proposals":[0],"authoritative":1,"viaMemo":false},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[-2],"gated":true,"boundary":"none"},{"id":1,"refs":[0],"gated":false,"boundary":"none"}],"show":false,"turns":[{"steps":[{"op":"start-action"},{"op":"resume-action"}]},{"steps":[{"op":"show","value":true}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"kind":"latest","proposals":[1],"authoritative":0,"viaMemo":false},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[-2],"gated":true,"boundary":"none"},{"id":1,"refs":[0],"gated":false,"boundary":"none"}],"show":false,"turns":[{"steps":[{"op":"start-action"}]},{"steps":[{"op":"show","value":true}]}],"strict":true},"signature":"S1: Published derivation disagrees with published input","comparison":null,"members":[{"batch":"007-latest","index":21},{"batch":"187-latest","index":15},{"batch":"227-latest","index":136},{"batch":"307-latest","index":128},{"batch":"047-latest","index":112},{"batch":"047-latest","index":169},{"batch":"167-latest","index":160},{"batch":"307-latest","index":16},{"batch":"127-latest","index":102},{"batch":"207-latest","index":50},{"batch":"307-latest","index":106},{"batch":"167-latest","index":74},{"batch":"247-latest","index":193},{"batch":"307-latest","index":220},{"batch":"058-latest--latest-equivalence","index":172},{"batch":"098-latest--latest-equivalence","index":192},{"batch":"158-latest--latest-equivalence","index":235},{"batch":"218-latest--latest-equivalence","index":243},{"batch":"218-latest--latest-equivalence","index":64},{"batch":"098-latest--latest-equivalence","index":54},{"batch":"078-latest--latest-equivalence","index":30},{"batch":"298-latest--latest-equivalence","index":12},{"batch":"278-latest--latest-equivalence","index":49},{"batch":"078-latest--latest-equivalence","index":112},{"batch":"198-latest--latest-equivalence","index":46},{"batch":"078-latest--latest-equivalence","index":248}]},{"id":"C11","input":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":true,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"show","value":false}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":true,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"show","value":false}]}],"strict":true},"signature":"W1: review: open: other: no-retained-witness: recovered: other-work-released","comparison":null,"members":[{"batch":"009-branches","index":15},{"batch":"269-branches","index":94},{"batch":"209-branches","index":170},{"batch":"255-branch-boundaries","index":191},{"batch":"269-branches","index":144},{"batch":"309-branches","index":73},{"batch":"069-branches","index":110},{"batch":"289-branches","index":138},{"batch":"069-branches","index":141},{"batch":"289-branches","index":190},{"batch":"089-branches","index":100},{"batch":"095-branch-boundaries","index":240},{"batch":"249-branches","index":183},{"batch":"055-branch-boundaries","index":169},{"batch":"129-branches","index":248},{"batch":"255-branch-boundaries","index":231},{"batch":"109-branches","index":56},{"batch":"188-observation","index":160}]},{"id":"C12","input":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"optimistic":{"proposals":[1,2],"authoritative":0},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action"}]},{"steps":[{"op":"resolve","node":0,"which":"newest","key":"0:[1]#1"}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"optimistic":{"proposals":[1],"authoritative":0},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action"}]}],"strict":true},"signature":"S4: Residual work changed a completed view without new input","comparison":null,"members":[{"batch":"144-optimistic","index":198},{"batch":"104-optimistic","index":158},{"batch":"224-optimistic","index":133},{"batch":"124-optimistic","index":240},{"batch":"044-optimistic","index":48},{"batch":"064-optimistic","index":3},{"batch":"164-optimistic","index":16},{"batch":"164-optimistic","index":107},{"batch":"024-optimistic","index":134},{"batch":"024-optimistic","index":145},{"batch":"244-optimistic","index":5},{"batch":"164-optimistic","index":208},{"batch":"284-optimistic","index":153},{"batch":"184-optimistic","index":26},{"batch":"124-optimistic","index":0}]},{"id":"C13","input":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"kind":"latest","proposals":[1],"authoritative":0,"viaMemo":false},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[-2],"gated":true,"boundary":"none"},{"id":1,"refs":[0],"gated":false,"boundary":"none"}],"show":false,"turns":[{"steps":[{"op":"start-action"}]},{"steps":[{"op":"show","value":true}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"kind":"latest","proposals":[1],"authoritative":0,"viaMemo":false},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[-2],"gated":true,"boundary":"none"},{"id":1,"refs":[0],"gated":false,"boundary":"none"}],"show":false,"turns":[{"steps":[{"op":"start-action"}]},{"steps":[{"op":"show","value":true}]}],"strict":true},"signature":"S1: Published derivation disagrees with published input","comparison":null,"members":[{"batch":"178-latest--latest-equivalence","index":229},{"batch":"158-latest--latest-equivalence","index":140},{"batch":"198-latest--latest-equivalence","index":7},{"batch":"218-latest--latest-equivalence","index":184},{"batch":"258-latest--latest-equivalence","index":37},{"batch":"058-latest--latest-equivalence","index":232},{"batch":"138-latest--latest-equivalence","index":14},{"batch":"258-latest--latest-equivalence","index":150},{"batch":"258-latest--latest-equivalence","index":247},{"batch":"278-latest--latest-equivalence","index":227},{"batch":"298-latest--latest-equivalence","index":146},{"batch":"298-latest--latest-equivalence","index":203},{"batch":"018-latest--latest-equivalence","index":70},{"batch":"007-latest","index":17},{"batch":"007-latest","index":124},{"batch":"047-latest","index":194},{"batch":"267-latest","index":19},{"batch":"187-latest","index":154},{"batch":"207-latest","index":84},{"batch":"227-latest","index":148},{"batch":"207-latest","index":175},{"batch":"267-latest","index":108},{"batch":"098-latest--latest-equivalence","index":73},{"batch":"127-latest","index":71},{"batch":"307-latest","index":216}]},{"id":"C14","input":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"optimistic":{"kind":"latest","proposals":[1],"authoritative":0,"viaMemo":false},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[-2],"gated":false,"boundary":"none"},{"id":1,"refs":[0],"gated":true,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action"},{"op":"show","value":false}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"kind":"latest","proposals":[1],"authoritative":0,"viaMemo":false},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"promise"}],"readers":[{"id":0,"refs":[0],"gated":true,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action"},{"op":"show","value":false}]}],"strict":true},"signature":"S1: Published derivation disagrees with published input","comparison":null,"members":[{"batch":"007-latest","index":9},{"batch":"267-latest","index":213},{"batch":"087-latest","index":115},{"batch":"287-latest","index":165},{"batch":"167-latest","index":10},{"batch":"287-latest","index":213},{"batch":"067-latest","index":14},{"batch":"107-latest","index":95},{"batch":"307-latest","index":175},{"batch":"047-latest","index":117},{"batch":"118-latest--latest-equivalence","index":141},{"batch":"298-latest--latest-equivalence","index":227},{"batch":"298-latest--latest-equivalence","index":3},{"batch":"198-latest--latest-equivalence","index":32},{"batch":"018-latest--latest-equivalence","index":94},{"batch":"138-latest--latest-equivalence","index":60},{"batch":"058-latest--latest-equivalence","index":66},{"batch":"138-latest--latest-equivalence","index":188},{"batch":"187-latest","index":179},{"batch":"178-latest--latest-equivalence","index":219}]},{"id":"C15","input":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"sync","branch":{"condition":-1,"otherwise":[0]}}],"readers":[{"id":0,"refs":[1],"gated":false,"boundary":"reset"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"sync","branch":{"condition":-1,"otherwise":[0]}}],"readers":[{"id":0,"refs":[1],"gated":false,"boundary":"reset"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"W1: review: open: other: no-retained-witness: recovered: other-work-released","comparison":null,"members":[{"batch":"055-branch-boundaries","index":211},{"batch":"195-branch-boundaries","index":233},{"batch":"135-branch-boundaries","index":46},{"batch":"315-branch-boundaries","index":30},{"batch":"015-branch-boundaries","index":7},{"batch":"175-branch-boundaries","index":96},{"batch":"115-branch-boundaries","index":159},{"batch":"315-branch-boundaries","index":101},{"batch":"155-branch-boundaries","index":174},{"batch":"195-branch-boundaries","index":2}]},{"id":"C16","input":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[0],"factor":1,"offset":0,"delivery":"promise"},{"id":2,"deps":[-1],"factor":1,"offset":0,"delivery":"promise","branch":{"condition":-1,"otherwise":[1]}}],"readers":[{"id":0,"refs":[2],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[0],"factor":1,"offset":0,"delivery":"promise"},{"id":2,"deps":[-1],"factor":1,"offset":0,"delivery":"promise","branch":{"condition":-1,"otherwise":[1]}}],"readers":[{"id":0,"refs":[2],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"L1: Required reader did not publish its final answer","comparison":null,"members":[{"batch":"055-branch-boundaries","index":195},{"batch":"155-branch-boundaries","index":232},{"batch":"289-branches","index":42},{"batch":"309-branches","index":80},{"batch":"235-branch-boundaries","index":198},{"batch":"095-branch-boundaries","index":207},{"batch":"015-branch-boundaries","index":15},{"batch":"015-branch-boundaries","index":123},{"batch":"055-branch-boundaries","index":140}]},{"id":"C17","input":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":1,"delivery":"promise"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"promise","branch":{"condition":-1,"otherwise":[0]}}],"readers":[{"id":0,"refs":[-1,1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":1,"delivery":"promise"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"promise","branch":{"condition":-1,"otherwise":[0]}}],"readers":[{"id":0,"refs":[-1,1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"L1: Required reader did not publish its final answer","comparison":null,"members":[{"batch":"135-branch-boundaries","index":240},{"batch":"015-branch-boundaries","index":241},{"batch":"029-branches","index":46},{"batch":"289-branches","index":77},{"batch":"055-branch-boundaries","index":144},{"batch":"209-branches","index":62},{"batch":"275-branch-boundaries","index":35},{"batch":"309-branches","index":197},{"batch":"195-branch-boundaries","index":177},{"batch":"129-branches","index":47},{"batch":"015-branch-boundaries","index":160},{"batch":"149-branches","index":188},{"batch":"269-branches","index":38},{"batch":"089-branches","index":159},{"batch":"269-branches","index":69},{"batch":"195-branch-boundaries","index":136},{"batch":"255-branch-boundaries","index":247},{"batch":"249-branches","index":239},{"batch":"029-branches","index":137}]},{"id":"C18","input":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[0],"factor":1,"offset":0,"delivery":"promise"},{"id":2,"deps":[-1],"factor":1,"offset":0,"delivery":"promise","branch":{"condition":-1,"otherwise":[1]}}],"readers":[{"id":0,"refs":[2],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[0],"factor":1,"offset":0,"delivery":"promise"},{"id":2,"deps":[-1],"factor":1,"offset":0,"delivery":"promise","branch":{"condition":-1,"otherwise":[1]}}],"readers":[{"id":0,"refs":[2],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"L1: Required reader did not publish its final answer","comparison":null,"members":[{"batch":"075-branch-boundaries","index":55},{"batch":"209-branches","index":136},{"batch":"015-branch-boundaries","index":244},{"batch":"149-branches","index":189},{"batch":"035-branch-boundaries","index":141},{"batch":"309-branches","index":52},{"batch":"155-branch-boundaries","index":167},{"batch":"175-branch-boundaries","index":160}]},{"id":"C19","input":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"proposals":[1],"authoritative":0},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"promise"},{"id":2,"deps":[0],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[1,2],"gated":false,"boundary":"none","pending":true}],"show":true,"turns":[{"steps":[{"op":"start-action"}]},{"steps":[{"op":"click","reader":0}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"optimistic":{"proposals":[1],"authoritative":0},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action"}]}],"strict":true},"signature":"S4: Residual work changed a completed view without new input","comparison":null,"members":[{"batch":"314-optimistic-readiness","index":205},{"batch":"194-optimistic-readiness","index":179},{"batch":"154-optimistic-readiness","index":68},{"batch":"194-optimistic-readiness","index":148},{"batch":"154-optimistic-readiness","index":97}]},{"id":"C20","input":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":1,"delivery":"promise"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"promise","branch":{"condition":-1,"otherwise":[0]}}],"readers":[{"id":0,"refs":[-1,1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":1,"delivery":"promise"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"promise","branch":{"condition":-1,"otherwise":[0]}}],"readers":[{"id":0,"refs":[-1,1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"L1: Required reader did not publish its final answer","comparison":null,"members":[{"batch":"009-branches","index":136},{"batch":"135-branch-boundaries","index":0},{"batch":"055-branch-boundaries","index":96},{"batch":"155-branch-boundaries","index":88},{"batch":"155-branch-boundaries","index":11},{"batch":"155-branch-boundaries","index":157},{"batch":"269-branches","index":47},{"batch":"009-branches","index":127},{"batch":"049-branches","index":32}]},{"id":"C21","input":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":2,"deps":[0],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[1,2],"gated":false,"boundary":"none","pending":true},{"id":1,"refs":[2],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":2,"deps":[1],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[0,2],"gated":false,"boundary":"none","pending":true},{"id":1,"refs":[2],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"S1: Published derivation disagrees with published input","comparison":null,"members":[{"batch":"013-derived-readiness","index":194},{"batch":"033-derived-readiness","index":23},{"batch":"173-derived-readiness","index":58},{"batch":"133-derived-readiness","index":37},{"batch":"253-derived-readiness","index":109},{"batch":"313-derived-readiness","index":166}]},{"id":"C22","input":{"version":2,"sources":[-1,-2],"anchorShow":false,"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"sync","branch":{"condition":-2,"otherwise":[-1]}},{"id":1,"deps":[0],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchorShow":false,"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"sync","branch":{"condition":-2,"otherwise":[-1]}},{"id":1,"deps":[0],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"S1: Published derivation disagrees with published input","comparison":null,"members":[{"batch":"109-branches","index":78},{"batch":"109-branches","index":226},{"batch":"289-branches","index":50},{"batch":"169-branches","index":237}]},{"id":"C23","input":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":true,"boundary":"none","pending":true}],"show":true,"turns":[{"steps":[{"op":"show","value":false},{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":true,"boundary":"none","pending":true}],"show":true,"turns":[{"steps":[{"op":"show","value":false},{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"W1: review: open: other: no-retained-witness: recovered: other-work-released","comparison":null,"members":[{"batch":"050-readiness","index":167},{"batch":"130-readiness","index":127},{"batch":"030-readiness","index":83},{"batch":"150-readiness","index":175},{"batch":"250-readiness","index":248},{"batch":"190-readiness","index":43},{"batch":"030-readiness","index":24}]},{"id":"C24","input":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"kind":"latest","proposals":[1],"authoritative":0,"viaMemo":false},"nodes":[],"readers":[{"id":0,"refs":[-2],"gated":false,"boundary":"none","mounted":true}],"show":true,"turns":[{"steps":[{"op":"start-action"},{"op":"mount","reader":0,"value":false}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"kind":"latest","proposals":[1],"authoritative":0,"viaMemo":false},"nodes":[],"readers":[{"id":0,"refs":[-2],"gated":false,"boundary":"none","mounted":true}],"show":true,"turns":[{"steps":[{"op":"start-action"},{"op":"mount","reader":0,"value":false}]}],"strict":true},"signature":"S1: Published derivation disagrees with published input","comparison":null,"members":[{"batch":"238-latest--latest-equivalence","index":229},{"batch":"178-latest--latest-equivalence","index":150},{"batch":"038-latest--latest-equivalence","index":33},{"batch":"218-latest--latest-equivalence","index":40},{"batch":"047-latest","index":204},{"batch":"267-latest","index":179},{"batch":"238-latest--latest-equivalence","index":228},{"batch":"127-latest","index":159},{"batch":"258-latest--latest-equivalence","index":227},{"batch":"118-latest--latest-equivalence","index":122}]},{"id":"C25","input":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"optimistic":{"kind":"latest","proposals":[0],"authoritative":1,"viaMemo":false},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none","mounted":false},{"id":1,"refs":[1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action"},{"op":"resume-action"}]},{"steps":[{"op":"mount","reader":0,"value":true}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"optimistic":{"kind":"latest","proposals":[0],"authoritative":1,"viaMemo":false},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none","mounted":false},{"id":1,"refs":[1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action"},{"op":"resume-action"}]},{"steps":[{"op":"mount","reader":0,"value":true}]}],"strict":true},"signature":"S2: Published mount control disagrees with its owned reader","comparison":null,"members":[{"batch":"147-latest","index":242},{"batch":"047-latest","index":229},{"batch":"087-latest","index":194}]},{"id":"C26","input":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"sync","branch":{"condition":-2,"otherwise":[-1]}},{"id":1,"deps":[0],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[-1,-2,1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchorShow":false,"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"sync","branch":{"condition":-2,"otherwise":[-1]}},{"id":1,"deps":[0],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"S1: Published derivation disagrees with published input","comparison":null,"members":[{"batch":"075-branch-boundaries","index":119},{"batch":"269-branches","index":112},{"batch":"209-branches","index":183}]},{"id":"C27","input":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":true,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"show","value":false}]},{"steps":[{"op":"dispose","reader":0}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":true,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"show","value":false}]},{"steps":[{"op":"dispose","reader":0}]}],"strict":true},"signature":"P1: Ordinary writes remain unpublished after all visible blocking demands have ended","comparison":null,"members":[{"batch":"243-multi","index":103},{"batch":"163-multi","index":51},{"batch":"209-branches","index":106}]},{"id":"C28","input":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"sync","branch":{"condition":-2,"otherwise":[-1]}}],"readers":[{"id":0,"refs":[-1,-2,1],"gated":false,"boundary":"none"},{"id":1,"refs":[0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchorShow":false,"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"sync","branch":{"condition":-2,"otherwise":[-1]}}],"readers":[{"id":0,"refs":[0,1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"S1: Published derivation disagrees with published input","comparison":null,"members":[{"batch":"309-branches","index":104},{"batch":"289-branches","index":200},{"batch":"289-branches","index":4}]},{"id":"C29","input":{"version":2,"sources":[-1],"anchors":[],"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"promise","branch":{"condition":-1,"otherwise":[0]}}],"readers":[{"id":0,"refs":[1],"gated":true,"boundary":"none"}],"show":false,"turns":[{"steps":[{"op":"show","value":true},{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"promise","branch":{"condition":-1,"otherwise":[0]}}],"readers":[{"id":0,"refs":[1],"gated":true,"boundary":"none"}],"show":false,"turns":[{"steps":[{"op":"show","value":true},{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"L1: Required reader did not publish its final answer","comparison":null,"members":[{"batch":"315-branch-boundaries","index":81},{"batch":"049-branches","index":190},{"batch":"135-branch-boundaries","index":237},{"batch":"295-branch-boundaries","index":210},{"batch":"135-branch-boundaries","index":191},{"batch":"229-branches","index":153}]},{"id":"C30","input":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"proposals":[0],"authoritative":1},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"promise"},{"id":2,"deps":[0],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[1,2],"gated":false,"boundary":"none","pending":true}],"show":true,"turns":[{"steps":[{"op":"start-action"},{"op":"resume-action"}]},{"steps":[{"op":"click","reader":0}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"optimistic":{"proposals":[1],"authoritative":0},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action"}]}],"strict":true},"signature":"S4: Residual work changed a completed view without new input","comparison":null,"members":[{"batch":"074-optimistic-readiness","index":137},{"batch":"094-optimistic-readiness","index":50},{"batch":"314-optimistic-readiness","index":131}]},{"id":"C31","input":{"version":2,"sources":[-1],"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":true,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"read","ref":-1,"mode":"latest"},{"op":"queue","id":0,"via":"microtask","steps":[{"op":"show","value":false}]},{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":true,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"show","value":false}]}],"strict":true},"signature":"P1: Ordinary writes remain unpublished after all visible blocking demands have ended","comparison":null,"members":[{"batch":"297-reads--latest-equivalence","index":215},{"batch":"077-reads--latest-equivalence","index":158},{"batch":"005-reads","index":134}]},{"id":"C32","input":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"promise","branch":{"condition":-2,"otherwise":[0]}}],"readers":[{"id":0,"refs":[-2,1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-2,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":1,"delivery":"promise"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"promise","branch":{"condition":-1,"otherwise":[0]}}],"readers":[{"id":0,"refs":[-1,1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"L1: Required reader did not publish its final answer","comparison":null,"members":[{"batch":"089-branches","index":174},{"batch":"175-branch-boundaries","index":145}]},{"id":"C33","input":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"manual","branch":{"condition":-2,"otherwise":[0]}}],"readers":[{"id":0,"refs":[1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"manual","branch":{"condition":-2,"otherwise":[0]}}],"readers":[{"id":0,"refs":[1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"L1: Required reader did not publish its final answer","comparison":null,"members":[{"batch":"075-branch-boundaries","index":133},{"batch":"249-branches","index":233}]},{"id":"C34","input":{"version":2,"sources":[-1,-2],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"},{"id":2,"deps":[0,1],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[2],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"write","source":-2,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"},{"id":2,"deps":[0,1],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[2],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"write","source":-2,"value":1}]}],"strict":true},"signature":"S1: Published derivation disagrees with published input","comparison":null,"members":[{"batch":"121-update-groups","index":40},{"batch":"201-update-groups","index":29}]},{"id":"C35","input":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":2,"deps":[0],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[1,2],"gated":false,"boundary":"none","pending":true}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"click","reader":0}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":2,"deps":[0],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[1,2],"gated":false,"boundary":"none","pending":true}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"click","reader":0}]}],"strict":true},"signature":"R2: A ready control read data inconsistent with published inputs","comparison":null,"members":[{"batch":"150-readiness","index":47},{"batch":"290-readiness","index":136}]},{"id":"C36","input":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":2,"deps":[0],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[-1,2],"gated":false,"boundary":"none"},{"id":1,"refs":[1,2],"gated":false,"boundary":"none","pending":true}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":2,"deps":[1],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[0,2],"gated":false,"boundary":"none","pending":true},{"id":1,"refs":[2],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"S1: Published derivation disagrees with published input","comparison":null,"members":[{"batch":"270-readiness","index":60},{"batch":"170-readiness","index":133},{"batch":"110-readiness","index":165},{"batch":"313-derived-readiness","index":192}]},{"id":"C37","input":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"proposals":[1],"authoritative":0},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[0,1],"gated":false,"boundary":"none","pending":true}],"show":true,"turns":[{"steps":[{"op":"start-action"},{"op":"resume-action"}]},{"steps":[{"op":"resolve","node":0,"which":"newest","key":"0:[1]#1"}]},{"steps":[{"op":"click","reader":0}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"optimistic":{"proposals":[1],"authoritative":0},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action"}]}],"strict":true},"signature":"S4: Residual work changed a completed view without new input","comparison":null,"members":[{"batch":"014-optimistic-readiness","index":194},{"batch":"174-optimistic-readiness","index":133}]},{"id":"C38","input":{"version":2,"sources":[-1],"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":true,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"read","ref":0,"mode":"latest"},{"op":"queue","id":0,"via":"microtask","steps":[{"op":"show","value":false}]},{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":true,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"show","value":false}]}],"strict":true},"signature":"P1: Ordinary writes remain unpublished after all visible blocking demands have ended","comparison":null,"members":[{"batch":"017-reads--latest-equivalence","index":58},{"batch":"297-reads--latest-equivalence","index":130}]},{"id":"C39","input":{"version":2,"sources":[-1,-2],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-2,0],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"dispose","reader":0}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-2,0],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"dispose","reader":0}]}],"strict":true},"signature":"P1: Ordinary writes remain unpublished after all visible blocking demands have ended","comparison":null,"members":[{"batch":"271-boundaries","index":65},{"batch":"203-multi","index":121}]},{"id":"C40","input":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"proposals":[0],"authoritative":1},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"promise"},{"id":2,"deps":[0],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[1,2],"gated":false,"boundary":"none","pending":true},{"id":1,"refs":[2],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action"}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"proposals":[0],"authoritative":1},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"promise"},{"id":2,"deps":[1],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[0,2],"gated":false,"boundary":"none","pending":true},{"id":1,"refs":[2],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action"}]}],"strict":true},"signature":"S1: Published derivation disagrees with published input","comparison":null,"members":[{"batch":"034-optimistic-readiness","index":201}]},{"id":"C41","input":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"optimistic":{"proposals":[0],"authoritative":1},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"promise"},{"id":2,"deps":[0],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[-2,2],"gated":false,"boundary":"none"},{"id":1,"refs":[1,2],"gated":false,"boundary":"none","pending":true}],"show":true,"turns":[{"steps":[{"op":"start-action"}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"proposals":[0],"authoritative":1},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"promise"},{"id":2,"deps":[1],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[0,2],"gated":false,"boundary":"none","pending":true},{"id":1,"refs":[2],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action"}]}],"strict":true},"signature":"S1: Published derivation disagrees with published input","comparison":null,"members":[{"batch":"054-optimistic-readiness","index":237}]},{"id":"C42","input":{"version":2,"sources":[-1],"anchors":[],"nodes":[{"id":0,"deps":[-1],"factor":2,"offset":0,"delivery":"sync"},{"id":1,"deps":[-1,0],"factor":1,"offset":0,"delivery":"manual"},{"id":2,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[-1,1],"gated":true,"boundary":"none"},{"id":1,"refs":[2],"gated":true,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"show","value":false},{"op":"write","source":-1,"value":2}]},{"steps":[{"op":"resolve","node":1,"which":"newest","key":"1:[2,4]#1"}]},{"steps":[{"op":"show","value":true}]},{"steps":[{"op":"write","source":-1,"value":0}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchors":[],"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[-1,1],"gated":true,"boundary":"none"},{"id":1,"refs":[0],"gated":true,"boundary":"none"}],"show":false,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"resolve","node":1,"which":"newest","key":"1:[1]#1"}]},{"steps":[{"op":"show","value":true}]},{"steps":[{"op":"write","source":-1,"value":0}]}],"strict":true},"signature":"S2: Published observation control disagrees with its reader","comparison":null,"members":[{"batch":"085-reads","index":87}]},{"id":"C43","input":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"proposals":[1],"authoritative":0},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-2,0],"factor":1,"offset":0,"delivery":"promise"},{"id":2,"deps":[-2],"factor":1,"offset":0,"delivery":"promise"}],"readers":[{"id":0,"refs":[1,2],"gated":false,"boundary":"none","pending":true}],"show":true,"turns":[{"steps":[{"op":"start-action"}]},{"steps":[{"op":"click","reader":0}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"optimistic":{"proposals":[1],"authoritative":0},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action"}]}],"strict":true},"signature":"S4: Residual work changed a completed view without new input","comparison":null,"members":[{"batch":"094-optimistic-readiness","index":91}]},{"id":"C44","input":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"reset"},{"id":1,"refs":[1],"gated":false,"boundary":"none","parent":0}],"show":true,"turns":[{"steps":[{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"reset"},{"id":1,"refs":[1],"gated":false,"boundary":"none","parent":0}],"show":true,"turns":[{"steps":[{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"S1: Published derivation disagrees with published input","comparison":null,"members":[{"batch":"152-nested","index":87}]},{"id":"C45","input":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"proposals":[1],"authoritative":2},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[0],"factor":1,"offset":0,"delivery":"sync"},{"id":2,"deps":[1],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[-2,2],"gated":false,"boundary":"none","pending":true},{"id":1,"refs":[2],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action"}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"optimistic":{"proposals":[1],"authoritative":0},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action"}]}],"strict":true},"signature":"S4: Residual work changed a completed view without new input","comparison":null,"members":[{"batch":"274-optimistic-readiness","index":143}]},{"id":"C46","input":{"version":2,"sources":[-1,-2],"anchors":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[-1],"gated":true,"boundary":"none"},{"id":1,"refs":[0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"show","value":false},{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[-1],"gated":true,"boundary":"none"},{"id":1,"refs":[0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"show","value":false},{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"S1: Published derivation disagrees with published input","comparison":null,"members":[{"batch":"291-boundaries","index":57},{"batch":"231-boundaries","index":120},{"batch":"151-boundaries","index":63},{"batch":"163-multi","index":212}]},{"id":"C47","input":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[0],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0,1],"gated":true,"boundary":"none","pending":true}],"show":false,"turns":[{"steps":[{"op":"write","source":-1,"value":1},{"op":"show","value":true}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[0],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0,1],"gated":true,"boundary":"none","pending":true}],"show":false,"turns":[{"steps":[{"op":"write","source":-1,"value":1},{"op":"show","value":true}]}],"strict":true},"signature":"W1: review: open: other: no-retained-witness: recovered: other-work-released","comparison":null,"members":[{"batch":"293-derived-readiness","index":101}]},{"id":"C48","input":{"version":2,"sources":[-1,-2],"anchorShow":false,"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"sync","branch":{"condition":-2,"otherwise":[-1]}}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"},{"id":1,"refs":[1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchorShow":false,"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"sync","branch":{"condition":-2,"otherwise":[-1]}}],"readers":[{"id":0,"refs":[0,1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"S1: Published derivation disagrees with published input","comparison":null,"members":[{"batch":"315-branch-boundaries","index":225}]},{"id":"C49","input":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"manual","branch":{"condition":-2,"otherwise":[0]}}],"readers":[{"id":0,"refs":[1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1},{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"write","source":-1,"value":0}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"manual","branch":{"condition":-2,"otherwise":[0]}}],"readers":[{"id":0,"refs":[1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1},{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"write","source":-1,"value":0}]}],"strict":true},"signature":"L1: Required reader did not publish its final answer","comparison":null,"members":[{"batch":"029-branches","index":27}]},{"id":"C50","input":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"proposals":[1],"authoritative":0},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[0,1],"gated":false,"boundary":"none","pending":true}],"show":true,"turns":[{"steps":[{"op":"start-action"}]},{"steps":[{"op":"resolve","node":0,"which":"newest","key":"0:[1]#1"},{"op":"resume-action"}]},{"steps":[{"op":"click","reader":0}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"optimistic":{"proposals":[1],"authoritative":0},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action"}]}],"strict":true},"signature":"S4: Residual work changed a completed view without new input","comparison":null,"members":[{"batch":"074-optimistic-readiness","index":47}]},{"id":"C51","input":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":true,"boundary":"reset"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"show","value":false},{"op":"write","source":-1,"value":0}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":true,"boundary":"reset"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"show","value":false},{"op":"write","source":-1,"value":0}]}],"strict":true},"signature":"W1: review: open: other: no-retained-witness: recovered: other-work-released","comparison":null,"members":[{"batch":"205-reads","index":175}]},{"id":"C52","input":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":true,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"queue","id":0,"via":"microtask","steps":[{"op":"write","source":-1,"value":0}]},{"op":"resolve","node":0,"which":"newest","key":"0:[1]#1"}]},{"steps":[{"op":"read","ref":0,"mode":"latest"},{"op":"show","value":false},{"op":"write","source":-1,"value":0}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":true,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"resolve","node":0,"which":"newest","key":"0:[1]#1"},{"op":"write","source":-1,"value":0}]},{"steps":[{"op":"read","ref":0,"mode":"latest"},{"op":"show","value":false},{"op":"write","source":-1,"value":0}]}],"strict":true},"signature":"W1: review: open: other: no-retained-witness: recovered: other-work-released","comparison":null,"members":[{"batch":"245-reads","index":249}]},{"id":"C53","input":{"version":2,"sources":[-1],"anchors":[],"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[0],"factor":1,"offset":0,"delivery":"promise","branch":{"condition":-1,"otherwise":[-1]}}],"readers":[{"id":0,"refs":[1],"gated":true,"boundary":"none"}],"show":false,"turns":[{"steps":[{"op":"show","value":true},{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"write","source":-1,"value":0}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[0],"factor":1,"offset":0,"delivery":"promise","branch":{"condition":-1,"otherwise":[-1]}}],"readers":[{"id":0,"refs":[1],"gated":true,"boundary":"none"}],"show":false,"turns":[{"steps":[{"op":"show","value":true},{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"write","source":-1,"value":0}]}],"strict":true},"signature":"L1: Required reader did not publish its final answer","comparison":null,"members":[{"batch":"295-branch-boundaries","index":99}]},{"id":"C54","input":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[-2,0],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[-2,1],"gated":false,"boundary":"reset"},{"id":1,"refs":[0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[-2,0],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[-2,1],"gated":false,"boundary":"reset"},{"id":1,"refs":[0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"S1: Published derivation disagrees with published input","comparison":null,"members":[{"batch":"312-nested","index":99}]},{"id":"C55","input":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":2,"deps":[1],"factor":1,"offset":0,"delivery":"manual","branch":{"condition":-1,"otherwise":[0]}}],"readers":[{"id":0,"refs":[2],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"write","source":-1,"value":0},{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":2,"deps":[1],"factor":1,"offset":0,"delivery":"manual","branch":{"condition":-1,"otherwise":[0]}}],"readers":[{"id":0,"refs":[2],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"write","source":-1,"value":0},{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"W1: review: open: other: no-retained-witness: recovered: other-work-released","comparison":null,"members":[{"batch":"055-branch-boundaries","index":58}]},{"id":"C56","input":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"proposals":[1],"authoritative":0},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"promise"},{"id":2,"deps":[0],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[1,2],"gated":false,"boundary":"none","pending":true},{"id":1,"refs":[2],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action"}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"optimistic":{"proposals":[1],"authoritative":0},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action"}]}],"strict":true},"signature":"S4: Residual work changed a completed view without new input","comparison":null,"members":[{"batch":"074-optimistic-readiness","index":195}]},{"id":"C57","input":{"version":2,"sources":[-1],"anchors":[],"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"await"},{"id":1,"deps":[0],"factor":1,"offset":0,"delivery":"sync"},{"id":2,"deps":[-1],"factor":1,"offset":0,"delivery":"manual","branch":{"condition":-1,"otherwise":[1]}}],"readers":[{"id":0,"refs":[1,2],"gated":true,"boundary":"none"}],"show":false,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"show","value":true}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"await"},{"id":1,"deps":[0],"factor":1,"offset":0,"delivery":"sync"},{"id":2,"deps":[-1],"factor":1,"offset":0,"delivery":"manual","branch":{"condition":-1,"otherwise":[1]}}],"readers":[{"id":0,"refs":[1,2],"gated":true,"boundary":"none"}],"show":false,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"show","value":true}]}],"strict":true},"signature":"L1: Required reader did not publish its final answer","comparison":null,"members":[{"batch":"089-branches","index":204}]},{"id":"C58","input":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":1,"delivery":"manual"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"promise","branch":{"condition":-1,"otherwise":[0]}}],"readers":[{"id":0,"refs":[1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":1,"delivery":"manual"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"promise","branch":{"condition":-1,"otherwise":[0]}}],"readers":[{"id":0,"refs":[1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"W1: review: open: other: no-retained-witness: recovered: other-work-released","comparison":null,"members":[{"batch":"109-branches","index":244}]},{"id":"C59","input":{"version":1,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"await"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"resolve","node":0,"which":"newest","key":"0:[1]#1"},{"op":"queue","id":0,"via":"microtask","steps":[{"op":"write","source":-1,"value":0}]}]},{"steps":[{"op":"write","source":-1,"value":1},{"op":"read","ref":0,"mode":"latest"},{"op":"dispose","reader":0}]}],"strict":true},"reduced":{"version":1,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"await"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"resolve","node":0,"which":"newest","key":"0:[1]#1"},{"op":"queue","id":0,"via":"microtask","steps":[{"op":"write","source":-1,"value":0}]}]},{"steps":[{"op":"write","source":-1,"value":1},{"op":"read","ref":0,"mode":"latest"},{"op":"dispose","reader":0}]}],"strict":true},"signature":"P1: Ordinary writes remain unpublished after all visible blocking demands have ended","comparison":null,"members":[{"batch":"117-reads--latest-equivalence","index":96}]},{"id":"C60","input":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"optimistic":{"proposals":[1,3],"authoritative":0},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":2,"delivery":"sync"},{"id":1,"deps":[-2,0],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action"}]},{"steps":[{"op":"resolve","node":1,"which":"newest","key":"1:[1,3]#1"}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"optimistic":{"proposals":[1],"authoritative":0},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action"}]}],"strict":true},"signature":"S4: Residual work changed a completed view without new input","comparison":null,"members":[{"batch":"124-optimistic","index":155}]},{"id":"C61","input":{"version":2,"sources":[-1],"anchors":[],"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"promise","branch":{"condition":-1,"otherwise":[0]}}],"readers":[{"id":0,"refs":[1],"gated":true,"boundary":"none"}],"show":false,"turns":[{"steps":[{"op":"queue","id":0,"via":"microtask","steps":[{"op":"write","source":-1,"value":1}]},{"op":"show","value":true}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"promise","branch":{"condition":-1,"otherwise":[0]}}],"readers":[{"id":0,"refs":[1],"gated":true,"boundary":"none"}],"show":false,"turns":[{"steps":[{"op":"show","value":true},{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"L1: Required reader did not publish its final answer","comparison":null,"members":[{"batch":"135-branch-boundaries","index":26}]},{"id":"C62","input":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":2,"deps":[0],"factor":1,"offset":0,"delivery":"sync","branch":{"condition":-1,"otherwise":[1]}},{"id":3,"deps":[-1],"factor":1,"offset":0,"delivery":"promise","branch":{"condition":-1,"otherwise":[2]}}],"readers":[{"id":0,"refs":[3],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[0],"factor":1,"offset":0,"delivery":"promise"},{"id":2,"deps":[-1],"factor":1,"offset":0,"delivery":"promise","branch":{"condition":-1,"otherwise":[1]}}],"readers":[{"id":0,"refs":[2],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"L1: Required reader did not publish its final answer","comparison":null,"members":[{"batch":"169-branches","index":161}]},{"id":"C63","input":{"version":2,"sources":[-1,-2],"anchorShow":false,"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual","branch":{"condition":-2,"otherwise":[-1]}}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"resolve","node":0,"which":"newest","key":"0:[1,1]#1"},{"op":"write","source":-2,"value":2}]},{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchorShow":false,"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual","branch":{"condition":-2,"otherwise":[-1]}}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"resolve","node":0,"which":"newest","key":"0:[1,1]#1"},{"op":"write","source":-2,"value":2}]},{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"S1: Published derivation disagrees with published input","comparison":null,"members":[{"batch":"189-branches","index":90}]},{"id":"C64","input":{"version":2,"sources":[-1],"anchors":[],"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[0],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"},{"id":1,"refs":[1],"gated":true,"boundary":"reset"}],"show":false,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"show","value":true}]},{"steps":[{"op":"write","source":-1,"value":0}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchors":[],"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[0],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"},{"id":1,"refs":[1],"gated":true,"boundary":"reset"}],"show":false,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"show","value":true}]},{"steps":[{"op":"write","source":-1,"value":0}]}],"strict":true},"signature":"S2: Published observation control disagrees with its reader","comparison":null,"members":[{"batch":"191-boundaries","index":94},{"batch":"317-reads--latest-equivalence","index":242}]},{"id":"C65","input":{"version":2,"sources":[-1,-2],"anchors":[],"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-1,0],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":true,"boundary":"retain"},{"id":1,"refs":[1],"gated":true,"boundary":"none"}],"show":false,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"show","value":true}]},{"steps":[{"op":"write","source":-1,"value":0},{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"dispose","reader":0}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[],"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-1,0],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":true,"boundary":"retain"},{"id":1,"refs":[1],"gated":true,"boundary":"none"}],"show":false,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"show","value":true}]},{"steps":[{"op":"write","source":-1,"value":0},{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"dispose","reader":0}]}],"strict":true},"signature":"S2: Published observation control disagrees with its reader","comparison":null,"members":[{"batch":"291-boundaries","index":133}]},{"id":"C66","input":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-1],"factor":1,"offset":1,"delivery":"sync"},{"id":2,"deps":[-1],"factor":1,"offset":0,"delivery":"promise","branch":{"condition":-1,"otherwise":[1]}},{"id":3,"deps":[2],"factor":1,"offset":0,"delivery":"sync","branch":{"condition":-1,"otherwise":[0]}}],"readers":[{"id":0,"refs":[3],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-1],"factor":1,"offset":1,"delivery":"sync"},{"id":2,"deps":[-1],"factor":1,"offset":0,"delivery":"promise","branch":{"condition":-1,"otherwise":[1]}},{"id":3,"deps":[2],"factor":1,"offset":0,"delivery":"sync","branch":{"condition":-1,"otherwise":[0]}}],"readers":[{"id":0,"refs":[3],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"W1: review: open: other: no-retained-witness: recovered: other-work-released","comparison":null,"members":[{"batch":"295-branch-boundaries","index":184}]},{"id":"C67","input":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"kind":"latest","proposals":[1,0],"authoritative":0,"viaMemo":false},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":true,"boundary":"none"}],"show":false,"turns":[{"steps":[{"op":"start-action"}]},{"steps":[{"op":"resume-action"},{"op":"show","value":true}]},{"steps":[{"op":"show","value":false}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"kind":"latest","proposals":[1,0],"authoritative":0,"viaMemo":false},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":true,"boundary":"none"}],"show":false,"turns":[{"steps":[{"op":"start-action"}]},{"steps":[{"op":"resume-action"},{"op":"show","value":true}]},{"steps":[{"op":"show","value":false}]}],"strict":true},"signature":"O1: Ready optimistic source did not publish while its parent action remained open","comparison":null,"members":[{"batch":"298-latest--latest-equivalence","index":238}]},{"id":"C68","input":{"version":2,"sources":[-1,-2],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"promise"},{"id":2,"deps":[0,1],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[2],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1},{"op":"flush"},{"op":"write","source":-2,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"},{"id":2,"deps":[0,1],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[2],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"write","source":-2,"value":1}]}],"strict":true},"signature":"S1: Published derivation disagrees with published input","comparison":null,"members":[{"batch":"021-update-groups","index":205}]},{"id":"C69","input":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"},{"id":1,"refs":[1],"gated":true,"boundary":"none"}],"show":false,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"show","value":true}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"},{"id":1,"refs":[1],"gated":true,"boundary":"none"}],"show":false,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"show","value":true}]}],"strict":true},"signature":"S1: Published derivation disagrees with published input","comparison":null,"members":[{"batch":"026-mounts","index":128}]},{"id":"C70","input":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":true,"boundary":"none"},{"id":1,"refs":[1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"show","value":false},{"op":"resolve","node":0,"which":"newest","key":"0:[1]#1"}]},{"steps":[{"op":"show","value":true}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"},{"id":1,"refs":[1],"gated":true,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"show","value":false},{"op":"resolve","node":1,"which":"newest","key":"1:[1]#1"}]},{"steps":[{"op":"show","value":true}]}],"strict":true},"signature":"L1: Required reader did not publish its final answer","comparison":null,"members":[{"batch":"029-branches","index":88}]},{"id":"C71","input":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":2,"delivery":"promise"},{"id":1,"deps":[0,-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[-2,1],"gated":false,"boundary":"retain"}],"show":true,"turns":[{"steps":[{"op":"write","source":-2,"value":3}]},{"steps":[{"op":"write","source":-1,"value":1},{"op":"resolve","node":1,"which":"newest","key":"1:[2,3]#1"}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":2,"delivery":"promise"},{"id":1,"deps":[0,-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[-2,1],"gated":false,"boundary":"retain"}],"show":true,"turns":[{"steps":[{"op":"write","source":-2,"value":3}]},{"steps":[{"op":"write","source":-1,"value":1},{"op":"resolve","node":1,"which":"newest","key":"1:[2,3]#1"}]}],"strict":true},"signature":"S1: Published derivation disagrees with published input","comparison":null,"members":[{"batch":"063-multi","index":24}]},{"id":"C72","input":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[0],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[1],"gated":true,"boundary":"none"}],"show":false,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"resolve","node":0,"which":"newest","key":"0:[1]#1"}]},{"steps":[{"op":"show","value":true}]},{"steps":[{"op":"write","source":-1,"value":0}]},{"steps":[{"op":"dispose","reader":0}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[0],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[1],"gated":true,"boundary":"none"}],"show":false,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"resolve","node":0,"which":"newest","key":"0:[1]#1"}]},{"steps":[{"op":"show","value":true}]},{"steps":[{"op":"write","source":-1,"value":0}]},{"steps":[{"op":"dispose","reader":0}]}],"strict":true},"signature":"P1: Ordinary writes remain unpublished after all visible blocking demands have ended","comparison":null,"members":[{"batch":"082-ordinary","index":163}]},{"id":"C73","input":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"kind":"latest","proposals":[1,0],"authoritative":0,"viaMemo":false},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":true,"boundary":"none"}],"show":false,"turns":[{"steps":[{"op":"start-action"}]},{"steps":[{"op":"show","value":true}]},{"steps":[{"op":"resume-action"},{"op":"show","value":false}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"kind":"latest","proposals":[1,0],"authoritative":0,"viaMemo":false},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"promise"}],"readers":[{"id":0,"refs":[0],"gated":true,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action"}]},{"steps":[{"op":"resume-action"},{"op":"show","value":false}]}],"strict":true},"signature":"S1: Published derivation disagrees with published input","comparison":null,"members":[{"batch":"107-latest","index":71}]},{"id":"C74","input":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[0],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[-1,1],"gated":false,"boundary":"none"},{"id":1,"refs":[0,1],"gated":false,"boundary":"none","mounted":false,"pending":true}],"show":true,"turns":[{"steps":[{"op":"mount","reader":1,"value":true},{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[0],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[-1,1],"gated":false,"boundary":"none"},{"id":1,"refs":[0,1],"gated":false,"boundary":"none","mounted":false,"pending":true}],"show":true,"turns":[{"steps":[{"op":"mount","reader":1,"value":true},{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"S1: Torn tuple delivered to an effect","comparison":null,"members":[{"batch":"110-readiness","index":13},{"batch":"270-readiness","index":64}]},{"id":"C75","input":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"resolve","node":1,"which":"newest","key":"1:[1]#1"}]},{"steps":[{"op":"write","source":-1,"value":2},{"op":"read","ref":1,"mode":"latest"}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"resolve","node":1,"which":"newest","key":"1:[1]#1"}]},{"steps":[{"op":"write","source":-1,"value":2},{"op":"read","ref":1,"mode":"latest"}]}],"strict":true},"signature":"P1: Ordinary writes remain unpublished after all visible blocking demands have ended","comparison":null,"members":[{"batch":"145-reads","index":162},{"batch":"177-reads--latest-equivalence","index":115},{"batch":"137-reads--latest-equivalence","index":103}]},{"id":"C76","input":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":2,"deps":[1],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[0,2],"gated":false,"boundary":"none","pending":true},{"id":1,"refs":[2],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":2,"deps":[1],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[0,2],"gated":false,"boundary":"none","pending":true},{"id":1,"refs":[2],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"S1: Published derivation disagrees with published input","comparison":null,"members":[{"batch":"173-derived-readiness","index":66},{"batch":"310-readiness","index":128}]},{"id":"C77","input":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"kind":"latest","proposals":[1],"authoritative":0,"viaMemo":false},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none","mounted":true}],"show":true,"turns":[{"steps":[{"op":"start-action"}]},{"steps":[{"op":"queue","id":0,"via":"microtask","steps":[{"op":"mount","reader":0,"value":false}]},{"op":"resolve","node":0,"which":"newest","key":"0:[1]#1"}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"kind":"latest","proposals":[1],"authoritative":0,"viaMemo":false},"nodes":[],"readers":[{"id":0,"refs":[-2],"gated":false,"boundary":"none","mounted":true}],"show":true,"turns":[{"steps":[{"op":"start-action"},{"op":"mount","reader":0,"value":false}]}],"strict":true},"signature":"S1: Published derivation disagrees with published input","comparison":null,"members":[{"batch":"187-latest","index":120},{"batch":"187-latest","index":206}]},{"id":"C78","input":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual","branch":{"condition":-2,"otherwise":[-1]}},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"manual","branch":{"condition":-2,"otherwise":[0]}}],"readers":[{"id":0,"refs":[-2,1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"manual","branch":{"condition":-2,"otherwise":[0]}}],"readers":[{"id":0,"refs":[1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"L1: Required reader did not publish its final answer","comparison":null,"members":[{"batch":"235-branch-boundaries","index":153}]},{"id":"C79","input":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0,1],"gated":false,"boundary":"retain"}],"show":true,"turns":[{"steps":[{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0,1],"gated":false,"boundary":"retain"}],"show":true,"turns":[{"steps":[{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"S1: Published derivation disagrees with published input","comparison":null,"members":[{"batch":"051-boundaries","index":56}]},{"id":"C80","input":{"version":2,"sources":[-1,-2],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"promise"}],"readers":[{"id":0,"refs":[-2,0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"write","source":-2,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"promise"}],"readers":[{"id":0,"refs":[-2,0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"write","source":-2,"value":1}]}],"strict":true},"signature":"G2: A ready update group remains unpublished: independent","comparison":null,"members":[{"batch":"061-update-groups","index":5}]},{"id":"C81","input":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"promise"}],"readers":[{"id":0,"refs":[-1,1],"gated":false,"boundary":"reset"},{"id":1,"refs":[0],"gated":false,"boundary":"none","parent":0}],"show":true,"turns":[{"steps":[{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"promise"}],"readers":[{"id":0,"refs":[-1,1],"gated":false,"boundary":"reset"},{"id":1,"refs":[0],"gated":false,"boundary":"none","parent":0}],"show":true,"turns":[{"steps":[{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"S1: Published derivation disagrees with published input","comparison":null,"members":[{"batch":"132-nested","index":133}]},{"id":"C82","input":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[0],"factor":1,"offset":0,"delivery":"sync"},{"id":2,"deps":[1],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[-1,2],"gated":false,"boundary":"none","pending":true}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"write","source":-1,"value":2}]},{"steps":[{"op":"click","reader":0}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[0],"factor":1,"offset":0,"delivery":"sync"},{"id":2,"deps":[1],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[-1,2],"gated":false,"boundary":"none","pending":true}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"write","source":-1,"value":2}]},{"steps":[{"op":"click","reader":0}]}],"strict":true},"signature":"R2: A ready control read data inconsistent with published inputs","comparison":null,"members":[{"batch":"190-readiness","index":73}]},{"id":"C83","input":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"optimistic":{"proposals":[1],"authoritative":0},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"promise"},{"id":2,"deps":[0],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[-2,2],"gated":false,"boundary":"none"},{"id":1,"refs":[1,2],"gated":false,"boundary":"none","pending":true}],"show":true,"turns":[{"steps":[{"op":"start-action"}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"optimistic":{"proposals":[1],"authoritative":0},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action"}]}],"strict":true},"signature":"S4: Residual work changed a completed view without new input","comparison":null,"members":[{"batch":"194-optimistic-readiness","index":119}]},{"id":"C84","input":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":2,"delivery":"promise"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"sync","branch":{"condition":-1,"otherwise":[0]}},{"id":2,"deps":[-1,1],"factor":1,"offset":0,"delivery":"promise"}],"readers":[{"id":0,"refs":[-1,2],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":1,"delivery":"promise"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"promise","branch":{"condition":-1,"otherwise":[0]}}],"readers":[{"id":0,"refs":[-1,1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"L1: Required reader did not publish its final answer","comparison":null,"members":[{"batch":"255-branch-boundaries","index":243},{"batch":"229-branches","index":67}]},{"id":"C85","input":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"kind":"latest","proposals":[1,0],"authoritative":0,"viaMemo":false},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[0],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[1],"gated":true,"boundary":"none"}],"show":false,"turns":[{"steps":[{"op":"start-action"}]},{"steps":[{"op":"show","value":true},{"op":"resume-action"}]},{"steps":[{"op":"show","value":false}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"kind":"latest","proposals":[1,0],"authoritative":0,"viaMemo":false},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[0],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[1],"gated":true,"boundary":"none"}],"show":false,"turns":[{"steps":[{"op":"start-action"}]},{"steps":[{"op":"show","value":true},{"op":"resume-action"}]},{"steps":[{"op":"show","value":false}]}],"strict":true},"signature":"O1: Ready optimistic source did not publish while its parent action remained open","comparison":null,"members":[{"batch":"287-latest","index":168}]},{"id":"C86","input":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise","branch":{"condition":-2,"otherwise":[-1]}},{"id":1,"deps":[-1],"factor":1,"offset":1,"delivery":"manual","branch":{"condition":-2,"otherwise":[-1]}},{"id":2,"deps":[0],"factor":1,"offset":0,"delivery":"sync","branch":{"condition":-2,"otherwise":[1]}}],"readers":[{"id":0,"refs":[2],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-2,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise","branch":{"condition":-2,"otherwise":[-1]}},{"id":1,"deps":[-1],"factor":1,"offset":1,"delivery":"manual","branch":{"condition":-2,"otherwise":[-1]}},{"id":2,"deps":[0],"factor":1,"offset":0,"delivery":"sync","branch":{"condition":-2,"otherwise":[1]}}],"readers":[{"id":0,"refs":[2],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-2,"value":1}]}],"strict":true},"signature":"W1: review: open: other: no-retained-witness: recovered: other-work-released","comparison":null,"members":[{"batch":"289-branches","index":176}]},{"id":"C87","input":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":true,"boundary":"none"},{"id":1,"refs":[0],"gated":true,"boundary":"reset"}],"show":false,"turns":[{"steps":[{"op":"show","value":true},{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"show","value":false}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":true,"boundary":"none"},{"id":1,"refs":[0],"gated":true,"boundary":"reset"}],"show":false,"turns":[{"steps":[{"op":"show","value":true},{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"show","value":false}]}],"strict":true},"signature":"W1: review: open: other: no-retained-witness: recovered: other-work-released","comparison":null,"members":[{"batch":"297-reads--latest-equivalence","index":10}]},{"id":"C88","input":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"kind":"latest","proposals":[0],"authoritative":1,"viaMemo":false},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"promise"}],"readers":[{"id":0,"refs":[-2],"gated":true,"boundary":"none"},{"id":1,"refs":[0],"gated":false,"boundary":"none"}],"show":false,"turns":[{"steps":[{"op":"start-action"},{"op":"resume-action"},{"op":"queue","id":0,"via":"microtask","steps":[{"op":"show","value":true}]}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"kind":"latest","proposals":[0],"authoritative":1,"viaMemo":false},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"promise"}],"readers":[{"id":0,"refs":[-2],"gated":true,"boundary":"none"},{"id":1,"refs":[0],"gated":false,"boundary":"none"}],"show":false,"turns":[{"steps":[{"op":"start-action"},{"op":"resume-action"},{"op":"queue","id":0,"via":"microtask","steps":[{"op":"show","value":true}]}]}],"strict":true},"signature":"S1: Published derivation disagrees with published input","comparison":null,"members":[{"batch":"027-latest","index":141}]},{"id":"C89","input":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":2,"delivery":"manual"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"sync","branch":{"condition":-1,"otherwise":[0]}},{"id":2,"deps":[1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[2],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"write","source":-1,"value":2}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":1,"delivery":"manual"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"promise","branch":{"condition":-1,"otherwise":[0]}}],"readers":[{"id":0,"refs":[1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"W1: review: open: other: no-retained-witness: recovered: other-work-released","comparison":null,"members":[{"batch":"029-branches","index":179}]},{"id":"C90","input":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[1],"gated":false,"boundary":"reset"},{"id":1,"refs":[0],"gated":false,"boundary":"none","parent":0}],"show":true,"turns":[{"steps":[{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[1],"gated":false,"boundary":"reset"},{"id":1,"refs":[0],"gated":false,"boundary":"none","parent":0}],"show":true,"turns":[{"steps":[{"op":"write","source":-2,"value":1}]},{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"S1: Published derivation disagrees with published input","comparison":null,"members":[{"batch":"132-nested","index":243}]},{"id":"C91","input":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"kind":"latest","proposals":[1],"authoritative":0,"viaMemo":true},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[0],"factor":1,"offset":0,"delivery":"sync"},{"id":2,"deps":[1],"factor":1,"offset":0,"delivery":"promise"}],"readers":[{"id":0,"refs":[2],"gated":true,"boundary":"none"}],"show":false,"turns":[{"steps":[{"op":"start-action"}]},{"steps":[{"op":"show","value":true}]}]},"reduced":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"kind":"latest","proposals":[1],"authoritative":0,"viaMemo":true},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[0],"factor":1,"offset":0,"delivery":"sync"},{"id":2,"deps":[1],"factor":1,"offset":0,"delivery":"promise"}],"readers":[{"id":0,"refs":[2],"gated":true,"boundary":"none"}],"show":false,"turns":[{"steps":[{"op":"start-action"}]},{"steps":[{"op":"show","value":true}]}]},"signature":"late-early: E4: Early latest creation changes later observations","comparison":"latest","members":[{"batch":"298-latest--latest-equivalence","index":73}]},{"id":"C92","input":{"version":2,"sources":[-1],"anchors":[],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":2,"deps":[1],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[-1,2],"gated":false,"boundary":"none"},{"id":1,"refs":[0,2],"gated":false,"boundary":"none","pending":true}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":2,"deps":[1],"factor":1,"offset":0,"delivery":"sync"}],"readers":[{"id":0,"refs":[0,2],"gated":false,"boundary":"none","pending":true},{"id":1,"refs":[2],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"S1: Published derivation disagrees with published input","comparison":null,"members":[{"batch":"093-derived-readiness","index":159}]},{"id":"C93","input":{"version":2,"sources":[-1,-2],"anchors":[-2],"anchorShow":false,"optimistic":{"proposals":[1],"authoritative":0},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[-2],"factor":1,"offset":0,"delivery":"promise"},{"id":2,"deps":[-2,1],"factor":1,"offset":0,"delivery":"promise"}],"readers":[{"id":0,"refs":[0,2],"gated":false,"boundary":"none","pending":true}],"show":true,"turns":[{"steps":[{"op":"start-action"}]},{"steps":[{"op":"click","reader":0}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"optimistic":{"proposals":[1],"authoritative":0},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action"}]}],"strict":true},"signature":"S4: Residual work changed a completed view without new input","comparison":null,"members":[{"batch":"154-optimistic-readiness","index":113}]},{"id":"C94","input":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"optimistic":{"proposals":[3,1],"authoritative":0},"nodes":[{"id":0,"deps":[-2,-2],"factor":2,"offset":0,"delivery":"sync"},{"id":1,"deps":[-2,0],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action"}]},{"steps":[{"op":"resolve","node":1,"which":"newest","key":"1:[3,12]#1"}]}],"strict":true},"reduced":{"version":2,"sources":[-1,-2],"anchors":[],"anchorShow":false,"optimistic":{"proposals":[1],"authoritative":0},"nodes":[{"id":0,"deps":[-2],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"start-action"}]}],"strict":true},"signature":"S4: Residual work changed a completed view without new input","comparison":null,"members":[{"batch":"244-optimistic","index":230}]},{"id":"C95","input":{"version":1,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"queue","id":0,"via":"microtask","steps":[{"op":"show","value":false}]},{"op":"resolve","node":1,"which":"newest","key":"1:[1]#1"},{"op":"write","source":-1,"value":0}]},{"steps":[{"op":"show","value":false},{"op":"read","ref":1,"mode":"latest"}]}],"strict":true},"reduced":{"version":1,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"},{"id":1,"deps":[-1],"factor":1,"offset":0,"delivery":"manual"}],"readers":[{"id":0,"refs":[0],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"write","source":-1,"value":1}]},{"steps":[{"op":"resolve","node":1,"which":"newest","key":"1:[1]#1"},{"op":"show","value":false},{"op":"write","source":-1,"value":0}]},{"steps":[{"op":"show","value":false},{"op":"read","ref":1,"mode":"latest"}]}],"strict":true},"signature":"P1: Ordinary writes remain unpublished after all visible blocking demands have ended","comparison":null,"members":[{"batch":"265-reads","index":80}]},{"id":"C96","input":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[0],"factor":1,"offset":0,"delivery":"promise"}],"readers":[{"id":0,"refs":[0,1],"gated":false,"boundary":"none","mounted":false,"pending":true},{"id":1,"refs":[1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"mount","reader":0,"value":true},{"op":"write","source":-1,"value":1}]}],"strict":true},"reduced":{"version":2,"sources":[-1],"anchorShow":false,"nodes":[{"id":0,"deps":[-1],"factor":1,"offset":0,"delivery":"promise"},{"id":1,"deps":[0],"factor":1,"offset":0,"delivery":"promise"}],"readers":[{"id":0,"refs":[0,1],"gated":false,"boundary":"none","mounted":false,"pending":true},{"id":1,"refs":[1],"gated":false,"boundary":"none"}],"show":true,"turns":[{"steps":[{"op":"mount","reader":0,"value":true},{"op":"write","source":-1,"value":1}]}],"strict":true},"signature":"S1: Published derivation disagrees with published input","comparison":null,"members":[{"batch":"293-derived-readiness","index":74}]}],"routes":[{"source":"C32","target":"C17","reason":"source merge and offset"},{"source":"C61","target":"C29","reason":"queued write follows visibility"},{"source":"C76","target":"C21","reason":"delivery and declaration"},{"source":"C85","target":"C67","reason":"action and visibility order"},{"source":"C16","target":"C17","reason":"async helper and observed source"},{"source":"C34","target":"C80","reason":"combined memo and delivery"},{"source":"C88","target":"C10","reason":"delivery enables lifecycle removal"},{"source":"C96","target":"C74","reason":"source read enables synchronous child"}],"holdout":[{"cohort":"ordinary","seed":91500,"cases":150},{"cohort":"branches","seed":91501,"cases":150},{"cohort":"observation","seed":91502,"cases":150},{"cohort":"optimistic","seed":91503,"cases":150},{"cohort":"readiness","seed":91504,"cases":150},{"cohort":"attachment","seed":91505,"cases":150}]} diff --git a/packages/signals/tests/semantics/quality/results.json b/packages/signals/tests/semantics/quality/results.json new file mode 100644 index 000000000..d9c1f5e1d --- /dev/null +++ b/packages/signals/tests/semantics/quality/results.json @@ -0,0 +1 @@ +{"final-original":{"workerHash":"f688dcadb8319ed9db1a9f95bc0520b1d65121154f8810f0f9de5f10da1227ec","historical":true,"runtime":"9fa294fb58e6e0ff8003efdd6fcf9a5e6dba3ef3","budget":500,"inputs":"original","runs":{"baseline":{"initialExecutions":98,"searchExecutions":6701,"verificationExecutions":98,"elapsedMs":27069.623514,"representatives":64,"routes":[{"source":"C32","target":"C17","reason":"source merge and offset","exact":true,"noMoreComplex":true},{"source":"C61","target":"C29","reason":"queued write follows visibility","exact":true,"noMoreComplex":true},{"source":"C76","target":"C21","reason":"delivery and declaration","exact":true,"noMoreComplex":true},{"source":"C85","target":"C67","reason":"action and visibility order","exact":false,"noMoreComplex":false},{"source":"C16","target":"C17","reason":"async helper and observed source","exact":false,"noMoreComplex":false},{"source":"C34","target":"C80","reason":"combined memo and delivery","exact":false,"noMoreComplex":false},{"source":"C88","target":"C10","reason":"delivery enables lifecycle removal","exact":false,"noMoreComplex":false},{"source":"C96","target":"C74","reason":"source read enables synchronous child","exact":false,"noMoreComplex":false}],"rows":[{"id":"C00","complexity":[3,4,2,1],"counted":9},{"id":"C01","complexity":[4,4,1,2],"counted":9},{"id":"C02","complexity":[3,4,2,2],"counted":14},{"id":"C03","complexity":[4,4,1,2],"counted":16},{"id":"C04","complexity":[4,7,4,3],"counted":22},{"id":"C05","complexity":[4,5,2,2],"counted":18},{"id":"C06","complexity":[4,7,4,3],"counted":22},{"id":"C07","complexity":[4,5,2,3],"counted":17},{"id":"C08","complexity":[3,4,2,2],"counted":14},{"id":"C09","complexity":[5,4,1,4],"counted":22},{"id":"C10","complexity":[5,6,2,3],"counted":34},{"id":"C11","complexity":[3,4,2,2],"counted":13},{"id":"C12","complexity":[4,4,1,2],"counted":18},{"id":"C13","complexity":[5,6,2,3],"counted":17},{"id":"C14","complexity":[4,5,2,2],"counted":31},{"id":"C15","complexity":[4,3,1,5],"counted":22},{"id":"C16","complexity":[5,4,1,6],"counted":37},{"id":"C17","complexity":[4,3,1,7],"counted":26},{"id":"C18","complexity":[5,4,1,6],"counted":39},{"id":"C19","complexity":[4,4,1,2],"counted":39},{"id":"C20","complexity":[4,3,1,7],"counted":24},{"id":"C21","complexity":[6,4,1,6],"counted":260},{"id":"C22","complexity":[5,3,2,5],"counted":35},{"id":"C23","complexity":[3,4,2,2],"counted":13},{"id":"C24","complexity":[3,4,2,1],"counted":10},{"id":"C25","complexity":[6,7,3,4],"counted":176},{"id":"C26","complexity":[5,3,2,5],"counted":59},{"id":"C27","complexity":[3,5,3,2],"counted":18},{"id":"C28","complexity":[5,3,2,6],"counted":111},{"id":"C29","complexity":[4,4,2,5],"counted":26},{"id":"C30","complexity":[4,4,1,2],"counted":61},{"id":"C31","complexity":[3,4,2,2],"counted":46},{"id":"C32","complexity":[4,3,1,7],"counted":50},{"id":"C33","complexity":[5,4,2,5],"counted":34},{"id":"C34","complexity":[6,4,2,5],"counted":53},{"id":"C35","complexity":[5,5,2,5],"counted":202},{"id":"C36","complexity":[6,4,1,6],"counted":342},{"id":"C37","complexity":[4,4,1,2],"counted":39},{"id":"C38","complexity":[3,4,2,2],"counted":46},{"id":"C39","complexity":[5,5,3,4],"counted":40},{"id":"C40","complexity":[7,6,1,6],"counted":270},{"id":"C41","complexity":[7,6,1,6],"counted":353},{"id":"C42","complexity":[5,8,4,5],"counted":423},{"id":"C43","complexity":[4,4,1,2],"counted":40},{"id":"C44","complexity":[6,5,2,4],"counted":32},{"id":"C45","complexity":[4,4,1,2],"counted":29},{"id":"C46","complexity":[5,4,3,3],"counted":22},{"id":"C47","complexity":[4,5,2,4],"counted":26},{"id":"C48","complexity":[5,3,2,6],"counted":67},{"id":"C49","complexity":[5,4,3,5],"counted":36},{"id":"C50","complexity":[4,4,1,2],"counted":32},{"id":"C51","complexity":[3,5,3,2],"counted":16},{"id":"C52","complexity":[3,5,6,2],"counted":40},{"id":"C53","complexity":[4,5,3,5],"counted":31},{"id":"C54","complexity":[6,5,2,6],"counted":41},{"id":"C55","complexity":[5,5,3,6],"counted":47},{"id":"C56","complexity":[4,4,1,2],"counted":24},{"id":"C57","complexity":[5,5,2,7],"counted":49},{"id":"C58","complexity":[4,3,1,6],"counted":23},{"id":"C59","complexity":[3,5,7,2],"counted":24},{"id":"C60","complexity":[4,4,1,2],"counted":29},{"id":"C61","complexity":[4,4,2,5],"counted":51},{"id":"C62","complexity":[5,4,1,6],"counted":101},{"id":"C63","complexity":[4,4,4,4],"counted":29},{"id":"C64","complexity":[5,7,3,4],"counted":32},{"id":"C65","complexity":[6,9,5,5],"counted":53},{"id":"C66","complexity":[6,3,1,10],"counted":67},{"id":"C67","complexity":[4,8,4,2],"counted":23},{"id":"C68","complexity":[6,4,2,5],"counted":77},{"id":"C69","complexity":[5,5,2,4],"counted":164},{"id":"C70","complexity":[5,6,4,4],"counted":325},{"id":"C71","complexity":[5,5,3,6],"counted":41},{"id":"C72","complexity":[4,8,5,3],"counted":37},{"id":"C73","complexity":[4,7,3,2],"counted":52},{"id":"C74","complexity":[5,4,2,6],"counted":28},{"id":"C75","complexity":[4,5,4,3],"counted":184},{"id":"C76","complexity":[6,4,1,6],"counted":282},{"id":"C77","complexity":[3,4,2,1],"counted":53},{"id":"C78","complexity":[5,4,2,5],"counted":58},{"id":"C79","complexity":[5,5,2,4],"counted":33},{"id":"C80","complexity":[5,4,2,4],"counted":30},{"id":"C81","complexity":[6,5,2,5],"counted":223},{"id":"C82","complexity":[5,5,3,5],"counted":42},{"id":"C83","complexity":[4,4,1,2],"counted":27},{"id":"C84","complexity":[4,3,1,7],"counted":61},{"id":"C85","complexity":[5,8,4,3],"counted":30},{"id":"C86","complexity":[6,3,1,11],"counted":51},{"id":"C87","complexity":[4,6,3,3],"counted":21},{"id":"C88","complexity":[5,6,4,3],"counted":19},{"id":"C89","complexity":[4,3,1,6],"counted":62},{"id":"C90","complexity":[6,5,2,4],"counted":32},{"id":"C91","complexity":[6,7,2,4],"counted":37},{"id":"C92","complexity":[6,4,1,6],"counted":318},{"id":"C93","complexity":[4,4,1,2],"counted":56},{"id":"C94","complexity":[4,4,1,2],"counted":27},{"id":"C95","complexity":[4,5,6,3],"counted":173},{"id":"C96","complexity":[5,5,2,5],"counted":27}],"familyTotals":{"normalize":{"attempts":25,"accepted":25},"pruning":{"attempts":10,"accepted":9},"anchors":{"attempts":259,"accepted":16},"optimistic":{"attempts":150,"accepted":4},"actions":{"attempts":4,"accepted":0},"sources":{"attempts":0,"accepted":0},"turns":{"attempts":865,"accepted":15},"focused":{"attempts":154,"accepted":9},"arithmetic":{"attempts":24,"accepted":6},"structural":{"attempts":2376,"accepted":80},"steps":{"attempts":490,"accepted":5},"readers":{"attempts":109,"accepted":7},"nodes":{"attempts":94,"accepted":2},"ordering":{"attempts":64,"accepted":47},"edges":{"attempts":450,"accepted":0},"observation":{"attempts":24,"accepted":6},"lifecycle":{"attempts":47,"accepted":2},"cones":{"attempts":3,"accepted":1},"schedule":{"attempts":4,"accepted":2},"branches":{"attempts":9,"accepted":2},"deliverySchedule":{"attempts":1138,"accepted":3},"observationCollapse":{"attempts":88,"accepted":2},"initialVisibility":{"attempts":12,"accepted":1},"callbackEnd":{"attempts":3,"accepted":3},"mergeArithmetic":{"attempts":160,"accepted":1},"deliveryOrdering":{"attempts":65,"accepted":44}}},"current":{"initialExecutions":98,"searchExecutions":6765,"verificationExecutions":98,"elapsedMs":24780.808363000004,"representatives":59,"routes":[{"source":"C32","target":"C17","reason":"source merge and offset","exact":true,"noMoreComplex":true},{"source":"C61","target":"C29","reason":"queued write follows visibility","exact":true,"noMoreComplex":true},{"source":"C76","target":"C21","reason":"delivery and declaration","exact":true,"noMoreComplex":true},{"source":"C85","target":"C67","reason":"action and visibility order","exact":true,"noMoreComplex":true},{"source":"C16","target":"C17","reason":"async helper and observed source","exact":true,"noMoreComplex":true},{"source":"C34","target":"C80","reason":"combined memo and delivery","exact":true,"noMoreComplex":true},{"source":"C88","target":"C10","reason":"delivery enables lifecycle removal","exact":true,"noMoreComplex":true},{"source":"C96","target":"C74","reason":"source read enables synchronous child","exact":true,"noMoreComplex":true}],"rows":[{"id":"C00","complexity":[3,4,2,1],"counted":9,"exhausted":false},{"id":"C01","complexity":[4,4,1,2],"counted":10,"exhausted":false},{"id":"C02","complexity":[3,4,2,2],"counted":15,"exhausted":false},{"id":"C03","complexity":[4,4,1,2],"counted":17,"exhausted":false},{"id":"C04","complexity":[4,7,4,3],"counted":24,"exhausted":false},{"id":"C05","complexity":[4,5,2,2],"counted":19,"exhausted":false},{"id":"C06","complexity":[4,7,4,3],"counted":24,"exhausted":false},{"id":"C07","complexity":[4,5,2,3],"counted":19,"exhausted":false},{"id":"C08","complexity":[3,4,2,2],"counted":15,"exhausted":false},{"id":"C09","complexity":[5,4,1,4],"counted":29,"exhausted":false},{"id":"C10","complexity":[5,6,2,3],"counted":36,"exhausted":false},{"id":"C11","complexity":[3,4,2,2],"counted":14,"exhausted":false},{"id":"C12","complexity":[4,4,1,2],"counted":25,"exhausted":false},{"id":"C13","complexity":[5,6,2,3],"counted":19,"exhausted":false},{"id":"C14","complexity":[4,5,2,2],"counted":32,"exhausted":false},{"id":"C15","complexity":[4,3,1,5],"counted":26,"exhausted":false},{"id":"C16","complexity":[4,3,1,7],"counted":70,"exhausted":false},{"id":"C17","complexity":[4,3,1,7],"counted":52,"exhausted":false},{"id":"C18","complexity":[4,3,1,7],"counted":109,"exhausted":false},{"id":"C19","complexity":[4,4,1,2],"counted":47,"exhausted":false},{"id":"C20","complexity":[4,3,1,7],"counted":26,"exhausted":false},{"id":"C21","complexity":[6,4,1,6],"counted":180,"exhausted":false},{"id":"C22","complexity":[5,3,2,5],"counted":43,"exhausted":false},{"id":"C23","complexity":[3,4,2,2],"counted":15,"exhausted":false},{"id":"C24","complexity":[3,4,2,1],"counted":10,"exhausted":false},{"id":"C25","complexity":[6,7,3,4],"counted":124,"exhausted":false},{"id":"C26","complexity":[5,3,2,5],"counted":78,"exhausted":false},{"id":"C27","complexity":[3,5,3,2],"counted":19,"exhausted":false},{"id":"C28","complexity":[5,3,2,6],"counted":101,"exhausted":false},{"id":"C29","complexity":[4,4,2,5],"counted":55,"exhausted":false},{"id":"C30","complexity":[5,5,2,5],"counted":81,"exhausted":false},{"id":"C31","complexity":[3,4,2,2],"counted":47,"exhausted":false},{"id":"C32","complexity":[4,3,1,7],"counted":52,"exhausted":false},{"id":"C33","complexity":[5,4,2,5],"counted":43,"exhausted":false},{"id":"C34","complexity":[5,4,2,4],"counted":160,"exhausted":false},{"id":"C35","complexity":[5,5,2,5],"counted":182,"exhausted":false},{"id":"C36","complexity":[6,4,1,6],"counted":236,"exhausted":false},{"id":"C37","complexity":[4,4,1,2],"counted":40,"exhausted":false},{"id":"C38","complexity":[3,4,2,2],"counted":47,"exhausted":false},{"id":"C39","complexity":[5,5,3,4],"counted":49,"exhausted":false},{"id":"C40","complexity":[7,6,1,6],"counted":184,"exhausted":false},{"id":"C41","complexity":[7,6,1,6],"counted":238,"exhausted":false},{"id":"C42","complexity":[5,8,4,5],"counted":368,"exhausted":false},{"id":"C43","complexity":[4,4,1,2],"counted":47,"exhausted":false},{"id":"C44","complexity":[6,5,2,4],"counted":36,"exhausted":false},{"id":"C45","complexity":[4,4,1,2],"counted":36,"exhausted":false},{"id":"C46","complexity":[5,4,3,3],"counted":24,"exhausted":false},{"id":"C47","complexity":[4,5,2,4],"counted":31,"exhausted":false},{"id":"C48","complexity":[5,3,2,6],"counted":77,"exhausted":false},{"id":"C49","complexity":[5,4,3,5],"counted":45,"exhausted":false},{"id":"C50","complexity":[4,4,1,2],"counted":37,"exhausted":false},{"id":"C51","complexity":[3,5,3,2],"counted":17,"exhausted":false},{"id":"C52","complexity":[3,5,6,2],"counted":41,"exhausted":false},{"id":"C53","complexity":[4,5,3,5],"counted":65,"exhausted":false},{"id":"C54","complexity":[6,5,2,6],"counted":52,"exhausted":false},{"id":"C55","complexity":[5,5,3,6],"counted":59,"exhausted":false},{"id":"C56","complexity":[4,4,1,2],"counted":33,"exhausted":false},{"id":"C57","complexity":[5,5,2,7],"counted":107,"exhausted":false},{"id":"C58","complexity":[4,3,1,6],"counted":28,"exhausted":false},{"id":"C59","complexity":[3,5,7,2],"counted":25,"exhausted":false},{"id":"C60","complexity":[4,4,1,2],"counted":34,"exhausted":false},{"id":"C61","complexity":[4,4,2,5],"counted":58,"exhausted":false},{"id":"C62","complexity":[4,3,1,7],"counted":146,"exhausted":false},{"id":"C63","complexity":[4,4,4,4],"counted":31,"exhausted":false},{"id":"C64","complexity":[5,7,3,4],"counted":40,"exhausted":false},{"id":"C65","complexity":[6,9,5,5],"counted":66,"exhausted":false},{"id":"C66","complexity":[6,3,1,10],"counted":86,"exhausted":false},{"id":"C67","complexity":[4,8,4,2],"counted":24,"exhausted":false},{"id":"C68","complexity":[5,4,2,4],"counted":184,"exhausted":false},{"id":"C69","complexity":[5,5,2,4],"counted":115,"exhausted":false},{"id":"C70","complexity":[5,6,4,4],"counted":163,"exhausted":false},{"id":"C71","complexity":[5,5,3,6],"counted":49,"exhausted":false},{"id":"C72","complexity":[4,8,5,3],"counted":42,"exhausted":false},{"id":"C73","complexity":[4,7,3,2],"counted":78,"exhausted":false},{"id":"C74","complexity":[5,4,2,6],"counted":31,"exhausted":false},{"id":"C75","complexity":[4,5,4,3],"counted":165,"exhausted":false},{"id":"C76","complexity":[6,4,1,6],"counted":169,"exhausted":false},{"id":"C77","complexity":[4,5,2,2],"counted":51,"exhausted":false},{"id":"C78","complexity":[5,4,2,5],"counted":63,"exhausted":false},{"id":"C79","complexity":[5,5,2,4],"counted":43,"exhausted":false},{"id":"C80","complexity":[5,4,2,4],"counted":43,"exhausted":false},{"id":"C81","complexity":[6,5,2,5],"counted":172,"exhausted":false},{"id":"C82","complexity":[5,5,3,5],"counted":42,"exhausted":false},{"id":"C83","complexity":[4,4,1,2],"counted":27,"exhausted":false},{"id":"C84","complexity":[4,3,1,7],"counted":67,"exhausted":false},{"id":"C85","complexity":[4,8,4,2],"counted":69,"exhausted":false},{"id":"C86","complexity":[6,3,1,11],"counted":83,"exhausted":false},{"id":"C87","complexity":[4,6,3,3],"counted":23,"exhausted":false},{"id":"C88","complexity":[5,6,2,3],"counted":80,"exhausted":false},{"id":"C89","complexity":[4,3,1,6],"counted":71,"exhausted":false},{"id":"C90","complexity":[6,5,2,4],"counted":37,"exhausted":false},{"id":"C91","complexity":[6,7,2,4],"counted":84,"exhausted":false},{"id":"C92","complexity":[6,4,1,6],"counted":224,"exhausted":false},{"id":"C93","complexity":[4,4,1,2],"counted":50,"exhausted":false},{"id":"C94","complexity":[4,4,1,2],"counted":34,"exhausted":false},{"id":"C95","complexity":[4,5,6,3],"counted":183,"exhausted":false},{"id":"C96","complexity":[5,4,2,6],"counted":89,"exhausted":false}],"familyTotals":{"anchors":{"attempts":175,"accepted":16},"optimistic":{"attempts":98,"accepted":4},"turns":{"attempts":622,"accepted":14},"structural":{"attempts":1927,"accepted":75},"steps":{"attempts":427,"accepted":7},"readers":{"attempts":69,"accepted":5},"lifecycle":{"attempts":56,"accepted":3},"observationCollapse":{"attempts":80,"accepted":3},"edges":{"attempts":426,"accepted":4},"cuts":{"attempts":923,"accepted":7},"deliverySchedule":{"attempts":1227,"accepted":6},"actions":{"attempts":4,"accepted":0},"focused":{"attempts":145,"accepted":9},"observation":{"attempts":33,"accepted":7},"mergeArithmetic":{"attempts":172,"accepted":1},"initialVisibility":{"attempts":16,"accepted":1},"deliveryOrdering":{"attempts":119,"accepted":39},"normalize":{"attempts":23,"accepted":23},"nodes":{"attempts":111,"accepted":2},"ordering":{"attempts":33,"accepted":18},"branches":{"attempts":10,"accepted":2},"schedule":{"attempts":5,"accepted":3},"arithmetic":{"attempts":16,"accepted":4},"cones":{"attempts":3,"accepted":1},"callbackEnd":{"attempts":3,"accepted":3},"deliveryEscape":{"attempts":41,"accepted":1},"pruning":{"attempts":1,"accepted":0}}}}},"final-reduced":{"workerHash":"f688dcadb8319ed9db1a9f95bc0520b1d65121154f8810f0f9de5f10da1227ec","historical":true,"runtime":"9fa294fb58e6e0ff8003efdd6fcf9a5e6dba3ef3","budget":500,"inputs":"reduced","runs":{"current":{"initialExecutions":98,"searchExecutions":5173,"verificationExecutions":98,"elapsedMs":17842.230207,"representatives":59,"routes":[{"source":"C32","target":"C17","reason":"source merge and offset","exact":true,"noMoreComplex":true},{"source":"C61","target":"C29","reason":"queued write follows visibility","exact":true,"noMoreComplex":true},{"source":"C76","target":"C21","reason":"delivery and declaration","exact":true,"noMoreComplex":true},{"source":"C85","target":"C67","reason":"action and visibility order","exact":true,"noMoreComplex":true},{"source":"C16","target":"C17","reason":"async helper and observed source","exact":true,"noMoreComplex":true},{"source":"C34","target":"C80","reason":"combined memo and delivery","exact":true,"noMoreComplex":true},{"source":"C88","target":"C10","reason":"delivery enables lifecycle removal","exact":true,"noMoreComplex":true},{"source":"C96","target":"C74","reason":"source read enables synchronous child","exact":true,"noMoreComplex":true}],"rows":[{"id":"C00","complexity":[3,4,2,1],"counted":9,"exhausted":false},{"id":"C01","complexity":[4,4,1,2],"counted":10,"exhausted":false},{"id":"C02","complexity":[3,4,2,2],"counted":15,"exhausted":false},{"id":"C03","complexity":[4,4,1,2],"counted":10,"exhausted":false},{"id":"C04","complexity":[4,7,4,3],"counted":24,"exhausted":false},{"id":"C05","complexity":[4,5,2,2],"counted":13,"exhausted":false},{"id":"C06","complexity":[4,7,4,3],"counted":24,"exhausted":false},{"id":"C07","complexity":[4,5,2,3],"counted":19,"exhausted":false},{"id":"C08","complexity":[3,4,2,2],"counted":15,"exhausted":false},{"id":"C09","complexity":[5,4,1,4],"counted":29,"exhausted":false},{"id":"C10","complexity":[5,6,2,3],"counted":19,"exhausted":false},{"id":"C11","complexity":[3,4,2,2],"counted":14,"exhausted":false},{"id":"C12","complexity":[4,4,1,2],"counted":10,"exhausted":false},{"id":"C13","complexity":[5,6,2,3],"counted":19,"exhausted":false},{"id":"C14","complexity":[4,5,2,2],"counted":13,"exhausted":false},{"id":"C15","complexity":[4,3,1,5],"counted":26,"exhausted":false},{"id":"C16","complexity":[4,3,1,7],"counted":70,"exhausted":false},{"id":"C17","complexity":[4,3,1,7],"counted":26,"exhausted":false},{"id":"C18","complexity":[4,3,1,7],"counted":70,"exhausted":false},{"id":"C19","complexity":[4,4,1,2],"counted":10,"exhausted":false},{"id":"C20","complexity":[4,3,1,7],"counted":26,"exhausted":false},{"id":"C21","complexity":[6,4,1,6],"counted":169,"exhausted":false},{"id":"C22","complexity":[5,3,2,5],"counted":43,"exhausted":false},{"id":"C23","complexity":[3,4,2,2],"counted":15,"exhausted":false},{"id":"C24","complexity":[3,4,2,1],"counted":10,"exhausted":false},{"id":"C25","complexity":[6,7,3,4],"counted":124,"exhausted":false},{"id":"C26","complexity":[5,3,2,5],"counted":43,"exhausted":false},{"id":"C27","complexity":[3,5,3,2],"counted":19,"exhausted":false},{"id":"C28","complexity":[5,3,2,6],"counted":48,"exhausted":false},{"id":"C29","complexity":[4,4,2,5],"counted":29,"exhausted":false},{"id":"C30","complexity":[4,4,1,2],"counted":10,"exhausted":false},{"id":"C31","complexity":[3,4,2,2],"counted":15,"exhausted":false},{"id":"C32","complexity":[4,3,1,7],"counted":26,"exhausted":false},{"id":"C33","complexity":[5,4,2,5],"counted":43,"exhausted":false},{"id":"C34","complexity":[5,4,2,4],"counted":160,"exhausted":false},{"id":"C35","complexity":[5,5,2,5],"counted":182,"exhausted":false},{"id":"C36","complexity":[6,4,1,6],"counted":169,"exhausted":false},{"id":"C37","complexity":[4,4,1,2],"counted":10,"exhausted":false},{"id":"C38","complexity":[3,4,2,2],"counted":15,"exhausted":false},{"id":"C39","complexity":[5,5,3,4],"counted":49,"exhausted":false},{"id":"C40","complexity":[7,6,1,6],"counted":174,"exhausted":false},{"id":"C41","complexity":[7,6,1,6],"counted":174,"exhausted":false},{"id":"C42","complexity":[5,8,4,5],"counted":180,"exhausted":false},{"id":"C43","complexity":[4,4,1,2],"counted":10,"exhausted":false},{"id":"C44","complexity":[6,5,2,4],"counted":36,"exhausted":false},{"id":"C45","complexity":[4,4,1,2],"counted":10,"exhausted":false},{"id":"C46","complexity":[5,4,3,3],"counted":24,"exhausted":false},{"id":"C47","complexity":[4,5,2,4],"counted":31,"exhausted":false},{"id":"C48","complexity":[5,3,2,6],"counted":48,"exhausted":false},{"id":"C49","complexity":[5,4,3,5],"counted":45,"exhausted":false},{"id":"C50","complexity":[4,4,1,2],"counted":10,"exhausted":false},{"id":"C51","complexity":[3,5,3,2],"counted":17,"exhausted":false},{"id":"C52","complexity":[3,5,6,2],"counted":21,"exhausted":false},{"id":"C53","complexity":[4,5,3,5],"counted":34,"exhausted":false},{"id":"C54","complexity":[6,5,2,6],"counted":52,"exhausted":false},{"id":"C55","complexity":[5,5,3,6],"counted":59,"exhausted":false},{"id":"C56","complexity":[4,4,1,2],"counted":10,"exhausted":false},{"id":"C57","complexity":[5,5,2,7],"counted":58,"exhausted":false},{"id":"C58","complexity":[4,3,1,6],"counted":28,"exhausted":false},{"id":"C59","complexity":[3,5,7,2],"counted":25,"exhausted":false},{"id":"C60","complexity":[4,4,1,2],"counted":10,"exhausted":false},{"id":"C61","complexity":[4,4,2,5],"counted":29,"exhausted":false},{"id":"C62","complexity":[4,3,1,7],"counted":70,"exhausted":false},{"id":"C63","complexity":[4,4,4,4],"counted":31,"exhausted":false},{"id":"C64","complexity":[5,7,3,4],"counted":40,"exhausted":false},{"id":"C65","complexity":[6,9,5,5],"counted":66,"exhausted":false},{"id":"C66","complexity":[6,3,1,10],"counted":86,"exhausted":false},{"id":"C67","complexity":[4,8,4,2],"counted":24,"exhausted":false},{"id":"C68","complexity":[5,4,2,4],"counted":160,"exhausted":false},{"id":"C69","complexity":[5,5,2,4],"counted":115,"exhausted":false},{"id":"C70","complexity":[5,6,4,4],"counted":164,"exhausted":false},{"id":"C71","complexity":[5,5,3,6],"counted":49,"exhausted":false},{"id":"C72","complexity":[4,8,5,3],"counted":42,"exhausted":false},{"id":"C73","complexity":[4,7,3,2],"counted":19,"exhausted":false},{"id":"C74","complexity":[5,4,2,6],"counted":31,"exhausted":false},{"id":"C75","complexity":[4,5,4,3],"counted":165,"exhausted":false},{"id":"C76","complexity":[6,4,1,6],"counted":169,"exhausted":false},{"id":"C77","complexity":[3,4,2,1],"counted":10,"exhausted":false},{"id":"C78","complexity":[5,4,2,5],"counted":43,"exhausted":false},{"id":"C79","complexity":[5,5,2,4],"counted":43,"exhausted":false},{"id":"C80","complexity":[5,4,2,4],"counted":43,"exhausted":false},{"id":"C81","complexity":[6,5,2,5],"counted":172,"exhausted":false},{"id":"C82","complexity":[5,5,3,5],"counted":42,"exhausted":false},{"id":"C83","complexity":[4,4,1,2],"counted":10,"exhausted":false},{"id":"C84","complexity":[4,3,1,7],"counted":26,"exhausted":false},{"id":"C85","complexity":[4,8,4,2],"counted":69,"exhausted":false},{"id":"C86","complexity":[6,3,1,11],"counted":83,"exhausted":false},{"id":"C87","complexity":[4,6,3,3],"counted":23,"exhausted":false},{"id":"C88","complexity":[5,6,2,3],"counted":80,"exhausted":false},{"id":"C89","complexity":[4,3,1,6],"counted":28,"exhausted":false},{"id":"C90","complexity":[6,5,2,4],"counted":37,"exhausted":false},{"id":"C91","complexity":[6,7,2,4],"counted":84,"exhausted":false},{"id":"C92","complexity":[6,4,1,6],"counted":169,"exhausted":false},{"id":"C93","complexity":[4,4,1,2],"counted":10,"exhausted":false},{"id":"C94","complexity":[4,4,1,2],"counted":10,"exhausted":false},{"id":"C95","complexity":[4,5,6,3],"counted":136,"exhausted":false},{"id":"C96","complexity":[5,4,2,6],"counted":89,"exhausted":false}],"familyTotals":{"anchors":{"attempts":131,"accepted":1},"optimistic":{"attempts":55,"accepted":0},"turns":{"attempts":485,"accepted":0},"structural":{"attempts":1378,"accepted":42},"steps":{"attempts":322,"accepted":0},"readers":{"attempts":49,"accepted":0},"lifecycle":{"attempts":41,"accepted":0},"observationCollapse":{"attempts":63,"accepted":0},"edges":{"attempts":325,"accepted":4},"cuts":{"attempts":864,"accepted":6},"actions":{"attempts":4,"accepted":0},"focused":{"attempts":112,"accepted":0},"observation":{"attempts":24,"accepted":0},"mergeArithmetic":{"attempts":171,"accepted":0},"initialVisibility":{"attempts":10,"accepted":0},"deliverySchedule":{"attempts":858,"accepted":1},"deliveryOrdering":{"attempts":120,"accepted":41},"nodes":{"attempts":72,"accepted":0},"normalize":{"attempts":3,"accepted":3},"ordering":{"attempts":29,"accepted":13},"branches":{"attempts":6,"accepted":0},"arithmetic":{"attempts":8,"accepted":0},"deliveryEscape":{"attempts":41,"accepted":1},"pruning":{"attempts":1,"accepted":0},"schedule":{"attempts":1,"accepted":0}}}}},"sweep":{"workerHash":"f688dcadb8319ed9db1a9f95bc0520b1d65121154f8810f0f9de5f10da1227ec","historical":true,"runtime":"9fa294fb58e6e0ff8003efdd6fcf9a5e6dba3ef3","budget":500,"inputs":"reduced","runs":{"current":{"initialExecutions":98,"searchExecutions":3967,"verificationExecutions":98,"elapsedMs":15444.447655,"representatives":64,"routes":[{"source":"C32","target":"C17","reason":"source merge and offset","exact":true,"noMoreComplex":true},{"source":"C61","target":"C29","reason":"queued write follows visibility","exact":true,"noMoreComplex":true},{"source":"C76","target":"C21","reason":"delivery and declaration","exact":true,"noMoreComplex":true},{"source":"C85","target":"C67","reason":"action and visibility order","exact":false,"noMoreComplex":false},{"source":"C16","target":"C17","reason":"async helper and observed source","exact":false,"noMoreComplex":false},{"source":"C34","target":"C80","reason":"combined memo and delivery","exact":false,"noMoreComplex":false},{"source":"C88","target":"C10","reason":"delivery enables lifecycle removal","exact":false,"noMoreComplex":false},{"source":"C96","target":"C74","reason":"source read enables synchronous child","exact":false,"noMoreComplex":false}],"rows":[{"id":"C00","complexity":[3,4,2,1],"counted":9,"exhausted":false},{"id":"C01","complexity":[4,4,1,2],"counted":9,"exhausted":false},{"id":"C02","complexity":[3,4,2,2],"counted":14,"exhausted":false},{"id":"C03","complexity":[4,4,1,2],"counted":9,"exhausted":false},{"id":"C04","complexity":[4,7,4,3],"counted":22,"exhausted":false},{"id":"C05","complexity":[4,5,2,2],"counted":12,"exhausted":false},{"id":"C06","complexity":[4,7,4,3],"counted":22,"exhausted":false},{"id":"C07","complexity":[4,5,2,3],"counted":17,"exhausted":false},{"id":"C08","complexity":[3,4,2,2],"counted":14,"exhausted":false},{"id":"C09","complexity":[5,4,1,4],"counted":22,"exhausted":false},{"id":"C10","complexity":[5,6,2,3],"counted":17,"exhausted":false},{"id":"C11","complexity":[3,4,2,2],"counted":13,"exhausted":false},{"id":"C12","complexity":[4,4,1,2],"counted":9,"exhausted":false},{"id":"C13","complexity":[5,6,2,3],"counted":17,"exhausted":false},{"id":"C14","complexity":[4,5,2,2],"counted":12,"exhausted":false},{"id":"C15","complexity":[4,3,1,5],"counted":22,"exhausted":false},{"id":"C16","complexity":[5,4,1,6],"counted":37,"exhausted":false},{"id":"C17","complexity":[4,3,1,7],"counted":24,"exhausted":false},{"id":"C18","complexity":[5,4,1,6],"counted":37,"exhausted":false},{"id":"C19","complexity":[4,4,1,2],"counted":9,"exhausted":false},{"id":"C20","complexity":[4,3,1,7],"counted":24,"exhausted":false},{"id":"C21","complexity":[6,4,1,6],"counted":131,"exhausted":false},{"id":"C22","complexity":[5,3,2,5],"counted":35,"exhausted":false},{"id":"C23","complexity":[3,4,2,2],"counted":13,"exhausted":false},{"id":"C24","complexity":[3,4,2,1],"counted":10,"exhausted":false},{"id":"C25","complexity":[6,7,3,4],"counted":100,"exhausted":false},{"id":"C26","complexity":[5,3,2,5],"counted":35,"exhausted":false},{"id":"C27","complexity":[3,5,3,2],"counted":18,"exhausted":false},{"id":"C28","complexity":[5,3,2,6],"counted":38,"exhausted":false},{"id":"C29","complexity":[4,4,2,5],"counted":24,"exhausted":false},{"id":"C30","complexity":[4,4,1,2],"counted":9,"exhausted":false},{"id":"C31","complexity":[3,4,2,2],"counted":14,"exhausted":false},{"id":"C32","complexity":[4,3,1,7],"counted":24,"exhausted":false},{"id":"C33","complexity":[5,4,2,5],"counted":34,"exhausted":false},{"id":"C34","complexity":[6,4,2,5],"counted":53,"exhausted":false},{"id":"C35","complexity":[5,5,2,5],"counted":175,"exhausted":false},{"id":"C36","complexity":[6,4,1,6],"counted":131,"exhausted":false},{"id":"C37","complexity":[4,4,1,2],"counted":9,"exhausted":false},{"id":"C38","complexity":[3,4,2,2],"counted":14,"exhausted":false},{"id":"C39","complexity":[5,5,3,4],"counted":40,"exhausted":false},{"id":"C40","complexity":[7,6,1,6],"counted":136,"exhausted":false},{"id":"C41","complexity":[7,6,1,6],"counted":136,"exhausted":false},{"id":"C42","complexity":[5,8,4,5],"counted":152,"exhausted":false},{"id":"C43","complexity":[4,4,1,2],"counted":9,"exhausted":false},{"id":"C44","complexity":[6,5,2,4],"counted":32,"exhausted":false},{"id":"C45","complexity":[4,4,1,2],"counted":9,"exhausted":false},{"id":"C46","complexity":[5,4,3,3],"counted":22,"exhausted":false},{"id":"C47","complexity":[4,5,2,4],"counted":26,"exhausted":false},{"id":"C48","complexity":[5,3,2,6],"counted":38,"exhausted":false},{"id":"C49","complexity":[5,4,3,5],"counted":36,"exhausted":false},{"id":"C50","complexity":[4,4,1,2],"counted":9,"exhausted":false},{"id":"C51","complexity":[3,5,3,2],"counted":16,"exhausted":false},{"id":"C52","complexity":[3,5,6,2],"counted":20,"exhausted":false},{"id":"C53","complexity":[4,5,3,5],"counted":29,"exhausted":false},{"id":"C54","complexity":[6,5,2,6],"counted":41,"exhausted":false},{"id":"C55","complexity":[5,5,3,6],"counted":47,"exhausted":false},{"id":"C56","complexity":[4,4,1,2],"counted":9,"exhausted":false},{"id":"C57","complexity":[5,5,2,7],"counted":47,"exhausted":false},{"id":"C58","complexity":[4,3,1,6],"counted":23,"exhausted":false},{"id":"C59","complexity":[3,5,7,2],"counted":24,"exhausted":false},{"id":"C60","complexity":[4,4,1,2],"counted":9,"exhausted":false},{"id":"C61","complexity":[4,4,2,5],"counted":24,"exhausted":false},{"id":"C62","complexity":[5,4,1,6],"counted":37,"exhausted":false},{"id":"C63","complexity":[4,4,4,4],"counted":29,"exhausted":false},{"id":"C64","complexity":[5,7,3,4],"counted":32,"exhausted":false},{"id":"C65","complexity":[6,9,5,5],"counted":53,"exhausted":false},{"id":"C66","complexity":[6,3,1,10],"counted":67,"exhausted":false},{"id":"C67","complexity":[4,8,4,2],"counted":23,"exhausted":false},{"id":"C68","complexity":[6,4,2,5],"counted":53,"exhausted":false},{"id":"C69","complexity":[5,5,2,4],"counted":91,"exhausted":false},{"id":"C70","complexity":[5,6,4,4],"counted":127,"exhausted":false},{"id":"C71","complexity":[5,5,3,6],"counted":41,"exhausted":false},{"id":"C72","complexity":[4,8,5,3],"counted":37,"exhausted":false},{"id":"C73","complexity":[4,7,3,2],"counted":17,"exhausted":false},{"id":"C74","complexity":[5,4,2,6],"counted":28,"exhausted":false},{"id":"C75","complexity":[4,5,4,3],"counted":149,"exhausted":false},{"id":"C76","complexity":[6,4,1,6],"counted":131,"exhausted":false},{"id":"C77","complexity":[3,4,2,1],"counted":10,"exhausted":false},{"id":"C78","complexity":[5,4,2,5],"counted":34,"exhausted":false},{"id":"C79","complexity":[5,5,2,4],"counted":33,"exhausted":false},{"id":"C80","complexity":[5,4,2,4],"counted":30,"exhausted":false},{"id":"C81","complexity":[6,5,2,5],"counted":151,"exhausted":false},{"id":"C82","complexity":[5,5,3,5],"counted":42,"exhausted":false},{"id":"C83","complexity":[4,4,1,2],"counted":9,"exhausted":false},{"id":"C84","complexity":[4,3,1,7],"counted":24,"exhausted":false},{"id":"C85","complexity":[5,8,4,3],"counted":30,"exhausted":false},{"id":"C86","complexity":[6,3,1,11],"counted":51,"exhausted":false},{"id":"C87","complexity":[4,6,3,3],"counted":21,"exhausted":false},{"id":"C88","complexity":[5,6,4,3],"counted":19,"exhausted":false},{"id":"C89","complexity":[4,3,1,6],"counted":23,"exhausted":false},{"id":"C90","complexity":[6,5,2,4],"counted":32,"exhausted":false},{"id":"C91","complexity":[6,7,2,4],"counted":74,"exhausted":false},{"id":"C92","complexity":[6,4,1,6],"counted":131,"exhausted":false},{"id":"C93","complexity":[4,4,1,2],"counted":9,"exhausted":false},{"id":"C94","complexity":[4,4,1,2],"counted":9,"exhausted":false},{"id":"C95","complexity":[4,5,6,3],"counted":121,"exhausted":false},{"id":"C96","complexity":[5,5,2,5],"counted":27,"exhausted":false}],"familyTotals":{"anchors":{"attempts":115,"accepted":0},"optimistic":{"attempts":48,"accepted":0},"turns":{"attempts":447,"accepted":0},"structural":{"attempts":1296,"accepted":41},"steps":{"attempts":297,"accepted":0},"readers":{"attempts":47,"accepted":0},"lifecycle":{"attempts":39,"accepted":0},"observationCollapse":{"attempts":60,"accepted":0},"edges":{"attempts":300,"accepted":0},"actions":{"attempts":4,"accepted":0},"focused":{"attempts":99,"accepted":0},"observation":{"attempts":15,"accepted":0},"mergeArithmetic":{"attempts":147,"accepted":0},"initialVisibility":{"attempts":10,"accepted":0},"deliverySchedule":{"attempts":819,"accepted":0},"deliveryOrdering":{"attempts":120,"accepted":41},"nodes":{"attempts":63,"accepted":0},"ordering":{"attempts":25,"accepted":9},"branches":{"attempts":6,"accepted":0},"arithmetic":{"attempts":8,"accepted":0},"pruning":{"attempts":1,"accepted":0},"schedule":{"attempts":1,"accepted":0}},"initialPairedReclassified":2}}},"originals":{"workerHash":"f688dcadb8319ed9db1a9f95bc0520b1d65121154f8810f0f9de5f10da1227ec","historical":true,"runtime":"9fa294fb58e6e0ff8003efdd6fcf9a5e6dba3ef3","budget":500,"inputs":"original","runs":{"baseline":{"initialExecutions":98,"searchExecutions":6701,"verificationExecutions":98,"elapsedMs":27035.888889,"representatives":64,"routes":[{"source":"C32","target":"C17","reason":"source merge and offset","exact":true,"noMoreComplex":true},{"source":"C61","target":"C29","reason":"queued write follows visibility","exact":true,"noMoreComplex":true},{"source":"C76","target":"C21","reason":"delivery and declaration","exact":true,"noMoreComplex":true},{"source":"C85","target":"C67","reason":"action and visibility order","exact":false,"noMoreComplex":false},{"source":"C16","target":"C17","reason":"async helper and observed source","exact":false,"noMoreComplex":false},{"source":"C34","target":"C80","reason":"combined memo and delivery","exact":false,"noMoreComplex":false},{"source":"C88","target":"C10","reason":"delivery enables lifecycle removal","exact":false,"noMoreComplex":false},{"source":"C96","target":"C74","reason":"source read enables synchronous child","exact":false,"noMoreComplex":false}],"rows":[{"id":"C00","complexity":[3,4,2,1],"counted":9},{"id":"C01","complexity":[4,4,1,2],"counted":9},{"id":"C02","complexity":[3,4,2,2],"counted":14},{"id":"C03","complexity":[4,4,1,2],"counted":16},{"id":"C04","complexity":[4,7,4,3],"counted":22},{"id":"C05","complexity":[4,5,2,2],"counted":18},{"id":"C06","complexity":[4,7,4,3],"counted":22},{"id":"C07","complexity":[4,5,2,3],"counted":17},{"id":"C08","complexity":[3,4,2,2],"counted":14},{"id":"C09","complexity":[5,4,1,4],"counted":22},{"id":"C10","complexity":[5,6,2,3],"counted":34},{"id":"C11","complexity":[3,4,2,2],"counted":13},{"id":"C12","complexity":[4,4,1,2],"counted":18},{"id":"C13","complexity":[5,6,2,3],"counted":17},{"id":"C14","complexity":[4,5,2,2],"counted":31},{"id":"C15","complexity":[4,3,1,5],"counted":22},{"id":"C16","complexity":[5,4,1,6],"counted":37},{"id":"C17","complexity":[4,3,1,7],"counted":26},{"id":"C18","complexity":[5,4,1,6],"counted":39},{"id":"C19","complexity":[4,4,1,2],"counted":39},{"id":"C20","complexity":[4,3,1,7],"counted":24},{"id":"C21","complexity":[6,4,1,6],"counted":260},{"id":"C22","complexity":[5,3,2,5],"counted":35},{"id":"C23","complexity":[3,4,2,2],"counted":13},{"id":"C24","complexity":[3,4,2,1],"counted":10},{"id":"C25","complexity":[6,7,3,4],"counted":176},{"id":"C26","complexity":[5,3,2,5],"counted":59},{"id":"C27","complexity":[3,5,3,2],"counted":18},{"id":"C28","complexity":[5,3,2,6],"counted":111},{"id":"C29","complexity":[4,4,2,5],"counted":26},{"id":"C30","complexity":[4,4,1,2],"counted":61},{"id":"C31","complexity":[3,4,2,2],"counted":46},{"id":"C32","complexity":[4,3,1,7],"counted":50},{"id":"C33","complexity":[5,4,2,5],"counted":34},{"id":"C34","complexity":[6,4,2,5],"counted":53},{"id":"C35","complexity":[5,5,2,5],"counted":202},{"id":"C36","complexity":[6,4,1,6],"counted":342},{"id":"C37","complexity":[4,4,1,2],"counted":39},{"id":"C38","complexity":[3,4,2,2],"counted":46},{"id":"C39","complexity":[5,5,3,4],"counted":40},{"id":"C40","complexity":[7,6,1,6],"counted":270},{"id":"C41","complexity":[7,6,1,6],"counted":353},{"id":"C42","complexity":[5,8,4,5],"counted":423},{"id":"C43","complexity":[4,4,1,2],"counted":40},{"id":"C44","complexity":[6,5,2,4],"counted":32},{"id":"C45","complexity":[4,4,1,2],"counted":29},{"id":"C46","complexity":[5,4,3,3],"counted":22},{"id":"C47","complexity":[4,5,2,4],"counted":26},{"id":"C48","complexity":[5,3,2,6],"counted":67},{"id":"C49","complexity":[5,4,3,5],"counted":36},{"id":"C50","complexity":[4,4,1,2],"counted":32},{"id":"C51","complexity":[3,5,3,2],"counted":16},{"id":"C52","complexity":[3,5,6,2],"counted":40},{"id":"C53","complexity":[4,5,3,5],"counted":31},{"id":"C54","complexity":[6,5,2,6],"counted":41},{"id":"C55","complexity":[5,5,3,6],"counted":47},{"id":"C56","complexity":[4,4,1,2],"counted":24},{"id":"C57","complexity":[5,5,2,7],"counted":49},{"id":"C58","complexity":[4,3,1,6],"counted":23},{"id":"C59","complexity":[3,5,7,2],"counted":24},{"id":"C60","complexity":[4,4,1,2],"counted":29},{"id":"C61","complexity":[4,4,2,5],"counted":51},{"id":"C62","complexity":[5,4,1,6],"counted":101},{"id":"C63","complexity":[4,4,4,4],"counted":29},{"id":"C64","complexity":[5,7,3,4],"counted":32},{"id":"C65","complexity":[6,9,5,5],"counted":53},{"id":"C66","complexity":[6,3,1,10],"counted":67},{"id":"C67","complexity":[4,8,4,2],"counted":23},{"id":"C68","complexity":[6,4,2,5],"counted":77},{"id":"C69","complexity":[5,5,2,4],"counted":164},{"id":"C70","complexity":[5,6,4,4],"counted":325},{"id":"C71","complexity":[5,5,3,6],"counted":41},{"id":"C72","complexity":[4,8,5,3],"counted":37},{"id":"C73","complexity":[4,7,3,2],"counted":52},{"id":"C74","complexity":[5,4,2,6],"counted":28},{"id":"C75","complexity":[4,5,4,3],"counted":184},{"id":"C76","complexity":[6,4,1,6],"counted":282},{"id":"C77","complexity":[3,4,2,1],"counted":53},{"id":"C78","complexity":[5,4,2,5],"counted":58},{"id":"C79","complexity":[5,5,2,4],"counted":33},{"id":"C80","complexity":[5,4,2,4],"counted":30},{"id":"C81","complexity":[6,5,2,5],"counted":223},{"id":"C82","complexity":[5,5,3,5],"counted":42},{"id":"C83","complexity":[4,4,1,2],"counted":27},{"id":"C84","complexity":[4,3,1,7],"counted":61},{"id":"C85","complexity":[5,8,4,3],"counted":30},{"id":"C86","complexity":[6,3,1,11],"counted":51},{"id":"C87","complexity":[4,6,3,3],"counted":21},{"id":"C88","complexity":[5,6,4,3],"counted":19},{"id":"C89","complexity":[4,3,1,6],"counted":62},{"id":"C90","complexity":[6,5,2,4],"counted":32},{"id":"C91","complexity":[6,7,2,4],"counted":37},{"id":"C92","complexity":[6,4,1,6],"counted":318},{"id":"C93","complexity":[4,4,1,2],"counted":56},{"id":"C94","complexity":[4,4,1,2],"counted":27},{"id":"C95","complexity":[4,5,6,3],"counted":173},{"id":"C96","complexity":[5,5,2,5],"counted":27}],"familyTotals":{"normalize":{"attempts":25,"accepted":25},"pruning":{"attempts":10,"accepted":9},"anchors":{"attempts":259,"accepted":16},"optimistic":{"attempts":150,"accepted":4},"actions":{"attempts":4,"accepted":0},"sources":{"attempts":0,"accepted":0},"turns":{"attempts":865,"accepted":15},"focused":{"attempts":154,"accepted":9},"arithmetic":{"attempts":24,"accepted":6},"structural":{"attempts":2376,"accepted":80},"steps":{"attempts":490,"accepted":5},"readers":{"attempts":109,"accepted":7},"nodes":{"attempts":94,"accepted":2},"ordering":{"attempts":64,"accepted":47},"edges":{"attempts":450,"accepted":0},"observation":{"attempts":24,"accepted":6},"lifecycle":{"attempts":47,"accepted":2},"cones":{"attempts":3,"accepted":1},"schedule":{"attempts":4,"accepted":2},"branches":{"attempts":9,"accepted":2},"deliverySchedule":{"attempts":1138,"accepted":3},"observationCollapse":{"attempts":88,"accepted":2},"initialVisibility":{"attempts":12,"accepted":1},"callbackEnd":{"attempts":3,"accepted":3},"mergeArithmetic":{"attempts":160,"accepted":1},"deliveryOrdering":{"attempts":65,"accepted":44}},"initialPairedReclassified":2},"current":{"initialExecutions":98,"searchExecutions":5379,"verificationExecutions":98,"elapsedMs":21748.95384,"representatives":64,"routes":[{"source":"C32","target":"C17","reason":"source merge and offset","exact":true,"noMoreComplex":true},{"source":"C61","target":"C29","reason":"queued write follows visibility","exact":true,"noMoreComplex":true},{"source":"C76","target":"C21","reason":"delivery and declaration","exact":true,"noMoreComplex":true},{"source":"C85","target":"C67","reason":"action and visibility order","exact":false,"noMoreComplex":false},{"source":"C16","target":"C17","reason":"async helper and observed source","exact":false,"noMoreComplex":false},{"source":"C34","target":"C80","reason":"combined memo and delivery","exact":false,"noMoreComplex":false},{"source":"C88","target":"C10","reason":"delivery enables lifecycle removal","exact":false,"noMoreComplex":false},{"source":"C96","target":"C74","reason":"source read enables synchronous child","exact":false,"noMoreComplex":false}],"rows":[{"id":"C00","complexity":[3,4,2,1],"counted":9,"exhausted":false},{"id":"C01","complexity":[4,4,1,2],"counted":9,"exhausted":false},{"id":"C02","complexity":[3,4,2,2],"counted":14,"exhausted":false},{"id":"C03","complexity":[4,4,1,2],"counted":16,"exhausted":false},{"id":"C04","complexity":[4,7,4,3],"counted":22,"exhausted":false},{"id":"C05","complexity":[4,5,2,2],"counted":18,"exhausted":false},{"id":"C06","complexity":[4,7,4,3],"counted":22,"exhausted":false},{"id":"C07","complexity":[4,5,2,3],"counted":17,"exhausted":false},{"id":"C08","complexity":[3,4,2,2],"counted":14,"exhausted":false},{"id":"C09","complexity":[5,4,1,4],"counted":22,"exhausted":false},{"id":"C10","complexity":[5,6,2,3],"counted":34,"exhausted":false},{"id":"C11","complexity":[3,4,2,2],"counted":13,"exhausted":false},{"id":"C12","complexity":[4,4,1,2],"counted":24,"exhausted":false},{"id":"C13","complexity":[5,6,2,3],"counted":17,"exhausted":false},{"id":"C14","complexity":[4,5,2,2],"counted":31,"exhausted":false},{"id":"C15","complexity":[4,3,1,5],"counted":22,"exhausted":false},{"id":"C16","complexity":[5,4,1,6],"counted":37,"exhausted":false},{"id":"C17","complexity":[4,3,1,7],"counted":50,"exhausted":false},{"id":"C18","complexity":[5,4,1,6],"counted":76,"exhausted":false},{"id":"C19","complexity":[4,4,1,2],"counted":46,"exhausted":false},{"id":"C20","complexity":[4,3,1,7],"counted":24,"exhausted":false},{"id":"C21","complexity":[6,4,1,6],"counted":142,"exhausted":false},{"id":"C22","complexity":[5,3,2,5],"counted":35,"exhausted":false},{"id":"C23","complexity":[3,4,2,2],"counted":13,"exhausted":false},{"id":"C24","complexity":[3,4,2,1],"counted":10,"exhausted":false},{"id":"C25","complexity":[6,7,3,4],"counted":100,"exhausted":false},{"id":"C26","complexity":[5,3,2,5],"counted":76,"exhausted":false},{"id":"C27","complexity":[3,5,3,2],"counted":18,"exhausted":false},{"id":"C28","complexity":[5,3,2,6],"counted":100,"exhausted":false},{"id":"C29","complexity":[4,4,2,5],"counted":50,"exhausted":false},{"id":"C30","complexity":[5,5,2,5],"counted":80,"exhausted":false},{"id":"C31","complexity":[3,4,2,2],"counted":46,"exhausted":false},{"id":"C32","complexity":[4,3,1,7],"counted":50,"exhausted":false},{"id":"C33","complexity":[5,4,2,5],"counted":34,"exhausted":false},{"id":"C34","complexity":[6,4,2,5],"counted":53,"exhausted":false},{"id":"C35","complexity":[5,5,2,5],"counted":175,"exhausted":false},{"id":"C36","complexity":[6,4,1,6],"counted":163,"exhausted":false},{"id":"C37","complexity":[4,4,1,2],"counted":39,"exhausted":false},{"id":"C38","complexity":[3,4,2,2],"counted":46,"exhausted":false},{"id":"C39","complexity":[5,5,3,4],"counted":40,"exhausted":false},{"id":"C40","complexity":[7,6,1,6],"counted":146,"exhausted":false},{"id":"C41","complexity":[7,6,1,6],"counted":165,"exhausted":false},{"id":"C42","complexity":[5,8,4,5],"counted":255,"exhausted":false},{"id":"C43","complexity":[4,4,1,2],"counted":46,"exhausted":false},{"id":"C44","complexity":[6,5,2,4],"counted":32,"exhausted":false},{"id":"C45","complexity":[4,4,1,2],"counted":35,"exhausted":false},{"id":"C46","complexity":[5,4,3,3],"counted":22,"exhausted":false},{"id":"C47","complexity":[4,5,2,4],"counted":26,"exhausted":false},{"id":"C48","complexity":[5,3,2,6],"counted":67,"exhausted":false},{"id":"C49","complexity":[5,4,3,5],"counted":36,"exhausted":false},{"id":"C50","complexity":[4,4,1,2],"counted":36,"exhausted":false},{"id":"C51","complexity":[3,5,3,2],"counted":16,"exhausted":false},{"id":"C52","complexity":[3,5,6,2],"counted":40,"exhausted":false},{"id":"C53","complexity":[4,5,3,5],"counted":60,"exhausted":false},{"id":"C54","complexity":[6,5,2,6],"counted":41,"exhausted":false},{"id":"C55","complexity":[5,5,3,6],"counted":47,"exhausted":false},{"id":"C56","complexity":[4,4,1,2],"counted":32,"exhausted":false},{"id":"C57","complexity":[5,5,2,7],"counted":96,"exhausted":false},{"id":"C58","complexity":[4,3,1,6],"counted":23,"exhausted":false},{"id":"C59","complexity":[3,5,7,2],"counted":24,"exhausted":false},{"id":"C60","complexity":[4,4,1,2],"counted":33,"exhausted":false},{"id":"C61","complexity":[4,4,2,5],"counted":53,"exhausted":false},{"id":"C62","complexity":[5,4,1,6],"counted":113,"exhausted":false},{"id":"C63","complexity":[4,4,4,4],"counted":29,"exhausted":false},{"id":"C64","complexity":[5,7,3,4],"counted":32,"exhausted":false},{"id":"C65","complexity":[6,9,5,5],"counted":53,"exhausted":false},{"id":"C66","complexity":[6,3,1,10],"counted":67,"exhausted":false},{"id":"C67","complexity":[4,8,4,2],"counted":23,"exhausted":false},{"id":"C68","complexity":[6,4,2,5],"counted":77,"exhausted":false},{"id":"C69","complexity":[5,5,2,4],"counted":91,"exhausted":false},{"id":"C70","complexity":[5,6,4,4],"counted":125,"exhausted":false},{"id":"C71","complexity":[5,5,3,6],"counted":41,"exhausted":false},{"id":"C72","complexity":[4,8,5,3],"counted":37,"exhausted":false},{"id":"C73","complexity":[4,7,3,2],"counted":76,"exhausted":false},{"id":"C74","complexity":[5,4,2,6],"counted":28,"exhausted":false},{"id":"C75","complexity":[4,5,4,3],"counted":149,"exhausted":false},{"id":"C76","complexity":[6,4,1,6],"counted":131,"exhausted":false},{"id":"C77","complexity":[4,5,2,2],"counted":50,"exhausted":false},{"id":"C78","complexity":[5,4,2,5],"counted":54,"exhausted":false},{"id":"C79","complexity":[5,5,2,4],"counted":33,"exhausted":false},{"id":"C80","complexity":[5,4,2,4],"counted":30,"exhausted":false},{"id":"C81","complexity":[6,5,2,5],"counted":151,"exhausted":false},{"id":"C82","complexity":[5,5,3,5],"counted":42,"exhausted":false},{"id":"C83","complexity":[4,4,1,2],"counted":26,"exhausted":false},{"id":"C84","complexity":[4,3,1,7],"counted":65,"exhausted":false},{"id":"C85","complexity":[5,8,4,3],"counted":30,"exhausted":false},{"id":"C86","complexity":[6,3,1,11],"counted":51,"exhausted":false},{"id":"C87","complexity":[4,6,3,3],"counted":21,"exhausted":false},{"id":"C88","complexity":[5,6,4,3],"counted":19,"exhausted":false},{"id":"C89","complexity":[4,3,1,6],"counted":66,"exhausted":false},{"id":"C90","complexity":[6,5,2,4],"counted":32,"exhausted":false},{"id":"C91","complexity":[6,7,2,4],"counted":74,"exhausted":false},{"id":"C92","complexity":[6,4,1,6],"counted":165,"exhausted":false},{"id":"C93","complexity":[4,4,1,2],"counted":49,"exhausted":false},{"id":"C94","complexity":[4,4,1,2],"counted":33,"exhausted":false},{"id":"C95","complexity":[4,5,6,3],"counted":151,"exhausted":false},{"id":"C96","complexity":[5,5,2,5],"counted":27,"exhausted":false}],"familyTotals":{"anchors":{"attempts":154,"accepted":15},"optimistic":{"attempts":89,"accepted":4},"turns":{"attempts":564,"accepted":14},"structural":{"attempts":1821,"accepted":75},"steps":{"attempts":392,"accepted":7},"readers":{"attempts":64,"accepted":5},"lifecycle":{"attempts":53,"accepted":3},"observationCollapse":{"attempts":73,"accepted":3},"edges":{"attempts":384,"accepted":0},"deliverySchedule":{"attempts":1140,"accepted":5},"actions":{"attempts":4,"accepted":0},"focused":{"attempts":130,"accepted":9},"observation":{"attempts":24,"accepted":7},"mergeArithmetic":{"attempts":160,"accepted":1},"initialVisibility":{"attempts":16,"accepted":1},"deliveryOrdering":{"attempts":122,"accepted":39},"normalize":{"attempts":18,"accepted":18},"nodes":{"attempts":101,"accepted":2},"ordering":{"attempts":34,"accepted":16},"branches":{"attempts":10,"accepted":2},"schedule":{"attempts":5,"accepted":3},"arithmetic":{"attempts":15,"accepted":4},"cones":{"attempts":2,"accepted":1},"callbackEnd":{"attempts":3,"accepted":3},"pruning":{"attempts":1,"accepted":0}},"initialPairedReclassified":2}}},"monotone":{"workerHash":"f688dcadb8319ed9db1a9f95bc0520b1d65121154f8810f0f9de5f10da1227ec","historical":true,"runtime":"9fa294fb58e6e0ff8003efdd6fcf9a5e6dba3ef3","budget":500,"inputs":"reduced","runs":{"baseline":{"initialExecutions":98,"searchExecutions":4238,"verificationExecutions":98,"elapsedMs":17673.780061,"representatives":61,"routes":[{"source":"C32","target":"C17","reason":"source merge and offset","exact":true,"noMoreComplex":true},{"source":"C61","target":"C29","reason":"queued write follows visibility","exact":true,"noMoreComplex":true},{"source":"C76","target":"C21","reason":"delivery and declaration","exact":false,"noMoreComplex":true},{"source":"C85","target":"C67","reason":"action and visibility order","exact":false,"noMoreComplex":true},{"source":"C16","target":"C17","reason":"async helper and observed source","exact":true,"noMoreComplex":true},{"source":"C34","target":"C80","reason":"combined memo and delivery","exact":true,"noMoreComplex":true},{"source":"C88","target":"C10","reason":"delivery enables lifecycle removal","exact":true,"noMoreComplex":true},{"source":"C96","target":"C74","reason":"source read enables synchronous child","exact":false,"noMoreComplex":true}],"rows":[{"id":"C00","complexity":[3,4,2,1],"counted":8,"exhausted":false},{"id":"C01","complexity":[4,4,1,2],"counted":9,"exhausted":false},{"id":"C02","complexity":[3,4,2,2],"counted":14,"exhausted":false},{"id":"C03","complexity":[4,4,1,2],"counted":9,"exhausted":false},{"id":"C04","complexity":[4,7,4,3],"counted":22,"exhausted":false},{"id":"C05","complexity":[4,5,2,2],"counted":12,"exhausted":false},{"id":"C06","complexity":[4,7,4,3],"counted":22,"exhausted":false},{"id":"C07","complexity":[4,5,2,3],"counted":16,"exhausted":false},{"id":"C08","complexity":[3,4,2,2],"counted":13,"exhausted":false},{"id":"C09","complexity":[5,4,1,4],"counted":25,"exhausted":false},{"id":"C10","complexity":[5,6,2,3],"counted":16,"exhausted":false},{"id":"C11","complexity":[3,4,2,2],"counted":12,"exhausted":false},{"id":"C12","complexity":[4,4,1,2],"counted":9,"exhausted":false},{"id":"C13","complexity":[5,6,2,3],"counted":16,"exhausted":false},{"id":"C14","complexity":[4,5,2,2],"counted":12,"exhausted":false},{"id":"C15","complexity":[4,3,1,5],"counted":24,"exhausted":false},{"id":"C16","complexity":[4,3,1,7],"counted":63,"exhausted":false},{"id":"C17","complexity":[4,3,1,7],"counted":26,"exhausted":false},{"id":"C18","complexity":[4,3,1,7],"counted":63,"exhausted":false},{"id":"C19","complexity":[4,4,1,2],"counted":9,"exhausted":false},{"id":"C20","complexity":[4,3,1,7],"counted":26,"exhausted":false},{"id":"C21","complexity":[6,4,1,6],"counted":129,"exhausted":false},{"id":"C22","complexity":[5,3,2,5],"counted":40,"exhausted":false},{"id":"C23","complexity":[3,4,2,2],"counted":14,"exhausted":false},{"id":"C24","complexity":[3,4,2,1],"counted":8,"exhausted":false},{"id":"C25","complexity":[6,7,3,4],"counted":88,"exhausted":false},{"id":"C26","complexity":[5,3,2,5],"counted":40,"exhausted":false},{"id":"C27","complexity":[3,5,3,2],"counted":17,"exhausted":false},{"id":"C28","complexity":[5,3,2,6],"counted":43,"exhausted":false},{"id":"C29","complexity":[4,4,2,5],"counted":28,"exhausted":false},{"id":"C30","complexity":[4,4,1,2],"counted":9,"exhausted":false},{"id":"C31","complexity":[3,4,2,2],"counted":14,"exhausted":false},{"id":"C32","complexity":[4,3,1,7],"counted":26,"exhausted":false},{"id":"C33","complexity":[5,4,2,5],"counted":40,"exhausted":false},{"id":"C34","complexity":[5,4,2,4],"counted":145,"exhausted":false},{"id":"C35","complexity":[5,5,2,5],"counted":114,"exhausted":false},{"id":"C36","complexity":[6,4,1,6],"counted":129,"exhausted":false},{"id":"C37","complexity":[4,4,1,2],"counted":9,"exhausted":false},{"id":"C38","complexity":[3,4,2,2],"counted":14,"exhausted":false},{"id":"C39","complexity":[5,5,3,4],"counted":44,"exhausted":false},{"id":"C40","complexity":[7,6,1,6],"counted":132,"exhausted":false},{"id":"C41","complexity":[7,6,1,6],"counted":132,"exhausted":false},{"id":"C42","complexity":[5,8,4,5],"counted":124,"exhausted":false},{"id":"C43","complexity":[4,4,1,2],"counted":9,"exhausted":false},{"id":"C44","complexity":[6,5,2,4],"counted":32,"exhausted":false},{"id":"C45","complexity":[4,4,1,2],"counted":9,"exhausted":false},{"id":"C46","complexity":[5,4,3,3],"counted":22,"exhausted":false},{"id":"C47","complexity":[4,5,2,4],"counted":29,"exhausted":false},{"id":"C48","complexity":[5,3,2,6],"counted":43,"exhausted":false},{"id":"C49","complexity":[5,4,3,5],"counted":42,"exhausted":false},{"id":"C50","complexity":[4,4,1,2],"counted":9,"exhausted":false},{"id":"C51","complexity":[3,5,3,2],"counted":15,"exhausted":false},{"id":"C52","complexity":[3,5,6,2],"counted":20,"exhausted":false},{"id":"C53","complexity":[4,5,3,5],"counted":32,"exhausted":false},{"id":"C54","complexity":[6,5,2,6],"counted":49,"exhausted":false},{"id":"C55","complexity":[5,5,3,6],"counted":56,"exhausted":false},{"id":"C56","complexity":[4,4,1,2],"counted":9,"exhausted":false},{"id":"C57","complexity":[5,5,2,7],"counted":54,"exhausted":false},{"id":"C58","complexity":[4,3,1,6],"counted":26,"exhausted":false},{"id":"C59","complexity":[3,5,7,2],"counted":23,"exhausted":false},{"id":"C60","complexity":[4,4,1,2],"counted":9,"exhausted":false},{"id":"C61","complexity":[4,4,2,5],"counted":28,"exhausted":false},{"id":"C62","complexity":[4,3,1,7],"counted":63,"exhausted":false},{"id":"C63","complexity":[4,4,4,4],"counted":28,"exhausted":false},{"id":"C64","complexity":[5,7,3,4],"counted":37,"exhausted":false},{"id":"C65","complexity":[6,9,5,5],"counted":62,"exhausted":false},{"id":"C66","complexity":[6,3,1,10],"counted":85,"exhausted":false},{"id":"C67","complexity":[4,8,4,2],"counted":21,"exhausted":false},{"id":"C68","complexity":[5,4,2,4],"counted":145,"exhausted":false},{"id":"C69","complexity":[5,5,2,4],"counted":79,"exhausted":false},{"id":"C70","complexity":[5,6,4,4],"counted":97,"exhausted":false},{"id":"C71","complexity":[5,5,3,6],"counted":47,"exhausted":false},{"id":"C72","complexity":[4,8,5,3],"counted":37,"exhausted":false},{"id":"C73","complexity":[4,7,3,2],"counted":18,"exhausted":false},{"id":"C74","complexity":[5,4,2,6],"counted":29,"exhausted":false},{"id":"C75","complexity":[4,5,4,3],"counted":82,"exhausted":false},{"id":"C76","complexity":[6,4,1,6],"counted":129,"exhausted":false},{"id":"C77","complexity":[3,4,2,1],"counted":8,"exhausted":false},{"id":"C78","complexity":[5,4,2,5],"counted":40,"exhausted":false},{"id":"C79","complexity":[5,5,2,4],"counted":39,"exhausted":false},{"id":"C80","complexity":[5,4,2,4],"counted":40,"exhausted":false},{"id":"C81","complexity":[6,5,2,5],"counted":110,"exhausted":false},{"id":"C82","complexity":[5,5,3,5],"counted":40,"exhausted":false},{"id":"C83","complexity":[4,4,1,2],"counted":9,"exhausted":false},{"id":"C84","complexity":[4,3,1,7],"counted":26,"exhausted":false},{"id":"C85","complexity":[4,8,4,2],"counted":47,"exhausted":false},{"id":"C86","complexity":[6,3,1,11],"counted":82,"exhausted":false},{"id":"C87","complexity":[4,6,3,3],"counted":20,"exhausted":false},{"id":"C88","complexity":[5,6,2,3],"counted":68,"exhausted":false},{"id":"C89","complexity":[4,3,1,6],"counted":26,"exhausted":false},{"id":"C90","complexity":[6,5,2,4],"counted":33,"exhausted":false},{"id":"C91","complexity":[6,7,2,4],"counted":78,"exhausted":false},{"id":"C92","complexity":[6,4,1,6],"counted":129,"exhausted":false},{"id":"C93","complexity":[4,4,1,2],"counted":9,"exhausted":false},{"id":"C94","complexity":[4,4,1,2],"counted":9,"exhausted":false},{"id":"C95","complexity":[4,5,6,3],"counted":107,"exhausted":false},{"id":"C96","complexity":[5,4,2,6],"counted":78,"exhausted":false}],"familyTotals":{"optimistic":{"attempts":46,"accepted":0},"turns":{"attempts":389,"accepted":0},"structural":{"attempts":978,"accepted":0},"steps":{"attempts":306,"accepted":0},"readers":{"attempts":42,"accepted":0},"lifecycle":{"attempts":41,"accepted":0},"observationCollapse":{"attempts":61,"accepted":0},"edges":{"attempts":318,"accepted":4},"cuts":{"attempts":757,"accepted":6},"anchors":{"attempts":11,"accepted":0},"actions":{"attempts":4,"accepted":0},"focused":{"attempts":64,"accepted":0},"observation":{"attempts":16,"accepted":0},"mergeArithmetic":{"attempts":171,"accepted":0},"initialVisibility":{"attempts":10,"accepted":0},"deliverySchedule":{"attempts":813,"accepted":1},"nodes":{"attempts":69,"accepted":0},"ordering":{"attempts":29,"accepted":6},"deliveryOrdering":{"attempts":65,"accepted":39},"branches":{"attempts":6,"accepted":0},"arithmetic":{"attempts":6,"accepted":0},"deliveryEscape":{"attempts":34,"accepted":1},"pruning":{"attempts":1,"accepted":0},"schedule":{"attempts":1,"accepted":0}},"initialPairedReclassified":2},"current":{"initialExecutions":98,"searchExecutions":5173,"verificationExecutions":98,"elapsedMs":17565.413429,"representatives":59,"routes":[{"source":"C32","target":"C17","reason":"source merge and offset","exact":true,"noMoreComplex":true},{"source":"C61","target":"C29","reason":"queued write follows visibility","exact":true,"noMoreComplex":true},{"source":"C76","target":"C21","reason":"delivery and declaration","exact":true,"noMoreComplex":true},{"source":"C85","target":"C67","reason":"action and visibility order","exact":true,"noMoreComplex":true},{"source":"C16","target":"C17","reason":"async helper and observed source","exact":true,"noMoreComplex":true},{"source":"C34","target":"C80","reason":"combined memo and delivery","exact":true,"noMoreComplex":true},{"source":"C88","target":"C10","reason":"delivery enables lifecycle removal","exact":true,"noMoreComplex":true},{"source":"C96","target":"C74","reason":"source read enables synchronous child","exact":true,"noMoreComplex":true}],"rows":[{"id":"C00","complexity":[3,4,2,1],"counted":9,"exhausted":false},{"id":"C01","complexity":[4,4,1,2],"counted":10,"exhausted":false},{"id":"C02","complexity":[3,4,2,2],"counted":15,"exhausted":false},{"id":"C03","complexity":[4,4,1,2],"counted":10,"exhausted":false},{"id":"C04","complexity":[4,7,4,3],"counted":24,"exhausted":false},{"id":"C05","complexity":[4,5,2,2],"counted":13,"exhausted":false},{"id":"C06","complexity":[4,7,4,3],"counted":24,"exhausted":false},{"id":"C07","complexity":[4,5,2,3],"counted":19,"exhausted":false},{"id":"C08","complexity":[3,4,2,2],"counted":15,"exhausted":false},{"id":"C09","complexity":[5,4,1,4],"counted":29,"exhausted":false},{"id":"C10","complexity":[5,6,2,3],"counted":19,"exhausted":false},{"id":"C11","complexity":[3,4,2,2],"counted":14,"exhausted":false},{"id":"C12","complexity":[4,4,1,2],"counted":10,"exhausted":false},{"id":"C13","complexity":[5,6,2,3],"counted":19,"exhausted":false},{"id":"C14","complexity":[4,5,2,2],"counted":13,"exhausted":false},{"id":"C15","complexity":[4,3,1,5],"counted":26,"exhausted":false},{"id":"C16","complexity":[4,3,1,7],"counted":70,"exhausted":false},{"id":"C17","complexity":[4,3,1,7],"counted":26,"exhausted":false},{"id":"C18","complexity":[4,3,1,7],"counted":70,"exhausted":false},{"id":"C19","complexity":[4,4,1,2],"counted":10,"exhausted":false},{"id":"C20","complexity":[4,3,1,7],"counted":26,"exhausted":false},{"id":"C21","complexity":[6,4,1,6],"counted":169,"exhausted":false},{"id":"C22","complexity":[5,3,2,5],"counted":43,"exhausted":false},{"id":"C23","complexity":[3,4,2,2],"counted":15,"exhausted":false},{"id":"C24","complexity":[3,4,2,1],"counted":10,"exhausted":false},{"id":"C25","complexity":[6,7,3,4],"counted":124,"exhausted":false},{"id":"C26","complexity":[5,3,2,5],"counted":43,"exhausted":false},{"id":"C27","complexity":[3,5,3,2],"counted":19,"exhausted":false},{"id":"C28","complexity":[5,3,2,6],"counted":48,"exhausted":false},{"id":"C29","complexity":[4,4,2,5],"counted":29,"exhausted":false},{"id":"C30","complexity":[4,4,1,2],"counted":10,"exhausted":false},{"id":"C31","complexity":[3,4,2,2],"counted":15,"exhausted":false},{"id":"C32","complexity":[4,3,1,7],"counted":26,"exhausted":false},{"id":"C33","complexity":[5,4,2,5],"counted":43,"exhausted":false},{"id":"C34","complexity":[5,4,2,4],"counted":160,"exhausted":false},{"id":"C35","complexity":[5,5,2,5],"counted":182,"exhausted":false},{"id":"C36","complexity":[6,4,1,6],"counted":169,"exhausted":false},{"id":"C37","complexity":[4,4,1,2],"counted":10,"exhausted":false},{"id":"C38","complexity":[3,4,2,2],"counted":15,"exhausted":false},{"id":"C39","complexity":[5,5,3,4],"counted":49,"exhausted":false},{"id":"C40","complexity":[7,6,1,6],"counted":174,"exhausted":false},{"id":"C41","complexity":[7,6,1,6],"counted":174,"exhausted":false},{"id":"C42","complexity":[5,8,4,5],"counted":180,"exhausted":false},{"id":"C43","complexity":[4,4,1,2],"counted":10,"exhausted":false},{"id":"C44","complexity":[6,5,2,4],"counted":36,"exhausted":false},{"id":"C45","complexity":[4,4,1,2],"counted":10,"exhausted":false},{"id":"C46","complexity":[5,4,3,3],"counted":24,"exhausted":false},{"id":"C47","complexity":[4,5,2,4],"counted":31,"exhausted":false},{"id":"C48","complexity":[5,3,2,6],"counted":48,"exhausted":false},{"id":"C49","complexity":[5,4,3,5],"counted":45,"exhausted":false},{"id":"C50","complexity":[4,4,1,2],"counted":10,"exhausted":false},{"id":"C51","complexity":[3,5,3,2],"counted":17,"exhausted":false},{"id":"C52","complexity":[3,5,6,2],"counted":21,"exhausted":false},{"id":"C53","complexity":[4,5,3,5],"counted":34,"exhausted":false},{"id":"C54","complexity":[6,5,2,6],"counted":52,"exhausted":false},{"id":"C55","complexity":[5,5,3,6],"counted":59,"exhausted":false},{"id":"C56","complexity":[4,4,1,2],"counted":10,"exhausted":false},{"id":"C57","complexity":[5,5,2,7],"counted":58,"exhausted":false},{"id":"C58","complexity":[4,3,1,6],"counted":28,"exhausted":false},{"id":"C59","complexity":[3,5,7,2],"counted":25,"exhausted":false},{"id":"C60","complexity":[4,4,1,2],"counted":10,"exhausted":false},{"id":"C61","complexity":[4,4,2,5],"counted":29,"exhausted":false},{"id":"C62","complexity":[4,3,1,7],"counted":70,"exhausted":false},{"id":"C63","complexity":[4,4,4,4],"counted":31,"exhausted":false},{"id":"C64","complexity":[5,7,3,4],"counted":40,"exhausted":false},{"id":"C65","complexity":[6,9,5,5],"counted":66,"exhausted":false},{"id":"C66","complexity":[6,3,1,10],"counted":86,"exhausted":false},{"id":"C67","complexity":[4,8,4,2],"counted":24,"exhausted":false},{"id":"C68","complexity":[5,4,2,4],"counted":160,"exhausted":false},{"id":"C69","complexity":[5,5,2,4],"counted":115,"exhausted":false},{"id":"C70","complexity":[5,6,4,4],"counted":164,"exhausted":false},{"id":"C71","complexity":[5,5,3,6],"counted":49,"exhausted":false},{"id":"C72","complexity":[4,8,5,3],"counted":42,"exhausted":false},{"id":"C73","complexity":[4,7,3,2],"counted":19,"exhausted":false},{"id":"C74","complexity":[5,4,2,6],"counted":31,"exhausted":false},{"id":"C75","complexity":[4,5,4,3],"counted":165,"exhausted":false},{"id":"C76","complexity":[6,4,1,6],"counted":169,"exhausted":false},{"id":"C77","complexity":[3,4,2,1],"counted":10,"exhausted":false},{"id":"C78","complexity":[5,4,2,5],"counted":43,"exhausted":false},{"id":"C79","complexity":[5,5,2,4],"counted":43,"exhausted":false},{"id":"C80","complexity":[5,4,2,4],"counted":43,"exhausted":false},{"id":"C81","complexity":[6,5,2,5],"counted":172,"exhausted":false},{"id":"C82","complexity":[5,5,3,5],"counted":42,"exhausted":false},{"id":"C83","complexity":[4,4,1,2],"counted":10,"exhausted":false},{"id":"C84","complexity":[4,3,1,7],"counted":26,"exhausted":false},{"id":"C85","complexity":[4,8,4,2],"counted":69,"exhausted":false},{"id":"C86","complexity":[6,3,1,11],"counted":83,"exhausted":false},{"id":"C87","complexity":[4,6,3,3],"counted":23,"exhausted":false},{"id":"C88","complexity":[5,6,2,3],"counted":80,"exhausted":false},{"id":"C89","complexity":[4,3,1,6],"counted":28,"exhausted":false},{"id":"C90","complexity":[6,5,2,4],"counted":37,"exhausted":false},{"id":"C91","complexity":[6,7,2,4],"counted":84,"exhausted":false},{"id":"C92","complexity":[6,4,1,6],"counted":169,"exhausted":false},{"id":"C93","complexity":[4,4,1,2],"counted":10,"exhausted":false},{"id":"C94","complexity":[4,4,1,2],"counted":10,"exhausted":false},{"id":"C95","complexity":[4,5,6,3],"counted":136,"exhausted":false},{"id":"C96","complexity":[5,4,2,6],"counted":89,"exhausted":false}],"familyTotals":{"anchors":{"attempts":131,"accepted":1},"optimistic":{"attempts":55,"accepted":0},"turns":{"attempts":485,"accepted":0},"structural":{"attempts":1378,"accepted":42},"steps":{"attempts":322,"accepted":0},"readers":{"attempts":49,"accepted":0},"lifecycle":{"attempts":41,"accepted":0},"observationCollapse":{"attempts":63,"accepted":0},"edges":{"attempts":325,"accepted":4},"cuts":{"attempts":864,"accepted":6},"actions":{"attempts":4,"accepted":0},"focused":{"attempts":112,"accepted":0},"observation":{"attempts":24,"accepted":0},"mergeArithmetic":{"attempts":171,"accepted":0},"initialVisibility":{"attempts":10,"accepted":0},"deliverySchedule":{"attempts":858,"accepted":1},"deliveryOrdering":{"attempts":120,"accepted":41},"nodes":{"attempts":72,"accepted":0},"normalize":{"attempts":3,"accepted":3},"ordering":{"attempts":29,"accepted":13},"branches":{"attempts":6,"accepted":0},"arithmetic":{"attempts":8,"accepted":0},"deliveryEscape":{"attempts":41,"accepted":1},"pruning":{"attempts":1,"accepted":0},"schedule":{"attempts":1,"accepted":0}},"initialPairedReclassified":2}}},"full-without-deliveryOrdering":{"workerHash":"f688dcadb8319ed9db1a9f95bc0520b1d65121154f8810f0f9de5f10da1227ec","historical":true,"runtime":"9fa294fb58e6e0ff8003efdd6fcf9a5e6dba3ef3","budget":500,"inputs":"original","runs":{"current":{"initialExecutions":98,"searchExecutions":4968,"verificationExecutions":98,"elapsedMs":18043.549906,"representatives":60,"routes":[{"source":"C32","target":"C17","reason":"source merge and offset","exact":true,"noMoreComplex":true},{"source":"C61","target":"C29","reason":"queued write follows visibility","exact":true,"noMoreComplex":true},{"source":"C76","target":"C21","reason":"delivery and declaration","exact":true,"noMoreComplex":true},{"source":"C85","target":"C67","reason":"action and visibility order","exact":true,"noMoreComplex":true},{"source":"C16","target":"C17","reason":"async helper and observed source","exact":true,"noMoreComplex":true},{"source":"C34","target":"C80","reason":"combined memo and delivery","exact":true,"noMoreComplex":true},{"source":"C88","target":"C10","reason":"delivery enables lifecycle removal","exact":true,"noMoreComplex":true},{"source":"C96","target":"C74","reason":"source read enables synchronous child","exact":true,"noMoreComplex":true}],"rows":[{"id":"C00","complexity":[3,4,2,1],"counted":9,"exhausted":false},{"id":"C01","complexity":[4,4,1,2],"counted":10,"exhausted":false},{"id":"C02","complexity":[3,4,2,2],"counted":15,"exhausted":false},{"id":"C03","complexity":[4,4,1,2],"counted":17,"exhausted":false},{"id":"C04","complexity":[4,7,4,3],"counted":24,"exhausted":false},{"id":"C05","complexity":[4,5,2,2],"counted":19,"exhausted":false},{"id":"C06","complexity":[4,7,4,3],"counted":24,"exhausted":false},{"id":"C07","complexity":[4,5,2,3],"counted":19,"exhausted":false},{"id":"C08","complexity":[3,4,2,2],"counted":15,"exhausted":false},{"id":"C09","complexity":[5,4,1,4],"counted":26,"exhausted":false},{"id":"C10","complexity":[5,6,2,3],"counted":36,"exhausted":false},{"id":"C11","complexity":[3,4,2,2],"counted":14,"exhausted":false},{"id":"C12","complexity":[4,4,1,2],"counted":25,"exhausted":false},{"id":"C13","complexity":[5,6,2,3],"counted":19,"exhausted":false},{"id":"C14","complexity":[4,5,2,2],"counted":32,"exhausted":false},{"id":"C15","complexity":[4,3,1,5],"counted":26,"exhausted":false},{"id":"C16","complexity":[4,3,1,7],"counted":70,"exhausted":false},{"id":"C17","complexity":[4,3,1,7],"counted":52,"exhausted":false},{"id":"C18","complexity":[4,3,1,7],"counted":109,"exhausted":false},{"id":"C19","complexity":[4,4,1,2],"counted":47,"exhausted":false},{"id":"C20","complexity":[4,3,1,7],"counted":26,"exhausted":false},{"id":"C21","complexity":[6,4,1,6],"counted":50,"exhausted":false},{"id":"C22","complexity":[5,3,2,5],"counted":43,"exhausted":false},{"id":"C23","complexity":[3,4,2,2],"counted":15,"exhausted":false},{"id":"C24","complexity":[3,4,2,1],"counted":10,"exhausted":false},{"id":"C25","complexity":[6,7,3,4],"counted":39,"exhausted":false},{"id":"C26","complexity":[5,3,2,5],"counted":78,"exhausted":false},{"id":"C27","complexity":[3,5,3,2],"counted":19,"exhausted":false},{"id":"C28","complexity":[5,3,2,6],"counted":99,"exhausted":false},{"id":"C29","complexity":[4,4,2,5],"counted":55,"exhausted":false},{"id":"C30","complexity":[5,5,2,5],"counted":81,"exhausted":false},{"id":"C31","complexity":[3,4,2,2],"counted":47,"exhausted":false},{"id":"C32","complexity":[4,3,1,7],"counted":52,"exhausted":false},{"id":"C33","complexity":[5,4,2,5],"counted":43,"exhausted":false},{"id":"C34","complexity":[5,4,2,4],"counted":160,"exhausted":false},{"id":"C35","complexity":[5,5,2,5],"counted":38,"exhausted":false},{"id":"C36","complexity":[6,4,1,6],"counted":106,"exhausted":false},{"id":"C37","complexity":[4,4,1,2],"counted":40,"exhausted":false},{"id":"C38","complexity":[3,4,2,2],"counted":47,"exhausted":false},{"id":"C39","complexity":[5,5,3,4],"counted":49,"exhausted":false},{"id":"C40","complexity":[7,6,1,6],"counted":51,"exhausted":false},{"id":"C41","complexity":[7,6,1,6],"counted":105,"exhausted":false},{"id":"C42","complexity":[6,8,4,8],"counted":142,"exhausted":false},{"id":"C43","complexity":[4,4,1,2],"counted":47,"exhausted":false},{"id":"C44","complexity":[6,5,2,4],"counted":36,"exhausted":false},{"id":"C45","complexity":[4,4,1,2],"counted":36,"exhausted":false},{"id":"C46","complexity":[5,4,3,3],"counted":24,"exhausted":false},{"id":"C47","complexity":[4,5,2,4],"counted":31,"exhausted":false},{"id":"C48","complexity":[5,3,2,6],"counted":75,"exhausted":false},{"id":"C49","complexity":[5,4,3,5],"counted":45,"exhausted":false},{"id":"C50","complexity":[4,4,1,2],"counted":37,"exhausted":false},{"id":"C51","complexity":[3,5,3,2],"counted":17,"exhausted":false},{"id":"C52","complexity":[3,5,6,2],"counted":41,"exhausted":false},{"id":"C53","complexity":[4,5,3,5],"counted":65,"exhausted":false},{"id":"C54","complexity":[6,5,2,6],"counted":52,"exhausted":false},{"id":"C55","complexity":[5,5,3,6],"counted":57,"exhausted":false},{"id":"C56","complexity":[4,4,1,2],"counted":33,"exhausted":false},{"id":"C57","complexity":[5,5,2,7],"counted":107,"exhausted":false},{"id":"C58","complexity":[4,3,1,6],"counted":28,"exhausted":false},{"id":"C59","complexity":[3,5,7,2],"counted":25,"exhausted":false},{"id":"C60","complexity":[4,4,1,2],"counted":34,"exhausted":false},{"id":"C61","complexity":[4,4,2,5],"counted":58,"exhausted":false},{"id":"C62","complexity":[4,3,1,7],"counted":146,"exhausted":false},{"id":"C63","complexity":[4,4,4,4],"counted":31,"exhausted":false},{"id":"C64","complexity":[5,7,3,4],"counted":40,"exhausted":false},{"id":"C65","complexity":[6,9,5,5],"counted":66,"exhausted":false},{"id":"C66","complexity":[6,3,1,10],"counted":86,"exhausted":false},{"id":"C67","complexity":[4,8,4,2],"counted":24,"exhausted":false},{"id":"C68","complexity":[5,4,2,4],"counted":184,"exhausted":false},{"id":"C69","complexity":[5,5,2,4],"counted":36,"exhausted":false},{"id":"C70","complexity":[5,6,4,4],"counted":40,"exhausted":false},{"id":"C71","complexity":[5,5,3,6],"counted":49,"exhausted":false},{"id":"C72","complexity":[4,8,5,3],"counted":42,"exhausted":false},{"id":"C73","complexity":[4,7,3,2],"counted":78,"exhausted":false},{"id":"C74","complexity":[5,4,2,6],"counted":31,"exhausted":false},{"id":"C75","complexity":[4,5,4,3],"counted":34,"exhausted":false},{"id":"C76","complexity":[6,4,1,6],"counted":49,"exhausted":false},{"id":"C77","complexity":[4,5,2,2],"counted":51,"exhausted":false},{"id":"C78","complexity":[5,4,2,5],"counted":63,"exhausted":false},{"id":"C79","complexity":[5,5,2,4],"counted":43,"exhausted":false},{"id":"C80","complexity":[5,4,2,4],"counted":43,"exhausted":false},{"id":"C81","complexity":[6,5,2,5],"counted":41,"exhausted":false},{"id":"C82","complexity":[5,5,3,5],"counted":42,"exhausted":false},{"id":"C83","complexity":[4,4,1,2],"counted":27,"exhausted":false},{"id":"C84","complexity":[4,3,1,7],"counted":67,"exhausted":false},{"id":"C85","complexity":[4,8,4,2],"counted":69,"exhausted":false},{"id":"C86","complexity":[6,3,1,11],"counted":83,"exhausted":false},{"id":"C87","complexity":[4,6,3,3],"counted":23,"exhausted":false},{"id":"C88","complexity":[5,6,2,3],"counted":80,"exhausted":false},{"id":"C89","complexity":[4,3,1,6],"counted":71,"exhausted":false},{"id":"C90","complexity":[6,5,2,4],"counted":37,"exhausted":false},{"id":"C91","complexity":[6,7,2,4],"counted":84,"exhausted":false},{"id":"C92","complexity":[6,4,1,6],"counted":104,"exhausted":false},{"id":"C93","complexity":[4,4,1,2],"counted":50,"exhausted":false},{"id":"C94","complexity":[4,4,1,2],"counted":34,"exhausted":false},{"id":"C95","complexity":[4,5,6,3],"counted":80,"exhausted":false},{"id":"C96","complexity":[5,4,2,6],"counted":89,"exhausted":false}],"familyTotals":{"anchors":{"attempts":133,"accepted":16},"optimistic":{"attempts":88,"accepted":4},"turns":{"attempts":437,"accepted":14},"structural":{"attempts":1386,"accepted":33},"steps":{"attempts":333,"accepted":7},"readers":{"attempts":50,"accepted":5},"lifecycle":{"attempts":48,"accepted":3},"observationCollapse":{"attempts":53,"accepted":3},"edges":{"attempts":337,"accepted":4},"cuts":{"attempts":601,"accepted":6},"deliverySchedule":{"attempts":954,"accepted":6},"actions":{"attempts":4,"accepted":0},"focused":{"attempts":115,"accepted":8},"observation":{"attempts":31,"accepted":7},"mergeArithmetic":{"attempts":160,"accepted":1},"initialVisibility":{"attempts":16,"accepted":1},"normalize":{"attempts":23,"accepted":23},"nodes":{"attempts":110,"accepted":2},"branches":{"attempts":10,"accepted":2},"ordering":{"attempts":16,"accepted":9},"schedule":{"attempts":5,"accepted":3},"arithmetic":{"attempts":11,"accepted":4},"cones":{"attempts":2,"accepted":0},"callbackEnd":{"attempts":3,"accepted":3},"deliveryEscape":{"attempts":41,"accepted":1},"pruning":{"attempts":1,"accepted":0}}}}},"full-without-initialVisibility":{"workerHash":"f688dcadb8319ed9db1a9f95bc0520b1d65121154f8810f0f9de5f10da1227ec","historical":true,"runtime":"9fa294fb58e6e0ff8003efdd6fcf9a5e6dba3ef3","budget":500,"inputs":"original","runs":{"current":{"initialExecutions":98,"searchExecutions":6687,"verificationExecutions":98,"elapsedMs":24623.831301,"representatives":59,"routes":[{"source":"C32","target":"C17","reason":"source merge and offset","exact":true,"noMoreComplex":true},{"source":"C61","target":"C29","reason":"queued write follows visibility","exact":true,"noMoreComplex":true},{"source":"C76","target":"C21","reason":"delivery and declaration","exact":true,"noMoreComplex":true},{"source":"C85","target":"C67","reason":"action and visibility order","exact":true,"noMoreComplex":true},{"source":"C16","target":"C17","reason":"async helper and observed source","exact":true,"noMoreComplex":true},{"source":"C34","target":"C80","reason":"combined memo and delivery","exact":true,"noMoreComplex":true},{"source":"C88","target":"C10","reason":"delivery enables lifecycle removal","exact":true,"noMoreComplex":true},{"source":"C96","target":"C74","reason":"source read enables synchronous child","exact":true,"noMoreComplex":true}],"rows":[{"id":"C00","complexity":[3,4,2,1],"counted":9,"exhausted":false},{"id":"C01","complexity":[4,4,1,2],"counted":10,"exhausted":false},{"id":"C02","complexity":[3,4,2,2],"counted":15,"exhausted":false},{"id":"C03","complexity":[4,4,1,2],"counted":17,"exhausted":false},{"id":"C04","complexity":[4,7,4,3],"counted":24,"exhausted":false},{"id":"C05","complexity":[4,5,2,2],"counted":18,"exhausted":false},{"id":"C06","complexity":[4,7,4,3],"counted":24,"exhausted":false},{"id":"C07","complexity":[4,5,2,3],"counted":19,"exhausted":false},{"id":"C08","complexity":[3,4,2,2],"counted":14,"exhausted":false},{"id":"C09","complexity":[5,4,1,4],"counted":29,"exhausted":false},{"id":"C10","complexity":[5,6,2,3],"counted":36,"exhausted":false},{"id":"C11","complexity":[3,4,2,2],"counted":14,"exhausted":false},{"id":"C12","complexity":[4,4,1,2],"counted":25,"exhausted":false},{"id":"C13","complexity":[5,6,2,3],"counted":19,"exhausted":false},{"id":"C14","complexity":[4,5,2,2],"counted":30,"exhausted":false},{"id":"C15","complexity":[4,3,1,5],"counted":26,"exhausted":false},{"id":"C16","complexity":[4,3,1,7],"counted":70,"exhausted":false},{"id":"C17","complexity":[4,3,1,7],"counted":52,"exhausted":false},{"id":"C18","complexity":[4,3,1,7],"counted":109,"exhausted":false},{"id":"C19","complexity":[4,4,1,2],"counted":47,"exhausted":false},{"id":"C20","complexity":[4,3,1,7],"counted":26,"exhausted":false},{"id":"C21","complexity":[6,4,1,6],"counted":180,"exhausted":false},{"id":"C22","complexity":[5,3,2,5],"counted":43,"exhausted":false},{"id":"C23","complexity":[3,4,2,2],"counted":14,"exhausted":false},{"id":"C24","complexity":[3,4,2,1],"counted":10,"exhausted":false},{"id":"C25","complexity":[6,7,3,4],"counted":124,"exhausted":false},{"id":"C26","complexity":[5,3,2,5],"counted":78,"exhausted":false},{"id":"C27","complexity":[3,5,3,2],"counted":19,"exhausted":false},{"id":"C28","complexity":[5,3,2,6],"counted":101,"exhausted":false},{"id":"C29","complexity":[4,4,2,5],"counted":53,"exhausted":false},{"id":"C30","complexity":[5,5,2,5],"counted":81,"exhausted":false},{"id":"C31","complexity":[3,4,2,2],"counted":47,"exhausted":false},{"id":"C32","complexity":[4,3,1,7],"counted":52,"exhausted":false},{"id":"C33","complexity":[5,4,2,5],"counted":43,"exhausted":false},{"id":"C34","complexity":[5,4,2,4],"counted":160,"exhausted":false},{"id":"C35","complexity":[5,5,2,5],"counted":182,"exhausted":false},{"id":"C36","complexity":[6,4,1,6],"counted":236,"exhausted":false},{"id":"C37","complexity":[4,4,1,2],"counted":40,"exhausted":false},{"id":"C38","complexity":[3,4,2,2],"counted":47,"exhausted":false},{"id":"C39","complexity":[5,5,3,4],"counted":49,"exhausted":false},{"id":"C40","complexity":[7,6,1,6],"counted":184,"exhausted":false},{"id":"C41","complexity":[7,6,1,6],"counted":238,"exhausted":false},{"id":"C42","complexity":[5,8,5,5],"counted":305,"exhausted":false},{"id":"C43","complexity":[4,4,1,2],"counted":47,"exhausted":false},{"id":"C44","complexity":[6,5,2,4],"counted":36,"exhausted":false},{"id":"C45","complexity":[4,4,1,2],"counted":36,"exhausted":false},{"id":"C46","complexity":[5,4,3,3],"counted":23,"exhausted":false},{"id":"C47","complexity":[4,5,2,4],"counted":30,"exhausted":false},{"id":"C48","complexity":[5,3,2,6],"counted":77,"exhausted":false},{"id":"C49","complexity":[5,4,3,5],"counted":45,"exhausted":false},{"id":"C50","complexity":[4,4,1,2],"counted":37,"exhausted":false},{"id":"C51","complexity":[3,5,3,2],"counted":17,"exhausted":false},{"id":"C52","complexity":[3,5,6,2],"counted":41,"exhausted":false},{"id":"C53","complexity":[4,5,3,5],"counted":63,"exhausted":false},{"id":"C54","complexity":[6,5,2,6],"counted":52,"exhausted":false},{"id":"C55","complexity":[5,5,3,6],"counted":59,"exhausted":false},{"id":"C56","complexity":[4,4,1,2],"counted":33,"exhausted":false},{"id":"C57","complexity":[5,5,2,7],"counted":107,"exhausted":false},{"id":"C58","complexity":[4,3,1,6],"counted":28,"exhausted":false},{"id":"C59","complexity":[3,5,7,2],"counted":25,"exhausted":false},{"id":"C60","complexity":[4,4,1,2],"counted":34,"exhausted":false},{"id":"C61","complexity":[4,4,2,5],"counted":56,"exhausted":false},{"id":"C62","complexity":[4,3,1,7],"counted":146,"exhausted":false},{"id":"C63","complexity":[4,4,4,4],"counted":31,"exhausted":false},{"id":"C64","complexity":[5,7,3,4],"counted":40,"exhausted":false},{"id":"C65","complexity":[6,9,5,5],"counted":66,"exhausted":false},{"id":"C66","complexity":[6,3,1,10],"counted":86,"exhausted":false},{"id":"C67","complexity":[4,8,4,2],"counted":24,"exhausted":false},{"id":"C68","complexity":[5,4,2,4],"counted":184,"exhausted":false},{"id":"C69","complexity":[5,5,2,4],"counted":115,"exhausted":false},{"id":"C70","complexity":[5,6,4,4],"counted":163,"exhausted":false},{"id":"C71","complexity":[5,5,3,6],"counted":49,"exhausted":false},{"id":"C72","complexity":[4,8,5,3],"counted":42,"exhausted":false},{"id":"C73","complexity":[4,7,3,2],"counted":78,"exhausted":false},{"id":"C74","complexity":[5,4,2,6],"counted":31,"exhausted":false},{"id":"C75","complexity":[4,5,4,3],"counted":165,"exhausted":false},{"id":"C76","complexity":[6,4,1,6],"counted":169,"exhausted":false},{"id":"C77","complexity":[4,5,2,2],"counted":50,"exhausted":false},{"id":"C78","complexity":[5,4,2,5],"counted":63,"exhausted":false},{"id":"C79","complexity":[5,5,2,4],"counted":43,"exhausted":false},{"id":"C80","complexity":[5,4,2,4],"counted":43,"exhausted":false},{"id":"C81","complexity":[6,5,2,5],"counted":172,"exhausted":false},{"id":"C82","complexity":[5,5,3,5],"counted":42,"exhausted":false},{"id":"C83","complexity":[4,4,1,2],"counted":27,"exhausted":false},{"id":"C84","complexity":[4,3,1,7],"counted":67,"exhausted":false},{"id":"C85","complexity":[4,8,4,2],"counted":69,"exhausted":false},{"id":"C86","complexity":[6,3,1,11],"counted":83,"exhausted":false},{"id":"C87","complexity":[4,6,3,3],"counted":22,"exhausted":false},{"id":"C88","complexity":[5,6,2,3],"counted":80,"exhausted":false},{"id":"C89","complexity":[4,3,1,6],"counted":71,"exhausted":false},{"id":"C90","complexity":[6,5,2,4],"counted":37,"exhausted":false},{"id":"C91","complexity":[6,7,2,4],"counted":84,"exhausted":false},{"id":"C92","complexity":[6,4,1,6],"counted":224,"exhausted":false},{"id":"C93","complexity":[4,4,1,2],"counted":50,"exhausted":false},{"id":"C94","complexity":[4,4,1,2],"counted":34,"exhausted":false},{"id":"C95","complexity":[4,5,6,3],"counted":183,"exhausted":false},{"id":"C96","complexity":[5,4,2,6],"counted":89,"exhausted":false}],"familyTotals":{"anchors":{"attempts":174,"accepted":16},"optimistic":{"attempts":98,"accepted":4},"turns":{"attempts":613,"accepted":14},"structural":{"attempts":1909,"accepted":75},"steps":{"attempts":426,"accepted":7},"readers":{"attempts":69,"accepted":5},"lifecycle":{"attempts":56,"accepted":3},"observationCollapse":{"attempts":79,"accepted":3},"edges":{"attempts":422,"accepted":4},"cuts":{"attempts":923,"accepted":7},"deliverySchedule":{"attempts":1207,"accepted":6},"actions":{"attempts":4,"accepted":0},"focused":{"attempts":140,"accepted":9},"observation":{"attempts":33,"accepted":7},"mergeArithmetic":{"attempts":172,"accepted":1},"deliveryOrdering":{"attempts":119,"accepted":39},"normalize":{"attempts":23,"accepted":23},"nodes":{"attempts":110,"accepted":2},"ordering":{"attempts":33,"accepted":18},"branches":{"attempts":10,"accepted":2},"schedule":{"attempts":5,"accepted":3},"arithmetic":{"attempts":15,"accepted":4},"cones":{"attempts":2,"accepted":1},"callbackEnd":{"attempts":3,"accepted":3},"deliveryEscape":{"attempts":41,"accepted":1},"pruning":{"attempts":1,"accepted":0}}}}},"native-final":{"experiment":"native","budget":150,"workerHash":"f688dcadb8319ed9db1a9f95bc0520b1d65121154f8810f0f9de5f10da1227ec","rows":[{"cohort":"ordinary","seed":91500,"first":[5,14,16,7],"native":[3,6,7,2],"external":[3,4,2,2],"hybrid":[3,4,2,2],"nativeCalls":114,"externalCalls":58,"hybridCalls":30,"invalid":0,"interrupted":false},{"cohort":"ordinary","seed":91501,"first":[7,18,16,14],"native":[7,10,11,6],"external":[3,4,2,2],"hybrid":[7,10,11,6],"nativeCalls":150,"externalCalls":72,"hybridCalls":0,"invalid":0,"interrupted":true},{"cohort":"ordinary","seed":91502,"first":[5,18,18,11],"native":[5,8,8,4],"external":[3,4,2,2],"hybrid":[3,5,2,2],"nativeCalls":121,"externalCalls":78,"hybridCalls":29,"invalid":0,"interrupted":false},{"cohort":"ordinary","seed":91503,"first":[3,15,18,4],"native":[3,12,12,2],"external":[3,4,2,2],"hybrid":[3,12,12,2],"nativeCalls":150,"externalCalls":72,"hybridCalls":0,"invalid":0,"interrupted":true},{"cohort":"ordinary","seed":91504,"first":[4,19,25,4],"native":[4,7,10,3],"external":[3,4,2,2],"hybrid":[4,7,10,3],"nativeCalls":150,"externalCalls":54,"hybridCalls":0,"invalid":0,"interrupted":true},{"cohort":"branches","seed":91500,"found":false,"calls":150},{"cohort":"branches","seed":91501,"first":[8,17,14,23],"native":[8,17,14,21],"external":[5,3,2,5],"hybrid":[6,4,3,9],"nativeCalls":113,"externalCalls":140,"hybridCalls":37,"invalid":0,"interrupted":false},{"cohort":"branches","seed":91502,"first":[6,12,15,13],"native":[6,12,15,13],"external":[3,4,2,2],"hybrid":[3,4,2,2],"nativeCalls":97,"externalCalls":45,"hybridCalls":45,"invalid":0,"interrupted":false},{"cohort":"branches","seed":91503,"first":[7,22,29,16],"native":[7,22,25,12],"external":[4,3,1,7],"hybrid":[4,5,4,9],"nativeCalls":105,"externalCalls":127,"hybridCalls":45,"invalid":0,"interrupted":false},{"cohort":"branches","seed":91504,"first":[9,12,11,28],"native":[9,9,9,18],"external":[4,3,1,7],"hybrid":[9,9,9,18],"nativeCalls":150,"externalCalls":150,"hybridCalls":0,"invalid":0,"interrupted":true}]},"chunks":{"experiment":"chunks","budget":150,"workerHash":"f688dcadb8319ed9db1a9f95bc0520b1d65121154f8810f0f9de5f10da1227ec","rows":[{"id":"C00","halving":{"chunkCalls":1,"calls":10,"invalid":0,"complexity":[3,4,2,1]},"cdd":{"chunkCalls":1,"calls":10,"invalid":0,"complexity":[3,4,2,1]}},{"id":"C01","halving":{"chunkCalls":1,"calls":11,"invalid":0,"complexity":[4,4,1,2]},"cdd":{"chunkCalls":1,"calls":11,"invalid":0,"complexity":[4,4,1,2]}},{"id":"C02","halving":{"chunkCalls":2,"calls":17,"invalid":0,"complexity":[3,4,2,2]},"cdd":{"chunkCalls":3,"calls":18,"invalid":0,"complexity":[3,4,2,2]}},{"id":"C03","halving":{"chunkCalls":1,"calls":18,"invalid":0,"complexity":[4,4,1,2]},"cdd":{"chunkCalls":1,"calls":18,"invalid":0,"complexity":[4,4,1,2]}},{"id":"C04","halving":{"chunkCalls":2,"calls":26,"invalid":0,"complexity":[4,7,4,3]},"cdd":{"chunkCalls":3,"calls":27,"invalid":0,"complexity":[4,7,4,3]}},{"id":"C05","halving":{"chunkCalls":1,"calls":20,"invalid":0,"complexity":[4,5,2,2]},"cdd":{"chunkCalls":1,"calls":20,"invalid":0,"complexity":[4,5,2,2]}},{"id":"C06","halving":{"chunkCalls":2,"calls":26,"invalid":0,"complexity":[4,7,4,3]},"cdd":{"chunkCalls":3,"calls":27,"invalid":0,"complexity":[4,7,4,3]}},{"id":"C07","halving":{"chunkCalls":2,"calls":21,"invalid":0,"complexity":[4,5,2,3]},"cdd":{"chunkCalls":3,"calls":22,"invalid":0,"complexity":[4,5,2,3]}},{"id":"C09","halving":{"chunkCalls":1,"calls":30,"invalid":0,"complexity":[5,4,1,4]},"cdd":{"chunkCalls":1,"calls":30,"invalid":0,"complexity":[5,4,1,4]}},{"id":"C10","halving":{"chunkCalls":2,"calls":38,"invalid":0,"complexity":[5,6,2,3]},"cdd":{"chunkCalls":3,"calls":39,"invalid":0,"complexity":[5,6,2,3]}},{"id":"C12","halving":{"chunkCalls":3,"calls":16,"invalid":1,"complexity":[4,4,1,2]},"cdd":{"chunkCalls":3,"calls":16,"invalid":1,"complexity":[4,4,1,2]}},{"id":"C13","halving":{"chunkCalls":2,"calls":21,"invalid":0,"complexity":[5,6,2,3]},"cdd":{"chunkCalls":3,"calls":22,"invalid":0,"complexity":[5,6,2,3]}},{"id":"C14","halving":{"chunkCalls":1,"calls":33,"invalid":0,"complexity":[4,5,2,2]},"cdd":{"chunkCalls":1,"calls":33,"invalid":0,"complexity":[4,5,2,2]}},{"id":"C16","halving":{"chunkCalls":1,"calls":71,"invalid":0,"complexity":[4,3,1,7]},"cdd":{"chunkCalls":1,"calls":71,"invalid":0,"complexity":[4,3,1,7]}},{"id":"C17","halving":{"chunkCalls":1,"calls":53,"invalid":0,"complexity":[4,3,1,7]},"cdd":{"chunkCalls":1,"calls":53,"invalid":0,"complexity":[4,3,1,7]}},{"id":"C18","halving":{"chunkCalls":1,"calls":110,"invalid":0,"complexity":[4,3,1,7]},"cdd":{"chunkCalls":1,"calls":110,"invalid":0,"complexity":[4,3,1,7]}},{"id":"C19","halving":{"chunkCalls":2,"calls":49,"invalid":0,"complexity":[4,4,1,2]},"cdd":{"chunkCalls":3,"calls":50,"invalid":0,"complexity":[4,4,1,2]}},{"id":"C20","halving":{"chunkCalls":1,"calls":27,"invalid":0,"complexity":[4,3,1,7]},"cdd":{"chunkCalls":1,"calls":27,"invalid":0,"complexity":[4,3,1,7]}},{"id":"C21","halving":{"chunkCalls":1,"calls":150,"invalid":0,"complexity":[6,4,1,6]},"cdd":{"chunkCalls":1,"calls":150,"invalid":0,"complexity":[6,4,1,6]}},{"id":"C22","halving":{"chunkCalls":2,"calls":45,"invalid":0,"complexity":[5,3,2,5]},"cdd":{"chunkCalls":3,"calls":46,"invalid":0,"complexity":[5,3,2,5]}},{"id":"C24","halving":{"chunkCalls":1,"calls":11,"invalid":0,"complexity":[3,4,2,1]},"cdd":{"chunkCalls":1,"calls":11,"invalid":0,"complexity":[3,4,2,1]}},{"id":"C25","halving":{"chunkCalls":2,"calls":126,"invalid":0,"complexity":[6,7,3,4]},"cdd":{"chunkCalls":3,"calls":127,"invalid":0,"complexity":[6,7,3,4]}},{"id":"C26","halving":{"chunkCalls":2,"calls":80,"invalid":0,"complexity":[5,3,2,5]},"cdd":{"chunkCalls":3,"calls":81,"invalid":0,"complexity":[5,3,2,5]}},{"id":"C27","halving":{"chunkCalls":4,"calls":23,"invalid":0,"complexity":[3,5,3,2]},"cdd":{"chunkCalls":5,"calls":24,"invalid":0,"complexity":[3,5,3,2]}},{"id":"C28","halving":{"chunkCalls":2,"calls":103,"invalid":0,"complexity":[5,3,2,6]},"cdd":{"chunkCalls":3,"calls":104,"invalid":0,"complexity":[5,3,2,6]}},{"id":"C29","halving":{"chunkCalls":1,"calls":56,"invalid":0,"complexity":[4,4,2,5]},"cdd":{"chunkCalls":1,"calls":56,"invalid":0,"complexity":[4,4,2,5]}},{"id":"C30","halving":{"chunkCalls":2,"calls":83,"invalid":0,"complexity":[5,5,2,5]},"cdd":{"chunkCalls":3,"calls":84,"invalid":0,"complexity":[5,5,2,5]}},{"id":"C31","halving":{"chunkCalls":1,"calls":48,"invalid":0,"complexity":[3,4,2,2]},"cdd":{"chunkCalls":1,"calls":48,"invalid":0,"complexity":[3,4,2,2]}},{"id":"C32","halving":{"chunkCalls":1,"calls":53,"invalid":0,"complexity":[4,3,1,7]},"cdd":{"chunkCalls":1,"calls":53,"invalid":0,"complexity":[4,3,1,7]}},{"id":"C33","halving":{"chunkCalls":2,"calls":45,"invalid":0,"complexity":[5,4,2,5]},"cdd":{"chunkCalls":3,"calls":46,"invalid":0,"complexity":[5,4,2,5]}},{"id":"C34","halving":{"chunkCalls":2,"calls":150,"invalid":0,"complexity":[5,4,2,4]},"cdd":{"chunkCalls":3,"calls":150,"invalid":0,"complexity":[5,4,2,4]}},{"id":"C35","halving":{"chunkCalls":2,"calls":150,"invalid":0,"complexity":[5,5,2,5]},"cdd":{"chunkCalls":3,"calls":150,"invalid":0,"complexity":[5,5,2,5]}},{"id":"C36","halving":{"chunkCalls":1,"calls":150,"invalid":0,"complexity":[6,4,1,6]},"cdd":{"chunkCalls":1,"calls":150,"invalid":0,"complexity":[6,4,1,6]}},{"id":"C37","halving":{"chunkCalls":6,"calls":34,"invalid":1,"complexity":[4,4,1,2]},"cdd":{"chunkCalls":6,"calls":34,"invalid":1,"complexity":[4,4,1,2]}},{"id":"C38","halving":{"chunkCalls":1,"calls":48,"invalid":0,"complexity":[3,4,2,2]},"cdd":{"chunkCalls":1,"calls":48,"invalid":0,"complexity":[3,4,2,2]}},{"id":"C39","halving":{"chunkCalls":4,"calls":53,"invalid":0,"complexity":[5,5,3,4]},"cdd":{"chunkCalls":5,"calls":54,"invalid":0,"complexity":[5,5,3,4]}},{"id":"C40","halving":{"chunkCalls":1,"calls":150,"invalid":0,"complexity":[7,6,1,6]},"cdd":{"chunkCalls":1,"calls":150,"invalid":0,"complexity":[7,6,1,6]}},{"id":"C41","halving":{"chunkCalls":1,"calls":150,"invalid":0,"complexity":[7,6,1,6]},"cdd":{"chunkCalls":1,"calls":150,"invalid":0,"complexity":[7,6,1,6]}},{"id":"C42","halving":{"chunkCalls":6,"calls":150,"invalid":1,"complexity":[6,9,4,8]},"cdd":{"chunkCalls":7,"calls":150,"invalid":1,"complexity":[6,9,4,8]}},{"id":"C43","halving":{"chunkCalls":2,"calls":49,"invalid":0,"complexity":[4,4,1,2]},"cdd":{"chunkCalls":3,"calls":50,"invalid":0,"complexity":[4,4,1,2]}},{"id":"C44","halving":{"chunkCalls":2,"calls":38,"invalid":0,"complexity":[6,5,2,4]},"cdd":{"chunkCalls":3,"calls":39,"invalid":0,"complexity":[6,5,2,4]}},{"id":"C45","halving":{"chunkCalls":1,"calls":37,"invalid":0,"complexity":[4,4,1,2]},"cdd":{"chunkCalls":1,"calls":37,"invalid":0,"complexity":[4,4,1,2]}},{"id":"C46","halving":{"chunkCalls":2,"calls":26,"invalid":0,"complexity":[5,4,3,3]},"cdd":{"chunkCalls":3,"calls":27,"invalid":0,"complexity":[5,4,3,3]}},{"id":"C48","halving":{"chunkCalls":2,"calls":79,"invalid":0,"complexity":[5,3,2,6]},"cdd":{"chunkCalls":3,"calls":80,"invalid":0,"complexity":[5,3,2,6]}},{"id":"C49","halving":{"chunkCalls":2,"calls":47,"invalid":0,"complexity":[5,4,3,5]},"cdd":{"chunkCalls":3,"calls":48,"invalid":0,"complexity":[5,4,3,5]}},{"id":"C50","halving":{"chunkCalls":6,"calls":31,"invalid":1,"complexity":[4,4,1,2]},"cdd":{"chunkCalls":6,"calls":31,"invalid":1,"complexity":[4,4,1,2]}},{"id":"C53","halving":{"chunkCalls":2,"calls":67,"invalid":0,"complexity":[4,5,3,5]},"cdd":{"chunkCalls":3,"calls":68,"invalid":0,"complexity":[4,5,3,5]}},{"id":"C54","halving":{"chunkCalls":2,"calls":54,"invalid":0,"complexity":[6,5,2,6]},"cdd":{"chunkCalls":3,"calls":55,"invalid":0,"complexity":[6,5,2,6]}},{"id":"C56","halving":{"chunkCalls":1,"calls":34,"invalid":0,"complexity":[4,4,1,2]},"cdd":{"chunkCalls":1,"calls":34,"invalid":0,"complexity":[4,4,1,2]}},{"id":"C57","halving":{"chunkCalls":2,"calls":109,"invalid":0,"complexity":[5,5,2,7]},"cdd":{"chunkCalls":3,"calls":110,"invalid":0,"complexity":[5,5,2,7]}},{"id":"C59","halving":{"chunkCalls":4,"calls":29,"invalid":1,"complexity":[3,5,7,2]},"cdd":{"chunkCalls":5,"calls":30,"invalid":1,"complexity":[3,5,7,2]}},{"id":"C60","halving":{"chunkCalls":3,"calls":31,"invalid":1,"complexity":[4,4,1,2]},"cdd":{"chunkCalls":3,"calls":31,"invalid":1,"complexity":[4,4,1,2]}},{"id":"C61","halving":{"chunkCalls":1,"calls":59,"invalid":0,"complexity":[4,4,2,5]},"cdd":{"chunkCalls":1,"calls":59,"invalid":0,"complexity":[4,4,2,5]}},{"id":"C62","halving":{"chunkCalls":1,"calls":147,"invalid":0,"complexity":[4,3,1,7]},"cdd":{"chunkCalls":1,"calls":147,"invalid":0,"complexity":[4,3,1,7]}},{"id":"C63","halving":{"chunkCalls":4,"calls":35,"invalid":1,"complexity":[4,4,4,4]},"cdd":{"chunkCalls":5,"calls":36,"invalid":1,"complexity":[4,4,4,4]}},{"id":"C64","halving":{"chunkCalls":4,"calls":44,"invalid":0,"complexity":[5,7,3,4]},"cdd":{"chunkCalls":5,"calls":45,"invalid":0,"complexity":[5,7,3,4]}},{"id":"C65","halving":{"chunkCalls":6,"calls":72,"invalid":0,"complexity":[6,9,5,5]},"cdd":{"chunkCalls":7,"calls":73,"invalid":0,"complexity":[6,9,5,5]}},{"id":"C67","halving":{"chunkCalls":4,"calls":28,"invalid":0,"complexity":[4,8,4,2]},"cdd":{"chunkCalls":5,"calls":29,"invalid":0,"complexity":[4,8,4,2]}},{"id":"C68","halving":{"chunkCalls":1,"calls":150,"invalid":0,"complexity":[5,4,2,4]},"cdd":{"chunkCalls":1,"calls":150,"invalid":0,"complexity":[5,4,2,4]}},{"id":"C69","halving":{"chunkCalls":2,"calls":117,"invalid":0,"complexity":[5,5,2,4]},"cdd":{"chunkCalls":3,"calls":118,"invalid":0,"complexity":[5,5,2,4]}},{"id":"C70","halving":{"chunkCalls":4,"calls":150,"invalid":1,"complexity":[5,6,4,4]},"cdd":{"chunkCalls":5,"calls":150,"invalid":1,"complexity":[5,6,4,4]}},{"id":"C71","halving":{"chunkCalls":2,"calls":51,"invalid":1,"complexity":[5,5,3,6]},"cdd":{"chunkCalls":3,"calls":52,"invalid":1,"complexity":[5,5,3,6]}},{"id":"C72","halving":{"chunkCalls":7,"calls":49,"invalid":1,"complexity":[4,8,5,3]},"cdd":{"chunkCalls":9,"calls":51,"invalid":1,"complexity":[4,8,5,3]}},{"id":"C73","halving":{"chunkCalls":4,"calls":82,"invalid":0,"complexity":[4,7,3,2]},"cdd":{"chunkCalls":5,"calls":83,"invalid":0,"complexity":[4,7,3,2]}},{"id":"C74","halving":{"chunkCalls":1,"calls":32,"invalid":0,"complexity":[5,4,2,6]},"cdd":{"chunkCalls":1,"calls":32,"invalid":0,"complexity":[5,4,2,6]}},{"id":"C75","halving":{"chunkCalls":4,"calls":150,"invalid":1,"complexity":[4,5,4,3]},"cdd":{"chunkCalls":5,"calls":150,"invalid":1,"complexity":[4,5,4,3]}},{"id":"C76","halving":{"chunkCalls":1,"calls":150,"invalid":0,"complexity":[6,4,1,6]},"cdd":{"chunkCalls":1,"calls":150,"invalid":0,"complexity":[6,4,1,6]}},{"id":"C77","halving":{"chunkCalls":2,"calls":53,"invalid":1,"complexity":[4,5,2,2]},"cdd":{"chunkCalls":3,"calls":54,"invalid":1,"complexity":[4,5,2,2]}},{"id":"C78","halving":{"chunkCalls":2,"calls":65,"invalid":0,"complexity":[5,4,2,5]},"cdd":{"chunkCalls":3,"calls":66,"invalid":0,"complexity":[5,4,2,5]}},{"id":"C79","halving":{"chunkCalls":2,"calls":45,"invalid":0,"complexity":[5,5,2,4]},"cdd":{"chunkCalls":3,"calls":46,"invalid":0,"complexity":[5,5,2,4]}},{"id":"C80","halving":{"chunkCalls":2,"calls":45,"invalid":0,"complexity":[5,4,2,4]},"cdd":{"chunkCalls":3,"calls":46,"invalid":0,"complexity":[5,4,2,4]}},{"id":"C81","halving":{"chunkCalls":2,"calls":150,"invalid":0,"complexity":[6,5,2,5]},"cdd":{"chunkCalls":3,"calls":150,"invalid":0,"complexity":[6,5,2,5]}},{"id":"C82","halving":{"chunkCalls":4,"calls":46,"invalid":0,"complexity":[5,5,3,5]},"cdd":{"chunkCalls":5,"calls":47,"invalid":0,"complexity":[5,5,3,5]}},{"id":"C83","halving":{"chunkCalls":1,"calls":28,"invalid":0,"complexity":[4,4,1,2]},"cdd":{"chunkCalls":1,"calls":28,"invalid":0,"complexity":[4,4,1,2]}},{"id":"C84","halving":{"chunkCalls":1,"calls":68,"invalid":0,"complexity":[4,3,1,7]},"cdd":{"chunkCalls":1,"calls":68,"invalid":0,"complexity":[4,3,1,7]}},{"id":"C85","halving":{"chunkCalls":4,"calls":73,"invalid":0,"complexity":[4,8,4,2]},"cdd":{"chunkCalls":5,"calls":74,"invalid":0,"complexity":[4,8,4,2]}},{"id":"C88","halving":{"chunkCalls":1,"calls":81,"invalid":0,"complexity":[5,6,2,3]},"cdd":{"chunkCalls":1,"calls":81,"invalid":0,"complexity":[5,6,2,3]}},{"id":"C90","halving":{"chunkCalls":2,"calls":39,"invalid":0,"complexity":[6,5,2,4]},"cdd":{"chunkCalls":3,"calls":40,"invalid":0,"complexity":[6,5,2,4]}},{"id":"C92","halving":{"chunkCalls":1,"calls":150,"invalid":0,"complexity":[6,4,1,6]},"cdd":{"chunkCalls":1,"calls":150,"invalid":0,"complexity":[6,4,1,6]}},{"id":"C93","halving":{"chunkCalls":3,"calls":51,"invalid":0,"complexity":[4,4,1,2]},"cdd":{"chunkCalls":3,"calls":51,"invalid":0,"complexity":[4,4,1,2]}},{"id":"C94","halving":{"chunkCalls":3,"calls":30,"invalid":1,"complexity":[4,4,1,2]},"cdd":{"chunkCalls":3,"calls":30,"invalid":1,"complexity":[4,4,1,2]}},{"id":"C95","halving":{"chunkCalls":4,"calls":150,"invalid":1,"complexity":[4,5,6,3]},"cdd":{"chunkCalls":5,"calls":150,"invalid":1,"complexity":[4,5,6,3]}},{"id":"C96","halving":{"chunkCalls":1,"calls":90,"invalid":0,"complexity":[5,4,2,6]},"cdd":{"chunkCalls":1,"calls":90,"invalid":0,"complexity":[5,4,2,6]}}]},"selection-final":{"experiment":"selection","budget":150,"workerHash":"f688dcadb8319ed9db1a9f95bc0520b1d65121154f8810f0f9de5f10da1227ec","rows":[{"mode":"first","id":"C00","signature":"R2: A ready control read data inconsistent with published inputs","raw":[3,4,2,1],"final":[3,4,2,1],"calls":9},{"mode":"first","id":"C01","signature":"S4: Residual work changed a completed view without new input","raw":[4,4,1,2],"final":[4,4,1,2],"calls":10},{"mode":"first","id":"C02","signature":"P1: Ordinary writes remain unpublished after all visible blocking demands have ended","raw":[3,4,2,2],"final":[3,4,2,2],"calls":15},{"mode":"first","id":"C03","signature":"O2: Authoritative publication still needs work outside its current requirements after the action body completes","raw":[4,5,1,2],"final":[4,4,1,2],"calls":17},{"mode":"first","id":"C04","signature":"G2: A ready update group remains unpublished: independent","raw":[4,7,4,3],"final":[4,7,4,3],"calls":24},{"mode":"first","id":"C05","signature":"O1: Ready optimistic source did not publish while its parent action remained open","raw":[4,5,2,2],"final":[4,5,2,2],"calls":19},{"mode":"first","id":"C07","signature":"S1: Published derivation disagrees with published input","raw":[4,5,2,3],"final":[4,5,2,3],"calls":19},{"mode":"first","id":"C09","signature":"R4: Tracked verdict is false while a distinct ordinary answer is held","raw":[5,4,1,4],"final":[5,4,1,4],"calls":29},{"mode":"queue","id":"C24","signature":"S1: Published derivation disagrees with published input","raw":[3,4,2,1],"final":[3,4,2,1],"calls":10},{"mode":"queue","id":"C00","signature":"R2: A ready control read data inconsistent with published inputs","raw":[3,4,2,1],"final":[3,4,2,1],"calls":9},{"mode":"queue","id":"C02","signature":"P1: Ordinary writes remain unpublished after all visible blocking demands have ended","raw":[3,4,2,2],"final":[3,4,2,2],"calls":15},{"mode":"queue","id":"C20","signature":"L1: Required reader did not publish its final answer","raw":[4,3,1,7],"final":[4,3,1,7],"calls":26},{"mode":"queue","id":"C01","signature":"S4: Residual work changed a completed view without new input","raw":[4,4,1,2],"final":[4,4,1,2],"calls":10},{"mode":"queue","id":"C29","signature":"S2: Published observation control disagrees with its reader","raw":[4,4,2,5],"final":[4,4,2,5],"calls":55},{"mode":"queue","id":"C03","signature":"O2: Authoritative publication still needs work outside its current requirements after the action body completes","raw":[4,5,1,2],"final":[4,4,1,2],"calls":17},{"mode":"queue","id":"C05","signature":"O1: Ready optimistic source did not publish while its parent action remained open","raw":[4,5,2,2],"final":[4,5,2,2],"calls":19}]},"holdout":[{"metadata":{"target":"e69d4769b25e695c010f34c592fb75be4e33cc8f","sourceHash":"121b9f37eda1ff56bd18f42561463278fef3ab8038ad23060b21806aa2cec513","workerHash":"c70b00b411a9facea718f85017ef9cd272363d6f5f6d4f58705703dad0780c2b","node":"v24.18.0","seed":91505,"ruleRevision":17,"waitingPolicy":"review","allowances":[],"cohort":"attachment"},"total":150,"executions":150,"statuses":{"pass":76,"fail":74},"coverage":{"detached-output-prepared":150,"observed-pending":124,"output-local":81,"reader-disposal":79,"resolve-newest":124,"write-source--1":150,"output-portal":69,"unrequired-flight":26,"residual-completion":16},"operations":1264,"requests":996,"maxReportedHeapBytes":10052048,"elapsedMs":3934.236585,"generationMs":10.843152000000003,"campaignMs":3375.0851819999993,"reductionMs":529.5687549999998,"reportingMs":17.997308000001,"reductionExecutions":{"initial":0,"search":62,"verification":2},"searchStatuses":{"fail":15,"invalid":1,"pass":46},"reducedCandidates":2,"reducedReproKeys":1,"casesPerSecond":38.12683776362168,"failureSymptomGroups":1,"findingSymptomGroups":1,"retainedCases":74,"workerErrors":0,"waitingFindings":0,"progressFindings":0,"waivedProgress":{},"groupScopes":{"output-attachment":150},"groupChecks":0,"groupHeld":0,"batchingHeld":0,"batchingCases":0,"possibleGroupJoins":0,"attachmentCases":150,"attachmentChecks":2700,"outputNodes":1083,"privateOutputWrites":394},{"metadata":{"target":"e69d4769b25e695c010f34c592fb75be4e33cc8f","sourceHash":"121b9f37eda1ff56bd18f42561463278fef3ab8038ad23060b21806aa2cec513","workerHash":"c70b00b411a9facea718f85017ef9cd272363d6f5f6d4f58705703dad0780c2b","node":"v24.18.0","seed":91501,"ruleRevision":17,"waitingPolicy":"review","allowances":[],"cohort":"branches"},"total":150,"executions":150,"statuses":{"pass":147,"policy":2,"fail":1},"coverage":{"await":70,"branch-true":102,"cancel-task":88,"explicit-flush":91,"observed-pending":79,"partial-source-observation":78,"residual-completion":87,"resolve-oldest":32,"task":132,"unrequired-flight":92,"write-source--1":135,"write-source--2":135,"reader-disposal":76,"branch-false":70,"pending-work-uses-multiple-sources":59,"promise":58,"resolve-newest":28,"microtask":79,"ordinary-progress-probe":6,"ordinary-progress-scope":10},"operations":1705,"requests":931,"maxReportedHeapBytes":12414216,"elapsedMs":2696.6601180000002,"generationMs":27.25885799999999,"campaignMs":315.90608,"reductionMs":2348.0376429999997,"reportingMs":5.088996000000407,"reductionExecutions":{"initial":0,"search":292,"verification":3},"searchStatuses":{"fail":22,"pass":223,"policy":47},"reducedCandidates":3,"reducedReproKeys":2,"casesPerSecond":55.62436252116515,"failureSymptomGroups":1,"findingSymptomGroups":2,"retainedCases":3,"workerErrors":0,"waitingFindings":2,"progressFindings":0,"waivedProgress":{},"groupScopes":{"branch-history":13,"changing-observation":135,"partial-anchors":1,"checked":1},"groupChecks":0,"groupHeld":0,"batchingHeld":0,"batchingCases":0,"possibleGroupJoins":0,"attachmentCases":0,"attachmentChecks":0,"outputNodes":0,"privateOutputWrites":0},{"metadata":{"target":"e69d4769b25e695c010f34c592fb75be4e33cc8f","sourceHash":"121b9f37eda1ff56bd18f42561463278fef3ab8038ad23060b21806aa2cec513","workerHash":"c70b00b411a9facea718f85017ef9cd272363d6f5f6d4f58705703dad0780c2b","node":"v24.18.0","seed":91502,"ruleRevision":17,"waitingPolicy":"review","allowances":[],"cohort":"observation"},"total":150,"executions":150,"statuses":{"pass":150},"coverage":{"cancel-task":69,"explicit-flush":98,"partial-source-observation":80,"reader-unmount-request":118,"task":136,"write-source--1":149,"await":74,"ordinary-progress-scope":70,"promise":67,"reader-mount-request":93,"residual-completion":93,"unrequired-flight":100,"microtask":86,"observed-pending":71,"ordinary-progress-probe":26,"owned-reader-created":63,"resolve-newest":33,"resolve-oldest":33,"no-visibility-anchor":66,"published-fallback":11,"witnessed-fallback-work":10,"fallback-beside-content":1},"operations":1677,"requests":903,"maxReportedHeapBytes":19064624,"elapsedMs":222.97146399999997,"generationMs":21.112512999999993,"campaignMs":193.671333,"reductionMs":0,"reportingMs":0.6519829999999729,"reductionExecutions":{"initial":0,"search":0,"verification":0},"searchStatuses":{},"reducedCandidates":0,"reducedReproKeys":0,"casesPerSecond":672.7318254500944,"failureSymptomGroups":0,"findingSymptomGroups":0,"retainedCases":0,"workerErrors":0,"waitingFindings":0,"progressFindings":0,"waivedProgress":{},"groupScopes":{"changing-observation":150},"groupChecks":0,"groupHeld":0,"batchingHeld":0,"batchingCases":0,"possibleGroupJoins":0,"attachmentCases":0,"attachmentChecks":0,"outputNodes":0,"privateOutputWrites":0},{"metadata":{"target":"e69d4769b25e695c010f34c592fb75be4e33cc8f","sourceHash":"121b9f37eda1ff56bd18f42561463278fef3ab8038ad23060b21806aa2cec513","workerHash":"c70b00b411a9facea718f85017ef9cd272363d6f5f6d4f58705703dad0780c2b","node":"v24.18.0","seed":91503,"ruleRevision":17,"waitingPolicy":"review","allowances":[],"cohort":"optimistic"},"total":150,"executions":150,"statuses":{"pass":124,"fail":26},"coverage":{"action-body-completed":150,"action-started":150,"optimistic-ready-with-parent-open":92,"task":132,"write-source--1":150,"write-source--2":150,"microtask":71,"promise":60,"await":55,"observed-pending":98,"resolve-oldest":45,"unrequired-flight":93,"residual-completion":77,"cancel-task":80,"resolve-newest":30},"operations":2116,"requests":790,"maxReportedHeapBytes":11596680,"elapsedMs":2193.6264189999997,"generationMs":24.276675999999995,"campaignMs":1087.339802,"reductionMs":1073.860204,"reportingMs":7.01740299999949,"reductionExecutions":{"initial":0,"search":73,"verification":2},"searchStatuses":{"fail":25,"pass":39,"invalid":9},"reducedCandidates":2,"reducedReproKeys":1,"casesPerSecond":68.3799204371271,"failureSymptomGroups":1,"findingSymptomGroups":1,"retainedCases":26,"workerErrors":0,"waitingFindings":0,"progressFindings":0,"waivedProgress":{},"groupScopes":{"optimistic-lifetime":150},"groupChecks":0,"groupHeld":0,"batchingHeld":0,"batchingCases":0,"possibleGroupJoins":0,"attachmentCases":0,"attachmentChecks":0,"outputNodes":0,"privateOutputWrites":0},{"metadata":{"target":"e69d4769b25e695c010f34c592fb75be4e33cc8f","sourceHash":"121b9f37eda1ff56bd18f42561463278fef3ab8038ad23060b21806aa2cec513","workerHash":"c70b00b411a9facea718f85017ef9cd272363d6f5f6d4f58705703dad0780c2b","node":"v24.18.0","seed":91500,"ruleRevision":17,"waitingPolicy":"review","allowances":[],"cohort":"ordinary"},"total":150,"executions":150,"statuses":{"pass":145,"fail":4,"policy":1},"coverage":{"await":72,"cancel-task":77,"explicit-flush":91,"microtask":90,"observed-pending":75,"ordinary-progress-probe":71,"ordinary-progress-scope":150,"residual-completion":80,"resolve-oldest":30,"task":132,"unrequired-flight":86,"write-source--1":147,"fallback-beside-content":2,"promise":75,"published-fallback":8,"reader-disposal":62,"witnessed-fallback-work":6,"resolve-newest":30,"progress-violation":4},"operations":1630,"requests":995,"maxReportedHeapBytes":14107952,"elapsedMs":2556.334652,"generationMs":18.014409999999998,"campaignMs":404.30445799999984,"reductionMs":2128.377931000001,"reportingMs":5.292688999999484,"reductionExecutions":{"initial":0,"search":234,"verification":3},"searchStatuses":{"fail":33,"invalid":2,"pass":173,"policy":26},"reducedCandidates":3,"reducedReproKeys":2,"casesPerSecond":58.67776344644254,"failureSymptomGroups":1,"findingSymptomGroups":2,"retainedCases":5,"workerErrors":0,"waitingFindings":1,"progressFindings":4,"waivedProgress":{},"groupScopes":{"changing-observation":149,"checked":1},"groupChecks":1,"groupHeld":1,"batchingHeld":0,"batchingCases":0,"possibleGroupJoins":0,"attachmentCases":0,"attachmentChecks":0,"outputNodes":0,"privateOutputWrites":0},{"metadata":{"target":"e69d4769b25e695c010f34c592fb75be4e33cc8f","sourceHash":"121b9f37eda1ff56bd18f42561463278fef3ab8038ad23060b21806aa2cec513","workerHash":"c70b00b411a9facea718f85017ef9cd272363d6f5f6d4f58705703dad0780c2b","node":"v24.18.0","seed":91504,"ruleRevision":17,"waitingPolicy":"review","allowances":[],"cohort":"readiness"},"total":150,"executions":150,"statuses":{"pass":149,"fail":1},"coverage":{"await":79,"cancel-task":78,"explicit-flush":94,"imperative-pending-read":150,"microtask":73,"no-visibility-anchor":70,"reader-unmount-request":62,"residual-completion":74,"resolve-newest":22,"task":129,"unrequired-flight":81,"write-source--1":149,"observed-pending":76,"promise":67,"ready-control-click":94,"resolve-oldest":32,"tracked-held-answer-verdict":49,"owned-reader-created":31,"reader-mount-request":42,"partial-source-observation":37,"published-fallback":5,"witnessed-fallback-work":4},"operations":3928,"requests":898,"maxReportedHeapBytes":19767672,"elapsedMs":1035.10755,"generationMs":28.494624,"campaignMs":297.71123,"reductionMs":706.1912179999999,"reportingMs":2.4475099999998235,"reductionExecutions":{"initial":0,"search":83,"verification":1},"searchStatuses":{"fail":15,"pass":68},"reducedCandidates":1,"reducedReproKeys":1,"casesPerSecond":144.91247793526384,"failureSymptomGroups":1,"findingSymptomGroups":1,"retainedCases":1,"workerErrors":0,"waitingFindings":0,"progressFindings":0,"waivedProgress":{},"groupScopes":{"changing-observation":150},"groupChecks":0,"groupHeld":0,"batchingHeld":0,"batchingCases":0,"possibleGroupJoins":0,"attachmentCases":0,"attachmentChecks":0,"outputNodes":0,"privateOutputWrites":0}]} diff --git a/packages/signals/tests/semantics/readiness.test.ts b/packages/signals/tests/semantics/readiness.test.ts new file mode 100644 index 000000000..b7c9f1ad6 --- /dev/null +++ b/packages/signals/tests/semantics/readiness.test.ts @@ -0,0 +1,170 @@ +import { readyControl } from "./fixtures.js"; +import { generate } from "./generate.js"; +import { runScenario } from "./runner.js"; +import { validate } from "./scenario.js"; +import { withWorker } from "./worker-fixture.js"; + +test("a rendered pending control guards an async read until it becomes ready", async () => { + const s = readyControl(); + const r = await runScenario(s); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect(r.clicks?.map(c => c.values)).toEqual([[4], [6]]); + expect(r.frames.some(f => JSON.stringify(f.outputs[0]) === '{"pending":true}')).toBe(true); + expect(r.frames.at(-1)?.outputs[0]).toEqual({ pending: false }); + expect(r.reads?.at(-1)?.verdict).toBe(false); +}); + +test("readiness generation preserves explicit clicks, pending probes and canonical replay", async () => { + await withWorker(async client => { + for (const s of generate(2928, 40, "readiness")) { + expect(validate(s)).toBeUndefined(); + const a = await client.run(s); + const b = await client.run(a.scenario); + expect(["pass", "fail", "policy", "error"], JSON.stringify(a)).toContain(a.status); + expect(b.status).toBe(a.status); + expect(b.frames).toEqual(a.frames); + expect(b.clicks).toEqual(a.clicks); + expect(b.reads).toEqual(a.reads); + } + }); +}); + +test("readiness verdicts are outside exact data-trace delivery equivalence", async () => { + const { variants } = await import("./equivalence.js"); + expect(() => variants(readyControl())).toThrow("excludes observable readiness verdicts"); +}); + +test("readiness reduction removes the feature and only its guarded clicks", async () => { + const { reductions } = await import("./shrink.js"); + const reduced = [...reductions(readyControl())].find( + s => s.readers.length && !s.readers[0].pending + ); + expect(reduced).toBeDefined(); + expect(validate(reduced!)).toBeUndefined(); + expect(JSON.stringify(reduced!.turns)).not.toContain('"click"'); + expect(JSON.stringify(reduced!.turns)).toContain('"write"'); +}); + +test("A24 catches false verdicts over distinct held answers without requiring a click", async () => { + const { checkPending } = await import("./rules.js"); + const s = readyControl(); + const frame = { + at: "after flush", + input: 0, + inputs: { [-1]: 0 }, + show: true, + outputs: { 0: { pending: false } } + }; + expect(checkPending(s, frame, { [-1]: 1 })).toMatchObject({ rule: "R4", reader: 0 }); + frame.outputs[0].pending = true; + expect(checkPending(s, frame, { [-1]: 1 })).toBeUndefined(); + frame.outputs[0].pending = false; + expect(checkPending(s, frame, { [-1]: 0 })).toBeUndefined(); + s.anchors = []; + expect(checkPending(s, frame, { [-1]: 1 })).toBeUndefined(); +}); + +test("A24's held-answer check does not infer pending from unrelated or equal-answer work", async () => { + const { checkPending } = await import("./rules.js"); + const s = readyControl(); + s.sources!.push(-2); + const frame = { + at: "after flush", + input: 0, + inputs: { [-1]: 0, [-2]: 0 }, + show: true, + outputs: { 0: { pending: false } } + }; + expect(checkPending(s, frame, { [-1]: 0, [-2]: 1 })).toBeUndefined(); + s.nodes[0].factor = 0; + expect(checkPending(s, frame, { [-1]: 1, [-2]: 1 })).toBeUndefined(); + s.nodes[0].factor = 2; + s.readers[0].mounted = true; + expect(checkPending(s, frame, { [-1]: 1, [-2]: 1 })).toBeUndefined(); +}); + +test("the held sync-memo verdict candidate is independent of completion probes", async () => { + const s = readyControl(); + s.nodes = [ + { id: 0, deps: [-1], factor: 1, offset: 0, delivery: "sync" }, + { id: 1, deps: [-1], factor: 1, offset: 0, delivery: "manual" } + ]; + s.readers[0].refs = [0]; + s.readers.push({ id: 1, refs: [1], gated: false, boundary: "none" }); + s.turns = [{ steps: [{ op: "write", value: 1 }] }]; + const a = await runScenario(s); + const b = await runScenario(s, { completionProbes: false }); + expect(b.status).toBe(a.status); + expect(b.failure).toEqual(a.failure); + expect(b.frames).toEqual(a.frames); + expect(a.coverage).toContain("tracked-held-answer-verdict"); +}); + +test.each([1, 2, 3])( + "pending verdicts propagate through %i synchronous numeric memos", + async pendingDepth => { + const s = readyControl(); + s.readers[0].pendingDepth = pendingDepth; + const r = await runScenario(s); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect(r.coverage).toContain("derived-pending-verdict"); + expect(r.clicks?.map(c => c.values)).toEqual([[4], [6]]); + expect( + r.frames.some( + f => + typeof f.outputs[0] === "object" && !Array.isArray(f.outputs[0]) && f.outputs[0].pending + ) + ).toBe(true); + } +); + +test("derived readiness has structural reductions and exact replay", async () => { + const { reductions } = await import("./shrink.js"); + await withWorker(async client => { + for (const s of generate(2928, 40, "derived-readiness")) { + expect(validate(s)).toBeUndefined(); + expect( + [...reductions(s)].some( + c => + !validate(c) && + c.readers.length === s.readers.length && + c.readers[0].pendingDepth !== s.readers[0].pendingDepth + ) + ).toBe(true); + const a = await client.run(s); + const b = await client.run(a.scenario); + expect(["pass", "fail", "policy", "error"], JSON.stringify(a)).toContain(a.status); + expect(b.status).toBe(a.status); + expect(b.frames).toEqual(a.frames); + expect(b.clicks).toEqual(a.clicks); + } + }); +}); + +test("an optimistic override is verdict-inert while its parent is held (A24)", async () => { + const s = readyControl(); + s.sources = [-1, -2]; + s.nodes = []; + s.readers[0].refs = [-2]; + s.readers[0].pendingDepth = 2; + s.optimistic = { proposals: [1], authoritative: 0, hold: true }; + s.turns = [{ steps: [{ op: "start-action" }] }, { steps: [{ op: "click", reader: 0 }] }]; + const r = await runScenario(s); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect(r.clicks?.at(-1)?.values).toEqual([1]); + expect(r.frames.at(-1)?.outputs[0]).toEqual({ pending: false }); +}); + +test("optimistic readiness generation keeps guarded clicks and reproducible lifetimes", async () => { + await withWorker(async client => { + for (const s of generate(2928, 40, "optimistic-readiness")) { + expect(validate(s)).toBeUndefined(); + const a = await client.run(s); + const b = await client.run(a.scenario); + expect(["pass", "fail", "policy", "error"], JSON.stringify(a)).toContain(a.status); + expect(b.status).toBe(a.status); + expect(b.frames).toEqual(a.frames); + expect(b.clicks).toEqual(a.clicks); + } + }); +}); diff --git a/packages/signals/tests/semantics/reads.test.ts b/packages/signals/tests/semantics/reads.test.ts new file mode 100644 index 000000000..fd69e0a56 --- /dev/null +++ b/packages/signals/tests/semantics/reads.test.ts @@ -0,0 +1,137 @@ +import { chain } from "./fixtures.js"; +import { runScenario } from "./runner.js"; +import { runEquivalence, shrinkEquivalence, variants } from "./equivalence.js"; +import { generate } from "./generate.js"; +import { validate, type Scenario } from "./scenario.js"; + +test("explicit ordinary reads see publication before and after the scheduled flush", async () => { + const s = chain("sync"); + s.turns = [ + { + steps: [ + { op: "write", value: 1 }, + { op: "read", mode: "plain", ref: -1 } + ] + }, + { + steps: [ + { op: "read", mode: "plain", ref: -1 }, + { op: "read", mode: "plain", ref: 1 } + ] + } + ]; + const r = await runScenario(s); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect(r.reads?.map(r => r.value)).toEqual([0, 1, 6]); +}); + +test("early latest warming adds no live reader and agrees after an unblocked update", async () => { + const s = chain("sync"); + s.turns.push({ steps: [{ op: "read", ref: -1, mode: "latest" }] }); + const r = await runEquivalence(s, runScenario, "latest"); + expect(r.result.status, JSON.stringify(r)).toBe("pass"); + expect(r.pair[0].scenario.readers).toEqual(r.pair[1].scenario.readers); + expect(r.pair[0].scenario.warmLatest).toEqual([]); + expect(r.pair[1].scenario.warmLatest).toEqual([-1]); +}); + +test("an unobserved async memo is not required to have its source's new value", async () => { + const s = chain("manual"); + s.readers = []; + s.turns.push({ steps: [{ op: "read", mode: "plain", ref: 0 }] }); + const r = await runScenario(s); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect(r.coverage).toContain("imperative-read-without-published-witness"); +}); + +test("paired latest reduction preserves the read that exposes the difference", async () => { + const s = chain("sync"); + s.turns.push({ steps: [{ op: "read", ref: -1, mode: "latest" }] }); + const run = async (scenario: Scenario) => { + const r = await runScenario(scenario); + // Deliberately wrong returned sample, only for calibrating paired reduction. + if (scenario.warmLatest?.length && r.reads?.length) r.reads[0].value = 99; + return r; + }; + const original = await runEquivalence(s, run, "latest"); + expect(original.signature).toContain("E4"); + const reduced = await shrinkEquivalence(s, run, 50, "latest"); + expect(reduced.accepted).toBeGreaterThan(0); + expect((await runEquivalence(reduced.scenario, run, "latest")).signature).toBe( + original.signature + ); + expect((await runEquivalence(reduced.scenario, runScenario, "latest")).result.status).toBe( + "pass" + ); +}); + +test("generated imperative reads preserve canonical scheduling and replay", async () => { + for (const s of generate(3336, 30, "reads")) { + expect(validate(s)).toBeUndefined(); + const a = await runScenario(s); + const b = await runScenario(a.scenario); + expect(["pass", "fail", "policy"]).toContain(a.status); + expect(b.status).toBe(a.status); + expect(b.reads).toEqual(a.reads); + expect(b.frames).toEqual(a.frames); + } +}); + +test("paired replay keeps each side's request choice, including a missing request", () => { + const s = chain(); + s.turns.push({ + steps: [ + { op: "resolve", node: 0, which: "newest", variantKeys: ["0:[1]#1", null] }, + { op: "read", ref: -1, mode: "latest" } + ] + }); + const [cold, warm] = variants(s, "latest"); + expect(cold.strict).toBe(true); + expect(cold.turns.at(-1)).toEqual({ + steps: [ + { op: "resolve", node: 0, which: "newest", key: "0:[1]#1" }, + { op: "read", ref: -1, mode: "latest" } + ] + }); + expect(warm.turns.at(-1)).toMatchObject({ steps: [{ op: "noop" }, { op: "read" }] }); + expect(() => + variants( + { + ...s, + turns: [{ steps: [{ op: "resolve", node: 0, which: "newest", variantKeys: ["0:[1]#1"] }] }] + }, + "latest" + ) + ).toThrow("Missing paired request identity"); +}); + +test("plain getters and memo reads through latest leave the outside-read contract provisional", async () => { + const s: Scenario = { + version: 2, + sources: [-1, -2], + show: true, + optimistic: { kind: "latest", proposals: [1], authoritative: 1, hold: true }, + nodes: [ + { id: 0, deps: [-2], factor: 1, offset: 0, delivery: "sync" }, + { id: 1, deps: [0], factor: 1, offset: 0, delivery: "manual" } + ], + readers: [{ id: 0, refs: [-2, 0, 1], gated: false, boundary: "none" }], + turns: [ + { steps: [{ op: "start-action" }] }, + { + steps: [ + { op: "read", mode: "plain", ref: -2 }, + { op: "read", mode: "plain", ref: 0 }, + { op: "read", mode: "plain", ref: -1 } + ] + } + ] + }; + const r = await runScenario(s); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect(r.coverage).toContain("imperative-latest-contract-provisional"); + expect(r.reads).toHaveLength(3); + // Retain latest samples as evidence without asserting staged or published. + expect(r.reads![2].value).toBe(0); + expect(r.frames.at(-1)?.outputs[0]).toEqual([1, 1, 1]); +}); diff --git a/packages/signals/tests/semantics/reduce.test.ts b/packages/signals/tests/semantics/reduce.test.ts new file mode 100644 index 000000000..3bab4dbe8 --- /dev/null +++ b/packages/signals/tests/semantics/reduce.test.ts @@ -0,0 +1,192 @@ +import { chain } from "./fixtures.js"; +import { canonicalize, structuralReductions } from "./reduce.js"; +import { validate, type Scenario } from "./scenario.js"; +import { runScenario } from "./runner.js"; + +const candidates = (s: Scenario) => [...structuralReductions(s)].filter(c => !validate(c)); + +test("reader deletion prunes its subtree and targeted operations, keeping event boundaries", () => { + const s: Scenario = { + ...chain("sync"), + version: 2, + sources: [-1], + readers: [ + { id: 0, refs: [1], gated: false, boundary: "none", mounted: true, pending: true }, + { id: 1, refs: [-1], gated: false, boundary: "none" } + ], + turns: [ + { + steps: [ + { + op: "queue", + id: 4, + via: "task", + steps: [ + { op: "mount", reader: 0, value: false }, + { op: "click", reader: 0 }, + { op: "write", value: 1 } + ] + } + ] + }, + { task: 4 } + ] + }; + const original = structuredClone(s); + const c = candidates(s).find(c => c.readers.length === 1 && c.readers[0].id === 1)!; + expect(c.turns).toEqual([ + { + steps: [ + { + op: "queue", + id: 4, + via: "task", + steps: [{ op: "noop" }, { op: "noop" }, { op: "write", value: 1 }] + } + ] + }, + { task: 4 } + ]); + expect(s).toEqual(original); + s.turns = []; + s.readers = [ + { id: 0, refs: [1], gated: false, boundary: "retain" }, + { id: 1, refs: [1], gated: false, boundary: "retain", parent: 0 }, + { id: 2, refs: [1], gated: false, boundary: "none", parent: 1 }, + { id: 3, refs: [-1], gated: false, boundary: "none" } + ]; + expect(candidates(s).some(c => c.readers.length === 1 && c.readers[0].id === 3)).toBe(true); +}); + +test("bypass rewires branches, warmups and reads but never redirects a surviving flight", () => { + const s: Scenario = { + ...chain("manual"), + version: 2, + sources: [-1], + warmLatest: [0], + nodes: [ + { id: 0, deps: [-1], factor: 1, offset: 0, delivery: "manual" }, + { + id: 1, + deps: [0], + factor: 1, + offset: 0, + delivery: "manual", + branch: { condition: 0, otherwise: [0] } + } + ], + turns: [ + { + steps: [ + { op: "resolve", node: 0, which: "newest", key: "0:[1]#1" }, + { op: "resolve", node: 1, which: "oldest", key: "1:[1,1]#2" }, + { op: "read", ref: 0, mode: "latest" } + ] + } + ] + }; + const original = structuredClone(s); + const c = candidates(s).find(c => c.nodes.length === 1 && c.nodes[0].id === 1)!; + expect(c.nodes[0]).toMatchObject({ deps: [-1], branch: { condition: -1, otherwise: [-1] } }); + expect(c.warmLatest).toEqual([-1]); + expect(c.turns).toEqual([ + { + steps: [ + { op: "noop" }, + { op: "resolve", node: 1, which: "oldest", key: "1:[1,1]#2" }, + { op: "read", ref: -1, mode: "latest" } + ] + } + ]); + expect(s).toEqual(original); +}); + +test("dead chains and removed deliveries drop only their own resolve operations", () => { + const s = chain("await"); + s.turns = [ + { + steps: [ + { + op: "queue", + id: 1, + via: "promise", + step: { + op: "resolve", + node: 0, + which: "oldest", + key: "0:[1]#1", + variantKeys: [null, "0:[1]#1"] + } + } + ] + } + ]; + const c = candidates(s).find(c => c.nodes.length === 2 && c.nodes[0].delivery === "sync")!; + expect(c.turns).toEqual([ + { steps: [{ op: "queue", id: 1, via: "promise", step: { op: "noop" } }] } + ]); + s.readers[0].refs = [-1]; + expect( + candidates(s).some( + c => + !c.nodes.length && + JSON.stringify(c.turns) === + JSON.stringify([ + { steps: [{ op: "queue", id: 1, via: "promise", step: { op: "noop" } }] } + ]) + ) + ).toBe(true); +}); + +test("queue deletion repairs its task and cancel references", () => { + const s = chain("sync"); + s.turns = [ + { steps: [{ op: "queue", id: 0, via: "task", step: { op: "write", value: 1 } }] }, + { + steps: [ + { op: "cancel", id: 0 }, + { op: "write", value: 2 } + ] + }, + { task: 0 } + ]; + expect( + candidates(s).some( + c => + JSON.stringify(c.turns) === + JSON.stringify([{ steps: [] }, { steps: [{ op: "write", value: 2 }] }, { steps: [] }]) + ) + ).toBe(true); +}); + +test("canonicalization removes noop debris without changing execution or timing", async () => { + const s = chain("promise"); + const debris = { op: "noop" as const, node: 0, which: "newest", key: "obsolete" }; + s.turns.push({ steps: [{ op: "queue", id: 2, via: "microtask", step: debris }] }, { steps: [] }); + const c = canonicalize(s); + expect(c.turns[1]).toEqual({ + steps: [{ op: "queue", id: 2, via: "microtask", step: { op: "noop" } }] + }); + expect(c.turns[2]).toEqual({ steps: [] }); + const before = await runScenario(s), + after = await runScenario(c); + expect(after.frames).toEqual(before.frames); + expect(after.events).toEqual(before.events); + expect(canonicalize(c)).toBe(c); +}); + +test("a bypass that changes an explicitly resolved downstream question is rejected", async () => { + const s = chain("manual"); + s.nodes[1].delivery = "manual"; + s.turns = [ + { steps: [{ op: "write", value: 1 }] }, + { steps: [{ op: "resolve", node: 0, which: "oldest", key: "0:[1]#1" }] }, + { steps: [{ op: "resolve", node: 1, which: "oldest", key: "1:[3]#1" }] } + ]; + s.strict = true; + expect((await runScenario(s)).status).toBe("pass"); + const bypassed = candidates(s).find(c => c.nodes.length === 1 && c.nodes[0].id === 1)!; + const result = await runScenario(bypassed); + expect(result.status).toBe("invalid"); + expect(result.error).toContain("1:[3]#1"); +}); diff --git a/packages/signals/tests/semantics/reduce.ts b/packages/signals/tests/semantics/reduce.ts new file mode 100644 index 000000000..4bd692b2c --- /dev/null +++ b/packages/signals/tests/semantics/reduce.ts @@ -0,0 +1,1316 @@ +import { + anchorIds, + sourceIds, + validate, + dependencies, + queuedSteps, + type Leaf, + type MemoSpec, + type ReaderSpec, + type Turn, + type Scenario, + type Step +} from "./scenario.js"; + +import { normalize, scenarioKey } from "./normalize.js"; + +function without(items: T[], index: number): T[] { + const copy = items.slice(); + copy.splice(index, 1); + return copy; +} + +function replace(items: T[], index: number, item: T): T[] { + const copy = items.slice(); + copy[index] = item; + return copy; +} + +/** Rewrite payloads without erasing callback or host-task boundaries. */ +function leaves(s: Scenario, change: (leaf: Leaf) => Leaf): Scenario { + let turns = s.turns; + for (let i = 0; i < turns.length; i++) { + const turn = turns[i]; + if (!("steps" in turn)) continue; + let steps = turn.steps; + for (let j = 0; j < steps.length; j++) { + const step = steps[j]; + let next: Step = step; + if (step.op === "queue") { + const payload = queuedSteps(step); + let changed = payload; + for (let k = 0; k < payload.length; k++) { + const value = change(payload[k]); + if (value !== payload[k]) { + if (changed === payload) changed = payload.slice(); + changed[k] = value; + } + } + if (changed !== payload) + next = step.step ? { ...step, step: changed[0] } : { ...step, steps: changed }; + } else if (step.op !== "cancel") next = change(step); + if (next !== step) { + if (steps === turn.steps) steps = steps.slice(); + steps[j] = next; + } + } + if (steps !== turn.steps) { + if (turns === s.turns) turns = s.turns.slice(); + turns[i] = { steps }; + } + } + return turns === s.turns ? s : { ...s, turns }; +} + +/** A failed loose resolve used to retain its node/key fields after becoming a + * noop. Clean the representation, but keep the noop's scheduling position. */ +export function canonicalize(s: Scenario): Scenario { + return leaves(s, leaf => { + if (leaf.op === "noop") for (const key in leaf) if (key !== "op") return { op: "noop" }; + return leaf; + }); +} + +function removeReader(s: Scenario, id: number): Scenario { + const removed = new Set([id]); + const readers: ReaderSpec[] = []; + // Parents precede children; removing a region removes its subtree. + for (const r of s.readers) { + if (r.parent !== undefined && removed.has(r.parent)) removed.add(r.id); + if (!removed.has(r.id)) readers.push(r); + } + return leaves({ ...s, readers }, leaf => + (leaf.op === "mount" || leaf.op === "dispose" || leaf.op === "click") && + removed.has(leaf.reader) + ? { op: "noop" } + : leaf + ); +} + +function replaceRef(refs: number[], from: number, to: number): number[] { + let result = refs; + for (let i = 0; i < refs.length; i++) + if (refs[i] === from) { + if (result === refs) result = refs.slice(); + result[i] = to; + } + return result; +} + +/** Possible numeric inputs, in read order, without repeated candidates. */ +function inputRefs(n: MemoSpec): number[] { + const refs: number[] = []; + for (const ref of n.deps) if (!refs.includes(ref)) refs.push(ref); + if (n.branch) for (const ref of n.branch.otherwise) if (!refs.includes(ref)) refs.push(ref); + return refs; +} + +/** Replace richer observation mechanisms with simpler existing ones. These + * change ownership/attachment and must replay; they are not normalizations. */ +export function* observationReductions(s: Scenario): Generator { + let usesShow = s.version === 2 && s.anchorShow !== false; + const addressed = new Set(); + for (const r of s.readers) if (r.gated) usesShow = true; + for (const turn of s.turns) { + if (!("steps" in turn)) continue; + for (const step of turn.steps) { + const payload = step.op === "queue" ? queuedSteps(step) : [step]; + for (const leaf of payload) { + if (leaf.op === "show") usesShow = true; + if (leaf.op === "mount" || leaf.op === "dispose" || leaf.op === "click") + addressed.add(leaf.reader); + } + } + } + for (let i = 0; i < s.readers.length; i++) { + const r = s.readers[i]; + // Reuse the global visibility signal only when it has no existing users + // or setters. Other independently mounted readers keep their controls. + if (!usesShow && r.mounted !== undefined) { + const reader = { ...r, gated: true }; + delete reader.mounted; + yield leaves({ ...s, show: r.mounted, readers: replace(s.readers, i, reader) }, x => + x.op === "mount" && x.reader === r.id ? { op: "show", value: x.value } : x + ); + } + if ( + s.version !== 2 || + r.gated || + r.boundary !== "none" || + r.parent !== undefined || + r.mounted !== undefined || + r.pending || + r.render || + addressed.has(r.id) + ) + continue; + const anchors = anchorIds(s); + const projected = anchors.slice(); + const refs: number[] = []; + let sources = 0; + for (const ref of r.refs) { + if (ref >= 0) refs.push(ref); + else { + sources++; + if (!projected.includes(ref)) projected.push(ref); + } + } + if (!sources || (!refs.length && projected.length === anchors.length)) continue; + yield { + ...s, + readers: refs.length ? replace(s.readers, i, { ...r, refs }) : without(s.readers, i), + anchors: projected + }; + } +} + +/** Replace observation scaffolding with one fixed data reader. This is a + * new observation shape, not a claim that boundaries or effects are equivalent. */ +export function* observationCollapseReductions(s: Scenario): Generator { + if (!s.readers.length) return; + for (const r of s.readers) if (r.render) return; + const first = s.readers[0]; + if ( + s.readers.length === 1 && + !first.gated && + first.boundary === "none" && + first.mounted === undefined && + !first.pending + ) + return; + const refs: number[] = []; + for (const r of s.readers) for (const ref of r.refs) if (!refs.includes(ref)) refs.push(ref); + const candidate = leaves( + { ...s, readers: [{ id: first.id, refs, gated: false, boundary: "none" }] }, + x => (x.op === "mount" || x.op === "dispose" || x.op === "click" ? { op: "noop" } : x) + ); + yield pruneCoordinated(candidate); +} + +/** These intentionally change the program, including its optimistic lifetime. */ +export function* lifecycleReductions(s: Scenario): Generator { + const o = s.optimistic; + if (!o || s.actions || s.warmLatest?.length) return; + if (o.kind === "latest") + yield { ...s, optimistic: { ...o, kind: "override", viaMemo: undefined } }; + + if (o.proposals.length === 1 && o.proposals[0] === 0 && o.authoritative !== 0) + for (let i = 0; i < s.turns.length; i++) { + const t = s.turns[i]; + if (!("steps" in t)) continue; + for (let j = 1; j < t.steps.length; j++) + if (t.steps[j - 1].op === "start-action" && t.steps[j].op === "resume-action") + yield { + ...s, + optimistic: { ...o, proposals: [o.authoritative], authoritative: 0 }, + turns: replace(s.turns, i, { steps: without(t.steps, j) }) + }; + } + + if (s.version !== 2 || s.sources?.length !== 2 || s.anchors?.includes(-1)) return; + for (const n of s.nodes) if (dependencies(n).includes(-1)) return; + for (const r of s.readers) if (r.refs.includes(-1) || r.render?.on === -1) return; + for (const t of s.turns) { + if (!("steps" in t)) return; + for (const x of t.steps) + if ( + x.op === "queue" || + x.op === "cancel" || + x.op === "resolve" || + (x.op === "read" && x.ref === -1) || + (x.op === "write" && (x.source ?? -1) === -1) + ) + return; + } + let proposal = -1; + const rewritten = leaves(s, x => { + if (x.op === "start-action") { + if (proposal !== -1) return { op: "noop" }; + proposal = 0; + return { op: "write", source: -2, value: o.proposals[0] }; + } + if (x.op === "resume-action") { + if (proposal < 0 || proposal >= o.proposals.length) return { op: "noop" }; + proposal++; + return { + op: "write", + source: -2, + value: proposal < o.proposals.length ? o.proposals[proposal] : o.authoritative + }; + } + return x; + }); + // Both roles become one ordinary source. A later implicit action completion + // disappears; discovery checks the resulting program, not lifetime equivalence. + yield mergeSource({ ...rewritten, optimistic: undefined }, -2); +} + +/** Remove one queue boundary as an enabling edit, then let ordinary pruning run. */ +export function* scheduleReductions(s: Scenario): Generator { + for (let i = 0; i < s.turns.length; i++) { + const t = s.turns[i]; + if (!("steps" in t)) continue; + for (let j = 0; j < t.steps.length; j++) { + const q = t.steps[j]; + if (q.op !== "queue" || q.via !== "microtask") continue; + const payload = queuedSteps(q); + if (payload.length !== 1 || payload[0].op !== "show") continue; + let addressed = false; + for (const turn of s.turns) { + if ("task" in turn) { + if (turn.task === q.id) addressed = true; + } else + for (const step of turn.steps) + if (step.op === "cancel" && step.id === q.id) addressed = true; + } + if (addressed) continue; + const turns = s.turns.slice(); + turns.splice(i, 1, { steps: without(t.steps, j) }, { steps: payload.slice() }); + yield { ...s, turns }; + } + } +} + +/** Collapse [source, affine(source)] while translating its selected async flight. */ +export function* coneReductions(s: Scenario): Generator { + for (const parent of s.nodes) { + const source = parent.deps[0]; + if (parent.delivery !== "sync" || parent.branch || source >= 0) continue; + if (parent.deps.some(ref => ref !== source)) continue; + let child: MemoSpec | undefined; + let shared = false; + for (const n of s.nodes) + if (dependencies(n).includes(parent.id)) { + if (child) shared = true; + child = n; + } + if (!child || shared || child.delivery === "sync" || child.branch) continue; + const position = child.deps.indexOf(source); + if (position < 0 || child.deps.some(ref => ref !== source && ref !== parent.id)) continue; + if (s.warmLatest?.includes(parent.id)) continue; + if (s.readers.some(r => r.refs.includes(parent.id) || r.render?.on === parent.id)) continue; + for (const turn of s.turns) + if ("steps" in turn) + for (const step of turn.steps) + for (const x of step.op === "queue" ? queuedSteps(step) : [step]) + if (x.op === "read" && x.ref === parent.id) shared = true; + if (shared) continue; + const translated = translateQuestions(s, child.id, inputs => { + if (inputs.length !== child.deps.length) return; + const value = inputs[position]; + const sum = value * parent.deps.length; + const product = sum * parent.factor; + const derived = product + parent.offset; + if (![sum, product, derived].every(Number.isSafeInteger)) return; + for (let i = 0; i < inputs.length; i++) + if (inputs[i] !== (child.deps[i] === source ? value : derived)) return; + return [value]; + }); + if (!translated) continue; + const nodes: MemoSpec[] = []; + for (const n of s.nodes) + if (n.id !== parent.id) + nodes.push(n === child ? { ...n, deps: [source], factor: 1, offset: 0 } : n); + yield { ...translated, nodes }; + } +} + +/** Finish a coordinated graph edit without spending replays deleting dead nodes. */ +function pruneCoordinated(s: Scenario): Scenario { + for (const candidate of pruningReductions(s)) s = candidate; + return s; +} + +/** Fold a conditional into its consumer, or shorten both paths together. + * These remove reactive boundaries intentionally; they are not equivalences. */ +export function* branchReductions(s: Scenario): Generator { + for (let i = 0; i < s.nodes.length; i++) { + const n = s.nodes[i]; + if (!n.branch) continue; + if (n.factor === 1 && n.offset === 0) + for (let j = i + 1; j < s.nodes.length; j++) { + const child = s.nodes[j]; + if (child.branch || !child.deps.includes(n.id)) continue; + const deps: number[] = [], + otherwise: number[] = []; + for (const ref of child.deps) { + if (ref === n.id) { + deps.push(...n.deps); + otherwise.push(...n.branch.otherwise); + } else { + deps.push(ref); + otherwise.push(ref); + } + } + // Retain exact keys on surviving computations. If the changed tuple no + // longer reaches a selected flight, normal strict replay rejects it. + yield pruneCoordinated({ + ...s, + nodes: replace(s.nodes, j, { + ...child, + deps, + branch: { condition: n.branch.condition, otherwise } + }) + }); + } + + // A chain in each arm can require both bypasses before the bug reappears. + // Try immediate upstream inputs only, avoiding arbitrary expression search. + for (let a = 0; a < n.deps.length; a++) { + const left = s.nodes.find(p => p.id === n.deps[a]); + if (!left) continue; + for (let b = 0; b < n.branch.otherwise.length; b++) { + const right = s.nodes.find(p => p.id === n.branch!.otherwise[b]); + if (!right) continue; + for (const x of inputRefs(left)) + for (const y of inputRefs(right)) + yield pruneCoordinated({ + ...s, + nodes: replace(s.nodes, i, { + ...n, + deps: replace(n.deps, a, x), + branch: { ...n.branch, otherwise: replace(n.branch.otherwise, b, y) } + }) + }); + } + } + } +} + +/** A short menu of alternative event boundaries, not arbitrary permutations. */ +function* eventAlternatives(s: Scenario): Generator { + for (let i = 0; i < s.turns.length; i++) { + const t = s.turns[i]; + if (!("steps" in t)) continue; + for (let j = 1; j < t.steps.length - 1; j++) { + if (t.steps[j].op !== "flush") continue; + const turns = s.turns.slice(); + turns.splice(i, 1, { steps: t.steps.slice(0, j) }, { steps: t.steps.slice(j + 1) }); + yield { ...s, turns }; + } + } +} + +/** Change async mechanisms as a group, optionally with a scheduler boundary. + * Paired edits are replayed directly, even if either edit alone would pass. + * The shrinker's shared seen set and budget bound this alternative search. */ +export function* deliveryScheduleReductions(s: Scenario): Generator { + let count = 0; + for (const n of s.nodes) if (n.delivery !== "sync") count++; + if (!count) return; + for (const delivery of ["promise", "manual", "await", "sync"] as const) { + let nodes = s.nodes; + const changed = new Set(); + for (let i = 0; i < s.nodes.length; i++) { + const n = s.nodes[i]; + if (n.delivery === "sync" || n.delivery === delivery) continue; + if (nodes === s.nodes) nodes = s.nodes.slice(); + nodes[i] = { ...n, delivery }; + changed.add(n.id); + } + let candidate = nodes === s.nodes ? s : { ...s, nodes }; + if (delivery !== "manual" && changed.size) + candidate = leaves(candidate, x => + x.op === "resolve" && changed.has(x.node) ? { op: "noop" } : x + ); + // Automatic promise/sync forms are preferred. Manual gates and an extra + // await are enabling edits only: pair them with removing a flush or node. + // This avoids spending the budget cycling through equivalent-size forms. + if (candidate !== s && (delivery === "promise" || delivery === "sync")) yield candidate; + yield* eventAlternatives(candidate); + if (candidate !== s && (delivery === "manual" || delivery === "await")) + for (let i = 0; i < candidate.nodes.length; i++) + for (const ref of inputRefs(candidate.nodes[i])) + yield pruneCoordinated(bypass(candidate, i, ref)); + } + // Keep mixed delivery when it matters. Changing one memo's async boundary + // and bypassing another is a bounded two-edit candidate, not a recursive + // search through arbitrary passing programs. + for (let i = 0; i < s.nodes.length; i++) { + const n = s.nodes[i]; + for (const delivery of ["promise", "manual", "await"] as const) { + if (n.delivery === delivery) continue; + let candidate = { ...s, nodes: replace(s.nodes, i, { ...n, delivery }) }; + if (delivery !== "manual") + candidate = leaves(candidate, x => + x.op === "resolve" && x.node === n.id ? { op: "noop" } : x + ); + for (let j = 0; j < s.nodes.length; j++) { + if (i === j) continue; + for (const ref of inputRefs(s.nodes[j])) yield pruneCoordinated(bypass(candidate, j, ref)); + } + } + } +} + +/** A merge can destroy old/new answer equality. Try a small offset repair in + * the same candidate; strict replay decides whether the new program fails. */ +export function* mergeArithmeticReductions(s: Scenario): Generator { + if (s.version !== 2 || s.optimistic || s.sources!.length < 2) return; + for (const source of s.sources!) { + if (source === -1) continue; + const merged = mergeSource(s, source); + for (let i = 0; i < merged.nodes.length; i++) + for (const offset of [0, 1, -1, 2]) { + const n = merged.nodes[i]; + if (offset === n.offset) continue; + yield { ...merged, nodes: replace(merged.nodes, i, { ...n, offset }) }; + } + } +} + +/** Reordering alone or changing delivery alone may hide the failure. Prefer + * one canonical direction; allow an uphill pivot only with cone removal. */ +export function* deliveryOrderingReductions(s: Scenario): Generator { + const key = scenarioKey(s); + function* offer(candidate: Scenario): Generator { + if (validate(candidate)) return; + const named = normalize(candidate); + if (candidate.nodes.length < s.nodes.length || JSON.stringify(named) < key) yield named; + } + for (let i = 0; i < s.nodes.length; i++) { + const n = s.nodes[i]; + for (const delivery of ["promise", "manual", "await"] as const) { + if (delivery === n.delivery) continue; + const nodes = replace(s.nodes, i, { ...n, delivery }); + // Leave exact keys intact: unlike removal, changing the delivery here + // does not excuse silently resolving a different request. + for (let j = 1; j < nodes.length; j++) { + const reordered = nodes.slice(); + reordered[j - 1] = nodes[j]; + reordered[j] = nodes[j - 1]; + const candidate = { ...s, nodes: reordered }; + if (validate(candidate)) continue; + yield* offer(candidate); + for (const cone of coneReductions(candidate)) yield* offer(pruneCoordinated(cone)); + } + } + } +} + +/** Append an uncanceled queued callback after the enclosing callback's work. + * This changes batching intentionally, while preserving the other operations. */ +export function* callbackEndReductions(s: Scenario): Generator { + for (let i = 0; i < s.turns.length; i++) { + const t = s.turns[i]; + if (!("steps" in t)) continue; + for (let j = 0; j < t.steps.length; j++) { + const q = t.steps[j]; + if (q.op !== "queue" || q.via === "task") continue; + let addressed = false; + for (const turn of s.turns) { + if ("task" in turn) { + if (turn.task === q.id) addressed = true; + } else + for (const step of turn.steps) + if (step.op === "cancel" && step.id === q.id) addressed = true; + } + if (addressed) continue; + const steps = without(t.steps, j); + steps.push(...queuedSteps(q)); + yield { ...s, turns: replace(s.turns, i, { steps }) }; + } + } +} + +/** Absorb the first visibility setter even when its event contains other work. */ +export function* initialVisibilityReductions(s: Scenario): Generator { + const first = s.turns[0]; + if (!first || !("steps" in first)) return; + for (let i = 0; i < first.steps.length; i++) { + const step = first.steps[i]; + if (step.op !== "show") continue; + yield { + ...s, + show: step.value, + turns: replace(s.turns, 0, { steps: without(first.steps, i) }) + }; + break; + } +} + +/** Bypass one use of a memo while retaining its other consumers and flights. + * Whole-node bypass can erase the other observation path needed by a failure. + * Inputs always come from upstream, so these edits cannot introduce cycles. */ +export function* edgeReductions(s: Scenario): Generator { + for (let i = 0; i < s.readers.length; i++) { + const r = s.readers[i]; + for (let j = 0; j < r.refs.length; j++) { + const n = s.nodes.find(n => n.id === r.refs[j]); + if (!n) continue; + for (const ref of inputRefs(n)) + yield { + ...s, + readers: replace(s.readers, i, { ...r, refs: replace(r.refs, j, ref) }) + }; + } + } + for (let i = 0; i < s.nodes.length; i++) { + const n = s.nodes[i]; + for (const field of ["deps", "otherwise"] as const) { + const refs = field === "deps" ? n.deps : n.branch?.otherwise; + if (!refs) continue; + for (let j = 0; j < refs.length; j++) { + const parent = s.nodes.find(n => n.id === refs[j]); + if (!parent) continue; + for (const ref of inputRefs(parent)) { + const changed = replace(refs, j, ref); + const node = + field === "deps" + ? { ...n, deps: changed } + : { ...n, branch: { ...n.branch!, otherwise: changed } }; + yield { + ...s, + nodes: replace(s.nodes, i, node) + }; + } + } + } + } +} + +/** Bypass a computation as an experiment, not as an identity law: even a sync + * identity changes observation and entanglement. All surviving flight keys + * stay exact; a downstream request that no longer exists rejects the trial. */ +function bypass(s: Scenario, index: number, to: number): Scenario { + const from = s.nodes[index].id; + const nodes: MemoSpec[] = []; + for (const n of s.nodes) { + if (n.id === from) continue; + const deps = replaceRef(n.deps, from, to); + let branch = n.branch; + if (branch) { + const otherwise = replaceRef(branch.otherwise, from, to); + const condition = branch.condition === from ? to : branch.condition; + if (otherwise !== branch.otherwise || condition !== branch.condition) + branch = { condition, otherwise }; + } + nodes.push(deps === n.deps && branch === n.branch ? n : { ...n, deps, branch }); + } + const readers: ReaderSpec[] = []; + for (const r of s.readers) { + const refs = replaceRef(r.refs, from, to); + const render = r.render?.on === from ? { ...r.render, on: to } : r.render; + readers.push(refs === r.refs && render === r.render ? r : { ...r, refs, render }); + } + return leaves( + { ...s, nodes, readers, warmLatest: s.warmLatest && replaceRef(s.warmLatest, from, to) }, + leaf => { + if (leaf.op === "resolve" && leaf.node === from) return { op: "noop" }; + if (leaf.op === "read" && leaf.ref === from) return { ...leaf, ref: to }; + return leaf; + } + ); +} + +function removeQueue(s: Scenario, id: number): Scenario { + const turns: Turn[] = []; + for (const turn of s.turns) { + if ("task" in turn) { + // Keep a host turn in place until a separately tested turn deletion. + turns.push(turn.task === id ? { steps: [] } : turn); + continue; + } + const steps: Step[] = []; + for (const step of turn.steps) + if ((step.op !== "queue" && step.op !== "cancel") || step.id !== id) steps.push(step); + turns.push({ steps }); + } + return { ...s, turns }; +} + +/** Reductions learned from manual triage. Changes in graph structure, + * observation and delivery always need a replay (and an optional control). */ +export function* pruningReductions(s: Scenario): Generator { + const canonical = canonicalize(s); + if (canonical !== s) yield canonical; + + // Remove disconnected subgraphs together: removing one leaf per replay used + // to spend most of the budget dismantling a comparison that no longer read. + const needed = new Set(); + for (const r of s.readers) { + for (const ref of r.refs) needed.add(ref); + if (r.render) needed.add(r.render.on); + } + for (const ref of s.warmLatest ?? []) needed.add(ref); + for (const turn of s.turns) + if ("steps" in turn) + for (const step of turn.steps) { + const payload = step.op === "queue" ? queuedSteps(step) : [step]; + for (const leaf of payload) if (leaf.op === "read") needed.add(leaf.ref); + } + for (let i = s.nodes.length - 1; i >= 0; i--) + if (needed.has(s.nodes[i].id)) for (const dep of dependencies(s.nodes[i])) needed.add(dep); + const nodes: MemoSpec[] = []; + const removed = new Set(); + for (const n of s.nodes) { + if (needed.has(n.id)) nodes.push(n); + else removed.add(n.id); + } + if (removed.size) + yield leaves({ ...s, nodes }, leaf => + leaf.op === "resolve" && removed.has(leaf.node) ? { op: "noop" } : leaf + ); +} + +export function* structuralReductions(s: Scenario, prune = true): Generator { + if (prune) yield* pruningReductions(s); + for (let i = 0; i < s.readers.length; i++) { + const r = s.readers[i]; + yield removeReader(s, r.id); + // An unused mounting signal still adds an owner and an attachment effect. + // Remove the mechanism as a replay-tested edit, not syntax normalization. + if (r.mounted !== undefined) { + const reader = { ...r }; + delete reader.mounted; + yield leaves({ ...s, readers: replace(s.readers, i, reader) }, x => + x.op === "mount" && x.reader === r.id ? { op: "noop" } : x + ); + } + for (let j = 0; j < r.refs.length; j++) { + const n = s.nodes.find(n => n.id === r.refs[j]); + if (!n || n.delivery !== "sync" || n.branch || n.deps.length < 2) continue; + // Read a sum's inputs directly. This deliberately removes a derivation's + // entanglement and changes the tuple; the original fingerprint must hold. + const refs = r.refs.slice(); + refs.splice(j, 1, ...n.deps); + yield { ...s, readers: replace(s.readers, i, { ...r, refs }) }; + } + if (r.refs.length > 1) + for (let j = 0; j < r.refs.length; j++) + yield { ...s, readers: replace(s.readers, i, { ...r, refs: without(r.refs, j) }) }; + if (r.gated) yield { ...s, readers: replace(s.readers, i, { ...r, gated: false }) }; + if (r.boundary === "reset") + yield { ...s, readers: replace(s.readers, i, { ...r, boundary: "retain" }) }; + } + if (s.warmLatest) + for (let i = 0; i < s.warmLatest.length; i++) + yield { ...s, warmLatest: without(s.warmLatest, i) }; + if (s.optimistic?.viaMemo) yield { ...s, optimistic: { ...s.optimistic, viaMemo: false } }; + + for (let i = 0; i < s.nodes.length; i++) { + const n = s.nodes[i]; + const choices = new Set(dependencies(n)); + for (const dep of choices) yield bypass(s, i, dep); + if (n.delivery !== "sync") { + const delivery = + n.delivery === "await" ? "manual" : n.delivery === "manual" ? "promise" : "sync"; + // These completions belong to the removed async mechanism, never to a + // different pending request. Keep their event-loop slots as noops. + const candidate = { ...s, nodes: replace(s.nodes, i, { ...n, delivery }) }; + yield delivery === "manual" + ? candidate + : leaves(candidate, leaf => + leaf.op === "resolve" && leaf.node === n.id ? { op: "noop" } : leaf + ); + if (delivery !== "sync") + yield leaves( + { ...s, nodes: replace(s.nodes, i, { ...n, delivery: "sync" }) }, + leaf => (leaf.op === "resolve" && leaf.node === n.id ? { op: "noop" } : leaf) + ); + } + if (n.deps.length > 1) + for (let j = 0; j < n.deps.length; j++) + yield { ...s, nodes: replace(s.nodes, i, { ...n, deps: without(n.deps, j) }) }; + if (n.branch && n.branch.otherwise.length > 1) + for (let j = 0; j < n.branch.otherwise.length; j++) + yield { + ...s, + nodes: replace(s.nodes, i, { + ...n, + branch: { ...n.branch, otherwise: without(n.branch.otherwise, j) } + }) + }; + } + + for (let i = 0; i < s.turns.length; i++) { + const turn = s.turns[i]; + if (!("steps" in turn)) continue; + for (let j = 0; j < turn.steps.length; j++) { + const step = turn.steps[j]; + if (step.op === "queue") { + yield removeQueue(s, step.id); + // Inline only a queued callback, not a task invocation elsewhere. The + // trial is allowed to fail if the async boundary was load-bearing. + if (step.via !== "task") { + const candidate = removeQueue(s, step.id); + const changed = candidate.turns[i]; + if ("steps" in changed) changed.steps.splice(j, 0, ...queuedSteps(step)); + yield candidate; + } + } + const payload = step.op === "queue" ? queuedSteps(step) : [step]; + for (let k = 0; k < payload.length; k++) { + const leaf = payload[k]; + if (leaf.op !== "write") continue; + for (const value of [0, 1]) { + if (leaf.value === value || (value === 1 && leaf.value === 0)) continue; + const changed = { ...leaf, value }; + let next: Step = changed; + if (step.op === "queue") + next = step.step + ? { ...step, step: changed } + : { ...step, steps: replace(queuedSteps(step), k, changed) }; + yield { ...s, turns: replace(s.turns, i, { steps: replace(turn.steps, j, next) }) }; + } + } + } + } +} + +/** Merge an ordinary input into the primary input as a tested simplification. + * This can change entanglement. Optimistic truth/overlay roles are never merged, + * and exact async questions are deliberately not translated to new values. */ +function mergeSource(s: Scenario, from: number): Scenario { + const to = -1; + const nodes: MemoSpec[] = []; + for (const n of s.nodes) + nodes.push({ + ...n, + deps: replaceRef(n.deps, from, to), + branch: n.branch && { + condition: n.branch.condition === from ? to : n.branch.condition, + otherwise: replaceRef(n.branch.otherwise, from, to) + } + }); + const readers: ReaderSpec[] = []; + for (const r of s.readers) + readers.push({ + ...r, + refs: replaceRef(r.refs, from, to), + render: r.render?.on === from ? { ...r.render, on: to } : r.render + }); + const sources: number[] = []; + for (const id of s.sources!) if (id !== from) sources.push(id); + let anchors: number[] | undefined; + if (s.anchors) { + anchors = []; + for (const id of s.anchors) { + const next = id === from ? to : id; + if (!anchors.includes(next)) anchors.push(next); + } + } + let actions = s.actions; + if (actions) { + actions = []; + for (const a of s.actions!) { + const segments: typeof a.segments = []; + for (const segment of a.segments) { + const writes: typeof segment = []; + for (const w of segment) writes.push(w.source === from ? { ...w, source: to } : w); + segments.push(writes); + } + actions.push({ ...a, segments }); + } + } + return leaves( + { + ...s, + sources, + anchors, + nodes, + readers, + actions, + warmLatest: s.warmLatest && replaceRef(s.warmLatest, from, to) + }, + x => { + if (x.op === "write" && x.source === from) return { ...x, source: to }; + if (x.op === "read" && x.ref === from) return { ...x, ref: to }; + return x; + } + ); +} + +/** Patterns remaining in the real minimized corpus. None is an oracle law: + * moving setup into initial state or changing an async boundary needs replay. */ +export function* focusedReductions(s: Scenario): Generator { + // v1's retained-disposal allowance only covers ungated readers with no + // boundaries. Outside that scope, try the explicit single-source format. + // Replay still checks the fingerprint; do not normalize all v1 cases away. + if (s.version === 1 && s.readers.some(r => r.gated || r.boundary !== "none")) + yield { ...s, version: 2, sources: [-1] }; + + const renamed = identityValues(s); + if (renamed) yield renamed; + + // Compose the arithmetic while testing the loss of the intermediate + // dependency. Dead-parent pruning happens in the usual structural pass. + for (let i = 0; i < s.nodes.length; i++) { + const child = s.nodes[i]; + if (child.branch || child.deps.length !== 1) continue; + const parent = s.nodes.find(n => n.id === child.deps[0]); + if (!parent || parent.delivery !== "sync" || parent.branch || parent.deps.length !== 1) + continue; + const candidate = { + ...s, + nodes: replace(s.nodes, i, { + ...child, + deps: [parent.deps[0]], + factor: parent.factor * child.factor, + offset: parent.offset * child.factor + child.offset + }) + }; + yield candidate; + const translated = affineQuestions(candidate, child.id, parent); + if (translated && translated !== candidate) yield translated; + } + + // Remove a leading proposal together with the resume that would otherwise + // advance past the desired replacement. Keep the start and action lifetime. + if (s.optimistic && s.optimistic.proposals.length > 1) { + let removed = false; + let started = false; + const changed = leaves(s, x => { + if (x.op === "start-action") started = true; + if (started && !removed && x.op === "resume-action") { + removed = true; + return { op: "noop" }; + } + return x; + }); + if (removed) + yield { + ...changed, + optimistic: { + ...s.optimistic, + proposals: s.optimistic.proposals.slice(1) + } + }; + } + + // Move a reader's first unmount into initial state as one coordinated edit. + // Unlike flipping the initial flag alone, this removes an operation too. + for (let i = 0; i < s.readers.length; i++) { + const reader = s.readers[i]; + if (reader.mounted !== true) continue; + let found = false; + let removed = false; + const changed = leaves(s, x => { + if (!found && x.op === "mount" && x.reader === reader.id) { + found = true; + if (!x.value) { + removed = true; + return { op: "noop" }; + } + } + return x; + }); + if (removed) + yield { ...changed, readers: replace(s.readers, i, { ...reader, mounted: false }) }; + } + + const first = s.turns[0]; + if ( + s.show && + first && + "steps" in first && + first.steps.length === 1 && + first.steps[0].op === "show" && + !first.steps[0].value + ) + yield { ...s, show: false, turns: s.turns.slice(1) }; + + if (!s.show) yield { ...s, show: true }; + for (let i = 0; i < s.readers.length; i++) + if (s.readers[i].mounted === false) + yield { ...s, readers: replace(s.readers, i, { ...s.readers[i], mounted: true }) }; + + if (s.version === 2 && !s.optimistic) + for (const id of s.sources!) if (id !== -1) yield mergeSource(s, id); + + // Trying zero alone misses bugs which need a change from the initial zero. + if (s.optimistic) { + const o = s.optimistic; + for (let i = 0; i < o.proposals.length; i++) + if (o.proposals[i] !== 0 && o.proposals[i] !== 1) + yield { ...s, optimistic: { ...o, proposals: replace(o.proposals, i, 1) } }; + if (o.authoritative !== 0 && o.authoritative !== 1) + yield { ...s, optimistic: { ...o, authoritative: 1 } }; + } + for (let i = 0; i < (s.actions?.length ?? 0); i++) { + const action = s.actions![i]; + for (let j = 0; j < action.segments.length; j++) { + const segment = action.segments[j]; + for (let k = 0; k < segment.length; k++) { + const w = segment[k]; + for (const value of [0, 1]) { + if (w.value === value || (value === 1 && w.value === 0)) continue; + const segments = replace(action.segments, j, replace(segment, k, { ...w, value })); + yield { ...s, actions: replace(s.actions!, i, { ...action, segments }) }; + } + } + } + } + + // Some branch repros need the old derived answer to equal the new input. + // Shrinking the offset or write alone destroys that equality. Try their + // shared literal together; derived arithmetic is not assumed equivalent, + // and surviving request keys still reject a changed question. + const offsets = new Set(); + for (const n of s.nodes) if (n.offset > 1) offsets.add(n.offset); + for (const offset of offsets) { + const changed = leaves(s, x => + x.op === "write" && x.value === offset ? { ...x, value: 1 } : x + ); + if (changed === s) continue; + const nodes: MemoSpec[] = []; + for (const n of s.nodes) nodes.push(n.offset === offset ? { ...n, offset: 1 } : n); + yield { ...changed, nodes }; + } + + // Dropping setup alone can leave write(0), which no longer changes the + // initial zero. Try dropping the prefix and retaining a changing write + // together. Any surviving exact request must still match on replay. + for (let i = 1; i < s.turns.length; i++) { + const turn = s.turns[i]; + if (!("steps" in turn) || turn.steps[0]?.op !== "write" || turn.steps[0].value !== 0) continue; + const turns = s.turns.slice(i); + turns[0] = { steps: replace(turn.steps, 0, { ...turn.steps[0], value: 1 }) }; + yield { ...s, turns }; + } + + const noReads = leaves(s, x => (x.op === "read" ? { op: "noop" } : x)); + if (noReads !== s) yield noReads; + + for (let i = 0; i < s.turns.length; i++) { + const turn = s.turns[i]; + if (!("steps" in turn)) continue; + for (let j = 0; j < turn.steps.length; j++) { + const step = turn.steps[j]; + if (step.op === "flush" && j > 0 && j + 1 < turn.steps.length) { + const turns = s.turns.slice(); + turns.splice(i, 1, { steps: turn.steps.slice(0, j) }, { steps: turn.steps.slice(j + 1) }); + yield { ...s, turns }; + } + if (step.op !== "queue") continue; + if (step.via === "promise" || step.via === "await") + yield { + ...s, + turns: replace(s.turns, i, { + steps: replace(turn.steps, j, { ...step, via: "microtask" }) + }) + }; + if (step.via !== "task") { + // A final microtask callback can sometimes be a plain next event. + // Do not move it past another operation in its containing callback. + if (j === turn.steps.length - 1) { + const turns = s.turns.slice(); + turns.splice( + i, + 1, + { steps: replace(turn.steps, j, { op: "noop" }) }, + { steps: queuedSteps(step) } + ); + yield { ...s, turns }; + } + continue; + } + let canceled = false; + let invocation = -1; + let repeated = false; + for (let k = 0; k < s.turns.length; k++) { + const t = s.turns[k]; + if ("task" in t) { + if (t.task === step.id) { + if (invocation >= 0) repeated = true; + invocation = k; + } + } else for (const x of t.steps) if (x.op === "cancel" && x.id === step.id) canceled = true; + } + if (canceled || repeated) continue; + // Keep the invocation at its own host turn, not at the queue site. + const turns = replace(s.turns, i, { steps: replace(turn.steps, j, { op: "noop" }) }); + const delivered = { steps: queuedSteps(step) }; + if (invocation >= 0) turns[invocation] = delivered; + else turns.push(delivered); // previously delivered by the external-task suffix + yield { ...s, turns }; + } + } +} + +/** Only identity computations permit arbitrary data-label renaming: every + * captured answer is one of the inputs. No arithmetic, branches or -0. + * Rename exact request inputs with the same bijection, retaining owner and + * occurrence. This is not a fallback to another live request. */ +export function identityValues(s: Scenario): Scenario | undefined { + for (const n of s.nodes) + if (n.branch || n.deps.length !== 1 || n.factor !== 1 || n.offset !== 0) return; + const labels = new Map([[0, 0]]); + let invalid = false; + let changed = false; + const add = (value: number) => { + if (!Number.isFinite(value) || Object.is(value, -0)) invalid = true; + if (!labels.has(value)) { + const label = labels.size; + labels.set(value, label); + if (label !== value) changed = true; + } + }; + if (s.optimistic) { + for (const value of s.optimistic.proposals) add(value); + add(s.optimistic.authoritative); + } + for (const a of s.actions ?? []) + for (const segment of a.segments) for (const w of segment) add(w.value); + leaves(s, x => { + if (x.op === "write") add(x.value); + return x; + }); + if (invalid || !changed) return; + const key = (text: string, owner: number): string => { + const match = /^(\d+):(\[[^\n]*\])#([1-9]\d*)$/.exec(text); + if (!match || match[1] !== String(owner)) { + invalid = true; + return text; + } + try { + const inputs = JSON.parse(match[2]); + if ( + !Array.isArray(inputs) || + inputs.length !== 1 || + JSON.stringify(inputs) !== match[2] || + !labels.has(inputs[0]) + ) { + invalid = true; + return text; + } + return `${match[1]}:[${labels.get(inputs[0])}]#${match[3]}`; + } catch { + invalid = true; + return text; + } + }; + const result = leaves(s, x => { + if (x.op === "write") return { ...x, value: labels.get(x.value)! }; + if (x.op !== "resolve") return x; + let variantKeys = x.variantKeys; + if (variantKeys) { + variantKeys = []; + for (const k of x.variantKeys!) variantKeys.push(k === null ? null : key(k, x.node)); + } + return { ...x, key: x.key === undefined ? undefined : key(x.key, x.node), variantKeys }; + }); + if (invalid) return; + let optimistic = s.optimistic; + if (optimistic) { + const proposals: number[] = []; + for (const value of optimistic.proposals) proposals.push(labels.get(value)!); + optimistic = { ...optimistic, proposals, authoritative: labels.get(optimistic.authoritative)! }; + } + let actions = s.actions; + if (actions) { + actions = []; + for (const a of s.actions!) { + const segments: typeof a.segments = []; + for (const segment of a.segments) { + const writes: typeof segment = []; + for (const w of segment) writes.push({ ...w, value: labels.get(w.value)! }); + segments.push(writes); + } + actions.push({ ...a, segments }); + } + } + return { ...result, optimistic, actions }; +} + +/** These coordinated edits can cross equality thresholds that individual + * literal/edge deletions cannot. Exact questions remain unchanged: a replay + * rejects the candidate when it would resolve a different request. */ +export function* arithmeticReductions(s: Scenario): Generator { + // Small dependency lists: avoid a Set allocation for the common unique case. + const unique = (refs: number[]) => { + let result = refs; + for (let i = 0; i < refs.length; i++) { + if (refs.indexOf(refs[i]) !== i) { + if (result === refs) result = refs.slice(0, i); + } else if (result !== refs) result.push(refs[i]); + } + return result; + }; + let nodes = s.nodes; + for (let i = 0; i < nodes.length; i++) { + const n = nodes[i], + deps = unique(n.deps); + const otherwise = n.branch && unique(n.branch.otherwise); + const factor = n.factor > 1 ? 1 : n.factor; + const offset = n.offset > 1 ? 1 : n.offset; + if ( + deps === n.deps && + otherwise === n.branch?.otherwise && + factor === n.factor && + offset === n.offset + ) + continue; + if (nodes === s.nodes) nodes = nodes.slice(); + nodes[i] = { + ...n, + deps, + factor, + offset, + branch: + n.branch && otherwise !== n.branch.otherwise + ? { ...n.branch, otherwise: otherwise! } + : n.branch + }; + } + const changed = leaves(s, x => (x.op === "write" && x.value > 1 ? { ...x, value: 1 } : x)); + if (nodes !== s.nodes || changed !== s) yield { ...changed, nodes }; + + // Individual zeroing can break an equality between branches. Try removing + // several offsets together; a single offset is already tried by nodesCandidates. + let offsets = 0; + for (const n of s.nodes) if (n.offset !== 0) offsets++; + if (offsets > 1) { + const nodes: MemoSpec[] = []; + for (const n of s.nodes) nodes.push(n.offset === 0 ? n : { ...n, offset: 0 }); + yield { ...s, nodes }; + } + + // Move synchronous output arithmetic upstream, then let ordinary bypass + // pruning remove the resulting identity memo. Other readers can make this + // invalid; the graph rewrite is always tested, never assumed equivalent. + for (let i = 0; i < s.nodes.length; i++) { + const child = s.nodes[i]; + if ( + child.delivery !== "sync" || + child.branch || + child.deps.length !== 1 || + (child.factor === 1 && child.offset === 0) + ) + continue; + const p = s.nodes.findIndex(n => n.id === child.deps[0]); + if (p < 0 || s.nodes[p].branch) continue; + const parent = s.nodes[p]; + const nodes = s.nodes.slice(); + nodes[p] = { + ...parent, + factor: parent.factor * child.factor, + offset: parent.offset * child.factor + child.offset + }; + nodes[i] = { ...child, factor: 1, offset: 0 }; + yield { ...s, nodes }; + } +} + +/** Invert a removed synchronous affine input in exact request keys. Retain + * request owner and occurrence, including paired keys. Only exact safe-integer + * inverses are supported; malformed or ambiguous questions reject the edit. */ +function affineQuestions(s: Scenario, owner: number, parent: MemoSpec): Scenario | undefined { + if ( + !Number.isSafeInteger(parent.factor) || + !Number.isSafeInteger(parent.offset) || + parent.factor === 0 || + (parent.factor === 1 && parent.offset === 0) + ) + return; + return translateQuestions(s, owner, inputs => { + if (inputs.length !== 1) return; + const difference = inputs[0] - parent.offset; + const input = difference / parent.factor; + if ( + !Number.isSafeInteger(difference) || + !Number.isSafeInteger(input) || + !Number.isSafeInteger(input * parent.factor) || + Object.is(input, -0) || + input * parent.factor + parent.offset !== inputs[0] + ) + return; + return [input]; + }); +} + +/** Exact tuple translation shared by affine inversion and cone projection. */ +function translateQuestions( + s: Scenario, + owner: number, + translate: (inputs: number[]) => number[] | undefined +): Scenario | undefined { + let invalid = false; + const key = (text: string): string => { + const match = /^(\d+):(\[[^\n]*\])#([1-9]\d*)$/.exec(text); + if (match && match[1] === String(owner)) { + try { + const inputs = JSON.parse(match[2]); + if ( + Array.isArray(inputs) && + JSON.stringify(inputs) === match[2] && + inputs.every(x => Number.isSafeInteger(x) && !Object.is(x, -0)) + ) { + const translated = translate(inputs); + if (translated) return `${owner}:${JSON.stringify(translated)}#${match[3]}`; + } + } catch { + // Malformed questions are not silently retargeted. + } + } + invalid = true; + return text; + }; + const result = leaves(s, x => { + if (x.op !== "resolve" || x.node !== owner) return x; + let variantKeys = x.variantKeys; + if (variantKeys) { + variantKeys = []; + for (const k of x.variantKeys!) variantKeys.push(k === null ? null : key(k)); + } + return { ...x, key: x.key === undefined ? undefined : key(x.key), variantKeys }; + }); + return invalid ? undefined : result; +} + +/** Start with a concrete graph/async cut, then preserve a useful visible witness. + * Intermediate edits may pass. The completed program must fail independently. */ +export function* cutReductions(s: Scenario): Generator { + const weight = (x: Scenario) => { + let total = x.nodes.length * 100; + for (const n of x.nodes) total += Number(n.delivery !== "sync"); + return total; + }; + const before = weight(s); + const sources = sourceIds(s); + for (const cut of structuralReductions(s)) { + const c = pruneCoordinated(cut); + if (weight(c) >= before) continue; + for (let i = 0; i < c.readers.length; i++) { + const r = c.readers[i]; + if (r.render || r.parent !== undefined) continue; + const ancestors = new Set(r.refs); + for (let j = c.nodes.length - 1; j >= 0; j--) + if (ancestors.has(c.nodes[j].id)) + for (const ref of dependencies(c.nodes[j])) ancestors.add(ref); + for (const source of sources) { + if (!ancestors.has(source)) continue; + const reader = { ...r, refs: r.refs.includes(source) ? r.refs : [source, ...r.refs] }; + delete reader.pending; + delete reader.pendingDepth; + const candidate = { ...c, readers: replace(c.readers, i, reader) }; + yield candidate; + // Arithmetic compensation only after removing a node, not for every + // delivery change; equality-sensitive branches motivate this trial. + if (c.nodes.length < s.nodes.length && c.nodes.some(n => n.branch)) + for (let j = 0; j < c.nodes.length; j++) + if (c.nodes[j].offset !== 1) + yield { ...candidate, nodes: replace(c.nodes, j, { ...c.nodes[j], offset: 1 }) }; + } + } + if (c.nodes.length < s.nodes.length) + for (let i = 0; i < c.nodes.length; i++) + if (c.nodes[i].delivery !== "sync" && c.nodes[i].delivery !== "promise") + yield { ...c, nodes: replace(c.nodes, i, { ...c.nodes[i], delivery: "promise" }) }; + } +} + +/** A delivery pivot is offered only when lifecycle/scheduling work can disappear. + * The search engine retains it only when ordinary cleanup makes a real cut. */ +export function* deliveryEscapeReductions(s: Scenario): Generator { + if (!s.optimistic) return; + let scaffolding = false; + for (const t of s.turns) + if ("steps" in t) + for (const step of t.steps) + if (step.op === "resume-action" || step.op === "queue") scaffolding = true; + if (!scaffolding) return; + for (let i = 0; i < s.nodes.length; i++) + if (s.nodes[i].delivery === "promise") + yield { ...s, nodes: replace(s.nodes, i, { ...s.nodes[i], delivery: "manual" }) }; +} diff --git a/packages/signals/tests/semantics/reporting.test.ts b/packages/signals/tests/semantics/reporting.test.ts new file mode 100644 index 000000000..77648f005 --- /dev/null +++ b/packages/signals/tests/semantics/reporting.test.ts @@ -0,0 +1,136 @@ +import { mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { main } from "./cli.js"; + +// Exercise retention with controlled findings: this test needs no Solid bug. +test.each([false, true])("all findings remain replayable when distinct=%s", async distinct => { + const dir = await mkdtemp(join(tmpdir(), "solid-fuzz-report-")); + const workerFile = join(dir, "worker.mjs"); + const exitCode = process.exitCode; + const log = vi.spyOn(console, "log").mockImplementation(() => {}); + await writeFile( + workerFile, + `import { parentPort } from 'node:worker_threads'; + parentPort.on('message', ({ scenario }) => parentPort.postMessage({ + scenario, status: 'fail', failure: { rule: 'test', message: ${distinct ? "JSON.stringify(scenario)" : "'same symptom'"} }, + frames: [], requirements: [], work: [], coverage: [], events: [], + metrics: { operations: 0, requests: 0, skipped: 0, frames: 0, elapsedMs: 0 } + }));` + ); + const context = { workerFile, workerHash: "fixture", sourceHash: "fixture" }; + try { + await main({ ...context, args: ["--cases", "25", "--out", join(dir, "run")] }); + const lines = (await readFile(join(dir, "run", "findings.jsonl"), "utf8")).trim().split("\n"); + expect(lines).toHaveLength(25); + const entries = lines.map(line => JSON.parse(line)); + expect(entries.map(e => e.index)).toEqual(Array.from({ length: 25 }, (_, i) => i)); + const reports = (await readdir(join(dir, "run"))).filter(f => /^case-\d+\.json$/.test(f)); + expect(reports.length).toBe(distinct ? 20 : 1); + await main({ + ...context, + args: [ + "--replay", + join(dir, "run", "findings.jsonl"), + "--index", + "24", + "--out", + join(dir, "replay") + ] + }); + const replay = JSON.parse(await readFile(join(dir, "replay", "case-0.json"), "utf8")); + expect(replay.scenario).toEqual(entries[24].scenario); + } finally { + process.exitCode = exitCode; + log.mockRestore(); + await rm(dir, { recursive: true, force: true }); + } +}); + +test("idle worker failure saves the original input and reports an incomplete campaign", async () => { + const { Client, WorkerFailure } = await import("./client.js"); + const { chain } = await import("./fixtures.js"); + const dir = await mkdtemp(join(tmpdir(), "solid-fuzz-idle-report-")); + const previous = chain("manual"); + const result = { + scenario: previous, + status: "error" as const, + error: "late fixture failure", + frames: [], + requirements: [], + work: [], + coverage: [], + events: [], + metrics: { operations: 0, requests: 0, skipped: 0, frames: 0, elapsedMs: 0 } + }; + const run = vi.spyOn(Client.prototype, "run").mockRejectedValue(new WorkerFailure(result)); + const log = vi.spyOn(console, "log").mockImplementation(() => {}); + const error = vi.spyOn(console, "error").mockImplementation(() => {}); + const exitCode = process.exitCode; + try { + await main({ + workerFile: "unused", + workerHash: "fixture", + sourceHash: "fixture", + args: ["--cases", "2", "--out", dir] + }); + const saved = JSON.parse(await readFile(join(dir, "worker-error.json"), "utf8")); + expect(saved.scenario).toEqual(previous); + const summary = JSON.parse(await readFile(join(dir, "summary.json"), "utf8")); + expect(summary.workerErrors).toBe(1); + expect(summary.total).toBe(0); + expect(process.exitCode).toBe(1); + } finally { + process.exitCode = exitCode; + run.mockRestore(); + log.mockRestore(); + error.mockRestore(); + await rm(dir, { recursive: true, force: true }); + } +}); + +test("bounded reduction retains originals and charges final replay separately", async () => { + const { Client } = await import("./client.js"); + const { chain } = await import("./fixtures.js"); + const { runScenario } = await import("./runner.js"); + const baseline = await runScenario(chain("sync")); + const dir = await mkdtemp(join(tmpdir(), "solid-fuzz-selection-")); + const exitCode = process.exitCode; + const log = vi.spyOn(console, "log").mockImplementation(() => {}); + let calls = 0; + const run = vi.spyOn(Client.prototype, "run").mockImplementation(async scenario => { + calls++; + return { ...baseline, scenario, status: "fail", failure: { rule: "S1", message: "fixture" } }; + }); + try { + await main({ + workerFile: "unused", + workerHash: "fixture", + sourceHash: "fixture", + args: ["--cases", "25", "--shrink", "--budget", "0", "--out", dir] + }); + const summary = JSON.parse(await readFile(join(dir, "summary.json"), "utf8")); + const records = JSON.parse(await readFile(join(dir, "reductions.json"), "utf8")); + expect(summary.retainedCases).toBe(25); + expect(records.length).toBeGreaterThan(0); + expect(records.length).toBeLessThanOrEqual(2); + expect(summary.reductionExecutions).toEqual({ + initial: 0, + search: 0, + verification: records.length + }); + expect(calls).toBe(25 + records.length); + for (const entry of records) { + const artifact = JSON.parse( + await readFile(join(dir, `case-${entry.index}-min.json`), "utf8") + ); + expect(artifact.reproKey).toBe(entry.reproKey); + expect(artifact.executionCounts.verification).toBe(1); + } + } finally { + process.exitCode = exitCode; + log.mockRestore(); + run.mockRestore(); + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/signals/tests/semantics/rules.ts b/packages/signals/tests/semantics/rules.ts new file mode 100644 index 000000000..4f45c8d23 --- /dev/null +++ b/packages/signals/tests/semantics/rules.ts @@ -0,0 +1,428 @@ +import { type Scenario } from "./scenario.js"; +import { Model, sameNumbers } from "./model.js"; + +export type Output = number[] | "hidden" | "loading" | "absent" | "covered" | { pending: boolean }; +export interface Frame { + at: string; + input: number; + inputs?: Record; + mounts?: Record; + show: boolean; + outputs: Record; +} +export interface Failure { + rule: string; + message: string; + reader?: number; + frame?: Frame; + expected?: unknown; +} +// Bump when the executable contract or completion policy changes. Target source +// hashes alone cannot explain a replay performed with different oracle rules. +export const ruleRevision = 17; +export const rules = [ + { + id: "A1", + law: "An initialized visible region keeps exactly one output contribution until replacement or explicit removal.", + scope: + "fixed ordinary output regions; visibility follows host attachment, not reactive ownership" + }, + { + id: "G1", + law: "Proven update groups publish atomically and authoritative action holds prevent early publication.", + scope: + "ordinary initialized flat DAGs, complete anchors, fixed readers; generated batch/action identity and same-source overlap" + }, + { + id: "G2", + law: "A group with no remaining conservative blocker publishes after a drain, independently of unrelated groups.", + scope: + "G1 scope; possible memo joins extend deadlines only; adjacent microtasks permit but do not require entanglement" + }, + { + id: "P1", + law: "Ordinary writes publish after a drain when no visible reader still needs an unresolved answer.", + scope: + "flat ordinary pure DAG, complete source anchors, settled observation controls; no actions, branches or readiness readers; fallback is not a blocker" + }, + { + id: "R4", + law: "A held ordinary change to an initialized expression's answer has a true tracked pending verdict.", + scope: + "continuous unconditional readiness readers; no boundary, mount or action; complete source anchors and distinct pure current/desired answers" + }, + { + id: "R2", + law: "A control publishing ready can read its guarded expression without suspending, consistently with witnessed published inputs.", + scope: + "explicit generated clicks on tracked isPending readers; initialized ordinary or one-parent override graph; no context-free false-implies-readable claim" + }, + { + id: "R3", + law: "Context-free isPending probes do not throw NotReadyError.", + scope: "explicit event-block probes; SPEC A16; user errors are not generated" + }, + { + id: "E5", + law: "A single optimistic proposal while its parent is held has the ordinary update's publication behavior.", + scope: + "settled pure graph, one write, unconditional readers, no boundaries; parent completion excluded" + }, + { + id: "S1", + law: "Published pure values describe the same published input.", + scope: "ordinary scalar pure DAG" + }, + { + id: "S2", + law: "A published observation toggle agrees with its reader's ready/hidden view.", + scope: "conditional readers; explicit fallback allowed" + }, + { + id: "S3", + law: "All scoped write completions imply those inputs have published.", + scope: "all generated setters; checked after flush, not at transitionSettled" + }, + { + id: "S4", + law: "Residual obsolete or unobserved completion cannot corrupt a settled view.", + scope: "no new external input; S1/S2 checked at every drain" + }, + { + id: "S5", + law: "A disposed reader receives no later publication callback.", + scope: "explicit disposal or cleanup of an owned reader generation" + }, + { + id: "L1", + law: "All finite generated work settling reaches the final pure view.", + scope: "no new external inputs; checked after residual completion" + }, + { + id: "W1", + law: "Publication may wait only for work permitted by the selected policy.", + scope: "unknown completion timing remains open; historical disposal allowance is opt-in" + }, + { + id: "E3", + law: "Delayed pure answers permit waiting, not inconsistent data publications.", + scope: "settled start, no intervening writes or fallback resets" + }, + { + id: "O1", + law: "Ready observed optimistic derivations can publish while the parent action is open.", + scope: + "one parent; continuous initialized data readers and optimistic source anchor; no boundaries, mounts, gates or authoritative writes until final action step" + }, + { + id: "O2", + law: "Authoritative completion does not require obsolete optimistic requests to settle first.", + scope: "one parent; current required correction work has been settled" + }, + { + id: "R1", + law: "Ordinary imperative reads describe published state.", + scope: + "explicit event-block reads outside action bodies; published witness; excludes getters and derivations that may read latest" + }, + { + id: "E4", + law: "Warming latest without adding an observer does not change later observations.", + scope: "paired identical schedules and readers; only companion creation differs" + } +] as const; + +export const ruleContracts = Object.fromEntries( + rules.map(rule => [ + rule.id, + { + ...rule, + authority: + rule.id === "A1" + ? "visible-output continuity; #3404 is a calibration case, not the definition of visibility" + : rule.id === "G1" || rule.id === "G2" + ? "proposed update-group model; batching/action controls plus effects-do-not-entangle ruling; adjacent-microtask entanglement permission" + : rule.id === "P1" + ? "ideal observational progress law; tested against #3375 and #3372, not defined by their implementation" + : rule.id === "R4" + ? "SPEC A24 maintainer ruling 2026-07-13: a new question pends until its answer reveals" + : rule.id === "R3" + ? "SPEC A16 maintainer keep 2026-07-06; Ryan commit 70e89bafa7de4b6cbdb47aa83ac37b59244647d0" + : rule.id === "R2" + ? "proposed readiness-control contract; initialized tracked probe, not context-free initial load" + : rule.id === "W1" + ? "scoped timing investigation; P1 independently checks proven release, legacy disposal permission requires an explicit allowance" + : rule.id === "S3" + ? "ordinary scoped completion; experimental extension includes observed optimistic correction" + : rule.id.startsWith("O") || rule.id === "E5" + ? "experimental optimistic contract; upstream conformance not presumed" + : "semantic-fuzzing.md scoped law", + checkpoints: + rule.id === "A1" + ? "completed flush and host drain; detached preparation and intermediate mutations within a flush are private" + : rule.id === "G1" + ? "complete publication checkpoints" + : rule.id === "G2" + ? "host drain" + : rule.id === "P1" + ? "host drain after each external turn and completion step" + : rule.id === "R2" || rule.id === "R3" || rule.id === "R4" + ? "completed flush for R4; explicit event-block click or probe for R2/R3" + : rule.id === "L1" + ? "final completion" + : rule.id === "W1" + ? "required and retained completion" + : "publication or paired replay", + controls: + rule.id === "A1" + ? "output.test.ts and attachment.test.ts; premature attachment, cleanup and duplicate-contribution controls" + : rule.id === "G1" || rule.id === "G2" + ? "group-controls.test.ts, groups.test.ts, entangle-effect/drop-action-hold calibration" + : rule.id === "P1" + ? "progress.test.ts and lost-disposal-wake/lost-fallback-wake calibration" + : rule.id === "R2" || rule.id === "R3" || rule.id === "R4" + ? "readiness.test.ts and pending-contract.test.ts" + : rule.id === "W1" || rule.id === "L1" + ? "completion.test.ts" + : rule.id === "E5" + ? "optimistic-equivalence.test.ts" + : rule.id.startsWith("O") + ? "optimistic.test.ts and optimistic-scope.test.ts" + : rule.id === "R1" || rule.id === "E4" + ? "reads.test.ts" + : rule.id === "S3" + ? "settled-writes.test.ts and optimistic.test.ts" + : rule.id === "S2" || rule.id === "S5" + ? "runner.test.ts and mounts.test.ts" + : "runner.test.ts and calibration.test.ts" + } + ]) +); + +export function checkFrame(s: Scenario, frame: Frame, model = new Model(s)): Failure | undefined { + const values = model.evaluate(frame.input, frame.inputs); + for (const r of s.readers) { + const output = frame.outputs[r.id]; + if (r.parent !== undefined) { + const parent = frame.outputs[r.parent]; + const covered = + parent === "loading" || parent === "covered" || parent === "hidden" || parent === "absent"; + if (covered !== (output === "covered")) + return { + rule: "S2", + message: "Nested content disagrees with its enclosing region", + reader: r.id, + frame + }; + if (covered) continue; + } else if (output === "covered") + return { + rule: "S2", + message: "Covered content without an enclosing region", + reader: r.id, + frame + }; + if (r.mounted !== undefined && !!frame.mounts?.[r.id] !== (output !== "absent")) + return { + rule: "S2", + message: "Published mount control disagrees with its owned reader", + reader: r.id, + frame + }; + if (output === "absent") continue; + if (output === "loading") { + if (r.boundary === "none") + return { rule: "S2", message: "Fallback without a boundary", reader: r.id, frame }; + continue; + } + const hidden = r.gated && !frame.show; + if (s.anchorShow !== false && hidden !== (output === "hidden")) + return { + rule: "S2", + message: "Published observation control disagrees with its reader", + reader: r.id, + frame + }; + if (output === "hidden") continue; + if ( + r.pending && + typeof output === "object" && + !Array.isArray(output) && + typeof output.pending === "boolean" + ) + continue; + let wrong = !Array.isArray(output) || output.length !== r.refs.length; + if (Array.isArray(output)) + for (let i = 0; i < output.length && !wrong; i++) + wrong = model.witnessed(r.refs[i]) && output[i] !== values.get(r.refs[i]); + if (wrong) + return { + rule: "S1", + message: "Published derivation disagrees with published input", + reader: r.id, + frame, + expected: r.refs.map(ref => values.get(ref)) + }; + } +} + +/** A24's new-question implication, deliberately one-way. A ready old answer + * is readable, so R2 alone cannot detect a false verdict over a held change. + * Compare pure answers only with complete publication witnesses, after flush. + * Equal answers, partial anchors and changing observation remain outside this + * implication; no reference copy of Solid's pending propagation is required. */ +export function checkPending( + s: Scenario, + frame: Frame, + desired: Record, + onSubject?: (reader: number) => void, + model = new Model(s) +): Failure | undefined { + if (!model.hasPending) return; + const published = model.evaluate(frame.input, frame.inputs); + const future = model.evaluate(desired[-1], desired); + for (const reader of s.readers) { + if ( + !reader.pending || + reader.gated || + reader.mounted !== undefined || + reader.boundary !== "none" + ) + continue; + const output = frame.outputs[reader.id]; + if (typeof output !== "object" || Array.isArray(output)) continue; + for (const ref of reader.refs) { + if (published.get(ref) === future.get(ref)) continue; + if (model.witnessed(ref)) { + onSubject?.(reader.id); + if (output.pending) continue; + return { + rule: "R4", + message: "Tracked verdict is false while a distinct ordinary answer is held", + reader: reader.id, + frame, + expected: { pending: true, published: published.get(ref), desired: future.get(ref) } + }; + } + } + } +} + +/** O1 concerns ready optimistic data, not the completion of ordinary mount, + * visibility, or authoritative updates sharing the action's lifetime. */ +export function checkOptimistic( + s: Scenario, + frame: Frame, + inputs: Record, + model = new Model(s) +): Failure | undefined { + if (!s.optimistic) return; + const values = model.evaluate(inputs[-1], inputs); + if (model.anchors.includes(-2) && frame.inputs?.[-2] !== inputs[-2]) + return { + rule: "O1", + message: "Ready optimistic source did not publish while its parent action remained open", + frame, + expected: inputs[-2] + }; + const optimisticMask = model.sourceMask(-2); + for (const reader of s.readers) { + if ( + reader.gated || + reader.mounted !== undefined || + reader.boundary !== "none" || + reader.pending + ) + continue; + const output = frame.outputs[reader.id]; + if (output === "absent") continue; + for (let i = 0; i < reader.refs.length; i++) { + const ref = reader.refs[i]; + if (!(model.sourceMask(ref) & optimisticMask)) continue; + if (!Array.isArray(output) || output[i] !== values.get(ref)) + return { + rule: "O1", + message: + "Ready optimistic derivation did not publish while its parent action remained open", + reader: reader.id, + frame, + expected: { ref, value: values.get(ref) } + }; + } + } +} + +export function checkFinal( + s: Scenario, + frame: Frame, + input: number, + show: boolean, + inputs?: Record, + mounts?: Record, + model = new Model(s) +): Failure | undefined { + const values = model.evaluate(input, inputs); + if ( + (model.anchors.includes(-1) && frame.input !== input) || + (s.anchorShow !== false && frame.show !== show) || + (inputs && model.anchors.some(id => (frame.inputs?.[id] ?? frame.input) !== inputs[id])) + ) + return { + rule: "L1", + message: "Final input or observation write did not publish", + frame, + expected: { input, show, ...(inputs ? { inputs } : {}) } + }; + for (const r of s.readers) { + if (r.mounted !== undefined) { + const wanted = mounts?.[r.id] ?? r.mounted; + if (!!frame.mounts?.[r.id] !== wanted || (frame.outputs[r.id] !== "absent") !== wanted) + return { + rule: "L1", + message: "Owned reader did not publish its requested mount state", + reader: r.id, + frame, + expected: wanted + }; + } + if (frame.outputs[r.id] === "absent") continue; + const expected = + r.gated && !show + ? "hidden" + : r.pending + ? { pending: false } + : r.refs.map(ref => values.get(ref)!); + if (!sameOutput(frame.outputs[r.id], expected)) + return { + rule: "L1", + message: "Required reader did not publish its final answer", + reader: r.id, + frame, + expected + }; + } +} + +export function sameOutput(a: Output, b: Output): boolean { + if (a === b) return true; + if (Array.isArray(a)) return Array.isArray(b) && sameNumbers(a, b); + if (typeof a === "object" && typeof b === "object" && !Array.isArray(b)) + return a.pending === b.pending; + return false; +} + +/** Compare meaningful data publications, retaining every inconsistent frame. */ +export function dataTrace(frames: Frame[]): string[] { + const result: string[] = []; + for (const { input, inputs, mounts, show, outputs } of frames) { + const frame = JSON.stringify([ + input, + show, + outputs, + ...(inputs ? [inputs] : []), + ...(mounts ? [mounts] : []) + ]); + if (result.at(-1) !== frame) result.push(frame); + } + return result; +} diff --git a/packages/signals/tests/semantics/runner.test.ts b/packages/signals/tests/semantics/runner.test.ts new file mode 100644 index 000000000..c4e03ca63 --- /dev/null +++ b/packages/signals/tests/semantics/runner.test.ts @@ -0,0 +1,83 @@ +import { runScenario } from "./runner.js"; +import { chain, obsolete, reveal } from "./fixtures.js"; +import { dataTrace } from "./rules.js"; +import { HostTasks } from "./host.js"; +import { generate } from "./generate.js"; + +test.each(["sync", "promise", "manual", "await"] as const)("settles a %s chain", async delivery => { + const r = await runScenario(chain(delivery)); + expect(r.status, JSON.stringify(r, null, 2)).toBe("pass"); +}); + +test.each(["none", "retain", "reset"] as const)( + "late observation with %s boundary", + async boundary => { + const r = await runScenario(reveal(boundary)); + expect(r.status, JSON.stringify(r, null, 2)).toBe("pass"); + } +); + +test("obsolete answers cannot replace the latest answer", async () => { + const r = await runScenario(obsolete()); + expect(r.status, JSON.stringify(r, null, 2)).toBe("pass"); + expect(r.coverage).toContain("residual-completion"); + expect((await runScenario(r.scenario)).frames).toEqual(r.frames); +}); + +test("native nested microtasks finish before the next host task", async () => { + const host = new HostTasks(); + const order: string[] = []; + try { + await host.run(() => { + order.push("task"); + queueMicrotask(() => { + order.push("microtask"); + Promise.resolve().then(() => queueMicrotask(() => order.push("nested"))); + }); + Promise.resolve().then(() => order.push("promise")); + }); + await host.run(() => order.push("next task")); + expect(order).toEqual(["task", "microtask", "promise", "nested", "next task"]); + } finally { + host.close(); + } +}); + +test("completion probes do not change the publication trace", async () => { + const a = await runScenario(obsolete()); + const b = await runScenario(obsolete(), { completionProbes: false }); + expect(dataTrace(a.frames)).toEqual(dataTrace(b.frames)); +}); + +test("initialization drains fulfilled promises before deciding no work remains", async () => { + const s = chain("promise"); + s.nodes[1].delivery = "manual"; + s.turns = []; + const r = await runScenario(s); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect(r.frames[0].outputs[0]).toEqual([0, 4]); +}); + +test("malformed replay data is invalid, not a runtime error", async () => { + const r = await runScenario(null as never); + expect(r.status).toBe("invalid"); +}); + +test("published fallback covers retained content work after a hide", async () => { + const s = chain("await"); + s.readers[0].boundary = "reset"; + s.readers[0].gated = true; + s.turns.push({ steps: [{ op: "show", value: false }] }); + const r = await runScenario(s); + expect(r.status, JSON.stringify(r)).toBe("pass"); + expect(r.coverage).toContain("published-fallback"); + expect(r.frames.at(-1)!.outputs[0]).toBe("hidden"); +}); + +test("completion probes preserve generated publication traces", async () => { + for (const s of generate(3334, 50)) { + const a = await runScenario(s); + const b = await runScenario(s, { completionProbes: false }); + expect(dataTrace(a.frames)).toEqual(dataTrace(b.frames)); + } +}); diff --git a/packages/signals/tests/semantics/runner.ts b/packages/signals/tests/semantics/runner.ts new file mode 100644 index 000000000..3dfacdedc --- /dev/null +++ b/packages/signals/tests/semantics/runner.ts @@ -0,0 +1,1555 @@ +import { + createLoadingBoundary, + createMemo, + createRenderEffect, + createRoot, + createSignal, + createOptimistic, + action, + latest, + isPending, + NotReadyError, + onCleanup, + flush, + onSettled +} from "../../src/index.js"; +import { + attrHooks, + setAttributionHooks, + type AttributionHooks +} from "../../src/core/attribution-hooks.js"; +import { Groups, groupScope } from "./groups.js"; +import { canonicalize } from "./reduce.js"; +import { HostTasks } from "./host.js"; +import { checkProgress, progressScope } from "./progress.js"; +import { Model, sameNumbers } from "./model.js"; +import { mountTree, type TreeHost } from "./tree.js"; +import { OutputTree, mountOutput, checkAttachment } from "./output.js"; +import { Footprint, type Witness } from "./footprints.js"; +import { + capture, + answer, + queuedSteps, + validate, + type ReaderSpec, + type Leaf, + type Scenario, + type Step +} from "./scenario.js"; +import { + checkFinal, + checkOptimistic, + checkFrame, + checkPending, + type Failure, + type Frame, + type Output +} from "./rules.js"; +import { + retainedObservations, + progressAllowance, + type Allowance, + type ProgressFinding, + type Observation, + type WaitingFinding, + type WaitingPolicy +} from "./policy.js"; + +interface Work { + key: string; + node: number; + inputs: number[]; + value: number; + state: "waiting" | "resolved"; + registered: boolean; + observations: Observation[]; + resolve: () => void; +} +export interface Requirement { + key?: string; // exact flight; absent when the desired request has not started + reader: number; + node: number; + inputs: number[]; + gate: "missing" | "waiting" | "resolved"; + fallback: boolean; + basis: "desired" | "fallback-retained"; + witness?: Witness; + boundary?: number; // enclosing published fallback covering a nested reader +} +export interface RunResult { + status: "pass" | "fail" | "policy" | "inapplicable" | "invalid" | "limit" | "error"; + failure?: Failure; + waiting?: WaitingFinding; + progress?: ProgressFinding[]; + actionScripts?: Array<{ + id: number; + started: boolean; + ended: boolean; + segment: number; + waiting: boolean; + }>; + groups?: { + scope?: string; + events: string[]; + checks: number; + held: number; + possibleJoins: number; + batchingHeld: number; + }; + attachment?: { nodes: number; privateWrites: number; visibleWrites: number; checks: number }; + reads?: Array<{ + at: string; + ref: number; + mode: "plain" | "latest" | "pending"; + verdict?: boolean; + value?: number; + pending?: boolean; + }>; + warmup?: { requests: number; pending: boolean }; + action?: { + started: boolean; + bodyCompleted: boolean; + gates: Array<{ step: number; state: "waiting" | "resolved" }>; + }; + error?: string; + diagnostics?: string; + cleanupError?: string; + clicks?: Array<{ at: string; reader: number; values?: number[]; pending?: boolean }>; + frames: Frame[]; + scenario: Scenario; + requirements: Requirement[]; + footprints?: Array<{ reader: number; requested: number[]; published?: number[] }>; + work: Array>; + coverage: string[]; + metrics: { + operations: number; + skipped: number; + requests: number; + frames: number; + elapsedMs: number; + heapUsedBytes?: number; // worker sample after cleanup, not a peak measurement + }; + events: string[]; +} +export interface RunOptions { + completionProbes?: boolean; + maxWork?: number; + maxRounds?: number; + maxFrames?: number; + waitingPolicy?: WaitingPolicy; + allowances?: Allowance[]; + groupChecks?: boolean; +} + +const noop = () => {}; +// Explicit hook shape: no Proxy trap on every runtime read/recompute. The type +// checker catches upstream additions rather than silently swallowing them. +const quietHooks: AttributionHooks = { + interactionStart: noop, + interactionEnd: noop, + originStart: noop, + originEnd: noop, + flushEnd: noop, + recomputeStart: noop, + recomputeEnd: noop, + derivedChanged: noop, + write: noop, + refreshed: noop, + flightStart: noop, + asyncStart: noop, + asyncEnd: noop, + effectRunStart: noop, + effectRunEnd: noop, + actionStepStart: noop, + actionStepEnd: noop, + holdStart: noop, + holdEnd: noop, + transitionSettled: noop, + transitionMerged: noop, + storeReplaced: noop, + listChurn: noop, + boundaryFallback: noop +}; +async function afterAwait(fn: () => void) { + await Promise.resolve(); + fn(); +} +async function awaitAnswer(promise: Promise, value: number) { + await promise; + return value; +} + +class HarnessStop extends Error { + constructor( + readonly status: "invalid" | "limit", + message: string + ) { + super(`${status.toUpperCase()}: ${message}`); + } +} + +export async function runScenario(input: Scenario, options: RunOptions = {}): Promise { + const start = performance.now(); + const s = structuredClone(input); + const invalid = validate(s); + const result: RunResult = { + status: invalid ? "invalid" : "pass", + error: invalid, + frames: [], + scenario: s, + requirements: [], + work: [], + coverage: [], + events: [], + metrics: { operations: 0, skipped: 0, requests: 0, frames: 0, elapsedMs: 0 } + }; + if (invalid) return result; + const model = new Model(s); + const sources = model.sources; + const canCheckProgress = progressScope(s, model); + const groupExclusion = options.groupChecks === false ? "disabled" : groupScope(s, model); + const groups = groupExclusion ? undefined : new Groups(model, s.readers); + const outputTree = s.readers.some(r => r.render) + ? new OutputTree(() => { + throw new HarnessStop("limit", "output nodes"); + }) + : undefined; + const outputRegions: Array<{ id: number; root: number; initialized: boolean; count: number }> = + []; + if (outputTree) result.attachment = { nodes: 0, privateWrites: 0, visibleWrites: 0, checks: 0 }; + const anchors = model.anchors; + const readers = new Map(); + for (const reader of s.readers) readers.set(reader.id, reader); + const children = new Map(); + for (const reader of s.readers) + if (reader.parent !== undefined) { + let siblings = children.get(reader.parent); + if (!siblings) children.set(reader.parent, (siblings = [])); + siblings.push(reader); + } + const nested = children.size > 0; + const history = new Map(); + for (const reader of s.readers) + if (reader.boundary !== "none" || reader.parent !== undefined) + history.set(reader.id, new Footprint(model, reader)); + const nodeIds = history.size ? s.nodes.map(node => node.id) : []; + const mountIds: number[] = []; + for (const reader of s.readers) if (reader.mounted !== undefined) mountIds.push(reader.id); + const workByNode = new Map(); + for (const node of s.nodes) workByNode.set(node.id, []); + const lastRequest = (node: number, inputs: number[]): Work | undefined => { + const flights = workByNode.get(node)!; + for (let i = flights.length - 1; i >= 0; i--) + if (sameNumbers(flights[i].inputs, inputs)) return flights[i]; + }; + const host = new HostTasks(groups ? () => groups.beginTask() : undefined); + const flightMasks = groups ? new WeakMap() : undefined; + const previousHooks = attrHooks; + const work: Work[] = []; + const byPromise = new WeakMap(); + const occurrences = new Map(); + const outputs: Record = {}; + const disposers = new Map void>(); + const liveGenerations = new Map(); + const attachedGenerations = new Map(); + const preparedOutputs = new Map(); + let readerGeneration = 0; + const mountSetters = new Map void>(); + const wantedMounts: Record = {}; + for (const id of mountIds) wantedMounts[id] = readers.get(id)!.mounted!; + const shownMounts = { ...wantedMounts }; + const callbacks = new Map(); + const coverage = new Set(); + if (canCheckProgress) coverage.add("ordinary-progress-scope"); + const writes = new Set(); + const completed = new Set(); + let writeId = 0; + let sourceWrites = 0; + let sourceWrite = 0; + let showWrites = 0; + let proposed = 0; + const proposedInputs: Record = {}; + for (const id of sources) proposedInputs[id] = 0; + const shownInputs = { ...proposedInputs }; + const publicationTarget = () => + s.optimistic?.kind === "latest" && actionStarted && !actionEnded + ? { ...proposedInputs, [-1]: 0 } + : proposedInputs; + let wantedShow = s.show; + let shown = 0; + let shownShow = s.show; + let phase: "setup" | "run" | "cleanup" = "setup"; + let at = "setup"; + let disposeRoot = () => {}; + const setters = new Map unknown>(); + const getters = new Map number>(); + let setShow!: (value: boolean) => unknown; + let retireError: string | undefined; + let stopped: { error: unknown } | undefined; + let executingAction: number | undefined; + const scripts = new Map< + number, + { + started: boolean; + ended: boolean; + segment: number; + resume?: () => void; + promise?: Promise; + } + >(); + for (const spec of s.actions ?? []) + scripts.set(spec.id, { started: false, ended: false, segment: 0 }); + let actionStarted = false; + let actionEnded = false; + let resumeAction: (() => void) | undefined; + let actionPromise: Promise | undefined; + const actionGates: Array<{ step: number; state: "waiting" | "resolved" }> = []; + const limit = options.maxWork ?? 120; + const rounds = options.maxRounds ?? 40; + const frameLimit = options.maxFrames ?? 600; + const stop = (status: "invalid" | "limit", message: string) => { + stopped ??= { error: new HarnessStop(status, message) }; + }; + const checkStopped = () => { + if (stopped) throw stopped.error; + }; + const drain = async () => { + await host.drain(); + checkStopped(); + }; + + const record = (event: string) => { + if (phase !== "cleanup" && result.events.length < 500) result.events.push(`${at}: ${event}`); + }; + const fail = (failure: Failure) => { + if ( + phase === "run" && + !stopped && + (!result.failure || + (["W1", "O1", "O2", "P1", "G2"].includes(result.failure.rule) && + !["W1", "O1", "O2", "P1", "G2"].includes(failure.rule))) + ) { + result.failure = failure; + result.status = "fail"; + result.requirements = requirements(); + } + }; + const snapshot = (): Frame => { + const saved: Record = {}; + for (const reader of s.readers) { + const value = outputs[reader.id]; + saved[reader.id] = Array.isArray(value) + ? value.slice() + : typeof value === "object" + ? { pending: value.pending } + : value; + } + const frame: Frame = { at, input: shown, show: shownShow, outputs: saved }; + if (s.version === 2) { + const inputs: Record = {}; + for (const id of anchors) inputs[id] = shownInputs[id]; + frame.inputs = inputs; + } + if (mountIds.length) frame.mounts = { ...shownMounts }; + return frame; + }; + function requirements(): Requirement[] { + const values = model.evaluate(proposed, proposedInputs); + const required: Requirement[] = []; + for (const reader of s.readers) { + if (reader.mounted !== undefined ? !wantedMounts[reader.id] : !disposers.has(reader.id)) + continue; + const active = !reader.gated || wantedShow; + let boundary: number | undefined; + let ancestor: ReaderSpec | undefined = reader; + while (ancestor) { + if (outputs[ancestor.id] === "loading") { + boundary = ancestor.id; + break; + } + ancestor = ancestor.parent === undefined ? undefined : readers.get(ancestor.parent); + } + const fallback = boundary !== undefined; + if (!active && !fallback) continue; + const needed = model.select(reader.refs, values); + for (let nodeIndex = 0; nodeIndex < s.nodes.length; nodeIndex++) { + const node = s.nodes[nodeIndex]; + if (!needed[model.nodes[nodeIndex].slot] || node.delivery === "sync") continue; + if (active) { + const inputs = values.inputs(nodeIndex); + const request = lastRequest(node.id, inputs); + required.push({ + reader: reader.id, + node: node.id, + key: request?.key, + inputs, + gate: request?.state ?? "missing", + fallback, + basis: "desired", + ...(boundary !== undefined && boundary !== reader.id ? { boundary } : {}) + }); + } + } + if (fallback) { + const footprint = history.get(reader.id); + if (footprint) + for (const [w, witness] of footprint.retained) + if (w.state === "waiting") + required.push({ + reader: reader.id, + node: w.node, + key: w.key, + inputs: w.inputs, + gate: "waiting", + fallback: true, + basis: "fallback-retained", + witness, + ...(boundary !== reader.id ? { boundary } : {}) + }); + } + } + return required; + } + const checkReleased = () => { + if (phase !== "run" || stopped) return; + if (groups?.pending) { + const groupFailure = groups.progress(snapshot(), requirements()); + if (groupFailure) fail(groupFailure); + } + if (!canCheckProgress) return; + let unpublished = s.anchorShow !== false && shownShow !== wantedShow; + for (const id of sources) if (shownInputs[id] !== proposedInputs[id]) unpublished = true; + if (!unpublished) return; + coverage.add("ordinary-progress-probe"); + const frame = snapshot(); + const failure = checkProgress( + s, + frame, + proposedInputs, + wantedShow, + wantedMounts, + requirements(), + model + ); + if (!failure) return; + const outstanding: ProgressFinding["outstanding"] = []; + for (const w of work) + if (w.state === "waiting") + outstanding.push({ key: w.key, observations: w.observations.map(o => ({ ...o })) }); + const narrowDisposal = + sourceWrites === 1 && + s.version === 1 && + showWrites === 0 && + s.readers.every(r => !r.gated && r.boundary === "none"); + const allowance = progressAllowance( + options.allowances ?? [], + narrowDisposal, + sourceWrite, + outstanding + ); + const findings = (result.progress ??= []); + // Keep the first witness of each disposition; a later violation must not be + // hidden because an earlier checkpoint matched the historical exception. + if (!findings.some(f => f.allowance === allowance)) + findings.push({ failure, outstanding, allowance }); + coverage.add(allowance ? "progress-exception" : "progress-violation"); + if (!allowance) fail(failure); + }; + const heldVerdict = () => { + coverage.add("tracked-held-answer-verdict"); + }; + const checkpoint = () => { + if (outputTree && phase !== "cleanup") { + for (const region of outputRegions) { + outputs[region.id] = outputTree.read(region.root); + region.count = outputTree.count; + } + for (const region of outputRegions) { + if (phase === "run") { + result.attachment!.checks++; + const failure = checkAttachment( + region.id, + region.count, + region.initialized, + disposers.has(region.id) + ); + if (failure) fail({ ...failure, frame: snapshot() }); + } + if (region.count === 1) region.initialized = true; + } + result.attachment!.nodes = outputTree.size; + result.attachment!.privateWrites = outputTree.privateWrites; + result.attachment!.visibleWrites = outputTree.visibleWrites; + if (outputTree.privateWrites) coverage.add("detached-output-prepared"); + } + if (phase !== "run" || stopped) return; + const frame = snapshot(); + const groupingFailure = groups?.publication(frame); + if (groupingFailure) fail(groupingFailure); + result.metrics.frames++; + if (result.frames.length >= frameLimit) { + stop("limit", "publication frames"); + return; + } + result.frames.push(frame); + if (history.size) { + const desired = model.evaluate(proposed, proposedInputs); + const published = model.evaluate(shown, shownInputs); + let held = 0; + for (let i = 0; i < sources.length; i++) + if (desired.data[i] !== published.data[i]) held |= 1 << i; + for (const [id, path] of history) { + const reader = readers.get(id)!; + const output = outputs[id]; + const active = disposers.has(id) && (!reader.gated || wantedShow); + let content = + Array.isArray(output) && path.anchored && output.length === reader.refs.length; + if (content && Array.isArray(output)) + for (let i = 0; i < output.length; i++) + if (output[i] !== published.get(reader.refs[i])) { + content = false; + break; + } + path.update(desired, published, content, active, lastRequest, nodeIds, at, sourceWrite); + // With complete source witnesses, equality to the desired answer closes + // the old content interval. Hidden/disposed regions close it as well. + if (output === "hidden" || output === "absent" || (content && !(held & path.sources))) + path.clear(); + if (outputs[id] === "loading" && path.retained.size) + coverage.add("witnessed-fallback-work"); + } + } + const failure = + checkFrame(s, frame, model) ?? checkPending(s, frame, proposedInputs, heldVerdict, model); + if (failure) fail(failure); + const req = requirements(); + let pendingSources = 0; + for (const r of req) if (r.gate === "waiting") pendingSources |= model.sourceMask(r.node); + if (pendingSources & (pendingSources - 1)) coverage.add("pending-work-uses-multiple-sources"); + // Scope the historical permission to unconditional readers without a + // boundary. A conditional desired path need not be the published path. + for (const r of req) { + const reader = readers.get(r.reader)!; + if (reader.gated || reader.boundary !== "none" || r.gate !== "waiting") continue; + const w = lastRequest(r.node, r.inputs); + if (w && !w.observations.some(o => o.reader === r.reader && o.write === sourceWrite)) + w.observations.push({ reader: r.reader, write: sourceWrite, at }); + } + if (req.some(r => r.gate === "waiting")) coverage.add("observed-pending"); + let fallbacks = 0; + let content = false; + for (const reader of s.readers) { + const value = outputs[reader.id]; + if (value === "loading") fallbacks++; + else if (Array.isArray(value)) content = true; + } + if (fallbacks) coverage.add("published-fallback"); + if (fallbacks > 1) coverage.add("multiple-published-fallbacks"); + if (nested && fallbacks) { + for (const reader of s.readers) { + if (reader.parent === undefined) continue; + if (outputs[reader.id] === "covered") coverage.add("nested-content-covered"); + if (outputs[reader.id] === "loading" && Array.isArray(outputs[reader.parent])) + coverage.add("inner-fallback-beside-parent-content"); + } + } + if (fallbacks && content) coverage.add("fallback-beside-content"); + if (work.some(w => w.state === "waiting" && !req.some(r => r.key === w.key))) + coverage.add("unrequired-flight"); + // A one-shot onSettled is scoped. Only use completion of ALL generated + // writes, and verify publication rather than treating it as global idle. + if ( + options.completionProbes !== false && + writes.size && + writes.size === completed.size && + (anchors.some(id => shownInputs[id] !== proposedInputs[id]) || + (s.anchorShow !== false && shownShow !== wantedShow) || + mountIds.some(id => shownMounts[id] !== wantedMounts[id])) + ) + fail({ + rule: "S3", + message: "All generated writes reported settled before their inputs published", + frame, + expected: { + input: proposed, + show: wantedShow, + ...(mountIds.length ? { mounts: { ...wantedMounts } } : {}), + ...(s.version === 2 ? { inputs: { ...proposedInputs } } : {}) + } + }); + }; + const hooks: AttributionHooks = { + ...quietHooks, + flushEnd: checkpoint, + asyncStart: node => { + const mask = flightMasks?.get(node); + if (mask !== undefined) groups!.landing(mask); + }, + flightStart: (node, promise) => { + const w = byPromise.get(promise); + if (w) { + w.registered = true; + flightMasks?.set(node, model.sourceMask(w.node)); + } + }, + transitionSettled: () => record("transition reported complete (before commit)") + }; + + const observe = (id: number, value: Output, generation: number, tree = false) => { + if (phase === "cleanup") { + if (!disposers.has(id)) + retireError ??= `Reader ${id} published after disposal during cleanup`; + return; + } + if (!tree && liveGenerations.get(id) !== generation) { + fail({ rule: "S5", message: "Disposed reader published", reader: id }); + return; + } + if (readers.get(id)!.mounted !== undefined) { + preparedOutputs.set(generation, value); + if (attachedGenerations.get(id) !== generation) { + record(`prepared reader ${id}: ${JSON.stringify(value)}`); + return; + } + } + outputs[id] = value; + record(`reader ${id}: ${JSON.stringify(value)}`); + if (phase === "run" && Array.isArray(value) && model.tupleMasks.get(id)) { + const reader = readers.get(id)!; + const inputs: Record = {}; + const mask = model.tupleMasks.get(id)!; + for (let i = 0; i < reader.refs.length; i++) { + const ref = reader.refs[i]; + if (ref < 0) inputs[ref] = value[i]; + } + if (mask) { + const values = model.evaluate(inputs[-1] ?? 0, inputs); + // A tuple can certify a derivation only if it contains all of that + // derivation's sources. Missing unrelated sources create no obligation. + if ( + reader.refs.some((ref, i) => model.witnessed(ref, mask) && value[i] !== values.get(ref)) + ) + fail({ + rule: "S1", + message: "Torn tuple delivered to an effect", + reader: id, + expected: reader.refs.map(ref => values.get(ref)), + frame: snapshot() + }); + } + } + }; + const skip = (message: string, strict = false) => { + result.metrics.skipped++; + if (strict && s.strict) throw new HarnessStop("invalid", message); + record(`skip ${message}`); + }; + function execute(step: Leaf) { + checkStopped(); + result.metrics.operations++; + if (result.metrics.operations > 800) throw new HarnessStop("limit", "operations"); + switch (step.op) { + case "write": + case "show": { + const id = ++writeId; + writes.add(id); + if (step.op === "write") { + sourceWrites++; + sourceWrite = id; + const source = step.source ?? -1; + proposedInputs[source] = step.value; + if (source === -1) proposed = step.value; + groups?.write(source, step.value, snapshot(), executingAction); + setters.get(source)!(step.value); + coverage.add(`write-source-${source}`); + } else { + showWrites++; + wantedShow = step.value; + setShow(step.value); + } + record( + `${step.op}${step.op === "write" && step.source !== undefined ? ` ${step.source}` : ""} ${step.value}` + ); + if (options.completionProbes !== false) + onSettled(() => { + completed.add(id); + record(`write ${id} settled`); + }); + break; + } + case "resolve": { + if (step.variantKeys) + throw new HarnessStop("invalid", "Paired request choices require an equivalence replay"); + const flights = workByNode.get(step.node)!; + let found: Work | undefined; + const direction = step.which === "oldest" ? 1 : -1; + for ( + let i = direction === 1 ? 0 : flights.length - 1; + i >= 0 && i < flights.length; + i += direction + ) { + const flight = flights[i]; + if (flight.state === "waiting" && (!step.key || flight.key === step.key)) { + found = flight; + break; + } + } + if (!found) { + skip(`missing request ${step.key ?? step.node}`, !!step.key); + if (!step.key) Object.assign(step, { op: "noop" }); + break; + } + step.key = found.key; + found.resolve(); + record(`resolve ${found.key}`); + coverage.add(`resolve-${step.which}`); + break; + } + case "dispose": { + const dispose = disposers.get(step.reader); + if (!dispose) { + skip("already disposed reader"); + break; + } + disposers.delete(step.reader); + liveGenerations.delete(step.reader); + for (const w of work) + if (w.state === "waiting") + for (const o of w.observations) if (o.reader === step.reader) o.disposedAt = at; + outputs[step.reader] = "absent"; + dispose(); + coverage.add("reader-disposal"); + break; + } + case "flush": + coverage.add("explicit-flush"); + flush(); + groups?.separator(); + break; + case "mount": { + const id = ++writeId; + writes.add(id); + wantedMounts[step.reader] = step.value; + mountSetters.get(step.reader)!(step.value); + coverage.add(step.value ? "reader-mount-request" : "reader-unmount-request"); + record(`mount ${step.reader} ${step.value}`); + if (options.completionProbes !== false) + onSettled(() => { + completed.add(id); + record(`write ${id} settled`); + }); + break; + } + case "start-action": { + if (s.actions) { + const spec = s.actions.find(a => a.id === step.action)!; + const state = scripts.get(spec.id)!; + if (state.started) { + skip("action already started"); + break; + } + state.started = true; + groups?.startAction(spec.id, snapshot()); + const run = action(function* () { + for (let i = 0; i < spec.segments.length; i++) { + if (phase === "cleanup") return; + if (i > 0) groups?.resumeAction(spec.id); + state.segment = i; + executingAction = spec.id; + try { + for (const write of spec.segments[i]) + execute({ op: "write", source: write.source, value: write.value }); + } finally { + executingAction = undefined; + } + if (i + 1 < spec.segments.length) + yield new Promise(resolve => { + state.resume = resolve; + }); + } + state.ended = true; + groups?.finishAction(spec.id); + record(`action ${spec.id} body completed`); + }); + state.promise = run(); + void state.promise.catch(error => { + stopped ??= { error }; + }); + record(`action ${spec.id} started`); + break; + } + if (actionStarted) { + skip("action already started"); + break; + } + actionStarted = true; + coverage.add("action-started"); + const run = action(function* () { + for (const value of s.optimistic!.proposals) { + if (phase === "cleanup") return; + if (s.optimistic!.kind === "latest") { + execute({ op: "write", source: -1, value }); + proposedInputs[-2] = value; + } else execute({ op: "write", source: -2, value }); + const gate = { step: actionGates.length, state: "waiting" as "waiting" | "resolved" }; + actionGates.push(gate); + yield new Promise(resolve => { + resumeAction = () => { + gate.state = "resolved"; + resolve(); + }; + }); + } + if (phase === "cleanup") return; + execute({ op: "write", source: -1, value: s.optimistic!.authoritative }); + proposedInputs[-2] = s.optimistic!.authoritative; + actionEnded = true; + coverage.add("action-body-completed"); + }); + actionPromise = run(); + // Own failures immediately; don't let an invalid experiment escape as + // an unhandled rejection, and don't call a rejected action successful. + void actionPromise.catch(error => { + stopped ??= { error }; + }); + break; + } + case "resume-action": { + if (s.actions) { + const state = scripts.get(step.action!)!; + const resume = state.resume; + if (!resume) { + skip("action is not waiting"); + break; + } + state.resume = undefined; + record(`resume action ${step.action}`); + resume(); + break; + } + if (!resumeAction) { + skip("action is not waiting"); + break; + } + const resume = resumeAction; + resumeAction = undefined; + record("resume action gate"); + resume(); + break; + } + case "read": { + const read: NonNullable[number] = { + at, + ref: step.ref, + mode: step.mode + }; + try { + const getter = getters.get(step.ref)!; + if (step.mode === "pending") read.verdict = isPending(getter); + else read.value = step.mode === "latest" ? latest(getter) : getter(); + } catch (error) { + if (!(error instanceof NotReadyError)) throw error; + read.pending = true; + if (step.mode === "pending") + fail({ + rule: "R3", + message: "Context-free isPending propagated NotReadyError", + frame: snapshot() + }); + } + (result.reads ??= []).push(read); + coverage.add(`imperative-${step.mode}-read`); + // Ordinary context-free reads describe published state. latest has its + // own unresolved outside-read contract, including forwarding getters: + // retain samples for paired laziness checks without choosing a slot. + if (step.mode === "plain" && model.readsLatest(step.ref)) + coverage.add("imperative-latest-contract-provisional"); + if (step.mode === "plain" && !model.readsLatest(step.ref)) { + let expected: number | undefined = anchors.includes(step.ref) + ? shownInputs[step.ref] + : undefined; + if (expected === undefined) + for (const reader of s.readers) { + const value = outputs[reader.id]; + const index = reader.refs.indexOf(step.ref); + if (Array.isArray(value) && index !== -1) { + expected = value[index]; + break; + } + } + if (expected === undefined) coverage.add("imperative-read-without-published-witness"); + else if (!read.pending && read.value !== expected) + fail({ + rule: "R1", + message: "Imperative ordinary read disagrees with its published view", + frame: snapshot(), + expected: { ref: step.ref, value: expected, actual: read.value } + }); + else if (read.pending) + fail({ + rule: "R1", + message: "Published readable value suspended in an imperative read", + frame: snapshot(), + expected: { ref: step.ref, value: expected } + }); + } + break; + } + case "click": { + const output = outputs[step.reader]; + if (!output || typeof output !== "object" || Array.isArray(output) || output.pending) { + skip("guarded control is not ready"); + break; + } + const reader = readers.get(step.reader)!; + const click: NonNullable[number] = { at, reader: step.reader }; + try { + click.values = reader.refs.map(id => getters.get(id)!()); + } catch (error) { + if (!(error instanceof NotReadyError)) throw error; + click.pending = true; + fail({ + rule: "R2", + message: "A published ready control suspended when clicked", + reader: reader.id, + frame: snapshot() + }); + } + (result.clicks ??= []).push(click); + coverage.add("ready-control-click"); + if (click.values) { + const expected = model.evaluate(shown, shownInputs); + if ( + reader.refs.some( + (ref, i) => model.witnessed(ref) && click.values![i] !== expected.get(ref) + ) + ) + fail({ + rule: "R2", + message: "A ready control read data inconsistent with published inputs", + reader: reader.id, + frame: snapshot(), + expected: reader.refs.map(id => expected.get(id)) + }); + } + break; + } + case "noop": + break; + } + } + function block(steps: Step[]) { + for (const step of steps) { + if (step.op === "queue") { + const cb = { steps: queuedSteps(step), via: step.via, done: false }; + callbacks.set(step.id, cb); + const run = () => { + if (!cb.done && !stopped) { + cb.done = true; + groups?.closeBatch(); + // Report owned callback failures at the next host barrier. Letting + // these escape would lose the ledger and crash the worker instead. + try { + for (const step of cb.steps) execute(step); + } catch (error) { + stopped ??= { error }; + } + } + }; + coverage.add(step.via); + if (step.via === "microtask") queueMicrotask(run); + else if (step.via === "promise") Promise.resolve().then(run); + else if (step.via === "await") void afterAwait(run); + } else if (step.op === "cancel") { + const cb = callbacks.get(step.id); + if (cb?.via === "task" && !cb.done) { + cb.done = true; + coverage.add("cancel-task"); + } else skip("callback unavailable for cancellation"); + } else execute(step); + } + } + async function settle(requiredOnly: boolean, setup = false) { + for (let round = 0; ; round++) { + // Fulfilled promises can schedule another memo even when no gate is + // currently waiting. Reach a real task boundary before declaring quiet. + await drain(); + if (setup) { + flush(); + checkStopped(); + } + checkReleased(); + const req = requiredOnly ? requirements() : undefined; + const selected: Work[] = []; + for (const w of work) + if (w.state === "waiting" && (!req || req.some(r => r.key === w.key))) selected.push(w); + if (!selected.length) return; + if (round >= rounds) throw new HarnessStop("limit", "settling rounds"); + await host.run(() => { + for (const w of selected) { + record(`required ${w.key}`); + w.resolve(); + } + }); + await drain(); + if (setup) { + flush(); + checkStopped(); + } + } + } + try { + setAttributionHooks(hooks); + createRoot(dispose => { + disposeRoot = dispose; + for (const id of sources) { + if (s.optimistic && id === -2) continue; + const [source, write] = createSignal(0); + getters.set(id, source); + setters.set(id, write); + } + const source = getters.get(-1)!; + if (s.optimistic) { + const latestSource = s.optimistic.viaMemo ? createMemo(() => source()) : source; + if (s.optimistic.kind === "latest") + getters.set(-2, () => { + if (phase === "run") { + coverage.add("latest-channel-read"); + coverage.add(s.optimistic!.viaMemo ? "latest-of-memo" : "latest-of-signal"); + } + return latest(latestSource); + }); + else { + const [view, write] = createOptimistic(() => source()); + getters.set(-2, view); + setters.set(-2, write); + } + } + const [visible, show] = createSignal(s.show); + setShow = show; + for (const node of s.nodes) { + getters.set( + node.id, + createMemo(() => { + const inputs = capture(node, id => getters.get(id)!()); + const value = answer(node, inputs); + if (node.branch && phase === "run") + coverage.add(inputs[0] ? "branch-true" : "branch-false"); + if (node.delivery === "sync") return value; + if (work.length >= limit) { + // The case is now inconclusive. Avoid throwing a harness budget + // error through Solid's user computation/error propagation path. + // No more observations are checked; the worker will be retired. + stop("limit", "async work"); + return value; + } + const question = `${node.id}:${JSON.stringify(inputs)}`; + const ordinal = (occurrences.get(question) ?? 0) + 1; + occurrences.set(question, ordinal); + let resolve!: (value: number) => void; + const gate = new Promise(r => { + resolve = r; + }); + const w: Work = { + key: `${question}#${ordinal}`, + node: node.id, + inputs, + value, + state: "waiting", + registered: false, + observations: [], + resolve() { + if (w.state === "waiting") { + w.state = "resolved"; + resolve(value); + } + } + }; + work.push(w); + workByNode.get(node.id)!.push(w); + const promise = node.delivery === "await" ? awaitAnswer(gate, value) : gate; + byPromise.set(promise, w); + if (node.delivery === "promise") w.resolve(); + record(`start ${w.key}`); + return promise; + }) + ); + } + for (const id of anchors) + createRenderEffect(getters.get(id)!, value => { + shownInputs[id] = value; + if (id === -1) shown = value; + }); + if (s.anchorShow !== false) + createRenderEffect(visible, value => { + shownShow = value; + }); + if (s.anchors !== undefined && s.anchors.length < sources.length) + coverage.add("partial-source-observation"); + if (s.anchorShow === false) coverage.add("no-visibility-anchor"); + const treeHost: TreeHost | undefined = nested + ? { + read(reader) { + const path = history.get(reader.id); + if (path) path.attempted = 0; + const values: number[] = []; + for (const ref of reader.refs) { + if (path) path.attempted++; + values.push(getters.get(ref)!()); + } + return values; + }, + reset: source, + born(reader, dispose) { + const generation = ++readerGeneration; + disposers.set(reader.id, dispose); + liveGenerations.set(reader.id, generation); + if (outputs[reader.id] === undefined) + outputs[reader.id] = reader.parent === undefined ? "hidden" : "covered"; + return generation; + }, + gone(reader, generation) { + if (liveGenerations.get(reader.id) === generation) { + liveGenerations.delete(reader.id); + disposers.delete(reader.id); + } + }, + publish(reader, output, generation) { + // Tree scopes check actual callbacks against their cleanup flags; + // attaching cached content is not itself a disposed effect callback. + observe(reader.id, output, generation, true); + }, + covered(reader) { + outputs[reader.id] = "covered"; + }, + stale(reader) { + fail({ rule: "S5", message: "Disposed reader published", reader: reader.id }); + } + } + : undefined; + for (const reader of s.readers) { + if (reader.render) { + const root = outputTree!.create(true); + outputRegions.push({ id: reader.id, root, initialized: false, count: 0 }); + outputs[reader.id] = "absent"; + createRoot(dispose => { + disposers.set(reader.id, dispose); + mountOutput(outputTree!, root, reader.render!, getters.get(reader.render!.on)!, () => { + const values: number[] = []; + for (const ref of reader.refs) values.push(getters.get(ref)!()); + return values; + }); + }); + coverage.add(`output-${reader.render.target}`); + continue; + } + if (nested) { + if (reader.parent === undefined) mountTree(reader, children, treeHost!); + continue; + } + outputs[reader.id] = "absent"; + const mount = () => + createRoot(dispose => { + const generation = ++readerGeneration; + disposers.set(reader.id, dispose); + liveGenerations.set(reader.id, generation); + // A marker distinguishes not-yet-published from explicitly disposed. + if (reader.mounted === undefined) outputs[reader.id] = "hidden"; + const footprint = history.get(reader.id); + const probe = () => { + if (footprint) footprint.attempted = 0; + for (const ref of reader.refs) { + if (footprint) footprint.attempted++; + getters.get(ref)!(); + } + }; + const readData = (): number[] => { + if (footprint) footprint.attempted = 0; + const values: number[] = []; + for (const ref of reader.refs) { + if (footprint) footprint.attempted++; + values.push(getters.get(ref)!()); + } + return values; + }; + let verdict: (() => number) | undefined; + let ready = 0; + if (reader.pendingDepth) { + verdict = createMemo(() => (isPending(probe) ? 1 : 0)); + for (let depth = 1; depth < reader.pendingDepth; depth++) { + const previous = verdict; + verdict = createMemo(() => previous!() * 3 + 2); + ready = ready * 3 + 2; + } + coverage.add("derived-pending-verdict"); + } + const read = (): Output => + reader.gated && !visible() + ? "hidden" + : reader.pending + ? { pending: verdict ? verdict() !== ready : isPending(probe) } + : readData(); + const view = + reader.boundary === "none" + ? read + : createLoadingBoundary( + read, + () => "loading" as const, + reader.boundary === "reset" ? { on: source } : undefined + ); + createRenderEffect(view, value => observe(reader.id, value, generation)); + return { + generation, + dispose: () => { + if (liveGenerations.get(reader.id) === generation) { + liveGenerations.delete(reader.id); + disposers.delete(reader.id); + } + dispose(); + preparedOutputs.delete(generation); + } + }; + }); + if (reader.mounted === undefined) mount(); + else { + const [mounted, setMounted] = createSignal(reader.mounted); + mountSetters.set(reader.id, setMounted); + // This anchor explicitly represents a displayed mount control, just + // like the source and visibility anchors elsewhere in this grammar. + createRenderEffect(mounted, value => { + shownMounts[reader.id] = value; + }); + createRenderEffect( + () => { + const value = mounted(); + if (value) { + const { generation, dispose } = mount(); + onCleanup(dispose); + if (phase === "run") coverage.add("owned-reader-created"); + return generation; + } + return undefined; + }, + generation => { + // Child effects may prepare detached content before this parent + // publishes. Only attachment makes that content observable here. + if (generation === undefined) { + attachedGenerations.delete(reader.id); + outputs[reader.id] = "absent"; + } else { + attachedGenerations.set(reader.id, generation); + const value = preparedOutputs.get(generation); + if (value !== undefined) observe(reader.id, value, generation); + else outputs[reader.id] = "absent"; + } + } + ); + } + } + }); + flush(); + checkStopped(); + await settle(false, true); + await drain(); + flush(); + if (s.warmLatest?.length) { + const before = work.length; + result.warmup = { requests: 0, pending: false }; + await host.run(() => { + for (const id of s.warmLatest!) + try { + latest(getters.get(id)!); + } catch (error) { + if (!(error instanceof NotReadyError)) throw error; + result.warmup!.pending = true; + } + }); + await drain(); + result.warmup.requests = work.length - before; + } else { + // Match the early-warm variant's scheduling boundary exactly. + await host.run(() => {}); + await drain(); + } + phase = "run"; + at = "initial"; + checkpoint(); + for (let i = 0; i < s.turns.length; i++) { + at = `turn ${i}`; + const turn = s.turns[i]; + await host.run(() => { + if ("steps" in turn) block(turn.steps); + else { + const cb = callbacks.get(turn.task); + if (!cb || cb.done || cb.via !== "task") skip("task unavailable"); + else { + cb.done = true; + for (const step of cb.steps) execute(step); + } + } + }); + await drain(); + checkpoint(); + checkReleased(); + if (s.optimistic && actionStarted && !actionEnded) { + const req = requirements(); + if (req.every(r => r.gate === "resolved")) { + coverage.add("optimistic-ready-with-parent-open"); + const target = publicationTarget(); + const failure = checkOptimistic(s, snapshot(), target, model); + if (failure) fail(failure); + } + } + } + // Undelivered task callbacks are future external inputs, not orphaned memos. + at = "remaining tasks"; + for (const cb of callbacks.values()) + if (!cb.done && cb.via === "task") { + await host.run(() => { + cb.done = true; + for (const step of cb.steps) execute(step); + }); + await drain(); + checkReleased(); + } + // Action gates are future external completions, not memo requests. Finish + // the finite script before entering the no-new-input completion suffix. + at = "remaining action steps"; + for (let i = 0; resumeAction && !s.optimistic?.hold; i++) { + if (i >= 4) throw new HarnessStop("limit", "action steps"); + await host.run(() => execute({ op: "resume-action" })); + await drain(); + } + for (const [id, state] of scripts) { + while (state.resume) { + await host.run(() => execute({ op: "resume-action", action: id })); + await drain(); + checkpoint(); + checkReleased(); + } + if (state.promise) await state.promise; + } + if (actionPromise && !s.optimistic?.hold) await actionPromise; + if (s.optimistic?.hold && actionStarted && !actionEnded) + coverage.add("completion-with-parent-held"); + at = "required completion"; + await settle(true); + await drain(); + checkpoint(); + checkReleased(); + const before = snapshot(); + const finalCheck = () => + checkFinal( + s, + snapshot(), + publicationTarget()[-1], + wantedShow, + s.version === 2 ? publicationTarget() : undefined, + wantedMounts, + model + ); + const earlyFailure = finalCheck(); + const policy = options.waitingPolicy ?? "review"; + const narrowScope = + sourceWrites === 1 && + s.version === 1 && + showWrites === 0 && + s.readers.every(r => !r.gated && r.boundary === "none"); + const pending = work.filter(w => w.state === "waiting"); + if (s.optimistic && actionEnded && earlyFailure && pending.length) + fail({ + ...earlyFailure, + rule: "O2", + message: + "Authoritative publication still needs work outside its current requirements after the action body completes" + }); + const retained = narrowScope + ? pending.flatMap(w => { + const observations = retainedObservations(w.observations, sourceWrite); + return observations.length ? [{ key: w.key, observations }] : []; + }) + : []; + const retainedKeys = new Set(retained.map(w => w.key)); + if (earlyFailure && pending.length) { + result.waiting = { + rule: "W1", + message: "Publication waits beyond currently required work", + policy, + disposition: "open", + scope: narrowScope ? "single-write-disposal" : "other", + before, + afterRetained: before, + after: before, + pending: pending.map(w => w.key), + retained, + releases: [], + recovered: false + }; + if (policy === "required-only") { + result.waiting.disposition = "violation"; + fail({ ...earlyFailure, rule: "W1" }); + } + } + // Once a correct final view has been reached, no residual completion may + // change it. Arm this even if reaching it required a retained request. + let stable = earlyFailure + ? undefined + : JSON.stringify([shownInputs, shownShow, shownMounts, outputs]); + const release = async (w: Work, retained: boolean) => { + coverage.add("residual-completion"); + record(`residual ${w.key}`); + await host.run(() => w.resolve()); + await drain(); + checkpoint(); + checkReleased(); + result.waiting?.releases.push({ key: w.key, retained, frame: snapshot() }); + const view = JSON.stringify([shownInputs, shownShow, shownMounts, outputs]); + if (stable !== undefined && stable !== view) + fail({ + rule: "S4", + message: "Residual work changed a completed view without new input", + frame: snapshot() + }); + if (stable === undefined && !finalCheck()) stable = view; + }; + // Resolve only the witnessed requests first. New descendants and other + // residual flights must not silently inherit their permission. + at = "retained completion"; + let residualRounds = 0; + for (const w of pending) { + if (!retainedKeys.has(w.key)) continue; + if (residualRounds++ >= rounds) throw new HarnessStop("limit", "residual rounds"); + await release(w, true); + } + if (result.waiting) { + result.waiting.afterRetained = snapshot(); + // The accepted disposal exception is a sufficient permission, not a + // claim that every other kind of residual waiting violates a contract. + const retainedFailure = finalCheck(); + if ( + policy === "review" && + options.allowances?.includes("legacy-disposal-wait") && + narrowScope && + retained.length && + !retainedFailure + ) + result.waiting.disposition = "allowed"; + if (policy === "retain-disposed") { + const failure = retainedFailure; + result.waiting.disposition = !narrowScope + ? "out-of-scope" + : failure + ? "violation" + : "allowed"; + if (narrowScope && failure) fail({ ...failure, rule: "W1" }); + } + } + at = "residual completion"; + for (;;) { + const residual = work.findLast(w => w.state === "waiting"); + if (!residual) break; + if (residualRounds++ >= rounds) throw new HarnessStop("limit", "residual rounds"); + await release(residual, false); + } + at = "final completion"; + await drain(); + checkpoint(); + checkStopped(); + const finalFailure = finalCheck(); + if (finalFailure) fail(finalFailure); + if (result.waiting) { + result.waiting.after = snapshot(); + result.waiting.recovered = !finalFailure; + if (!result.failure && result.waiting.disposition !== "allowed") result.status = "policy"; + } + if (!result.failure) result.requirements = requirements(); + } catch (error) { + result.error = error instanceof Error ? error.stack : String(error); + result.status = error instanceof HarnessStop ? error.status : "error"; + } finally { + if (s.actions) result.actionScripts = []; + for (const [id, state] of scripts) + result.actionScripts!.push({ + id, + started: state.started, + ended: state.ended, + segment: state.segment, + waiting: !!state.resume + }); + result.groups = { + scope: groupExclusion, + events: groups?.events ?? [], + checks: groups?.checks ?? 0, + held: groups?.held ?? 0, + possibleJoins: groups?.possibleJoins ?? 0, + batchingHeld: groups?.batchingHeld ?? 0 + }; + if (s.optimistic) + result.action = { + started: actionStarted, + bodyCompleted: actionEnded, + gates: structuredClone(actionGates) + }; + // Preserve the stopped case, not the physical gate releases used for teardown. + result.work = work.map(({ resolve, ...record }) => record); + phase = "cleanup"; + try { + for (const [id, dispose] of disposers) { + disposers.delete(id); + liveGenerations.delete(id); + outputs[id] = "absent"; + dispose(); + } + disposeRoot(); + for (const cb of callbacks.values()) cb.done = true; + // Roots are disposed: release remaining gates without reusing the case's + // exhausted completion budget or throwing its already-recorded stop again. + await host.run(() => { + for (const w of work) w.resolve(); + }); + await host.drain(); + // Finish a paused finite action after a harness stop. Its remaining + // generator writes are guarded by checkStopped and reject harmlessly. + for (const state of scripts.values()) { + state.resume?.(); + if (state.promise) await state.promise.catch(() => {}); + } + if (resumeAction) { + resumeAction(); + await host.drain(); + } + flush(); + } catch (error) { + retireError = String(error); + } + setAttributionHooks(previousHooks); + host.close(); + if (retireError) { + result.status = "error"; + result.cleanupError = retireError; + result.error ??= `Cleanup failed: ${retireError}`; + } + if (history.size) { + result.footprints = []; + for (const [reader, path] of history) { + const requested: number[] = []; + const published: number[] = []; + for (const [ref, slot] of model.slots) { + if (path.requested[slot]) requested.push(ref); + if (path.publishedKnown && path.published[slot]) published.push(ref); + } + result.footprints.push({ + reader, + requested, + ...(path.publishedKnown ? { published } : {}) + }); + } + } + result.coverage = [...coverage].sort(); + result.metrics.requests = work.length; + result.metrics.elapsedMs = performance.now() - start; + result.scenario.strict = true; + result.scenario = canonicalize(result.scenario); + } + return result; +} diff --git a/packages/signals/tests/semantics/scenario.ts b/packages/signals/tests/semantics/scenario.ts new file mode 100644 index 000000000..79a4c9014 --- /dev/null +++ b/packages/signals/tests/semantics/scenario.ts @@ -0,0 +1,406 @@ +/** The replay format is independent of fast-check and of Solid's node layout. */ +export type Delivery = "sync" | "promise" | "manual" | "await"; +export interface RenderSpec { + on: number; + target: "local" | "portal"; + // Local writes prepare fresh detached content. Portal writes always schedule. + scheduled: boolean; +} +export interface MemoSpec { + id: number; + deps: number[]; // -1 is the numeric source; other references are stable memo IDs. + factor: number; + offset: number; + delivery: Delivery; + branch?: { condition: number; otherwise: number[] }; +} +export interface ReaderSpec { + id: number; + refs: number[]; + gated: boolean; + boundary: "none" | "retain" | "reset"; + parent?: number; // nested region inside an earlier reader's Loading content + mounted?: boolean; + pendingDepth?: number; // synchronous numeric derivations of the pending verdict + pending?: boolean; // rendered isPending verdict instead of a data tuple + render?: RenderSpec; // parent compute + nested host output effect +} +export type Leaf = + | { op: "write"; value: number; source?: number } + | { op: "show"; value: boolean } + | { + op: "resolve"; + node: number; + which: "oldest" | "newest"; + key?: string; + variantKeys?: Array; + } + | { op: "dispose"; reader: number } + | { op: "flush" } + | { op: "start-action"; action?: number } + | { op: "resume-action"; action?: number } + | { op: "read"; ref: number; mode: "plain" | "latest" | "pending" } + | { op: "click"; reader: number } // read the data guarded by a published ready verdict + | { op: "mount"; reader: number; value: boolean } + | { op: "noop" }; +export type Step = + | Leaf + | ({ + op: "queue"; + id: number; + via: "microtask" | "promise" | "await" | "task"; + } & ({ step: Leaf; steps?: never } | { steps: Leaf[]; step?: never })) + | { op: "cancel"; id: number }; +export type Turn = { steps: Step[] } | { task: number }; +export interface ActionSpec { + id: number; + // Each segment executes synchronously in the action. A controlled yield + // separates adjacent segments; the final segment finishes the action body. + segments: Array>; +} +export interface Scenario { + version: 1 | 2; + sources?: number[]; // v2: stable negative IDs, all initialized to zero; includes -1. + anchors?: number[]; // sources whose published values have an explicit observing effect + anchorShow?: boolean; + optimistic?: { + proposals: number[]; + authoritative: number; + hold?: boolean; + kind?: "override" | "latest"; + viaMemo?: boolean; + }; // -1 truth, -2 optimistic view + actions?: ActionSpec[]; + warmLatest?: number[]; + nodes: MemoSpec[]; + readers: ReaderSpec[]; + show: boolean; + turns: Turn[]; + strict?: boolean; // Minimized/canonical resolves must keep their exact question. +} + +export function validate(s: Scenario): string | undefined { + // Replay files are external input. A malformed file is not a Solid failure. + try { + return validateShape(s); + } catch { + return "Invalid scenario format"; + } +} + +function validateShape(s: Scenario): string | undefined { + if ( + ![1, 2].includes(s.version) || + !Array.isArray(s.nodes) || + !Array.isArray(s.readers) || + !Array.isArray(s.turns) + ) + return "Invalid scenario format"; + if (typeof s.show !== "boolean") return "Invalid initial observation"; + if (s.nodes.length > 32 || s.readers.length > 12 || s.turns.length > 100) + return "Scenario size limit"; + const sources = sourceIds(s); + if ( + (s.version === 1 && s.sources !== undefined) || + (s.version === 2 && !Array.isArray(s.sources)) || + sources.length > 4 || + !sources.includes(-1) || + new Set(sources).size !== sources.length || + sources.some(id => !Number.isInteger(id) || id >= 0) + ) + return "Invalid sources"; + const refs = new Set(sources); + if ( + s.anchors !== undefined && + (s.version !== 2 || + !Array.isArray(s.anchors) || + new Set(s.anchors).size !== s.anchors.length || + s.anchors.some(id => !sources.includes(id))) + ) + return "Invalid publication anchors"; + if (s.anchorShow !== undefined && (s.version !== 2 || typeof s.anchorShow !== "boolean")) + return "Invalid visibility anchor"; + for (const node of s.nodes) { + if ( + !Number.isInteger(node.id) || + node.id < 0 || + refs.has(node.id) || + !Array.isArray(node.deps) || + !node.deps.length || + node.deps.length > 12 || + node.deps.some(id => !refs.has(id)) || + (node.branch !== undefined && + (s.version !== 2 || + !refs.has(node.branch.condition) || + !Array.isArray(node.branch.otherwise) || + !node.branch.otherwise.length || + node.branch.otherwise.length > 12 || + node.branch.otherwise.some(id => !refs.has(id)))) || + !Number.isFinite(node.factor) || + !Number.isFinite(node.offset) || + !["sync", "promise", "manual", "await"].includes(node.delivery) + ) + return `Invalid memo ${node.id}`; + refs.add(node.id); + } + const readers = new Set(); + let boundaries = 0; + for (const r of s.readers) { + if ( + !Number.isInteger(r.id) || + r.id < 0 || + typeof r.gated !== "boolean" || + (r.mounted !== undefined && (typeof r.mounted !== "boolean" || s.version !== 2)) || + (r.pending !== undefined && + (typeof r.pending !== "boolean" || s.version !== 2 || s.optimistic?.kind === "latest")) || + (r.parent !== undefined && + (s.version !== 2 || + !readers.has(r.parent) || + s.readers.find(p => p.id === r.parent)!.boundary === "none")) || + (r.pendingDepth !== undefined && + (!r.pending || + !Number.isInteger(r.pendingDepth) || + r.pendingDepth < 1 || + r.pendingDepth > 3)) || + readers.has(r.id) || + !Array.isArray(r.refs) || + !r.refs.length || + r.refs.length > 12 || + r.refs.some(id => !refs.has(id)) || + !["none", "retain", "reset"].includes(r.boundary) + ) + return `Invalid reader ${r.id}`; + readers.add(r.id); + if (r.render !== undefined) { + if ( + s.version !== 2 || + s.optimistic || + s.actions || + s.warmLatest || + sources.length !== 1 || + s.nodes.some(n => n.branch) || + r.gated || + r.pending || + r.parent !== undefined || + r.mounted !== undefined || + r.boundary !== "none" || + !refs.has(r.render.on) || + !["local", "portal"].includes(r.render.target) || + typeof r.render.scheduled !== "boolean" || + (r.render.target === "portal" && !r.render.scheduled) + ) + return "Output attachment currently requires ordinary fixed readers and scheduled portals"; + const needed = new Set(r.refs); + for (let i = s.nodes.length - 1; i >= 0; i--) + if (needed.has(s.nodes[i].id)) for (const dep of s.nodes[i].deps) needed.add(dep); + if (!needed.has(r.render.on)) return "Output parent must be an ancestor of its payload"; + } + if (r.boundary !== "none") boundaries++; + } + const nested = s.readers.some(r => r.parent !== undefined); + if (nested && s.readers.some(r => r.render)) + return "Output attachment does not yet compose with nested boundaries"; + if ( + nested && + (s.optimistic || s.readers.some(r => r.gated || r.mounted !== undefined || r.pending)) + ) + return "Nested regions currently require static ordinary data readers"; + if (boundaries > 1 && s.version === 1) return "Multiple boundaries require version 2"; + if ( + boundaries && + s.nodes.some(n => n.branch) && + (nested || s.optimistic || s.readers.some(r => r.mounted !== undefined)) + ) + return "Branch/fallback history currently covers ordinary flat readers"; + if ( + s.warmLatest && + (!Array.isArray(s.warmLatest) || + s.warmLatest.length > 32 || + s.warmLatest.some(id => !refs.has(id))) + ) + return "Invalid latest warmup"; + if ( + s.optimistic && + (s.version !== 2 || + sources.length !== 2 || + !sources.includes(-2) || + (s.optimistic.hold !== undefined && typeof s.optimistic.hold !== "boolean") || + (s.optimistic.kind !== undefined && !["override", "latest"].includes(s.optimistic.kind)) || + (s.optimistic.viaMemo !== undefined && + (s.optimistic.kind !== "latest" || typeof s.optimistic.viaMemo !== "boolean")) || + !Array.isArray(s.optimistic.proposals) || + !s.optimistic.proposals.length || + s.optimistic.proposals.length > 4 || + ![...s.optimistic.proposals, s.optimistic.authoritative].every( + v => Number.isFinite(v) && Math.abs(v) <= 100 + ) || + (s.optimistic.kind !== "latest" && + s.readers.some(r => r.gated || r.boundary !== "none" || r.mounted !== undefined))) + ) + return "Unsupported optimistic scenario"; + if (s.actions !== undefined) { + if ( + s.version !== 2 || + s.optimistic || + !Array.isArray(s.actions) || + !s.actions.length || + s.actions.length > 2 + ) + return "Unsupported action scripts"; + const ids = new Set(); + for (const script of s.actions) { + if ( + !Number.isInteger(script.id) || + script.id < 0 || + ids.has(script.id) || + !Array.isArray(script.segments) || + script.segments.length < 2 || + script.segments.length > 5 + ) + return "Invalid action script"; + ids.add(script.id); + for (const segment of script.segments) + if ( + !Array.isArray(segment) || + segment.length > 4 || + segment.some( + w => !sources.includes(w.source) || !Number.isFinite(w.value) || Math.abs(w.value) > 100 + ) + ) + return "Invalid action segment"; + } + } + const queued = new Set(); + const leaf = (x: Leaf): boolean => { + switch (x.op) { + case "write": + return ( + !s.optimistic && + sources.includes(x.source ?? -1) && + Number.isFinite(x.value) && + Math.abs(x.value) <= 100 + ); + case "show": + return typeof x.value === "boolean"; + case "resolve": + return ( + refs.has(x.node) && + x.node >= 0 && + ["oldest", "newest"].includes(x.which) && + (x.variantKeys === undefined || + (Array.isArray(x.variantKeys) && + x.variantKeys.length <= 3 && + x.variantKeys.every(k => k === null || typeof k === "string"))) + ); + case "dispose": + return ( + !s.optimistic && + !nested && + readers.has(x.reader) && + s.readers.find(r => r.id === x.reader)?.mounted === undefined + ); + case "mount": + return ( + typeof x.value === "boolean" && + s.readers.some(r => r.id === x.reader && r.mounted !== undefined) + ); + case "start-action": + case "resume-action": + return s.optimistic ? x.action === undefined : !!s.actions?.some(a => a.id === x.action); + case "read": + return refs.has(x.ref) && ["plain", "latest", "pending"].includes(x.mode); + case "click": + return s.readers.some(r => r.id === x.reader && r.pending); + case "flush": + case "noop": + return true; + default: + return false; + } + }; + for (const turn of s.turns) { + if ("task" in turn) { + if (!queued.has(turn.task)) return "Unknown task"; + } else if (Array.isArray(turn.steps) && turn.steps.length <= 12) { + for (const step of turn.steps) { + if (step.op === "queue") { + if ( + queued.has(step.id) || + (step.step !== undefined && step.steps !== undefined) || + !Array.isArray(queuedSteps(step)) || + queuedSteps(step).length > 12 || + !queuedSteps(step).every(leaf) || + !["task", "microtask", "promise", "await"].includes(step.via) + ) + return "Invalid callback"; + queued.add(step.id); + } else if (step.op === "cancel") { + if (!queued.has(step.id)) return "Unknown callback"; + } else if (!leaf(step)) return "Invalid operation"; + } + } else return "Invalid turn"; + } +} + +/** Pure specification: no runtime accessors, flags, counters, or lane IDs. */ +const defaultSources = [-1]; +export function sourceIds(s: Scenario): number[] { + return s.sources ?? defaultSources; +} + +export function anchorIds(s: Scenario): number[] { + return s.anchors ?? sourceIds(s); +} + +export function queuedSteps(step: Extract): Leaf[] { + return step.steps ?? [step.step!]; +} + +export function evaluate( + s: Scenario, + input: number, + inputs?: Record +): Map { + const values = new Map(); + for (const id of sourceIds(s)) values.set(id, inputs?.[id] ?? (id === -1 ? input : 0)); + for (const n of s.nodes) { + const deps = !n.branch || values.get(n.branch.condition) ? n.deps : n.branch.otherwise; + let sum = 0; + for (const dep of deps) sum += values.get(dep)!; + values.set(n.id, sum * n.factor + n.offset); + } + return values; +} + +export function dependencies(n: MemoSpec, values?: Map): number[] { + if (!n.branch) return n.deps; + return [ + n.branch.condition, + ...(values + ? values.get(n.branch.condition) + ? n.deps + : n.branch.otherwise + : [...n.deps, ...n.branch.otherwise]) + ]; +} + +export function capture(n: MemoSpec, read: (id: number) => number): number[] { + if (!n.branch) return n.deps.map(read); + const condition = read(n.branch.condition); + return [condition, ...(condition ? n.deps : n.branch.otherwise).map(read)]; +} + +export function answer(n: MemoSpec, inputs: number[]): number { + let sum = 0; + for (let i = n.branch ? 1 : 0; i < inputs.length; i++) sum += inputs[i]; + return sum * n.factor + n.offset; +} + +export function ancestors(s: Scenario, refs: number[], values?: Map): Set { + const needed = new Set(refs); + for (let i = s.nodes.length - 1; i >= 0; i--) { + const n = s.nodes[i]; + if (needed.has(n.id)) for (const dep of dependencies(n, values)) needed.add(dep); + } + return needed; +} diff --git a/packages/signals/tests/semantics/search.test.ts b/packages/signals/tests/semantics/search.test.ts new file mode 100644 index 000000000..f2d4acb9d --- /dev/null +++ b/packages/signals/tests/semantics/search.test.ts @@ -0,0 +1,74 @@ +import { chain } from "./fixtures.js"; +import { search, ordinaryKey, SearchBudget } from "./search.js"; +import { runScenario } from "./runner.js"; +import { shrink } from "./shrink.js"; + +test("a productive deletion pass does not restart earlier passes on every deletion", async () => { + const s = chain("sync"); + s.version = 2; + s.sources = [-1]; + s.anchors = [-1]; + s.anchorShow = false; + for (let i = 1; i < 8; i++) s.readers.push({ id: i, refs: [-1], gated: false, boundary: "none" }); + const baseline = await runScenario(s); + const failure = { rule: "test", message: "synthetic persistent reader" }; + const original = { ...baseline, status: "fail" as const, failure }; + const results: { + reduced: Awaited>; + stats: Record; + }[] = []; + for (const schedule of ["restart", "sweep"] as const) { + const stats: Record = {}; + const reduced = await shrink( + original, + async scenario => ({ + ...baseline, + scenario, + status: scenario.readers.length && scenario.anchors?.includes(-1) ? "fail" : "pass", + failure: scenario.readers.length && scenario.anchors?.includes(-1) ? failure : undefined + }), + 500, + { schedule, order: ["anchors", "readers"], stats } + ); + expect(reduced.result.scenario.readers).toHaveLength(1); + results.push({ reduced, stats }); + } + expect(results[1].reduced.attempts).toBeLessThan(results[0].reduced.attempts); +}); + +test("search counts statically invalid candidates separately and shares execution accounting", async () => { + const s = chain("sync"), + limit = new SearchBudget(2); + const r = await search( + { scenario: s }, + limit, + async () => { + await limit.run(async () => undefined); + return undefined; + }, + ordinaryKey, + { order: ["nodes"] } + ); + expect(limit.executions).toBeLessThanOrEqual(2); + expect(r.candidates).toBeGreaterThanOrEqual(limit.executions); + expect(r.accepted).toBe(0); +}); + +test("a delivery escape is rolled back unless cleanup removes machinery", async () => { + const scenario = chain("promise"); + scenario.version = 2; + scenario.sources = [-1, -2]; + scenario.optimistic = { proposals: [1], authoritative: 0, hold: false }; + scenario.turns = [{ steps: [{ op: "start-action" }] }, { steps: [{ op: "resume-action" }] }]; + const budget = new SearchBudget(20); + const result = await search( + { scenario }, + budget, + async scenario => budget.run(async () => ({ scenario })), + ordinaryKey, + { order: ["deliveryEscape"] } + ); + expect(budget.executions).toBeGreaterThan(0); + expect(result.accepted).toBe(0); + expect(result.result.scenario).toBe(scenario); +}); diff --git a/packages/signals/tests/semantics/search.ts b/packages/signals/tests/semantics/search.ts new file mode 100644 index 000000000..b88613a60 --- /dev/null +++ b/packages/signals/tests/semantics/search.ts @@ -0,0 +1,160 @@ +import { validate, type Scenario } from "./scenario.js"; +import { canonicalize } from "./reduce.js"; +import { OrderingLimit } from "./order.js"; +import { compareSize } from "./complexity.js"; +import { + reductionFamilies, + reductionOrder, + expensiveFamilies, + type ReductionFamily +} from "./candidates.js"; + +export interface ReductionStats { + attempts: number; + accepted: number; + elapsedMs: number; +} + +/** Counts actual interpreter executions, including comparison sides and controls. */ +export class SearchBudget { + executions = 0; + candidates = 0; + readonly candidateLimit: number; + constructor( + readonly limit: number, + readonly minimum = 1 + ) { + this.candidateLimit = Math.max(1000, limit * 100); + } + available(count = this.minimum) { + return this.executions + count <= this.limit; + } + async run(execute: () => Promise): Promise { + if (!this.available(1)) throw new Error("Reduction execution budget exceeded"); + this.executions++; + return execute(); + } +} + +export interface SearchOptions { + order?: readonly ReductionFamily[]; + stats?: Partial>; + /** Kept for controlled comparison; sweeps avoid retrying earlier passes on every edit. */ + schedule?: "restart" | "sweep"; +} + +/** Shared traversal only. Admission, comparison and controls belong to callers. */ +export async function search( + initial: T, + budget: SearchBudget, + trial: (scenario: Scenario) => Promise, + key: (scenario: Scenario) => string, + options: SearchOptions, + stop?: (result: T) => boolean +) { + let result = initial, + accepted = 0, + duplicates = 0, + candidates = 0, + invalid = 0; + let changedOrder = false; + const seen = new Set([key(initial.scenario)]); + const ordering = new OrderingLimit(initial.scenario); + const order = options.order ?? reductionOrder; + const schedule = options.schedule ?? "sweep"; + let finishing = false, + expensive = false; + // A fresh generator sees accepted edits; seen candidates are never replayed. + // Stay in the current family on success, advancing to earlier families next sweep. + let progress = true; + outer: while (progress && budget.available() && budget.candidates < budget.candidateLimit) { + progress = false; + for (let i = 0; i < order.length; i++) { + const family = order[i]; + if (finishing && family === "deliveryOrdering") continue; + if (!expensive && expensiveFamilies.has(family)) continue; + let again = true; + while (again && budget.available()) { + again = false; + for (const candidate of reductionFamilies[family](result.scenario)) { + if (budget.candidates >= budget.candidateLimit) break outer; + budget.candidates++; + candidates++; + if (validate(candidate)) { + invalid++; + continue; + } + if (family === "deliveryOrdering" && !ordering.allows(candidate)) continue; + const candidateKey = key(candidate); + if (seen.has(candidateKey)) { + duplicates++; + continue; + } + if (!budget.available()) break outer; + seen.add(candidateKey); + const stats = + options.stats && (options.stats[family] ??= { attempts: 0, accepted: 0, elapsedMs: 0 }); + const before = budget.executions, + started = stats ? performance.now() : 0; + let next = await trial(candidate); + if (next && family === "deliveryEscape") { + const order = (options.order ?? reductionOrder).filter(f => !expensiveFamilies.has(f)); + const finish = await search( + next, + budget, + trial, + key, + { order, schedule: "sweep" }, + stop + ); + candidates += finish.candidates; + duplicates += finish.duplicates; + invalid += finish.invalid; + next = + compareSize(finish.result.scenario, result.scenario) < 0 ? finish.result : undefined; + } + if (stats) { + stats.attempts += budget.executions - before; + stats.elapsedMs += performance.now() - started; + } + if (!next) continue; + result = next; + seen.add(key(result.scenario)); + ordering.accept(result.scenario); + accepted++; + if (stats) stats.accepted++; + if (family === "deliveryOrdering") changedOrder = true; + progress = true; + if (stop?.(result)) break outer; + if (schedule === "restart") { + i = -1; + break; + } + again = true; + break; + } + } + } + if (!progress && !expensive && order.some(f => expensiveFamilies.has(f))) { + expensive = true; + progress = true; + } + if (!progress && changedOrder && !finishing && budget.available()) { + // One ordinary finishing sweep, sharing the budget; no recursive search. + finishing = true; + seen.clear(); + seen.add(key(result.scenario)); + progress = true; + } + } + return { + result, + accepted, + duplicates, + candidates, + invalid, + exhausted: !budget.available() || budget.candidates >= budget.candidateLimit + }; +} + +export const ordinaryKey = (s: Scenario) => JSON.stringify(canonicalize(s)); diff --git a/packages/signals/tests/semantics/selection.test.ts b/packages/signals/tests/semantics/selection.test.ts new file mode 100644 index 000000000..8fc0cb586 --- /dev/null +++ b/packages/signals/tests/semantics/selection.test.ts @@ -0,0 +1,34 @@ +import { CandidateQueue, type Candidate } from "./selection.js"; +import { chain } from "./fixtures.js"; +import { runScenario } from "./runner.js"; + +test("later simpler examples replace early ones without confusing symptoms with bug identity", async () => { + const result = await runScenario(chain("sync")); + const candidate = (index: number, size: number, manual = false): Candidate => { + const scenario = chain(manual ? "manual" : "sync"); + for (let i = 0; i < size; i++) scenario.turns.push({ steps: [{ op: "write", value: i }] }); + return { + index, + signature: "same symptom", + scenario, + pair: [], + result: { + ...result, + scenario, + status: "fail", + failure: { rule: "S1", message: "fixture" } + } + }; + }; + const queue = new CandidateQueue(2); + queue.offer(candidate(0, 10)); + queue.offer(candidate(1, 1)); + queue.offer(candidate(2, 20, true)); + expect(queue.entries.map(c => c.index)).toEqual([1, 2]); + queue.offer(candidate(3, 0)); + expect(queue.entries.map(c => c.index)).toEqual([3, 2]); + const other = candidate(4, 0); + other.signature = "another symptom"; + queue.offer(other); + expect(queue.entries.map(c => c.index)).toEqual([3, 4]); +}); diff --git a/packages/signals/tests/semantics/selection.ts b/packages/signals/tests/semantics/selection.ts new file mode 100644 index 000000000..e7d8602e9 --- /dev/null +++ b/packages/signals/tests/semantics/selection.ts @@ -0,0 +1,76 @@ +import { compareComplexity } from "./complexity.js"; +import { isSemanticFailure } from "./admission.js"; +import type { RunResult } from "./runner.js"; +import type { Scenario } from "./scenario.js"; + +export interface Candidate { + index: number; + signature: string; + scenario: Scenario; + result: RunResult; + pair: RunResult[]; +} + +// A scheduling heuristic, never a bug identity. Keep a little structural variety +// without keeping every graph (or its full trace) in memory. +function shape(s: Scenario): number { + let bits = s.optimistic ? 1 : 0; + if (s.actions?.length) bits |= 2; + for (const n of s.nodes) { + if (n.delivery === "manual") bits |= 4; + else if (n.delivery !== "sync") bits |= 8; + if (n.branch) bits |= 16; + } + for (const r of s.readers) { + if (r.gated) bits |= 32; + if (r.render) bits |= 64; + if (r.pending) bits |= 128; + } + return bits; +} + +function priority(c: Candidate): number { + return isSemanticFailure(c.result) ? 0 : c.result.waiting || c.result.progress?.length ? 1 : 2; +} +function compare(a: Candidate, b: Candidate): number { + return ( + priority(a) - priority(b) || compareComplexity(a.scenario, b.scenario) || a.index - b.index + ); +} + +export class CandidateQueue { + readonly entries: Candidate[] = []; + constructor(readonly capacity = 8) {} + offer(candidate: Candidate) { + const entries = this.entries; + let same = 0, + worst = -1; + const candidateShape = shape(candidate.scenario); + for (let i = 0; i < entries.length; i++) { + if (entries[i].signature !== candidate.signature) continue; + same++; + worst = i; // Entries are sorted best first. + if (shape(entries[i].scenario) === candidateShape) { + if (compare(candidate, entries[i]) >= 0) return; + entries[i] = candidate; + entries.sort(compare); + return; + } + } + if (same >= 2) { + if (compare(candidate, entries[worst]) >= 0) return; + entries[worst] = candidate; + } else entries.push(candidate); + entries.sort(compare); + if (entries.length > this.capacity) { + // Reserve breadth before spending slots on a second shape of one symptom. + for (let i = entries.length - 1; i > 0; i--) + for (let j = 0; j < i; j++) + if (entries[j].signature === entries[i].signature) { + entries.splice(i, 1); + return; + } + entries.pop(); + } + } +} diff --git a/packages/signals/tests/semantics/settled-writes.test.ts b/packages/signals/tests/semantics/settled-writes.test.ts new file mode 100644 index 000000000..ace0498da --- /dev/null +++ b/packages/signals/tests/semantics/settled-writes.test.ts @@ -0,0 +1,72 @@ +import { + createMemo, + createRenderEffect, + createRoot, + createSignal, + flush, + onSettled +} from "../../src/index.js"; +import { HostTasks } from "./host.js"; + +test("onSettled can start another async update after the triggering update has published", async () => { + const host = new HostTasks(); + const gates = new Map void>(); + const published = { a: -1, b: -1, details: -1 }; + const callbacks: Array<{ label: string; a: number; b: number; details: number }> = []; + let setA!: (value: number) => void; + let setB!: (value: number) => void; + let dispose!: () => void; + createRoot(d => { + dispose = d; + const [a, writeA] = createSignal(0); + const [b, writeB] = createSignal(0); + setA = writeA; + setB = writeB; + const details = createMemo(() => { + const value = b(); + return new Promise(resolve => { + gates.set(value, () => resolve(value)); + }); + }); + createRenderEffect(a, value => { + published.a = value; + }); + createRenderEffect( + () => [b(), details()], + ([b, details]) => { + published.b = b; + published.details = details; + } + ); + }); + try { + flush(); + await host.run(() => gates.get(0)!()); + await host.drain(); + await host.run(() => { + setA(1); + onSettled(() => { + callbacks.push({ label: "first", ...published }); + setB(1); + onSettled(() => { + callbacks.push({ label: "second", ...published }); + }); + }); + }); + await host.drain(); + expect(callbacks).toEqual([{ label: "first", a: 1, b: 0, details: 0 }]); + expect(published).toEqual({ a: 1, b: 0, details: 0 }); + await host.run(() => gates.get(1)!()); + await host.drain(); + expect(callbacks).toEqual([ + { label: "first", a: 1, b: 0, details: 0 }, + { label: "second", a: 1, b: 1, details: 1 } + ]); + } finally { + dispose(); + for (const resolve of gates.values()) resolve(); + await host.drain(); + flush(); + host.close(); + } +}); diff --git a/packages/signals/tests/semantics/shrink.test.ts b/packages/signals/tests/semantics/shrink.test.ts new file mode 100644 index 000000000..da37166ce --- /dev/null +++ b/packages/signals/tests/semantics/shrink.test.ts @@ -0,0 +1,129 @@ +import { chain } from "./fixtures.js"; +import { shrink } from "./shrink.js"; +import { runScenario } from "./runner.js"; + +test("a rejected candidate at the attempt budget does not count an unexecuted attempt", async () => { + const baseline = await runScenario(chain("sync")); + const original = { + ...baseline, + status: "fail" as const, + failure: { rule: "test", message: "synthetic reducer target" } + }; + const run = vi.fn(async () => ({ ...baseline, status: "invalid" as const })); + const reduced = await shrink(original, run, 1); + expect(run).toHaveBeenCalledTimes(1); + expect(reduced.attempts).toBe(1); + expect(reduced.accepted).toBe(0); +}); + +test("a shrink search never executes the same candidate twice", async () => { + const s = chain("sync"); + s.readers.push({ id: 1, refs: [-1], gated: false, boundary: "none" }); + const baseline = await runScenario(s); + const failure = { rule: "test", message: "duplicate search" }; + const original = { ...baseline, status: "fail" as const, failure }; + const inputs = new Set(); + const run = async (scenario: typeof s) => { + const key = JSON.stringify(scenario); + expect(inputs.has(key)).toBe(false); + inputs.add(key); + return { + ...baseline, + scenario, + status: scenario.readers.length ? ("fail" as const) : ("pass" as const), + failure: scenario.readers.length ? failure : undefined + }; + }; + const reduced = await shrink(original, run, 100); + expect(reduced.accepted).toBeGreaterThan(1); + expect(reduced.duplicates).toBeGreaterThan(0); + expect(reduced.attempts).toBe(inputs.size); +}); + +test("a passing control prevents a reduction from losing the distinguishing behavior", async () => { + const s = chain("sync"); + s.readers[0].boundary = "retain"; + const baseline = await runScenario(s); + const failure = { rule: "test", message: "same symptom with a different cause" }; + const original = { ...baseline, status: "fail" as const, failure }; + let calls = 0; + const run = async (scenario: typeof s) => { + calls++; + return { ...original, scenario }; + }; + const control = async (scenario: typeof s) => { + calls++; + const pass = scenario.readers.some(r => r.boundary === "retain"); + return { + ...baseline, + scenario, + status: pass ? ("pass" as const) : ("fail" as const), + failure: pass ? undefined : failure + }; + }; + const reduced = await shrink(original, run, 60, { control }); + expect(reduced.result.scenario.readers.some(r => r.boundary === "retain")).toBe(true); + expect(reduced.controlAttempts).toBeGreaterThan(1); + expect(reduced.attempts).toBe(calls); + expect(calls).toBeLessThanOrEqual(60); +}); + +test("control checks share the budget and reject an invalid baseline comparison", async () => { + const baseline = await runScenario(chain("sync")); + const original = { + ...baseline, + status: "fail" as const, + failure: { rule: "test", message: "held" } + }; + const run = vi.fn(async () => original); + const control = vi.fn(async () => baseline); + const reduced = await shrink(original, run, 1, { control }); + expect(run).not.toHaveBeenCalled(); + expect(control).toHaveBeenCalledTimes(1); + expect(reduced.attempts).toBe(1); + expect(reduced.accepted).toBe(0); + await expect(shrink(original, run, 1, { control: async () => original })).rejects.toThrow( + "clean pass" + ); +}); + +test("a matching trial is not accepted when the budget cannot verify its control", async () => { + const baseline = await runScenario(chain("sync")); + const original = { + ...baseline, + status: "fail" as const, + failure: { rule: "test", message: "held" } + }; + const run = vi.fn(async (scenario: typeof baseline.scenario) => ({ ...original, scenario })); + const control = vi.fn(async () => baseline); + const reduced = await shrink(original, run, 2, { control }); + expect(run).toHaveBeenCalledTimes(1); + expect(control).toHaveBeenCalledTimes(1); + expect(reduced.attempts).toBe(2); + expect(reduced.accepted).toBe(0); + expect(reduced.result).toBe(original); +}); + +test("repro keys join alpha-renamed failures without merging different schedules", async () => { + const s = chain("sync"); + const baseline = await runScenario(s); + const original = { + ...baseline, + status: "fail" as const, + failure: { rule: "test", message: "same execution" } + }; + const renamed = structuredClone(baseline.scenario); + renamed.nodes[0].id = 10; + renamed.nodes[1].id = 20; + renamed.nodes[1].deps = [10]; + renamed.readers[0].id = 30; + renamed.readers[0].refs = [-1, 20]; + const run = vi.fn(async () => original); + const a = await shrink(original, run, 0); + const b = await shrink({ ...original, scenario: renamed }, run, 0); + expect(a.reproKey).toBe(b.reproKey); + renamed.turns.push({ steps: [] }); + const c = await shrink({ ...original, scenario: renamed }, run, 0); + expect(c.reproKey).not.toBe(a.reproKey); + expect(run).not.toHaveBeenCalled(); +}); diff --git a/packages/signals/tests/semantics/shrink.ts b/packages/signals/tests/semantics/shrink.ts new file mode 100644 index 000000000..4e62f59f0 --- /dev/null +++ b/packages/signals/tests/semantics/shrink.ts @@ -0,0 +1,56 @@ +import type { Scenario } from "./scenario.js"; +import type { RunResult } from "./runner.js"; +import { scenarioKey } from "./normalize.js"; +import { fingerprint, isSemanticFailure } from "./admission.js"; +import { search, ordinaryKey, SearchBudget, type SearchOptions } from "./search.js"; +export { fingerprint, isSemanticFailure } from "./admission.js"; +export { + reductions, + reductionFamilies, + reductionOrder, + type ReductionFamily +} from "./candidates.js"; +export type { ReductionStats } from "./search.js"; + +export interface ShrinkOptions extends SearchOptions { + mode?: "discovery" | "focused"; + control?: (s: Scenario) => Promise; +} + +export async function shrink( + original: RunResult, + run: (s: Scenario) => Promise, + budget = 150, + options: ShrinkOptions = {} +) { + const target = fingerprint(original); + if (!target) throw new Error("Only reproducible failures or policy findings can be shrunk"); + const discovery = options.mode !== "focused" && isSemanticFailure(original); + const limit = new SearchBudget(budget); + let controlAttempts = 0; + const controlPasses = async (s: Scenario) => { + controlAttempts++; + const r = await limit.run(() => options.control!(s)); + return r.status === "pass" && fingerprint(r) === undefined; + }; + if (options.control && budget > 0 && !(await controlPasses(original.scenario))) + throw new Error("The original scenario's control must be a clean pass before shrinking"); + const reduced = await search( + original, + limit, + async candidate => { + const trial = await limit.run(() => run(candidate)); + if (discovery ? !isSemanticFailure(trial) : fingerprint(trial) !== target) return; + if (options.control && (!limit.available() || !(await controlPasses(trial.scenario)))) return; + return trial; + }, + ordinaryKey, + options + ); + return { + ...reduced, + attempts: limit.executions, + controlAttempts, + reproKey: `${fingerprint(reduced.result)}\n${scenarioKey(reduced.result.scenario)}` + }; +} diff --git a/packages/signals/tests/semantics/tree.ts b/packages/signals/tests/semantics/tree.ts new file mode 100644 index 000000000..7c002203d --- /dev/null +++ b/packages/signals/tests/semantics/tree.ts @@ -0,0 +1,125 @@ +import { + createLoadingBoundary, + createRenderEffect, + createRoot, + onCleanup +} from "../../src/index.js"; +import type { Output } from "./rules.js"; +import type { ReaderSpec } from "./scenario.js"; + +interface Content { + output: Output; + children: Region[]; + disposed: boolean; +} +interface Region { + spec: ReaderSpec; + generation: number; + attached: boolean; + disposed: boolean; + view?: Content | "loading"; + dispose: () => void; +} +export interface TreeHost { + read(spec: ReaderSpec): Output; + reset: () => number; + born(spec: ReaderSpec, dispose: () => void): number; + gone(spec: ReaderSpec, generation: number): void; + publish(spec: ReaderSpec, output: Output, generation: number): void; + covered(spec: ReaderSpec): void; + stale(spec: ReaderSpec): void; +} + +/** A tiny renderer for nested boundary regions. Allocation follows structural + * mounting/reset, not each flush. A child's apply may prepare detached content; + * only its enclosing boundary's published content makes that child visible. */ +export function mountTree( + root: ReaderSpec, + children: Map, + host: TreeHost +): void { + const cover = (region: Region) => { + region.attached = false; + host.covered(region.spec); + if (region.view && region.view !== "loading") + for (const child of region.view.children) cover(child); + }; + const publish = (region: Region) => { + if (!region.view) return; + if (region.view === "loading") host.publish(region.spec, "loading", region.generation); + else { + host.publish(region.spec, region.view.output, region.generation); + for (const child of region.view.children) { + child.attached = true; + publish(child); + } + } + }; + const mount = (spec: ReaderSpec): Region => + createRoot(disposeRoot => { + const region: Region = { + spec, + generation: 0, + attached: false, + disposed: false, + dispose() { + if (region.disposed) return; + region.disposed = true; + host.gone(spec, region.generation); + disposeRoot(); + } + }; + region.generation = host.born(spec, region.dispose); + const content = (): Content => { + const result: Content = { output: "absent", children: [], disposed: false }; + onCleanup(() => { + result.disposed = true; + }); + // Child effects live in the enclosing Loading context, just as compiled + // JSX's individual text/attribute effects do. An inner boundary catches + // its own work; an unbounded child suspends the outer region instead. + createRenderEffect( + () => host.read(spec), + output => { + if (region.disposed || result.disposed) { + host.stale(spec); + return; + } + result.output = output; + if (region.attached && region.view === result) + host.publish(spec, output, region.generation); + } + ); + const nested = children.get(spec.id); + if (nested) + for (const child of nested) { + const scope = mount(child); + result.children.push(scope); + onCleanup(scope.dispose); + } + return result; + }; + if (spec.boundary === "none") region.view = content(); + else { + const view = createLoadingBoundary( + content, + () => "loading" as const, + spec.boundary === "reset" ? { on: host.reset } : undefined + ); + createRenderEffect(view, next => { + if (region.disposed) { + host.stale(spec); + return; + } + if (region.attached && region.view && region.view !== "loading") + for (const child of region.view.children) cover(child); + region.view = next; + if (region.attached) publish(region); + }); + } + return region; + }); + const region = mount(root); + region.attached = true; + publish(region); +} diff --git a/packages/signals/tests/semantics/tsconfig.json b/packages/signals/tests/semantics/tsconfig.json new file mode 100644 index 000000000..c017e9142 --- /dev/null +++ b/packages/signals/tests/semantics/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { "noEmit": true, "emitDeclarationOnly": false, "rootDir": "../.." }, + "include": ["../../src", "./*.ts"] +} diff --git a/packages/signals/tests/semantics/worker-fixture.ts b/packages/signals/tests/semantics/worker-fixture.ts new file mode 100644 index 000000000..281193087 --- /dev/null +++ b/packages/signals/tests/semantics/worker-fixture.ts @@ -0,0 +1,31 @@ +import { build } from "esbuild"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { Client } from "./client.js"; + +/** Generated cases may halt Solid or throw out of band. Keep those out of the + * Vitest process, just as the CLI keeps them out of the campaign coordinator. */ +export async function withWorker(test: (client: Client) => Promise): Promise { + const dir = await mkdtemp(join(tmpdir(), "solid-fuzz-fixture-")); + const file = join(dir, "worker.mjs"); + const client = new Client(file); + try { + await build({ + entryPoints: [fileURLToPath(new URL("./worker.ts", import.meta.url))], + outfile: file, + bundle: true, + platform: "node", + format: "esm", + target: "node22", + define: { __DEV__: "true", __OBSERVE__: "true", __TEST__: "true" }, + logLevel: "silent" + }); + await test(client); + } finally { + await client.close(); + await rm(dir, { recursive: true, force: true }); + client.checkHealth(); + } +} diff --git a/packages/signals/tests/semantics/worker.ts b/packages/signals/tests/semantics/worker.ts new file mode 100644 index 000000000..03ea9e5e8 --- /dev/null +++ b/packages/signals/tests/semantics/worker.ts @@ -0,0 +1,8 @@ +import { parentPort } from "node:worker_threads"; +import { runScenario } from "./runner.js"; + +parentPort!.on("message", async ({ scenario, options }) => { + const result = await runScenario(scenario, options); + result.metrics.heapUsedBytes = process.memoryUsage().heapUsed; + parentPort!.postMessage(result); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 21ed09cc3..22168ffce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -453,6 +453,9 @@ importers: '@types/node': specifier: ^25.0.8 version: 25.6.0 + fast-check: + specifier: 4.9.0 + version: 4.9.0 rimraf: specifier: ^5.0.1 version: 5.0.10 @@ -2810,6 +2813,10 @@ packages: resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} engines: {node: '>=8.0.0'} + fast-check@4.9.0: + resolution: {integrity: sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg==} + engines: {node: '>=12.17.0'} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -3523,6 +3530,9 @@ packages: pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + pure-rand@8.4.2: + resolution: {integrity: sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==} + quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} @@ -6475,6 +6485,10 @@ snapshots: dependencies: pure-rand: 6.1.0 + fast-check@4.9.0: + dependencies: + pure-rand: 8.4.2 + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -7132,6 +7146,8 @@ snapshots: pure-rand@6.1.0: {} + pure-rand@8.4.2: {} + quansync@0.2.11: {} queue-microtask@1.2.3: {}