Skip to content

Agent-asserted call edges: list the sites an answer stops at, link them, walk them as [asserted] (#1892) - #1895

Draft
swapnilpaliwal-sd wants to merge 3 commits into
apps/integration-0.1.9from
feat/asserted-edges
Draft

swapnilpaliwal-sd wants to merge 3 commits into
apps/integration-0.1.9from
feat/asserted-edges

Conversation

@swapnilpaliwal-sd

Copy link
Copy Markdown
Contributor

Closes #1892 (draft).

Symptom

An answer that reaches a call the graph could not resolve (a callable read with getattr or a computed key, a handler kept in a table, a reflective invoke, a delegate or dynamic value) says only "N unresolved call(s) inside — a lower bound". The agent can often read the code and see where the call lands, but nothing lets it tell the graph, so every later impact / path / tests answer stops at the same site.

Mechanism

  • The gaps as a work list. impact and path list the sites they stopped at (--json: unknown_sites, up to 30; text: --unknown; the front door and MCP: a short to resolve: block). Each site is file:line:col (the column of the callee's name), the call as written, the engine's own reason, and up to five ranked candidate targets from what the graph already knows (the engine's own target set, values the callers pass into the called parameter, a computed name's constant prefix, callables handed over as values in the same file, declarations of the name called). A library call that runs a value (Method.invoke, MethodInfo.Invoke, Function.apply) and a call through a function-typed holder are listed too.
  • axiomcode link <file:line[:col]> <target> (CLI and an MCP link tool) records the assertion in axiomcode-links.tsv at the repository root — committable on purpose: .axiomcode/ is derived and routinely deleted, a link is knowledge someone read the code to get. Each row carries the line's text hash, the column, the callee as written, the enclosing callable and the target as the graph names it. axiomcode link lists every link with its status; link <site> - removes; link <site> not:<target> (or --not) rejects a lead.
  • Validation, language-agnostic. A link applies only if a call is written at that site whose callee is consistent with the target (same name; the class of a constructor; or a call through a value — the callee names no callable, the call is computed/reflective, or the engine's reason says so; a member call on an untyped receiver only to its own name; a construction only to its type), the target is a declaration in the graph, and the line's text is unchanged. Two calls one name on a line are never chosen between: the column is required. A rejected or stale link is reported, never applied.
  • Applied as a call edge of tier asserted (certainty asserted, never resolved), at the end of every index (so a rebuild keeps them, O(links)), on link itself against the existing graph, and on the next query after the links file changes. Links only add edges; nothing the engine wrote is removed.
  • What the link makes resolvable after it. When the target's return type is known (declared, a JSDoc @returns, or every return constructing one type / returning this), calls chained on the result, on a local assigned from it (once), awaited, or on each element of a returned collection resolve to that type's members, also as asserted, and drop with the link.
  • Rejections. A by-name or one-of-a-set lead can be rejected per site; the walks skip it (impact's by-name rules and calls facts, path's edges and by-name closure), the engine row stays. An edge the engine resolved is refused. AXIOMCODE_LINKS_PREFER=1 additionally skips a linked site's own guesses.
  • Following edits. A link follows its line by text when lines above it move, but only within the callable it was made in (an identical line in another function is not it), re-maps its column through re-spacing, and drops when the line, the target (renamed / deleted / moved) or the file goes.
  • Cost. link patches the derived facts the walks read (path edges, impact calls / certainty table) and moves their stamps, instead of re-exporting.

Numbers

Stress (scenarios per language on one real repository each plus the synthetic cases; link N sites, edits above/on the line, target renamed/moved, file deleted, query during a rebuild, fans and duplicates, wrong links, checkout and back, a corrupt links file, two calls of one name on a line, typing what follows a link, rejections): every scenario passes in every language where the repository can exhibit it; the misses are data limits (a subject with 4 unknown sites cannot take 20 links).

Latency on the largest Python subject (11.7k call edges): link 0.08 s; the next impact 0.85 s with the patched facts vs 2.31 s re-exporting; the next path 0.25 s. A rebuild applies 200 links in 48–161 ms.

Simulated perfect asserter on the 16-repo Python corpus (a runtime trace says which unknown site calls which project function; links only those, validated by the same rules): 2,828 links.

all 16 repos base add-only add + prefer + reject
reach recall (src) 0.740 0.789 0.789 0.781
reach precision (ran) 0.675 0.650 0.653 0.711
reach F1 0.706 0.713 0.715 0.744
tests recall 0.851 0.872 0.872 0.868
tests precision 0.417 0.396 0.396 0.403
path found 0.727 0.792 0.792 0.785
path test→target found 0.559 0.685 0.685 0.682
callers recall / precision 0.828 / 0.921 0.889 / 0.924 0.889 / 0.924 0.889 / 0.954

Held-out alone: path found 0.628 → 0.745, reach recall 0.517 → 0.569. Of the runtime edges the graph misses, 26% sit in a caller with no unknown site (no link can reach them) and another 26% in a caller with several value calls the trace cannot tell apart. The graph's candidates named the true target for 45% of linked sites (top-1 25%). Rejections (22k, simulated) recover precision but cost a little recall: a rejected lead sometimes covered a true caller the graph misses elsewhere. Prefer mode moved almost nothing.

Validation

tests/cases/<lang>/asserted-links and asserted-links-derive in all five languages (fail on the base: the verb does not exist), with controls: a target that does not exist, a line without a call, a call naming another declaration, a member call on an untyped receiver, the linked line edited, an unlinked sibling site, a resolved edge that cannot be rejected. tests/run.py gained "edit" (a file edited before a check, restored after the case) and "env". Full suite, tests/front_door.py, tests/surfaces.py, tests/mcp.py: see the comment below.

Not done

  • Typing what follows a link through generics substituted from the call, destructuring, pattern matching (instanceof T t, match/case, is T t), records' accessors, tuples: not typed at apply time (needs the engine's solve); a link still applies, nothing is derived there.
  • Rejecting by key, decorator by name, protocol and library callback leads: refused for now (only by-name and set members).
  • The SQL port of impact (AXIOMCODE_SQL) does not read rejections.

swapnilpaliwal-sd and others added 3 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>
…'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

Copy link
Copy Markdown
Contributor Author

Validation on the pushed head:

  • tests/run.py, all five languages: 1718 of 1718 checks in 331 cases (10 new cases, 123 checks).
  • tests/front_door.py: 21 of 21. tests/mcp.py: ok.
  • tests/surfaces.py: one failure, pre-existing on the base (the README CLI section names the flag --fresh); this branch adds nothing it flags.
  • No engine rule or front end changed: the links are applied where graph.sqlite is indexed and read by the query layer, so the engine suites are unaffected.

The README row for the new verb is not in this PR: the README already contains a term the repository's scrub hook blocks, so any commit touching it is refused until that is cleaned separately.

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