An open API service indexing awesome lists of open source software.

https://github.com/zer0contextlost/axiom

Find the invariants your codebase assumes but never tests.
https://github.com/zer0contextlost/axiom

Last synced: 2 months ago
JSON representation

Find the invariants your codebase assumes but never tests.

Awesome Lists containing this project

README

          

# 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 v0.9.0 scanned 847 files in 1.2s

━━━ UNTESTED INVARIANTS (ranked by blast radius) ━━━

● CRITICAL billing.ts:47
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
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
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

◐ HIGH queue.ts:88
processJob() assumes initQueue() ran first
├─ relied on by 6 call sites
├─ nearest public entry: worker entrypoint (3 hops)
└─ tests covering out-of-order: 0

Summary: 3 critical · 8 high · 14 medium · 22 low
Run `axiom explain ` for remediation hints
```

---

## Install

```bash
npm install -g axiom-scan
```

Or run without installing:

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

---

## Usage

```bash
# 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 critical invariants found (for CI gates)
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:

```typescript
// 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 TypeScript/JavaScript/Python/Go/Ruby files
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, 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 |

### 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:*
```typescript
function getBillingPlan(user: User | null) {
return user.subscription.plan; // user could be null
}
```

*Python — dict.get() without None check:*
```python
def process(data: dict):
val = data.get("key")
return val.strip() # val can be None
```

*Go — map lookup or call with discarded ok/error:*
```go
func handle(m map[string]*User) {
user, _ := m["key"]
fmt.Println(user.Name) // user might be nil
}
```

*Ruby — .find() or hash subscript without nil guard:*
```ruby
def process(users)
user = users.find { |u| u.admin? }
user.email # user could be nil if none found
end
```

**Type 2 — Ordering Invariants**

Function B reads state written by function A, but nothing guarantees A ran first.

*TypeScript:*
```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
```

*Go — nil map write before make():*
```go
func build() {
var index map[string]int
index["key"] = 1 // panic: assignment to entry in nil map
}
```

**Type 3 — Type Shape Assumptions**

Values used as if they match a specific type or format without validation.

*TypeScript — UUID assumed without validation:*
```typescript
function createUser(id: string) {
await db.insert({ id, ... }); // id used as UUID in 8 places, never validated
}
```

*Go — unsafe type assertion:*
```go
func render(v interface{}) {
msg := v.(proto.Message) // panics if v is the wrong type
}
```

*Python — int() without isdigit() check:*
```python
def set_limit(n: str):
limit = int(n) # ValueError if n is not numeric
```

*Ruby — Integer() without rescue:*
```ruby
def set_limit(n)
limit = Integer(n) # ArgumentError if n is not numeric
end
```

*Rust — `.parse().unwrap()` without error handling:*
```rust
fn set_limit(n: &str) {
let limit: u32 = n.parse().unwrap(); // panics if n is not numeric
}
```

*Rust — truncating cast on function parameter:*
```rust
fn set_backlog(backlog: usize) {
setsockopt(fd, backlog as i32); // silently truncates if backlog > i32::MAX
}
```

**C# examples:**

*Nullable `.Value` without `.HasValue` check:*
```csharp
void Process(int? count) {
int val = count.Value; // throws InvalidOperationException if null
}
```

*LINQ `FirstOrDefault()` result accessed without null guard:*
```csharp
var user = users.FirstOrDefault(u => u.Name == name);
return user.Email; // NullReferenceException if no match
```

*`int.Parse()` without try-catch:*
```csharp
void SetLimit(string s) {
int limit = int.Parse(s); // FormatException if s is not numeric
}
```

### 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)

### 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 — `` in a parent component counts as a call site

---

## JSON output

```json
{
"scanned": 847,
"duration_ms": 1204,
"invariants": [
{
"id": "billing.ts:47",
"type": "null",
"description": "user.subscription assumed non-null",
"severity": "critical",
"score": 34,
"call_sites": 14,
"nearest_entrypoint": { "route": "POST /checkout", "hops": 2 },
"test_coverage": 0
}
]
}
```

---

## Configuration

Create `.axiomrc.json` in your project root:

```json
{
"ignore": [
"**/*.generated.ts",
"src/migrations/**"
],
"minScore": 5,
"maxResults": 50
}
```

| 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) |

Config is merged with CLI flags — CLI flags take precedence.

---

## CI / GitHub Actions

Add to your workflow to surface invariants without blocking:

```yaml
- name: Run AXIOM scan
run: axiom scan . --min-severity high --json > axiom-report.json
continue-on-error: true

- name: Upload AXIOM report
uses: actions/upload-artifact@v4
with:
name: axiom-report
path: axiom-report.json

- name: Gate on critical invariants
run: axiom scan . --min-severity high --fail-on critical
```

`--fail-on critical` exits with code 1 if any critical invariants are found, failing the job.

### GitHub Code Scanning (SARIF)

Upload results directly to GitHub's Security tab:

```yaml
- name: Run AXIOM scan
run: axiom scan . --sarif > axiom.sarif
continue-on-error: true

- name: Upload SARIF to GitHub
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: axiom.sarif
```

### Diff-only mode for PRs

Only scan files changed in a pull request — much faster in CI:

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

---

## Roadmap

| Version | Status | Scope |
|---------|--------|-------|
| **v0.1** | ✅ Shipped | Null/undefined, ordering, and type-shape invariants; cross-file call graph; blast-radius ranking; terminal + JSON output |
| **v0.2** | ✅ Shipped | `axiom explain` with remediation hints, `.axiomrc.json` config, GitHub Actions example |
| **v0.3** | ✅ Shipped | JSX/React component call tracking, Express route naming (`GET /path`), `find()`/`get()`/index-access undefined detection, `--fail-on` CI gate, false-positive reduction |
| **v0.4** | ✅ Shipped | Python support (null, ordering, shape via tree-sitter), watch mode, VS Code extension |
| **v0.5** | ✅ Shipped | Go support (nil map writes, unsafe type assertions, strconv without error handling) |
| **v0.6** | ✅ Shipped | Ruby support, SARIF output for GitHub code scanning, `--since` flag for diff-only mode |
| **v0.7** | ✅ Shipped | Java support (null, ordering, shape); ordering deduplication for reactive/DI frameworks |
| **v0.8** | ✅ Shipped | Rust support (`unwrap`/`expect` without guards, `.parse().unwrap()`, truncating casts); inline `axiom-ignore` suppressions for all languages |
| **v0.9** | ✅ Shipped | C# support (nullable `.Value`, LINQ null dereference, `int.Parse()`/`Convert.To*()` without try-catch, uninitialized field ordering) |

---

## 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 and diffs them against test coverage.** The gap is clear and unoccupied.

---

## Contributing

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

See [CONTRIBUTING.md](CONTRIBUTING.md) for details.

---

## License

MIT — see [LICENSE](LICENSE)