Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
febddf6
ci: gate main on build, repo invariants and all five language suites
swapnilpaliwal-sd Sep 25, 2026
3d8b436
release: one version everywhere, a tag and draft release per bump, np…
swapnilpaliwal-sd Sep 25, 2026
cc84330
test/java: the oracle-agreement golden counts case 60
swapnilpaliwal-sd Sep 25, 2026
044c323
release: publish with the CLI_BINARY_PUBLISH secret, the npm token al…
swapnilpaliwal-sd Sep 25, 2026
6e36826
build: darwin-arm64 on the hosted macos-15 runner, every run
swapnilpaliwal-sd Sep 25, 2026
65c12f3
ci: no approval required; only the admin role merges into main
swapnilpaliwal-sd Sep 25, 2026
ca5c662
ci: run on pushes to dev, which takes direct pushes
swapnilpaliwal-sd Sep 25, 2026
a9e67b9
release: merge main back into dev after every push to main
swapnilpaliwal-sd Sep 25, 2026
8acfaf5
Linux: an apostrophe in a # comment, and a sandbox PATH that kept /bin
swapnilpaliwal-sd Sep 25, 2026
c947c53
test/tools: one souffle-hiding helper, correct on merged-/usr Linux
swapnilpaliwal-sd Sep 25, 2026
b48ce29
ci: a docs-only change runs build and hygiene only; platform engines …
swapnilpaliwal-sd Sep 25, 2026
452d997
ci: dev is the default branch, a nightly from scratch, engines cached…
swapnilpaliwal-sd Sep 25, 2026
c274585
ci: cache keys cover the compile flags and, for -march=native, the CPU
swapnilpaliwal-sd Sep 25, 2026
d793c68
nightly: dev's daily status, published under the nightly dist-tag whe…
swapnilpaliwal-sd Sep 25, 2026
8071d29
test/csharp: one compiled engine per run, not one per case; admins ca…
swapnilpaliwal-sd Sep 25, 2026
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
3 changes: 3 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Reviewers requested automatically on every pull request.

* @swapnilpaliwal-sd @JaredHLZhang @suyashpaliwal26 @Whua689
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
blank_issues_enabled: false
52 changes: 52 additions & 0 deletions .github/ISSUE_TEMPLATE/engine-defect.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Engine defect
description: A call edge the engine resolves wrongly, or does not resolve at all.
labels: [bug, engine]
body:
- type: markdown
attributes:
value: |
**Describe the defect with a minimal synthetic example.** Do not paste code from
a real project, name the project, or quote its identifiers or file names — this
tracker is a durable public record, and naming someone's codebase reads as a
benchmark claim about it. Reduce the finding to `class Widget`, `pkg/_helpers.py`
and the like, and describe the mechanism.

- type: dropdown
id: language
attributes:
label: Front end
options: [java, python, typescript]
validations: { required: true }

- type: textarea
id: repro
attributes:
label: Minimal example
description: The smallest synthetic source that shows it. Include the call site.
render: text
validations: { required: true }

- type: textarea
id: expected
attributes:
label: What the engine should resolve, and what it resolves instead
description: |
Name the edge both ways — `A.foo -> B.bar`, versus the `ambiguous_unknown` or
wrong target you actually get.
validations: { required: true }

- type: textarea
id: mechanism
attributes:
label: Mechanism
description: |
What does the rule join on, and why does the join fail? If you already know which
relation derives zero rows, say which.
validations: { required: false }

- type: input
id: parser
attributes:
label: Parser commit
description: The SHA in `.github/parser-ref` you reproduced against.
validations: { required: false }
25 changes: 25 additions & 0 deletions .github/ISSUE_TEMPLATE/enhancement.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Enhancement
description: Resolution power the engine does not have yet.
labels: [enhancement]
body:
- type: dropdown
id: language
attributes:
label: Front end
options: [java, python, typescript, all]
validations: { required: true }

- type: textarea
id: construct
attributes:
label: The construct
description: A synthetic example of the code shape that does not resolve today.
render: text
validations: { required: true }

- type: textarea
id: why
attributes:
label: Why it matters
description: What becomes answerable once this resolves. Mechanism, not corpus.
validations: { required: true }
26 changes: 26 additions & 0 deletions .github/ISSUE_TEMPLATE/harness.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Test harness or CI
description: A suite, golden, oracle or workflow that measures the wrong thing.
labels: [test]
body:
- type: markdown
attributes:
value: |
A measurement fault outranks an engine fault: while it stands, every number the
suite reports is unsafe to act on. Say what the harness claims and why that claim
is not true.

- type: textarea
id: claim
attributes:
label: What the harness reports, and why it is wrong
validations: { required: true }

- type: textarea
id: isolate
attributes:
label: How you isolated it
description: |
The rows you read, the A/B you ran, the commit on both arms. A regression that
was really the harness looks identical to one that was really the engine until
this part exists.
validations: { required: true }
42 changes: 42 additions & 0 deletions .github/ISSUE_TEMPLATE/parser-blocked.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Parser-blocked
description: The engine cannot resolve something because the IR does not carry it.
labels: [parser-blocked]
body:
- type: markdown
attributes:
value: |
Use this when the fix does not belong in this repository. The engine's rules are
read-only with respect to the parser: a missing fact is filed here, proven, and
fixed in `AxiomCodeAI/parser`.

**A parser defect needs three things before it is one:** the parser source that
drops the fact, a synthetic input that reproduces it, and the real construct it
came from. Without all three this is a hypothesis.

- type: dropdown
id: language
attributes:
label: Front end
options: [java, python, typescript]
validations: { required: true }

- type: textarea
id: missing
attributes:
label: The fact that is missing
description: Which relation, which column, and what it should contain.
validations: { required: true }

- type: textarea
id: evidence
attributes:
label: Evidence in the parser source
description: The file and function that drops it. Synthetic example only.
validations: { required: true }

- type: textarea
id: blocked
attributes:
label: What this blocks here
description: The rule that cannot be written, or the edge that cannot resolve.
validations: { required: true }
68 changes: 68 additions & 0 deletions .github/RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Releasing

One version, `package.json`'s, names everything that ships: `@axiomcode/code-graph`, the
`@axiomcode/engine-<os>-<cpu>` packages it pins, the vendored parser and every plugin manifest.

## Cutting a release

1. **Bump** in a pull request: `node .github/scripts/version.mjs set 0.2.0`
(writes all ten locations; `check` verifies them). CI's version gate requires the bump to
move forward and not reuse a tagged version.
2. **Merge.** `release.yml` tags the merge commit `v0.2.0` and opens a **draft** release with
notes generated from the pull requests since the previous tag. Nothing is published yet.
3. **Publish the draft** under *Releases*. That runs `publish-npm.yml`: it checks the tag
against every manifest, builds every language's engine on every platform, publishes the
engine packages and then `@axiomcode/code-graph`, and attaches the tarballs to the release.
A version containing `-` (`0.2.0-rc.1`) is published under the `next` dist-tag, never `latest`.

A failed publish can be re-run: versions already on the registry are skipped.

To see what would ship without publishing: *Actions → publish-npm → Run workflow* (dry run by
default); the packed tarballs are uploaded as a workflow artifact.

## When a pull request needs a bump

Once a version is tagged, any change to a file that reaches users (everything outside the
`!` entries of `package.json`'s `files`, `graph/test/` and `.github/`) needs a new version, since
npm will not republish one. Before the first tag, changes simply join the unreleased version.

## One-time setup

- `CLI_BINARY_PUBLISH` repository secret: an npm token with publish rights on `@axiomcode`.
- The repository must be public: every platform, macOS included, builds on GitHub's standard
hosted runners, which are free only for public repositories.
- `bash .github/scripts/protect-main.sh` (admin): PR + green `CI` for everyone, only admins merge,
makes `v*` tags immutable.

## dev and main

`dev` is the default branch: every pull request lands there, and CI runs build, repo checks and
the five language suites on it (a docs-only change runs only the first two). Its one rule is that
it cannot be deleted, so direct pushes are fine too.

`main` moves only by promotion: a pull request `dev → main`, which also builds every platform's
engines, needs a green `CI`, and only an admin can merge. Every version released is a tag on
`main`. main only squash-merges, so after every push to main the `sync-dev` job in `release.yml`
merges main back into dev; a hotfix that conflicts with dev pushes nothing and opens an issue with
the commands to resolve it by hand.

## Nightly

`nightly.yml` is dev's status (the "nightly" badge in the README). Every night it builds `dev`
from scratch: no cached engines, the five suites, all four platforms' engines, then
`e2e-install.sh` packs the npm tarballs, installs them into an empty project without Soufflé and
runs `axiomcode` in four languages, and `npm publish --dry-run` checks each package.

A green night is published under the `nightly` dist-tag, as `<next>-nightly.<date>.g<sha>`, where
`<next>` is the patch after the last release (`npm i @axiomcode/code-graph@nightly`). The version
is never committed or tagged, and plain `npm i` keeps getting `latest`. No nightly is published
until a first real release exists, because npm makes a package's first publish its `latest`.

A red night publishes nothing and opens an issue; the next green night closes it. Run it by hand
from Actions at any time (it publishes only if you tick `publish`).

## Caches

Compiled engines are cached by ENGINE_ID, the hash of a language's rules and the Soufflé version,
so a change that touches no rules reuses every engine and a rule change recompiles only its own
language. The nightly and every real publish build fresh.
23 changes: 23 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
version: 2
updates:
# Actions are pinned by major tag; this keeps them current and, more to the
# point, surfaces the deprecations that otherwise fail CI without warning.
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
labels: [build, dependencies]
open-pull-requests-limit: 3

- package-ecosystem: npm
directory: /
schedule:
interval: weekly
labels: [build, dependencies]
open-pull-requests-limit: 3
# The engine's behaviour is pinned to the toolchain that produced the
# goldens. A TypeScript major arriving on its own schedule is a change to
# what this repo measures, not a routine bump.
ignore:
- dependency-name: typescript
update-types: [version-update:semver-major]
40 changes: 40 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<!--
Every change here follows: issue first, then a pull request that closes it.
If there is no issue yet, open one — the issue is where the measurement that
found the problem lives, and this is where the fix lives.
-->

Fixes #

## What changed

<!-- The mechanism. What the rule now joins on, or what the harness now measures. -->

## Why the goldens moved, or why they did not

<!--
Required whenever test/*/expected changed. A golden diff is a change in resolution
power and has to be readable as one: which edges appeared, which disappeared, and
why each is correct now.

If nothing under test/*/expected changed, say so — "no golden moved" is a real and
useful claim about a rules change.
-->

## Evidence

<!--
Synthetic examples only. Never name the project a defect was found in, quote its
identifiers, or paste its code — not here, not in the issue, not in the commit
message.

Keep the corpus measurement out of the prose too: describe the defect by mechanism,
not by how many links it cost.
-->

## Checklist

- [ ] The three suites pass locally against the parser commit in `.github/parser-ref`
- [ ] Any golden that moved is explained above
- [ ] A fix validated on more than one shape, so this is not overfitting to one case
- [ ] Labels set, including the front end (`java` / `python` / `typescript`)
61 changes: 61 additions & 0 deletions .github/scripts/e2e-install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# Install the packages the way a user would, and run them.
#
# e2e-install.sh <engines-dir> <platform> <language> <source-dir>
#
# <engines-dir> is one platform's build-engines output (<lang>/axiomcode-engine-<lang>
# + <lang>/ENGINE_ID). This packs @axiomcode/code-graph and that platform's engine
# package exactly as publish-npm.yml would, installs both into an empty project, and
# runs `axiomcode` on <source-dir> with souffle NOT on PATH. It passes only if the
# installed package found its engine package, used it, and wrote a graph with edges.
#
# The test suites cannot see any of this: they run from the checkout, where a missing
# `files` entry, a broken bin, or an engine package the driver does not find all go
# unnoticed until someone installs a release.
# ─────────────────────────────────────────────────────────────────────────────
set -uo pipefail

engines="${1:?usage: e2e-install.sh <engines-dir> <platform> <language> <source-dir>}"
platform="${2:?platform}"; lang="${3:?language}"; src="${4:?source-dir}"
root="$(cd "$(dirname "$0")/../.." && pwd)"
engines="$(cd "$engines" && pwd)"; src="$(cd "$src" && pwd)"
W="$(mktemp -d)"; SHADOW=""; trap 'rm -rf "$W" ${SHADOW:+"$SHADOW"}' EXIT
fail() { echo "::error::e2e: $*"; exit 1; }

version="$(node "$root/.github/scripts/version.mjs" get)"
echo "── packing $version"
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"
( cd "$W/engine" && npm pack --silent --pack-destination "$W/tgz" >/dev/null ) || fail "npm pack of the engine package failed"
# --ignore-scripts: the tree is already built; the tarball must carry what the build produced
( cd "$root" && npm pack --silent --ignore-scripts --pack-destination "$W/tgz" >/dev/null ) || fail "npm pack of code-graph failed"
ls -1 "$W/tgz" | sed 's/^/ /'

echo "── installing into an empty project"
mkdir -p "$W/proj"
( 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"
bin="$W/proj/node_modules/.bin/axiomcode"
[ -x "$bin" ] || fail "the installed package has no axiomcode bin"

. "$root/graph/test/tools/hide-souffle.sh"
PATH="$SANDBOX_PATH" command -v souffle >/dev/null 2>&1 && fail "souffle is still on the sandbox PATH"

echo "── axiomcode $lang on $(basename "$src"), no souffle"
PATH="$SANDBOX_PATH" "$bin" "$src" "$W/out" --language "$lang" > "$W/run.log" 2>&1
rc=$?
sed 's/^/ /' "$W/run.log" | tail -15
[ "$rc" -eq 0 ] || fail "axiomcode exited $rc"
grep -q "using packaged engine" "$W/run.log" || fail "the run did not use the installed engine package"

db="$(find "$W/out" -name graph.sqlite | head -1)"
[ -n "$db" ] || fail "no graph.sqlite was written"
edges="$(node -e '
const { DatabaseSync } = require("node:sqlite");
const db = new DatabaseSync(process.argv[1], { readOnly: true });
console.log(db.prepare("SELECT COUNT(*) AS n FROM call_edges").get().n);
' "$db" 2>/dev/null)"
[ -n "$edges" ] && [ "$edges" -gt 0 ] || fail "the graph has no call edges (${edges:-unreadable})"
echo "e2e: ok — installed from the tarballs, used the packaged $platform engine, $edges call edges"
Loading
Loading