Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .github/scripts/e2e-cases.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# The query cases (tests/run.py), every language, answered by the INSTALLED packages.
#
# e2e-cases.sh <engines-dir> <platform> [tests/run.py args...]
#
# e2e-install.sh proves an install answers every verb on one case per language. That is a smoke test: 0.1.10
# passed it on Windows while 43 of the cases' 1,844 checks failed there — tests not linked through a file path,
# an absolute file:line refused, any argument holding ' or " mangled on its way into Git Bash. None of it is
# platform-specific code; all of it is how Windows writes paths and command lines. So the cases run here, from
# this checkout's plugin scripts, with the engine and parser of the packages a user installs (AXIOMCODE_ENGINE),
# souffle hidden as it is for a user.
#
# CODEGRAPH_TGZ is the packed @axiomcode/code-graph, as for e2e-install.sh.
# ─────────────────────────────────────────────────────────────────────────────
set -uo pipefail

engines="${1:?usage: e2e-cases.sh <engines-dir> <platform> [tests/run.py args...]}"; platform="${2:?platform}"; shift 2
root="$(cd "$(dirname "$0")/../.." && pwd)"
engines="$(cd "$engines" && pwd)"
W="$(mktemp -d)"; SHADOW=""; trap 'rm -rf "$W" ${SHADOW:+"$SHADOW"}' EXIT
fail() { echo "::error::e2e cases: $*"; exit 1; }
[ -n "${CODEGRAPH_TGZ:-}" ] || fail "CODEGRAPH_TGZ is not set"

version="$(node "$root/.github/scripts/version.mjs" get)"
bash "$root/packaging/assemble-engine-package.sh" "$platform" "$version" "$engines" "$W/engine" >/dev/null \
|| fail "the engine package did not assemble"
mkdir -p "$W/tgz" "$W/proj"
( cd "$W/engine" && npm pack --silent --pack-destination "$W/tgz" >/dev/null ) || fail "npm pack of the engine package failed"
cp "$CODEGRAPH_TGZ" "$W/tgz/" || fail "no code-graph tarball at $CODEGRAPH_TGZ"
( cd "$W/proj" && npm init -y >/dev/null && npm install --no-audit --no-fund --loglevel=error "$W"/tgz/*.tgz ) \
|| fail "npm install of the packed tarballs failed"
export AXIOMCODE_ENGINE="$W/proj/node_modules/@axiomcode/code-graph"
[ -d "$AXIOMCODE_ENGINE/graph" ] || fail "the installed package has no graph/ at $AXIOMCODE_ENGINE"

. "$root/graph/test/tools/hide-souffle.sh"
py="$(command -v python3 || command -v python)" || fail "no python"
# the bash a native program starts, as the CLI hands it on: on Windows a bare `bash` is System32's WSL launcher
command -v cygpath >/dev/null 2>&1 && export AXIOMCODE_BASH="${AXIOMCODE_BASH:-$(cygpath -m "$BASH")}"
# and Python in UTF-8, as every launcher of the product sets it (the dispatcher, find-python.js): some tests start a
# verb's script directly, and an answer's → is not cp1252
export PYTHONUTF8=1
rc=0
PATH="$SANDBOX_PATH" "$py" "$root/tests/run.py" "$@" || rc=1
# what only Windows can show for real: its separators, its exec, its symlinks, its command lines
for t in mixed_separators no_symlink no_exec windows_argv; do
PATH="$SANDBOX_PATH" "$py" "$root/tests/$t.py" > "$W/$t.log" 2>&1 && echo "$t: $(tail -1 "$W/$t.log")" \
|| { echo "::error::e2e cases: tests/$t.py failed"; tail -30 "$W/$t.log"; rc=1; }
done
exit $rc
17 changes: 16 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,7 @@ jobs:
- { os: macos-15, platform: darwin-arm64 }
- { os: macos-15-intel, platform: darwin-x64 }
runs-on: ${{ matrix.target.os }}
timeout-minutes: 45
timeout-minutes: 90
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
Expand Down Expand Up @@ -581,6 +581,21 @@ jobs:
bash .github/scripts/e2e-install.sh artifacts/engines "$PLATFORM" "$lang" "$case_dir"
echo "::endgroup::"
done
# The smoke test above is one case per language; Windows writes paths and command lines differently enough that
# a release passed it there with 43 case checks failing (tests not linked through a file path, an absolute
# file:line refused, ' and " mangled into Git Bash). Every query case, answered by the installed packages.
- name: every query case, installed from the tarballs, no Soufflé (Windows)
if: matrix.target.platform == 'win32-x64'
shell: bash
env:
PLATFORM: ${{ matrix.target.platform }}
run: |
set -euo pipefail
# the decompilers `--library auto` uses for a Java class jar and a NuGet package, as the engine job installs them
mvn -q dependency:get -Dartifact=org.vineflower:vineflower:1.10.1
dotnet tool install -g ilspycmd --version 8.2.0.7535 && export PATH="$USERPROFILE/.dotnet/tools:$PATH"
export CODEGRAPH_TGZ="$(ls "$PWD"/artifacts/tgz/*.tgz)"
bash .github/scripts/e2e-cases.sh artifacts/engines "$PLATFORM" --jobs 4

# A single job the branch ruleset can require. Without it, every new matrix
# entry has to be added to the protection rules by hand, and a matrix job that
Expand Down
6 changes: 6 additions & 0 deletions bin/axiomcode
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@
# build option above.)
# ─────────────────────────────────────────────────────────────────────────────
set -eu
# Windows: a native caller (bin/axiomcode.js, the MCP server, ax_exec.bash_argv) hands the arguments over in
# AXIOMCODE_ARGV, each ended by \x1f, because msys misparses them on a command line. Taken once, so a script this one
# starts gets its own.
if [ -n "${AXIOMCODE_ARGV+x}" ]; then
set --; while IFS= read -r -d $'\x1f' _a; do set -- "$@" "$_a"; done < <(printf '%s' "$AXIOMCODE_ARGV"); unset AXIOMCODE_ARGV _a
fi
# RESOLVE $0 THROUGH SYMLINKS BEFORE THE WALK. npm installs a `bin` entry as a link in
# node_modules/.bin, so under `npx axiomcode` or a global install $0 is that link and
# dirname "$0" is .bin/, a directory this package does not live under. The walk below then
Expand Down
7 changes: 6 additions & 1 deletion bin/axiomcode.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,12 @@ if (error) fail(error);
const py = findPython();
// AXIOMCODE_BASH too, for the builds Python starts: a bare `bash` from Python on Windows is WSL's or nothing.
const env = { ...(py.exe ? withPython(process.env, py) : process.env), AXIOMCODE_BASH: bash };
const r = spawnSync(bash, [path.join(__dirname, 'axiomcode'), ...args], { stdio: 'inherit', env });
// On Windows the arguments travel in AXIOMCODE_ARGV, each ended by \x1f, not on the command line: Git Bash parses a
// command line by msys rules, where `'` quotes too and `\"` is no escape, so `context "the user's cart"` lost every
// argument after the apostrophe and a quoted string arrived as \Key\. bin/axiomcode turns them back into "$@".
const win = process.platform === 'win32';
const r = spawnSync(bash, [path.join(__dirname, 'axiomcode'), ...(win ? [] : args)],
{ stdio: 'inherit', env: win ? { ...env, AXIOMCODE_ARGV: args.map((a) => `${a}\x1f`).join('') } : env });
if (r.error) fail(`could not start bash: ${r.error.message}`);
// Die of the same signal the CLI died of, so a caller sees what really happened.
if (r.signal) process.kill(process.pid, r.signal);
Expand Down
2 changes: 1 addition & 1 deletion gemini-extension.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "axiomcode",
"version": "0.1.10",
"version": "0.1.11",
"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": {
Expand Down
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@axiomcode/code-graph",
"version": "0.1.10",
"version": "0.1.11",
"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",
Expand Down Expand Up @@ -31,11 +31,11 @@
],
"license": "FSL-1.1-Apache-2.0",
"optionalDependencies": {
"@axiomcode/engine-darwin-arm64": "0.1.10",
"@axiomcode/engine-darwin-x64": "0.1.10",
"@axiomcode/engine-linux-x64": "0.1.10",
"@axiomcode/engine-linux-arm64": "0.1.10",
"@axiomcode/engine-win32-x64": "0.1.10"
"@axiomcode/engine-darwin-arm64": "0.1.11",
"@axiomcode/engine-darwin-x64": "0.1.11",
"@axiomcode/engine-linux-x64": "0.1.11",
"@axiomcode/engine-linux-arm64": "0.1.11",
"@axiomcode/engine-win32-x64": "0.1.11"
},
"bin": {
"axiomcode": "bin/axiomcode.js"
Expand Down
5 changes: 3 additions & 2 deletions parser/src/workflows/csharp/csharp-project-analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,8 @@ export class CSharpProjectAnalyzer {
continue;
}
context.seen.add(absoluteFilePath);
const relativePath = path.relative(options.baseMservPath, absoluteFilePath);
// `/` on every platform, as the graph stores a file: on Windows `Shop\Pricer.cs` matched no rule joining paths
const relativePath = path.relative(options.baseMservPath, absoluteFilePath).split(path.sep).join('/');

// THE GOVERNING PROJECT decides the framework and the symbols, unless the
// caller fixed them. See cs-project-config.ts for why this is read at all.
Expand Down Expand Up @@ -498,7 +499,7 @@ function readProjectConfigFor(
defineConstants: config.defineConstants,
implicitFrameworkDefines: config.implicitFrameworkDefines,
langVersion: config.langVersion,
projectPath: path.relative(baseMservPath, config.projectFile),
projectPath: path.relative(baseMservPath, config.projectFile).split(path.sep).join('/'),
implicitUsings: config.implicitUsings,
usings: config.usings,
};
Expand Down
7 changes: 5 additions & 2 deletions parser/src/workflows/python/python-project-analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -572,11 +572,14 @@ export class PythonProjectAnalyzer {
rootDir: string,
baseMservPath: string
): string {
const fromMserv = path.relative(baseMservPath, filePath);
// Written with `/` on every platform, as the other languages' IR writes it: the rules join a module's directory to a
// string (`os.path.join(HERE, 'graph-core.py')`, `include("shop.urls")`) with `/`, and on Windows `pkg\changed.py`
// matched none of them.
const fromMserv = path.relative(baseMservPath, filePath).split(path.sep).join('/');
if (fromMserv !== '' && !fromMserv.startsWith('..') && !path.isAbsolute(fromMserv)) {
return fromMserv;
}
return path.relative(rootDir, filePath) || path.basename(filePath);
return path.relative(rootDir, filePath).split(path.sep).join('/') || path.basename(filePath);
}

/** The directory names above a module's top-level package, outermost first (see ProjectModuleFacts.outerDirs). */
Expand Down
2 changes: 1 addition & 1 deletion plugins/axiomcode/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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.1.10",
"version": "0.1.11",
"author": {
"name": "AxiomCode"
}
Expand Down
2 changes: 1 addition & 1 deletion plugins/axiomcode/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "axiomcode",
"version": "0.1.10",
"version": "0.1.11",
"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",
Expand Down
2 changes: 1 addition & 1 deletion plugins/axiomcode/.cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "axiomcode",
"version": "0.1.10",
"version": "0.1.11",
"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",
Expand Down
8 changes: 7 additions & 1 deletion plugins/axiomcode/mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,13 @@ def run(args, cwd=None, timeout=900):
note = stale_note(root)
head = (note + '\n') if note else ''
try:
r = subprocess.run([BASH, ax, *args], cwd=cwd or None, capture_output=True, text=True, timeout=timeout, env=env)
# 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.
# The same as ax_exec.bash_argv, which this server does not import.
argv = [BASH, ax, *args]
if os.name == 'nt':
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.")
Expand Down
11 changes: 7 additions & 4 deletions plugins/axiomcode/skills/axiomcode/scripts/ax_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
partial list is not read as a complete one.
"""
import collections, difflib, os, re, subprocess, sys, time
import ax_exec

SPLIT = re.compile(r'[^A-Za-z0-9]+')
CAMEL = re.compile(r'[A-Z]+(?![a-z])|[A-Z][a-z0-9]*|[a-z0-9]+')
Expand Down Expand Up @@ -492,7 +493,8 @@ def ensure_graph(repo, db):
if nowait:
os.makedirs(os.path.join(repo, '.axiomcode'), exist_ok=True)
log = open(os.path.join(repo, '.axiomcode', 'first-build.log'), 'w')
p = _start_build([bash, build, repo], log, env)
argv, benv = ax_exec.bash_argv(build, [repo], env, bash)
p = _start_build(argv, log, benv)
# until the build holds its lock a second query would see no build and start another, so that much is always waited
up = time.time() + 10
while p.poll() is None and not ax_fresh.building(repo) and time.time() < up: time.sleep(0.05)
Expand All @@ -508,7 +510,8 @@ def ensure_graph(repo, db):
os.makedirs(os.path.join(repo, '.axiomcode'), exist_ok=True)
logp = os.path.join(repo, '.axiomcode', 'first-build.log')
with open(logp, 'w') as log:
p = _start_build([bash, build, repo], log, env)
argv, benv = ax_exec.bash_argv(build, [repo], env, bash)
p = _start_build(argv, log, benv)
shown = [0]
def relay():
try:
Expand Down Expand Up @@ -597,8 +600,8 @@ def _rebuild_in_background(repo):
try:
log = open(os.path.join(repo, '.axiomcode', 'refresh.log'), 'a')
kw = dict(start_new_session=True) if os.name != 'nt' else dict(creationflags=0x00000008 | 0x00000200)
subprocess.Popen([os.environ.get('AXIOMCODE_BASH') or 'bash', build, repo], stdin=subprocess.DEVNULL, stdout=log, stderr=log,
close_fds=True, env=env, **kw)
argv, env = ax_exec.bash_argv(build, [repo], env)
subprocess.Popen(argv, stdin=subprocess.DEVNULL, stdout=log, stderr=log, close_fds=True, env=env, **kw)
except OSError: pass


Expand Down
19 changes: 19 additions & 0 deletions plugins/axiomcode/skills/axiomcode/scripts/ax_exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,22 @@ def become(argv):
except KeyboardInterrupt:
rc = 130
sys.exit(rc)


ARGV_ENV = 'AXIOMCODE_ARGV'


def bash_argv(script, args, env=None, bash=None):
"""(argv, env) that run the bash script `script` with exactly `args`. ON WINDOWS THE COMMAND LINE CANNOT CARRY THEM:
Git Bash's msys runtime parses it by its own rules, where `'` quotes too and `\\"` is no escape, so `it's` swallowed
every argument after it and `"Widgets:MinCount"` arrived as `\\Widgets:MinCount\\`. There they travel in
AXIOMCODE_ARGV, each ended by \\x1f, and the script's prologue turns them back into "$@": an environment value
reaches bash as written. Elsewhere they are passed on the command line as before. WINDOWS_ARGV is read at call time,
so a test can take the Windows branch on any OS."""
bash = bash or os.environ.get('AXIOMCODE_BASH') or 'bash'
if not WINDOWS_ARGV: return [bash, script, *args], env
e = dict(os.environ if env is None else env); e[ARGV_ENV] = ''.join(f'{a}\x1f' for a in args)
return [bash, script], e


WINDOWS_ARGV = os.name == 'nt'
Loading
Loading