Skip to content

zer0contextlost/axiom

Repository files navigation

AXIOM

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.subscription is never null before getBillingPlan() is called
  • initDB() always runs before query()
  • req.user is always populated by auth middleware before route handlers execute
  • users.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.


Demo

$ 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

How to use it

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.

Confidence tiers

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.


Install

npm install -g axiom-scan

Or run without installing:

npx --package=axiom-scan axiom scan ./src

Usage

# 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:47

Inline suppressions

To 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-ignore

Works in all supported languages using that language's comment syntax.


How it works

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

Languages supported

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

Invariant types detected

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 None

Go — 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 ran

Type 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
}

Blast radius score

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)

Confidence score

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

Call graph

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 labeled GET /users
  • JSX component usage — <UserCard /> in a parent component counts as a call site

JSON output

{
  "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
    }
  ]
}

Configuration

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)

CI / GitHub Actions

Quick start — GitHub Action

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

Action inputs

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

Action outputs

Output Description
findings-count Total number of findings detected

Full pipeline with multiple gates

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 critical

Diff-only mode for PRs

Scan only files changed in the current PR:

- name: AXIOM diff scan
  run: axiom scan ./src --since origin/main --fail-on critical

This runs the full AXIOM analysis but on only the delta, letting you catch new problems without noise from legacy code.

Manual setup (without the action)

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 critical

Roadmap

See 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.


Why not...

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.


Contributing

git clone https://github.com/zer0contextlost/axiom
cd axiom
npm install
npm run dev   # watch mode
npm test

See CONTRIBUTING.md for details.


License

MIT — see LICENSE

About

No description, website, or topics provided.

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors