Skip to content

fix(oracles): never instrument a super() call as a mutation site - #3

Merged
KyleAMathews merged 2 commits into
mainfrom
fix/gap-super-mutant
Nov 9, 2026
Merged

KyleAMathews merged 2 commits into
mainfrom
fix/gap-super-mutant

Conversation

@KyleAMathews

@KyleAMathews KyleAMathews commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

🎯 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 inherits super, 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-call site in schemata.mjs accepted any expression statement whose expression is a call, including super(...). The instrumented G.m(id) || super(...) made a derived constructor's super call conditional. With TypeScript parameter properties or field initializers, the class then read this before super, and it threw with no mutant active. So the gap report refused packages with derived classes. TanStack DB's packages/db/src/collection/subscription.ts failed about 80 oracle files with no mutant active.

A review then found other shapes that could still skip or delay super while a mutant is active: a super(...) inside an arrow argument, a super(...) as the right operand of && or ||, and an early return before super. The super-region rule covers all of them, so no mutant can prevent or delay a derived constructor's super call. The rule is superGuarded in schemata.mjs.

🔬 Oracle Coverage

  • Law: with no mutant active, instrumentation keeps the source's behavior. No mutant, when active, prevents or delays a derived constructor's super call, and statements after the super call keep their mutation sites. Authority: the gap report's contract that the harness preserves behavior and that each mutant is a legal variant, ECMAScript's rule for derived constructors, and a maintainer decision (2026-11-09) to cover every shape.
  • History grammar: gap-report-model-oracle.mjs generates 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 before super, and statements before and after super. The model owns the expected sites and the expected run outputs.
  • Comparison: the static oracle runs the source and the instrumented copy in process under an explicit mutant switch. It checks three laws (source behavior, no-mutant behavior, and an unconditional super call), and two calibrations check the comparison.
  • Reach: constructed histories name each form, including the early return. The reach states come from the observed runs, not from the plan.
  • RED: with the first fix's schemata.mjs and the new grammar, seed 20261105 fails super unconditional ("under mutant 21 the class for 0 has tag undefined"). The && and || forms return normally under a mutant that skips super, so only the tag check sees them.
  • GREEN: the gap oracles (static, classify, probe-form, and end-to-end) pass: 4 files and 34 tests. The package tests pass: 9 files and 64 tests. tsc and ESLint pass.
  • Mutant: a rule that guards only the super statement, and not the statements before it, fails super unconditional. A forced early return then ends the constructor with no tag.
  • Limits: the derived file is judged in process, transpiled to CommonJS at ES2022, not through Vitest's transform. The mutant switch is a hand copy, because the capture setup installs Vitest timers and globals when it loads. The end-to-end oracle instruments the derived file but does not construct the class. The grammar does not generate a super call in a method or nested class, super.method(), default parameters, or other kinds of early return.

The end-to-end oracle also had harness defects, which this PR fixes:

  • Its determinism law compared raw stderr, and a refusal's stack trace names the CLI copy's random directory. The cli and daemon observations now name the copy and package directories before the output is cut to 1,500 characters.
  • The scratch directory is now resolved to its real path.

A run of this CLI on TanStack DB's subscription.ts has not happened yet. This was follow-up G11 from TanStack/db#2378.

✅ Checklist

  • I have tested this code locally. I ran the gap oracles, the package tests, type checking, and linting. A code review and a simplification pass ran, and the second commit fixes their findings.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset (@tanstack/oracles: patch).
  • This change is docs/CI/dev-only (no release).

This pull request and its description were written by Isaac.

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>
@coderabbitai

coderabbitai Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

Important

  • Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 48cfd632-ea92-4db4-8573-aa5e1581eca9
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@changeset-bot

changeset-bot Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 287eca0

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@tanstack/oracles Patch

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

@pkg-pr-new

pkg-pr-new Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/TanStack/oracles/@tanstack/eslint-plugin-oracles@3
npm i https://pkg.pr.new/TanStack/oracles/@tanstack/oracles@3

commit: 287eca0

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>
@KyleAMathews
KyleAMathews merged commit fe532f4 into main Nov 9, 2026
8 checks passed
@KyleAMathews
KyleAMathews deleted the fix/gap-super-mutant branch November 9, 2026 20:33
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant