Skip to content

checker: knock down the remaining TS7 conformance MISSes - #251

Merged
mizchi merged 11 commits into
mainfrom
claude/wonderful-mccarthy-6ljdwx
Sep 20, 2026
Merged

mizchi merged 11 commits into
mainfrom
claude/wonderful-mccarthy-6ljdwx

Conversation

@mizchi

@mizchi mizchi commented Sep 18, 2026 •

Copy link
Copy Markdown
Owner

Working the remaining MISS in scope backlog down from the measured baseline of 50 (not 30 — the repo's own docs, the justfile budget and a fresh oracle run all said 50), at FP 0.

Status

baseline now
TP 2665 2670
MISS in scope 50 45
OUT OF SCOPE 19 19
FP 0 0
PFLEGAL 0 0
TN 1750 1750

--max-miss tightened 50 → 45, which is the direction that budget only ever moves.

Five conformance files, six capabilities the corpus cannot score, twelve false positives on legal code, and a segfault. Five of the eight batches buy zero files each and were taken for the capability.

The segfault, first

types_definitely_differ recurses on types unwrap has already resolved — that is its design, it proves rather than normalizes — and it had no depth bound. A self-referential typeof closes the loop:

function fnArg1(x: typeof fnArg1) { var x: (n: typeof fnArg1) => void; }

witness.ts, a corpus file that had been a TP for as long as the oracle existed, ended the process the moment batch EU's new sweep handed it that pair. The module-level spelling could always reach it — measured by stashing the branch, rebuilding HEAD and running it, not assumed. No gate here watches for a crash: the oracle scored it as one lost TP.

Batches

EU (+2) — TS2708 through an import a = A alias: the recorded blocker was true of what the target means and false of what it spells. Its other half was namespace_is_instantiated counting a nested namespace's own value entry; a test in the SILENT list said in its own comment that tsc reports it there — the 14th test in this repo found asserting a gap rather than a behaviour. Plus TS2403 inside a function scope (the rule ran over module_.top_level_stmts and nowhere else) and the polymorphic this.

EV (0 files) — generic method call inference. Four spellings of one declaration, each losing the binder a different way: interface/class record it and no call site reads it; a function-type property carries it on the GenericFunc wrapper lookup_method_sig had no arm for; infer_call had the same hole; an inline object type's method signature threw it away in the parser under a comment saying so. Construct/call-signature binders are deliberately not carried — wrapping them cost a TP outright.

EW (0 files) — index-signature reads through an anonymous object type, one of the two BLIND rows the capability probe found by asking the common shape. The other one, the WRITE, turns out not to be blind at all.

EX (+1) — a mapped type over an infinite key set. The guard cost two FPs first, and the first attempt to test "does the value depend on the key" was defeated by a shape: type_references_any has no IndexedAccess arm.

EY (+1) — an intersection source against an index signature. check_expr_against carried (Intersection(_), _) => return and the abstention was total. This is exactly what batch EN went looking for and could not find.

EZ (+1) — one file, callChain.3, carrying four independent defects. It is filed in the triage under "object literals, contextual typing, widening", which is not what any of the four is; each was found by probing the next thing the previous fix exposed.

  1. An optional chain short-circuits for the whole chain, and the parser puts only the guarded link inside the OptionalChain node — g?.p.q is PropAccess(OptionalChain(PropAccess(g, p)), q) — so that outer .q pruned the nullish receiver and handed back a bare number. The propagation reads the chain node's own result rather than re-deciding its guard. is_nullable_type is not that predicate: it answers "is this type entirely nullish", so { q: number } | undefined — the exact shape a short-circuiting chain produces — reads false there, and the first draft fired on nothing.
  2. m?<T>(x) did not parse, at either member parser. The grammar is PropertyName ?opt CallSignature and a CallSignature begins with its type parameters, so the ? comes first; both read the binders first. The object-type parser's failure was total — the whole literal fell back to Any, so every member became unknowable, not just the generic one.
  3. An optional method is stored as Union([callable, Undefined]), so every consumer matching the callable shape read it as opaque and a.m?.(1) came back Any.
  4. unwrap peels a GenericFunc — it says so at its own site — so a generic member of a union callee reached infer_call with its binders gone and c?.m({ x: 12 }) came back with T unsolved, where the same member reached directly is solved ten lines above.

FA (0 files) — a late-bound unique symbol member key, the top BLIND row of the capability table at 183 occurrences across 3,000 real .d.ts files and no conformance file anywhere. Three more defects, the first of them batch EZ's shape again:

  1. The anonymous spelling did not parse. try_parse_object_type_with_members's bracket arm falls back for anything that is not an index signature, so { [k]: number; plain: number } came out Any and every member of it was lost. That fall-back is load-bearing for a mapped type ([K in keyof T] reaches the same arm), so the fix is a positive shape test — exactly [ Ident ] — rather than a widened fallback, which would have broken one feature to fix another.
  2. The interface spelling named the member <computed>, a name no lookup can match, while the sibling well-known spelling twenty lines above has had a stable @@name for a long time. One question, two answers.
  3. Nothing translated the index expression back, so i[k] was Any even once the member had a name. Wired at both infer_index arms, since writing it at one is how the two spellings came to disagree in the first place.

FB (0 files) — a string source against a LITERAL or literal-union target, UNSUPPORTED.md §3's widest row: declare const s: string; const a: "other" = s and four siblings were all silent, which is the commonest real TS2322 / TS2345 there is. This is the first batch here whose whole case rests on real packages — corpus-neutral on every metric, and on real code it both gains the diagnostic and removes six false positives.

  1. It is one abstention whose stated reason had a date on it. The gate suppresses any mismatch whose source is exactly the primitive base of the target's literals, because "TypeScript keeps the const literal narrow" where we widen. Probed one base at a time, that is true for number, boolean and bigint and false for string: infer_expr erases the first three at the source (NumberLit(_) => Number) and keeps Literal(s). So const c = "z"; const c2: "a" | "b" = c already reported while the string-source form never could. Fifteen string spellings were probed before the arm came out, and every one tsc keeps narrow we keep narrow.
  2. The one divergence is an erased as const, which the parser drops for a reason stated at its own site (the transform passes want the raw expression), so { k: "a" } as const arrives as the plain object literal whose property genuinely does widen here. A file carrying one abstains wholesale, through a file-level marker read in ingest_module rather than beside the other directive flags at the check entry — that site reads only the root module's markers, and a namespace body re-parses with a fresh Parser.
  3. The relaxation exposed a latent false positive rather than creating one. A mutable binding's fresh literal initializer widens in tsc (let s = "a" is string, const s = "a" is "a") and did not here — only the composite half of that rule had ever been written. Invisible while no string source could be judged against a literal target, and five reports on legal lines the moment one could: zod's bg.ts has let invalid_adj = "Невалиден" reassigned five times.
  4. Two gates caught two wrong versions of that fix in turn. The corpus caught the first: TypeScript widens only a fresh literal type, so in let a: "foo" = "foo"; let b = a || "foo"; let c: "foo" = b — a conformance file — a's literal came from an annotation and neither it nor the || over it widens, and tsc accepts c. A test caught the second: a template literal with a constant substitution folds to a literal here (batch AU, for TS2367) while tsc calls it string, and a pre-existing parity pin requires a var holding one to behave exactly like the same string written plainly.
  5. A sixth false positive came out of probing the same neighbours, pre-existing and one line deep: widen_literal_deep's union arm mapped its members without deduping them, so ["a", "b"]'s widened element type was string | string — a message nobody can act on, and not equal to the base the abstention tests, so const arr = ["a", "b"] as const; const y: "a" = arr[0] reported on a line tsc accepts.

False positives on legal code the corpus cannot see

  • declare const c: (n: number) => number; const r: number = c?.(1) — x?.(args) has no receiver, so it took the "shape this cannot read" default.
  • A & B into A, and three siblings — the unrestricted intersection rule is the same +1 file with four FPs, because is_assignable_to is resolver-free.
  • An interface's overload set was invisible to method_overload_signatures, which had arms for a class, an Object type literal and a Struct — the three shapes that are not how a .d.ts declares overloads. With no candidates, two consumers of that one count went wrong at once: resolve_method_overload fell through to lookup_method_sig (first declaration only), and the argument check's > 1 abstention never fired. So interface O { m(x: string): number; m(x: number): string } with p.m(1) reported expected string but got number on a line tsc accepts. Confirmed pre-existing by stashing the branch and rebuilding HEAD; corpus-neutral when fixed.
  • u?.a[0] came back Any while u?.a.length resolved — the IndexAccess arm passed its receiver to infer_index raw, where PropAccess has pruned a nullable receiver and retried for a long time with its own comment saying why.
  • A computed key that is a string-literal const. const kk = "hello"; interface I { [kk]: number } names a member hello that neither member parser can evaluate — the value lives in another declaration — and reading the undecidable name as "no member called hello" reported i.hello on a line tsc accepts, twice. Also confirmed pre-existing by rebuilding HEAD.
  • A mutable binding's literal initializer never widened (batch FB bridge: emit simple typealias passthrough for concrete aliases #3), and a widened union was not deduped (checker/bridge: support InstanceType and ConstructorParameters from typeof classes #5) — six lines in zod and one in effect, all confirmed pre-existing against the baseline binary.

Batch FB's measurement method is new here and is the answer to a gate the corpus structurally cannot be: zod 118 → 113 diagnostics through mtsc --noEmit --bundle, and a sweep of mtsc check over 4,085 real files (every .d.ts under node_modules, plus effect's 362 .ts sources) diffed against the baseline binary — byte-identical, no diagnostic added.

Rejections, with their blockers

this in an object-literal function property was instrumented rather than argued about (the binding arrives as { n: number; f: () => any }; check_funcexpr_with_context then rebinds to Any, deliberately, and noImplicitThis is not plumbed). typeof x === "object" narrowing unknown needs two other narrowings first or it is two FPs in the file it would flip. a.m(1) on an optional method is TS2722 in tsc and stays a MISS. The class spelling of a late-bound key (class C { [k]: number }) is untouched. And batch FB's numeric / boolean / bigint half needs infer_expr to stop erasing those literals at the source, which is a change at every consumer of a numeric literal's type rather than a gate.

Why these three, when the corpus ranks them last

docs/checker-triage.md has a second axis the MISS count cannot see: how often each feature appears in 3,000 real .d.ts files and 2,697 real .ts sources. Optional chaining is 144 real occurrences against one conformance file; unique symbol is 183 against zero; a string assigned into a literal union is what every options-string API produces and has no corpus file at all. Reading the corpus count alone ranks all three last.

Gates run

moon check --deny-warn, moon check --deny-warn --target js, moon fmt --check, moon info, the full moon test --target native (3,074) plus every *_wbtest.mbt file individually after the last change (2,863; bench files excluded), checker_conformance_oracle.sh over all 4,484 files, verify-checker-scaling (12/12 axes within budget), verify-mangle-safety (186/186), verify-generated-fixtures, verify-examples, verify-scaffolds, verify-mbti-dts, verify-bridge-runtime (0 converter runtime failures), verify-bridge-enum-returns, verify-graph-walk, verify-dce-coverage (31 eliminated / 0 broken), verify-language-service (22/22), verify-language-service-types, verify-cli-node (21/21).

🤖 Generated with Claude Code

https://claude.ai/code/session_01TgePD6JZrRmq4VEYrS4LLk

TS2708 through an `import a = A` alias, TS2403 inside a function scope,
and the polymorphic `this` type. Plus an unbounded recursion that ends
the process, reachable at HEAD.

`importStatementsInterfaces` (TS2708) needed two things, and the
recorded blocker was only half of one. The note said whether an
`import a = A` alias binds a VALUE "depends on the TARGET, which is not
resolved at parse time" — true of what the target MEANS and false of
what it SPELLS, and the spelling is all the rule needs, since an alias
binds a value exactly when its target does. The parser records the whole
dotted target beside the `<import-eq-root>` marker it already wrote, and
the four exclusions then decide against the TARGET. Probed one cell at a
time: a type-only namespace target reports at top level, nested
(`A.inA`) and through a chained alias, while an INSTANTIATED target, an
enum, a class and a function stay silent — the last three because tsc
gives TS2702 / TS2503 / TS2693 there, codes this rule does not claim.

The second was `namespace_is_instantiated` counting a nested namespace's
own value entry. `parse_namespace_decl_with_mode` builds that value
before it has parsed the body, so `namespace A { export namespace inA {
export interface P {} } }` read as existing at runtime. It does not, and
tsc reports `typeof A` as TS2708 — which a test in the SILENT list said
in its own comment ("tsc reports TS2708 there"), the fourteenth test here
found asserting a gap rather than a behaviour. An EMPTY inner namespace
is the same answer, probed.

TS2403 is the applied-in-some-places family at a scope boundary: the
rule and its identity comparison ran over `module_.top_level_stmts` and
nowhere else, so `function f() { var b: number; var b: string }` was
silent, and so was every class method, constructor, arrow and function
expression. The comparison is extracted and shared rather than copied.
`var` is function-scoped, so a nested block, an `if` arm, a `for` head, a
`switch` case and a `try` block are one group while a nested function is
not — collected by a STATEMENT walk, which is complete by construction
because only a declaration statement can introduce a `var`. Parameters
participate, as they do in tsc, including `b?: number` against the
`number | undefined` our parser already builds; a DESTRUCTURED parameter
is excluded because `p.name` is only the pattern's first name while
`p.type_` is the whole pattern's type, so comparing it would report legal
code.

`typeOfThisGeneral` then falls out of one more fact: `this` in an
INSTANCE member is the polymorphic `this` type, not the enclosing class's
name. `identity_key` learns it (`this` is a reserved word, so no
declaration can claim the name), and `types_definitely_differ` gets the
`this`-against-a-named-type arm a class name needs, since
`structural_named_key` declines one. Probed in both directions and in
both rules that read it: tsc reports the var pair, and `interface D
extends A, B` with `A.m(): this` against `B.m(): A` is TS2320, while two
bases that BOTH say `this` are legal. A static method's `this` is
`typeof C` and a free function's is `any` — both real TS2403 sources,
both a MISS here rather than a guess.

The segfault is the batch's most useful finding and predates it.
`types_definitely_differ` recurses on types `unwrap` has already
resolved, with no depth bound, so a self-referential `typeof` closes the
loop: `witness.ts` crashed the binary the moment the function-scope sweep
fed it `function fnArg1(x: typeof fnArg1) { var x: (n: typeof fnArg1) =>
void }`. The module-level spelling could always reach it — measured by
stashing the branch, rebuilding HEAD and running it, not assumed — and no
corpus file had that shape at top level. Bounded at 16, matching
`identity_key`; running out of depth abstains, which is this function's
own fail direction.

TP 2665 -> 2667 | MISS in scope 50 -> 48 | FP 0 | PFLEGAL 0 | TN 1750.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TgePD6JZrRmq4VEYrS4LLk
Zero conformance files by design (TP 2667 / MISS in scope 48 / FP 0,
unchanged), and two things the corpus cannot score: a capability at the
shape real code writes, and a false positive on legal code.

A generic FUNCTION's type arguments have been inferred from the
arguments for as long as `infer_call` existed. A generic METHOD's never
were — `declare function f<T>(x: T): T; const q: string = f(12)`
reported while `interface I { m<T>(x: T): T }` with `i.m(12)` came back
carrying an unresolved `T`, which every downstream check reads as
unknowable. Probed one spelling at a time, and FOUR of them were
involved, each losing the binder its own way:

  - an interface records it in `method_type_params` and a class method in
    `TsClassMethodDecl.type_params`, both already there and read only by
    `scoped_bindings_for_method`, never at a call site;
  - a member written as a function TYPE (`m: <T>(x: T) => T`) carries it
    on the `GenericFunc` wrapper, and `lookup_method_sig` matched `Func`
    and `CallableMeta(Func)` and not that — the eighth fail-open
    wrapper-node arm in this repo's ledger, and it made the whole member
    opaque rather than merely un-instantiated;
  - `infer_call` had the same hole, so a generic callable arriving as a
    VALUE fell to the `Any` catch-all;
  - an inline object type's method signature threw the binders away in
    the parser, under a comment saying it did ("recorded nowhere on an
    anonymous object, so just skip them"). They ride the same
    `GenericFunc` wrapper the type parser already builds for the PROPERTY
    spelling of the same thing, so the two spellings stop disagreeing.

The solve is `solve_generic_bindings`, the function the `Call` arm
already uses, so a callee reached by NAME and one reached through a
member cannot answer differently.

A CONSTRUCT / CALL signature's binders are deliberately NOT carried, and
the reason is measured rather than argued: wrapping them cost a true
positive outright — `genericCallWithOverloadedConstructorTypedArguments2`
is all `new <T>(…)` members, and the wrapper is visible to every consumer
that matches the member's shape. Nothing reads those binders yet, which
is the same reason CLAUDE.md records for not walking them elsewhere.

The false positive is the optional-CALL form of a chain. Batch EH
removed the unconditional `| undefined` on a non-nullable receiver and
covered a property, an index and a method call; `x?.(args)` has no
receiver — what the `?.` guards is the CALLEE — so it took the "shape
this cannot read" default and was widened unconditionally. `declare
const c: (n: number) => number; const r: number = c?.(1)` is ACCEPTED by
tsc and was reported. No corpus file has the shape, which is why FP 0
never caught it; found by probing the legal neighbour of the generic
work above.

`infer_call`'s union arm also stopped treating a NULLISH member as a
reason to collapse the answer to `Any`: that member is not a callable
the union offers, it is why the call needs a `?.` at all, and invoking a
possibly-nullish value without one is TS2722 — a diagnostic of its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TgePD6JZrRmq4VEYrS4LLk
… type

Zero conformance files, again by design: this is one of the two BLIND
rows `UNSUPPORTED.md`'s capability probe found by asking the COMMON
SHAPE rather than the corpus, which contains neither. TP 2667 / MISS in
scope 48 / FP 0, unchanged.

`infer_index`'s `Named` / `Applied` arm has resolved an index-signature
read for a long time — a string key, a numeric key and a variable key
each falling back to the signature when no named field matches — and the
ANONYMOUS spelling had no arm at all, so `declare let m: { [k: string]:
V }; m["a"].nope` yielded no type and every check downstream abstained.
`collect_index_sigs` had the same hole one level down, so the signature
was invisible to every consumer of that list while the INTERFACE
spelling of one declaration was not. Both now read the member the parser
records keyed by its KEY TYPE, which is exactly what
`is_index_signature_key` recognises.

Two measurements worth keeping. The WRITE direction that `UNSUPPORTED.md`
lists beside it as BLIND is not: `m["x"] = "s"` against `{ [k: string]:
number }` already reported, in the anonymous spelling, the dotted
spelling, the numeric-key spelling and through an interface — so that
row is stale and the doc is corrected. And a MAPPED type is NOT reduced
to an index signature, so `{ [P in string]: D }` still yields nothing
where the `{ [k: string]: D }` spelling of the same type now resolves;
that is a declared MISS in the test rather than a claim, because the
first draft asserted it as a fire and the run said otherwise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TgePD6JZrRmq4VEYrS4LLk
…inite key set

`{ [P in string]: V }` IS `{ [k: string]: V }`, and so is the `keyof any`
spelling — TypeScript says so — but neither evaluator could ENUMERATE
that key set, so the type stayed an unresolved `MappedType`, read as
unmodelled, and every member check downstream abstained while the
`{ [k: string]: V }` spelling of the same declaration resolved. Reduced
in `unwrap`, next to the five computed-type forms it already reduces.

The guard is where the work is, and it cost TWO false positives on
TS7-accepted files before it was right. A HOMOMORPHIC mapped type over
`any` — `{ [P in keyof T]: Box<T[P]> }` at `T = any` — yields `any` and
not a shape, which is the whole subject of `mappedTypeWithAny`; rendering
it as an index signature produced `{ string: Box<any[string]> }` and
reported `mappedTypesArraysTuples` and `thisTypeInObjectLiterals2`. The
line TypeScript draws is whether the VALUE depends on the key, and the
first attempt at testing that was defeated by a shape rather than by an
argument: `type_references_any` walks the structural variants and has no
`IndexedAccess` arm, so `Box<T[P]>` reads through it as independent of
`P`. Answered by a separate predicate rather than by widening that walk,
whose other consumers ask a different question. `{ [P in string]: P }` is
the MISS the guard costs.

Two smaller facts fell out. The substituted value is REDUCED before it is
stored, because a homomorphic source leaves `T[P]` behind and `unwrap`
reduces `any[string]` to `any` only at the top level, never inside a type
ARGUMENT — the shape came out `PropDesc<any[string]>`. And only the
un-remapped form participates: an `as` clause can rename or drop keys, so
an infinite source says nothing about the result's shape.

TP 2667 -> 2668 | MISS in scope 48 -> 47 | FP 0 | PFLEGAL 0 | TN 1750.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TgePD6JZrRmq4VEYrS4LLk
…n indexer

`check_expr_against` carried `(Intersection(_), _) => return` under a
comment saying the modelling was "still too coarse structurally", and
the abstention was TOTAL: an intersection-typed value was invisible in
every assignment, against every target — `declare let s: {a: string} &
{b: number}; let t: string; t = s` reported nothing. That is exactly
what batch EN went looking for and could not find. It measured
`is_assignable_to_inner`'s source-intersection merge arm as unreachable
for an intersection-typed VALUE, reverted a fix that "provably changes
nothing", and wrote that finding what abstains first "is the actual
work". This is it.

The rule is restricted to an INDEX-SIGNATURE target, and the
restriction is a measurement rather than caution. Asking
`is_assignable_to(merged, target)` for ANY target is the same +1
conformance file and **four false positives on hand-written legal
code** — `A & B` into `A`, into `B`, an intersection carrying an index
signature into `A`, and a recursive alias into `A` — because that entry
point is resolver-free and cannot expand a `Named` target to compare
structurally, which is what the old comment's "too coarse" actually
meant. The corpus contains none of those shapes, so FP 0 stayed 0 with
the wrong rule in: the fourth time in this series that the legal
neighbour caught what the gate structurally could not.

An index-signature target needs no such expansion. An indexer is
constrained by EVERY property the source has, so the merged shape is the
exact question, and it is the case the corpus and real code both hit:
`tt = ss` with `ss: { a: string } & { b: number }` and `tt: { [k:
string]: string }` is TS2322 on `b`. A component that is not object-like
does not merge and still abstains; `interface A & interface B` into an
indexer is a MISS, since interface members get no implicit index
signature and that is a fact about the SOURCE this rule does not model.

TP 2668 -> 2669 | MISS in scope 47 -> 46 | FP 0 | PFLEGAL 0 | TN 1750.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TgePD6JZrRmq4VEYrS4LLk
No checker change. `TP 2669 / MISS in scope 46 / OUT OF SCOPE 19 / FP 0 /
PFLEGAL 0 / TN 1750`, and the five documents describing the backlog were
all four files behind it.

- `src/checker/UNSUPPORTED.md` §1 lists the 46 by machinery, with the four
  files batches EU–EY flipped removed from their sections
  (`importStatementsInterfaces`, `typeOfThisGeneral`, `mappedTypeWithAny`,
  `intersectionWithIndexSignatures`) and the per-section counts summing to
  46 again. §2 was RE-PROBED one file per row, and two rows were wrong in
  the direction that matters: the index-signature WRITE read BLIND for two
  revisions and had been handled in all four spellings the whole time, and
  the template-literal placeholder read BLIND while the error shape
  reports. A capability table nobody re-measures ranks the wrong work —
  the same defect, one level up, that retired `docs/checker-priority.md`.
  §3 gains four declared abstentions with their numbers, including `this`
  in an object-literal `function` property, which was instrumented rather
  than argued about and is filed with the two blockers that stopped it.
- `docs/checker-triage.md`'s family table is re-counted; `classes/this/
  mixins` is 7, `type-level machinery` 6, `name resolution` 3. The row
  that called `intersectionWithIndexSignatures` a recorded rejection now
  says what it turned out to be: batch EN had measured the deciding arm
  as unreachable and written that finding what abstains first "is the
  actual work", and it was an explicit early return with a stated reason.
- `justfile`'s `--max-miss` is tightened 50 -> 46, which is the direction
  that budget only ever moves, and the gate passes at it.
- `docs/mtsc.md`'s compatibility snapshot and `CLAUDE.md`'s headline
  numbers are re-measured; `CLAUDE.md`'s `src/checker` section records the
  batches, the segfault, the three false positives on legal code and the
  two rejections with their blockers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TgePD6JZrRmq4VEYrS4LLk
… methods

`TP 2669 -> 2670 | MISS in scope 46 -> 45 | OUT OF SCOPE 19 | FP 0 |
PFLEGAL 0 | TN 1750`

One conformance file (`callChain.3`) carrying FOUR independent defects,
plus a false positive on legal code the corpus structurally cannot see.
The file is filed in the triage under "object literals, contextual
typing, widening", which is not what any of the four is.

1. An optional chain short-circuits for the WHOLE chain, and the parser
   puts only the guarded link inside the `OptionalChain` node — `g?.p.q`
   is `PropAccess(OptionalChain(PropAccess(g, p)), q)` — so that outer
   `.q` pruned the nullish receiver and handed back a bare `number`.
   The propagation reads the chain node's own RESULT rather than
   re-deciding its guard. `is_nullable_type` is NOT that predicate: it
   answers "is this type ENTIRELY nullish", so `{ q: number } |
   undefined` reads false there and the first draft fired on nothing.

2. `m?<T>(x)` did not PARSE at either member parser. The grammar is
   `PropertyName ?opt CallSignature` and a CallSignature BEGINS with its
   type parameters, so the `?` comes first; both read the binders first.
   The object-type parser's failure was TOTAL — the whole literal fell
   back to `Any`, so every member became unknowable, not just the
   generic one.

3. An OPTIONAL method is stored as `Union([callable, Undefined])`, so
   every consumer matching the callable SHAPE read it as opaque and
   `a.m?.(1)` came back `Any`. `lookup_callable_field` removes the
   wrapper for the two callers asking "what CALLABLE is this member",
   and returns `None` unless exactly one non-nullish member remains, so
   an overload set still goes to the overload path.

4. `unwrap` PEELS a `GenericFunc` — it says so at the site — so a
   generic member of a union callee reached `infer_call` with its
   binders gone and `c?.m({ x: 12 })` came back with `T` unsolved.
   Generic members are instantiated only after every concrete one has
   failed, which is TypeScript's own overload order.

The false positive: `method_overload_signatures` had arms for a class,
an `Object` type literal and a `Struct` — the three shapes that are NOT
how a `.d.ts` declares an overload set. With no candidates,
`resolve_method_overload` fell through to `lookup_method_sig` (first
declaration only) AND the argument check's `> 1` abstention never fired,
so `interface O { m(x: string): number; m(x: number): string }` with
`p.m(1)` reported `expected string but got number` on a line tsc
accepts. Confirmed pre-existing by stashing the branch and rebuilding
HEAD; corpus-neutral when fixed.

One more found the same way: the IndexAccess arm passed its receiver to
`infer_index` raw, where the PropAccess arm has pruned a nullable
receiver and retried for a long time — `u?.a[0]` was `Any` while
`u?.a.length` resolved.

`--max-miss` lowered to 45. Gates: fmt, check --deny-warn (native and
js), moon info, every `*_wbtest.mbt` file (2,853 tests), the oracle,
verify-checker-scaling, verify-generated-fixtures, verify-scaffolds,
verify-examples, verify-mbti-dts, verify-bridge-runtime,
verify-bridge-enum-returns, verify-mangle-safety, verify-dce-coverage,
verify-graph-walk, verify-language-service, verify-cli-node.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TgePD6JZrRmq4VEYrS4LLk
`TP 2670 | MISS in scope 45 | OUT OF SCOPE 19 | FP 0 | PFLEGAL 0 |
TN 1750` — corpus-neutral by design.

`unique symbol` is 183 occurrences across 3,000 real `.d.ts` files, the
third commonest feature in `docs/checker-triage.md`'s real-code column,
and it was the top BLIND row of `UNSUPPORTED.md` §2 with no conformance
file anywhere. Three defects and two false positives on legal code.

1. The ANONYMOUS spelling did not PARSE.
   `try_parse_object_type_with_members`'s bracket arm falls back for
   anything that is not an index signature, so `{ [k]: number; plain:
   number }` came out `Any` and EVERY member of it was lost, not just
   the computed one — batch EZ's `m?<T>` shape again. That fall-back is
   load-bearing for a mapped type (`[K in keyof T]` reaches the same
   arm), so the fix is a positive shape test — exactly `[ Ident ]` —
   rather than a widened fallback.

2. The INTERFACE spelling named the member `<computed>`, a name no
   lookup can match, while the sibling WELL-KNOWN spelling twenty lines
   above has had a stable `@@name` for a long time. Both record
   `@@unique:<k>` now.

3. Nothing translated the index EXPRESSION back, so `i[k]` was `Any`
   even once the member had a name. Wired at BOTH `infer_index` arms,
   gated on the index's TYPE being a `unique symbol`.

The false positives are pre-existing and were found by probing a legal
neighbour: a computed key can also be a string-literal `const`
(`const kk = "hello"; interface I { [kk]: number }`), which tsc names
`hello` and neither member parser can evaluate. Reading the undecidable
name as "no member called `hello`" reported `i.hello` on a line tsc
accepts. Confirmed pre-existing by stashing and rebuilding HEAD — the
`<computed>` half predates the `@@unique:` name. The abstention sits in
`member_recv_unmodeled` beside the index-signature one it already makes
for the same reason; a well-known key is excluded because it IS decided
statically, measured (`w.nope` still reports).

One approximation, stated: tsc keys a late-bound member by SYMBOL, this
keys it by spelling, so two `unique symbol` bindings sharing a name
across scopes resolve to one member — a wrong member rather than a
report on legal code, since tsc calls that program TS2339.

Gates: fmt, check --deny-warn (native and js), moon info, every
`*_wbtest.mbt` file (2,854 tests), the oracle, verify-checker-scaling,
verify-generated-fixtures, verify-scaffolds, verify-examples,
verify-bridge-runtime, verify-bridge-enum-returns, verify-mangle-safety,
verify-dce-coverage, verify-graph-walk, verify-language-service,
verify-cli-node.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TgePD6JZrRmq4VEYrS4LLk
`UNSUPPORTED.md` §2 ranks work by what the checker HAS, and CLAUDE.md
records twice that a table nobody re-measures ranks the wrong work. A
table with MISSING rows is one level further out: `satisfies` (238) and
`as const` (108) are the two commonest features in the triage's
real-`.ts` column and neither had a row, nor did `infer` (156 in
`.d.ts`).

Probed, one file per row:

- `satisfies` — CAUGHT at all three cells (excess property, a member of
  the wrong type, and the narrowed type surviving the read).
- `infer` through a conditional alias — CAUGHT.
- `as const` on an OBJECT LITERAL — **BLIND**, and the cause is the
  non-obvious part, so it is filed in §3 rather than left as a label.
  `is_widening_direction_mismatch` is RIGHT to suppress it: that arm
  exists because our inference sometimes widens where tsc keeps a
  literal, so a report there can be about our gap rather than the
  program (batch EL kept it for exactly that). It fires here because
  `as const` does not keep its members' literal types — `a.k` comes out
  `string` where tsc has `"hello"`. Localized by measurement rather than
  by reading: the DECLARED spelling reports, so the member READ is not
  what loses it, and `a.nope` proves the member is `string` and not
  `Any`. The fix belongs in `as const`.
- `as const` elsewhere (`"hello" as const`, a tuple index, a declared
  literal member) — CAUGHT at the verdict, with the message naming
  `string` where tsc names `"hello"`: a single `Literal` widens and a
  UNION of literals does not.

No behaviour change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TgePD6JZrRmq4VEYrS4LLk
The row committed one commit earlier blamed `is_widening_direction_mismatch`
and was wrong twice over, which is the defect this repo's own notes warn
about: a stated reason that the code it names contradicts.

- Batch EL REMOVED that function's string arm. Only number, boolean,
  bigint and void remain, so it cannot be what suppresses a string
  source against a string-literal target.
- `as const` is not the discriminator either. The identical shape
  without it is equally silent, and the parser DROPS the const
  assertion outright (`parse_asserted_*` does not wrap with `As(...)`,
  by design, because the transform passes want the raw expression), so
  the checker never sees one at all.

What a six-cell matrix does say, measured:

  declare const d:  { k: "hello" }  ; d.k  against "other"  -> reports
  declare const d2: { k: string }   ; d2.k against "other"  -> SILENT
  const i1 = { k: "hello" }         ; i1.k against "other"  -> SILENT
  const i1 = { k: "hello" }         ; i1.k against number   -> reports
  declare const d3: { k: string }   ; d3.k against number   -> reports
  const i3: { k: string } = {...}   ; i3.k against "other"  -> SILENT

So it is the TARGET being a string literal that suppresses, whatever the
receiver's origin, and a LITERAL member is what makes the first cell
fire. `is_assignable_to(String_, Literal("foo"))` is correctly `false`
(asserted in `assignability_wbtest.mbt`) and the `(String_, Literal(_))`
arm at `expr_check.mbt:7950` is an OVERLAP predicate for `==`, correct
as written — so the suppression is a third site, the
applied-in-some-places family again. Filed with the matrix and NOT a
diagnosis; worth taking with the corpus as the gate, since a
`string`-to-literal report is the commonest real TS2322 there is.

No behaviour change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TgePD6JZrRmq4VEYrS4LLk
The capability probe's `as const` row turned out to be a corner of a
much larger gap, and instrumenting is what found it after three
candidate causes were each refuted by reading the code they named.

Seven cells, all probed against tsc:

  declare const s: string;
  const a1: "other" = s;                      // TS2322 — SILENT
  let a2: "other"; a2 = s;                    // TS2322 — SILENT
  const a3: "a" | "b" = s;                    // TS2322 — SILENT
  f(s) where f(x: "a" | "b")                  // TS2345 — SILENT
  type Mode = "r" | "w"; const a4: Mode = m;  // TS2322 — SILENT
  declare const n: number; const a6: 1 = n;   // TS2322 — SILENT
  const a5: "other" = "lit";                  // TS2322 — reports

So a PRIMITIVE source against a LITERAL or literal-union target is
accepted, at the binding, the assignment and the call argument, for
strings and numbers alike; only a syntactically literal source reports.
This is the commonest real TS2322 there is — every options-string and
discriminated-union API produces it.

Refuted along the way, each by reading the named code:
`is_widening_direction_mismatch` lost its string arm in batch EL;
`is_assignable_to(String_, Literal("foo"))` is correctly `false`
(asserted in `assignability_wbtest.mbt`); the `(String_, Literal(_))`
arm at `expr_check.mbt:7950` is an OVERLAP predicate for `==`, right as
written; and `as const` is not the discriminator (the parser drops the
assertion outright, and the identical shape without it is equally
silent).

Instrumenting settled it in one run: `check_expr_against` receives
exactly `src=String_ exp=Literal(other)` and emits nothing, and the
STRICT entry point reports the same three issues as the permissive one —
so the mismatch is never DECIDED and no filter is involved.

Filed rather than fixed, with the blocker named. Our inference widens a
literal to `string` where tsc keeps it, so reporting every `string` ->
literal would be about OUR widening rather than the program — the fact a
sound rule needs is "this type came from a WRITTEN annotation", the same
absent-versus-`: any` channel recorded for TS7031, TS7022, TS2729,
TS2448, TS2564 and TS2490. That is a batch with the corpus as its gate,
not a row edit.

No behaviour change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TgePD6JZrRmq4VEYrS4LLk
@mizchi
mizchi marked this pull request as ready for review September 20, 2026 04:17
@mizchi
mizchi merged commit a1eafc8 into main Sep 20, 2026
2 checks passed
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.

2 participants