From 5a4fc775e3f392735054993d3c41f01b5446ba53 Mon Sep 17 00:00:00 2001
From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com>
Date: Sat, 10 Oct 2026 14:36:22 -0700
Subject: [PATCH 01/11] csharp: a relative qualified name is read from the
site's namespace, not the file's
`Models.Report` written inside namespace P, in a file that also declares
P.Other.Models and Z.Models, bound to all three Report types: the relative
candidate (type_qn_tail + module_ns_chain) is keyed per FILE, so every
namespace the file declares a type in qualified every name in it. C# reads the
first segment from the namespaces enclosing the site, innermost outward, and
the innermost level with a match wins.
- type_name_out_of_scope(ns, name, arity, group): per site namespace and
dotted written name, candidates are ranked by the prefix they were found
under; only the longest survives, and a full match survives only when no
enclosing namespace matched. Demand-driven from names a file writes.
- type_ref_resolves, ref_names_type, expr_new_type and heritage_resolves drop
what the site's enclosing type's namespace rules out. Prune-only: the
file-level answer is unchanged and a site with no known enclosing type drops
nothing.
Case a-relative-name-is-read-from-the-sites-namespace fails 5/6 on the base
(the single-namespace control passes on both) and passes 6/6.
Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
---
.../engine/expression-resolution/expr-type.dl | 3 +-
.../csharp/engine/resolution/local-binding.dl | 3 +-
.../engine/resolution/type-hierarchy.dl | 1 +
.../engine/resolution/type-resolution.dl | 104 +++++++++++++++++-
graph/csharp/souffle/decls_all.dl | 14 +++
.../case.json | 22 ++++
.../src/Mixed.cs | 28 +++++
.../src/Report.csproj | 1 +
.../src/Single.cs | 8 ++
9 files changed, 181 insertions(+), 3 deletions(-)
create mode 100644 tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/case.json
create mode 100644 tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/src/Mixed.cs
create mode 100644 tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/src/Report.csproj
create mode 100644 tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/src/Single.cs
diff --git a/graph/csharp/engine/expression-resolution/expr-type.dl b/graph/csharp/engine/expression-resolution/expr-type.dl
index 72c20b462..2258c0e0b 100644
--- a/graph/csharp/engine/expression-resolution/expr-type.dl
+++ b/graph/csharp/engine/expression-resolution/expr-type.dl
@@ -281,7 +281,8 @@ expr_new_type(prov, e, gk) :-
!expr_new_type_ref(prov, e, _),
call_callee_name(prov, e, n), expr_ultimate_module(prov, e, mod),
call_type_argc(prov, e, ar),
- type_name_resolves(prov, mod, n, ar, gk).
+ type_name_resolves(prov, mod, n, ar, gk),
+ !expr_callee_name_dropped(prov, e, gk).
// AN INVOCATION has its callee's return type.
expr_type(prov, e, gk) :-
diff --git a/graph/csharp/engine/resolution/local-binding.dl b/graph/csharp/engine/resolution/local-binding.dl
index e7bc391fa..2c24e832a 100644
--- a/graph/csharp/engine/resolution/local-binding.dl
+++ b/graph/csharp/engine/resolution/local-binding.dl
@@ -239,7 +239,8 @@ ref_names_type(prov, e, gk) :-
expr_written_name(prov, e, n), expr_type_argc(prov, e, ar),
expr_ultimate_module(prov, e, mod),
!ref_binds_value(prov, e),
- type_name_resolves(prov, mod, n, ar, gk).
+ type_name_resolves(prov, mod, n, ar, gk),
+ !expr_name_dropped(prov, e, gk).
// ── IMPLICIT `this` ─────────────────────────────────────────────────────────
// An unqualified instance member reference has an implicit `this` receiver whose
diff --git a/graph/csharp/engine/resolution/type-hierarchy.dl b/graph/csharp/engine/resolution/type-hierarchy.dl
index 7c285be12..b329d00dd 100644
--- a/graph/csharp/engine/resolution/type-hierarchy.dl
+++ b/graph/csharp/engine/resolution/type-hierarchy.dl
@@ -73,6 +73,7 @@ heritage_resolves(prov, gk, pos, bgk) :-
heritage_of_entity(prov, gk, pos, bn, ba, ""),
type_group(prov, t, gk), type_module(prov, t, mod),
type_name_resolves(prov, mod, bn, ba, bgk),
+ !heritage_name_dropped(prov, t, bn, bgk),
bgk != gk.
// ── THE SPLIT ───────────────────────────────────────────────────────────────
diff --git a/graph/csharp/engine/resolution/type-resolution.dl b/graph/csharp/engine/resolution/type-resolution.dl
index f4d1b8031..75492a1d5 100644
--- a/graph/csharp/engine/resolution/type-resolution.dl
+++ b/graph/csharp/engine/resolution/type-resolution.dl
@@ -109,6 +109,107 @@ type_name_cand(prov, mod, s, ar, gk, 1) :-
type_qn_tail(prov, qn, p, s), module_ns_chain(prov, mod, p),
type_decl(prov, _, qn, ar, _, t), type_group(prov, t, gk).
+// ── ...FROM THE SITE'S NAMESPACE, NOT THE FILE'S ────────────────────────────
+// The candidate above is per FILE: module_ns_chain holds every namespace the file
+// declares a type in. A file declaring P.Models, P.Other.Models and a class in P
+// therefore offered `Models.Report` written in P both P.Models.Report and
+// P.Other.Models.Report -- and P.Other is a child of P, not in scope there.
+//
+// C# reads the first segment from the namespaces enclosing the SITE, innermost
+// outward, and the innermost level that has a match wins: it does not merge
+// levels, and a name found at a level hides the same name read in full from the
+// global namespace. So per (site namespace, written name), the candidates are
+// ranked by the length of the prefix they were found under and only the longest
+// survives; a full match is the outermost level and survives only when no
+// enclosing namespace matched.
+//
+// PRUNE-ONLY. The file-level answer is kept as it is and a site drops from it only
+// what its own namespace rules out; the site's own answer is always one of the
+// file-level ones, so nothing correct is lost. A site whose enclosing type is not
+// known drops nothing.
+type_name_rel_at(prov, n, ar, gk, p) :-
+ type_qn_tail(prov, qn, p, n),
+ type_decl(prov, _, qn, ar, _, t), type_group(prov, t, gk).
+type_name_rel_src(prov, n, ar, gk) :- type_name_rel_at(prov, n, ar, gk, _).
+
+// The namespaces a name written in `ns` is read from, with the length of each.
+ns_read_level(prov, ns, ns, l) :- ns_exists(prov, ns), l = strlen(ns).
+ns_read_level(prov, ns, p, l) :- ns_prefix(prov, ns, p), l = strlen(p).
+
+// A dotted name some site in this file actually writes. type_name_resolves answers
+// every tail of every type in every file whose chain holds its prefix, written or
+// not, so the scoping below is keyed on what is written. Keyed by FILE as well, so
+// the probe into type_name_resolves binds (module, name) and reuses the index its
+// consumers already need rather than building one on the name alone.
+type_name_written_dotted(prov, mod, n) :-
+ type_ref_name(prov, tr, n), contains(".", n), type_ref_module(prov, tr, mod).
+type_name_written_dotted(prov, mod, n) :-
+ expr_written_name(prov, e, n), contains(".", n), expr_ultimate_module(prov, e, mod).
+type_name_written_dotted(prov, mod, n) :-
+ call_callee_name(prov, e, n), contains(".", n), call_module(prov, e, mod).
+type_name_written_dotted(prov, mod, n) :-
+ heritage_slot(prov, t, _, n, _, ""), contains(".", n), type_module(prov, t, mod).
+
+type_name_scope_demand(prov, mod, ns, n, ar) :-
+ type_name_written_dotted(prov, mod, n),
+ type_name_resolves(prov, mod, n, ar, _),
+ module_namespace_own(prov, mod, ns).
+type_name_scope_level(prov, ns, n, ar, gk, l) :-
+ type_name_scope_demand(prov, _, ns, n, ar),
+ ns_read_level(prov, ns, p, l),
+ type_name_rel_at(prov, n, ar, gk, p).
+type_name_scope_best(prov, ns, n, ar, b) :-
+ type_name_scope_level(prov, ns, n, ar, _, _),
+ b = max l : { type_name_scope_level(prov, ns, n, ar, _, l) }.
+type_name_scope_keep(prov, ns, n, ar, gk) :-
+ type_name_scope_level(prov, ns, n, ar, gk, b),
+ type_name_scope_best(prov, ns, n, ar, b).
+
+// type_name_out_of_scope(Prov, Namespace, Name, Arity, GroupKey) -- a file-level
+// answer that a site in `ns` does not see: a relative candidate not at the
+// innermost matching level (a child or sibling namespace, or a shallower level),
+// or a full match hidden by a relative one.
+type_name_out_of_scope(prov, ns, n, ar, gk) :-
+ type_name_scope_demand(prov, mod, ns, n, ar),
+ type_name_resolves(prov, mod, n, ar, gk),
+ type_name_rel_src(prov, n, ar, gk),
+ !type_name_scope_keep(prov, ns, n, ar, gk).
+type_name_out_of_scope(prov, ns, n, ar, gk) :-
+ type_name_scope_demand(prov, mod, ns, n, ar),
+ type_name_resolves(prov, mod, n, ar, gk),
+ !type_name_rel_src(prov, n, ar, gk),
+ type_name_scope_best(prov, ns, n, ar, _).
+
+// The site side, one per consumer below: the written name, the site's enclosing
+// type, and that type's namespace. Joined from the (small) out-of-scope set, so only
+// sites that write a pruned name are visited.
+type_ref_scope_type(prov, tr, t) :- type_ref_owner(prov, tr, o, "TYPE"), t = o.
+type_ref_scope_type(prov, tr, t) :- type_ref_owner(prov, tr, o, "METHOD"), method_owner(prov, t, o).
+type_ref_scope_type(prov, tr, t) :- type_ref_owner(prov, tr, o, "FIELD"), field_decl(prov, _, t, o).
+type_ref_scope_type(prov, tr, t) :- type_ref_owner(prov, tr, o, "PROPERTY"), property_decl(prov, _, t, o).
+type_ref_scope_type(prov, tr, t) :- type_ref_owner(prov, tr, o, "EVENT"), event_decl(prov, _, t, o).
+type_ref_scope_type(prov, tr, t) :-
+ type_ref_owner(prov, tr, o, "METHOD_PARAMETER"), param_of(prov, o, m), method_owner(prov, t, m).
+type_ref_scope_type(prov, tr, t) :-
+ type_ref_owner(prov, tr, o, "VARIABLE"), var_method(prov, o, m), method_owner(prov, t, m).
+type_ref_scope_type(prov, tr, t) :- type_ref_owner(prov, tr, o, "EXPRESSION"), expr_ultimate_type(prov, o, t).
+
+type_ref_name_dropped(prov, tr, gk) :-
+ type_name_out_of_scope(prov, ns, n, ar, gk),
+ type_ref_name(prov, tr, n), type_ref_argc(prov, tr, ar),
+ type_ref_scope_type(prov, tr, t), type_namespace(prov, t, ns).
+expr_name_dropped(prov, e, gk) :-
+ type_name_out_of_scope(prov, ns, n, ar, gk),
+ expr_written_name(prov, e, n), expr_type_argc(prov, e, ar),
+ expr_ultimate_type(prov, e, t), type_namespace(prov, t, ns).
+expr_callee_name_dropped(prov, e, gk) :-
+ type_name_out_of_scope(prov, ns, n, ar, gk),
+ call_callee_name(prov, e, n), call_type_argc(prov, e, ar),
+ expr_ultimate_type(prov, e, t), type_namespace(prov, t, ns).
+heritage_name_dropped(prov, t, n, gk) :-
+ type_name_out_of_scope(prov, ns, n, ar, gk),
+ heritage_slot(prov, t, _, n, ar, ""), type_namespace(prov, t, ns).
+
// ── A CLIENT FILE NAMING A STAGED LIBRARY TYPE ──────────────────────────────
// Every clause above is SINGLE-PROVENANCE: it joins a module and a declaration
// under one `prov`, so a client module and a library namespace are never brought
@@ -266,7 +367,8 @@ type_ref_resolves(prov, tr, gk) :-
!type_ref_type_param(prov, tr, _),
type_ref_argc(prov, tr, ar),
type_ref_module(prov, tr, mod),
- type_name_resolves(prov, mod, n, ar, gk).
+ type_name_resolves(prov, mod, n, ar, gk),
+ !type_ref_name_dropped(prov, tr, gk).
// The module a type reference is written in, by owner kind. Every owner kind that
// can carry a reference is listed; one that is missing makes every reference in that
diff --git a/graph/csharp/souffle/decls_all.dl b/graph/csharp/souffle/decls_all.dl
index 882cc9b7d..182506dd0 100644
--- a/graph/csharp/souffle/decls_all.dl
+++ b/graph/csharp/souffle/decls_all.dl
@@ -337,6 +337,7 @@
.decl expr_argument(c0:symbol,c1:symbol,c2:symbol,c3:symbol)
.decl expr_arity(c0:symbol,c1:symbol,c2:symbol,c3:symbol)
.decl expr_call_candidate(c0:symbol,c1:symbol,c2:symbol)
+.decl expr_callee_name_dropped(c0:symbol,c1:symbol,c2:symbol)
.decl expr_cast_ref(c0:symbol,c1:symbol,c2:symbol)
.decl expr_child(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol)
.decl expr_dynamic_typed(c0:symbol,c1:symbol)
@@ -360,6 +361,7 @@
.decl expr_method_name_child(c0:symbol,c1:symbol,c2:symbol)
.decl expr_method_ref(c0:symbol,c1:symbol,c2:symbol)
.decl expr_module(c0:symbol,c1:symbol,c2:symbol)
+.decl expr_name_dropped(c0:symbol,c1:symbol,c2:symbol)
.decl expr_new_type(c0:symbol,c1:symbol,c2:symbol)
.decl expr_new_type_ref(c0:symbol,c1:symbol,c2:symbol)
.decl expr_node(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol)
@@ -481,6 +483,7 @@
.decl grpc_static_base(c0:symbol,c1:symbol,c2:symbol)
.decl grpc_written_base(c0:symbol,c1:symbol)
.decl heritage_has_ctor_args(c0:symbol,c1:symbol,c2:symbol)
+.decl heritage_name_dropped(c0:symbol,c1:symbol,c2:symbol,c3:symbol)
.decl heritage_of_entity(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol)
.decl heritage_resolves(c0:symbol,c1:symbol,c2:symbol,c3:symbol)
.decl heritage_slot(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol)
@@ -745,6 +748,7 @@
.decl ns_encloses(c0:symbol,c1:symbol,c2:symbol)
.decl ns_exists(c0:symbol,c1:symbol)
.decl ns_prefix(c0:symbol,c1:symbol,c2:symbol)
+.decl ns_read_level(c0:symbol,c1:symbol,c2:symbol,c3:number)
.decl override_cand(c0:symbol,c1:symbol,c2:symbol,c3:number)
.decl override_depth(c0:symbol,c1:symbol,c2:symbol,c3:number)
.decl override_pair(c0:symbol,c1:symbol)
@@ -980,10 +984,18 @@
.decl type_name_best_rank(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:number)
.decl type_name_cand(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:number)
.decl type_name_count(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:number)
+.decl type_name_out_of_scope(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol)
.decl type_name_predefined_unstaged(c0:symbol,c1:symbol,c2:symbol)
+.decl type_name_rel_at(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol)
+.decl type_name_rel_src(c0:symbol,c1:symbol,c2:symbol,c3:symbol)
.decl type_name_resolves(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol)
+.decl type_name_scope_best(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:number)
+.decl type_name_scope_demand(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol)
+.decl type_name_scope_keep(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol)
+.decl type_name_scope_level(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:number)
.decl type_name_unresolved(c0:symbol,c1:symbol,c2:symbol,c3:symbol)
.decl type_name_visible(c0:symbol,c1:symbol,c2:symbol)
+.decl type_name_written_dotted(c0:symbol,c1:symbol,c2:symbol)
.decl type_namespace(c0:symbol,c1:symbol,c2:symbol)
.decl type_nest_ancestor(c0:symbol,c1:symbol,c2:symbol)
.decl type_ns(c0:symbol,c1:symbol,c2:symbol)
@@ -1009,11 +1021,13 @@
.decl type_ref_is_tuple(c0:symbol,c1:symbol,c2:symbol)
.decl type_ref_module(c0:symbol,c1:symbol,c2:symbol)
.decl type_ref_name(c0:symbol,c1:symbol,c2:symbol)
+.decl type_ref_name_dropped(c0:symbol,c1:symbol,c2:symbol)
.decl type_ref_nullable(c0:symbol,c1:symbol)
.decl type_ref_owner(c0:symbol,c1:symbol,c2:symbol,c3:symbol)
.decl type_ref_parent(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol)
.decl type_ref_resolves(c0:symbol,c1:symbol,c2:symbol)
.decl type_ref_root_position(c0:symbol,c1:symbol,c2:symbol)
+.decl type_ref_scope_type(c0:symbol,c1:symbol,c2:symbol)
.decl type_ref_type_param(c0:symbol,c1:symbol,c2:symbol)
.decl type_satisfies(c0:symbol,c1:symbol,c2:symbol)
.decl type_scope_key(c0:symbol,c1:symbol,c2:symbol)
diff --git a/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/case.json b/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/case.json
new file mode 100644
index 000000000..9e5afadbd
--- /dev/null
+++ b/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/case.json
@@ -0,0 +1,22 @@
+{"lang": "csharp", "src": "src",
+ "checks": [
+ {"why": "inside namespace P, Models.Report is P.Models.Report",
+ "run": ["impact", "P.Models.Report.Make"],
+ "want": ["[resolved] Calls.Partially"]},
+ {"why": "control: a file that declares only namespace P keeps its one correct target",
+ "run": ["impact", "P.Models.Report.Make"],
+ "want": ["[resolved] Alone.Only"]},
+ {"why": "a child namespace of the site's is not in scope: P.Other.Models.Report is not named from P, even when the same file declares P.Other",
+ "run": ["impact", "P.Other.Models.Report.Make"],
+ "avoid": ["[resolved] Calls.Partially", "[one of a set] Calls.Partially", "Alone.Only"]},
+ {"why": "inside P.Other, Models is P.Other.Models: the innermost namespace with a match wins, for a nested type too",
+ "run": ["impact", "P.Other.Models.Report.Make"],
+ "want": ["[resolved] Deeper.Nearest", "[resolved] Deeper.Inner.Nested"]},
+ {"why": "...and the outer P.Models.Report is not also named from P.Other",
+ "run": ["impact", "P.Models.Report.Make"],
+ "avoid": ["[resolved] Deeper.Nearest", "[one of a set] Deeper.Nearest", "[resolved] Deeper.Inner.Nested", "[one of a set] Deeper.Inner.Nested"]},
+ {"why": "control: an unrelated namespace declared in the same file is never read",
+ "run": ["impact", "Z.Models.Report.Make"],
+ "avoid": ["[resolved] Calls.Partially", "[one of a set] Calls.Partially", "Alone.Only",
+ "[resolved] Deeper.Nearest", "[one of a set] Deeper.Nearest",
+ "[resolved] Deeper.Inner.Nested", "[one of a set] Deeper.Inner.Nested"]}]}
diff --git a/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/src/Mixed.cs b/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/src/Mixed.cs
new file mode 100644
index 000000000..0b4cf7534
--- /dev/null
+++ b/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/src/Mixed.cs
@@ -0,0 +1,28 @@
+// one file declaring several namespaces: the namespaces a name is read from are the SITE's, not the file's
+namespace P.Models { public sealed class Report { public static Report Make() { return new Report(); } } }
+namespace P.Other.Models { public sealed class Report { public static Report Make() { return new Report(); } } }
+namespace Z.Models { public sealed class Report { public static Report Make() { return new Report(); } } }
+
+namespace P
+{
+ public sealed class Calls
+ {
+ // inside P, Models is P.Models; P.Other is a child of P and not in scope here
+ public Models.Report Partially() => Models.Report.Make();
+ }
+}
+
+namespace P.Other
+{
+ public sealed class Deeper
+ {
+ // inside P.Other, Models is P.Other.Models: the innermost namespace with a match wins
+ public Models.Report Nearest() => Models.Report.Make();
+
+ // a nested type reads from the same namespace as the type around it
+ public sealed class Inner
+ {
+ public Models.Report Nested() => Models.Report.Make();
+ }
+ }
+}
diff --git a/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/src/Report.csproj b/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/src/Report.csproj
new file mode 100644
index 000000000..d3b2f0306
--- /dev/null
+++ b/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/src/Report.csproj
@@ -0,0 +1 @@
+net8.0
diff --git a/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/src/Single.cs b/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/src/Single.cs
new file mode 100644
index 000000000..55e77d379
--- /dev/null
+++ b/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/src/Single.cs
@@ -0,0 +1,8 @@
+// control: a file that declares only namespace P keeps its one correct target
+namespace P
+{
+ public sealed class Alone
+ {
+ public Models.Report Only() => Models.Report.Make();
+ }
+}
From c2ab50ce98efb2e0ce37939599ccee196bad382f Mon Sep 17 00:00:00 2001
From: Swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com>
Date: Thu, 29 Oct 2026 01:54:50 -0700
Subject: [PATCH 02/11] typescript: construct, namespace, inline-chain,
ThisType and bound-method callees resolve (#1997, #1998, #1999, #2000, #2001)
Five callee shapes resolved to nothing though the compiler names their
target:
- new C() where C is typed by a construct-signature literal or an alias
of one: construction read expr_type only, the plain-call side already
read expr_shape (#1997)
- ns.f() where f is an exported const of a function type whose value is
not a function literal: the namespace read gave var_type, never
var_shape (#1998)
- a.b.c.m() through nested inline object types: a shape-typed member
read off a shape qualifier had no field_shape clause (#1999)
- this.m() in an object literal typed X & ThisType: the marker is
read through intersections and aliases, with the alias parameter and
the call's explicit type argument substituted (#2000)
- this.m.bind/call/apply and the bound result's call: the Function
members attach to a method read as a value (#2001)
Two guards keep sites that were right from hedging: the namespace shape
is not added where the const holds a function literal, and the
construct shape is not added where the callee variable holds a class.
Cases 91 to 95 pin each shape with its controls. Goldens moved in 08,
80 and 83 (see the pull request); 83's known-missing edge now resolves.
---
.../cases/91-construct-through-shape/src/x.ts | 16 ++++
.../92-namespace-typed-const/src/scorers.ts | 9 +++
.../cases/92-namespace-typed-const/src/x.ts | 9 +++
.../cases/93-literal-member-chain/src/x.ts | 17 ++++
.../cases/94-thistype-object-literal/src/x.ts | 31 ++++++++
.../95-bind-on-method-value/src/globals.d.ts | 66 ++++++++++++++++
.../95-bind-on-method-value/src/tsconfig.json | 6 ++
.../cases/95-bind-on-method-value/src/x.ts | 21 +++++
.../08-function-values-and-callbacks.edges | 3 +-
.../08-function-values-and-callbacks.envelope | 1 -
...08-function-values-and-callbacks.lib.edges | 3 +-
...function-values-and-callbacks.lib.envelope | 1 -
...8-function-values-and-callbacks.lib.oracle | 3 +-
.../08-function-values-and-callbacks.oracle | 3 +-
.../80-method-passed-by-reference.edges | 6 +-
.../80-method-passed-by-reference.oracle | 5 +-
.../expected/83-closed-world-dispatch.edges | 2 +-
.../83-closed-world-dispatch.known-missing | 7 --
.../expected/83-closed-world-dispatch.oracle | 3 +-
.../expected/91-construct-through-shape.edges | 5 ++
.../91-construct-through-shape.entries | 6 ++
.../91-construct-through-shape.fields | 0
.../91-construct-through-shape.fields-oracle | 7 ++
.../91-construct-through-shape.oracle | 1 +
.../91-construct-through-shape.type-use | 10 +++
.../91-construct-through-shape.types-oracle | 8 ++
.../expected/92-namespace-typed-const.edges | 5 ++
.../expected/92-namespace-typed-const.entries | 6 ++
.../92-namespace-typed-const.envelope | 4 +
.../expected/92-namespace-typed-const.fields | 4 +
.../92-namespace-typed-const.fields-oracle | 7 ++
.../92-namespace-typed-const.known-missing | 7 ++
.../expected/92-namespace-typed-const.oracle | 3 +
.../92-namespace-typed-const.type-use | 2 +
.../92-namespace-typed-const.types-oracle | 7 ++
.../expected/93-literal-member-chain.edges | 4 +
.../expected/93-literal-member-chain.entries | 5 ++
.../expected/93-literal-member-chain.fields | 10 +++
.../93-literal-member-chain.fields-oracle | 18 +++++
.../expected/93-literal-member-chain.oracle | 1 +
.../expected/93-literal-member-chain.type-use | 8 ++
.../93-literal-member-chain.types-oracle | 7 ++
.../expected/94-thistype-object-literal.edges | 8 ++
.../94-thistype-object-literal.entries | 2 +
.../94-thistype-object-literal.envelope | 2 +
.../94-thistype-object-literal.fields | 0
.../94-thistype-object-literal.fields-oracle | 7 ++
.../94-thistype-object-literal.known-missing | 6 ++
.../94-thistype-object-literal.oracle | 2 +
.../94-thistype-object-literal.type-use | 10 +++
.../94-thistype-object-literal.types-oracle | 7 ++
.../expected/95-bind-on-method-value.edges | 9 +++
.../expected/95-bind-on-method-value.entries | 4 +
.../expected/95-bind-on-method-value.envelope | 5 ++
.../expected/95-bind-on-method-value.fields | 1 +
.../95-bind-on-method-value.fields-oracle | 7 ++
.../expected/95-bind-on-method-value.oracle | 2 +
.../expected/95-bind-on-method-value.type-use | 3 +
.../95-bind-on-method-value.types-oracle | 7 ++
.../callee-resolution.dl | 54 +++++++++++++
.../engine/expression-resolution/expr-type.dl | 78 +++++++++++++++++++
graph/typescript/souffle/decls_all.dl | 4 +
62 files changed, 545 insertions(+), 20 deletions(-)
create mode 100644 graph/test/typescript/cases/91-construct-through-shape/src/x.ts
create mode 100644 graph/test/typescript/cases/92-namespace-typed-const/src/scorers.ts
create mode 100644 graph/test/typescript/cases/92-namespace-typed-const/src/x.ts
create mode 100644 graph/test/typescript/cases/93-literal-member-chain/src/x.ts
create mode 100644 graph/test/typescript/cases/94-thistype-object-literal/src/x.ts
create mode 100644 graph/test/typescript/cases/95-bind-on-method-value/src/globals.d.ts
create mode 100644 graph/test/typescript/cases/95-bind-on-method-value/src/tsconfig.json
create mode 100644 graph/test/typescript/cases/95-bind-on-method-value/src/x.ts
delete mode 100644 graph/test/typescript/expected/83-closed-world-dispatch.known-missing
create mode 100644 graph/test/typescript/expected/91-construct-through-shape.edges
create mode 100644 graph/test/typescript/expected/91-construct-through-shape.entries
create mode 100644 graph/test/typescript/expected/91-construct-through-shape.fields
create mode 100644 graph/test/typescript/expected/91-construct-through-shape.fields-oracle
create mode 100644 graph/test/typescript/expected/91-construct-through-shape.oracle
create mode 100644 graph/test/typescript/expected/91-construct-through-shape.type-use
create mode 100644 graph/test/typescript/expected/91-construct-through-shape.types-oracle
create mode 100644 graph/test/typescript/expected/92-namespace-typed-const.edges
create mode 100644 graph/test/typescript/expected/92-namespace-typed-const.entries
create mode 100644 graph/test/typescript/expected/92-namespace-typed-const.envelope
create mode 100644 graph/test/typescript/expected/92-namespace-typed-const.fields
create mode 100644 graph/test/typescript/expected/92-namespace-typed-const.fields-oracle
create mode 100644 graph/test/typescript/expected/92-namespace-typed-const.known-missing
create mode 100644 graph/test/typescript/expected/92-namespace-typed-const.oracle
create mode 100644 graph/test/typescript/expected/92-namespace-typed-const.type-use
create mode 100644 graph/test/typescript/expected/92-namespace-typed-const.types-oracle
create mode 100644 graph/test/typescript/expected/93-literal-member-chain.edges
create mode 100644 graph/test/typescript/expected/93-literal-member-chain.entries
create mode 100644 graph/test/typescript/expected/93-literal-member-chain.fields
create mode 100644 graph/test/typescript/expected/93-literal-member-chain.fields-oracle
create mode 100644 graph/test/typescript/expected/93-literal-member-chain.oracle
create mode 100644 graph/test/typescript/expected/93-literal-member-chain.type-use
create mode 100644 graph/test/typescript/expected/93-literal-member-chain.types-oracle
create mode 100644 graph/test/typescript/expected/94-thistype-object-literal.edges
create mode 100644 graph/test/typescript/expected/94-thistype-object-literal.entries
create mode 100644 graph/test/typescript/expected/94-thistype-object-literal.envelope
create mode 100644 graph/test/typescript/expected/94-thistype-object-literal.fields
create mode 100644 graph/test/typescript/expected/94-thistype-object-literal.fields-oracle
create mode 100644 graph/test/typescript/expected/94-thistype-object-literal.known-missing
create mode 100644 graph/test/typescript/expected/94-thistype-object-literal.oracle
create mode 100644 graph/test/typescript/expected/94-thistype-object-literal.type-use
create mode 100644 graph/test/typescript/expected/94-thistype-object-literal.types-oracle
create mode 100644 graph/test/typescript/expected/95-bind-on-method-value.edges
create mode 100644 graph/test/typescript/expected/95-bind-on-method-value.entries
create mode 100644 graph/test/typescript/expected/95-bind-on-method-value.envelope
create mode 100644 graph/test/typescript/expected/95-bind-on-method-value.fields
create mode 100644 graph/test/typescript/expected/95-bind-on-method-value.fields-oracle
create mode 100644 graph/test/typescript/expected/95-bind-on-method-value.oracle
create mode 100644 graph/test/typescript/expected/95-bind-on-method-value.type-use
create mode 100644 graph/test/typescript/expected/95-bind-on-method-value.types-oracle
diff --git a/graph/test/typescript/cases/91-construct-through-shape/src/x.ts b/graph/test/typescript/cases/91-construct-through-shape/src/x.ts
new file mode 100644
index 000000000..5eeddcc76
--- /dev/null
+++ b/graph/test/typescript/cases/91-construct-through-shape/src/x.ts
@@ -0,0 +1,16 @@
+// `new C(spec)` where C's type is a construct signature written as a type literal, an alias
+// of one, a constructor type, or an interface. A factory that takes "the class to build"
+// is written this way, and only the interface form resolved before.
+interface WidgetDef { kind: string }
+class Button { constructor(public spec: WidgetDef) {} }
+
+type WidgetClass = { new (spec: WidgetDef): T };
+function viaAlias(C: WidgetClass): T { return new C({ kind: "a" }); }
+function viaLiteral(C: { new (spec: WidgetDef): Button }): Button { return new C({ kind: "b" }); }
+
+// controls: a constructor type and an interface with a construct signature
+function viaCtorType(C: new (spec: WidgetDef) => Button): Button { return new C({ kind: "c" }); }
+interface ButtonCtor { new (spec: WidgetDef): Button }
+function viaInterface(C: ButtonCtor): Button { return new C({ kind: "d" }); }
+const made = viaAlias(Button);
+export { viaAlias, viaLiteral, viaCtorType, viaInterface, made };
diff --git a/graph/test/typescript/cases/92-namespace-typed-const/src/scorers.ts b/graph/test/typescript/cases/92-namespace-typed-const/src/scorers.ts
new file mode 100644
index 000000000..75a9137c0
--- /dev/null
+++ b/graph/test/typescript/cases/92-namespace-typed-const/src/scorers.ts
@@ -0,0 +1,9 @@
+export type Scorer = (input: string) => number;
+export function makeScorer(base: number): Scorer {
+ const fn: Scorer = (input) => input.length + base;
+ return fn;
+}
+// a function-typed const whose value is a factory's result: no literal to read
+export const score: Scorer = makeScorer(1);
+export const scoreArrow: Scorer = (input) => input.length;
+export function plain(input: string): number { return input.length; }
diff --git a/graph/test/typescript/cases/92-namespace-typed-const/src/x.ts b/graph/test/typescript/cases/92-namespace-typed-const/src/x.ts
new file mode 100644
index 000000000..0517c80a5
--- /dev/null
+++ b/graph/test/typescript/cases/92-namespace-typed-const/src/x.ts
@@ -0,0 +1,9 @@
+// `ns.score(x)` reads the const off a namespace import. The named import of the same const
+// resolved and the namespace read did not.
+import * as scorers from "./scorers";
+import { score } from "./scorers";
+export function viaNamespace(s: string): number { return scorers.score(s); }
+// controls: the named import, a const holding an arrow, and an exported function
+export function viaNamed(s: string): number { return score(s); }
+export function viaArrow(s: string): number { return scorers.scoreArrow(s); }
+export function viaPlain(s: string): number { return scorers.plain(s); }
diff --git a/graph/test/typescript/cases/93-literal-member-chain/src/x.ts b/graph/test/typescript/cases/93-literal-member-chain/src/x.ts
new file mode 100644
index 000000000..ef3d4fa5e
--- /dev/null
+++ b/graph/test/typescript/cases/93-literal-member-chain/src/x.ts
@@ -0,0 +1,17 @@
+// A member chain through nested inline object types. Each level reads a shape-typed
+// property off a shape, and the chain stopped after the first.
+class Logger { write(msg: string): void {} }
+interface Order {
+ meta: { audit: { log: Logger; nested: { deep: { log: Logger } } } };
+}
+function record(o: Order): void {
+ o.meta.audit.log.write("a");
+ o.meta.audit.nested.deep.log.write("b");
+}
+// controls: one level of literal, and a named type at every level
+interface Flat { meta: { log: Logger } }
+interface Named { meta: Meta }
+interface Meta { log: Logger }
+function flat(f: Flat): void { f.meta.log.write("c"); }
+function named(n: Named): void { n.meta.log.write("d"); }
+export { record, flat, named };
diff --git a/graph/test/typescript/cases/94-thistype-object-literal/src/x.ts b/graph/test/typescript/cases/94-thistype-object-literal/src/x.ts
new file mode 100644
index 000000000..a6c0a6042
--- /dev/null
+++ b/graph/test/typescript/cases/94-thistype-object-literal/src/x.ts
@@ -0,0 +1,31 @@
+// `this` in the methods of an object literal written as `X & ThisType`: the compiler
+// types it as T. A prototype table or a mixin is written this way, and every `this.m()` in
+// one resolved to nothing. Covered: the marker through a generic alias bound by an explicit
+// type argument, through an annotation, and written inline.
+interface Greeter { name: string; greet(): string; shout(): string; hail(): string; wave(): string }
+type Members = { [K in keyof T]?: T[K] } & ThisType;
+interface Mixin { make(): T }
+declare function mixin(name: string, members?: Members): Mixin;
+
+const viaTypeArgument = mixin("Greeter", {
+ greet() { return this.shout(); },
+ shout: function shout() { return this.greet(); },
+});
+const viaAnnotation: Members = {
+ hail() { return this.greet(); },
+};
+interface Waver { wave?(): string }
+const inline: Waver & ThisType = {
+ wave() { return this.shout(); },
+};
+
+// controls: without the marker `this` is the literal; an arrow property keeps the class's
+const plainLiteral = {
+ a() { return 1; },
+ b() { return this.a(); },
+};
+class Host {
+ run(): string { return "r"; }
+ table = mixin("Host", { greet: () => this.run() });
+}
+export { viaTypeArgument, viaAnnotation, inline, plainLiteral, Host };
diff --git a/graph/test/typescript/cases/95-bind-on-method-value/src/globals.d.ts b/graph/test/typescript/cases/95-bind-on-method-value/src/globals.d.ts
new file mode 100644
index 000000000..befc3b74c
--- /dev/null
+++ b/graph/test/typescript/cases/95-bind-on-method-value/src/globals.d.ts
@@ -0,0 +1,66 @@
+// ============================================================================
+// The case's OWN standard library: the minimum the checker needs under `noLib`.
+// ============================================================================
+// WHY noLib. This mechanism's targets are declared in the REAL `lib.es5.d.ts`, and a
+// case never has it: the case oracle only records a target inside the case's own files,
+// and the engine is handed only `--library `. So with the real library
+// neither side can name the target and the case measures nothing. Declaring `Function`
+// and `CallableFunction` HERE puts them on both sides of the comparison.
+//
+// This is not a mock of the behaviour. The checker resolves `CallableFunction` by NAME
+// from whatever is in scope and then applies `strictBindCallApply` itself, so these
+// declarations exercise the real decision. Verified: the same two declarations answer
+// CallableFunction under `strict: true` and Function under
+// `strict: false, strictBindCallApply: false`.
+//
+// Everything other than Function/CallableFunction is here only because `noLib` removes
+// it and the checker requires it. Keep this file minimal: an interface added here is a
+// declaration the engine can resolve to, and an accidental one is an accidental answer.
+
+interface Object { toString(): string; }
+interface Boolean {}
+interface Number {}
+interface String { padStart(width: number): string; padEnd(width: number): string; trim(): string; }
+interface Array { length: number; }
+interface ReadonlyArray { length: number; }
+interface IArguments {}
+interface RegExp {}
+
+// ── the two declarations the flag chooses between ───────────────────────────
+// Mirrors lib.es5.d.ts: `Function` declares all three, and `CallableFunction extends
+// Function` REDECLARES the same three with precise generic signatures. Both are in
+// scope at every site; only the flag decides.
+// NO explicit `this` parameter on these. lib.es5.d.ts declares them with one, but the
+// case oracle's edge label includes a `this` parameter in the signature while the engine
+// (correctly) does not treat it as a parameter. So `this: Function` renders as
+// `Function#apply(Function,any,any)` on one side and `Function#apply(any,any)` on the
+// other, and every site scores as a disagreement the engine did not actually make.
+interface Function {
+ apply(thisArg: any, argArray?: any): any;
+ call(thisArg: any, ...argArray: any[]): any;
+ bind(thisArg: any, ...argArray: any[]): any;
+ readonly name: string;
+ toString(): string;
+}
+
+interface CallableFunction extends Function {
+ // Simply typed on purpose. lib.es5.d.ts gives these precise generic signatures
+ // (`this: (this: T, ...args: A) => R`), and this fixture does not need them: what it
+ // tests is that TWO declarations of the same member names exist and that
+ // `strictBindCallApply` picks between them, which the checker decides from the flag
+ // and not from how well a signature fits. A function-typed parameter also renders
+ // badly in the case oracle's edge labels: the label splits on the commas inside
+ // `(this: T, ...args: A) => R` and emits `apply(args: A) =,T,T)`, which can never
+ // match the engine's label, so every site scored as a disagreement.
+ //
+ // Arguments are optional and returns are `any` so every call form in the case source
+ // typechecks. The two declarations stay distinguishable without help: an edge label
+ // carries the OWNER, so `CallableFunction#call` and `Function#call` never collide, and
+ // that owner is exactly what the flag decides.
+ call(thisArg: any, arg0?: any, arg1?: any): any;
+ apply(thisArg: any, args?: any): any;
+ bind(thisArg: any, arg0?: any): any;
+}
+
+
+interface NewableFunction extends Function {}
diff --git a/graph/test/typescript/cases/95-bind-on-method-value/src/tsconfig.json b/graph/test/typescript/cases/95-bind-on-method-value/src/tsconfig.json
new file mode 100644
index 000000000..a19ccad92
--- /dev/null
+++ b/graph/test/typescript/cases/95-bind-on-method-value/src/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "noLib": true
+ }
+}
diff --git a/graph/test/typescript/cases/95-bind-on-method-value/src/x.ts b/graph/test/typescript/cases/95-bind-on-method-value/src/x.ts
new file mode 100644
index 000000000..13b4539d2
--- /dev/null
+++ b/graph/test/typescript/cases/95-bind-on-method-value/src/x.ts
@@ -0,0 +1,21 @@
+// `call` / `apply` / `bind` on a METHOD read as a value, `this.render.bind(this)`. The
+// receiver names a method, so it had no type for the Function members to attach to.
+// src/globals.d.ts and noLib, as in case 21, put the targets inside the case.
+class View {
+ render(): string { return "v"; }
+ get label(): () => string { return () => "l"; }
+ handler: (x: number) => number = (x) => x;
+ wire(): void {
+ this.render = this.render.bind(this);
+ const r = this.render.call(this);
+ const s = this.render.apply(this, []);
+ // the bound result runs the method
+ const bound = this.render.bind(this);
+ const b = bound();
+ // controls: a function-typed field already resolved; a getter's value is what it returns
+ const h = this.handler.bind(this);
+ const l = this.label.bind(this);
+ }
+}
+function runOn(v: View): string { return v.render.call(v); }
+export { View, runOn };
diff --git a/graph/test/typescript/expected/08-function-values-and-callbacks.edges b/graph/test/typescript/expected/08-function-values-and-callbacks.edges
index c0aeef314..f5d5e2e68 100644
--- a/graph/test/typescript/expected/08-function-values-and-callbacks.edges
+++ b/graph/test/typescript/expected/08-function-values-and-callbacks.edges
@@ -3,7 +3,8 @@ ambiguous_unknown METHOD_CALL values#drive() @L29 -> -
callback_registered FUNCTION_CALL values#useLibrary() @L40 -> values#local(?)
known_edge CONSTRUCTOR_CALL values#drive() @L27 -> Pipeline#()
known_edge FUNCTION_CALL values#apply(Transform,string) @L12 -> values#(string)
-known_edge FUNCTION_CALL values#drive() @L30 -> values#(string)
known_edge FUNCTION_CALL values#drive() @L30 -> values#apply(Transform,string)
known_edge METHOD_CALL Pipeline#run(string) @L19 -> values#(string)
known_edge METHOD_CALL values#drive() @L30 -> Pipeline#run(string)
+multi_inferred FUNCTION_CALL values#drive() @L30 -> Pipeline#tag(string)
+multi_inferred FUNCTION_CALL values#drive() @L30 -> values#(string)
diff --git a/graph/test/typescript/expected/08-function-values-and-callbacks.envelope b/graph/test/typescript/expected/08-function-values-and-callbacks.envelope
index b629f2f6b..95c6c001d 100644
--- a/graph/test/typescript/expected/08-function-values-and-callbacks.envelope
+++ b/graph/test/typescript/expected/08-function-values-and-callbacks.envelope
@@ -4,4 +4,3 @@ value values#@4:25 -> values#Pipeline.tag
value_call values#Pipeline.run -> values#@6
value_call values#apply -> values#@6
value_call values#apply -> values#@7
-value_call values#drive -> values#Pipeline.tag
diff --git a/graph/test/typescript/expected/08-function-values-and-callbacks.lib.edges b/graph/test/typescript/expected/08-function-values-and-callbacks.lib.edges
index 20caf154d..03047edcb 100644
--- a/graph/test/typescript/expected/08-function-values-and-callbacks.lib.edges
+++ b/graph/test/typescript/expected/08-function-values-and-callbacks.lib.edges
@@ -5,7 +5,8 @@ boundary_lib FUNCTION_CALL values#useLibrary() @L40 -> hooks#withHook(Hook,strin
callback_registered FUNCTION_CALL values#useLibrary() @L40 -> values#local(?)
known_edge CONSTRUCTOR_CALL values#drive() @L27 -> Pipeline#()
known_edge FUNCTION_CALL values#apply(Transform,string) @L12 -> values#(string)
-known_edge FUNCTION_CALL values#drive() @L30 -> values#(string)
known_edge FUNCTION_CALL values#drive() @L30 -> values#apply(Transform,string)
known_edge METHOD_CALL Pipeline#run(string) @L19 -> values#(string)
known_edge METHOD_CALL values#drive() @L30 -> Pipeline#run(string)
+multi_inferred FUNCTION_CALL values#drive() @L30 -> Pipeline#tag(string)
+multi_inferred FUNCTION_CALL values#drive() @L30 -> values#(string)
diff --git a/graph/test/typescript/expected/08-function-values-and-callbacks.lib.envelope b/graph/test/typescript/expected/08-function-values-and-callbacks.lib.envelope
index c3b500e6a..2df0528fc 100644
--- a/graph/test/typescript/expected/08-function-values-and-callbacks.lib.envelope
+++ b/graph/test/typescript/expected/08-function-values-and-callbacks.lib.envelope
@@ -5,4 +5,3 @@ value values#@4:25 -> values#Pipeline.tag
value_call values#Pipeline.run -> values#@6
value_call values#apply -> values#@6
value_call values#apply -> values#@7
-value_call values#drive -> values#Pipeline.tag
diff --git a/graph/test/typescript/expected/08-function-values-and-callbacks.lib.oracle b/graph/test/typescript/expected/08-function-values-and-callbacks.lib.oracle
index 0b8515211..068febee7 100644
--- a/graph/test/typescript/expected/08-function-values-and-callbacks.lib.oracle
+++ b/graph/test/typescript/expected/08-function-values-and-callbacks.lib.oracle
@@ -1,2 +1,3 @@
-oracle=8 engine=9 agree=8 missing=0 (known 0, NEW 0) extra=1
+oracle=8 engine=10 agree=8 missing=0 (known 0, NEW 0) extra=2
+ extra values#drive() -> Pipeline#tag(string)
extra values#useLibrary() -> hooks#identity(?)
diff --git a/graph/test/typescript/expected/08-function-values-and-callbacks.oracle b/graph/test/typescript/expected/08-function-values-and-callbacks.oracle
index 4794eb8a3..84fbe5b0d 100644
--- a/graph/test/typescript/expected/08-function-values-and-callbacks.oracle
+++ b/graph/test/typescript/expected/08-function-values-and-callbacks.oracle
@@ -1 +1,2 @@
-oracle=6 engine=6 agree=6 missing=0 (known 0, NEW 0) extra=0
+oracle=6 engine=7 agree=6 missing=0 (known 0, NEW 0) extra=1
+ extra values#drive() -> Pipeline#tag(string)
diff --git a/graph/test/typescript/expected/80-method-passed-by-reference.edges b/graph/test/typescript/expected/80-method-passed-by-reference.edges
index 3d1be41b8..0e5d76362 100644
--- a/graph/test/typescript/expected/80-method-passed-by-reference.edges
+++ b/graph/test/typescript/expected/80-method-passed-by-reference.edges
@@ -1,8 +1,5 @@
ambient_terminal FUNCTION_CALL Widget#now(number[]) @L40 -> globals#setTimeout(() =,number)
ambient_terminal FUNCTION_CALL Widget#wire(Target) @L24 -> globals#setTimeout(() =,number)
-ambiguous_unknown METHOD_CALL Widget#now(number[]) @L40 -> -
-ambiguous_unknown METHOD_CALL Widget#start(number[]) @L18 -> -
-ambiguous_unknown METHOD_CALL Widget#wire(Target) @L24 -> -
callback_registered FUNCTION_CALL Widget#wire(Target) @L24 -> Widget#onHover()
callback_registered METHOD_CALL Widget#relay(Widget,number[]) @L29 -> Widget#handle(number)
callback_registered METHOD_CALL Widget#start(number[]) @L18 -> Widget#handle(number)
@@ -10,11 +7,14 @@ callback_registered METHOD_CALL Widget#start(number[]) @L19 -> Widget#handle(num
callback_registered METHOD_CALL Widget#tap(Target) @L34 -> Widget#()
callback_registered METHOD_CALL Widget#wire(Target) @L23 -> Widget#onClick()
known_edge METHOD_CALL Widget#() @L34 -> Widget#onTap()
+known_edge METHOD_CALL Widget#now(number[]) @L40 -> Function#call(unknown,unknown[])
known_edge METHOD_CALL Widget#now(number[]) @L41 -> Array#forEach((v: T) =,unknown)
known_edge METHOD_CALL Widget#now(number[]) @L42 -> Array#forEach((v: T) =,unknown)
known_edge METHOD_CALL Widget#relay(Widget,number[]) @L29 -> Array#forEach((v: T) =,unknown)
known_edge METHOD_CALL Widget#start(number[]) @L18 -> Array#forEach((v: T) =,unknown)
+known_edge METHOD_CALL Widget#start(number[]) @L18 -> Function#bind(unknown,unknown[])
known_edge METHOD_CALL Widget#start(number[]) @L19 -> Array#map((v: T) =,unknown)
known_edge METHOD_CALL Widget#tap(Target) @L34 -> Target#addEventListener(string,() =)
known_edge METHOD_CALL Widget#wire(Target) @L23 -> Target#addEventListener(string,() =)
+known_edge METHOD_CALL Widget#wire(Target) @L24 -> Function#bind(unknown,unknown[])
known_edge PROPERTY_READ Widget#now(number[]) @L42 -> Widget#current()
diff --git a/graph/test/typescript/expected/80-method-passed-by-reference.oracle b/graph/test/typescript/expected/80-method-passed-by-reference.oracle
index 196060769..f8e70220b 100644
--- a/graph/test/typescript/expected/80-method-passed-by-reference.oracle
+++ b/graph/test/typescript/expected/80-method-passed-by-reference.oracle
@@ -1,3 +1,6 @@
-oracle=8 engine=10 agree=8 missing=0 (known 0, NEW 0) extra=2
+oracle=8 engine=13 agree=8 missing=0 (known 0, NEW 0) extra=5
+ extra Widget#now(number[]) -> Function#call(unknown,unknown[])
extra Widget#now(number[]) -> globals#setTimeout(() =,number)
+ extra Widget#start(number[]) -> Function#bind(unknown,unknown[])
+ extra Widget#wire(Target) -> Function#bind(unknown,unknown[])
extra Widget#wire(Target) -> globals#setTimeout(() =,number)
diff --git a/graph/test/typescript/expected/83-closed-world-dispatch.edges b/graph/test/typescript/expected/83-closed-world-dispatch.edges
index 3e35361f4..ad9e18770 100644
--- a/graph/test/typescript/expected/83-closed-world-dispatch.edges
+++ b/graph/test/typescript/expected/83-closed-world-dispatch.edges
@@ -1,7 +1,7 @@
-ambiguous_unknown CONSTRUCTOR_CALL handlers#main() @L52 -> -
ambiguous_unknown METHOD_CALL handlers#register(HandlerClass) @L38 -> -
known_edge CONSTRUCTOR_CALL handlers#main() @L46 -> Built#()
known_edge CONSTRUCTOR_CALL handlers#main() @L47 -> Base#()
+known_edge CONSTRUCTOR_CALL handlers#main() @L52 -> handlers#()
known_edge FUNCTION_CALL handlers#() @L39 -> handlers#register(HandlerClass)
known_edge FUNCTION_CALL handlers#main() @L50 -> handlers#dispatch(Handler,string)
known_edge FUNCTION_CALL handlers#main() @L51 -> handlers#dispatch(Handler,string)
diff --git a/graph/test/typescript/expected/83-closed-world-dispatch.known-missing b/graph/test/typescript/expected/83-closed-world-dispatch.known-missing
deleted file mode 100644
index 15bb2695f..000000000
--- a/graph/test/typescript/expected/83-closed-world-dispatch.known-missing
+++ /dev/null
@@ -1,7 +0,0 @@
-# Accepted gaps for 83-closed-world-dispatch — each line is an edge the TypeScript compiler
-# resolves and this engine does not. A NEW missing edge fails the suite;
-# a line here that STARTS working also fails, so the debt cannot rot.
-# `new C()` where C is typed by a construct-signature alias: the compiler names the
-# alias's signature, the engine leaves the site ambiguous_unknown. Unrelated to the
-# closed-world fan this case is about.
-handlers#main() -> handlers#()
diff --git a/graph/test/typescript/expected/83-closed-world-dispatch.oracle b/graph/test/typescript/expected/83-closed-world-dispatch.oracle
index 7f00d858a..35561d9c3 100644
--- a/graph/test/typescript/expected/83-closed-world-dispatch.oracle
+++ b/graph/test/typescript/expected/83-closed-world-dispatch.oracle
@@ -1,5 +1,4 @@
-oracle=6 engine=12 agree=5 missing=1 (known 1, NEW 0) extra=7
- known handlers#main() -> handlers#()
+oracle=6 engine=13 agree=6 missing=0 (known 0, NEW 0) extra=7
extra handlers#dispatch(Handler,string) -> Base#handle(string)
extra handlers#dispatch(Handler,string) -> Built#handle(string)
extra handlers#dispatch(Handler,string) -> Orphan#handle(string)
diff --git a/graph/test/typescript/expected/91-construct-through-shape.edges b/graph/test/typescript/expected/91-construct-through-shape.edges
new file mode 100644
index 000000000..d04b2b5ad
--- /dev/null
+++ b/graph/test/typescript/expected/91-construct-through-shape.edges
@@ -0,0 +1,5 @@
+known_edge CONSTRUCTOR_CALL x#viaAlias(WidgetClass) @L8 -> x#(WidgetDef)
+known_edge CONSTRUCTOR_CALL x#viaCtorType(new (spec: WidgetDef) =) @L12 -> x#(WidgetDef)
+known_edge CONSTRUCTOR_CALL x#viaInterface(ButtonCtor) @L14 -> ButtonCtor#(WidgetDef)
+known_edge CONSTRUCTOR_CALL x#viaLiteral({ new (spec: WidgetDef): Button }) @L9 -> x#(WidgetDef)
+known_edge FUNCTION_CALL x#() @L15 -> x#viaAlias(WidgetClass)
diff --git a/graph/test/typescript/expected/91-construct-through-shape.entries b/graph/test/typescript/expected/91-construct-through-shape.entries
new file mode 100644
index 000000000..71704302a
--- /dev/null
+++ b/graph/test/typescript/expected/91-construct-through-shape.entries
@@ -0,0 +1,6 @@
+── entry_point (5) ──
+ exported_from_entry_module x#viaAlias x.ts:8
+ exported_from_entry_module x#viaCtorType x.ts:12
+ exported_from_entry_module x#viaInterface x.ts:14
+ exported_from_entry_module x#viaLiteral x.ts:9
+ unimported_module x# x.ts:1
diff --git a/graph/test/typescript/expected/91-construct-through-shape.fields b/graph/test/typescript/expected/91-construct-through-shape.fields
new file mode 100644
index 000000000..e69de29bb
diff --git a/graph/test/typescript/expected/91-construct-through-shape.fields-oracle b/graph/test/typescript/expected/91-construct-through-shape.fields-oracle
new file mode 100644
index 000000000..2a4ef20c1
--- /dev/null
+++ b/graph/test/typescript/expected/91-construct-through-shape.fields-oracle
@@ -0,0 +1,7 @@
+91-construct-through-shape [fields]
+ precision 0.0000 (0 correct, 0 wrong)
+ recall 0.0000 (0 of 0 the compiler resolved)
+ sites 0 resolved 0
+ tiers
+ access
+ not scored: 0 rows whose target is not a client declaration
diff --git a/graph/test/typescript/expected/91-construct-through-shape.oracle b/graph/test/typescript/expected/91-construct-through-shape.oracle
new file mode 100644
index 000000000..6c4bb5cbb
--- /dev/null
+++ b/graph/test/typescript/expected/91-construct-through-shape.oracle
@@ -0,0 +1 @@
+oracle=5 engine=5 agree=5 missing=0 (known 0, NEW 0) extra=0
diff --git a/graph/test/typescript/expected/91-construct-through-shape.type-use b/graph/test/typescript/expected/91-construct-through-shape.type-use
new file mode 100644
index 000000000..746e05578
--- /dev/null
+++ b/graph/test/typescript/expected/91-construct-through-shape.type-use
@@ -0,0 +1,10 @@
+ambiguous_unknown OBJECT_CREATION_TYPE 0 x [EXPRESSION] -> -
+known_edge METHOD_PARAM 0 Button [METHOD_PARAM] -> WidgetDef
+known_edge METHOD_PARAM 0 ButtonCtor [METHOD_PARAM] -> WidgetDef
+known_edge METHOD_PARAM 0 x [METHOD_PARAM] -> ButtonCtor
+known_edge METHOD_PARAM 0 x [METHOD_PARAM] -> WidgetClass
+known_edge METHOD_PARAM 0 x [METHOD_PARAM] -> WidgetDef
+known_edge METHOD_PARAM 1 x [METHOD_PARAM] -> WidgetDef
+known_edge METHOD_RETURN 0 ButtonCtor [METHOD] -> Button
+known_edge METHOD_RETURN 0 x [METHOD] -> Button
+known_edge METHOD_RETURN 1 x [METHOD_PARAM] -> Button
diff --git a/graph/test/typescript/expected/91-construct-through-shape.types-oracle b/graph/test/typescript/expected/91-construct-through-shape.types-oracle
new file mode 100644
index 000000000..4be183f12
--- /dev/null
+++ b/graph/test/typescript/expected/91-construct-through-shape.types-oracle
@@ -0,0 +1,8 @@
+91-construct-through-shape [types]
+ precision 1.0000 (7 correct, 0 wrong)
+ recall 0.8750 (7 of 8 the compiler resolved)
+ sites 17 resolved 13 (76.5%)
+ tiers ambiguous_unknown=4 known_edge=13
+ contexts METHOD_PARAM=7 METHOD_RETURN=6 OBJECT_CREATION_TYPE=4
+ not scored: 4 rows whose target is not a client declaration
+ MISSING WidgetClass USES WidgetDef
diff --git a/graph/test/typescript/expected/92-namespace-typed-const.edges b/graph/test/typescript/expected/92-namespace-typed-const.edges
new file mode 100644
index 000000000..9205e5e9c
--- /dev/null
+++ b/graph/test/typescript/expected/92-namespace-typed-const.edges
@@ -0,0 +1,5 @@
+known_edge FUNCTION_CALL scorers#() @L7 -> scorers#makeScorer(number)
+known_edge FUNCTION_CALL x#viaNamed(string) @L7 -> scorers#(string)
+known_edge METHOD_CALL x#viaArrow(string) @L8 -> scorers#scoreArrow(?)
+known_edge METHOD_CALL x#viaNamespace(string) @L5 -> scorers#(string)
+known_edge METHOD_CALL x#viaPlain(string) @L9 -> scorers#plain(string)
diff --git a/graph/test/typescript/expected/92-namespace-typed-const.entries b/graph/test/typescript/expected/92-namespace-typed-const.entries
new file mode 100644
index 000000000..637ea1410
--- /dev/null
+++ b/graph/test/typescript/expected/92-namespace-typed-const.entries
@@ -0,0 +1,6 @@
+── entry_point (5) ──
+ exported_from_entry_module x#viaArrow x.ts:8
+ exported_from_entry_module x#viaNamed x.ts:7
+ exported_from_entry_module x#viaNamespace x.ts:5
+ exported_from_entry_module x#viaPlain x.ts:9
+ unimported_module x# x.ts:1
diff --git a/graph/test/typescript/expected/92-namespace-typed-const.envelope b/graph/test/typescript/expected/92-namespace-typed-const.envelope
new file mode 100644
index 000000000..5781aa1ce
--- /dev/null
+++ b/graph/test/typescript/expected/92-namespace-typed-const.envelope
@@ -0,0 +1,4 @@
+value scorers#@1:22 -> scorers#@3
+value scorers#@1:22 -> scorers#@8
+value_call x#viaNamed -> scorers#@3
+value_call x#viaNamespace -> scorers#@3
diff --git a/graph/test/typescript/expected/92-namespace-typed-const.fields b/graph/test/typescript/expected/92-namespace-typed-const.fields
new file mode 100644
index 000000000..e9b9013e7
--- /dev/null
+++ b/graph/test/typescript/expected/92-namespace-typed-const.fields
@@ -0,0 +1,4 @@
+ambiguous_unknown read scorers#fn(?) -> -
+ambiguous_unknown read scorers#plain(string) -> -
+ambiguous_unknown read scorers#scoreArrow(?) -> -
+known_edge read x#viaNamed(string) -> scorers#score
diff --git a/graph/test/typescript/expected/92-namespace-typed-const.fields-oracle b/graph/test/typescript/expected/92-namespace-typed-const.fields-oracle
new file mode 100644
index 000000000..da0990d4c
--- /dev/null
+++ b/graph/test/typescript/expected/92-namespace-typed-const.fields-oracle
@@ -0,0 +1,7 @@
+92-namespace-typed-const [fields]
+ precision 0.0000 (0 correct, 0 wrong)
+ recall 0.0000 (0 of 0 the compiler resolved)
+ sites 4 resolved 1 (25.0%)
+ tiers ambiguous_unknown=3 known_edge=1
+ access read=4
+ not scored: 4 rows whose target is not a client declaration
diff --git a/graph/test/typescript/expected/92-namespace-typed-const.known-missing b/graph/test/typescript/expected/92-namespace-typed-const.known-missing
new file mode 100644
index 000000000..9dacefbf2
--- /dev/null
+++ b/graph/test/typescript/expected/92-namespace-typed-const.known-missing
@@ -0,0 +1,7 @@
+# Accepted gaps for 92-namespace-typed-const: each line is an edge the TypeScript compiler
+# resolves and this engine does not. A NEW missing edge fails the suite;
+# a line here that STARTS working also fails, so the debt cannot rot.
+# A const that HOLDS an arrow: the compiler names the annotation's signature, the engine names
+# the arrow's body, as it does for the same const read by bare name. Kept on purpose: adding the
+# bodiless signature beside the body would hedge a site whose implementation is known.
+x#viaArrow(string) -> scorers#(string)
diff --git a/graph/test/typescript/expected/92-namespace-typed-const.oracle b/graph/test/typescript/expected/92-namespace-typed-const.oracle
new file mode 100644
index 000000000..aaf82eab3
--- /dev/null
+++ b/graph/test/typescript/expected/92-namespace-typed-const.oracle
@@ -0,0 +1,3 @@
+oracle=5 engine=5 agree=4 missing=1 (known 1, NEW 0) extra=1
+ known x#viaArrow(string) -> scorers#(string)
+ extra x#viaArrow(string) -> scorers#scoreArrow(?)
diff --git a/graph/test/typescript/expected/92-namespace-typed-const.type-use b/graph/test/typescript/expected/92-namespace-typed-const.type-use
new file mode 100644
index 000000000..fca5ea869
--- /dev/null
+++ b/graph/test/typescript/expected/92-namespace-typed-const.type-use
@@ -0,0 +1,2 @@
+known_edge METHOD_RETURN 0 scorers [METHOD] -> Scorer
+known_edge VARIABLE_TYPE 0 scorers [VARIABLE] -> Scorer
diff --git a/graph/test/typescript/expected/92-namespace-typed-const.types-oracle b/graph/test/typescript/expected/92-namespace-typed-const.types-oracle
new file mode 100644
index 000000000..1318ffdaa
--- /dev/null
+++ b/graph/test/typescript/expected/92-namespace-typed-const.types-oracle
@@ -0,0 +1,7 @@
+92-namespace-typed-const [types]
+ precision 1.0000 (1 correct, 0 wrong)
+ recall 1.0000 (1 of 1 the compiler resolved)
+ sites 4 resolved 4 (100.0%)
+ tiers known_edge=4
+ contexts METHOD_RETURN=1 VARIABLE_TYPE=3
+ not scored: 0 rows whose target is not a client declaration
diff --git a/graph/test/typescript/expected/93-literal-member-chain.edges b/graph/test/typescript/expected/93-literal-member-chain.edges
new file mode 100644
index 000000000..9e9c71b98
--- /dev/null
+++ b/graph/test/typescript/expected/93-literal-member-chain.edges
@@ -0,0 +1,4 @@
+known_edge METHOD_CALL x#flat(Flat) @L15 -> Logger#write(string)
+known_edge METHOD_CALL x#named(Named) @L16 -> Logger#write(string)
+known_edge METHOD_CALL x#record(Order) @L8 -> Logger#write(string)
+known_edge METHOD_CALL x#record(Order) @L9 -> Logger#write(string)
diff --git a/graph/test/typescript/expected/93-literal-member-chain.entries b/graph/test/typescript/expected/93-literal-member-chain.entries
new file mode 100644
index 000000000..ffd59efd0
--- /dev/null
+++ b/graph/test/typescript/expected/93-literal-member-chain.entries
@@ -0,0 +1,5 @@
+── entry_point (4) ──
+ exported_from_entry_module x#flat x.ts:15
+ exported_from_entry_module x#named x.ts:16
+ exported_from_entry_module x#record x.ts:7
+ unimported_module x# x.ts:1
diff --git a/graph/test/typescript/expected/93-literal-member-chain.fields b/graph/test/typescript/expected/93-literal-member-chain.fields
new file mode 100644
index 000000000..a14893e8a
--- /dev/null
+++ b/graph/test/typescript/expected/93-literal-member-chain.fields
@@ -0,0 +1,10 @@
+known_edge read x#flat(Flat) -> Flat#meta
+known_edge read x#flat(Flat) -> { log: Logger }#log
+known_edge read x#named(Named) -> Meta#log
+known_edge read x#named(Named) -> Named#meta
+known_edge read x#record(Order) -> Order#meta
+known_edge read x#record(Order) -> { audit: { log: Logger; nested: { deep: { log: Logger } } } }#audit
+known_edge read x#record(Order) -> { deep: { log: Logger } }#deep
+known_edge read x#record(Order) -> { log: Logger }#log
+known_edge read x#record(Order) -> { log: Logger; nested: { deep: { log: Logger } } }#log
+known_edge read x#record(Order) -> { log: Logger; nested: { deep: { log: Logger } } }#nested
diff --git a/graph/test/typescript/expected/93-literal-member-chain.fields-oracle b/graph/test/typescript/expected/93-literal-member-chain.fields-oracle
new file mode 100644
index 000000000..27fc7aac8
--- /dev/null
+++ b/graph/test/typescript/expected/93-literal-member-chain.fields-oracle
@@ -0,0 +1,18 @@
+93-literal-member-chain [fields]
+ precision 0.4000 (4 correct, 6 wrong)
+ recall 0.4444 (4 of 9 the compiler resolved)
+ sites 12 resolved 12 (100.0%)
+ tiers known_edge=12
+ access read=12
+ not scored: 0 rows whose target is not a client declaration
+ WRONG x#flat(Flat) READ { log: Logger }#log
+ WRONG x#record(Order) READ { audit: { log: Logger; nested: { deep: { log: Logger } } } }#audit
+ WRONG x#record(Order) READ { deep: { log: Logger } }#deep
+ WRONG x#record(Order) READ { log: Logger }#log
+ WRONG x#record(Order) READ { log: Logger; nested: { deep: { log: Logger } } }#log
+ WRONG x#record(Order) READ { log: Logger; nested: { deep: { log: Logger } } }#nested
+ MISSING x#flat(Flat) READ Flat#log
+ MISSING x#record(Order) READ Order#audit
+ MISSING x#record(Order) READ Order#deep
+ MISSING x#record(Order) READ Order#log
+ MISSING x#record(Order) READ Order#nested
diff --git a/graph/test/typescript/expected/93-literal-member-chain.oracle b/graph/test/typescript/expected/93-literal-member-chain.oracle
new file mode 100644
index 000000000..a43ea4f88
--- /dev/null
+++ b/graph/test/typescript/expected/93-literal-member-chain.oracle
@@ -0,0 +1 @@
+oracle=3 engine=3 agree=3 missing=0 (known 0, NEW 0) extra=0
diff --git a/graph/test/typescript/expected/93-literal-member-chain.type-use b/graph/test/typescript/expected/93-literal-member-chain.type-use
new file mode 100644
index 000000000..cecb1bce8
--- /dev/null
+++ b/graph/test/typescript/expected/93-literal-member-chain.type-use
@@ -0,0 +1,8 @@
+known_edge FIELD_TYPE 0 Meta [FIELD] -> Logger
+known_edge FIELD_TYPE 0 Named [FIELD] -> Meta
+known_edge FIELD_TYPE 1 Flat [FIELD] -> Logger
+known_edge FIELD_TYPE 2 Order [FIELD] -> Logger
+known_edge FIELD_TYPE 4 Order [FIELD] -> Logger
+known_edge METHOD_PARAM 0 x [METHOD_PARAM] -> Flat
+known_edge METHOD_PARAM 0 x [METHOD_PARAM] -> Named
+known_edge METHOD_PARAM 0 x [METHOD_PARAM] -> Order
diff --git a/graph/test/typescript/expected/93-literal-member-chain.types-oracle b/graph/test/typescript/expected/93-literal-member-chain.types-oracle
new file mode 100644
index 000000000..53acc6dcd
--- /dev/null
+++ b/graph/test/typescript/expected/93-literal-member-chain.types-oracle
@@ -0,0 +1,7 @@
+93-literal-member-chain [types]
+ precision 1.0000 (7 correct, 0 wrong)
+ recall 1.0000 (7 of 7 the compiler resolved)
+ sites 8 resolved 8 (100.0%)
+ tiers known_edge=8
+ contexts FIELD_TYPE=5 METHOD_PARAM=3
+ not scored: 0 rows whose target is not a client declaration
diff --git a/graph/test/typescript/expected/94-thistype-object-literal.edges b/graph/test/typescript/expected/94-thistype-object-literal.edges
new file mode 100644
index 000000000..ebda1c8aa
--- /dev/null
+++ b/graph/test/typescript/expected/94-thistype-object-literal.edges
@@ -0,0 +1,8 @@
+ambient_terminal FUNCTION_CALL x#() @L10 -> x#mixin(string,Members)
+ambient_terminal FUNCTION_CALL x#() @L29 -> x#mixin(string,Members)
+ambiguous_unknown METHOD_CALL x#b() @L25 -> -
+known_edge METHOD_CALL Host#() @L29 -> Host#run()
+known_edge METHOD_CALL x#greet() @L11 -> Greeter#shout()
+known_edge METHOD_CALL x#hail() @L15 -> Greeter#greet()
+known_edge METHOD_CALL x#shout() @L12 -> Greeter#greet()
+known_edge METHOD_CALL x#wave() @L19 -> Greeter#shout()
diff --git a/graph/test/typescript/expected/94-thistype-object-literal.entries b/graph/test/typescript/expected/94-thistype-object-literal.entries
new file mode 100644
index 000000000..6f5cfa1ff
--- /dev/null
+++ b/graph/test/typescript/expected/94-thistype-object-literal.entries
@@ -0,0 +1,2 @@
+── entry_point (1) ──
+ unimported_module x# x.ts:1
diff --git a/graph/test/typescript/expected/94-thistype-object-literal.envelope b/graph/test/typescript/expected/94-thistype-object-literal.envelope
new file mode 100644
index 000000000..6ef842dad
--- /dev/null
+++ b/graph/test/typescript/expected/94-thistype-object-literal.envelope
@@ -0,0 +1,2 @@
+value x#Greeter.hail -> x#hail
+value x#Waver.wave -> x#wave
diff --git a/graph/test/typescript/expected/94-thistype-object-literal.fields b/graph/test/typescript/expected/94-thistype-object-literal.fields
new file mode 100644
index 000000000..e69de29bb
diff --git a/graph/test/typescript/expected/94-thistype-object-literal.fields-oracle b/graph/test/typescript/expected/94-thistype-object-literal.fields-oracle
new file mode 100644
index 000000000..36c4e6506
--- /dev/null
+++ b/graph/test/typescript/expected/94-thistype-object-literal.fields-oracle
@@ -0,0 +1,7 @@
+94-thistype-object-literal [fields]
+ precision 0.0000 (0 correct, 0 wrong)
+ recall 0.0000 (0 of 0 the compiler resolved)
+ sites 0 resolved 0
+ tiers
+ access
+ not scored: 0 rows whose target is not a client declaration
diff --git a/graph/test/typescript/expected/94-thistype-object-literal.known-missing b/graph/test/typescript/expected/94-thistype-object-literal.known-missing
new file mode 100644
index 000000000..c34d8fcf5
--- /dev/null
+++ b/graph/test/typescript/expected/94-thistype-object-literal.known-missing
@@ -0,0 +1,6 @@
+# Accepted gaps for 94-thistype-object-literal: each line is an edge the TypeScript compiler
+# resolves and this engine does not. A NEW missing edge fails the suite;
+# a line here that STARTS working also fails, so the debt cannot rot.
+# The no-marker control: with no contextual type, `this` in a literal's method is the literal
+# itself. The engine does not type it, before this change and after; the marker cases are the fix.
+x#b() -> x#a()
diff --git a/graph/test/typescript/expected/94-thistype-object-literal.oracle b/graph/test/typescript/expected/94-thistype-object-literal.oracle
new file mode 100644
index 000000000..5c112c0a4
--- /dev/null
+++ b/graph/test/typescript/expected/94-thistype-object-literal.oracle
@@ -0,0 +1,2 @@
+oracle=7 engine=6 agree=6 missing=1 (known 1, NEW 0) extra=0
+ known x#b() -> x#a()
diff --git a/graph/test/typescript/expected/94-thistype-object-literal.type-use b/graph/test/typescript/expected/94-thistype-object-literal.type-use
new file mode 100644
index 000000000..9d88dab9d
--- /dev/null
+++ b/graph/test/typescript/expected/94-thistype-object-literal.type-use
@@ -0,0 +1,10 @@
+ambiguous_unknown TYPE_ELEMENT 1 Members [TYPE] -> -
+ambiguous_unknown TYPE_ELEMENT 1 x [VARIABLE] -> -
+known_edge METHOD_PARAM 0 x [METHOD_PARAM] -> Members
+known_edge METHOD_RETURN 0 x [METHOD] -> Mixin
+known_edge METHOD_TYPE_ARGUMENT 0 Host [EXPRESSION] -> Greeter
+known_edge METHOD_TYPE_ARGUMENT 0 x [EXPRESSION] -> Greeter
+known_edge TYPE_ARGUMENT 1 x [VARIABLE] -> Greeter
+known_edge TYPE_ARGUMENT 2 x [VARIABLE] -> Greeter
+known_edge TYPE_ELEMENT 1 x [VARIABLE] -> Waver
+known_edge VARIABLE_TYPE 0 x [VARIABLE] -> Members
diff --git a/graph/test/typescript/expected/94-thistype-object-literal.types-oracle b/graph/test/typescript/expected/94-thistype-object-literal.types-oracle
new file mode 100644
index 000000000..cd697dd3d
--- /dev/null
+++ b/graph/test/typescript/expected/94-thistype-object-literal.types-oracle
@@ -0,0 +1,7 @@
+94-thistype-object-literal [types]
+ precision 1.0000 (5 correct, 0 wrong)
+ recall 1.0000 (5 of 5 the compiler resolved)
+ sites 10 resolved 8 (80.0%)
+ tiers ambiguous_unknown=2 known_edge=8
+ contexts METHOD_PARAM=1 METHOD_RETURN=1 METHOD_TYPE_ARGUMENT=2 TYPE_ARGUMENT=2 TYPE_ELEMENT=3 VARIABLE_TYPE=1
+ not scored: 2 rows whose target is not a client declaration
diff --git a/graph/test/typescript/expected/95-bind-on-method-value.edges b/graph/test/typescript/expected/95-bind-on-method-value.edges
new file mode 100644
index 000000000..44e5302ec
--- /dev/null
+++ b/graph/test/typescript/expected/95-bind-on-method-value.edges
@@ -0,0 +1,9 @@
+known_edge FUNCTION_CALL View#wire() @L14 -> View#render()
+known_edge METHOD_CALL View#wire() @L10 -> CallableFunction#call(any,any,any)
+known_edge METHOD_CALL View#wire() @L11 -> CallableFunction#apply(any,any)
+known_edge METHOD_CALL View#wire() @L13 -> CallableFunction#bind(any,any)
+known_edge METHOD_CALL View#wire() @L16 -> CallableFunction#bind(any,any)
+known_edge METHOD_CALL View#wire() @L17 -> CallableFunction#bind(any,any)
+known_edge METHOD_CALL View#wire() @L9 -> CallableFunction#bind(any,any)
+known_edge METHOD_CALL x#runOn(View) @L20 -> CallableFunction#call(any,any,any)
+known_edge PROPERTY_READ View#wire() @L17 -> View#label()
diff --git a/graph/test/typescript/expected/95-bind-on-method-value.entries b/graph/test/typescript/expected/95-bind-on-method-value.entries
new file mode 100644
index 000000000..6997e79ce
--- /dev/null
+++ b/graph/test/typescript/expected/95-bind-on-method-value.entries
@@ -0,0 +1,4 @@
+── entry_point (3) ──
+ exported_from_entry_module x#runOn x.ts:20
+ unimported_module globals# globals.d.ts:1
+ unimported_module x# x.ts:1
diff --git a/graph/test/typescript/expected/95-bind-on-method-value.envelope b/graph/test/typescript/expected/95-bind-on-method-value.envelope
new file mode 100644
index 000000000..46f9e8590
--- /dev/null
+++ b/graph/test/typescript/expected/95-bind-on-method-value.envelope
@@ -0,0 +1,5 @@
+nominal globals#Function.apply -> globals#CallableFunction.apply
+nominal globals#Function.bind -> globals#CallableFunction.bind
+nominal globals#Function.call -> globals#CallableFunction.call
+value x#@6:16 -> x#View.@6
+value x#@7:12 -> x#View.@7
diff --git a/graph/test/typescript/expected/95-bind-on-method-value.fields b/graph/test/typescript/expected/95-bind-on-method-value.fields
new file mode 100644
index 000000000..88df97815
--- /dev/null
+++ b/graph/test/typescript/expected/95-bind-on-method-value.fields
@@ -0,0 +1 @@
+known_edge read View#wire() -> View#handler
diff --git a/graph/test/typescript/expected/95-bind-on-method-value.fields-oracle b/graph/test/typescript/expected/95-bind-on-method-value.fields-oracle
new file mode 100644
index 000000000..72328250a
--- /dev/null
+++ b/graph/test/typescript/expected/95-bind-on-method-value.fields-oracle
@@ -0,0 +1,7 @@
+95-bind-on-method-value [fields]
+ precision 1.0000 (1 correct, 0 wrong)
+ recall 1.0000 (1 of 1 the compiler resolved)
+ sites 1 resolved 1 (100.0%)
+ tiers known_edge=1
+ access read=1
+ not scored: 0 rows whose target is not a client declaration
diff --git a/graph/test/typescript/expected/95-bind-on-method-value.oracle b/graph/test/typescript/expected/95-bind-on-method-value.oracle
new file mode 100644
index 000000000..131c80216
--- /dev/null
+++ b/graph/test/typescript/expected/95-bind-on-method-value.oracle
@@ -0,0 +1,2 @@
+oracle=5 engine=6 agree=5 missing=0 (known 0, NEW 0) extra=1
+ extra View#wire() -> View#render()
diff --git a/graph/test/typescript/expected/95-bind-on-method-value.type-use b/graph/test/typescript/expected/95-bind-on-method-value.type-use
new file mode 100644
index 000000000..e6dc01bae
--- /dev/null
+++ b/graph/test/typescript/expected/95-bind-on-method-value.type-use
@@ -0,0 +1,3 @@
+known_edge METHOD_PARAM 0 x [METHOD_PARAM] -> View
+known_edge SUPER_TYPE 0 CallableFunction [HERITAGE] -> Function
+known_edge SUPER_TYPE 0 NewableFunction [HERITAGE] -> Function
diff --git a/graph/test/typescript/expected/95-bind-on-method-value.types-oracle b/graph/test/typescript/expected/95-bind-on-method-value.types-oracle
new file mode 100644
index 000000000..3cce60dfc
--- /dev/null
+++ b/graph/test/typescript/expected/95-bind-on-method-value.types-oracle
@@ -0,0 +1,7 @@
+95-bind-on-method-value [types]
+ precision 1.0000 (3 correct, 0 wrong)
+ recall 1.0000 (3 of 3 the compiler resolved)
+ sites 3 resolved 3 (100.0%)
+ tiers known_edge=3
+ contexts METHOD_PARAM=1 SUPER_TYPE=2
+ not scored: 0 rows whose target is not a client declaration
diff --git a/graph/typescript/engine/expression-resolution/callee-resolution.dl b/graph/typescript/engine/expression-resolution/callee-resolution.dl
index c42570448..d05fc58a1 100644
--- a/graph/typescript/engine/expression-resolution/callee-resolution.dl
+++ b/graph/typescript/engine/expression-resolution/callee-resolution.dl
@@ -190,6 +190,45 @@ expr_call_candidate(ce, m) :- call_site("client", _, cn, rk, recv, ce, _),
expr_shape(recv, s),
method_in_scope(s, cn, _, m).
+// (6b) `call` / `apply` / `bind` ON A METHOD READ AS A VALUE: `this.render.bind(this)`,
+// `proto.check.call(inst, x)`. The receiver is an access that names a METHOD, so it has no
+// type or shape of its own for rules (4) to (6) to search, and the Function prototype carve-out
+// in member-lookup.dl attaches only to a scope with a call signature. The compiler types the
+// receiver as the method's function type and answers with its `bind`. method_value
+// (value-flow.dl) recognises the same receiver but runs after this resolver, so the lookup is
+// repeated here exactly as rules (4) and (6) make it. A getter is excluded: reading it runs it,
+// and the value handed over is whatever it returns.
+expr_call_candidate(ce, fm) :- call_site("client", _, cn, rk, recv, ce, _),
+ receiver_kind_is_value(rk),
+ function_prototype_member(cn, fm),
+ property_access_recv(recv, n, q),
+ expr_type(q, _, qt),
+ method_in_scope(qt, n, "false", m),
+ !method_kind(_, "GETTER", _, m),
+ !method_kind(_, "SETTER", _, m).
+expr_call_candidate(ce, fm) :- call_site("client", _, cn, rk, recv, ce, _),
+ receiver_kind_is_value(rk),
+ function_prototype_member(cn, fm),
+ property_access_recv(recv, n, q),
+ expr_shape(q, s),
+ method_in_scope(s, n, _, m),
+ !method_kind(_, "GETTER", _, m),
+ !method_kind(_, "SETTER", _, m).
+// …and what the bound result runs. `const write = sink.write.bind(sink); write(x)` calls
+// `write`, which is what the compiler names. Typed only through `bind`'s generic return, the
+// call reached the bodiless function types inside that return in the standard library and
+// nothing else, a wrong answer as soon as (6b) gave the `bind` site its target.
+expr_call_candidate(ce, m) :- call_callee_expr(_, ce, ne),
+ expr_referenced("client", "VARIABLE", v, ne),
+ var_initializer("client", _, bc, v),
+ call_site("client", _, "bind", _, recv, bc, _),
+ recv != "",
+ property_access_recv(recv, n, q),
+ expr_type(q, _, qt),
+ method_in_scope(qt, n, "false", m),
+ !method_kind(_, "GETTER", _, m),
+ !method_kind(_, "SETTER", _, m).
+
// (5b) A MEMBER OF AN OBJECT LITERAL BOUND TO A NAME — `handlers.viaShorthand(n)`.
// An OBJECT_LITERAL_METHOD's owner is the literal's EXPRESSION, not a type, so it is in
// no member table at all: `method_in_scope` is keyed on a scope and there is no scope
@@ -484,6 +523,21 @@ expr_call_candidate(ce, ctor) :- call_site("client", "CONSTRUCTOR_CALL", _, _, _
call_callee_expr(_, ce, ne),
expr_type(ne, _, ctorType),
constructor_in_scope(ctorType, ctor).
+// …and where the callee's type is a SHAPE rather than a declared type: a parameter typed
+// `{ new (def: D): T }`, or an alias of one, as a factory taking "the class to build" is
+// written. Rule (3) reads expr_shape for a plain call through a callable shape; construction
+// read only expr_type, so the construct signature on the literal was never consulted and
+// `new C(def)` resolved to nothing. Same lookup, construct side.
+//
+// Not where the callee is a variable known to HOLD a class (`const Ctor: new () => Dog = Dog`):
+// the class's own constructor already answers there, and the annotation's bodiless signature
+// beside it would hedge a site that was right.
+expr_call_candidate(ce, ctor) :- call_site("client", "CONSTRUCTOR_CALL", _, _, _, ce, _),
+ call_callee_expr(_, ce, ne),
+ expr_shape(ne, s),
+ !callee_holds_class(ne),
+ constructor_in_scope(s, ctor).
+callee_holds_class(ne) :- expr_referenced("client", "VARIABLE", v, ne), class_valued_var(v, _).
// (10) `super(...)` — the enclosing class's `extends` parent's constructor. Never
// an implements edge: `implements` inherits nothing, so it has no constructor to run.
diff --git a/graph/typescript/engine/expression-resolution/expr-type.dl b/graph/typescript/engine/expression-resolution/expr-type.dl
index 06396ef12..f7fbc0d62 100644
--- a/graph/typescript/engine/expression-resolution/expr-type.dl
+++ b/graph/typescript/engine/expression-resolution/expr-type.dl
@@ -178,6 +178,60 @@ this_expr(e) :- expr_referenced("client", "THIS", _, e).
this_expr(e) :- expr_kind("client", "THIS_REFERENCE", _, e).
method_declared_this(m, p) :- param_shape("client", "THIS", _, _, p), param_decl("client", _, _, _, m, p).
+// ── `this` IN AN OBJECT LITERAL WRITTEN AS `X & ThisType` ────────────────
+// `defineProto({ inc() {…}, twice() { return this.inc() } })` where the parameter is
+// `ProtoFor = { … } & ThisType`: the compiler types `this` in the literal's methods as
+// T, which is the whole point of the marker. It is how a prototype table, a mixin or a store
+// definition is written when its methods run with the instance as receiver. Nothing typed
+// `this` there, so every `this.m()` in such a table resolved to nothing.
+//
+// thistype_arg(Ref, ArgRef): the contextual reference carries ThisType, with ArgRef as its
+// argument written in Ref's own scope. Found through an intersection, a parenthesised type,
+// and an alias, where the alias's own parameter is replaced by Ref's argument for it. That
+// argument is then either a written type or the CALLEE's type variable, bound by the call
+// (call_tvar_arg_binding, which covers an explicit type argument).
+//
+// Function-valued members only: an arrow property does not bind `this`, so it keeps the
+// enclosing one.
+thistype_arg(r, a) :- type_ref(prov, "TYPE_REFERENCE", _, "ThisType", _, _, _, r),
+ type_ref_parent(prov, r, "0", a).
+thistype_arg(r, a) :- type_ref(prov, k, _, _, _, _, _, r),
+ thistype_carrier_kind(k),
+ type_ref_parent(prov, r, _, c),
+ thistype_arg(c, a).
+thistype_arg(r, a) :- type_ref_resolves(_, r, t),
+ type_alias_target(_, ar, t),
+ thistype_arg(ar, v),
+ type_ref_type_param(_, tp, v),
+ type_param(_, pname, _, _, t, tp),
+ type_arg_binding(r, pname, a).
+thistype_arg(r, a) :- type_ref_resolves(_, r, t),
+ type_alias_target(_, ar, t),
+ thistype_arg(ar, a),
+ !type_ref_type_param(_, _, a).
+thistype_carrier_kind("INTERSECTION").
+thistype_carrier_kind("PARENTHESIZED").
+
+objlit_this_type(l, prov, t) :- objlit_ctx_ref(l, r),
+ thistype_arg(r, a),
+ !type_ref_type_param(_, _, a),
+ ref_type_target(a, prov, t).
+objlit_this_type(l, prov, t) :- expr_child("client", ce, "ARGUMENT", pos, l),
+ expr_kind("client", "OBJECT_LITERAL", _, l),
+ expr_call_candidate(ce, m),
+ param_decl(_, _, pos, _, m, p),
+ param_type_ref(_, r, p),
+ thistype_arg(r, a),
+ type_ref_type_param(_, tp, a),
+ type_param(_, _, _, _, m, tp),
+ call_tvar_arg_binding(ce, tp, prov, t).
+
+expr_type(e, prov, t) :- this_expr(e),
+ expr_enclosing_method(e, lm),
+ objlit_member_fn(l, _, lm),
+ !method_kind(_, "ARROW_FUNCTION", _, lm),
+ objlit_this_type(l, prov, t).
+
// `this` INSIDE A STATIC METHOD IS THE CLASS, so its members are the STATICS. A helper
// class that calls its own statics through `this` is ordinary TypeScript —
// `private static parse(...)` reached as `this.parse(...)` from another static — and the
@@ -426,6 +480,15 @@ expr_type(e, prov, t) :- property_access_recv(e, n, q),
expr_shape(q, qs),
field_in_scope(qs, n, _, f),
field_type(f, prov, t).
+// …and the member's SHAPE, off a shape qualifier. The typed-qualifier clause for
+// field_shape is below; without this one a chain of inline object types, `ctx.common.issues`
+// with `ctx: { common: { issues: Issue[] } }`, went one level deep and stopped: `ctx` had a
+// shape, `ctx.common` read a shape-typed member off it and got nothing, and every call on
+// the rest of the chain was unresolved.
+expr_shape(e, s) :- property_access_recv(e, n, q),
+ expr_shape(q, qs),
+ field_in_scope(qs, n, _, f),
+ field_shape(f, s).
// ── A PROPERTY ACCESS WHOSE MEMBER IS A GETTER ─────────────────────────────
// `get entityToSyncMetadatas(): EntityMetadata[]`, read as
// `this.entityToSyncMetadatas.map(...)`. Every rule above looks the member up in
@@ -471,6 +534,21 @@ expr_type(e, prov, t) :- property_access_recv(e, n, q),
expr_namespace(q, mprov, m),
module_export(mprov, m, n, "VARIABLE", v),
var_type(v, prov, t).
+// …and its SHAPE, which is the half a function-typed export has: `export const parse:
+// Parser = makeParser(1)` where `Parser` aliases a function type. The named import of the
+// same const already reads var_shape (the IMPORT_BINDING clause above); read off the
+// namespace, `ns.parse(x)` had a type-less, shape-less callee and resolved to nothing, while
+// `import { parse }` beside it resolved. The value is a factory's result, so var_is_function
+// has no literal to find and the declared shape is the only answer there is.
+//
+// Only there. Where the const DOES hold a function literal, rule (8) already answers with its
+// body, and adding the annotation's bodiless signature beside it turned an implementation the
+// site was right to name into a two-way hedge.
+expr_shape(e, s) :- property_access_recv(e, n, q),
+ expr_namespace(q, _, m),
+ module_export(_, m, n, "VARIABLE", v),
+ !var_is_function(v, _),
+ var_shape(v, s).
expr_static_type(e, prov, t) :- property_access_recv(e, n, q),
expr_namespace(q, prov, m),
module_export(prov, m, n, "TYPE", t).
diff --git a/graph/typescript/souffle/decls_all.dl b/graph/typescript/souffle/decls_all.dl
index be835f2c9..252fbc579 100644
--- a/graph/typescript/souffle/decls_all.dl
+++ b/graph/typescript/souffle/decls_all.dl
@@ -201,6 +201,7 @@
.decl callee_kind_is_expression(c0:symbol)
.decl objlit_ctx_ref(c0:symbol,c1:symbol)
.decl objlit_ctx_scope(c0:symbol,c1:symbol)
+.decl objlit_this_type(c0:symbol,c1:symbol,c2:symbol)
.decl objlit_prop(c0:symbol,c1:symbol,c2:symbol)
.decl objlit_member_fn(c0:symbol,c1:symbol,c2:symbol)
.decl fn_written_as_sig(c0:symbol,c1:symbol)
@@ -393,6 +394,8 @@
.decl method_signature_role_of(c0:symbol,c1:symbol)
.decl method_this_param(c0:symbol)
.decl this_expr(c0:symbol)
+.decl thistype_arg(c0:symbol,c1:symbol)
+.decl thistype_carrier_kind(c0:symbol)
.decl method_declared_this(c0:symbol,c1:symbol)
.decl module_ambient_specifier(c0:symbol,c1:symbol,c2:symbol)
.decl module_decl(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol)
@@ -556,6 +559,7 @@
.decl return_is_type_param(c0:symbol,c1:symbol)
.decl call_type_argument(c0:symbol,c1:symbol,c2:symbol)
.decl call_tvar_arg_binding(c0:symbol,c1:symbol,c2:symbol,c3:symbol)
+.decl callee_holds_class(c0:symbol)
.decl array_of_type_param(c0:symbol,c1:symbol)
.decl param_array_wrapper_kind(c0:symbol)
.decl return_type_variable(c0:symbol,c1:symbol)
From 4765b2a6a48f91983f2b483ec28297a38823077a Mon Sep 17 00:00:00 2001
From: Swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com>
Date: Thu, 29 Oct 2026 02:02:12 -0700
Subject: [PATCH 03/11] release 0.2.3: every package and plugin manifest at
0.2.3 (the parser keeps its own version)
0.2.2 is released; this PR ships in 0.2.3. package.json (version and the five @axiomcode/engine-* pins) and the four editor plugin and extension manifests move from 0.2.2 to 0.2.3. The parser is unchanged by this PR and stays at 0.2.1.
---
gemini-extension.json | 2 +-
package.json | 12 ++++++------
plugins/axiomcode/.claude-plugin/plugin.json | 2 +-
plugins/axiomcode/.codex-plugin/plugin.json | 2 +-
plugins/axiomcode/.cursor-plugin/plugin.json | 2 +-
5 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/gemini-extension.json b/gemini-extension.json
index ae6411e45..61a14197f 100644
--- a/gemini-extension.json
+++ b/gemini-extension.json
@@ -1,6 +1,6 @@
{
"name": "axiomcode",
- "version": "0.2.2",
+ "version": "0.2.3",
"description": "Ask your repository how its code connects: who calls this, what breaks if I change it, which tests an edit reaches, how A reaches B. Answers come from a resolved call graph and are verified against it; nothing is guessed.",
"contextFileName": "plugins/axiomcode/AGENTS.md",
"mcpServers": {
diff --git a/package.json b/package.json
index 0d7c265c3..c1fd43e95 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@axiomcode/code-graph",
- "version": "0.2.2",
+ "version": "0.2.3",
"description": "AxiomCode Graph: a resolved call graph of your codebase grounded in formal methods, so you and your coding agents can see who calls what, what a change breaks, and which tests it reaches.",
"repository": {
"type": "git",
@@ -31,11 +31,11 @@
],
"license": "FSL-1.1-Apache-2.0",
"optionalDependencies": {
- "@axiomcode/engine-darwin-arm64": "0.2.2",
- "@axiomcode/engine-darwin-x64": "0.2.2",
- "@axiomcode/engine-linux-x64": "0.2.2",
- "@axiomcode/engine-linux-arm64": "0.2.2",
- "@axiomcode/engine-win32-x64": "0.2.2"
+ "@axiomcode/engine-darwin-arm64": "0.2.3",
+ "@axiomcode/engine-darwin-x64": "0.2.3",
+ "@axiomcode/engine-linux-x64": "0.2.3",
+ "@axiomcode/engine-linux-arm64": "0.2.3",
+ "@axiomcode/engine-win32-x64": "0.2.3"
},
"bin": {
"axiomcode": "bin/axiomcode.js"
diff --git a/plugins/axiomcode/.claude-plugin/plugin.json b/plugins/axiomcode/.claude-plugin/plugin.json
index 6e2eae13a..a45810eee 100644
--- a/plugins/axiomcode/.claude-plugin/plugin.json
+++ b/plugins/axiomcode/.claude-plugin/plugin.json
@@ -1,7 +1,7 @@
{
"name": "axiomcode",
"description": "Ask your repository how its code connects: who calls this, what breaks if I change it, which tests an edit reaches, how A reaches B. Answers come from a resolved call graph and are verified against it; nothing is guessed.",
- "version": "0.2.2",
+ "version": "0.2.3",
"author": {
"name": "AxiomCode"
}
diff --git a/plugins/axiomcode/.codex-plugin/plugin.json b/plugins/axiomcode/.codex-plugin/plugin.json
index 0348a6a37..29c2ba521 100644
--- a/plugins/axiomcode/.codex-plugin/plugin.json
+++ b/plugins/axiomcode/.codex-plugin/plugin.json
@@ -1,6 +1,6 @@
{
"name": "axiomcode",
- "version": "0.2.2",
+ "version": "0.2.3",
"description": "Ask your repository how its code connects: who calls this, what breaks if I change it, which tests an edit reaches, how A reaches B. Answers come from a resolved call graph and are verified against it; nothing is guessed.",
"author": { "name": "AxiomCode", "url": "https://github.com/AxiomCodeAI/axiomcodegraph" },
"homepage": "https://github.com/AxiomCodeAI/axiomcodegraph",
diff --git a/plugins/axiomcode/.cursor-plugin/plugin.json b/plugins/axiomcode/.cursor-plugin/plugin.json
index 74ce345ef..fadd636d0 100644
--- a/plugins/axiomcode/.cursor-plugin/plugin.json
+++ b/plugins/axiomcode/.cursor-plugin/plugin.json
@@ -1,6 +1,6 @@
{
"name": "axiomcode",
- "version": "0.2.2",
+ "version": "0.2.3",
"description": "Ask your repository how its code connects: who calls this, what breaks if I change it, which tests an edit reaches, how A reaches B. Answers come from a resolved call graph and are verified against it; nothing is guessed. Java, TypeScript, Python, JavaScript.",
"author": { "name": "AxiomCode" },
"homepage": "https://github.com/AxiomCodeAI/axiomcodegraph",
From 7ca1a4a9d6953e8e671911b114c7d069268d8f19 Mon Sep 17 00:00:00 2001
From: Swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com>
Date: Thu, 29 Oct 2026 02:02:12 -0700
Subject: [PATCH 04/11] release 0.2.3: every package and plugin manifest at
0.2.3 (the parser keeps its own version)
0.2.2 is released, so what merges into this branch ships in 0.2.3. package.json (version and the five @axiomcode/engine-* pins) and the four editor plugin and extension manifests move from 0.2.2 to 0.2.3. The parser keeps its own version, 0.2.1.
---
gemini-extension.json | 2 +-
package.json | 12 ++++++------
plugins/axiomcode/.claude-plugin/plugin.json | 2 +-
plugins/axiomcode/.codex-plugin/plugin.json | 2 +-
plugins/axiomcode/.cursor-plugin/plugin.json | 2 +-
5 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/gemini-extension.json b/gemini-extension.json
index ae6411e45..61a14197f 100644
--- a/gemini-extension.json
+++ b/gemini-extension.json
@@ -1,6 +1,6 @@
{
"name": "axiomcode",
- "version": "0.2.2",
+ "version": "0.2.3",
"description": "Ask your repository how its code connects: who calls this, what breaks if I change it, which tests an edit reaches, how A reaches B. Answers come from a resolved call graph and are verified against it; nothing is guessed.",
"contextFileName": "plugins/axiomcode/AGENTS.md",
"mcpServers": {
diff --git a/package.json b/package.json
index 0d7c265c3..c1fd43e95 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@axiomcode/code-graph",
- "version": "0.2.2",
+ "version": "0.2.3",
"description": "AxiomCode Graph: a resolved call graph of your codebase grounded in formal methods, so you and your coding agents can see who calls what, what a change breaks, and which tests it reaches.",
"repository": {
"type": "git",
@@ -31,11 +31,11 @@
],
"license": "FSL-1.1-Apache-2.0",
"optionalDependencies": {
- "@axiomcode/engine-darwin-arm64": "0.2.2",
- "@axiomcode/engine-darwin-x64": "0.2.2",
- "@axiomcode/engine-linux-x64": "0.2.2",
- "@axiomcode/engine-linux-arm64": "0.2.2",
- "@axiomcode/engine-win32-x64": "0.2.2"
+ "@axiomcode/engine-darwin-arm64": "0.2.3",
+ "@axiomcode/engine-darwin-x64": "0.2.3",
+ "@axiomcode/engine-linux-x64": "0.2.3",
+ "@axiomcode/engine-linux-arm64": "0.2.3",
+ "@axiomcode/engine-win32-x64": "0.2.3"
},
"bin": {
"axiomcode": "bin/axiomcode.js"
diff --git a/plugins/axiomcode/.claude-plugin/plugin.json b/plugins/axiomcode/.claude-plugin/plugin.json
index 6e2eae13a..a45810eee 100644
--- a/plugins/axiomcode/.claude-plugin/plugin.json
+++ b/plugins/axiomcode/.claude-plugin/plugin.json
@@ -1,7 +1,7 @@
{
"name": "axiomcode",
"description": "Ask your repository how its code connects: who calls this, what breaks if I change it, which tests an edit reaches, how A reaches B. Answers come from a resolved call graph and are verified against it; nothing is guessed.",
- "version": "0.2.2",
+ "version": "0.2.3",
"author": {
"name": "AxiomCode"
}
diff --git a/plugins/axiomcode/.codex-plugin/plugin.json b/plugins/axiomcode/.codex-plugin/plugin.json
index 0348a6a37..29c2ba521 100644
--- a/plugins/axiomcode/.codex-plugin/plugin.json
+++ b/plugins/axiomcode/.codex-plugin/plugin.json
@@ -1,6 +1,6 @@
{
"name": "axiomcode",
- "version": "0.2.2",
+ "version": "0.2.3",
"description": "Ask your repository how its code connects: who calls this, what breaks if I change it, which tests an edit reaches, how A reaches B. Answers come from a resolved call graph and are verified against it; nothing is guessed.",
"author": { "name": "AxiomCode", "url": "https://github.com/AxiomCodeAI/axiomcodegraph" },
"homepage": "https://github.com/AxiomCodeAI/axiomcodegraph",
diff --git a/plugins/axiomcode/.cursor-plugin/plugin.json b/plugins/axiomcode/.cursor-plugin/plugin.json
index 74ce345ef..fadd636d0 100644
--- a/plugins/axiomcode/.cursor-plugin/plugin.json
+++ b/plugins/axiomcode/.cursor-plugin/plugin.json
@@ -1,6 +1,6 @@
{
"name": "axiomcode",
- "version": "0.2.2",
+ "version": "0.2.3",
"description": "Ask your repository how its code connects: who calls this, what breaks if I change it, which tests an edit reaches, how A reaches B. Answers come from a resolved call graph and are verified against it; nothing is guessed. Java, TypeScript, Python, JavaScript.",
"author": { "name": "AxiomCode" },
"homepage": "https://github.com/AxiomCodeAI/axiomcodegraph",
From 14eaf4c816115d28ea6ae5a53e611c814650f19d Mon Sep 17 00:00:00 2001
From: G6KlayWang
Date: Fri, 30 Oct 2026 02:20:49 -0400
Subject: [PATCH 05/11] mcp: an execution failure is a tool error (isError),
not an answer (#2006)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
A call the CLI could not carry out — bash missing, the verb crashed or was killed, an exit no verb
uses, silence, a timeout with no build running, a refusal (exit 2) — came back as an ordinary result
on both server paths; only schema-invalid arguments were flagged. run() now raises the server's own
ToolError with the same words, which the SDK and the fallback both mark isError, so the model still
reads the correction. An answer, path's "no chain" (exit 1 in words) and the note that a graph is
still being built (exit 2 while the build lock is held) stay ordinary results.
AXIOMCODE_MCP_TIMEOUT (seconds) exists so a test can see the timeout. tests/mcp.py checks nine
cases per launch mode, on the fallback and, with AXIOMCODE_SDK_PYTHON, on the SDK.
---
plugins/axiomcode/mcp/_fallback.py | 11 +++-
plugins/axiomcode/mcp/server.py | 82 +++++++++++++++++------
tests/corrupt_graph.py | 7 +-
tests/latency.py | 7 +-
tests/mcp.py | 102 +++++++++++++++++++++++++++++
5 files changed, 183 insertions(+), 26 deletions(-)
diff --git a/plugins/axiomcode/mcp/_fallback.py b/plugins/axiomcode/mcp/_fallback.py
index 8eb322681..a00b95b61 100644
--- a/plugins/axiomcode/mcp/_fallback.py
+++ b/plugins/axiomcode/mcp/_fallback.py
@@ -18,7 +18,14 @@
"""
import inspect, json, sys, typing
-__all__ = ["MCPServer"]
+__all__ = ["MCPServer", "ToolError"]
+
+
+class ToolError(Exception):
+ """A failure the tool anticipated and put into words: the words are the result's text and the result is marked
+ isError, as the SDK's own ToolError is. Any other exception is marked the same way, but with its type in front,
+ because its words were never meant for the caller."""
+
_JSON_TYPE = {str: "string", int: "integer", float: "number", bool: "boolean", list: "array", dict: "object"}
@@ -132,6 +139,8 @@ def _handle(self, msg):
try:
out = t["fn"](**args)
return {"content": [{"type": "text", "text": "" if out is None else str(out)}]}
+ except ToolError as e:
+ return {"content": [{"type": "text", "text": str(e)}], "isError": True}
except Exception as e:
return {"content": [{"type": "text", "text": f"{type(e).__name__}: {e}"}], "isError": True}
if m == "ping":
diff --git a/plugins/axiomcode/mcp/server.py b/plugins/axiomcode/mcp/server.py
index b1ebc5e62..cf1c3bda4 100755
--- a/plugins/axiomcode/mcp/server.py
+++ b/plugins/axiomcode/mcp/server.py
@@ -126,11 +126,17 @@ def unknown_arguments(name, arguments):
said = [f"{k}: unexpected argument" for k in extra]
return f"invalid arguments for {name}: " + '; '.join(said) + f". {name} takes: {', '.join(params) or 'no arguments'}"
-try:
- import importlib
- ToolError = importlib.import_module(MCPServer.__module__.rsplit('.', 1)[0] + '.exceptions').ToolError
-except Exception:
- ToolError = ValueError
+# A FAILURE IS RAISED, NOT RETURNED. A client marks a result isError only when the tool raised, and only the server's
+# own ToolError keeps the words: the SDK (mcp ≥ 2.3) replaces any other exception's message with a bare "Error
+# executing tool", and the fallback puts the type in front. So run() raises this one class, from whichever half serves.
+if MCPServer.__module__ == '_fallback':
+ from _fallback import ToolError
+else:
+ try:
+ import importlib
+ ToolError = importlib.import_module(MCPServer.__module__.rsplit('.', 1)[0] + '.exceptions').ToolError
+ except Exception:
+ ToolError = ValueError # an SDK with no exceptions module: the result is still isError, wordless
class Server(MCPServer):
def tool(self, *a, **k):
@@ -152,6 +158,9 @@ async def call_tool(self, name, arguments, *a, **k): # the SDK: refused as a
# launch.js hands over the bash it chose, because on Windows a bare `bash` is WSL's or nothing (#1233).
BASH = os.environ.get('AXIOMCODE_BASH') or 'bash'
+# how long a call may take before it is given up as a failure (seconds; a first build is minutes, so the default is
+# generous); AXIOMCODE_MCP_TIMEOUT exists for a test that must see the timeout in seconds, not in minutes
+TIMEOUT = float(os.environ.get('AXIOMCODE_MCP_TIMEOUT') or 900)
# THE TIMER (#1305). The hooks refresh the graph while an agent works; an edit made in an editor or a terminal while the
# session sits idle is caught only at the next prompt. The server lives as long as the session, so once
@@ -190,12 +199,36 @@ def _timer(interval):
if time.time() - fresh.last_update(repo) >= interval: fresh.hook_kick(repo, 'the timer')
except Exception: pass
-def run(args, cwd=None, timeout=900):
+def repository(args, cwd=None):
+ """the repository a call asks about: the last positional argument that is a directory (a flag's value, `--in `,
+ is not one), else the working directory"""
+ pos = [a for i, a in enumerate(args[1:], 1) if args[i - 1] not in VALUED]
+ return next((a for a in reversed(pos) if os.path.isdir(a)), cwd or os.getcwd())
+
+# WHAT A FAILURE IS. The CLI's exits: 0 is an answer, however empty; 1 is `path` saying in words that no chain connects
+# the two; 2 is a refusal with its correction (nothing named X and the nearest names, no graph, no such directory),
+# and ALSO the note that the graph is still being built. A refusal is a failure of the call, as an argument the tool
+# does not take already is (#1567): the client marks the result isError and the model still reads the words. The
+# building note is not: it is told apart by the build lock, which a build holds for as long as it runs. A crash, a
+# kill, an exit no verb uses, or silence is a failure whatever the code says.
+def failed(r, repo):
+ """whether a finished call is a failure the client must see as one, not an answer"""
+ if r.returncode == 0: return False
+ if r.returncode < 0 or r.returncode not in (1, 2): return True
+ if 'Traceback (most recent call last)' in (r.stderr or ''): return True
+ if r.returncode == 1: return not ((r.stdout or '').strip() or (r.stderr or '').strip())
+ try: return not scripts_module('ax_fresh').building(os.path.realpath(repo))
+ except Exception: return True
+
+def run(args, cwd=None, timeout=None):
+ """the CLI's words for a call; a call that failed (failed, a bash that will not start, a timeout with no build
+ running) raises ToolError with the words, so the client marks the result isError and the model still reads them"""
+ if timeout is None: timeout = TIMEOUT
for a in args[1:]:
if os.path.isdir(a) and os.path.isdir(os.path.join(a, '.axiomcode')): SEEN.add(os.path.realpath(a))
# A QUERY ON A REPOSITORY WITH NO GRAPH YET does not hold the call for the length of a first build (minutes; past this
- # server's timeout it came back as a bare "Error executing tool"): the build is started in the background and the
- # answer says what stage it is at (ax_contract.ensure_graph). `index` is asked for a build and still waits for it.
+ # server's timeout it is a failure saying only that): the build is started in the background and the answer says
+ # what stage it is at (ax_contract.ensure_graph). `index` is asked for a build and still waits for it.
env = dict(os.environ, AXIOMCODE_BUILD_NOWAIT='1', AXIOMCODE_SURFACE='mcp') if args and args[0] != 'index' else None
root = plugin_root()
ax = os.path.join(root, 'skills', 'axiomcode', 'scripts', 'axiomcode')
@@ -204,6 +237,7 @@ def run(args, cwd=None, timeout=900):
if o: return f"axiomcode: {o}."
note = stale_note(root)
head = (note + '\n') if note else ''
+ repo = repository(args, cwd)
try:
# On Windows the arguments travel in AXIOMCODE_ARGV, not on the command line: Git Bash parses a command line by
# msys rules (`'` quotes, `\"` is no escape), so a question holding an apostrophe lost every argument after it.
@@ -213,23 +247,22 @@ def run(args, cwd=None, timeout=900):
argv, env = [BASH, ax], dict(env or os.environ, AXIOMCODE_ARGV=''.join(f'{a}\x1f' for a in args))
r = subprocess.run(argv, cwd=cwd or None, capture_output=True, text=True, timeout=timeout, env=env)
except OSError as e:
- return (f"axiomcode could not start bash ({BASH}): {e}. On Windows it needs the bash that comes with "
- "Git for Windows; install it, or set AXIOMCODE_BASH to its bin\\bash.exe.")
+ raise ToolError(f"axiomcode could not start bash ({BASH}): {e}. On Windows it needs the bash that comes with "
+ "Git for Windows; install it, or set AXIOMCODE_BASH to its bin\\bash.exe.") from None
except subprocess.TimeoutExpired:
- # a flag's value (`--in `) is not the repository
- pos = [a for i, a in enumerate(args[1:], 1) if args[i - 1] not in VALUED]
- repo = next((a for a in reversed(pos) if os.path.isdir(a)), cwd or os.getcwd())
try:
ax_fresh = scripts_module('ax_fresh'); ax_contract = scripts_module('ax_contract')
if ax_fresh.building(os.path.realpath(repo)): return head + ax_contract.building_note(os.path.realpath(repo))
except Exception: pass
- return head + (f"axiomcode {args[0] if args else ''} did not answer within {timeout} s. Nothing was changed; "
- f"see {os.path.join(repo, '.axiomcode', 'build.log')} if a build was running, and ask again.")
+ raise ToolError(head + f"axiomcode {args[0] if args else ''} did not answer within {timeout:g} s. Nothing was changed; "
+ f"see {os.path.join(repo, '.axiomcode', 'build.log')} if a build was running, and ask again.") from None
out = (r.stdout or '') + (('\n' + r.stderr.strip()) if r.returncode and r.stderr.strip() else '')
# an answer given from a graph that predates some edit says so, and names the files (#1305); one given from a graph a
# fallback engine built, in place of the checkout's own, names that engine
if not r.returncode: out += ''.join('\n' + l for l in (r.stderr or '').splitlines() if l.startswith(('graph refresh:', 'graph built by:')))
- return head + (out.strip() or f"(no output, exit {r.returncode})")
+ text = head + (out.strip() or f"(no output, exit {r.returncode})")
+ if failed(r, repo): raise ToolError(text + (f"\n(exit {r.returncode})" if out.strip() else ''))
+ return text
# THE TOOLS TAKE NO OPTIONS, so an answer never tells the agent to pass one. The notes the verbs add (a stale
# graph, a refresh in flight) are kept for what they say; a clause that names a flag or a parameter to set is dropped.
@@ -249,6 +282,13 @@ def plain(text):
if keep and ''.join(keep).strip(): out.append(' '.join(keep).rstrip(' ;,'))
return '\n'.join(out)
+def answer(args):
+ """a tool's answer: the CLI's words with the option-naming clauses dropped. A failure run() raises goes the same
+ way — its words through plain(), then on as the ToolError the client marks isError — so the model reads the
+ correction (the nearest names, the fix) either way, and a flag it cannot pass is named in neither."""
+ try: return plain(run(args))
+ except ToolError as e: raise ToolError(plain(str(e))) from None
+
# THE SMALL SURFACE. Search is grep's job; the graph answers what grep cannot. Three questions, each answered as
# numbered places with the code of the function each sits in, so a place is understood without opening its file,
@@ -259,20 +299,20 @@ def context(task: str, source: bool = False) -> str:
"""How something works, from a task in words: the files and callables the task touches, and for a "how does X
work" question the call FLOW — every step in the order the calls are written, with ⚠ where the graph lost a
call. source=True asks for the flow with each step's code, so it is read without opening files."""
- return plain(run(['context', task] + (['--source'] if source else []) + [os.getcwd()]))
+ return answer(['context', task] + (['--source'] if source else []) + [os.getcwd()])
@srv.tool()
def impact(name: str = '') -> str:
"""What a change reaches. With a name (as written in the code: Owner.method, function, Type, or file.py:123): who
calls it, what depends on it further out, and which tests exercise it, each with its code. With no name: the same
for the declarations your uncommitted edits changed."""
- return plain(run(['impact'] + ([name] if name.strip() else []) + [os.getcwd()]))
+ return answer(['impact'] + ([name] if name.strip() else []) + [os.getcwd()])
@srv.tool()
def path(start: str, end: str) -> str:
"""How one declaration reaches another: every hop of the call chain with the code at the line the call is
written on. start / end as written in the code (Owner.method, function, Type)."""
- return plain(run(['path', start, end, os.getcwd()]))
+ return answer(['path', start, end, os.getcwd()])
@srv.tool()
def link(site: str = '', target: str = '') -> str:
@@ -282,12 +322,12 @@ def link(site: str = '', target: str = '') -> str:
whether the graph took it (a link whose line changed is dropped, never trusted). target "-" removes the site's links;
target "not:" rejects a lead (a by-name or one-of-a-set guess) at that site, which is then not walked.
Never link a guess, and never link a candidate for its rank alone."""
- return plain(run(['link'] + ([site] if site.strip() else []) + ([target] if site.strip() and target.strip() else []) + [os.getcwd()]))
+ return answer(['link'] + ([site] if site.strip() else []) + ([target] if site.strip() and target.strip() else []) + [os.getcwd()])
@srv.tool()
def tests() -> str:
"""The tests your uncommitted edits reach, each with its code, and the command that runs exactly those."""
- return plain(run(['tests', os.getcwd()]))
+ return answer(['tests', os.getcwd()])
if __name__ == '__main__':
# catch up on whatever changed while no session was running (#1305): started, never waited on
diff --git a/tests/corrupt_graph.py b/tests/corrupt_graph.py
index b999e00d1..a37cd2e04 100644
--- a/tests/corrupt_graph.py
+++ b/tests/corrupt_graph.py
@@ -121,13 +121,16 @@ def check(ok, why, detail=''):
open(os.path.realpath(ptr), 'wb').write(os.urandom(8192))
sys.path.insert(0, os.path.join(ROOT, 'plugins', 'axiomcode', 'mcp')); os.environ.update(AXIOMCODE_ENGINE=ROOT, AXIOMCODE_NO_REFRESH='1')
import server
- text = server.run(['impact', 'helper', repo])
+ def words(args): # a refusal comes as the server's ToolError, words intact
+ try: return server.run(args)
+ except server.ToolError as e: return str(e)
+ text = words(['impact', 'helper', repo])
check('Traceback' not in text and 'corrupt' in text, 'mcp: the tool answer says the graph is corrupt, no traceback', text)
sys.path.insert(0, SCRIPTS); import ax_fresh
for _ in range(600): # the rebuild it started
if os.path.exists(ptr) and not ax_fresh.building(repo): break
time.sleep(1)
- text = server.run(['impact', 'helper', repo])
+ text = words(['impact', 'helper', repo])
check('caller' in text and 'corrupt' not in text, 'mcp: once rebuilt the tool answers again', text)
# ── bad page ────────────────────────────────────────────────────────────────────────────────────────
diff --git a/tests/latency.py b/tests/latency.py
index ad4064985..a908df6ef 100644
--- a/tests/latency.py
+++ b/tests/latency.py
@@ -313,9 +313,12 @@ def timeout_checks():
with contextlib.redirect_stderr(io.StringIO()): spec.loader.exec_module(m)
work = tempfile.mkdtemp(prefix='axiomcode-timeout-')
try:
- try: out = m.run(['context', 'anything', work], timeout=0.01); raised = ''
+ # the words come as the server's own ToolError, which the client marks isError; a bare exception would lose them,
+ # and a plain return would pass the failure off as an answer
+ try: out = m.run(['context', 'anything', work], timeout=0.01); raised = 'returned as an answer'
+ except m.ToolError as e: out, raised = str(e), ''
except Exception as e: out, raised = '', repr(e)
- check("timeout: an MCP call past the server's timeout answers in words, not with an exception",
+ check("timeout: an MCP call past the server's timeout fails in words that say so, as a tool error",
not raised and 'did not answer within' in out, raised or out)
finally:
shutil.rmtree(work, ignore_errors=True)
diff --git a/tests/mcp.py b/tests/mcp.py
index f85b37d37..1b4311dd6 100644
--- a/tests/mcp.py
+++ b/tests/mcp.py
@@ -190,11 +190,96 @@ def text(self, name, arguments):
return ''.join(c.get('text', '') for c in self.ask('tools/call', {'name': name, 'arguments': arguments}).get('content', []))
def close(self):
+ """ends the server; returns what it wrote on stderr, which says which half served (the fallback's banner)"""
self.timer.cancel()
self.p.stdin.close()
self.p.wait()
+ err = self.p.stderr.read()
self.p.stdout.close()
self.p.stderr.close()
+ return err
+
+
+FALLBACK_BANNER = 'serving with the built-in fallback'
+# holds a repository's build lock the way a build does (ax_fresh._flock: LOCK_EX on .axiomcode/build.lock), from a
+# process of its own, and says so once it has it
+HOLD = ("import fcntl, os, sys, time; fd = os.open(sys.argv[1], os.O_RDWR | os.O_CREAT, 0o644); "
+ "fcntl.flock(fd, fcntl.LOCK_EX); print('held', flush=True); time.sleep(120)")
+
+
+def check_failures(label, cmd, cwd, env=None, workdir=None, sdk=None):
+ """A call the CLI could not carry out is a tool error (isError), never an answer: a bash that is not there, a verb
+ that crashed or was killed, an exit no verb uses, silence, a timeout with no build running, and a refusal (nothing
+ named X, no graph: exit 2, as an argument the tool lacks already is, #1567). The controls are what must NOT be an
+ error: an answer, an empty one, `path` saying "no chain" (exit 1, in words), and the note that a graph is still
+ being built (exit 2 while the build lock is held). The CLI is a fake bash that says what each case needs, so what is
+ tested is the server's own reading of it; the words must survive, and the SDK puts a prefix on them, so substrings.
+ `sdk` True or False asserts which half served the mode, read off the stderr banner."""
+ if os.name == 'nt':
+ return []
+ bad = []
+ here = workdir or cwd # the server's cwd, which is the repository every tool asks about
+ fakes = tempfile.mkdtemp(prefix='ax-bash-')
+ base = {k: v for k, v in (env or os.environ).items() if k != 'AXIOMCODE_BASH'}
+ base.update(AXIOMCODE_MCP_TIMEOUT='2', AXIOMCODE_REFRESH_INTERVAL='0')
+
+ def fake(name, body):
+ p = os.path.join(fakes, name)
+ with open(p, 'w') as f:
+ f.write('#!/bin/sh\n' + body + '\n')
+ os.chmod(p, 0o755)
+ return p
+ crash = r"""printf 'Traceback (most recent call last):\n File "x.py", line 1, in \nValueError: bad\n' >&2; exit 1"""
+ note = 'the graph for $PWD is being built (starting; 0m01s so far). Nothing else is needed: ask again in a minute or two'
+ cases = [('missing', os.path.join(fakes, 'no-bash'), True, ['could not start']),
+ ('boom', fake('boom', "printf 'boom\\n' >&2; exit 7"), True, ['boom', 'exit 7']),
+ ('slow', fake('slow', 'exec sleep 30'), True, ['did not answer within']), # exec: the kill reaches the sleeper
+ ('crash', fake('crash', crash), True, ['Traceback']),
+ ('no chain', fake('nochain', 'echo "no chain of resolved calls connects a and b in either direction"; exit 1'), False, ['no chain']),
+ ('empty ok', fake('empty', 'exit 0'), False, ['(no output, exit 0)']),
+ ('building', fake('building', f'echo "{note}"; exit 2'), False, ['being built']),
+ ('refusal', fake('refusal', 'echo "nothing named zz is declared in this graph. Exact names that are close: a, b"; exit 2'), True, ['close:', 'exit 2']),
+ ('silent 1', fake('silent', 'exit 1'), True, ['(no output, exit 1)'])]
+ served = None
+ try:
+ for name, bash, want, needles in cases:
+ s = Session(cmd, here, dict(base, AXIOMCODE_BASH=bash))
+ holder = None
+ try:
+ if name == 'building': # after the start, so the server's own start-up sees no .axiomcode
+ os.makedirs(os.path.join(here, '.axiomcode'), exist_ok=True)
+ holder = subprocess.Popen([sys.executable, '-c', HOLD, os.path.join(here, '.axiomcode', 'build.lock')],
+ stdout=subprocess.PIPE, text=True)
+ holder.stdout.readline()
+ res = s.ask('tools/call', {'name': 'path', 'arguments': {'start': 'a', 'end': 'b'}})
+ finally:
+ if holder:
+ holder.kill(); holder.wait(); holder.stdout.close()
+ shutil.rmtree(os.path.join(here, '.axiomcode'), ignore_errors=True)
+ err = s.close()
+ if served is None:
+ served = 'the fallback' if FALLBACK_BANNER in err else 'the SDK'
+ text = ' '.join(c.get('text', '') for c in res.get('content', []))
+ if bool(res.get('isError')) != want:
+ bad.append(f"{label}, {name}: isError {res.get('isError')!r}, want {want}: {text[:300]!r}")
+ for n in needles:
+ if n not in text:
+ bad.append(f"{label}, {name}: the words lack {n!r}: {text[:300]!r}")
+ finally:
+ shutil.rmtree(fakes, ignore_errors=True)
+ print(f"{label}: failures checked, served by {served}")
+ if sdk is not None and served != ('the SDK' if sdk else 'the fallback'):
+ bad.append(f"{label}: served by {served}, but this mode was meant to test {'the SDK' if sdk else 'the fallback'}")
+ return bad
+
+
+def sdk_python():
+ """an interpreter with the MCP SDK, so the failure checks run on that half too: AXIOMCODE_SDK_PYTHON, else this
+ one; None when neither imports it"""
+ for py in filter(None, (os.environ.get('AXIOMCODE_SDK_PYTHON'), sys.executable)):
+ if subprocess.run([py, '-c', 'import mcp.server.mcpserver'], capture_output=True).returncode == 0:
+ return py
+ return None
def check_install_move(work):
@@ -309,6 +394,7 @@ def main():
bad += check('bin/axiomcode mcp', ['bash', CLI, 'mcp'], repo)
# the SDK when the launcher finds one, which ignored an argument it did not know (#1567); else the fallback again
bad += check_arguments('bin/axiomcode mcp', ['bash', CLI, 'mcp'], repo, lax=True)
+ bad += check_failures('bin/axiomcode mcp', ['bash', CLI, 'mcp'], repo)
bad += check_front_door()
if os.name != 'nt':
bad += check_install_move(work)
@@ -316,6 +402,15 @@ def main():
env_note = 'python3 -S server.py (fallback, no SDK)'
bad += check(env_note, [sys.executable, '-S', SERVER], repo)
bad += check_arguments(env_note, [sys.executable, '-S', SERVER], repo)
+ bad += check_failures(env_note, [sys.executable, '-S', SERVER], repo, sdk=False)
+ # the same failures through the SDK, when an interpreter has it (a venv with `pip install mcp`, named by
+ # AXIOMCODE_SDK_PYTHON); the skip is said out loud, so a run without the SDK is not mistaken for one with it
+ py = sdk_python()
+ if py:
+ bad += check_failures('server.py (SDK)', [py, SERVER], repo, sdk=True)
+ else:
+ print('SDK path skipped: mcp not importable (python3 -m venv && /bin/pip install mcp && '
+ 'AXIOMCODE_SDK_PYTHON=/bin/python python3 tests/mcp.py)')
# The plugin's own server entries, run as their hosts run them, from a copy of the plugin under a path
# with a space, which splits an unquoted path into two words.
@@ -329,6 +424,8 @@ def main():
server = json.load(f)['mcpServers']['axiomcode']
bad += check('.codex-plugin/mcp.json', [server['command'], *server['args']], repo, base,
os.path.join(plugin, server.get('cwd', '.')))
+ bad += check_failures('.codex-plugin/mcp.json', [server['command'], *server['args']], repo, base,
+ os.path.join(plugin, server.get('cwd', '.')))
# Cursor's own manifest names the server with ${CURSOR_PLUGIN_ROOT}, which Cursor replaces with the
# plugin directory before it starts the command, and starts it in the user's project.
@@ -338,6 +435,7 @@ def main():
cmd = [server['command'], *map(expand, server['args'])]
env = dict(base, **{k: expand(v) for k, v in server.get('env', {}).items()})
bad += check('.cursor-plugin/plugin.json', cmd, repo, env, repo)
+ bad += check_failures('.cursor-plugin/plugin.json', cmd, repo, env, repo)
with open(os.path.join(plugin, '.mcp.json')) as f:
server = json.load(f)['mcpServers']['axiomcode']
@@ -345,6 +443,10 @@ def main():
cmd = [server['command'], *map(expand, server['args'])]
env = dict(base, **{k: expand(v) for k, v in server.get('env', {}).items()})
bad += check('.mcp.json', cmd, repo, env, repo)
+ bad += check_failures('.mcp.json', cmd, repo, env, repo)
+ if py: # Claude Code's entry with the SDK's interpreter first in the launcher's search
+ sdk_env = dict(env, AXIOMCODE_PYTHON=py, PATH=os.path.dirname(py) + os.pathsep + env.get('PATH', ''))
+ bad += check_failures('.mcp.json (SDK python)', cmd, repo, sdk_env, repo, sdk=True)
# A uv on PATH that cannot build its environment is passed over for the fallback, rather than started
# and left to exit before `initialize` (#1249). python3 and python are shadowed by interpreters without
From 2bb88485ebd2019f7b0b3d5b1adbefbb78387098 Mon Sep 17 00:00:00 2001
From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com>
Date: Sun, 1 Nov 2026 21:29:21 -0800
Subject: [PATCH 06/11] link: key links on the target's declaration, not its
name
Two links from one call site to two anonymous functions collapsed into one:
every arrow function and lambda shares a placeholder name (``,
``, `Owner.`), and adding a link replaced any row at the same
site with the same target name, ignoring the file:line that tells them apart.
- add: a row is replaced only when it names the same declaration (same name
and target_file, or resolving to the same declaration in the graph), so
re-linking stays idempotent and a second anonymous target is a second link
- --remove : may be a declaration's file:line; a name
that covers links to more than one declaration is refused with each one's
file:line instead of removing them all
- link output and --list print a placeholder target with its declaration
Cases for TypeScript arrows, Python lambdas and C# lambdas, with controls:
idempotent re-link, two named targets at one site, remove by name.
Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
---
.../skills/axiomcode/scripts/ax_links.py | 76 ++++++-
.../skills/axiomcode/scripts/axiomcode-link | 2 +
.../asserted-links-anonymous/App/Bus.cs | 23 +++
.../asserted-links-anonymous/App/Wire.cs | 18 ++
.../csharp/asserted-links-anonymous/case.json | 190 +++++++++++++++++
.../asserted-links-anonymous/app/__init__.py | 0
.../asserted-links-anonymous/app/bus.py | 10 +
.../asserted-links-anonymous/app/wire.py | 11 +
.../python/asserted-links-anonymous/case.json | 191 ++++++++++++++++++
.../asserted-links-anonymous/case.json | 191 ++++++++++++++++++
.../asserted-links-anonymous/src/bus.ts | 13 ++
.../asserted-links-anonymous/src/wire.ts | 13 ++
12 files changed, 729 insertions(+), 9 deletions(-)
create mode 100644 tests/cases/csharp/asserted-links-anonymous/App/Bus.cs
create mode 100644 tests/cases/csharp/asserted-links-anonymous/App/Wire.cs
create mode 100644 tests/cases/csharp/asserted-links-anonymous/case.json
create mode 100644 tests/cases/python/asserted-links-anonymous/app/__init__.py
create mode 100644 tests/cases/python/asserted-links-anonymous/app/bus.py
create mode 100644 tests/cases/python/asserted-links-anonymous/app/wire.py
create mode 100644 tests/cases/python/asserted-links-anonymous/case.json
create mode 100644 tests/cases/typescript/asserted-links-anonymous/case.json
create mode 100644 tests/cases/typescript/asserted-links-anonymous/src/bus.ts
create mode 100644 tests/cases/typescript/asserted-links-anonymous/src/wire.ts
diff --git a/plugins/axiomcode/skills/axiomcode/scripts/ax_links.py b/plugins/axiomcode/skills/axiomcode/scripts/ax_links.py
index 940f845a1..103edbba5 100644
--- a/plugins/axiomcode/skills/axiomcode/scripts/ax_links.py
+++ b/plugins/axiomcode/skills/axiomcode/scripts/ax_links.py
@@ -295,6 +295,24 @@ def stable_name(g, mid, display, tfile):
return display, (f"{tfile}:{ln[0][0]}" if ln else tfile)
+def same_target(a, b, g=None, mid=None):
+ """two links name ONE declaration: the same recorded name in the same recorded file (and line, when the file carries
+ one). A name alone is not a declaration: every arrow function, lambda and anonymous class shares its placeholder name
+ (``, ``), so one site linked to two of them is two links. With a graph and the id `b` resolved to, a
+ row recorded under another spelling of that declaration (the file gained a second one of its name since) is it too."""
+ if a.get('target') == b.get('target') and (a.get('target_file') or '') == (b.get('target_file') or ''): return True
+ if g is not None and mid is not None:
+ try: ts = g.targets(a.get('target') or '', a.get('target_file') or '')
+ except sqlite3.Error: return False
+ return len(ts) == 1 and ts[0][0] == mid
+ return False
+
+
+def target_label(target, tfile):
+ """a target as printed: a placeholder name (``) says nothing alone, so it is printed with its declaration"""
+ return f"{target} ({tfile})" if tfile and re.split(r'[.:]', target or '')[-1].startswith('<') else target
+
+
def is_value_lib(g, sid):
"""a site resolved only into a library method that runs a value (Method.invoke, Function.apply)"""
rows = g.q("SELECT e.tier, s.callee_name FROM call_edges e JOIN call_sites s ON s.id = e.call_site_id WHERE e.call_site_id = ?", sid)
@@ -1165,11 +1183,45 @@ def list_links(repo, as_json=False):
print(f"{len(rows)} asserted link(s) in {os.path.relpath(links_path(repo), repo)}:")
for r in rows:
where = r['site'] + (f" (now line {r['now']})" if r.get('now') and r['site'] and int(r['site'].split(':')[1]) != r['now'] else '')
- tgt = f"NOT {r['target']} (a lead rejected: not walked)" if r.get('rejects') else r['target']
+ tl = target_label(r['target'], r.get('target_file'))
+ tgt = f"NOT {tl} (a lead rejected: not walked)" if r.get('rejects') else tl
print(f" {r['link']:>3}. [{r['status']}] {where} → {tgt}" + (f" — {r['reason']}" if r['reason'] else ''))
return 0
+def removal_targets(repo, at_site, tgt):
+ """the links at one site that `--remove ` names -> [link], or a refusal (str). is a name as the link
+ recorded it, or a declaration's file:line (the only way to tell two ``s at one site apart). A name that covers
+ links to more than one declaration removes none of them: it is refused with the file:line of each."""
+ text = [l for l in at_site if l['target'] == tgt or (l.get('target_file') or '') == tgt]
+ ids = set()
+ for db, _r, live in graph_dbs(repo):
+ if not live: continue
+ try:
+ con = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
+ try:
+ g = Graph(con)
+ want = {t[0] for t in g.targets(tgt)}
+ if not want: continue
+ gone = set()
+ for l in at_site:
+ ts = g.targets(l['target'], l.get('target_file') or '')
+ if len(ts) == 1 and ts[0][0] in want: ids.add(id(l))
+ if not ts: gone.add(id(l))
+ if len(want) == 1:
+ # the declaration it names: exactly the links that resolve to it (and a stale one recorded under that name)
+ return [l for l in at_site if id(l) in ids or (id(l) in gone and any(l is x for x in text))]
+ finally: con.close()
+ except sqlite3.Error: continue
+ hit = [l for l in at_site if any(l is x for x in text) or id(l) in ids]
+ decls = sorted({(l['target'], l.get('target_file') or '') for l in hit})
+ if len(decls) > 1:
+ return (f"`{tgt}` names {len(decls)} links at {at_site[0]['file']}:{at_site[0]['line']} "
+ f"({'; '.join(f'{t} ({tf})' if tf else t for t, tf in decls)}): name the one to remove by its declaration's "
+ f"file:line, or `-` for all of them")
+ return hit
+
+
def main(argv):
as_json = '--json' in argv
argv = [a for a in argv if a != '--json']
@@ -1192,8 +1244,11 @@ def main(argv):
links, bad = read_links(repo)
if remove or (len(args) > 1 and args[1] == '-'):
tgt = args[1] if len(args) > 1 and args[1] != '-' else None
- keep = [l for l in links if not (l['file'] == f and l['line'] == line and (tgt is None or l['target'] == tgt)
- and (col is None or l.get('col') in (None, col)))]
+ at_site = [l for l in links if l['file'] == f and l['line'] == line and (col is None or l.get('col') in (None, col))]
+ hit = at_site if tgt is None else removal_targets(repo, at_site, tgt)
+ if isinstance(hit, str):
+ print(f"axiomcode link: {hit}", file=sys.stderr); return 1
+ keep = [l for l in links if not any(l is h for h in hit)]
gone = len(links) - len(keep)
write_links(repo, keep + []) # malformed lines are dropped only by hand; they stay listed until fixed
_restore_bad(repo, bad)
@@ -1230,24 +1285,27 @@ def main(argv):
new.update(callee=r.get('callee_written') or callee, caller=g.display(r['caller']) if r.get('caller') else '',
target=r['target_display'], target_file=r.get('target_file') or '', **({'not': True} if reject else {}))
new['target'], new['target_file'] = stable_name(g, r['callee_id'], r['target_display'], new['target_file'])
- g.con.close()
# the column the link resolved to is recorded even when none was given: it is what the link names from now on
if r.get('col') and not new.get('col'): new['col'] = r['col']; new['ncol'] = norm_col(L[line - 1], r['col'])
- dup = [l for l in links if l['file'] == f and l['line'] == line and l['target'] == new['target'] and l.get('col') == new.get('col')
- and bool(l.get('not')) == bool(new.get('not'))]
+ # re-linking the same site to the same DECLARATION replaces that row; the same name declared elsewhere (two arrows
+ # handed to one registry) is another link
+ dup = [l for l in links if l['file'] == f and l['line'] == line and l.get('col') == new.get('col')
+ and bool(l.get('not')) == bool(new.get('not')) and same_target(l, new, g, r['callee_id'])]
+ g.con.close()
links = [l for l in links if l not in dup] + [new]
t0 = time.time()
write_links(repo, links); _restore_bad(repo, bad)
res = apply_repo(repo)
ms = (time.time() - t0) * 1000
note_ = next((x['reason'] for live, rs in res.values() if live for l_, x in rs
- if l_['file'] == f and l_['line'] == line and l_['target'] == new['target'] and l_.get('col') == new.get('col') and x['status'] in ('applied', 'moved')), '')
+ if l_['file'] == f and l_['line'] == line and same_target(l_, new) and l_.get('col') == new.get('col') and x['status'] in ('applied', 'moved')), '')
where = f"{f}:{line}" + (f":{new['col']}" if new.get('col') else '')
+ shown = target_label(new['target'], new['target_file'])
if reject:
- print(f"rejected the lead {where} → {new['target']}: no walk takes it from now on (the engine's row is kept; "
+ print(f"rejected the lead {where} → {shown}: no walk takes it from now on (the engine's row is kept; "
f"`axiomcode link {where} -` restores it) — applied to {sum(1 for v in res.values() if v[0])} graph(s) in {ms:.0f} ms")
return 0
- print(f"linked {where} `{new['callee']}` → {new['target']} [asserted]" + (" (the graph already had this edge; recorded, nothing added)" if r['status'] == 'redundant' else '')
+ print(f"linked {where} `{new['callee']}` → {shown} [asserted]" + (" (the graph already had this edge; recorded, nothing added)" if r['status'] == 'redundant' else '')
+ (f"; {note_}" if note_ else '') + f" — applied to {sum(1 for v in res.values() if v[0])} graph(s) in {ms:.0f} ms")
return 0
diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-link b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-link
index d8f96c6c8..d26ff9ad2 100755
--- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-link
+++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-link
@@ -2,6 +2,8 @@
"""axiomcode link [] []
axiomcode link [] every asserted link, and whether the graph took it (--json)
axiomcode link - [] remove the links at that site (or --remove [])
+ ( as a name, or its declaration's file:line: two ``s at one
+ site are two links, told apart only by where each is declared)
Records that the call written at file:line reaches — a declaration as the graph names it (Owner.method,
function) or its file:line — where the graph could not resolve it. The link is kept in axiomcode-links.tsv at the
diff --git a/tests/cases/csharp/asserted-links-anonymous/App/Bus.cs b/tests/cases/csharp/asserted-links-anonymous/App/Bus.cs
new file mode 100644
index 000000000..f836e27df
--- /dev/null
+++ b/tests/cases/csharp/asserted-links-anonymous/App/Bus.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Collections.Generic;
+
+namespace App
+{
+ public class Bus
+ {
+ private readonly List> listeners = new List>();
+
+ public void On(Action listener)
+ {
+ listeners.Add(listener);
+ }
+
+ public void Emit(string x)
+ {
+ foreach (var listener in listeners)
+ {
+ listener(x);
+ }
+ }
+ }
+}
diff --git a/tests/cases/csharp/asserted-links-anonymous/App/Wire.cs b/tests/cases/csharp/asserted-links-anonymous/App/Wire.cs
new file mode 100644
index 000000000..c02d368f6
--- /dev/null
+++ b/tests/cases/csharp/asserted-links-anonymous/App/Wire.cs
@@ -0,0 +1,18 @@
+namespace App
+{
+ public static class Wire
+ {
+ public static void Created(string x) { }
+ public static void Updated(string x) { }
+ public static void OnCreated(string x) { Created(x); }
+ public static void OnUpdated(string x) { Updated(x); }
+
+ public static void Connect(Bus bus)
+ {
+ bus.On(x => Created(x));
+ bus.On(x => Updated(x));
+ bus.On(OnCreated);
+ bus.On(OnUpdated);
+ }
+ }
+}
diff --git a/tests/cases/csharp/asserted-links-anonymous/case.json b/tests/cases/csharp/asserted-links-anonymous/case.json
new file mode 100644
index 000000000..caf1fd3ee
--- /dev/null
+++ b/tests/cases/csharp/asserted-links-anonymous/case.json
@@ -0,0 +1,190 @@
+{
+ "lang": "csharp",
+ "checks": [
+ {
+ "why": "BASE: one call site linked to an anonymous function: the link names it by its declaration, since every one of the lambdas shares the name Wire.",
+ "run": [
+ "link",
+ "App/Bus.cs:19",
+ "App/Wire.cs:12"
+ ],
+ "want": [
+ "linked App/Bus.cs:19:17 `listener` → Wire. (App/Wire.cs:12) [asserted]"
+ ]
+ },
+ {
+ "why": "a second link from the SAME site to ANOTHER of the lambdas is a second link, not a replacement of the first (both are named Wire.)",
+ "run": [
+ "link",
+ "App/Bus.cs:19",
+ "App/Wire.cs:13"
+ ],
+ "want": [
+ "linked App/Bus.cs:19:17 `listener` → Wire. (App/Wire.cs:13) [asserted]"
+ ]
+ },
+ {
+ "why": "both links are kept and both applied",
+ "run": [
+ "link",
+ "--list"
+ ],
+ "want": [
+ "2 asserted link(s)",
+ "[applied] App/Bus.cs:19:17 → Wire. (App/Wire.cs:12)",
+ "[applied] App/Bus.cs:19:17 → Wire. (App/Wire.cs:13)"
+ ]
+ },
+ {
+ "why": "CONTROL: re-linking the same site to the same declaration replaces its row (idempotent)",
+ "run": [
+ "link",
+ "App/Bus.cs:19",
+ "App/Wire.cs:12"
+ ],
+ "want": [
+ "linked App/Bus.cs:19:17 `listener` → Wire. (App/Wire.cs:12) [asserted]"
+ ]
+ },
+ {
+ "why": "CONTROL: still two links after the re-link",
+ "run": [
+ "link",
+ "--list"
+ ],
+ "want": [
+ "2 asserted link(s)",
+ "→ Wire. (App/Wire.cs:12)",
+ "→ Wire. (App/Wire.cs:13)"
+ ],
+ "avoid": [
+ "3 asserted link(s)"
+ ]
+ },
+ {
+ "why": "removing by the placeholder name would remove two different links: refused, naming each declaration",
+ "run": [
+ "link",
+ "--remove",
+ "App/Bus.cs:19",
+ "Wire."
+ ],
+ "expect_error": true,
+ "want": [
+ "`Wire.` names 2 links at App/Bus.cs:19",
+ "(App/Wire.cs:12)",
+ "(App/Wire.cs:13)",
+ "file:line"
+ ]
+ },
+ {
+ "why": "nothing was removed by the refused removal",
+ "run": [
+ "link",
+ "--list"
+ ],
+ "want": [
+ "2 asserted link(s)"
+ ]
+ },
+ {
+ "why": "removing by the declaration's file:line removes that link only",
+ "run": [
+ "link",
+ "--remove",
+ "App/Bus.cs:19",
+ "App/Wire.cs:12"
+ ],
+ "want": [
+ "removed 1 link(s) at App/Bus.cs:19"
+ ]
+ },
+ {
+ "why": "the other anonymous target's link stays",
+ "run": [
+ "link",
+ "--list"
+ ],
+ "want": [
+ "1 asserted link(s)",
+ "→ Wire. (App/Wire.cs:13)"
+ ],
+ "avoid": [
+ "→ Wire. (App/Wire.cs:12)"
+ ]
+ },
+ {
+ "why": "CONTROL: the same site linked to two NAMED functions keeps both",
+ "run": [
+ "link",
+ "App/Bus.cs:19",
+ "Wire.OnCreated"
+ ],
+ "want": [
+ "→ Wire.OnCreated [asserted]"
+ ]
+ },
+ {
+ "why": "CONTROL: the second named function",
+ "run": [
+ "link",
+ "App/Bus.cs:19",
+ "Wire.OnUpdated"
+ ],
+ "want": [
+ "→ Wire.OnUpdated [asserted]"
+ ]
+ },
+ {
+ "why": "CONTROL: three links: one anonymous, two named",
+ "run": [
+ "link",
+ "--list"
+ ],
+ "want": [
+ "3 asserted link(s)",
+ "→ Wire. (App/Wire.cs:13)",
+ "→ Wire.OnCreated",
+ "→ Wire.OnUpdated"
+ ]
+ },
+ {
+ "why": "CONTROL: --remove removes only the one named",
+ "run": [
+ "link",
+ "--remove",
+ "App/Bus.cs:19",
+ "Wire.OnCreated"
+ ],
+ "want": [
+ "removed 1 link(s) at App/Bus.cs:19"
+ ]
+ },
+ {
+ "why": "CONTROL: the other named link and the anonymous one stay",
+ "run": [
+ "link",
+ "--list"
+ ],
+ "want": [
+ "2 asserted link(s)",
+ "→ Wire. (App/Wire.cs:13)",
+ "→ Wire.OnUpdated"
+ ],
+ "avoid": [
+ "→ Wire.OnCreated "
+ ]
+ },
+ {
+ "why": "`-` still removes every link at the site",
+ "run": [
+ "link",
+ "App/Bus.cs:19",
+ "-"
+ ],
+ "want": [
+ "removed 2 link(s) at App/Bus.cs:19"
+ ]
+ }
+ ]
+}
diff --git a/tests/cases/python/asserted-links-anonymous/app/__init__.py b/tests/cases/python/asserted-links-anonymous/app/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/cases/python/asserted-links-anonymous/app/bus.py b/tests/cases/python/asserted-links-anonymous/app/bus.py
new file mode 100644
index 000000000..890eda87f
--- /dev/null
+++ b/tests/cases/python/asserted-links-anonymous/app/bus.py
@@ -0,0 +1,10 @@
+class Bus:
+ def __init__(self):
+ self.listeners = []
+
+ def on(self, listener):
+ self.listeners.append(listener)
+
+ def emit(self, x):
+ for listener in self.listeners:
+ listener(x)
diff --git a/tests/cases/python/asserted-links-anonymous/app/wire.py b/tests/cases/python/asserted-links-anonymous/app/wire.py
new file mode 100644
index 000000000..d81e122a9
--- /dev/null
+++ b/tests/cases/python/asserted-links-anonymous/app/wire.py
@@ -0,0 +1,11 @@
+def created(x): pass
+def updated(x): pass
+def on_created(x): created(x)
+def on_updated(x): updated(x)
+
+
+def wire(bus):
+ bus.on(lambda x: created(x))
+ bus.on(lambda x: updated(x))
+ bus.on(on_created)
+ bus.on(on_updated)
diff --git a/tests/cases/python/asserted-links-anonymous/case.json b/tests/cases/python/asserted-links-anonymous/case.json
new file mode 100644
index 000000000..eec9c2cb1
--- /dev/null
+++ b/tests/cases/python/asserted-links-anonymous/case.json
@@ -0,0 +1,191 @@
+{
+ "lang": "python",
+ "src": ".",
+ "checks": [
+ {
+ "why": "BASE: one call site linked to an anonymous function: the link names it by its declaration, since every one of the lambdas shares the name ",
+ "run": [
+ "link",
+ "app/bus.py:10",
+ "app/wire.py:8"
+ ],
+ "want": [
+ "linked app/bus.py:10:13 `listener` → (app/wire.py:8) [asserted]"
+ ]
+ },
+ {
+ "why": "a second link from the SAME site to ANOTHER of the lambdas is a second link, not a replacement of the first (both are named )",
+ "run": [
+ "link",
+ "app/bus.py:10",
+ "app/wire.py:9"
+ ],
+ "want": [
+ "linked app/bus.py:10:13 `listener` → (app/wire.py:9) [asserted]"
+ ]
+ },
+ {
+ "why": "both links are kept and both applied",
+ "run": [
+ "link",
+ "--list"
+ ],
+ "want": [
+ "2 asserted link(s)",
+ "[applied] app/bus.py:10:13 → (app/wire.py:8)",
+ "[applied] app/bus.py:10:13 → (app/wire.py:9)"
+ ]
+ },
+ {
+ "why": "CONTROL: re-linking the same site to the same declaration replaces its row (idempotent)",
+ "run": [
+ "link",
+ "app/bus.py:10",
+ "app/wire.py:8"
+ ],
+ "want": [
+ "linked app/bus.py:10:13 `listener` → (app/wire.py:8) [asserted]"
+ ]
+ },
+ {
+ "why": "CONTROL: still two links after the re-link",
+ "run": [
+ "link",
+ "--list"
+ ],
+ "want": [
+ "2 asserted link(s)",
+ "→ (app/wire.py:8)",
+ "→ (app/wire.py:9)"
+ ],
+ "avoid": [
+ "3 asserted link(s)"
+ ]
+ },
+ {
+ "why": "removing by the placeholder name would remove two different links: refused, naming each declaration",
+ "run": [
+ "link",
+ "--remove",
+ "app/bus.py:10",
+ ""
+ ],
+ "expect_error": true,
+ "want": [
+ "`` names 2 links at app/bus.py:10",
+ "(app/wire.py:8)",
+ "(app/wire.py:9)",
+ "file:line"
+ ]
+ },
+ {
+ "why": "nothing was removed by the refused removal",
+ "run": [
+ "link",
+ "--list"
+ ],
+ "want": [
+ "2 asserted link(s)"
+ ]
+ },
+ {
+ "why": "removing by the declaration's file:line removes that link only",
+ "run": [
+ "link",
+ "--remove",
+ "app/bus.py:10",
+ "app/wire.py:8"
+ ],
+ "want": [
+ "removed 1 link(s) at app/bus.py:10"
+ ]
+ },
+ {
+ "why": "the other anonymous target's link stays",
+ "run": [
+ "link",
+ "--list"
+ ],
+ "want": [
+ "1 asserted link(s)",
+ "→ (app/wire.py:9)"
+ ],
+ "avoid": [
+ "→ (app/wire.py:8)"
+ ]
+ },
+ {
+ "why": "CONTROL: the same site linked to two NAMED functions keeps both",
+ "run": [
+ "link",
+ "app/bus.py:10",
+ "on_created"
+ ],
+ "want": [
+ "→ on_created [asserted]"
+ ]
+ },
+ {
+ "why": "CONTROL: the second named function",
+ "run": [
+ "link",
+ "app/bus.py:10",
+ "on_updated"
+ ],
+ "want": [
+ "→ on_updated [asserted]"
+ ]
+ },
+ {
+ "why": "CONTROL: three links: one anonymous, two named",
+ "run": [
+ "link",
+ "--list"
+ ],
+ "want": [
+ "3 asserted link(s)",
+ "→ (app/wire.py:9)",
+ "→ on_created",
+ "→ on_updated"
+ ]
+ },
+ {
+ "why": "CONTROL: --remove removes only the one named",
+ "run": [
+ "link",
+ "--remove",
+ "app/bus.py:10",
+ "on_created"
+ ],
+ "want": [
+ "removed 1 link(s) at app/bus.py:10"
+ ]
+ },
+ {
+ "why": "CONTROL: the other named link and the anonymous one stay",
+ "run": [
+ "link",
+ "--list"
+ ],
+ "want": [
+ "2 asserted link(s)",
+ "→ (app/wire.py:9)",
+ "→ on_updated"
+ ],
+ "avoid": [
+ "→ on_created "
+ ]
+ },
+ {
+ "why": "`-` still removes every link at the site",
+ "run": [
+ "link",
+ "app/bus.py:10",
+ "-"
+ ],
+ "want": [
+ "removed 2 link(s) at app/bus.py:10"
+ ]
+ }
+ ]
+}
diff --git a/tests/cases/typescript/asserted-links-anonymous/case.json b/tests/cases/typescript/asserted-links-anonymous/case.json
new file mode 100644
index 000000000..1254461e7
--- /dev/null
+++ b/tests/cases/typescript/asserted-links-anonymous/case.json
@@ -0,0 +1,191 @@
+{
+ "lang": "typescript",
+ "src": "src",
+ "checks": [
+ {
+ "why": "BASE: one call site linked to an anonymous function: the link names it by its declaration, since every one of the arrow functions shares the name ",
+ "run": [
+ "link",
+ "src/bus.ts:10",
+ "src/wire.ts:9"
+ ],
+ "want": [
+ "linked src/bus.ts:10:7 `listener` → (src/wire.ts:9) [asserted]"
+ ]
+ },
+ {
+ "why": "a second link from the SAME site to ANOTHER of the arrow functions is a second link, not a replacement of the first (both are named )",
+ "run": [
+ "link",
+ "src/bus.ts:10",
+ "src/wire.ts:10"
+ ],
+ "want": [
+ "linked src/bus.ts:10:7 `listener` → (src/wire.ts:10) [asserted]"
+ ]
+ },
+ {
+ "why": "both links are kept and both applied",
+ "run": [
+ "link",
+ "--list"
+ ],
+ "want": [
+ "2 asserted link(s)",
+ "[applied] src/bus.ts:10:7 → (src/wire.ts:9)",
+ "[applied] src/bus.ts:10:7 → (src/wire.ts:10)"
+ ]
+ },
+ {
+ "why": "CONTROL: re-linking the same site to the same declaration replaces its row (idempotent)",
+ "run": [
+ "link",
+ "src/bus.ts:10",
+ "src/wire.ts:9"
+ ],
+ "want": [
+ "linked src/bus.ts:10:7 `listener` → (src/wire.ts:9) [asserted]"
+ ]
+ },
+ {
+ "why": "CONTROL: still two links after the re-link",
+ "run": [
+ "link",
+ "--list"
+ ],
+ "want": [
+ "2 asserted link(s)",
+ "→ (src/wire.ts:9)",
+ "→ (src/wire.ts:10)"
+ ],
+ "avoid": [
+ "3 asserted link(s)"
+ ]
+ },
+ {
+ "why": "removing by the placeholder name would remove two different links: refused, naming each declaration",
+ "run": [
+ "link",
+ "--remove",
+ "src/bus.ts:10",
+ ""
+ ],
+ "expect_error": true,
+ "want": [
+ "`` names 2 links at src/bus.ts:10",
+ "(src/wire.ts:9)",
+ "(src/wire.ts:10)",
+ "file:line"
+ ]
+ },
+ {
+ "why": "nothing was removed by the refused removal",
+ "run": [
+ "link",
+ "--list"
+ ],
+ "want": [
+ "2 asserted link(s)"
+ ]
+ },
+ {
+ "why": "removing by the declaration's file:line removes that link only",
+ "run": [
+ "link",
+ "--remove",
+ "src/bus.ts:10",
+ "src/wire.ts:9"
+ ],
+ "want": [
+ "removed 1 link(s) at src/bus.ts:10"
+ ]
+ },
+ {
+ "why": "the other anonymous target's link stays",
+ "run": [
+ "link",
+ "--list"
+ ],
+ "want": [
+ "1 asserted link(s)",
+ "→ (src/wire.ts:10)"
+ ],
+ "avoid": [
+ "→ (src/wire.ts:9)"
+ ]
+ },
+ {
+ "why": "CONTROL: the same site linked to two NAMED functions keeps both",
+ "run": [
+ "link",
+ "src/bus.ts:10",
+ "onCreated"
+ ],
+ "want": [
+ "→ onCreated [asserted]"
+ ]
+ },
+ {
+ "why": "CONTROL: the second named function",
+ "run": [
+ "link",
+ "src/bus.ts:10",
+ "onUpdated"
+ ],
+ "want": [
+ "→ onUpdated [asserted]"
+ ]
+ },
+ {
+ "why": "CONTROL: three links: one anonymous, two named",
+ "run": [
+ "link",
+ "--list"
+ ],
+ "want": [
+ "3 asserted link(s)",
+ "→ (src/wire.ts:10)",
+ "→ onCreated",
+ "→ onUpdated"
+ ]
+ },
+ {
+ "why": "CONTROL: --remove removes only the one named",
+ "run": [
+ "link",
+ "--remove",
+ "src/bus.ts:10",
+ "onCreated"
+ ],
+ "want": [
+ "removed 1 link(s) at src/bus.ts:10"
+ ]
+ },
+ {
+ "why": "CONTROL: the other named link and the anonymous one stay",
+ "run": [
+ "link",
+ "--list"
+ ],
+ "want": [
+ "2 asserted link(s)",
+ "→ (src/wire.ts:10)",
+ "→ onUpdated"
+ ],
+ "avoid": [
+ "→ onCreated "
+ ]
+ },
+ {
+ "why": "`-` still removes every link at the site",
+ "run": [
+ "link",
+ "src/bus.ts:10",
+ "-"
+ ],
+ "want": [
+ "removed 2 link(s) at src/bus.ts:10"
+ ]
+ }
+ ]
+}
diff --git a/tests/cases/typescript/asserted-links-anonymous/src/bus.ts b/tests/cases/typescript/asserted-links-anonymous/src/bus.ts
new file mode 100644
index 000000000..ac164a341
--- /dev/null
+++ b/tests/cases/typescript/asserted-links-anonymous/src/bus.ts
@@ -0,0 +1,13 @@
+export class Bus {
+ private readonly listeners: Array<(x: string) => void> = [];
+
+ on(listener: (x: string) => void): void {
+ this.listeners.push(listener);
+ }
+
+ emit(x: string): void {
+ for (const listener of this.listeners) {
+ listener(x);
+ }
+ }
+}
diff --git a/tests/cases/typescript/asserted-links-anonymous/src/wire.ts b/tests/cases/typescript/asserted-links-anonymous/src/wire.ts
new file mode 100644
index 000000000..5c8b29fea
--- /dev/null
+++ b/tests/cases/typescript/asserted-links-anonymous/src/wire.ts
@@ -0,0 +1,13 @@
+import { Bus } from "./bus";
+
+export function created(x: string): void {}
+export function updated(x: string): void {}
+export function onCreated(x: string): void { created(x); }
+export function onUpdated(x: string): void { updated(x); }
+
+export function wire(bus: Bus): void {
+ bus.on((x) => created(x));
+ bus.on((x) => updated(x));
+ bus.on(onCreated);
+ bus.on(onUpdated);
+}
From a66e57569a626edc53d4f1fb13665a088b11a95a Mon Sep 17 00:00:00 2001
From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com>
Date: Sun, 1 Nov 2026 21:52:04 -0800
Subject: [PATCH 07/11] ts: type arithmetic results, `??`/`||` defaults and
namespace consts as receivers
Three receiver shapes had no type, so a call on them resolved to nothing even
with the declaration staged:
- Arithmetic: `-`, `*`, `/`, `%`, `**`, bitwise/shift operators and unary
`-`, `~`, `++`, `--` yield Number (BigInt when an operand is a bigint);
unary `+` is always Number; `+` is Number only when both operands are.
Typing these also fixes overload choice where the arithmetic result is an
argument (a numeric constructor overload, a `number` function overload).
- Defaults: `a ?? b` / `a || b` (and `a && b`'s right side) carry the named
type of the operand the compiler names, as the shape rule already did.
An un-annotated field initialised by a generic `new` now passes its type
arguments on, so `this.byKey.get(k) ?? []` substitutes V.
- Namespace consts: `N.v(...)` and `N.v.m(...)` where `v` is a const
declared in a namespace (local, nested, ambient, or a global the binder
left unbound) now take the const's declared type or shape. A const that
holds a function literal keeps its body as the only answer.
Cases 96-98 cover each with controls: a string `+` stays String, a bigint
result never names Number, an annotated default is unchanged, an untyped
left side does not borrow the right's type, a namespace function and a
parameter shadowing the namespace name are unchanged.
Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
---
.../src/arith.ts | 41 +++++
.../src/globals.d.ts | 14 ++
.../src/tsconfig.json | 1 +
.../97-nullish-default-receiver/src/bus.ts | 37 +++++
.../src/globals.d.ts | 16 ++
.../src/tsconfig.json | 1 +
.../lib/runtime.d.ts | 14 ++
.../src/globals.d.ts | 11 ++
.../src/tsconfig.json | 1 +
.../98-namespace-const-callable/src/use.ts | 36 +++++
.../96-arithmetic-result-receiver.edges | 12 ++
.../96-arithmetic-result-receiver.entries | 15 ++
.../96-arithmetic-result-receiver.fields | 0
...6-arithmetic-result-receiver.fields-oracle | 7 +
.../96-arithmetic-result-receiver.oracle | 1 +
.../96-arithmetic-result-receiver.type-use | 3 +
...96-arithmetic-result-receiver.types-oracle | 7 +
.../97-nullish-default-receiver.edges | 13 ++
.../97-nullish-default-receiver.entries | 3 +
.../97-nullish-default-receiver.fields | 4 +
.../97-nullish-default-receiver.fields-oracle | 7 +
.../97-nullish-default-receiver.oracle | 1 +
.../97-nullish-default-receiver.type-use | 9 ++
.../97-nullish-default-receiver.types-oracle | 9 ++
.../98-namespace-const-callable.edges | 12 ++
.../98-namespace-const-callable.entries | 8 +
.../98-namespace-const-callable.envelope | 1 +
.../98-namespace-const-callable.fields | 2 +
.../98-namespace-const-callable.fields-oracle | 7 +
.../98-namespace-const-callable.known-missing | 7 +
.../98-namespace-const-callable.lib.edges | 12 ++
.../98-namespace-const-callable.lib.envelope | 1 +
...namespace-const-callable.lib.known-missing | 7 +
.../98-namespace-const-callable.lib.oracle | 3 +
.../98-namespace-const-callable.oracle | 3 +
.../98-namespace-const-callable.type-use | 5 +
.../98-namespace-const-callable.types-oracle | 10 ++
.../engine/expression-resolution/expr-type.dl | 143 ++++++++++++++++++
.../typescript/engine/resolution/generics.dl | 11 ++
graph/typescript/souffle/decls_all.dl | 8 +
40 files changed, 503 insertions(+)
create mode 100644 graph/test/typescript/cases/96-arithmetic-result-receiver/src/arith.ts
create mode 100644 graph/test/typescript/cases/96-arithmetic-result-receiver/src/globals.d.ts
create mode 100644 graph/test/typescript/cases/96-arithmetic-result-receiver/src/tsconfig.json
create mode 100644 graph/test/typescript/cases/97-nullish-default-receiver/src/bus.ts
create mode 100644 graph/test/typescript/cases/97-nullish-default-receiver/src/globals.d.ts
create mode 100644 graph/test/typescript/cases/97-nullish-default-receiver/src/tsconfig.json
create mode 100644 graph/test/typescript/cases/98-namespace-const-callable/lib/runtime.d.ts
create mode 100644 graph/test/typescript/cases/98-namespace-const-callable/src/globals.d.ts
create mode 100644 graph/test/typescript/cases/98-namespace-const-callable/src/tsconfig.json
create mode 100644 graph/test/typescript/cases/98-namespace-const-callable/src/use.ts
create mode 100644 graph/test/typescript/expected/96-arithmetic-result-receiver.edges
create mode 100644 graph/test/typescript/expected/96-arithmetic-result-receiver.entries
create mode 100644 graph/test/typescript/expected/96-arithmetic-result-receiver.fields
create mode 100644 graph/test/typescript/expected/96-arithmetic-result-receiver.fields-oracle
create mode 100644 graph/test/typescript/expected/96-arithmetic-result-receiver.oracle
create mode 100644 graph/test/typescript/expected/96-arithmetic-result-receiver.type-use
create mode 100644 graph/test/typescript/expected/96-arithmetic-result-receiver.types-oracle
create mode 100644 graph/test/typescript/expected/97-nullish-default-receiver.edges
create mode 100644 graph/test/typescript/expected/97-nullish-default-receiver.entries
create mode 100644 graph/test/typescript/expected/97-nullish-default-receiver.fields
create mode 100644 graph/test/typescript/expected/97-nullish-default-receiver.fields-oracle
create mode 100644 graph/test/typescript/expected/97-nullish-default-receiver.oracle
create mode 100644 graph/test/typescript/expected/97-nullish-default-receiver.type-use
create mode 100644 graph/test/typescript/expected/97-nullish-default-receiver.types-oracle
create mode 100644 graph/test/typescript/expected/98-namespace-const-callable.edges
create mode 100644 graph/test/typescript/expected/98-namespace-const-callable.entries
create mode 100644 graph/test/typescript/expected/98-namespace-const-callable.envelope
create mode 100644 graph/test/typescript/expected/98-namespace-const-callable.fields
create mode 100644 graph/test/typescript/expected/98-namespace-const-callable.fields-oracle
create mode 100644 graph/test/typescript/expected/98-namespace-const-callable.known-missing
create mode 100644 graph/test/typescript/expected/98-namespace-const-callable.lib.edges
create mode 100644 graph/test/typescript/expected/98-namespace-const-callable.lib.envelope
create mode 100644 graph/test/typescript/expected/98-namespace-const-callable.lib.known-missing
create mode 100644 graph/test/typescript/expected/98-namespace-const-callable.lib.oracle
create mode 100644 graph/test/typescript/expected/98-namespace-const-callable.oracle
create mode 100644 graph/test/typescript/expected/98-namespace-const-callable.type-use
create mode 100644 graph/test/typescript/expected/98-namespace-const-callable.types-oracle
diff --git a/graph/test/typescript/cases/96-arithmetic-result-receiver/src/arith.ts b/graph/test/typescript/cases/96-arithmetic-result-receiver/src/arith.ts
new file mode 100644
index 000000000..aced3df06
--- /dev/null
+++ b/graph/test/typescript/cases/96-arithmetic-result-receiver/src/arith.ts
@@ -0,0 +1,41 @@
+// A call on the RESULT of an arithmetic operator. The operators below yield a number
+// (or a bigint when the operands are bigints), so the receiver is Number's or BigInt's.
+export function elapsed(start: number, end: number): string {
+ return (end - start).toFixed(1);
+}
+export function ratio(a: number, b: number): string {
+ return (a / b).toFixed(2);
+}
+export function scaled(a: number): string {
+ return (a * 3 % 7).toString(16);
+}
+export function power(a: number): string {
+ return (a ** 2).toFixed(0);
+}
+export function masked(a: number): string {
+ return (a & 0xff).toString(2);
+}
+export function negated(a: number): string {
+ return (-a).toFixed(0);
+}
+export function parsed(s: string): string {
+ return (+s).toFixed(0);
+}
+export function total(a: number, b: number): string {
+ return (a + b).toFixed(1);
+}
+export function bigDiff(a: bigint, b: bigint): string {
+ return (a - b).toString();
+}
+export function bigNeg(a: bigint): string {
+ return (-a).toString();
+}
+
+// controls: a `+` with a string operand is a concatenation and stays String's alone,
+// and a bigint result never names Number's declaration.
+export function joined(s: string, n: number): string {
+ return (s + n).toString();
+}
+export function joinedTrim(s: string, n: number): string {
+ return (n + s).trim();
+}
diff --git a/graph/test/typescript/cases/96-arithmetic-result-receiver/src/globals.d.ts b/graph/test/typescript/cases/96-arithmetic-result-receiver/src/globals.d.ts
new file mode 100644
index 000000000..86c7a3bfe
--- /dev/null
+++ b/graph/test/typescript/cases/96-arithmetic-result-receiver/src/globals.d.ts
@@ -0,0 +1,14 @@
+// The case's own standard library under `noLib`. Number, BigInt and String all declare
+// `toString`, so a receiver typed as the wrong one of the three names a different
+// declaration rather than the same one by luck.
+interface Object { valueOf(): Object; }
+interface Boolean {}
+interface Number { toFixed(digits?: number): string; toString(radix?: number): string; }
+interface BigInt { toString(radix?: number): string; }
+interface String { trim(): string; toString(): string; }
+interface Array { length: number; }
+interface Function {}
+interface CallableFunction extends Function {}
+interface NewableFunction extends Function {}
+interface IArguments {}
+interface RegExp {}
diff --git a/graph/test/typescript/cases/96-arithmetic-result-receiver/src/tsconfig.json b/graph/test/typescript/cases/96-arithmetic-result-receiver/src/tsconfig.json
new file mode 100644
index 000000000..dcbfb4f8d
--- /dev/null
+++ b/graph/test/typescript/cases/96-arithmetic-result-receiver/src/tsconfig.json
@@ -0,0 +1 @@
+{ "compilerOptions": { "strict": true, "noLib": true, "target": "es2022" } }
diff --git a/graph/test/typescript/cases/97-nullish-default-receiver/src/bus.ts b/graph/test/typescript/cases/97-nullish-default-receiver/src/bus.ts
new file mode 100644
index 000000000..8714caed5
--- /dev/null
+++ b/graph/test/typescript/cases/97-nullish-default-receiver/src/bus.ts
@@ -0,0 +1,37 @@
+// `a ?? b` and `a || b` carry the left operand's type. A defaulted lookup is the
+// ordinary way to read-or-create an entry, and the receiver after the default had no
+// type.
+type Listener = (payload: unknown) => void;
+
+export class Bus {
+ private readonly listeners = new Map();
+ private readonly labels = new Map();
+
+ on(event: string, l: Listener): void {
+ const list = this.listeners.get(event) ?? [];
+ list.push(l);
+ this.listeners.set(event, list);
+ }
+ direct(event: string, l: Listener): number {
+ return (this.listeners.get(event) ?? []).push(l);
+ }
+ label(event: string): string {
+ return (this.labels.get(event) || "none").trim();
+ }
+
+ // A left side that is never nullish carries its type through the default too.
+ nonNullable(xs: Listener[], l: Listener): number {
+ return (xs ?? []).push(l);
+ }
+
+ // controls: an annotated default resolved before and must not change, and an untyped
+ // left side does not borrow the right side's type.
+ annotated(event: string, l: Listener): number {
+ const list: Listener[] = this.listeners.get(event) ?? [];
+ return list.push(l);
+ }
+ untyped(x: any, l: Listener): void {
+ const list = x ?? [];
+ list.push(l);
+ }
+}
diff --git a/graph/test/typescript/cases/97-nullish-default-receiver/src/globals.d.ts b/graph/test/typescript/cases/97-nullish-default-receiver/src/globals.d.ts
new file mode 100644
index 000000000..a93966091
--- /dev/null
+++ b/graph/test/typescript/cases/97-nullish-default-receiver/src/globals.d.ts
@@ -0,0 +1,16 @@
+// The case's own standard library under `noLib`: a Map whose `get` may return
+// undefined, and an Array to default to.
+interface Object {}
+interface Boolean {}
+interface Number {}
+interface String { trim(): string; }
+interface Array { length: number; push(...items: T[]): number; }
+interface ReadonlyArray { length: number; }
+interface Map { get(key: K): V | undefined; set(key: K, value: V): this; }
+interface MapConstructor { new (): Map; }
+declare var Map: MapConstructor;
+interface Function {}
+interface CallableFunction extends Function {}
+interface NewableFunction extends Function {}
+interface IArguments {}
+interface RegExp {}
diff --git a/graph/test/typescript/cases/97-nullish-default-receiver/src/tsconfig.json b/graph/test/typescript/cases/97-nullish-default-receiver/src/tsconfig.json
new file mode 100644
index 000000000..dcbfb4f8d
--- /dev/null
+++ b/graph/test/typescript/cases/97-nullish-default-receiver/src/tsconfig.json
@@ -0,0 +1 @@
+{ "compilerOptions": { "strict": true, "noLib": true, "target": "es2022" } }
diff --git a/graph/test/typescript/cases/98-namespace-const-callable/lib/runtime.d.ts b/graph/test/typescript/cases/98-namespace-const-callable/lib/runtime.d.ts
new file mode 100644
index 000000000..bbcdaac96
--- /dev/null
+++ b/graph/test/typescript/cases/98-namespace-const-callable/lib/runtime.d.ts
@@ -0,0 +1,14 @@
+// A runtime's global namespace, staged as a library: a const typed by an interface with
+// a call signature, a const typed by a plain interface, and a function.
+declare namespace Rt {
+ export interface TestFn {
+ (name: string, fn: () => void): void;
+ only(name: string, fn: () => void): void;
+ }
+ export interface Env {
+ get(key: string): string | undefined;
+ }
+ export const test: TestFn;
+ export const env: Env;
+ export function serve(port: number): void;
+}
diff --git a/graph/test/typescript/cases/98-namespace-const-callable/src/globals.d.ts b/graph/test/typescript/cases/98-namespace-const-callable/src/globals.d.ts
new file mode 100644
index 000000000..dd5843cc1
--- /dev/null
+++ b/graph/test/typescript/cases/98-namespace-const-callable/src/globals.d.ts
@@ -0,0 +1,11 @@
+// The case's own standard library under `noLib`.
+interface Object {}
+interface Boolean {}
+interface Number {}
+interface String {}
+interface Array { length: number; }
+interface Function {}
+interface CallableFunction extends Function {}
+interface NewableFunction extends Function {}
+interface IArguments {}
+interface RegExp {}
diff --git a/graph/test/typescript/cases/98-namespace-const-callable/src/tsconfig.json b/graph/test/typescript/cases/98-namespace-const-callable/src/tsconfig.json
new file mode 100644
index 000000000..dcbfb4f8d
--- /dev/null
+++ b/graph/test/typescript/cases/98-namespace-const-callable/src/tsconfig.json
@@ -0,0 +1 @@
+{ "compilerOptions": { "strict": true, "noLib": true, "target": "es2022" } }
diff --git a/graph/test/typescript/cases/98-namespace-const-callable/src/use.ts b/graph/test/typescript/cases/98-namespace-const-callable/src/use.ts
new file mode 100644
index 000000000..6d7e8cbae
--- /dev/null
+++ b/graph/test/typescript/cases/98-namespace-const-callable/src/use.ts
@@ -0,0 +1,36 @@
+///
+// (The reference is for the compiler alone: the parser reads this directory only, so
+// `Rt` reaches the engine as an unbound global name and the namespace as library IR.)
+// A call through a const declared in a namespace. The const's type carries the call
+// signature, and a member call one hop further needs the const's type as its receiver.
+export function suite(): void {
+ Rt.test("adds", () => {});
+ Rt.test.only("focused", () => {});
+ Rt.env.get("HOME");
+}
+
+// The same through a namespace declared in the project.
+interface Runner { (n: number): number; describe(): string; }
+interface Step { (n: number): number; }
+declare function makeRunner(): Runner;
+namespace Local {
+ export const run: Runner = makeRunner();
+ export const arrow: Step = (n) => n + 1;
+ export function helper(): void {}
+}
+export function local(): void {
+ Local.run(1);
+ Local.run.describe();
+}
+
+// controls: a namespace function keeps its one edge; a const holding an arrow keeps its
+// body as the target with no bodiless signature beside it; a parameter that shadows the
+// namespace's name resolves against its own type.
+export function controls(): void {
+ Rt.serve(8000);
+ Local.helper();
+ Local.arrow(2);
+}
+export function shadowed(Rt: { test(name: string): void }): void {
+ Rt.test("shadowed");
+}
diff --git a/graph/test/typescript/expected/96-arithmetic-result-receiver.edges b/graph/test/typescript/expected/96-arithmetic-result-receiver.edges
new file mode 100644
index 000000000..84ba005a3
--- /dev/null
+++ b/graph/test/typescript/expected/96-arithmetic-result-receiver.edges
@@ -0,0 +1,12 @@
+known_edge METHOD_CALL arith#bigDiff(bigint,bigint) @L28 -> BigInt#toString(number)
+known_edge METHOD_CALL arith#bigNeg(bigint) @L31 -> BigInt#toString(number)
+known_edge METHOD_CALL arith#elapsed(number,number) @L4 -> Number#toFixed(number)
+known_edge METHOD_CALL arith#joined(string,number) @L37 -> String#toString()
+known_edge METHOD_CALL arith#joinedTrim(string,number) @L40 -> String#trim()
+known_edge METHOD_CALL arith#masked(number) @L16 -> Number#toString(number)
+known_edge METHOD_CALL arith#negated(number) @L19 -> Number#toFixed(number)
+known_edge METHOD_CALL arith#parsed(string) @L22 -> Number#toFixed(number)
+known_edge METHOD_CALL arith#power(number) @L13 -> Number#toFixed(number)
+known_edge METHOD_CALL arith#ratio(number,number) @L7 -> Number#toFixed(number)
+known_edge METHOD_CALL arith#scaled(number) @L10 -> Number#toString(number)
+known_edge METHOD_CALL arith#total(number,number) @L25 -> Number#toFixed(number)
diff --git a/graph/test/typescript/expected/96-arithmetic-result-receiver.entries b/graph/test/typescript/expected/96-arithmetic-result-receiver.entries
new file mode 100644
index 000000000..ab7f81f10
--- /dev/null
+++ b/graph/test/typescript/expected/96-arithmetic-result-receiver.entries
@@ -0,0 +1,15 @@
+── entry_point (14) ──
+ exported_from_entry_module arith#bigDiff arith.ts:27
+ exported_from_entry_module arith#bigNeg arith.ts:30
+ exported_from_entry_module arith#elapsed arith.ts:3
+ exported_from_entry_module arith#joined arith.ts:36
+ exported_from_entry_module arith#joinedTrim arith.ts:39
+ exported_from_entry_module arith#masked arith.ts:15
+ exported_from_entry_module arith#negated arith.ts:18
+ exported_from_entry_module arith#parsed arith.ts:21
+ exported_from_entry_module arith#power arith.ts:12
+ exported_from_entry_module arith#ratio arith.ts:6
+ exported_from_entry_module arith#scaled arith.ts:9
+ exported_from_entry_module arith#total arith.ts:24
+ unimported_module arith# arith.ts:1
+ unimported_module globals# globals.d.ts:1
diff --git a/graph/test/typescript/expected/96-arithmetic-result-receiver.fields b/graph/test/typescript/expected/96-arithmetic-result-receiver.fields
new file mode 100644
index 000000000..e69de29bb
diff --git a/graph/test/typescript/expected/96-arithmetic-result-receiver.fields-oracle b/graph/test/typescript/expected/96-arithmetic-result-receiver.fields-oracle
new file mode 100644
index 000000000..18021fe45
--- /dev/null
+++ b/graph/test/typescript/expected/96-arithmetic-result-receiver.fields-oracle
@@ -0,0 +1,7 @@
+96-arithmetic-result-receiver [fields]
+ precision 0.0000 (0 correct, 0 wrong)
+ recall 0.0000 (0 of 0 the compiler resolved)
+ sites 0 resolved 0
+ tiers
+ access
+ not scored: 0 rows whose target is not a client declaration
diff --git a/graph/test/typescript/expected/96-arithmetic-result-receiver.oracle b/graph/test/typescript/expected/96-arithmetic-result-receiver.oracle
new file mode 100644
index 000000000..8dfb8bec2
--- /dev/null
+++ b/graph/test/typescript/expected/96-arithmetic-result-receiver.oracle
@@ -0,0 +1 @@
+oracle=12 engine=12 agree=12 missing=0 (known 0, NEW 0) extra=0
diff --git a/graph/test/typescript/expected/96-arithmetic-result-receiver.type-use b/graph/test/typescript/expected/96-arithmetic-result-receiver.type-use
new file mode 100644
index 000000000..07e338d22
--- /dev/null
+++ b/graph/test/typescript/expected/96-arithmetic-result-receiver.type-use
@@ -0,0 +1,3 @@
+known_edge METHOD_RETURN 0 Object [METHOD] -> Object
+known_edge SUPER_TYPE 0 CallableFunction [HERITAGE] -> Function
+known_edge SUPER_TYPE 0 NewableFunction [HERITAGE] -> Function
diff --git a/graph/test/typescript/expected/96-arithmetic-result-receiver.types-oracle b/graph/test/typescript/expected/96-arithmetic-result-receiver.types-oracle
new file mode 100644
index 000000000..0f875e309
--- /dev/null
+++ b/graph/test/typescript/expected/96-arithmetic-result-receiver.types-oracle
@@ -0,0 +1,7 @@
+96-arithmetic-result-receiver [types]
+ precision 1.0000 (3 correct, 0 wrong)
+ recall 1.0000 (3 of 3 the compiler resolved)
+ sites 3 resolved 3 (100.0%)
+ tiers known_edge=3
+ contexts METHOD_RETURN=1 SUPER_TYPE=2
+ not scored: 0 rows whose target is not a client declaration
diff --git a/graph/test/typescript/expected/97-nullish-default-receiver.edges b/graph/test/typescript/expected/97-nullish-default-receiver.edges
new file mode 100644
index 000000000..5cba14cc0
--- /dev/null
+++ b/graph/test/typescript/expected/97-nullish-default-receiver.edges
@@ -0,0 +1,13 @@
+ambiguous_unknown METHOD_CALL Bus#untyped(any,Listener) @L35 -> -
+known_edge CONSTRUCTOR_CALL bus#() @L7 -> MapConstructor#()
+known_edge CONSTRUCTOR_CALL bus#() @L8 -> MapConstructor#()
+known_edge METHOD_CALL Bus#annotated(string,Listener) @L30 -> Map#get(T)
+known_edge METHOD_CALL Bus#annotated(string,Listener) @L31 -> Array#push(T[])
+known_edge METHOD_CALL Bus#direct(string,Listener) @L16 -> Array#push(T[])
+known_edge METHOD_CALL Bus#direct(string,Listener) @L16 -> Map#get(T)
+known_edge METHOD_CALL Bus#label(string) @L19 -> Map#get(T)
+known_edge METHOD_CALL Bus#label(string) @L19 -> String#trim()
+known_edge METHOD_CALL Bus#nonNullable(Listener[],Listener) @L24 -> Array#push(T[])
+known_edge METHOD_CALL Bus#on(string,Listener) @L11 -> Map#get(T)
+known_edge METHOD_CALL Bus#on(string,Listener) @L12 -> Array#push(T[])
+known_edge METHOD_CALL Bus#on(string,Listener) @L13 -> Map#set(T,T)
diff --git a/graph/test/typescript/expected/97-nullish-default-receiver.entries b/graph/test/typescript/expected/97-nullish-default-receiver.entries
new file mode 100644
index 000000000..8fa4a6c6d
--- /dev/null
+++ b/graph/test/typescript/expected/97-nullish-default-receiver.entries
@@ -0,0 +1,3 @@
+── entry_point (2) ──
+ unimported_module bus# bus.ts:1
+ unimported_module globals# globals.d.ts:1
diff --git a/graph/test/typescript/expected/97-nullish-default-receiver.fields b/graph/test/typescript/expected/97-nullish-default-receiver.fields
new file mode 100644
index 000000000..8b0eabc4f
--- /dev/null
+++ b/graph/test/typescript/expected/97-nullish-default-receiver.fields
@@ -0,0 +1,4 @@
+known_edge read Bus#annotated(string,Listener) -> Bus#listeners
+known_edge read Bus#direct(string,Listener) -> Bus#listeners
+known_edge read Bus#label(string) -> Bus#labels
+known_edge read Bus#on(string,Listener) -> Bus#listeners
diff --git a/graph/test/typescript/expected/97-nullish-default-receiver.fields-oracle b/graph/test/typescript/expected/97-nullish-default-receiver.fields-oracle
new file mode 100644
index 000000000..ff08c2b5b
--- /dev/null
+++ b/graph/test/typescript/expected/97-nullish-default-receiver.fields-oracle
@@ -0,0 +1,7 @@
+97-nullish-default-receiver [fields]
+ precision 1.0000 (4 correct, 0 wrong)
+ recall 1.0000 (4 of 4 the compiler resolved)
+ sites 5 resolved 5 (100.0%)
+ tiers known_edge=5
+ access read=5
+ not scored: 0 rows whose target is not a client declaration
diff --git a/graph/test/typescript/expected/97-nullish-default-receiver.oracle b/graph/test/typescript/expected/97-nullish-default-receiver.oracle
new file mode 100644
index 000000000..373cdb593
--- /dev/null
+++ b/graph/test/typescript/expected/97-nullish-default-receiver.oracle
@@ -0,0 +1 @@
+oracle=11 engine=11 agree=11 missing=0 (known 0, NEW 0) extra=0
diff --git a/graph/test/typescript/expected/97-nullish-default-receiver.type-use b/graph/test/typescript/expected/97-nullish-default-receiver.type-use
new file mode 100644
index 000000000..60abe7649
--- /dev/null
+++ b/graph/test/typescript/expected/97-nullish-default-receiver.type-use
@@ -0,0 +1,9 @@
+known_edge METHOD_PARAM 0 Bus [METHOD_PARAM] -> Listener
+known_edge METHOD_RETURN 0 MapConstructor [METHOD] -> Map
+known_edge OBJECT_CREATION_TYPE 0 Bus [EXPRESSION] -> Map
+known_edge SUPER_TYPE 0 CallableFunction [HERITAGE] -> Function
+known_edge SUPER_TYPE 0 NewableFunction [HERITAGE] -> Function
+known_edge TYPE_ELEMENT 1 Bus [EXPRESSION] -> Listener
+known_edge TYPE_ELEMENT 1 Bus [METHOD_PARAM] -> Listener
+known_edge TYPE_ELEMENT 1 Bus [VARIABLE] -> Listener
+known_edge VARIABLE_TYPE 0 globals [VARIABLE] -> MapConstructor
diff --git a/graph/test/typescript/expected/97-nullish-default-receiver.types-oracle b/graph/test/typescript/expected/97-nullish-default-receiver.types-oracle
new file mode 100644
index 000000000..a1ed2e12d
--- /dev/null
+++ b/graph/test/typescript/expected/97-nullish-default-receiver.types-oracle
@@ -0,0 +1,9 @@
+97-nullish-default-receiver [types]
+ precision 0.8333 (5 correct, 1 wrong)
+ recall 0.8333 (5 of 6 the compiler resolved)
+ sites 14 resolved 14 (100.0%)
+ tiers known_edge=14
+ contexts METHOD_PARAM=5 METHOD_RETURN=1 OBJECT_CREATION_TYPE=2 SUPER_TYPE=2 TYPE_ELEMENT=3 VARIABLE_TYPE=1
+ not scored: 0 rows whose target is not a client declaration
+ WRONG globals USES MapConstructor
+ MISSING globals.d USES MapConstructor
diff --git a/graph/test/typescript/expected/98-namespace-const-callable.edges b/graph/test/typescript/expected/98-namespace-const-callable.edges
new file mode 100644
index 000000000..c29154ff4
--- /dev/null
+++ b/graph/test/typescript/expected/98-namespace-const-callable.edges
@@ -0,0 +1,12 @@
+ambient_terminal FUNCTION_CALL use#() @L17 -> use#makeRunner()
+ambiguous_unknown METHOD_CALL use#controls() @L30 -> -
+ambiguous_unknown METHOD_CALL use#suite() @L7 -> -
+ambiguous_unknown METHOD_CALL use#suite() @L8 -> -
+ambiguous_unknown METHOD_CALL use#suite() @L9 -> -
+callback_registered METHOD_CALL use#suite() @L7 -> use#()
+callback_registered METHOD_CALL use#suite() @L8 -> use#()
+known_edge METHOD_CALL use#controls() @L31 -> Local#helper()
+known_edge METHOD_CALL use#controls() @L32 -> Local#arrow(?)
+known_edge METHOD_CALL use#local() @L22 -> Runner#(number)
+known_edge METHOD_CALL use#local() @L23 -> Runner#describe()
+known_edge METHOD_CALL use#shadowed({ test(name: string): void }) @L35 -> use#test(string)
diff --git a/graph/test/typescript/expected/98-namespace-const-callable.entries b/graph/test/typescript/expected/98-namespace-const-callable.entries
new file mode 100644
index 000000000..e4446c5aa
--- /dev/null
+++ b/graph/test/typescript/expected/98-namespace-const-callable.entries
@@ -0,0 +1,8 @@
+── entry_point (7) ──
+ exported_from_entry_module use#controls use.ts:29
+ exported_from_entry_module use#local use.ts:21
+ exported_from_entry_module use#shadowed use.ts:34
+ exported_from_entry_module use#suite use.ts:6
+ test use# use.ts:7
+ unimported_module globals# globals.d.ts:1
+ unimported_module use# use.ts:1
diff --git a/graph/test/typescript/expected/98-namespace-const-callable.envelope b/graph/test/typescript/expected/98-namespace-const-callable.envelope
new file mode 100644
index 000000000..61c29593d
--- /dev/null
+++ b/graph/test/typescript/expected/98-namespace-const-callable.envelope
@@ -0,0 +1 @@
+value use#Step. -> use#Local.
diff --git a/graph/test/typescript/expected/98-namespace-const-callable.fields b/graph/test/typescript/expected/98-namespace-const-callable.fields
new file mode 100644
index 000000000..7f7ac1a4f
--- /dev/null
+++ b/graph/test/typescript/expected/98-namespace-const-callable.fields
@@ -0,0 +1,2 @@
+ambiguous_unknown read use#local() -> -
+ambiguous_unknown read use#suite() -> -
diff --git a/graph/test/typescript/expected/98-namespace-const-callable.fields-oracle b/graph/test/typescript/expected/98-namespace-const-callable.fields-oracle
new file mode 100644
index 000000000..db1ae7a2a
--- /dev/null
+++ b/graph/test/typescript/expected/98-namespace-const-callable.fields-oracle
@@ -0,0 +1,7 @@
+98-namespace-const-callable [fields]
+ precision 0.0000 (0 correct, 0 wrong)
+ recall 0.0000 (0 of 0 the compiler resolved)
+ sites 3 resolved 0 (0.0%)
+ tiers ambiguous_unknown=3
+ access read=3
+ not scored: 3 rows whose target is not a client declaration
diff --git a/graph/test/typescript/expected/98-namespace-const-callable.known-missing b/graph/test/typescript/expected/98-namespace-const-callable.known-missing
new file mode 100644
index 000000000..9a6bcdf71
--- /dev/null
+++ b/graph/test/typescript/expected/98-namespace-const-callable.known-missing
@@ -0,0 +1,7 @@
+# Accepted gaps for 98-namespace-const-callable: each line is an edge the TypeScript compiler
+# resolves and this engine does not. A NEW missing edge fails the suite;
+# a line here that STARTS working also fails, so the debt cannot rot.
+# The control const that HOLDS an arrow: the compiler names the annotation's signature, the
+# engine names the arrow's body, the same accepted shape as 92-namespace-typed-const. Kept on
+# purpose: the namespace-const rule must not add the bodiless signature beside the body.
+use#controls() -> Step#(number)
diff --git a/graph/test/typescript/expected/98-namespace-const-callable.lib.edges b/graph/test/typescript/expected/98-namespace-const-callable.lib.edges
new file mode 100644
index 000000000..c3df497ab
--- /dev/null
+++ b/graph/test/typescript/expected/98-namespace-const-callable.lib.edges
@@ -0,0 +1,12 @@
+ambient_terminal FUNCTION_CALL use#() @L17 -> use#makeRunner()
+boundary_lib METHOD_CALL use#controls() @L30 -> Rt#serve(number)
+boundary_lib METHOD_CALL use#suite() @L7 -> TestFn#(string,() =)
+boundary_lib METHOD_CALL use#suite() @L8 -> TestFn#only(string,() =)
+boundary_lib METHOD_CALL use#suite() @L9 -> Env#get(string)
+callback_registered METHOD_CALL use#suite() @L7 -> use#()
+callback_registered METHOD_CALL use#suite() @L8 -> use#()
+known_edge METHOD_CALL use#controls() @L31 -> Local#helper()
+known_edge METHOD_CALL use#controls() @L32 -> Local#arrow(?)
+known_edge METHOD_CALL use#local() @L22 -> Runner#(number)
+known_edge METHOD_CALL use#local() @L23 -> Runner#describe()
+known_edge METHOD_CALL use#shadowed({ test(name: string): void }) @L35 -> use#test(string)
diff --git a/graph/test/typescript/expected/98-namespace-const-callable.lib.envelope b/graph/test/typescript/expected/98-namespace-const-callable.lib.envelope
new file mode 100644
index 000000000..61c29593d
--- /dev/null
+++ b/graph/test/typescript/expected/98-namespace-const-callable.lib.envelope
@@ -0,0 +1 @@
+value use#Step. -> use#Local.
diff --git a/graph/test/typescript/expected/98-namespace-const-callable.lib.known-missing b/graph/test/typescript/expected/98-namespace-const-callable.lib.known-missing
new file mode 100644
index 000000000..9a6bcdf71
--- /dev/null
+++ b/graph/test/typescript/expected/98-namespace-const-callable.lib.known-missing
@@ -0,0 +1,7 @@
+# Accepted gaps for 98-namespace-const-callable: each line is an edge the TypeScript compiler
+# resolves and this engine does not. A NEW missing edge fails the suite;
+# a line here that STARTS working also fails, so the debt cannot rot.
+# The control const that HOLDS an arrow: the compiler names the annotation's signature, the
+# engine names the arrow's body, the same accepted shape as 92-namespace-typed-const. Kept on
+# purpose: the namespace-const rule must not add the bodiless signature beside the body.
+use#controls() -> Step#(number)
diff --git a/graph/test/typescript/expected/98-namespace-const-callable.lib.oracle b/graph/test/typescript/expected/98-namespace-const-callable.lib.oracle
new file mode 100644
index 000000000..c256eb244
--- /dev/null
+++ b/graph/test/typescript/expected/98-namespace-const-callable.lib.oracle
@@ -0,0 +1,3 @@
+oracle=10 engine=10 agree=9 missing=1 (known 1, NEW 0) extra=1
+ known use#controls() -> Step#(number)
+ extra use#controls() -> Local#arrow(?)
diff --git a/graph/test/typescript/expected/98-namespace-const-callable.oracle b/graph/test/typescript/expected/98-namespace-const-callable.oracle
new file mode 100644
index 000000000..a5054e035
--- /dev/null
+++ b/graph/test/typescript/expected/98-namespace-const-callable.oracle
@@ -0,0 +1,3 @@
+oracle=6 engine=6 agree=5 missing=1 (known 1, NEW 0) extra=1
+ known use#controls() -> Step#(number)
+ extra use#controls() -> Local#arrow(?)
diff --git a/graph/test/typescript/expected/98-namespace-const-callable.type-use b/graph/test/typescript/expected/98-namespace-const-callable.type-use
new file mode 100644
index 000000000..680d17cce
--- /dev/null
+++ b/graph/test/typescript/expected/98-namespace-const-callable.type-use
@@ -0,0 +1,5 @@
+known_edge METHOD_RETURN 0 use [METHOD] -> Runner
+known_edge SUPER_TYPE 0 CallableFunction [HERITAGE] -> Function
+known_edge SUPER_TYPE 0 NewableFunction [HERITAGE] -> Function
+known_edge VARIABLE_TYPE 0 Local [VARIABLE] -> Runner
+known_edge VARIABLE_TYPE 0 Local [VARIABLE] -> Step
diff --git a/graph/test/typescript/expected/98-namespace-const-callable.types-oracle b/graph/test/typescript/expected/98-namespace-const-callable.types-oracle
new file mode 100644
index 000000000..b5b65c7d9
--- /dev/null
+++ b/graph/test/typescript/expected/98-namespace-const-callable.types-oracle
@@ -0,0 +1,10 @@
+98-namespace-const-callable [types]
+ precision 0.6000 (3 correct, 2 wrong)
+ recall 0.7500 (3 of 4 the compiler resolved)
+ sites 5 resolved 5 (100.0%)
+ tiers known_edge=5
+ contexts METHOD_RETURN=1 SUPER_TYPE=2 VARIABLE_TYPE=2
+ not scored: 0 rows whose target is not a client declaration
+ WRONG Local USES Runner
+ WRONG Local USES Step
+ MISSING use USES Step
diff --git a/graph/typescript/engine/expression-resolution/expr-type.dl b/graph/typescript/engine/expression-resolution/expr-type.dl
index f7fbc0d62..92f65cfca 100644
--- a/graph/typescript/engine/expression-resolution/expr-type.dl
+++ b/graph/typescript/engine/expression-resolution/expr-type.dl
@@ -162,6 +162,57 @@ expr_type(e, prov, t) :- property_access_recv(e, n, q),
expr_type(q, _, qt),
namespace_member(prov, qt, n, "TYPE", t).
+// ── A CONST DECLARED IN A NAMESPACE — `N.run(x)`, `N.env.get(k)` ────────────
+// `declare namespace N { export const run: Runner; }` with `interface Runner { (x): void }`.
+// The const is a VARIABLE whose enclosing type is the namespace, so namespace_member
+// (types, methods, fields) never lists it and the access `N.run` had no type: the call
+// through it found no call signature, and a member call one hop further found no
+// receiver. This is how a runtime or a standard-library namespace publishes a callable
+// object or a constructor (`var Format: FormatConstructor` inside a namespace).
+//
+// The namespace receiver comes three ways: a namespace typed as a value (a local
+// namespace, a nested one), one typed as a static receiver (a global the binder knew
+// as ambient), and a bare global name the binder could not bind at all, which resolves
+// by name to a global NAMESPACE_TYPE exactly as rule (2d) in callee-resolution does
+// for a namespace's functions. The name lookup is only for a reference the binder
+// left unbound or ambient, so a local of the same name keeps its own binding.
+//
+// A const that holds a function literal is left to the method lookup that already
+// answers with its body; its annotation beside it would add a bodiless signature as a
+// second answer. A const whose type has no call signature stays uncallable: the call
+// rules ask the type for a call signature, and a plain interface has none.
+namespace_value_member(prov, ns, name, v) :- var_enclosing_type(prov, ns, v),
+ type_decl(prov, _, _, _, "NAMESPACE_TYPE", _, ns),
+ var_decl(prov, name, _, sk, _, _, v),
+ namespace_value_scope(sk),
+ name != "",
+ !var_is_function(v, _).
+namespace_value_scope("NAMESPACE_SCOPE").
+namespace_value_scope("AMBIENT_SCOPE").
+expr_namespace_decl(q, ns) :- expr_type(q, prov, ns),
+ type_decl(prov, _, _, _, "NAMESPACE_TYPE", _, ns).
+expr_namespace_decl(q, ns) :- expr_static_type(q, prov, ns),
+ type_decl(prov, _, _, _, "NAMESPACE_TYPE", _, ns).
+expr_namespace_decl(q, ns) :- expr_kind("client", "IDENTIFIER_REFERENCE", _, q),
+ expr_referenced("client", rk, _, q),
+ unbound_global_ref_kind(rk),
+ expr_name("client", n, q),
+ n != "",
+ global_decl_wide(n, "TYPE", prov, ns),
+ type_decl(prov, _, _, _, "NAMESPACE_TYPE", _, ns).
+unbound_global_ref_kind("UNKNOWN").
+unbound_global_ref_kind("AMBIENT_GLOBAL").
+expr_type(e, prov, t) :- property_access_recv(e, n, q),
+ expr_namespace_decl(q, ns),
+ scope_sibling(ns, sib),
+ namespace_value_member(_, sib, n, v),
+ var_type(v, prov, t).
+expr_shape(e, s) :- property_access_recv(e, n, q),
+ expr_namespace_decl(q, ns),
+ scope_sibling(ns, sib),
+ namespace_value_member(_, sib, n, v),
+ var_shape(v, s).
+
// ── `this` ──────────────────────────────────────────────────────────────────
expr_type(e, "client", t) :- expr_referenced("client", "THIS", _, e),
expr_enclosing_type(e, t).
@@ -361,6 +412,20 @@ expr_shape(e, s) :- expr_kind("client", "BINARY_EXPRESSION", _, e),
binary_op_names_operand("||", "LEFT_OPERAND").
binary_op_names_operand("??", "LEFT_OPERAND").
binary_op_names_operand("&&", "RIGHT_OPERAND").
+// …and that same operand's named TYPE: `const list = map.get(k) ?? []; list.push(x)`.
+// The compiler types `a ?? b` as `NonNullable | B`, so the left operand's type is a
+// member of the result whatever the right one is; dropping the nullish part is free here
+// because `undefined` and `null` have no member table to look anything up in. The SHAPE
+// rule above already flowed; the named type did not, so a defaulted collection or a
+// defaulted service object lost its receiver type at the default.
+//
+// Still only the operand the compiler names, for the reason above: the right operand's
+// type beside it puts a second, wrong declaration into a site the left one decides.
+expr_type(e, prov, t) :- expr_kind("client", "BINARY_EXPRESSION", _, e),
+ expr_operator("client", op, e),
+ binary_op_names_operand(op, r),
+ expr_child("client", e, r, _, side),
+ expr_type(side, prov, t).
// ── an ARROW / FUNCTION expression — the value IS the function ──────────────
// `anonymousDeclarationHash` gives the ts_method the expression introduces. Java
@@ -407,6 +472,84 @@ expr_type(e, prov, t) :- expr_kind("client", "BINARY_EXPRESSION", _, e),
edge_role_is_binary_operand("LEFT_OPERAND").
edge_role_is_binary_operand("RIGHT_OPERAND").
+// ── ARITHMETIC — `(end - start).toFixed(1)`, `(-x).toString()` ──────────────
+// `-`, `*`, `/`, `%`, `**`, the bitwise and shift operators, and unary `-`, `~`, `++`,
+// `--` yield a number, or a bigint when the operands are bigints: the compiler rejects
+// every other operand type for these operators (an object with a numeric valueOf
+// included), so an operand typed anything but BigInt means the result is a number.
+// Unary `+` is a number unconditionally. The receiver had no type at all before, so a
+// call on a computed number — formatting a duration, rounding a ratio — resolved to
+// nothing.
+//
+// Keyed on what the operand IS typed, never on what it is not: "a number unless an
+// operand is a bigint" would negate expr_type inside its own fixpoint. An operand with
+// no type (an untyped value) therefore gives the result no type either.
+//
+// `+` is a number only when BOTH operands are numbers. One string operand makes it a
+// concatenation (the rule above), and that must stay a string and nothing else.
+operand_typed_named(x, n) :- expr_type(x, prov, t),
+ type_decl(prov, n, _, _, _, _, t).
+operand_not_bigint(x) :- operand_typed_named(x, n), n != "BigInt".
+expr_type(e, prov, t) :- expr_kind("client", "BINARY_EXPRESSION", _, e),
+ expr_operator("client", op, e),
+ numeric_only_binary_op(op),
+ expr_child("client", e, r, _, operand),
+ edge_role_is_binary_operand(r),
+ operand_not_bigint(operand),
+ expr_module("client", mod, e),
+ name_binds_any(mod, "Number", prov, "TYPE", t).
+expr_type(e, prov, t) :- expr_kind("client", "BINARY_EXPRESSION", _, e),
+ expr_operator("client", op, e),
+ numeric_only_binary_op(op),
+ expr_child("client", e, r, _, operand),
+ edge_role_is_binary_operand(r),
+ operand_typed_named(operand, "BigInt"),
+ expr_module("client", mod, e),
+ name_binds_any(mod, "BigInt", prov, "TYPE", t).
+expr_type(e, prov, t) :- expr_kind("client", "BINARY_EXPRESSION", _, e),
+ expr_operator("client", "+", e),
+ expr_child("client", e, "LEFT_OPERAND", _, l),
+ operand_typed_named(l, "Number"),
+ expr_child("client", e, "RIGHT_OPERAND", _, r),
+ operand_typed_named(r, "Number"),
+ expr_module("client", mod, e),
+ name_binds_any(mod, "Number", prov, "TYPE", t).
+expr_type(e, prov, t) :- expr_kind("client", "UNARY_EXPRESSION", _, e),
+ expr_operator("client", op, e),
+ numeric_unary_op(op),
+ expr_child("client", e, "UNARY_OPERAND", _, operand),
+ operand_not_bigint(operand),
+ expr_module("client", mod, e),
+ name_binds_any(mod, "Number", prov, "TYPE", t).
+expr_type(e, prov, t) :- expr_kind("client", "UNARY_EXPRESSION", _, e),
+ expr_operator("client", op, e),
+ numeric_unary_op(op),
+ op != "+",
+ expr_child("client", e, "UNARY_OPERAND", _, operand),
+ operand_typed_named(operand, "BigInt"),
+ expr_module("client", mod, e),
+ name_binds_any(mod, "BigInt", prov, "TYPE", t).
+expr_type(e, prov, t) :- expr_kind("client", "UNARY_EXPRESSION", _, e),
+ expr_operator("client", "+", e),
+ expr_module("client", mod, e),
+ name_binds_any(mod, "Number", prov, "TYPE", t).
+numeric_only_binary_op("-").
+numeric_only_binary_op("*").
+numeric_only_binary_op("/").
+numeric_only_binary_op("%").
+numeric_only_binary_op("**").
+numeric_only_binary_op("&").
+numeric_only_binary_op("|").
+numeric_only_binary_op("^").
+numeric_only_binary_op("<<").
+numeric_only_binary_op(">>").
+numeric_only_binary_op(">>>").
+numeric_unary_op("-").
+numeric_unary_op("+").
+numeric_unary_op("~").
+numeric_unary_op("++").
+numeric_unary_op("--").
+
// ── an ARRAY literal receiver — `[1,2].map(f)` ─────────────────────────────
expr_type(e, prov, t) :- expr_kind("client", "ARRAY_LITERAL", _, e),
expr_module("client", mod, e),
diff --git a/graph/typescript/engine/resolution/generics.dl b/graph/typescript/engine/resolution/generics.dl
index 77f3f5197..5ea648375 100644
--- a/graph/typescript/engine/resolution/generics.dl
+++ b/graph/typescript/engine/resolution/generics.dl
@@ -52,6 +52,17 @@ expr_decl_ref(e, r) :- property_access_recv(e, n, q),
expr_type(q, _, qt),
field_in_scope(qt, n, "false", f),
field_type_ref(_, r, f).
+// An un-annotated field takes its initializer's reference, the field twin of the
+// variable rule below: `private readonly byKey = new Map()` then
+// `this.byKey.get(k)` substitutes V exactly as an annotated field does. The field's
+// TYPE already came from the initializer (reference-types.dl); its type arguments did
+// not, so the call resolved and its result had no type.
+expr_decl_ref(e, r) :- property_access_recv(e, n, q),
+ expr_type(q, _, qt),
+ field_in_scope(qt, n, "false", f),
+ !field_has_declared_type(f),
+ field_initializer("client", init, f),
+ expr_decl_ref(init, r).
// A chained receiver: `getItems().find(p)` — the reference is the callee's declared
// RETURN type, which carries its own arguments.
expr_decl_ref(e, r) :- expr_call_candidate(e, callee),
diff --git a/graph/typescript/souffle/decls_all.dl b/graph/typescript/souffle/decls_all.dl
index 252fbc579..a703e6015 100644
--- a/graph/typescript/souffle/decls_all.dl
+++ b/graph/typescript/souffle/decls_all.dl
@@ -579,6 +579,14 @@
.decl specifier_global_target(c0:symbol,c1:symbol,c2:symbol)
.decl specifier_is_relative(c0:symbol)
.decl string_typed(c0:symbol)
+.decl operand_typed_named(c0:symbol,c1:symbol)
+.decl operand_not_bigint(c0:symbol)
+.decl numeric_only_binary_op(c0:symbol)
+.decl numeric_unary_op(c0:symbol)
+.decl namespace_value_member(c0:symbol,c1:symbol,c2:symbol,c3:symbol)
+.decl namespace_value_scope(c0:symbol)
+.decl expr_namespace_decl(c0:symbol,c1:symbol)
+.decl unbound_global_ref_kind(c0:symbol)
.decl structural_implementor(c0:symbol,c1:symbol)
.decl taint_gating(c0:symbol)
.decl target_has_nominal_implementor(c0:symbol)
From ad2dcd99a5229cede5fda8f485baae06e6e09150 Mon Sep 17 00:00:00 2001
From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com>
Date: Tue, 3 Nov 2026 02:19:10 -0800
Subject: [PATCH 08/11] typescript: a destructured parameter typed by its
context, and a literal nested in a typed literal
`const poll: (ctx: Ctx) => R = async ({ data, utils }) => utils.http(..)` and
`define({ lifecycle: { start: async ({ utils }) => utils.http(..) } })` left every call
on `utils` unresolved, while `ctx.utils.http(..)` on the same arrow resolved.
Two causes:
- a bound name reads its pattern's type through param_type, which only an annotation
fills; contextual typing (lambda_param_type) typed references to the parameter and
never the names destructured from it. The bound name now takes the type (and shape)
of the field it names from the contextual type, typed at its uses beside the plain
parameter's, not through param_type, which overload rejection reads under negation.
- an object literal written as a property of a typed literal had no scope, so functions
in a nested slot took no parameter types. It now takes the type of the field it fills.
Case 99 pins both, with controls: an `any` pattern and a parameter typed by an object
type literal stay unresolved. Every other TypeScript case is unchanged (109/109).
On a Deno connector repository (~750 endpoint definitions): known_edge rows 7,410 -> 8,923,
ambiguous_unknown 35,820 -> 34,300, multi_inferred 63 -> 77, no edge lost; 194 of 198
hook calls of the framework's HTTP helper now resolve.
Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
---
.../src/globals.d.ts | 35 +++++++++++++
.../src/tsconfig.json | 1 +
.../src/use.ts | 51 +++++++++++++++++++
.../99-contextual-destructured-param.edges | 9 ++++
.../99-contextual-destructured-param.entries | 5 ++
.../99-contextual-destructured-param.envelope | 5 ++
.../99-contextual-destructured-param.fields | 4 ++
...ontextual-destructured-param.fields-oracle | 7 +++
...ontextual-destructured-param.known-missing | 14 +++++
.../99-contextual-destructured-param.oracle | 8 +++
.../99-contextual-destructured-param.type-use | 12 +++++
...contextual-destructured-param.types-oracle | 7 +++
.../engine/resolution/contextual-params.dl | 36 +++++++++++++
graph/typescript/souffle/decls_all.dl | 3 ++
14 files changed, 197 insertions(+)
create mode 100644 graph/test/typescript/cases/99-contextual-destructured-param/src/globals.d.ts
create mode 100644 graph/test/typescript/cases/99-contextual-destructured-param/src/tsconfig.json
create mode 100644 graph/test/typescript/cases/99-contextual-destructured-param/src/use.ts
create mode 100644 graph/test/typescript/expected/99-contextual-destructured-param.edges
create mode 100644 graph/test/typescript/expected/99-contextual-destructured-param.entries
create mode 100644 graph/test/typescript/expected/99-contextual-destructured-param.envelope
create mode 100644 graph/test/typescript/expected/99-contextual-destructured-param.fields
create mode 100644 graph/test/typescript/expected/99-contextual-destructured-param.fields-oracle
create mode 100644 graph/test/typescript/expected/99-contextual-destructured-param.known-missing
create mode 100644 graph/test/typescript/expected/99-contextual-destructured-param.oracle
create mode 100644 graph/test/typescript/expected/99-contextual-destructured-param.type-use
create mode 100644 graph/test/typescript/expected/99-contextual-destructured-param.types-oracle
diff --git a/graph/test/typescript/cases/99-contextual-destructured-param/src/globals.d.ts b/graph/test/typescript/cases/99-contextual-destructured-param/src/globals.d.ts
new file mode 100644
index 000000000..bf77e7993
--- /dev/null
+++ b/graph/test/typescript/cases/99-contextual-destructured-param/src/globals.d.ts
@@ -0,0 +1,35 @@
+// The case's OWN standard library, for the reason 21 gives: under `noLib` the target has
+// to be declared HERE or neither side of the comparison can name it.
+//
+// Every signature below is copied from lib.es5.d.ts / lib.es2015.promise.d.ts as written,
+// because the shape of the callback parameter IS the case: `then` takes a UNION of a
+// function type with `undefined | null`, and `map` declares its own `U` next to Array's `T`.
+interface Array {
+ length: number;
+ forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void;
+ map(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[];
+ filter(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any): T[];
+ some(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any): boolean;
+}
+interface ReadonlyArray {
+ readonly length: number;
+ forEach(callbackfn: (value: T, index: number, array: readonly T[]) => void, thisArg?: any): void;
+ map(callbackfn: (value: T, index: number, array: readonly T[]) => U, thisArg?: any): U[];
+ some(predicate: (value: T, index: number, array: readonly T[]) => unknown, thisArg?: any): boolean;
+}
+interface PromiseLike {
+ then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): PromiseLike;
+}
+interface Promise {
+ then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): Promise;
+ catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): Promise;
+}
+interface Object { toString(): string; }
+interface Boolean {}
+interface Number {}
+interface String {}
+interface Function {}
+interface CallableFunction extends Function {}
+interface NewableFunction extends Function {}
+interface IArguments {}
+interface RegExp {}
diff --git a/graph/test/typescript/cases/99-contextual-destructured-param/src/tsconfig.json b/graph/test/typescript/cases/99-contextual-destructured-param/src/tsconfig.json
new file mode 100644
index 000000000..c901fced9
--- /dev/null
+++ b/graph/test/typescript/cases/99-contextual-destructured-param/src/tsconfig.json
@@ -0,0 +1 @@
+{ "compilerOptions": { "strict": true, "noLib": true, "target": "es2022", "module": "commonjs" } }
diff --git a/graph/test/typescript/cases/99-contextual-destructured-param/src/use.ts b/graph/test/typescript/cases/99-contextual-destructured-param/src/use.ts
new file mode 100644
index 000000000..a0567197b
--- /dev/null
+++ b/graph/test/typescript/cases/99-contextual-destructured-param/src/use.ts
@@ -0,0 +1,51 @@
+// A parameter destructured where its type comes from the context, not an annotation.
+export interface Utils {
+ send(url: string): number;
+ sleep(ms: number): void;
+}
+export interface Ctx {
+ url: string;
+ utils: Utils;
+}
+
+// typed by the const's function-type annotation: `utils.send` is Utils.send
+export const viaConst: (ctx: Ctx) => number = ({ url, utils }) => {
+ return utils.send(url);
+};
+
+// the same arrow with a plain parameter (already resolved; must not change)
+export const plainConst: (ctx: Ctx) => number = (ctx) => {
+ return ctx.utils.send(ctx.url);
+};
+
+// an annotated pattern (already resolved; must not change)
+export function annotated({ url, utils }: Ctx): number {
+ return utils.send(url);
+}
+
+// control: a destructured parameter with no type anywhere stays unresolved
+export const untyped = ({ url, utils }: any) => {
+ return utils.send(url);
+};
+
+// control: a local object of another type with a member of the same name
+export interface Other { send(url: string): string }
+export const otherConst: (o: { utils: Other; url: string }) => string = ({ url, utils }) => {
+ return utils.send(url);
+};
+
+// written in a slot of a nested literal whose type comes from the call's parameter
+type Slots = { start?: (ctx: Ctx) => number };
+export function define(seed: { name: string } & { lifecycle?: Slots }) { return seed; }
+export const op = define({
+ name: "op",
+ lifecycle: {
+ start: ({ url, utils }) => utils.send(url),
+ },
+});
+export const op2 = define({
+ name: "op2",
+ lifecycle: {
+ start: (ctx) => ctx.utils.send(ctx.url),
+ },
+});
diff --git a/graph/test/typescript/expected/99-contextual-destructured-param.edges b/graph/test/typescript/expected/99-contextual-destructured-param.edges
new file mode 100644
index 000000000..679f22e1a
--- /dev/null
+++ b/graph/test/typescript/expected/99-contextual-destructured-param.edges
@@ -0,0 +1,9 @@
+ambiguous_unknown METHOD_CALL use#otherConst(?,?,?) @L34 -> -
+ambiguous_unknown METHOD_CALL use#untyped(any,any,any) @L28 -> -
+known_edge FUNCTION_CALL use#() @L40 -> use#define({ name: string } & { lifecycle?: Slots })
+known_edge FUNCTION_CALL use#() @L46 -> use#define({ name: string } & { lifecycle?: Slots })
+known_edge METHOD_CALL use#(?,?,?) @L43 -> Utils#send(string)
+known_edge METHOD_CALL use#(?) @L49 -> Utils#send(string)
+known_edge METHOD_CALL use#annotated(Ctx,Ctx,Ctx) @L23 -> Utils#send(string)
+known_edge METHOD_CALL use#plainConst(?) @L18 -> Utils#send(string)
+known_edge METHOD_CALL use#viaConst(?,?,?) @L13 -> Utils#send(string)
diff --git a/graph/test/typescript/expected/99-contextual-destructured-param.entries b/graph/test/typescript/expected/99-contextual-destructured-param.entries
new file mode 100644
index 000000000..0b69514d5
--- /dev/null
+++ b/graph/test/typescript/expected/99-contextual-destructured-param.entries
@@ -0,0 +1,5 @@
+── entry_point (4) ──
+ exported_from_entry_module use#annotated use.ts:22
+ exported_from_entry_module use#define use.ts:39
+ unimported_module globals# globals.d.ts:1
+ unimported_module use# use.ts:1
diff --git a/graph/test/typescript/expected/99-contextual-destructured-param.envelope b/graph/test/typescript/expected/99-contextual-destructured-param.envelope
new file mode 100644
index 000000000..1f7efd137
--- /dev/null
+++ b/graph/test/typescript/expected/99-contextual-destructured-param.envelope
@@ -0,0 +1,5 @@
+value use#@12:24 -> use#@12
+value use#@17:26 -> use#@17
+value use#@33:26 -> use#@33
+value use#@38:24 -> use#@43
+value use#@38:24 -> use#@49
diff --git a/graph/test/typescript/expected/99-contextual-destructured-param.fields b/graph/test/typescript/expected/99-contextual-destructured-param.fields
new file mode 100644
index 000000000..e203e14a0
--- /dev/null
+++ b/graph/test/typescript/expected/99-contextual-destructured-param.fields
@@ -0,0 +1,4 @@
+known_edge read use#(?) -> Ctx#url
+known_edge read use#(?) -> Ctx#utils
+known_edge read use#plainConst(?) -> Ctx#url
+known_edge read use#plainConst(?) -> Ctx#utils
diff --git a/graph/test/typescript/expected/99-contextual-destructured-param.fields-oracle b/graph/test/typescript/expected/99-contextual-destructured-param.fields-oracle
new file mode 100644
index 000000000..c9021690f
--- /dev/null
+++ b/graph/test/typescript/expected/99-contextual-destructured-param.fields-oracle
@@ -0,0 +1,7 @@
+99-contextual-destructured-param [fields]
+ precision 1.0000 (4 correct, 0 wrong)
+ recall 1.0000 (4 of 4 the compiler resolved)
+ sites 4 resolved 4 (100.0%)
+ tiers known_edge=4
+ access read=4
+ not scored: 0 rows whose target is not a client declaration
diff --git a/graph/test/typescript/expected/99-contextual-destructured-param.known-missing b/graph/test/typescript/expected/99-contextual-destructured-param.known-missing
new file mode 100644
index 000000000..e86f77872
--- /dev/null
+++ b/graph/test/typescript/expected/99-contextual-destructured-param.known-missing
@@ -0,0 +1,14 @@
+# Accepted gaps for 99-contextual-destructured-param — each line is an edge the TypeScript compiler
+# resolves and this engine does not. A NEW missing edge fails the suite;
+# a line here that STARTS working also fails, so the debt cannot rot.
+#
+# The engine DOES resolve these three (they show as `extra` with the same target): the edge differs
+# only in its caller's label. A function whose parameter is destructured is labelled with one slot
+# per bound name as well as the pattern, `(?,?,?)` for `({ url, utils })`, where the compiler names
+# one parameter. `annotated` had this before contextual typing was added; it is the label, not the edge.
+use#(?) -> Utils#send(string)
+use#annotated(Ctx) -> Utils#send(string)
+use#viaConst(?) -> Utils#send(string)
+# A parameter typed by an object type LITERAL ({ utils: Other }) has a shape, not a type
+# declaration, and a bound name reads only a declaration's fields. Not covered yet.
+use#otherConst(?) -> Other#send(string)
diff --git a/graph/test/typescript/expected/99-contextual-destructured-param.oracle b/graph/test/typescript/expected/99-contextual-destructured-param.oracle
new file mode 100644
index 000000000..90a473678
--- /dev/null
+++ b/graph/test/typescript/expected/99-contextual-destructured-param.oracle
@@ -0,0 +1,8 @@
+oracle=7 engine=6 agree=3 missing=4 (known 4, NEW 0) extra=3
+ known use#(?) -> Utils#send(string)
+ known use#annotated(Ctx) -> Utils#send(string)
+ known use#otherConst(?) -> Other#send(string)
+ known use#viaConst(?) -> Utils#send(string)
+ extra use#(?,?,?) -> Utils#send(string)
+ extra use#annotated(Ctx,Ctx,Ctx) -> Utils#send(string)
+ extra use#viaConst(?,?,?) -> Utils#send(string)
diff --git a/graph/test/typescript/expected/99-contextual-destructured-param.type-use b/graph/test/typescript/expected/99-contextual-destructured-param.type-use
new file mode 100644
index 000000000..271aea409
--- /dev/null
+++ b/graph/test/typescript/expected/99-contextual-destructured-param.type-use
@@ -0,0 +1,12 @@
+known_edge FIELD_TYPE 0 Ctx [FIELD] -> Utils
+known_edge FIELD_TYPE 2 use [METHOD_PARAM] -> Slots
+known_edge FIELD_TYPE 2 use [VARIABLE] -> Other
+known_edge METHOD_PARAM 0 use [METHOD_PARAM] -> Ctx
+known_edge METHOD_PARAM 1 use [VARIABLE] -> Ctx
+known_edge METHOD_PARAM 2 Slots [TYPE] -> Ctx
+known_edge METHOD_RETURN 0 Promise [METHOD] -> Promise
+known_edge METHOD_RETURN 0 PromiseLike [METHOD] -> PromiseLike
+known_edge SUPER_TYPE 0 CallableFunction [HERITAGE] -> Function
+known_edge SUPER_TYPE 0 NewableFunction [HERITAGE] -> Function
+known_edge TYPE_ELEMENT 4 Promise [METHOD_PARAM] -> PromiseLike
+known_edge TYPE_ELEMENT 4 PromiseLike [METHOD_PARAM] -> PromiseLike
diff --git a/graph/test/typescript/expected/99-contextual-destructured-param.types-oracle b/graph/test/typescript/expected/99-contextual-destructured-param.types-oracle
new file mode 100644
index 000000000..22b7a3067
--- /dev/null
+++ b/graph/test/typescript/expected/99-contextual-destructured-param.types-oracle
@@ -0,0 +1,7 @@
+99-contextual-destructured-param [types]
+ precision 1.0000 (10 correct, 0 wrong)
+ recall 1.0000 (10 of 10 the compiler resolved)
+ sites 17 resolved 17 (100.0%)
+ tiers known_edge=17
+ contexts FIELD_TYPE=3 METHOD_PARAM=4 METHOD_RETURN=3 SUPER_TYPE=2 TYPE_ELEMENT=5
+ not scored: 0 rows whose target is not a client declaration
diff --git a/graph/typescript/engine/resolution/contextual-params.dl b/graph/typescript/engine/resolution/contextual-params.dl
index 99b05b5a1..e54f5131c 100644
--- a/graph/typescript/engine/resolution/contextual-params.dl
+++ b/graph/typescript/engine/resolution/contextual-params.dl
@@ -370,6 +370,33 @@ expr_decl_ref(e, c) :- expr_referenced("client", "PARAMETER", p, e),
expr_type(e, prov, t) :- expr_referenced("client", "PARAMETER", p, e),
lambda_param_type(p, prov, t).
+// ── …AND THE NAMES A CONTEXTUALLY TYPED PARAMETER IS DESTRUCTURED INTO ──────
+// `const poll: (ctx: TickCtx) => R = async ({ data, utils }) => utils.http(..)`, and the same
+// arrow written where an interface member or a function-typed field is expected. The pattern
+// parameter takes its type from that context above, but a bound name reads the pattern's type
+// through param_type (reference-types.dl, binding_pattern_type), which only an annotation fills:
+// so `utils` stayed untyped and every call on it was unresolved, while `ctx.utils.http(..)` on
+// the same arrow resolved. The bound name takes the type of the field it names, exactly as for
+// an annotated pattern; its uses are typed here, beside the plain parameter's, and not through
+// param_type, which overload rejection reads under negation.
+lambda_binding_target(bh, prov, t) :- param_binding(_, _, pos, "PROPERTY", _, m, bh),
+ param_decl(_, _, pos, _, m, ph),
+ lambda_param_type(ph, prov, t).
+lambda_binding_type(bh, prov, ft) :- param_binding(_, _, _, "PROPERTY", src, _, bh),
+ lambda_binding_target(bh, _, t),
+ scope_sibling(t, sib),
+ declared_field(sib, src, _, f),
+ field_type(f, prov, ft).
+lambda_binding_shape(bh, sh) :- param_binding(_, _, _, "PROPERTY", src, _, bh),
+ lambda_binding_target(bh, _, t),
+ scope_sibling(t, sib),
+ declared_field(sib, src, _, f),
+ field_shape(f, sh).
+expr_type(e, prov, t) :- expr_referenced("client", "PARAMETER", bh, e),
+ lambda_binding_type(bh, prov, t).
+expr_shape(e, sh) :- expr_referenced("client", "PARAMETER", bh, e),
+ lambda_binding_shape(bh, sh).
+
// ── …AND A CALLBACK THAT RECEIVES A MODULE NAMESPACE ────────────────────────
// `import("./m").then((m) => m.run(1))` — the same namespace as the awaited form, only
// reached through a callback. `await` is not required for a dynamic import to be
@@ -653,6 +680,15 @@ objlit_ctx_ref(l, r) :- expr_child("client", ce, "ARGUMENT", pos, l),
param_type_ref(_, r, p).
objlit_ctx_scope(l, s) :- objlit_ctx_ref(l, r), ref_type_target(r, _, s).
objlit_ctx_scope(l, s) :- objlit_ctx_ref(l, r), ref_shape_target(r, s).
+// …and a literal written as a PROPERTY of a typed literal takes the type of the field it fills:
+// `define({ lifecycle: { start: async (ctx) => .. } })`, where the parameter is
+// `{ lifecycle?: Slots }`. The outer literal had a scope and the inner one none, so every
+// function in a nested slot (a config's hooks, a router's handlers) took no parameter types.
+objlit_ctx_ref(l, r) :- objlit_prop(p, name, l),
+ expr_kind("client", "OBJECT_LITERAL", _, l),
+ objlit_ctx_scope(p, s),
+ field_in_scope(s, name, _, f),
+ field_type_ref(_, r, f).
// a property is a KEY and a VALUE at the same position under the literal; a method is its own row
objlit_prop(l, name, val) :- expr_child("client", l, "OBJECT_PROPERTY_KEY", pos, key),
diff --git a/graph/typescript/souffle/decls_all.dl b/graph/typescript/souffle/decls_all.dl
index a703e6015..223d5f526 100644
--- a/graph/typescript/souffle/decls_all.dl
+++ b/graph/typescript/souffle/decls_all.dl
@@ -64,6 +64,9 @@
.decl builtin_promise_ref(c0:symbol,c1:symbol)
.decl lambda_param_ref(c0:symbol,c1:symbol)
.decl fn_value_kind(c0:symbol)
+.decl lambda_binding_shape(c0:symbol,c1:symbol)
+.decl lambda_binding_target(c0:symbol,c1:symbol,c2:symbol)
+.decl lambda_binding_type(c0:symbol,c1:symbol,c2:symbol)
.decl lambda_param_shape(c0:symbol,c1:symbol)
.decl lambda_param_namespace(c0:symbol,c1:symbol,c2:symbol)
.decl lambda_param_type(c0:symbol,c1:symbol,c2:symbol)
From 4fc62fcbe70a53745fcdd80d9633595adde3fe23 Mon Sep 17 00:00:00 2001
From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com>
Date: Tue, 3 Nov 2026 09:16:58 -0800
Subject: [PATCH 09/11] typescript: a name destructured from a
type-literal-typed value takes its member's type
`const { port } = opts` with `opts: { port: Port }`, and `({ port }: Opts)` with
`type Opts = { port: Port }`, left `port` untyped, so `port.execute()` was
unresolved while `opts.port.execute()` beside it resolved. A type literal has no
declaration for param_type / expr_type to name: it is a shape. The destructuring
rules for parameters and variables now read the shape too (a shape is its own
scope), for fields, nested shapes and methods.
Case 100 covers inline and aliased literals, parameters and variables, a nested
shape and a shape method, with controls: an untyped source stays unresolved, and
a look-alike literal resolves to its own method, not to the interface. Case 75:
five destructured-prop calls now resolve to the targets the compiler oracle had
listed as known-missing (the shadowing guard still holds); they still show as
missing+extra only because a destructured caller is labelled once per bound name.
On a 1.9k-file Deno TypeScript repo: known_edge 7,410 -> 7,441, ambiguous_unknown
35,820 -> 35,789, 0 resolved edges lost; spot-checked against the source.
Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
---
.../src/globals.d.ts | 11 +++++
.../src/tsconfig.json | 1 +
.../src/use.ts | 47 +++++++++++++++++++
.../100-destructure-from-type-literal.edges | 10 ++++
.../100-destructure-from-type-literal.entries | 13 +++++
.../100-destructure-from-type-literal.fields | 1 +
...estructure-from-type-literal.fields-oracle | 9 ++++
...estructure-from-type-literal.known-missing | 6 +++
.../100-destructure-from-type-literal.oracle | 7 +++
...100-destructure-from-type-literal.type-use | 9 ++++
...destructure-from-type-literal.types-oracle | 7 +++
.../expected/75-jsx-wrapper-guards.edges | 10 ++--
.../expected/75-jsx-wrapper-guards.oracle | 7 ++-
.../engine/resolution/reference-types.dl | 43 +++++++++++++++++
graph/typescript/souffle/decls_all.dl | 2 +
15 files changed, 177 insertions(+), 6 deletions(-)
create mode 100644 graph/test/typescript/cases/100-destructure-from-type-literal/src/globals.d.ts
create mode 100644 graph/test/typescript/cases/100-destructure-from-type-literal/src/tsconfig.json
create mode 100644 graph/test/typescript/cases/100-destructure-from-type-literal/src/use.ts
create mode 100644 graph/test/typescript/expected/100-destructure-from-type-literal.edges
create mode 100644 graph/test/typescript/expected/100-destructure-from-type-literal.entries
create mode 100644 graph/test/typescript/expected/100-destructure-from-type-literal.fields
create mode 100644 graph/test/typescript/expected/100-destructure-from-type-literal.fields-oracle
create mode 100644 graph/test/typescript/expected/100-destructure-from-type-literal.known-missing
create mode 100644 graph/test/typescript/expected/100-destructure-from-type-literal.oracle
create mode 100644 graph/test/typescript/expected/100-destructure-from-type-literal.type-use
create mode 100644 graph/test/typescript/expected/100-destructure-from-type-literal.types-oracle
diff --git a/graph/test/typescript/cases/100-destructure-from-type-literal/src/globals.d.ts b/graph/test/typescript/cases/100-destructure-from-type-literal/src/globals.d.ts
new file mode 100644
index 000000000..dd5843cc1
--- /dev/null
+++ b/graph/test/typescript/cases/100-destructure-from-type-literal/src/globals.d.ts
@@ -0,0 +1,11 @@
+// The case's own standard library under `noLib`.
+interface Object {}
+interface Boolean {}
+interface Number {}
+interface String {}
+interface Array { length: number; }
+interface Function {}
+interface CallableFunction extends Function {}
+interface NewableFunction extends Function {}
+interface IArguments {}
+interface RegExp {}
diff --git a/graph/test/typescript/cases/100-destructure-from-type-literal/src/tsconfig.json b/graph/test/typescript/cases/100-destructure-from-type-literal/src/tsconfig.json
new file mode 100644
index 000000000..dcbfb4f8d
--- /dev/null
+++ b/graph/test/typescript/cases/100-destructure-from-type-literal/src/tsconfig.json
@@ -0,0 +1 @@
+{ "compilerOptions": { "strict": true, "noLib": true, "target": "es2022" } }
diff --git a/graph/test/typescript/cases/100-destructure-from-type-literal/src/use.ts b/graph/test/typescript/cases/100-destructure-from-type-literal/src/use.ts
new file mode 100644
index 000000000..1012b8536
--- /dev/null
+++ b/graph/test/typescript/cases/100-destructure-from-type-literal/src/use.ts
@@ -0,0 +1,47 @@
+// A name destructured out of a value whose declared type is an object TYPE LITERAL
+// (inline, or through an alias) takes that member's type, as `opts.port.execute()` does.
+export interface Port { execute(x: number): number }
+export interface Clock { now(): number }
+
+export function inlineVar(opts: { port: Port; n: number }) {
+ const { port } = opts;
+ return port.execute(1); // -> Port.execute
+}
+export function memberAccess(opts: { port: Port }) {
+ return opts.port.execute(2); // -> Port.execute (already resolved: the reference)
+}
+type Opts = { port: Port; clock: Clock };
+export function aliasParam({ port, clock }: Opts) {
+ clock.now(); // -> Clock.now
+ return port.execute(3); // -> Port.execute
+}
+export function inlineParam({ port }: { port: Port }) {
+ return port.execute(4); // -> Port.execute
+}
+export function aliasVar(o: Opts) {
+ const { clock } = o;
+ return clock.now(); // -> Clock.now
+}
+export function nestedShape(opts: { deps: { port: Port } }) {
+ const { deps } = opts;
+ const { port } = deps;
+ return port.execute(5); // -> Port.execute
+}
+export function shapeMethod(opts: { run(x: number): number }) {
+ const { run } = opts;
+ return run(6); // -> the type literal's run
+}
+
+// controls: no declared type, and a name the shape does not have
+export function untyped(opts: any) {
+ const { port } = opts;
+ return port.execute(7); // stays unresolved
+}
+export function absent(opts: { clock: Clock }) {
+ const { port } = opts as { clock: Clock; port?: never };
+ return port; // no call
+}
+export function otherShape(opts: { port: { execute(x: number): number } }) {
+ const { port } = opts;
+ return port.execute(8); // -> its own literal's execute, NOT Port.execute
+}
diff --git a/graph/test/typescript/expected/100-destructure-from-type-literal.edges b/graph/test/typescript/expected/100-destructure-from-type-literal.edges
new file mode 100644
index 000000000..98740484c
--- /dev/null
+++ b/graph/test/typescript/expected/100-destructure-from-type-literal.edges
@@ -0,0 +1,10 @@
+ambiguous_unknown METHOD_CALL use#untyped(any) @L38 -> -
+known_edge FUNCTION_CALL use#shapeMethod({ run(x: number): number }) @L32 -> use#run(number)
+known_edge METHOD_CALL use#aliasParam(Opts,Opts,Opts) @L15 -> Clock#now()
+known_edge METHOD_CALL use#aliasParam(Opts,Opts,Opts) @L16 -> Port#execute(number)
+known_edge METHOD_CALL use#aliasVar(Opts) @L23 -> Clock#now()
+known_edge METHOD_CALL use#inlineParam({ port: Port },{ port: Port }) @L19 -> Port#execute(number)
+known_edge METHOD_CALL use#inlineVar({ port: Port; n: number }) @L8 -> Port#execute(number)
+known_edge METHOD_CALL use#memberAccess({ port: Port }) @L11 -> Port#execute(number)
+known_edge METHOD_CALL use#nestedShape({ deps: { port: Port } }) @L28 -> Port#execute(number)
+known_edge METHOD_CALL use#otherShape({ port: { execute(x: number): number } }) @L46 -> use#execute(number)
diff --git a/graph/test/typescript/expected/100-destructure-from-type-literal.entries b/graph/test/typescript/expected/100-destructure-from-type-literal.entries
new file mode 100644
index 000000000..17fbd316e
--- /dev/null
+++ b/graph/test/typescript/expected/100-destructure-from-type-literal.entries
@@ -0,0 +1,13 @@
+── entry_point (12) ──
+ exported_from_entry_module use#absent use.ts:40
+ exported_from_entry_module use#aliasParam use.ts:14
+ exported_from_entry_module use#aliasVar use.ts:21
+ exported_from_entry_module use#inlineParam use.ts:18
+ exported_from_entry_module use#inlineVar use.ts:6
+ exported_from_entry_module use#memberAccess use.ts:10
+ exported_from_entry_module use#nestedShape use.ts:25
+ exported_from_entry_module use#otherShape use.ts:44
+ exported_from_entry_module use#shapeMethod use.ts:30
+ exported_from_entry_module use#untyped use.ts:36
+ unimported_module globals# globals.d.ts:1
+ unimported_module use# use.ts:1
diff --git a/graph/test/typescript/expected/100-destructure-from-type-literal.fields b/graph/test/typescript/expected/100-destructure-from-type-literal.fields
new file mode 100644
index 000000000..33a761ca6
--- /dev/null
+++ b/graph/test/typescript/expected/100-destructure-from-type-literal.fields
@@ -0,0 +1 @@
+known_edge read use#memberAccess({ port: Port }) -> { port: Port }#port
diff --git a/graph/test/typescript/expected/100-destructure-from-type-literal.fields-oracle b/graph/test/typescript/expected/100-destructure-from-type-literal.fields-oracle
new file mode 100644
index 000000000..503afa2c0
--- /dev/null
+++ b/graph/test/typescript/expected/100-destructure-from-type-literal.fields-oracle
@@ -0,0 +1,9 @@
+100-destructure-from-type-literal [fields]
+ precision 0.0000 (0 correct, 1 wrong)
+ recall 0.0000 (0 of 1 the compiler resolved)
+ sites 1 resolved 1 (100.0%)
+ tiers known_edge=1
+ access read=1
+ not scored: 0 rows whose target is not a client declaration
+ WRONG use#memberAccess({ port: Port }) READ { port: Port }#port
+ MISSING use#memberAccess({ port: Port }) READ use#port
diff --git a/graph/test/typescript/expected/100-destructure-from-type-literal.known-missing b/graph/test/typescript/expected/100-destructure-from-type-literal.known-missing
new file mode 100644
index 000000000..28825d635
--- /dev/null
+++ b/graph/test/typescript/expected/100-destructure-from-type-literal.known-missing
@@ -0,0 +1,6 @@
+# A destructured parameter's caller is labelled with one slot per bound name, `aliasParam(Opts,Opts,Opts)`, where the
+# compiler oracle prints the annotation, `aliasParam(Opts)`. The callee is right: each of
+# these appears in the engine output under that caller label (see .edges).
+use#aliasParam(Opts) -> Clock#now()
+use#aliasParam(Opts) -> Port#execute(number)
+use#inlineParam({ port: Port }) -> Port#execute(number)
diff --git a/graph/test/typescript/expected/100-destructure-from-type-literal.oracle b/graph/test/typescript/expected/100-destructure-from-type-literal.oracle
new file mode 100644
index 000000000..a21d560fd
--- /dev/null
+++ b/graph/test/typescript/expected/100-destructure-from-type-literal.oracle
@@ -0,0 +1,7 @@
+oracle=9 engine=9 agree=6 missing=3 (known 3, NEW 0) extra=3
+ known use#aliasParam(Opts) -> Clock#now()
+ known use#aliasParam(Opts) -> Port#execute(number)
+ known use#inlineParam({ port: Port }) -> Port#execute(number)
+ extra use#aliasParam(Opts,Opts,Opts) -> Clock#now()
+ extra use#aliasParam(Opts,Opts,Opts) -> Port#execute(number)
+ extra use#inlineParam({ port: Port },{ port: Port }) -> Port#execute(number)
diff --git a/graph/test/typescript/expected/100-destructure-from-type-literal.type-use b/graph/test/typescript/expected/100-destructure-from-type-literal.type-use
new file mode 100644
index 000000000..07d5aaba0
--- /dev/null
+++ b/graph/test/typescript/expected/100-destructure-from-type-literal.type-use
@@ -0,0 +1,9 @@
+known_edge FIELD_TYPE 1 Opts [TYPE] -> Clock
+known_edge FIELD_TYPE 1 Opts [TYPE] -> Port
+known_edge FIELD_TYPE 1 use [EXPRESSION] -> Clock
+known_edge FIELD_TYPE 1 use [METHOD_PARAM] -> Clock
+known_edge FIELD_TYPE 1 use [METHOD_PARAM] -> Port
+known_edge FIELD_TYPE 2 use [METHOD_PARAM] -> Port
+known_edge METHOD_PARAM 0 use [METHOD_PARAM] -> Opts
+known_edge SUPER_TYPE 0 CallableFunction [HERITAGE] -> Function
+known_edge SUPER_TYPE 0 NewableFunction [HERITAGE] -> Function
diff --git a/graph/test/typescript/expected/100-destructure-from-type-literal.types-oracle b/graph/test/typescript/expected/100-destructure-from-type-literal.types-oracle
new file mode 100644
index 000000000..feb77f3a8
--- /dev/null
+++ b/graph/test/typescript/expected/100-destructure-from-type-literal.types-oracle
@@ -0,0 +1,7 @@
+100-destructure-from-type-literal [types]
+ precision 1.0000 (7 correct, 0 wrong)
+ recall 1.0000 (7 of 7 the compiler resolved)
+ sites 12 resolved 12 (100.0%)
+ tiers known_edge=12
+ contexts FIELD_TYPE=8 METHOD_PARAM=2 SUPER_TYPE=2
+ not scored: 0 rows whose target is not a client declaration
diff --git a/graph/test/typescript/expected/75-jsx-wrapper-guards.edges b/graph/test/typescript/expected/75-jsx-wrapper-guards.edges
index 78ebdfe74..9ccfa6609 100644
--- a/graph/test/typescript/expected/75-jsx-wrapper-guards.edges
+++ b/graph/test/typescript/expected/75-jsx-wrapper-guards.edges
@@ -7,14 +7,9 @@ ambient_terminal FUNCTION_CALL app#() @L22 -> parts#dynamic(() =Fc
ambient_terminal FUNCTION_CALL app#() @L29 -> parts#memo(Fc)
ambient_terminal FUNCTION_CALL app#() @L30 -> parts#observer(Fc)
ambiguous_unknown FUNCTION_CALL app#MemoInner() @L29 -> -
-ambiguous_unknown FUNCTION_CALL app#RenameCall({ run: () =,{ run: () =) @L67 -> -
-ambiguous_unknown FUNCTION_CALL app#ShorthandCall({ Button: () =,{ Button: () =) @L64 -> -
ambiguous_unknown FUNCTION_CALL heavy#Heavy() @L11 -> -
ambiguous_unknown FUNCTION_CALL heavy#NamedHeavy() @L15 -> -
-ambiguous_unknown JSX_COMPONENT_CALL app#(?,?) @L57 -> -
ambiguous_unknown JSX_COMPONENT_CALL app#Page() @L42 -> -
-ambiguous_unknown JSX_COMPONENT_CALL app#RenameTag({ icon: Fc },{ icon: Fc }) @L54 -> -
-ambiguous_unknown JSX_COMPONENT_CALL app#ShorthandTag({ Button: Fc },{ Button: Fc }) @L51 -> -
ambiguous_unknown METHOD_CALL app#() @L15 -> -
ambiguous_unknown METHOD_CALL app#() @L16 -> -
ambiguous_unknown METHOD_CALL app#() @L18 -> -
@@ -41,10 +36,13 @@ known_edge FUNCTION_CALL app#() @L25 -> app#factoryArgBody()
known_edge FUNCTION_CALL app#() @L25 -> parts#makeThing(() =)
known_edge FUNCTION_CALL app#() @L26 -> parts#useMemoLike(() =,unknown[])
known_edge FUNCTION_CALL app#MemoInner() @L29 -> app#memoBody()
+known_edge FUNCTION_CALL app#RenameCall({ run: () =,{ run: () =) @L67 -> app#