Skip to content

mathema audit

Population-level report: every function mathema can find under the given targets, whether it's claimed at all (any of the four authoring surfaces), whether it's liftable for a derive-route proof specifically, and if not, why not, distinct from verify's per- function completeness view, which only ever reports on functions that already have at least one claim. A function with zero claims is invisible to verify/status by construction; audit is the tool that answers "where do I even start" on a real, existing codebase.

mathema audit mypkg [mypkg.sub ...]

Arguments

Flag Meaning
target importable module or package name(s), must already be importable
--root project root holding claims/ and any coverage.json/.coverage (default .)
--exclude ANALYSIS skip an analysis entirely, not just hide its column: derivable, complexity, typing, scope, tested, docs, docsync (comma-separated or repeatable)
--index write the global index record (.mathema/index.yaml) instead of the analysis table, and print what it wrote as a per-module table (overwriting the previous index is by design; it is a generated view nothing reads back into the declared or verified layers)
--docs skip the wide table and report per-function checkbox breakdowns of just the docs criteria, both the quality checklist and the docsync schema, see below
--deriv-report the full per-function detail below the table (off by default): underivability reasons with reason-code hints, and each function's complete module-state view, the names the grid's vars/mutates cells condense, with the file lines they appear at
--one-line one row per function with the full dotted key and no cell compression: the default tree layout (module line, class line, functions indented under their scope) plus its name ellipses and +N condensing all off
--compact column-oriented JSON instead of the grid: {"prefix", "cols", "rows"} with column names once, the shared key prefix factored out of every row, and raw values (JSON null for missing, never -); the same shape the MCP audit_targets tool returns
--filter TERM,... keep only matching rows (implies --compact): semantic terms, derive_unlock classes (actionable, limitation, N/A), claimed/unclaimed, derivable/underivable (what the derive route can do given the declared domain), unconditional/needs-context (whether the body lifts with nothing supplied), underclaimed (fewer claims than the function's own structural floor; the "what have I not evidenced" query), and columnar substring matches (blocker~loop scopes to one column, ~mutual matches any column). Terms from the same dimension OR together (actionable,limitation keeps either, which is also how "everything except N/A" is spelled); dimensions and columnar terms AND across, and an empty match text (~, col~) is an error, never a keep-everything no-op. A filter's column references are computed even when not selected as output, so an empty result always means "nothing matched", never "never looked". Explicit by design, never a default, right for a lifting pass, wrong for a claims pass, since probe claims stay viable on every limitation row
--cols COL,... columns for --compact (implies it), e.g. key,span,claims,derivable,unconditional,blocker,typed,tested (the default set); also min_expected_claims, claims_vs_floor (the [actual, floor] pair, side by side), blocker_params, blocker_more, blocker_hint and blocker_unlock (the remedy and its derive_unlock class, carried in the row rather than needing a second reason_code lookup), constructs, cx, quality (alias doc_quality), docsync (the 0-100 percent), intent, domain_declared, raises_declared, callees_doc_quality, callees_docsync, concepts, global_vars, mutates, unresolved. The resolved selection is echoed back in the output's cols field; an unknown name exits 2 listing the vocabulary

What each column reports

Column Meaning
key the canonical dotted key, module.qualname
span the function's source span, sed-address style (142:187p), a ready-made sed -n range, so reading exactly one function needs no search
claims the claim triplet {floor | actual | expected}: the floor is the least this function's shape gives you to state (one claim per relevant claim family), the actual is how many claims it carries across the four authoring surfaces, and the expected is how many a function of this shape typically carries, which needs a corpus and so reads -. {9 | 1 | -} is a function with one claim against a floor of nine
cx cyclomatic complexity (branches + loops + 1), shown for every row
derives (derive route group) whether the derive route can work on this function given the domain its signature, docstring and claims declare. This is the provability question
reason (derive route group) the structural shape blocking an unconditional lift, the body with nothing supplied ("2 branches, 1 loop", "recursive (2 call sites)"); a dash means nothing blocks it even bare
code (derive route group) the compact underivability code, see Reason codes
typed signature typing coverage
finite_domain (typing group) a Literal[...]/Enum/bool parameter annotation, surfaced as a finite domain (a bool's stated values are the real False/True objects, so identity guards behave)
vars (globals group) genuine global variable dependencies, real, hidden state; a claim about this function is only as reliable as that value
mutates (globals group) module state this function writes, the stronger relationship
funcs (globals group) references to a sibling function/class/module by name, ordinary code structure, not a state dependency
unresolved (globals group) free names with no binding mathema can find at all, almost always a real bug
tested whether an existing coverage.json/.coverage covers this function (never runs tests itself): yes, no, no-report, or outdated when the function's source file changed since the report measured it (by content hash when the report is stamped with mathema coverage --stamp, else by file modification time); a stale yes is not evidence
locked whether the function's form hash is locked: yes (with who pinned it) means the body cannot change until a human runs mathema unlock. Hidden when nothing in the population is locked; --compact carries the pinned form hash in a locked column on request
quality (docs group) the docstring quality-checklist score, see below

The grid prints a group-title row above the columns (derive route, typing, globals, docs), so the short names above always sit under their group's own heading, and the key column is a tree by default: one line for the module, one per enclosing class, each function indented under its scope (--one-line for the flat, fully-expanded table). Long name lists in the vars/mutates/ funcs cells condense (each long name ellipsized, at most three shown, then a bare +N), an overlong reason ellipsizes too, and the unresolved column drops entirely when nothing is unresolved across the whole population, --deriv-report and --one-line carry the full view. A concept-tag count rides the --docs report, not the wide grid. A mistyped function key suggests close matches ("did you mean: ..."). The compact/MCP column vocabulary is unchanged and additive-only (global_vars, blocker, ... stay the selection names there).

Discovers module-level functions and a class's own methods (static and instance, self/cls intact; a read-only method derives like a plain function, and one that modifies instance state (or lets self escape) is reported derivable: no <- stateful). Only dunder names (__init__, ...) are excluded; a single-leading-underscore helper is included like any other function, since it's often the more claimable one (a public function frequently just dispatches to a private numeric core).

derivable and unconditional are different questions

unconditional asks whether the body lifts with nothing supplied: no declared domain, no claim context. It is a property of the code alone, and it is not a ceiling on what can be proven.

derivable asks what the derive route can do here, given the domain the signature, docstring and claims declare.

They differ constantly, and the difference is not an edge case. A function guarded by a branch reports:

reason  1 branch
code    branch:needs-domain(theta)
derives yes

The blocker is real; the body does not lift bare, and the claim's own for theta in [...] quantifier prunes the branch, so the derive route proves it anyway. A repository read 2/75 on the unconditional number while carrying derive-route proofs on functions the column called underivable.

Read derivable for "can this be proven", unconditional for "does this need help". Neither rules out a probe claim.

The code column and the underivable detail

The reason column reports the shape; code names the exact blocker as a compact, stable code, loop:non-affine-update, branch:needs-domain(scale), unsupported:unbound-name, and --deriv-report adds a detail block below the table for every underivable function, one source line and code per blocking construct, plus each function's full module-state view. The full meaning, the derive_unlock tag, and the fix hint for every code live in Reason codes; mathema describe --issue <key> carries the same detail (plus the interpolated hints) as a structured payload. For a branch, each condition is classified independently, a function can have one resolvable branch and one structurally blocked one, and both get their own line.

Against strength_to_distance (a real fixture in tests/test_branch_pruning.py), here in mypkg.py:

import math


def strength_to_distance(r: float, scale: str = "info") -> float:
    if scale == "info":
        return math.sqrt(1.0 - r ** 2)
    if scale == "linear":
        return 1.0 - r
    raise ValueError(f"unknown scale {scale!r}, use 'info' or 'linear'")

--deriv-report closes the report with this block (the table above it trimmed here):

$ mathema audit mypkg --deriv-report
underivable functions:
  mypkg.strength_to_distance:
    line 2  branch:needs-domain(scale)
    line 4  branch:needs-domain(scale)
codes explained: the reason-code reference in the docs, or `mathema describe --issue <key>` for the full details

Both branches are resolvable once scale has a declared domain, a claim like for r in [0, 1], scale in {"info"}, f(r, scale) == sqrt(1.0 - r^2) proves. A function whose branch condition depends on a local variable not traceable back to its own parameters (or one with an unrecognized condition shape) gets a branch:untraceable-local / branch:unrecognized-shape code instead.

See The derive route for what resolvable and blocked mean in terms of derivability, and Reason codes for the complete code table.

The quality score

quality is a quality checklist, not a style grade, every criterion is a presence check, not a correctness check, and a criterion that genuinely doesn't apply to a given function (no parameters, no raise anywhere in the body, no return value) shrinks the denominator instead of counting against the score. "documents its parameters" and "documents raising" are scored per parameter/exception, not as one all-or-nothing point each, a function with 3 parameters and 2 documented contributes 2 to the score and 3 to the denominator, not a flat 0 or 1, so the overall "N/M" reflects how close a docstring is, not just whether it's perfect.

Criterion Applicable when Passes when
has a docstring always the docstring is non-empty
has a summary the docstring is non-empty there's real text before the first recognized section header (Args/Parameters/... or a numpydoc Header\n----)
documents its parameters the function has at least one real parameter (self/cls excluded) every parameter's name appears literally somewhere in the docstring
documents its return value analyze() infers a non-None, non-unknown return kind a Returns:/Yields: section exists
documents raising the body contains at least one raise <Exception>(...) with a resolvable exception type name a Raises:/Exceptions: section exists and every distinct raised exception type is individually named in it, a section that exists but never mentions one of the actually-raised types still fails

Example: this function only partially satisfies "documents raising"; ValueError is named, OverflowError isn't, so documents_raises is False even though a Raises: section is present:

def half(x: float) -> float:
    """Halves x.

    Raises:
        ValueError: if x is negative.
    """
    if x < 0:
        raise ValueError("x must be nonnegative")
    if x > 1e6:
        raise OverflowError("x too large")
    return x / 2

Run this checklist against a single function without the audit CLI, useful in a notebook or a quick interactive check:

>>> import mathema
>>> mathema.docstring_report(half)
mathema.DocstringReport(half) · 4/6 criteria met
  ✓ has a docstring
  ✓ has a summary
  ✓ params documented (1/1)
  ✗ documents its return value
  ✗ exceptions documented (1/2)

This is the same checklist audit's docs column runs, a separate, much stricter, opinionated docstring convention also exists (claims, domain, and type declarations live in the docstring itself); see The mathema docstring.

--docs: just the docs criteria, broken out

Two labeled tables: quality: (the checklist, plus a concepts/tags count) and docsync: (the strict schema; its claims column header reads {min_expected|actual|est_applicable}).

mathema audit --docs skips the wide table and every other analysis (nothing else is even computed) in favor of a colored grid, one row per function, with each checklist criterion as its own column (has_docstring, has_summary, params, returns, raises, quality_ratio, claims, concepts/tags) instead of one summary "N/M", the same per-parameter/per-exception counts described above, laid out so a whole package's docstring gaps are scannable at a glance rather than read one function-block at a time. The docsync sync grid is appended by default (see docsync); exclude it with --exclude docsync.

Rollups

A by module: section prints automatically whenever a sweep touches more than one module; a by package: section prints when more than one target was given on the command line. Both share the same aggregate counts (claimed, derivable, typed, tested, docs) the grand-total line already computes.