Back to updates
New releaseJul 24, 2026

trailmark v0.5.0

Build and query a graph database representation of source code

Share

Trailmark

CI Mutation Testing

Parse source code into queryable graphs of functions, classes, calls, and semantic annotations for security analysis.

Trailmark uses tree-sitter for language-agnostic AST parsing and rustworkx for high-performance graph traversal. The long-term vision is to combine this graph with mutation testing and coverage-guided fuzzing to identify gaps between assumptions and test coverage that are reachable from user input.

How It Works

Trailmark operates in three phases: parse, index, and query.

flowchart TD
    A["Source Files"] --> B["tree-sitter Parser"]
    B --> C["CodeGraph (nodes + edges)"]
    C --> D["rustworkx GraphStore"]
    D --> E["QueryEngine"]
    E --> F["JSON / Summary / Hotspots"]

    classDef src fill:#007bff26,stroke:#007bff,color:#007bff
    classDef parse fill:#28a74526,stroke:#28a745,color:#28a745
    classDef data fill:#6f42c126,stroke:#6f42c1,color:#6f42c1
    classDef query fill:#ffc10726,stroke:#e6a817,color:#e6a817

    class A src
    class B parse
    class C,D data
    class E,F query

1. Parse

A language-specific parser walks the directory, parses each file into a tree-sitter AST, and extracts:

  • Nodes — functions, methods, classes, structs, interfaces, traits, enums, modules, namespaces
  • Edges — calls, inheritance, implementation, containment, imports
  • Metadata — type annotations, cyclomatic complexity, branches, docstrings, exception types

Supported Languages

LanguageExtensionsKey constructs
Python.pyfunctions, classes, methods
JavaScript.js, .jsx, .mjs, .cjsfunctions, classes, arrow functions
TypeScript.ts, .tsxfunctions, classes, interfaces, enums
PHP.phpfunctions, classes, interfaces, traits
Ruby.rbmethods, classes, modules
C.c, .hfunctions, structs, enums
C++.cpp, .hpp, .cc, .hh, .cxx, .hxxfunctions, classes, structs, namespaces
C#.csmethods, classes, interfaces, structs, enums, namespaces
Java.javamethods, classes, interfaces, enums
Go.gofunctions, methods, structs, interfaces
Rust.rsfunctions, structs, traits, enums, impl blocks, verus! blocks
Solidity.solcontracts, interfaces, libraries, functions, modifiers, structs, enums
Cairo.cairofunctions, traits, structs, enums, impl blocks, StarkNet contracts
Circom.circomtemplates, functions, signals, components
Haskell.hsfunctions, data types, type classes, instances
Erlang.erlfunctions, records, behaviours, modules
Miden Assembly.masmprocedures, entrypoints, constants, invocations
Swift.swiftfunctions, classes, structs, enums, protocols, extensions
Objective-C.m, .mm, .hC functions, classes, methods (selector-based naming)
Kotlin.kt, .ktsfunctions, classes, interfaces, data classes, objects, methods
Dart.dartfunctions, classes, abstract classes, methods, constructors
Move.movemodules, functions, imports, direct calls
Tact.tactcontracts, structs, receivers, functions
Func.fc, .funcfunctions, includes, direct calls
Sway.swABI interfaces, structs, impl methods, functions
Rego.regopackages, imports, policy rules, rule calls
Proto.protoservices, RPCs, messages, fields, enums
Thrift.thriftservices, functions, structs, fields, enums
GraphQL.graphql, .gqlobject types, root operations, fields, enums
SQL.sqlschemas, tables, views, functions, procedures

The Rust parser extracts ordinary, spec, and proof functions with bodies from verus! { ... } blocks. The Verus grammar currently supports only brace-delimited blocks; verus!(...) and verus![...] are skipped with a warning. Inline modules retain qualified IDs (for example, file.inner:function), and methods retain their impl or trait owner. Each block is parsed independently; syntax errors produce a warning with the file and block location. Qualified invocations such as vstd::prelude::verus! are supported too. Local helper functions retain their enclosing function in their IDs (for example, file:outer.inner). Calls to these helpers and relative imports such as use super::* resolve to declarations present in the file. Repeated local names in different blocks receive a byte-offset suffix to keep their nodes distinct, and calls respect block scope. Local structs, enums, traits, and inline modules retain the enclosing function too: for example, file:outer.Guard.drop and file:outer.local.helper. Their bodies have separate calls and complexity. Explicit calls to known local types and modules resolve within the file. Bare calls search function and module scopes; associated items use paths such as Type::method. Dotted calls resolve only when the receiver names a known struct or enum. Other value receivers, including self.method(), remain unresolved. Implicit destructor calls and receiver type inference are not modeled. These rules apply to ordinary Rust as well as Verus blocks. Editing code before a declaration with an offset suffix changes its ID, so trailmark diff can report that declaration as removed and added.

Trailmark does not expand arbitrary macros. Literal Verus invocations inside opaque macros or function bodies are skipped with a warning. Uninvoked macro_rules! definitions are not expanded. Function signatures without bodies and calls in requires/ensures clauses are not extracted.

flowchart TD
    subgraph "Per-File Parsing"
        F["Source file"] --> TS["tree-sitter AST"]
        TS --> EX["Extract nodes"]
        TS --> EC["Extract call edges"]
        TS --> EB["Count branches"]
        TS --> ET["Resolve types"]
    end

    EX --> CG["CodeGraph"]
    EC --> CG
    EB --> CG
    ET --> CG

    classDef src fill:#007bff26,stroke:#007bff,color:#007bff
    classDef parse fill:#28a74526,stroke:#28a745,color:#28a745
    classDef extract fill:#ffc10726,stroke:#e6a817,color:#e6a817
    classDef data fill:#6f42c126,stroke:#6f42c1,color:#6f42c1

    class F src
    class TS parse
    class EX,EC,EB,ET extract
    class CG data

Node IDs follow the scheme module:function, module:Class, or module:Class.method for unambiguous lookup. Directory parsing resolves bare cross-file calls when a unique definition exists; ambiguous cross-file calls are left at their original best-effort target and marked uncertain. Edge confidence is tagged as certain (direct calls, self.method()), inferred (attribute access on non-self objects), or uncertain (dynamic dispatch or ambiguous resolution).

2. Index

The GraphStore loads the CodeGraph into a rustworkx PyDiGraph and builds bidirectional ID/index mappings for fast traversal.

3. Query

The QueryEngine provides a high-level API over the indexed graph:

Categories