Repository navigation
fix(oracles): never instrument a super() call as a mutation site - #3
Merged
Merged
Conversation
The statement-deletion mutant wrapped an expression statement that calls
`super(...)` (`G.m(id) || super(...)`). A derived constructor then ran
with `super` conditional or moved, and with TypeScript's parameter
properties or field initializers it read `this` before `super` and threw
("Must call super constructor..."), so every construction failed with no
mutant active. Expression-position super calls were already excluded;
statement-position ones were not.
The fix excludes a `super(...)` call from `delete-call` in schemata.mjs.
The static oracle gains a derived-class grammar (DERIVED file: a base
class and a derived class whose constructor runs statements before and
after `super('a')`, with and without parameter properties and a field
initializer). Its laws: the source runs as the model says; the
instrumented class with no mutant active runs as its source does; under
every mutant the constructor completes with the base tag; and the model's
expected sites contain no site for the super call.
Law: The static half of the gap report keeps each derived class's super
call unconditional (no mutant removes or moves it; with no mutant active
the instrumented class behaves as its source). Authority: the gap
report's contract that instrumentation with no mutant active does not
change behavior and that a mutant is a legal program variant (the
operator contract in schemata.mjs), and ECMAScript's rule that a derived
constructor calls super before it reads this and before it returns.
Maintainer report G11 (TanStack DB subscription.ts, 2026-11-09).
Reach: 21 generated histories (fixed seed 20261105) and 25 (random seed)
entered `a derived class constructed`, and every derived state
(`a derived class with a parameter property`, `... with a plain parameter`,
`... with a field initializer`, `... with statements before super`,
`... with no statement before super`, `... with a conditional before
super`, `... with a statement after super`, `... reading this after
super`) is reached under both seeds, with two constructed histories
naming them so each is reached on every campaign.
RED: on the parent code (schemata.mjs unchanged, new grammar), the static
oracle fails law `no-mutant behavior` at checkpoint `derived`, fixed seed
20261105 (the shrunk history has `derived: {before: [], after: []}`):
the instrumented class with no mutant active throws "Must call super
constructor in derived class before accessing 'this' or returning from
derived constructor", the source returns labels [] and tag "a". Both
calibrations of the new laws are then also red, because their clean trace
fails.
GREEN: `vitest --run --config oracle-gaps/vitest.config.mjs` (all four
files, 34 tests, exit 0) on the final tree, including the static oracle
with its two new calibrations (`super unconditional`, `no-mutant behavior`)
detected.
Mutant: a hostile "fix" that exempts only a leading `super(...)` statement
(the first statement of the constructor body) leaves a later super call
wrapped. The static oracle rejects it with `no-mutant behavior` (fixed and
random seeds). With no mutant active, the wrapped super call throws in the
TypeScript-emitted class, so the no-mutant law catches it before the
per-mutant law does.
Harness fix found by the end-to-end run: the determinism law compared the
CLI's raw stderr, and a refusal's stack names the CLI copy's directory
(`oracle-gaps-mutant cli #<random>`), which differs per run for a
spaced-path history. The daemon branch already names those directories;
the `cli` observation now names them the same way, in raw and file-URL
form. The law still compares everything else the CLI reports.
Mutant check: the failing determinism run (before the normalization) is the
RED for this harness change; the GREEN is the full run above.
Limits: derived classes are judged only in the static oracle's in-process
run. No test imports the derived file, so the end-to-end oracle does not
run it, and the run transpiles it to CommonJS at ES2022 (other downlevel
targets and Vitest's transform are not run). The mutant switch restates
the contract in schemata.mjs. The grammar has no `super.method()` call,
no super in a value position, and no class of a container other than
`Derived`. Verifying the TanStack DB subscription.ts case itself (~80
oracle files) is not in this change: it needs a run against that checkout.
`createOracleReach` is TanStack DB's; this package declares reach with
`states` and `constructed` histories (oracle-property.ts).
Co-authored-by: Isaac <no-reply@databricks.com>
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars.
|
🦋 Changeset detectedLatest commit: 287eca0 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
commit: |
Review follow-up to 77a06fd. That commit excluded a `super(...)` statement from `delete-call`, which left shapes where a mutant still prevents or delays the call: a `super` in an arrow argument, a `super` as a logical operand, and a statement before `super` that returns early. Maintainer decision (2026-11-09): one rule for every shape. A derived constructor gets no mutation site in its super region, which is its parameters, the statements before its first statement that calls `super`, and that statement. An arrow inherits `super`, so its body counts; a function, method, accessor, or class does not. Statements after the call keep their sites. `superGuarded` in schemata.mjs implements it. Law: no mutant, when active, prevents or delays a derived constructor's super call; with no mutant active the instrumented class behaves as its source; under every mutant the constructor completes with its base tag, unless an early return ends it first; statements after super keep their sites. Authority: the gap report's behavior-preservation and legal-variant contract (schemata.mjs operator contract), ECMAScript's rule that a derived constructor calls super before it reads this and before it returns, and the maintainer decision above. Grammar (gap-report-model-oracle.mjs): a derived class's before statements (push, conditional push, early `return {}`), one of four super forms (a statement; `[0].forEach(() => super('a'))`; `out && super('a')`; `nothing(n) || super('a')`), and after statements (push, `this.tag` read, conditional push). The model gives sites only to after statements. The form filter keeps sources legal TypeScript: a non-statement super needs plain parameters and no field, since TypeScript emits parameter-property assignments before the body and requires a root-level super beside a field. Reach: derive-from-observation (static oracle). Counts from the static run, fixed seed 20261105 / random seed: constructed 30 / 24; plain parameter 25 / 14; parameter property 5 / 7; field 9 / 7; early return 7 / 6; push before 9 / 6; conditional push before 7 / 5; push after 19 / 13; conditional push after 5 / 4; this read after 7 / 6; super as statement 10 / 15; super in an arrow 6 / 3; super after && 6 / 1; super after || 8 / 5. Plus constructed histories for each form and for the early return. RED: static oracle, on the 77a06fd schemata.mjs with the new grammar, fixed seed 20261105: law `super unconditional` at checkpoint `derived`, "under mutant 21 the class for 0 has tag undefined" (and mutant 30). The arrow, `&&`, and `||` forms let a mutant skip super; the `&&` and `||` forms return the `out` array with no error, so only the tag check sees it. The before-super sites that the rule removes are not separately shown red: the derived law fails first for them. Two harness defects were also red before the fix: the grammar's list cap (3 statements) and the form builds, which the oracle correctly rejected as outside the language. GREEN: static oracle 11/11 on the final tree (fixed and random seed, with the calibrations `super unconditional` and `no-mutant behavior` detected). Full suite (`vitest --run --config oracle-gaps/vitest.config.mjs`): 4 files, 34 tests, exit 0, on this commit's tree. Mutant: a hostile rule that covers only the super statement, not the statements before it, is rejected by `super unconditional` (mutant 21 and 23: a forced early return ends the constructor with no tag). The fix is restored byte-for-byte after the check. Harness hygiene (e2e, gap-report-oracle.property.test.mjs), no RED of its own: stderr is named (CLI directory and scratch package) before it is cut to its last 1500 characters (`readRun`); the daemon's first line reads that already named text; the scratch directory is resolved with realpathSync, as the static driver does. Comparison change: the `cli` observation now names the CLI copy by how.cli and the package directory before the cut, and the daemon path no longer calls replaceAll on a possibly undefined copy. No law is relaxed; the normalization is the determinism law's existing rule for scratch paths. Simplifications: one `derivedStates` table shared by the reach derivation and `required`; reach states come from the observed runs and the instrumented copy's super form, not the plan; `isSuperCall` and `holdsSuper` are shared; the `no-mutant behavior` calibration uses `corrupt`; `DERIVED_FN` is no longer exported; the plan carries one `derived` object (`spec`, `source`, `statements`). Limits: - The in-process run transpiles to CommonJS at ES2022; Vitest's transform (CR-6) and other downlevel targets are not exercised. - The mutant switch (`switchFor`) is a hand copy of the switch's contract, not the capture setup's runtime (CR-5): the capture setup imports Vitest and installs timers and globals on load, so it cannot run in-process. - The end-to-end oracle instruments the derived file and judges its sites as unowned, but no test imports or constructs the class; its no-mutant and super-region behavior is judged only in-process. - The model special case (CR-8) is kept as written. - Not generated: super in a method, a nested class, a `super.method()` call, a constructor default parameter, or a before-super `return` other than the early-object form. Co-authored-by: Isaac <no-reply@databricks.com>
2 of 3 tasks
KyleAMathews
added a commit
to TanStack/db
that referenced
this pull request
Nov 9, 2026
TanStack/oracles#3 merged (fe532f4): the gap report no longer creates mutation sites in the super region of a derived-class constructor, so it can judge class-heavy TanStack DB code such as subscription.ts. Both packages move from the 98a9610 preview to the pkg.pr.new preview of fe532f4. The framework API is unchanged. pnpm-lock.yaml was resolved from the public registry by a temporary CI job (run 42697076117, which also checked a frozen install), because the local registry proxy cannot fetch the new previews. Co-authored-by: Test <test@test.com> Co-authored-by: Isaac <no-reply@databricks.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🎯 Changes
The gap report no longer creates a mutation site in the super region of a derived-class constructor. The super region is the constructor's parameters, the statements before the first statement that calls
super(...), and that statement. An arrow function inheritssuper, so its body counts; function, method, accessor, and class bodies do not. Statements after the super call keep their mutation sites.Before this change, the
delete-callsite inschemata.mjsaccepted any expression statement whose expression is a call, includingsuper(...). The instrumentedG.m(id) || super(...)made a derived constructor's super call conditional. With TypeScript parameter properties or field initializers, the class then readthisbeforesuper, and it threw with no mutant active. So the gap report refused packages with derived classes. TanStack DB'spackages/db/src/collection/subscription.tsfailed about 80 oracle files with no mutant active.A review then found other shapes that could still skip or delay
superwhile a mutant is active: asuper(...)inside an arrow argument, asuper(...)as the right operand of&&or||, and an earlyreturnbeforesuper. The super-region rule covers all of them, so no mutant can prevent or delay a derived constructor's super call. The rule issuperGuardedinschemata.mjs.🔬 Oracle Coverage
gap-report-model-oracle.mjsgenerates a base class and a derived class. The super call is a statement, an arrow argument, or the right operand of&&or||. The constructor can have plain parameters or parameter properties, a field initializer, an early return beforesuper, and statements before and aftersuper. The model owns the expected sites and the expected run outputs.schemata.mjsand the new grammar, seed 20261105 failssuper unconditional("under mutant 21 the class for 0 has tag undefined"). The&&and||forms return normally under a mutant that skipssuper, so only the tag check sees them.tscand ESLint pass.super unconditional. A forced early return then ends the constructor with no tag.super.method(), default parameters, or other kinds of early return.The end-to-end oracle also had harness defects, which this PR fixes:
cliand daemon observations now name the copy and package directories before the output is cut to 1,500 characters.A run of this CLI on TanStack DB's
subscription.tshas not happened yet. This was follow-up G11 from TanStack/db#2378.✅ Checklist
🚀 Release Impact
@tanstack/oracles: patch).This pull request and its description were written by Isaac.