Repository navigation
Release 0.2.1 - #1992
Merged
Merged
Release 0.2.1#1992
Conversation
) 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>
… keeps its own version)
…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)
Release 0.2.1
swapnilpaliwal-sd
requested review from
JaredHLZhang and
suyashpaliwal26
as code owners
October 27, 2026 01:22
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Releasing 0.2.1
TypeScript / Deno
jsr:at the lockfile's version,npm:, andhttps:modules mirrored from Deno's cache (typescript: resolve imports the way Deno does in a Deno project (#1984) #1985), plus lockfile-only redirects,^in URLs, npm ranges served fromnode_modules/.deno, and@typestwins (typescript(deno): what a stress test against deno info found: lockfile redirects, ^ in URLs, npm ranges, package.json deps, @types twins #1986)export { z },export * as core) binds, so calls through it resolve<div onClick={h}>) gets an element row (parser(ts): a plain JSX tag like <div onClick={h}> gets an element row (still no call site) #1969)Python
Rebuilds
AXIOMCODE_REINDEX=1always rebuilds (perf(build): stage IR with perl, a faster axiomcode-index, and AXIOMCODE_REINDEX=1 rebuilds; same graphs (#1835) #1965)Speed (median of 4 cold indexes, quiet 16-core Linux; answers byte-identical)