Skip to content

TypeScript: multi-line test registrars, runner set-up files, Object.assign function objects, overload implementations (stacked on #1882) - #1888

Merged
swapnilpaliwal-sd merged 16 commits into
apps/integration-0.1.9from
fix/ts-recall-next
Oct 10, 2026
Merged

swapnilpaliwal-sd merged 16 commits into
apps/integration-0.1.9from
fix/ts-recall-next

Conversation

@swapnilpaliwal-sd

Copy link
Copy Markdown
Contributor

Stacked on #1882 (release/0.1.9-impact-loops): the diff against apps/integration-0.1.9 includes #1882 until it merges; this branch adds the four commits listed below.

TypeScript test selection and the other query verbs, measured with a mutation oracle (a function made to throw on entry; truth = the test files that newly fail, cached per mutant from the previous loop's runs and re-queried locally). 9 repositories: 5 used for tuning, 4 held out from the previous loop. No true test file is lost on any mutant, for impact or for tests.

Why the held-out set did not move last loop. Its micro recall (0.736 here, 0.74 then) is one monorepo: two mutants whose function a vitest globalSetup runs fail 180 test files each, which is 347 of the set's 361 missed pairs. Macro recall was already 0.894. Fix 2 is that shape. It was diagnosed on that held-out repo, so the held-out number below is not a clean held-out measurement for fix 2. The other fixes were chosen on the tuning set.

1. A test whose registrar call opens on an earlier line is a test (3d60d3e, query layer)

  • Symptom: a formatter puts a long test name on a line of its own (it(\n 'name',\n () => …), test.each(rows)(\n …)). The rule that names an anonymous callable a test reads only the callable's own line, so the test was test code but no test. A file written entirely this way was never selected.
  • Fix: the callable is a test when its line starts with the function value and the innermost multi-line call opened above it names a registrar. Control: a callback on its own line, handed to an ordinary call inside a test body, stays a callback.
  • Population: the shape occurs in 7 of 11 cloned repositories (6 to 46 calls each). On one tuning repository it lost a whole test file on 9 mutants, and missed pairs went 16 → 7.

2. A vitest / jest set-up file runs before the tests it configures (0ad6936, query layer, ax_runner_setup.py)

  • Symptom: setupFiles, setupFilesAfterEnv and globalSetup are files that nothing imports or calls. A function they reach therefore reached no test, yet breaking it fails every test of the project, and for a global set-up, every test of the run.
  • Fix: string entries of those keys are read from the runner configurations. The set-up module, plus the function a global set-up exports to the runner, becomes a runs_before fixture of each test under the configuration's root. For a global set-up, the root is the nearest directory above that lists projects. Control: a helper file that no configuration names reaches no test.
  • Population: set-up files that call project source occur in 2 repositories (one with 7 packages).

3. Object.assign(fn, members) evaluates to fn (25296c2, engine rule)

  • Symptom: a callable API built as a function object (export const api: Factory = assign(createApi, members)) and typed by a callable interface ran nothing when called.
  • Fix: value_branch follows the merge to its first argument, whether Object.assign is written out or reached through an alias const assign = Object.assign. Control: a function passed as a later source is not what a call of the result runs.
  • Population: 22 such merges across 3 repositories.
  • Effect: one tuning repository +35 pairs (+166 edges), and a second +96 edges with no file-level change.

4. A call that selected an overload signature reaches the implementation (26b20b4, query layer)

  • Symptom: calls bind to bodiless overload signatures, so the implementation had no caller in any verb. impact <file:line> said "the change is local", tests on an edit selected nothing, and path called the test and the implementation independent.
  • Fix: the query layer reads the engine's own ext_call_runs_edge. Impact facts gain a verified edge, the fast path gains it too, and path shows an overload hop. Controls: a same-named function in another module is not in the set. A placeholder name (<arrow>) names no set; the existing member-assigned-function-names case caught that in a first version.
  • Effect: tests recall 0.79 → 1.00 and 0.93 → 0.96 on two tuning repositories; impact +7 and +23 pairs on two more; +1 on a held-out one.

Results, base (#1882 tip) → this branch

set impact R micro / macro impact P tests R micro / macro tests P empty selections (impact / tests) path reached (forward only) context @1 / @3 / @10
tuning (5 repos, 105 mutants) 0.696 → 0.758 / 0.801 → 0.831 0.470 → 0.471 0.684 → 0.757 / 0.751 → 0.830 0.451 → 0.457 6 → 3 / 8 → 3 179 → 192 / 229 (171 → 187) 3 / 8 / 16 of 31, unchanged
held out (4 repos, 95 mutants) 0.736 → 0.993 / 0.894 → 0.975 0.272 → 0.308 0.736 → 0.992 / 0.894 → 0.975 0.296 → 0.333 1 → 1 / 2 → 2 163 / 183, unchanged 2 / 3 / 9 of 18, unchanged

Files selected for mutants whose truth is empty: 97 → 97 on tuning, 167 → 177 on held out. The +10 is one mutant whose newly credited multi-line tests reach the target by name.

Verbs with no gold here: direct and transitive impact callers have no runtime trace for TypeScript, so they are scored through the test files they select. context has only 49 mutants with a usable doc-comment question, and its score does not move.

Cost: engine solve time on the same IR is unchanged (interleaved, 2 runs each: 3.9–4.1 s vs 4.0 s; 8.8–10.1 s vs 9.5 s; 9.4–9.5 s vs 8.8–9.6 s). Edges change by +166, +96 and 0.

Checks: tests/run.py TypeScript 317/317, JavaScript 319/319, Python 330/330, Java 343/343, C# 241/241; TypeScript engine suite 101/101 with no golden change; tests/fastpath.py --lang typescript 6/6. Each new case fails on base and passes here: test-registrar-modifiers, runner-setup-files, object-assign-function-object, overload-implementation.

Open, not fixed (one repository each so far):

  • A workspace package whose dist/ holds dev stubs named after the package (pkg.cjs.d.mts → export * from "../src/index.js"). This accounts for 27 of 53 remaining misses on that repository.
  • JS tests that sit outside every tsconfig, in a repository whose vendored minified bundles under docs/ make the JavaScript solve run for more than 24 minutes. This accounts for 158 of that repository's 236 remaining misses.
  • Function-object members (f.resolve = g, Object.assign source members) read through a callable interface's member. A rule was written and gave path +3 and recall +0, because the next hop breaks. It is not included.
  • JSON.stringify(x) running x.toJSON.
  • import() of a module inside a test.
  • A React memo / HOC hand-back.
  • A fixture module whose class decorators register at import.
  • A vitest import.meta.glob fixture loader passed into a library.

swapnilpaliwal-sd and others added 4 commits October 9, 2026 19:01
A formatter puts a long test name on a line of its own: `it(` or `test.each(rows)(` opens the call, the name follows,
and the arrow starts a later line that names no registrar. The rule that names an anonymous callable a test reads only
its own declaration line, so the runner's test was test code but no test: a file whose tests are all written this way
was never selected, and the change it covers listed it only as "a route this count does not credit".

The callable is now a test when its line starts with the function value and the innermost multi-line call opened above
it names a registrar on its first line. Innermost, so a callback on its own line handed to `settle(` inside a test body
stays that call's argument (control). The same helper serves the impact facts and the query layer's test sets.

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
A runner configuration names files it loads before every test file of the project (setupFiles, jest's
setupFilesAfterEnv) and files whose exported setup it calls once before the run (globalSetup). Nothing imports or calls
them, so a function they reach -- a client a set-up file configures at module level, a code generator a global set-up
runs -- reached no test, while breaking it fails every test of the run.

ax_runner_setup reads the string entries of those keys in vitest/vite/jest configuration files (relative, `<rootDir>/`,
`__dirname + '/...'`, jest's rootDir), skipping a package's own set-up. The set-up file's module, and for a global set-up
the function it exports to the runner (`export const setup = codegen`, `export default fn`, a declared setup/teardown),
is a fixture (runs_before) of each test under the configuration's root; a global set-up's failure aborts the whole run,
so its root is the nearest directory above that lists projects. Control: a helper file no configuration names reaches
no test.

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
…the function object runs it

A callable API is often a function object merged with its members and typed by a callable interface whose call
signature has no body: `export const widget: Factory = assign(createWidget, members)`,
`Object.assign(task, { started, finished })`. The holder held the merge's result, for which nothing returned a
function, so a call of the merged object ran nothing and everything below the function had no caller from the
package's own tests or from the packages that import it.

value_branch now follows `Object.assign(target, ...sources)` to its first argument, written out or through an alias
whose value is `Object.assign` (`export const assign = Object.assign`), never by the bare name. Control: a function
passed as a later source is not what a call of the result runs.

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
…d set's implementation

The compiler binds `pick(1)` to `function pick(x: number): number`, a declaration with no body, and call_edges records
exactly that. The implementation below the signatures -- the one body that runs -- had no caller in any verb: an edit
to it reached no test (`tests`, `impact <file:line>`: "the change is local"), anything it calls was cut off from the
tests above it, and `path` called the test and the implementation independent.

The engine already answers which body runs (ext_call_runs_edge, from call_runs_method). graph_sql.overload_impl_edges
pairs it with the call site that selected a same-named signature declared above the implementation (same file, same
owner), and every reader takes the pair: impact's calls facts (a verified edge, at the site's line), the fast path's
edge list, and path, as an `overload` hop. Controls: a same-named function in another module is no part of the set,
and a placeholder name (`<arrow>`, shared by every anonymous arrow of a module) names no set at all -- without that
guard member-assigned-function-names cited a caller at another arrow's call.

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
swapnilpaliwal-sd and others added 3 commits October 9, 2026 23:42
Measured against runtime traces on the Python oracle corpus, every false
transitive-reach row has a static edge the runtime never made (the scorer
accepts any runtime caller at any depth, so context-insensitivity cannot
produce one). Two of the edge kinds are wrong by construction and are
fixed here; reach precision 0.675 -> 0.724, reach recall -0.1pt (src) /
-0.2pt (test-side), test-file recall -0.1pt, callers and path unchanged.

1. A decoration string naming a PARAMETER of the decorated declaration is
   no registration key (ax_registration.decoration_key_strings, shape 4),
   nor is a string inside another call in the decoration (shape 5).
   `@option("--params", "-p", "params") def main(params)`: the flags are
   what a caller writes to reach the command; the parameter name is written
   by every function that builds a dict with that key, and the by-key join
   made each of them a caller of the command. The flags stay keys.

2. The "decorator by name" hop lands on what the decorator RETURNS, not on
   the decorator's body. It used to run decorated -> decorator, making the
   decorated function (and everything reaching it: a route's tests) a
   caller of whatever the decorator calls. Calling the decorated name never
   runs the decorator; it runs what the decorator returned:
     - bare `@d`: d's return; written as a call `@d(...)`: the return of
       what d returned (decorated_call);
     - a wrapper (fn_returns, from the engine's method_returns_method): the
       decorated declaration now stands for that wrapper;
     - the function handed back unchanged (fn_returns_param, a registering
       decorator): no hop at all; the decoration itself runs at import,
       which the import walk already follows;
     - unknown return: the old hop stays (a route the walk cannot judge).

IMPACT_VERSION 70 (new facts fn_returns, fn_returns_param, decorated_call).
Cases: decorator-by-name-runs-its-wrapper, option-parameter-name-is-no-key
(each fails on the base, with a control that must not change);
decorator-the-project-declares now expects the route it really takes
(`at import`) instead of the by-name hop.

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
… its class

`cls: type[X] | None = None` and `Optional[Type[X]]` are the default-None
spelling of a class-object parameter: the body swaps None for a default class
and calls `cls(...)`. Only a top-level `type[...]` annotation was read, so the
call stayed `callee_is_parameter` and every constructor behind it lost that
caller (a decorator factory building its command class in a closure is the
common shape). param_class_object_ref now names the `type[...]` subscript an
annotation is or one of its union operands is, and both the concrete
(expr_type_class_object) and the bounded-TypeVar (param_class_object_bound)
rules read it. The existing top-level rules are unchanged.

Corpus: one-hop callers src 0.828 -> 0.829, path found 0.727 -> 0.729,
test->target path 0.559 -> 0.562; reach and tests unchanged; engine suite and
torture unchanged (agree 588 / missing 46 / extra 37); index time flat.
Case: optional-class-object-parameter (fails on the base; the bare
`type[Command]` control passes on both).

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
swapnilpaliwal-sd and others added 6 commits October 10, 2026 01:13
…m, walk them as [asserted] (#1892)

An answer that stops at a call the graph could not resolve now lists the site (file:line:col, the call
as written, the engine's reason). axiomcode link <file:line[:col]> <target> records where it lands in
axiomcode-links.tsv; the graph validates each link (a call there consistent with the target, the
target declared, the line text unchanged) and applies it as a call edge of tier asserted, never
resolved. Links only add edges, survive rebuilds, follow their line by its text within the callable,
and drop (reported) when the line is edited. A link whose target declares a return type also resolves
calls chained on the result and calls on a local assigned from it.

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
…ha load-time throw selects the whole run

impact
- imports_fact resolves a directory import (`require('..')`, `require('../')`, a folder with a
  package.json main/exports), the repository's own package name, a side-effect `import './x'` and a
  literal `import('./x')`; a package whose entry is a build output the checkout lacks names its
  src/ (or lib/) entry. imports_dynamic records the files that load a module by a computed name.
- loads_change asks whether the test loads the asked declaration's FILE, not the end of the
  shortest route (which can run through another test file's subclass or callback); it says nothing
  where the target file has no importer, the test file imports nothing here, or the test's import
  closure holds a computed-name loader.
- a JavaScript test whose route is in-process (sound, one of a set, dispatch, registered, defines,
  fixture) and whose file never loads the changed file is kept apart as `not_loading_tests` and
  named on a `not counted:` line, never selected: a plugin's prototype override is credited only to
  the tests that load the plugin, and a callback one test file hands to a shared function no longer
  pulls in every other caller's tests. Key joins, remote hops and spawns are unaffected.
- load_run (impact.dl): test-code declarations that reach the change through calls alone; a test
  credited through its module body or describe() block that is one of them carries `at_load`.

path
- from a JavaScript test file that never loads the target's file: "no route: X never loads Y".

test-impact
- a selected JavaScript file no runner collects is replaced by the collected test files that
  require it, or moved to not_collected when none does (unless a test file loads modules by a
  computed name).
- a mocha run whose specs reach the change at load time (`at_load`) lists every spec that run
  collects: mocha loads them in one process and a throw there fails all of them. jest/vitest are
  per-file and unchanged.
- a changed function-constructor (`function Foo(opts) {` header edit is a TYPE to `changed`) is asked
  again as the function when impact finds no type at that line.

Measured on 8 JavaScript subjects against cached mutation truth (no-library protocol), base ->
this: impact R 0.522 -> 0.542, P 0.238 -> 0.295; tests R 0.494 -> 0.559, P 0.264 -> 0.310; held-out
impact P 0.156 -> 0.478 at equal recall; no failing (test file, target) pair lost in impact, tests
or path on any subject.

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
IMPACT_VERSION 70 carries both 69s (the library-callback receiver sites and the JavaScript import facts).

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
…'s result (#1892)

A site is file:line:col (the callee's name); two calls of one name on a line are never chosen between.
link --not rejects a by-name or one-of-a-set lead at a site (never an edge the engine resolved); the
walks skip it, nothing is deleted. AXIOMCODE_LINKS_PREFER=1 skips a linked site's own guesses.
Unknown sites carry ranked candidates from what the graph knows (the engine's set, values passed in,
a computed name's prefix, values registered in the file, the name called). A link whose target has a
declared, JSDoc or inferred return type resolves the calls chained on its result, on a local assigned
from it, awaited, or on each element it yields. Front-door answers list confirmed places before leads.

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
…cts a lead; candidates ranked by the name called (#1892)

A decorator factory's call and the decoration applying its result are one call at one column. Stress
harness scenarios S1-S13 pass in all five languages where the subject can exhibit them.

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
@swapnilpaliwal-sd
swapnilpaliwal-sd marked this pull request as ready for review October 10, 2026 17:40
…o fix/js-impact-f1

# Conflicts:
#	plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact
# Conflicts:
#	plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact
# Conflicts:
#	plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact
#	plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path
@swapnilpaliwal-sd

Copy link
Copy Markdown
Contributor Author

Tracked in #1897 (0.1.9 batch); merge in the order listed there.

@swapnilpaliwal-sd
swapnilpaliwal-sd merged commit b26f444 into apps/integration-0.1.9 Oct 10, 2026
12 checks passed
@swapnilpaliwal-sd
swapnilpaliwal-sd deleted the fix/ts-recall-next branch October 10, 2026 18:22
swapnilpaliwal-sd added a commit that referenced this pull request Oct 10, 2026
TypeScript test selection: leave out tests that never load the change, type tests, package-imported names (stacked on #1888)
This was referenced Oct 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant