Find the invariants your codebase assumes but never tests.
85% line coverage doesn't mean your code is safe. It means 85% of your lines ran during tests — not that the assumptions those lines make have ever been challenged.
Every non-trivial codebase is full of implicit invariants:
user.subscriptionis nevernullbeforegetBillingPlan()is calledinitDB()always runs beforequery()req.useris always populated by auth middleware before route handlers executeusers.find()will always return something before you access its properties
These assumptions are true by convention, not by contract. Nobody wrote them down. Nobody tests them. When they break — due to a refactor, a new code path, a missing middleware — production breaks and the test suite stays green.
AXIOM reads your code statically, infers what it assumes to be true, diffs that against your test suite, and hands you a ranked list of the bets you're making on code that's never been verified.
$ axiom scan ./src
AXIOM v1.3.28 scanned 847 files in 1.2s
━━━ UNTESTED INVARIANTS (ranked by blast radius) ━━━
● CRITICAL billing.ts:47 [high confidence]
user.subscription assumed non-null
├─ relied on by 14 call sites
├─ nearest public entry: POST /checkout (2 hops)
└─ tests covering null path: 0
● CRITICAL auth.ts:203 [high confidence]
session.userId assumed UUID format
├─ relied on by 37 call sites
├─ nearest public entry: GET /profile (1 hop)
└─ tests covering non-UUID: 0
◐ HIGH users.ts:31 [medium confidence]
user.email assumed non-null (.find() can return undefined)
├─ relied on by 8 call sites
├─ nearest public entry: GET /users/:id (1 hop)
└─ tests covering undefined path: 0
○ LOW utils.ts:88 [low confidence]
item.name assumed non-null (index access can return undefined)
├─ relied on by 0 call sites
├─ (nearest entrypoint: unknown)
└─ tests covering null path: 0
Summary: 3 critical · 8 high · 14 medium · 22 low
Run `axiom explain <file:line>` for remediation hints
AXIOM is a signal reducer, not a verdict machine. The workflow:
1. axiom scan → ranked candidate list (seconds)
2. Review [high confidence] findings → 2 min per finding
3. Real bugs get filed · FP patterns go in .axiomrc.json ignore list
On a 6,000-file codebase, AXIOM narrows your review to ~20 specific lines. Reading those 20 lines takes less time than a single code review. That's the value.
Each finding carries a confidence score:
| Tier | Meaning | Action |
|---|---|---|
[high confidence] |
Multiple call sites, reachable entrypoint, no test coverage | Investigate immediately |
[medium confidence] |
Some corroborating signals | Review before each release |
[low confidence] |
Isolated or index-access pattern | Use as code review aide, not a CI gate |
Gate your CI on high-confidence criticals only — everything else is a review queue.
npm install -g axiom-scanOr run without installing:
npx --package=axiom-scan axiom scan ./src# Scan current directory
axiom scan
# Scan a specific path
axiom scan ./src
# JSON output for CI pipelines
axiom scan --json > axiom-report.json
# Only show critical and high severity
axiom scan --min-severity high
# Exit with code 1 if any high-confidence critical invariants found
axiom scan --fail-on critical
# Ignore paths
axiom scan --ignore "**/*.generated.ts" --ignore "src/migrations/**"
# SARIF output for GitHub code scanning
axiom scan --sarif > results.sarif
# Only scan files changed since a branch or commit
axiom scan --since main
axiom scan --since HEAD~5
# Explain a specific invariant with remediation hints
axiom explain billing.ts:47To suppress a specific finding, add axiom-ignore on the flagged line or the line above:
// axiom-ignore
const plan = user.subscription.plan;
const plan = user.subscription.plan; // axiom-ignoreWorks in all supported languages using that language's comment syntax.
AXIOM operates in four phases:
1. CRAWL Walk the source tree, collect source files across all 8 supported languages
2. INFER Extract implicit invariant assumptions from the AST
3. DIFF Map which invariants are exercised by the test suite
4. RANK Score gaps by blast radius and confidence, emit report
| Language | Parser | Invariant types |
|---|---|---|
| TypeScript / JavaScript | @typescript-eslint/typescript-estree |
null, ordering, shape |
| Python | tree-sitter-python (WASM) |
null, ordering, shape |
| Go | tree-sitter-go (WASM) |
null, ordering, shape |
| Ruby | tree-sitter-ruby (WASM) |
null, ordering, shape |
| Java | tree-sitter-java (WASM) |
null, ordering, shape |
| Rust | tree-sitter-rust (WASM) |
null, ordering, shape |
| C# | tree-sitter-c_sharp (WASM) |
null, ordering, shape |
| PHP | tree-sitter-php (WASM) |
null, ordering, shape |
Type 1 — Null / Nil Assumptions
Property accesses on values that could be null, undefined, or nil, where no guard exists.
TypeScript — nullable parameter:
function getBillingPlan(user: User | null) {
return user.subscription.plan; // user could be null
}Python — dict.get() without None check:
def process(data: dict):
val = data.get("key")
return val.strip() # val can be NoneGo — map lookup with discarded ok:
func handle(m map[string]*User) {
user, _ := m["key"]
fmt.Println(user.Name) // user might be nil
}Type 2 — Ordering Invariants
Function B reads state written by function A, but nothing guarantees A ran first.
TypeScript:
let db: Database;
export function initDB(url: string) { db = new Database(url); }
export function query(sql: string) { return db.run(sql); } // assumes initDB ranType 3 — Type Shape Assumptions
Values used as if they match a specific type or format without validation.
TypeScript — UUID assumed without validation:
function createUser(id: string) {
await db.insert({ id, ... }); // id used as UUID in 8 places, never validated
}Go — unsafe type assertion:
func render(v interface{}) {
msg := v.(proto.Message) // panics if v is the wrong type
}score = (call_site_count × 2)
+ (public_entrypoint_proximity) // closer to HTTP handler = higher
+ (type_severity) // null > ordering > shape
- (partial_test_coverage_discount)
Buckets: CRITICAL (>20) · HIGH (10–20) · MEDIUM (5–10) · LOW (<5)
high = 2+ call sites AND reachable entrypoint AND 0 test coverage
OR 3+ call sites AND not index-access AND 0 test coverage
medium = some corroborating signals
low = 0 call sites, no entrypoint, OR index-access with no entrypoint
AXIOM builds a cross-file call graph and traces invariants back to their nearest public entrypoint. It recognizes:
- Named route/handler/controller/middleware functions
- Express-style route callbacks —
router.get('/users', (req, res) => {...})is labeledGET /users - JSX component usage —
<UserCard />in a parent component counts as a call site
{
"scanned": 847,
"duration_ms": 1204,
"invariants": [
{
"id": "billing.ts:47",
"type": "null",
"description": "user.subscription assumed non-null",
"severity": "critical",
"confidence": "high",
"score": 34,
"call_sites": 14,
"nearest_entrypoint": { "route": "POST /checkout", "hops": 2 },
"test_coverage": 0
}
]
}Create .axiomrc.json in your project root:
{
"ignore": [
"**/*.generated.ts",
"src/migrations/**"
],
"minScore": 5,
"maxResults": 50,
"patterns": {
"nullMethods": ["findBySlug", "fetchLatest", "findActive"],
"nullFunctions": ["my_custom_fetch"],
"lifecycleMethods": ["warmUp", "onBoot", "postActivate"],
"assertionFunctions": ["invariant", "ensure", "checkNotNull"]
}
}| Field | Type | Description |
|---|---|---|
ignore |
string[] |
Glob patterns to exclude (merged with built-in ignores) |
minScore |
number |
Minimum blast-radius score to include in output |
maxResults |
number |
Cap total results (sorted by score, highest first) |
patterns.nullMethods |
string[] |
Extra method names that return null/nil/undefined |
patterns.nullFunctions |
string[] |
Extra function names that return null (PHP and Python) |
patterns.lifecycleMethods |
string[] |
Extra method names treated as secondary constructors for ordering checks |
patterns.assertionFunctions |
string[] |
Extra function names that guarantee non-null (e.g. invariant, assert, ensure) |
Use AXIOM as a one-liner in your workflow:
- name: AXIOM invariant scan
uses: zer0contextlost/axiom@main
with:
path: './src'
min-severity: 'high'
fail-on: 'critical'
sarif-output: 'axiom-report.sarif'This automatically:
- Installs
axiom-scan - Scans your code
- Uploads SARIF to GitHub Code Scanning (shows findings as PR annotations)
- Exits with code 1 if any critical findings exist
| Input | Default | Description |
|---|---|---|
path |
. |
Directory to scan |
min-severity |
medium |
Minimum severity: critical, high, medium, low |
fail-on |
critical |
Gate CI on findings at this severity or above |
sarif-output |
axiom.sarif |
Path to write SARIF file for code scanning upload |
| Output | Description |
|---|---|
findings-count |
Total number of findings detected |
jobs:
axiom-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# Full scan — upload to code scanning
- uses: zer0contextlost/axiom@main
with:
path: './src'
fail-on: 'critical'
# PR diff-only scan — fail if ANY invariant in changed files
- name: Check only changed files
if: github.event_name == 'pull_request'
run: axiom scan ./src --since origin/main --fail-on criticalScan only files changed in the current PR:
- name: AXIOM diff scan
run: axiom scan ./src --since origin/main --fail-on criticalThis runs the full AXIOM analysis but on only the delta, letting you catch new problems without noise from legacy code.
If you prefer explicit control:
- name: Install AXIOM
run: npm install -g axiom-scan
- name: Run AXIOM scan
run: axiom scan . --sarif > axiom.sarif
continue-on-error: true
- name: Upload SARIF to GitHub Code Scanning
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: axiom.sarif
- name: Gate on critical invariants
run: axiom scan . --fail-on criticalSee VERSIONS.md for the full version history.
Next up (v1.4): Worker threads for TS/JS parsing; cross-language call graph; TypeScript compiler API for zero-FP TS pipeline; inter-procedural alias taint.
| Tool | Gap |
|---|---|
| Istanbul / V8 coverage | Measures line execution, not behavioral assumptions |
| Mutation testing (Stryker) | Slow, noisy, you need tests for it to work |
| TypeScript strict mode | Catches declared-type nulls only, not behavioral invariants |
| ESLint | Rule-based; you write the rules. AXIOM infers them |
| Semgrep | Pattern matching; you write the patterns. AXIOM infers them |
AXIOM's position: nobody infers invariants from code behavior, ranks them by blast radius, and diffs them against test coverage. The gap is clear and unoccupied.
git clone https://github.com/zer0contextlost/axiom
cd axiom
npm install
npm run dev # watch mode
npm testSee CONTRIBUTING.md for details.
MIT — see LICENSE