Skip to content

Release 0.2.1 - #1992

Merged
swapnilpaliwal-sd merged 28 commits into
mainfrom
dev
Oct 27, 2026
Merged

swapnilpaliwal-sd merged 28 commits into
mainfrom
dev

Conversation

@swapnilpaliwal-sd

Copy link
Copy Markdown
Contributor

Releasing 0.2.1

TypeScript / Deno

Python

Rebuilds

Speed (median of 4 cold indexes, quiet 16-core Linux; answers byte-identical)

swapnilpaliwal-sd and others added 28 commits October 11, 2026 10:57
)

Bundle (raw relations + IR -> graph.sqlite):
- the IR reader splits a quote-free line on tabs and keeps the character
  reader for lines a quote touches; rows the caller drops are skipped before
  they are split (call-site and expression passes)
- keyed (WITHOUT ROWID) tables are loaded in key order (stable sort of a copy)
- 16 KiB pages (AXIOM_BUNDLE_PAGE_SIZE overrides)
- the c0 index only on ext relations the query layer reads (EXT_QUERIED);
  bundle-test fails when a script names one missing from it
- AXIOM_BUNDLE_PROFILE=1 prints per-stage times

Facts export (axiomcode-impact --warm):
- const_values reads its whole-graph tables once, not once per const
- gen_table finds each decorator's first declaration in one pass
- Impact.code() copies runs of plain code whole
- the route walk and stub_sites run once for both exports
- config rows are filtered by a count of symbols rows instead of a join

Every table row, every exported fact and every impact/path --json answer
is byte-identical before and after on both measured projects.

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
…nd an orphan writes nothing

An agent session started before an engine or plugin upgrade keeps the old
version's MCP server and hooks running beside the new ones. The old one
rebuilt the graph with its engine and the new one rebuilt it back, a full
rebuild on every turn (reproduced: 3 rebuilds in 3 turns against a 0.1.9
build). The answers only said the graph was stale. And the old process
recreated .axiomcode (refresh.log, refresh.json) while the user deleted it.

- ax_orphan.orphaned(): one check for a plugin directory marked .orphaned_at,
  or one whose scripts are gone. When it fires, the refresher, hooks, timer,
  query, relink and write_state write nothing. The MCP server answers with
  "restart the agent session", stops its timer and refuses to start.
- ax_fresh.displace/contested: a build that replaces another axiomcode's graph
  records who built that graph. If the same builder rebuilds it again later,
  this version stops rebuilding it in the background and says two versions
  are running. `axiomcode index` still rebuilds it.
- The newer-graph note and the `newer` command now name the two-versions
  case and say to restart the session.
- README: restart the agent session after upgrading the engine or the plugin.

Test: tests/two_versions.py (fails 14 of 19 on the base, passes 19 of 19 now).

Refs #1835
Staging copies every IR CSV into a Souffle fact file and drops rows whose
tab-separated field count differs from the header's. BSD awk spends most of
that time splitting fields it never reads: on a ~1,300-file Python project
the step took 16-19 s of wall time per build, 13.6 CPU-s of it on the
largest IR file (expressions). perl counts tabs instead and stages the same
bytes (identical md5 over every staged file of three projects, two Python
and one Java) at about a tenth of the CPU.

The filter moves to graph/pipeline/stage-rows.sh. It picks perl when it runs
(macOS, mainstream Linux, and Git for Windows' bash, which is the shell the
pipeline runs under on win32) and falls back to the same awk program where
perl is missing, so no machine loses the step. AXIOM_STAGE_WITH=awk forces
the fallback. graph/test/tools/stage-rows-test.sh holds both to the same
stdout and stderr on the edge cases (trailing tabs, empty lines, CRLF, no
final newline, torn rows) and runs as a Java suite preflight.

Refs #1835
… nothing to reclaim, do not rebuild bundle indexes

On a ~1,300-file Python project axiomcode-index took 12-14 s. Three parts
of that bought nothing:

- csv.DictReader over the IR. The reader is QUOTE_NONE, so a row is one
  line and a field is the text between tabs; ir_rows() does exactly that
  with str.split and dict(zip()) at about half the cost. It keeps the
  reader's semantics where they are not obvious: \r, \n and \r\n each end
  a row, blank lines are skipped, a short row gets None for the missing
  columns and a long one keeps the rest under the key None.
  tests/index_reader.py compares the two on fixtures (quoted tabs and
  newlines, CRLF, lone CR, repeated header names, invalid UTF-8) and on
  any IR directory given to it.
- VACUUM of a database the bundle wrote seconds earlier. It now runs only
  when this run dropped a table it recreates (a re-index) or the file has
  free pages; a freshly bundled graph has neither.
- ce_caller, ce_callee and fa_caller duplicate the bundle's own
  idx_call_edges_caller_id, idx_call_edges_callee_method_id and
  idx_field_access_caller_id. They are created only when the graph has no
  index on that column, so an older graph keeps getting them.

graph.sqlite holds the same rows in every table (rowids included) on two
Python projects and one Java project; only index_meta.built_at and the
three duplicate indexes differ.

Refs #1835
The build's messages, SKILL.md and ax_fresh all describe
AXIOMCODE_REINDEX=1 as a rebuild ("rebuilds it with this one", "to
force"). It skipped the newer-build shortcut and the remembered languages,
but both up-to-date checks still answered "graph up to date" whenever the
files matched the file table, so it rebuilt nothing. Both checks now step
aside when it is set.

tests/refresh.py checks that `index` with AXIOMCODE_REINDEX=1 rebuilds
over an unchanged tree; the existing check beside it, the same `index`
without it, is the control.

Refs #1835
…uotes is found

The parser writes the IR as RFC 4180 over a tab: EntityUtils.escapeTsv turns
newlines, CRs and tabs into two-character escapes first, then quotes a field
that holds a double quote and doubles the quote. axiomcode-index read it with
QUOTE_NONE semantics, so the bundle decoded these fields and the index did not:
a literal `Order "A-1" Is Late` was stored with doubled quotes, `impact`
on that string said it appears in no literal, `impact KEY` printed
'a \""b\"" c', a Java annotation was stored as @route("""/x"""), and a C#
member read on a string literal was named `Length"`.

ir_rows() now decodes a field that opens with a quote exactly as the bundle's
readRfc4180 does (a doubled quote is one, the closing quote ends the field, a
quote elsewhere is text). A line with no quote is still a plain tab split, so
the reader costs the same. A quoted field left open at a line end takes the
next line, as the bundle's reader does; escapeTsv never writes one.

The hand-written decoders that ran on the quoted text go: decorations'
unquoted() (Python decorator text starts with @, so it can no longer fire), and
the C# literal un-doubling, replaced by cs_string_text(), which reads the C#
token: regular, verbatim (a doubled quote is one) and raw (as many quotes close
it as open it). On decoded text the old un-doubling read the raw string
""""a "" b"""" as "a " b" and left a verbatim string's doubled quotes.
C# verbatim and raw strings are now literals. ax_registration's '"""' replace
stays: a Python triple-quoted decorator path still needs it.

INDEX_VERSION 10. On four measured projects every table but literals,
comments and decorations is identical, and those now match an index read with
csv QUOTE_MINIMAL row for row.

Tests: tests/index_reader.py holds ir_rows to csv.DictReader and to the built
bundle reader, with the decoded values written out; tests/cases
quoted-literal-text for python, java and csharp, with controls.

Refs #1835
A bound or unbound method or a function named as a VALUE and not called
(executor.submit(self._work, x), Thread(target=svc.run), sorted(xs,
key=self.rank), [h.on_event], partial(svc.cancel, oid), view_func=
views.index, return self._handle) had no caller in the Python graph, so
impact OrderService.cancel did not name the function that queued it and
path said the two were independent. The engine already resolved what each
of these expressions denotes; it dropped the answer because no call
wrapped it.

call-edge-generation/method-reference.dl emits it as a call_chain_edge
with kind METHOD_REFERENCE and tier callback_registered, the tier the
other front ends use for a function handed over, so answers say "handed
over as a value" and never "calls it". Only what is resolved: a typed
receiver through the MRO, a class's unbound method, a scoped name, a
module member. Refused: an untyped receiver (stays as unresolved as
thing.cancel() is), a flowed value (a parameter, a name or attribute
assigned from an expression), a property read, a decorator application,
the argument of a one-dunder builtin (str(f)), and a comparison operand.

Measured on three open-source Python projects used for tuning: 3-4% more
resolved caller->callee pairs, and 20 of 20 sampled new edges right after the
guards (17 of 20 before the flowed-value guard). On three held-out projects,
graded blind by a reviewer who did not tune it: 39 of 40 sampled new edges
right (Wilson 95% interval 0.87-1.00). The one miss is a function handed to an
unregistration call; the only repeated wrong shape left is a function passed
to inspect.signature, about 1% of the new edges. The engine goldens gain only reference rows; the
torture oracle scorecard is unchanged.

Refs #1832
`<div onClick={h} ref={r}>` produced no expression row in the TypeScript
parser, so nothing in the IR said a handler or a ref callback sat on a
plain tag: an attribute's value existed only as an unattached embedded
expression. The JavaScript parser records the same tag as an element.

An intrinsic tag (`<div>`, `<my-el>`, `<svg:circle>`) is now a
JSX_INTRINSIC_ELEMENT row with no child and no call site, rooted wherever
it sits (a return, a ternary branch, another element's children, a
function inside an attribute). It is never added to the call nodes, so
nothing can resolve it into JSX.IntrinsicElements. Component tags keep
their JSX_ELEMENT / JSX_SELF_CLOSING rows and JSX_COMPONENT_CALL sites.
A JSX_INTRINSIC_ELEMENT row said a plain tag was there but not which
one: `<a>`, `<form>` and `<iframe>` were indistinguishable, so nothing
reading the IR could ask about a particular element without going back
to the source.

The row now carries the tag as written (`div`, `my-el`, `svg:circle`)
in potentialQualifiedName. A component element is unchanged: its tag is
still the METHOD_NAME child that resolves, and its own
potentialQualifiedName stays empty. No engine projection reads this
column for expression rows, so call sites and call edges cannot move.
…er branch it merged into

GitHub closes issues from "Closes #N" only when the merge lands on the default branch. Pull requests into
integration or release branches (0.2.1, apps/integration-*) leave their issues open after the work reaches
dev. On each push to dev this closes them, with a comment naming the pull request. "Part of #N" and
"Refs #N" are not closing words, so umbrella issues stay open. Manual runs are dry runs by default.
ci: close the issues a pull request names once it reaches dev
…utes

parser(ts): a plain JSX tag like <div onClick={h}> gets an element row (still no call site)
python: a method handed over as a value reaches its target (#1832)
Updated the description to emphasize code correctness and reduce bugs.
A file under a deno.json / deno.jsonc now resolves its imports through Deno's
rules before the compiler's: the import map (imports, scopes, an importMap file,
and a package key serving its subpaths), workspace members by name through their
exports, jsr: packages at the version the lockfile pins (or the highest cached
one in range), npm: packages in node_modules or Deno's npm cache, and https:
modules as downloaded, following redirects and x-typescript-types.

jsr: and URL modules are copied from Deno's cache, with everything they import,
into a per-project mirror laid out as vendor/ is, so an import resolves to a
real .ts file and the mirror stages as one library whose module paths carry the
package and version. `--library auto` finds the mirror and the npm packages a
Deno project imports through the parser's own resolver (dist/deno-libraries.js).

Also: deno.json marks a TypeScript project (a workspace is one project); Deno
files with no tsconfig compile under Deno's defaults (ESNext, Bundler, strict);
a barrel re-exporting a workspace member by name links; vendor/ beside a
"vendor": true config is not walked as project source; a file two programs
would both take is analysed once; npm:/jsr: specifiers name their package and
URL or scheme specifiers name none; a deno.json or deno.lock edit marks the
graph stale.

Outside a Deno project nothing changes: the resolver answers nothing.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
`import * as z from "./x"; export { z }`, `export default z` and
`export * as core from "./m"` export a namespace under a name. The module
graph had no way to carry that: module_export_any rows name an entity, so
an importer's `import { z } from "pkg"` bound nothing and every member
call through it had an untyped receiver.

module_export_namespace carries the module-valued export through bare
exports, `export * as`, named re-exports and `export *` barrels, and feeds
import_binds_namespace_any for named and default imports. A property
access on a namespace that is itself an exported namespace (`z.iso.date`)
is a namespace too.

EXPORT_STAR_AS_NAMESPACE is no longer expanded as a star: it put every
member of the source module on the barrel's own surface, so a bare
import could bind a name that is only reachable as `core.x`.

On a 157k-line project importing zod as `import { z }`: unresolved sites
27,414 -> 6,496, library edges 12,529 -> 33,447 (all single-target), no
client edge gained, one spurious callback edge dropped. A control project
importing zod as `import * as z` is byte-identical (65,009 edges) at the
same solve time. TS suite 101/101 with no golden change; case suite
72 cases.

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

A solution-style root whose references sit in a generated directory --
Nuxt's tsconfig.json is `files: []` plus references to
.nuxt/tsconfig.app.json, .server, .shared and .node, each including
`../app/**` and the like -- extracted almost nothing: each referenced config
was rooted at its directory, .nuxt/, and a walk of .nuxt/ finds none of the
files it claims. They then reached no program and no skip row.

programsOf now returns such a config as a program of its own, which
analyzePrograms runs over the whole root (TypeScriptAnalysisOptions
.programConfig): the files it claims, each under that config, in a separate
program, so app and server keep separate global scopes. A config whose
files all lie under its directory is still that directory's program.
A file a config claims that no program run extracted is now reported as
PROGRAM_NOT_EXTRACTED instead of vanishing.

A stock Nuxt 4 app: 2 of 63 modules -> 63; 0 -> 1,299 call sites under
app/ and 460 under server/. Eight TS repos (six using project references,
two not): identical file sets, governing configs and skip rows. TS checks
50/50, JS 49/49, discovery and Deno green; engine TS suite 101/101; case
suites TS 356/356, JS 354/354.

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
…json deps, @types twins (#1986)

* typescript(deno): lockfile redirects, a caret in a URL, npm ranges over node_modules/.deno

Found by checking six public Deno projects import by import against deno info.

- A remote redirect Deno 2 recorded only in deno.lock (`<cdn>/pkg@1.7` to
  `pkg@1.7.5`) is followed; it often has no cache entry of its own.
- A URL with `^` in its path finds the cache entry: Deno hashes the path as
  written, the WHATWG parser here escapes `^` to `%5E`.
- An npm: range the hoisted node_modules copy does not satisfy resolves into the
  matching node_modules/.deno/<name>@<version> copy (`npm:pkg@^3` beside a
  hoisted 4). A package typed by its @types twin keeps the twin.
- `--library auto` says how many Deno dependency imports resolve to nothing and
  to run `deno install`, instead of staging nothing silently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* typescript(deno): package.json dependencies under nodeModulesDir none, and @types twins

- Under `"nodeModulesDir": "none"` (or `auto`), a bare specifier that no import
  map or member claims, but that the nearest package.json declares, resolves
  into Deno's npm cache, as Deno serves it. Under `manual`, Deno's default
  beside a package.json, Deno demands node_modules and refuses otherwise, so
  nothing is answered and the compiler's own resolution stands.
- A package in Deno's npm cache that ships only JavaScript resolves to its
  cached @types twin, the file Deno type-checks against.

On a 46-config Deno workspace whose Node-style members declare their
dependencies in package.json, this is most of what was left unresolved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The graph has no ANALYZE statistics, so SQLite picks among equal-cost
single-column indexes by creation order. With only the bundle's
idx_<table>_<column> indexes, `field_access WHERE caller_id=? AND access=?`
planned on idx_field_access_access and `call_edges WHERE caller_id=? AND
callee_method_id=? AND tier=?` on idx_call_edges_tier. Skipping ce_callee,
ce_caller and fa_caller made the impact warm 1.5-1.9x slower on large Java
repositories (facts ready 296 s -> 550 s on a 4.6k-file one, four rounds on
a quiet 16-core machine; warm 79 s -> 54 s on an ~8k-file one).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
plugin: two axiomcode versions over one graph rebuild once, say so, and an orphan writes nothing (#1835)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
perf(build): stage IR with perl, a faster axiomcode-index, and AXIOMCODE_REINDEX=1 rebuilds; same graphs (#1835)
perf: faster graph export — bundle and impact facts, same answers
plugin: axiomcode-index decodes quoted IR fields, so a literal with quotes is found (#1835)
@swapnilpaliwal-sd
swapnilpaliwal-sd merged commit cea9119 into main Oct 27, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant