Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

“Send in your skeletons Sing as their bones go marchin’ in again The need you buried deep The secrets that you keep are ever ready Are you ready? I’m finished makin’ sense Done pleading ignorance, that whole defense Spinning infinity, boy The wheel is spinnin’ me, it’s never ending, never ending Same old story” — Foo Fighters

Getting Started

This guide walks you from a fresh install to a passing pretender check in a real repository. It covers the six steps most teams need on day one.

Prerequisites

  • Rust toolchain (stable) — install via rustup.rs
  • Git ≥ 2.x — required for --diff-only and hook installation
  • Internet access for the initial cargo install

1. Install

cargo install pretender

Verify the install:

pretender --version

2. Initialise your repository

Run the interactive wizard from the root of your repository:

pretender init

The wizard will:

  1. Ask which languages your project uses
  2. Suggest threshold values based on project size
  3. Write pretender.toml to the repo root
  4. Offer to install the pre-commit hook
  5. Offer to generate a CI workflow file

A minimal generated pretender.toml looks like:

[pretender]
mode = "tiered"
languages = ["auto"]

[thresholds]
cyclomatic_max = 10
cognitive_max  = 15

See the Configuration reference for every available key.


3. Run your first check

pretender check .

pretender check . scans all files in the given paths. To check only files changed relative to origin/main:

pretender check . --diff-only

Reading the output

⚠ src/parser.py — parse_token(): cyclomatic 14 > 10
  role: test
  parse_token: cyclomatic=14, cognitive=8, assertions=0, function_lines=15, params=2, nesting_max=2, abc=3.46
    ADVISORY cyclomatic 14 > 10
SymbolMeaning
⚠Advisory — metric exceeds the configured threshold (tiered mode)
✗Violation — metric exceeds the threshold in gate mode

In gate mode every violation exits non-zero. In the default tiered mode advisories are printed but exit is still zero.


4. Fix a violation

Suppose parse_token() scores cyclomatic=14 against a threshold of 10. Extract the per-branch logic into helpers to bring each function under the threshold:

Before — one function with 14 branches:

def parse_token(token, context, strict):
    if token.type == "string":
        if strict and not token.value.startswith('"'):
            raise ValueError("unquoted string")
        return token.value.strip('"')
    elif token.type == "number":
        try:
            return int(token.value)
        except ValueError:
            return float(token.value)
    # … six more branches omitted for brevity

After — each helper is cyclomatic ≤ 3:

def _parse_string(token, strict):
    if strict and not token.value.startswith('"'):
        raise ValueError("unquoted string")
    return token.value.strip('"')

def _parse_number(token):
    try:
        return int(token.value)
    except ValueError:
        return float(token.value)

def parse_token(token, context, strict):
    if token.type == "string":
        return _parse_string(token, strict)
    elif token.type == "number":
        return _parse_number(token)
    # …

Recheck after refactoring:

pretender check src/parser.py

Adopting a gate baseline

Some violations are legitimate but hard to fix right now, and mechanical changes (a tree-wide reformat, a generated file) shouldn’t read as “new” violations forever. check --adopt writes the current scan’s violations into a gate baseline — pretender-baseline.json, next to pretender.toml:

pretender check --adopt
# ✓ adopted 3 findings into pretender-baseline.json (file_lines 1, function_lines 2)

Commit that file. From then on, pretender check --mode gate fails only on genuinely new violations — findings whose stable ID (path, function name, rule — deliberately not line numbers) is not in the baseline. Known violations pass with a summary line:

→ gate: 3 known (baselined), 0 new

Without a baseline file, gate mode keeps its fail-closed behavior: any violation fails. A corrupt or unreadable baseline also fails the check rather than silently degrading. Adopted findings still appear in reports and resolution tracking — the baseline changes gate semantics, not visibility. Refresh the baseline by re-running with --adopt.


5. Install the pre-commit hook

Keep violations from reaching the repository by running pretender before each commit:

pretender hooks install

This writes a .git/hooks/pre-commit script that runs pretender check --staged on the files you are about to commit. To remove it:

pretender hooks uninstall

6. Add CI integration

Generate a GitHub Actions workflow:

pretender ci generate github

This writes .github/workflows/pretender.yml. The workflow runs pretender check on every pull request and, if mutation testing is configured, gates merges on the mutation score defined by thresholds.mutation_min in your pretender.toml.

For mutation testing details, see Mutation testing. To gate on mutation score in CI, set thresholds.mutation_min in pretender.toml — the generated workflow picks it up automatically.


Next steps

TopicDocument
All pretender.toml keys and defaultsConfiguration reference
Mutation testing flags and JSON outputMutation testing
Writing custom metric pluginsWriting plugins
Language-specific tracked nodeslanguages/

Ecosystem

pretender is part of the charly-vibes Tool Ecosystem — the map shows how it relates to its sibling tools.

Release Status

Generated artifact (docs.yml, from Cargo.toml + git ref). Do not hand-edit — the repo source files remain authoritative.

  • Version: v0.10.0
  • Built from: main @ 1fcf431

Configuration Reference

pretender.toml is placed at the root of your repository. All sections and keys are optional — omitted keys fall back to the defaults shown here.


For charly-vibes suite repos (Rust CLI tools like pretender, wai, dont, espectacular, vampiro, testaruda, crua, livin), we recommend pinning roles and overriding the default thresholds to match a typical Rust project layout:

RolePathsNotes
testtests/**, **/*_test.*, spec/**Default — already matches
unit-testtests/unit/**Explicit sub-role for fast unit tests
integration-testtests/integration/**Explicit sub-role for slower integration
scriptscripts/**, examples/**Default — already matches
generated**/*_generated.*Default — pb.go, protobuf, etc.

Why pin roles explicitly

Without a pretender.toml, pretender uses library defaults: all files get the app role, tests are detected via heuristics only, and every role’s paths use convention over configuration. This works, but has two downsides:

  1. No sub-role awareness — tests/unit/ and tests/integration/ are not differentiated, so duration thresholds (duration_max_ms) never activate.
  2. No version-controlled baseline — new team members or CI environments must guess which role layout your project uses.

Pinning roles in pretender.toml resolves both issues and makes the config part of the reviewed, committed project state.

Baseline threshold values

These values are copied from templates/pretender.toml.example and represent the recommended starting point for Rust CLI repos:

[thresholds]
cyclomatic_max        = 10
cognitive_max         = 15
function_lines_max    = 40
file_lines_max        = 400
nesting_max           = 3
params_max            = 4
abc_max               = 30
duplication_pct_max   = 5

[thresholds.test]
cyclomatic_max      = 3
cognitive_max       = 5
function_lines_max  = 80
nesting_max         = 2
params_max          = 2
duplication_pct_max = 30
min_assertions      = 1

[thresholds.library]
exported_params_max       = 3
exported_cyclomatic_max   = 8
exported_lines_max        = 30
require_docstring         = true

[thresholds.unit-test]
duration_max_ms = 100

[thresholds.integration-test]
duration_max_ms = 2000

that may not match the project’s quality targets.

Doctor check for effective hooks path

pretender doctor also includes a Hook location check: it resolves core.hooksPath across all config scopes (local, global, system) using genesis::git_hooks::effective_hooks_dir and warns when git will invoke hooks from a directory other than where the pretender hook was installed — the case where a global core.hooksPath shim (e.g. lefthook) silently makes the installed gate inert. An empty-string core.hooksPath (git disables hooks entirely) is reported as an error.

Deploying across the suite

Each suite repo should commit its own pretender.toml (start from the template at templates/pretender.toml.example) and adjust thresholds as needed. The canonical template lives in the pretender repo and serves as the upstream source of truth for all suite repos.


[pretender]

KeyTypeDefaultDescription
modestring"gate"Check behaviour: "gate" (any violation fails — the fail-closed default, softened only by an adopted gate baseline), "tiered" (failures scale with severity; advisory), "guidance" (hints only; advisory)
advisory_untilISO date string— (required for tiered/guidance)Advisory lease expiry (pretender-1te). Advisory modes are dated downgrades from the gate default: an expired, missing, or unparseable lease makes check FAIL, forcing a renewed, dated, reviewable decision. Renewal = push the date forward in this file (a visible diff). Gate mode never consults the lease.
languagesarray of strings["auto"]Languages to analyse. "auto" detects from file extensions. Explicit values: "python", "rust", "go", "javascript", "typescript", "java", "ruby", "c", "cpp"
excludearray of glob strings["vendor/**", "node_modules/**", "**/*_generated.*"]Path globs to skip during analysis
respect_gitignorebooleantrueWhen walking directories, skip anything ignored by git (repo + nested .gitignore, global core.excludesFile, .git/info/exclude) so target/, node_modules/, and vendored code aren’t scanned without per-repo exclude lists. Outside a git repo the walk is unfiltered. Explicitly passed file paths and --staged are never filtered. Pass --scan-ignored to check for a one-off exhaustive walk (pretender-094).

[thresholds]

App-level metric limits applied to files assigned the app role (the default for files not matched by any other role).

KeyTypeDefaultDescription
cyclomatic_maxinteger10Maximum cyclomatic complexity per function
cognitive_maxinteger15Maximum cognitive complexity per function
function_lines_maxinteger40Maximum lines per function
file_lines_maxinteger400Maximum lines per file
nesting_maxinteger3Maximum control-flow nesting depth
params_maxinteger4Maximum formal parameters per function
abc_maxinteger30Maximum ABC score (√(A²+B²+C²)) per function
duplication_pct_maxinteger5Maximum structural duplication percentage (0–100)

Planned (not yet enforced): Maintainability Index (mi_min), coverage (coverage_line_min / coverage_branch_min), and mutation score (mutation_min) thresholds are not currently configurable. They were removed from the schema in v0.5.0 because they were never enforced — passing a config that sets these keys silently ignored them. Mutation score is already gateable via pretender mutation --score-min N; coverage and MI enforcement are future work.

[thresholds.unit-test]

Overrides for files assigned the unit-test sub-role. Inherits all [thresholds.test] values and overlays the duration threshold.

KeyTypeDefaultDescription
duration_max_msinteger0Maximum allowed test duration in milliseconds. 0 disables the check.

[thresholds.integration-test]

Overrides for files assigned the integration-test sub-role. Inherits all [thresholds.test] values and overlays the duration threshold.

KeyTypeDefaultDescription
duration_max_msinteger0Maximum allowed test duration in milliseconds. 0 disables the check.

[thresholds.test]

Overrides for files assigned the test role.

KeyTypeDefaultDescription
cyclomatic_maxinteger3Cyclomatic limit for test functions
cognitive_maxinteger5Cognitive limit for test functions
function_lines_maxinteger80Line limit for test functions
nesting_maxinteger2Nesting limit for test functions
params_maxinteger2Parameter limit for test functions
duplication_pct_maxinteger30Duplication tolerance in test files
min_assertionsinteger or null1Minimum assertion calls per test function; null disables

[thresholds.library]

Overrides for files assigned the library role.

KeyTypeDefaultDescription
exported_params_maxinteger3Parameter limit on exported/public functions
exported_cyclomatic_maxinteger8Cyclomatic limit on exported/public functions
exported_lines_maxinteger30Line limit on exported/public functions
require_docstringbooleantrueRequire a docstring on every exported/public function

[thresholds.script]

Overrides for files assigned the script role.

KeyTypeDefaultDescription
function_lines_maxinteger100Line limit per function
file_lines_maxinteger300Line limit per file

[bands]

Colour bands used when mode = "tiered" to map raw metric values to severity levels. Each band specifies thresholds for green (acceptable), yellow (warning), and red (violation). Values must satisfy green <= yellow <= red.

Each band (cyclomatic, cognitive) is optional. When the [bands] section is entirely absent from pretender.toml, both bands are active with the defaults shown below. However, once you add a [bands] section, any band key you omit is disabled (set to None) — it does not fall back to its default. Always specify both bands together when overriding either one.

# Inline form
[bands]
cyclomatic = { green = 10, yellow = 15, red = 20 }
cognitive  = { green = 15, yellow = 25, red = 40 }

# Block form (equivalent)
[bands.cyclomatic]
green  = 10
yellow = 15
red    = 20

[bands.cognitive]
green  = 15
yellow = 25
red    = 40

[bands.cyclomatic]

Default (when [bands] is absent): { green = 10, yellow = 15, red = 20 }

KeyTypeDescription
greenintegerCyclomatic complexity at or below this is green
yellowintegerAt or below this is yellow
redintegerAbove this is red

[bands.cognitive]

Default (when [bands] is absent): { green = 15, yellow = 25, red = 40 }

KeyTypeDescription
greenintegerCognitive complexity at or below this is green
yellowintegerAt or below this is yellow
redintegerAbove this is red

[scope]

Controls which files are analysed during pretender check.

KeyTypeDefaultDescription
diff_onlybooleantrueWritten by pretender init to record your project preference. Setting this to true does not automatically enable diff filtering — you must pass --diff-only on the CLI (or CI step) to activate it. When active, only files changed relative to diff_base are checked
diff_basestring"origin/main"Git ref used as the comparison base for --diff-only

[execute]

Optional shell commands pretender can run to collect coverage and mutation data.

KeyTypeDefaultDescription
enabledbooleanfalseRun external commands (test, coverage, mutation) during pretender check
coverage_cmdstring or nullnullPlanned: Shell command that produces a coverage report. Not yet executed by pretender check
mutation_cmdstring or nullnullPlanned: Shell command that runs mutation testing. Mutation score is already gateable via pretender mutation --score-min N
test_cmdstring or nullnullShell command that runs the test suite and produces a JUnit XML report (used with --execute)
test_report_pathstring or nullnullPath to the JUnit XML report produced by test_cmd (relative to repo root)
test_timeout_sinteger600Timeout in seconds for test_cmd; on timeout pretender emits a test-cmd-timeout error
test_time_unitstring"seconds"Unit of the JUnit time attribute: "seconds" (XSD-conformant) or "milliseconds" (non-conformant runners)

[plugins]

Controls which built-in language and metric plugins are active.

KeyTypeDefaultDescription
languagesarray of strings["python","javascript","typescript","go","rust"]Language plugins to load
metricsarray of strings["eslint","ruff","clippy"]Built-in metric tool plugins to load

External metric plugins are always loaded in addition to this list from the first directory found in this order:

  1. $PRETENDER_METRICS_DIR (if set)
  2. $XDG_CONFIG_HOME/pretender/metrics/ (if XDG_CONFIG_HOME is set)
  3. ~/.config/pretender/metrics/ (fallback)

See Writing plugins.


[output]

KeyTypeDefaultDescription
formatsarray of strings["human","sarif"]Output formats to emit. Valid values: "human", "json", "sarif", "junit", "markdown". At least one value is required
sarif_pathstring"pretender.sarif"File path for SARIF output when "sarif" is in formats

[roles.*]

Role matchers assign a role to files based on path globs. The resolved role determines which [thresholds.*] block applies.

Each role section has a single key:

KeyTypeDescription
pathsarray of glob stringsFiles matching any glob are assigned this role
classname_rootstringFor [roles.test] only: directory to search when resolving JUnit classname to a source file (default "tests")

Available roles and their defaults:

RoleDefault paths
test["tests/**", "**/*_test.*", "spec/**"]
unit-test["tests/unit/**"]
integration-test["tests/integration/**"]
library["pkg/**", "lib/**"]
script["scripts/**", "examples/**"]
generated["**/*.pb.go", "**/*_generated.*"]
vendor["vendor/**", "node_modules/**"]

Example override:

[roles.test]
paths = ["tests/**", "**/*.spec.*", "**/__tests__/**"]

Role detection order

pretender assigns the first role that matches, checked in this priority order:

  1. Pragma — a comment on any line of the file: # pretender: role=<name> or // pretender: role=<name>
  2. Configured glob — the first [roles.*] section whose glob matches the file path
  3. File-name heuristic — built-in patterns (e.g. _test. suffix → test, _generated. → generated)
  4. Default — app

Test-duration check

pretender can analyse per-test timing data from a JUnit XML report and flag tests that exceed role-appropriate duration budgets. This is the first dynamic check in pretender — it consumes a runtime artifact rather than source text.

Activation

The check activates when you pass --test-report <path> to pretender check. You can also configure [execute] test_cmd and use --execute to have pretender run the test suite for you:

# Analyse an existing report
pretender check --test-report target/junit-report.xml

# Run tests first, then analyse
pretender check --execute

Thresholds

Set duration limits per test sub-role:

[thresholds.unit-test]
duration_max_ms = 100       # unit tests should be fast

[thresholds.integration-test]
duration_max_ms = 2000      # integration tests can be slower

A finding is emitted when observed_ms > duration_max_ms (strict comparison, so a test exactly at the threshold does not fail). A threshold of 0 (the default) disables the check.

Sub-role detection

pretender detects unit-test and integration-test sub-roles using the same resolution order as other roles:

  1. Pragma — # pretender: role=unit-test or // pretender: role=unit
  2. Configured glob — [roles.unit-test] with custom paths
  3. Heuristic — tests/unit/** and test/unit/** → unit-test; tests/integration/** and test/integration/** → integration-test
  4. Default — test (no duration threshold)

Sub-roles inherit all [thresholds.test] values and overlay only duration_max_ms.

How timing data is read

pretender parses standard JUnit XML (<testsuite> / <testcase time="...">). The time attribute is interpreted as seconds by default (XSD-conformant); set [execute] test_time_unit = "milliseconds" for non-conformant runners. Rounding uses round-half-up to the nearest millisecond.

JUnit emission incantations per runner

RunnerCommand
pytestpytest --junitxml=report.xml
cargo testcargo2junit > report.xml (requires cargo2junit crate)
Mavenmvn surefire-report:report (produces target/surefire-reports/*.xml)
GradleBuilt-in: build/reports/tests/test/*.xml
Jestjest --junitOutput=report.xml (requires jest-junit reporter)

CLI flags

FlagDescription
--test-report <path>Path to a JUnit XML report for duration analysis
--executeRun [execute] test_cmd before analysing durations

Output

Findings appear in all output formats:

  • Human: unit-test test_addition tests/unit/test_math.py: 100ms > 50ms
  • JSON: test_findings top-level array with test_name, file, role, observed_ms, threshold_ms
  • SARIF: pretender/test-duration rule id, Warning level

In gate mode, any duration finding causes a non-zero exit code.

pretender maintains a local event log that surfaces recurring problem areas across runs — useful for calibrating thresholds and identifying structural debt before it compounds.

pretender tracks every violation it reports in .pretender/history/events.jsonl at the root of your repository. Each line is a JSON object (a ViolationEvent) recording the file fingerprint, rule key, role, area, run ID, and Unix timestamp.

Events older than 90 days are pruned automatically on each run.

From the event log pretender computes two summaries shown at the end of pretender check:

  • Hotspots — the 10 files with the highest total violation count across at least two distinct days. A file that repeatedly triggers is a structural problem, not a fluke.
  • Patterns — the 10 (rule, role, area) combinations that recur most often across the most files. Use these to calibrate thresholds or identify conventions your team hasn’t codified yet.

Resolution tracking — every check run (including clean runs) also persists the current set of finding IDs to .pretender/history/last-findings.json. IDs are stable across line-shift edits (path::unit_name::rule_key). The next run reports a delta:

→ resolution: 3 fixed, 5 still open, 2 new (38%)

The rate is fixed / (fixed + still_open) — null on the first tracked run. The same delta is available to machine consumers under data.history.resolution in JSON output and in history/summaries.json, so CI dashboards can track the fixed-vs-flagged ratio over time.

The .pretender/ directory should be committed so the feedback loop survives across machines and CI runs. Note: events.jsonl records file paths and fingerprints — review the file before committing if your repository contains sensitive filenames.

Mutation Testing

Mutation testing measures test-suite effectiveness by injecting small code changes (“mutants”) and checking whether the tests catch them. A high mutation score means your tests fail when the code is wrong — a low score means gaps exist even when line coverage looks healthy.

pretender mutation <paths...> wraps language-specific mutation testing tools and reports results in a unified format. Each tool must be installed separately — pretender invokes them as subprocesses.


Quick reference

FlagPurposeExample
(none)Run mutation testing and print resultspretender mutation src/
--dry-runList mutation sites without running testspretender mutation src/ --dry-run
--score-min NExit non-zero if score < N%pretender mutation src/ --score-min 80
--format jsonEmit structured JSON reportpretender mutation src/ --format json

Tool selection

pretender detects the dominant language from the target paths and selects the appropriate tool automatically:

LanguageToolInstall
Pythonmutmutpip install mutmut
Rustcargo-mutantscargo install cargo-mutants
JavaScript / TypeScriptStrykernpm install --save-dev @stryker-mutator/core

If a project contains multiple languages, pretender uses the language of the majority of files in the given paths.

Java (PIT) — not yet automated

pretender mutation does not yet invoke PIT directly. Run mutation coverage manually with Maven or Gradle:

mvn org.pitest:pitest-maven:mutationCoverage
# or
gradle pitest

--dry-run still works for Java files — pretender uses tree-sitter to list candidate mutation sites without invoking PIT.


Basic usage

# Run mutation testing against a package
pretender mutation src/

# Preview mutation sites without running tests
pretender mutation src/ --dry-run

# Fail if mutation score falls below a threshold
pretender mutation src/ --score-min 80

# Emit results as JSON
pretender mutation src/ --format json

--dry-run: preview mutation sites

Lists every planned mutation without executing the test suite. Useful for understanding coverage before committing to a full run.

pretender mutation src/ --dry-run

Example output:

src/engine.rs:42  replace operator `>`
src/engine.rs:67  replace return value
src/config.rs:15  replace operator `+`

Note: For Rust, --dry-run queries cargo mutants --list and reflects the real mutation plan. For all other languages (Python, JavaScript, TypeScript, Java), it uses a built-in tree-sitter analysis that enumerates operator and return-value sites — the count and descriptions are approximate and may differ from what the underlying tool would produce.


--score-min: CI gate

Exits non-zero if the achieved mutation score is below the given percentage. Use this in CI to enforce a minimum quality bar.

pretender mutation src/ --score-min 80

The exit code is 0 when the score meets or exceeds the threshold, 1 otherwise.


--format json: machine-readable output

Emits a MutationReport JSON object to stdout.

pretender mutation src/ --format json

MutationReport fields

FieldTypeDescription
toolstringName of the mutation tool used (e.g. "cargo-mutants")
totalintegerTotal number of mutants generated
killedintegerMutants caught by the test suite
survivedintegerMutants not caught (test gaps)
score_pctfloatMutation score: (killed / total) * 100. Returns 100.0 when no mutants are generated
survivorsarrayList of surviving mutant descriptors (see below)

Surviving mutant fields

FieldTypeDescription
filestringSource file containing the mutant
lineintegerLine number of the mutation
descriptionstringHuman-readable description of the applied mutation
missed_testsarray of stringsTest names that should have caught this mutant but did not. Populated by mutmut only; always [] for cargo-mutants and Stryker, which do not report per-mutant test attribution

Example (mutmut, which populates missed_tests):

{
  "tool": "mutmut",
  "total": 120,
  "killed": 102,
  "survived": 18,
  "score_pct": 85.0,
  "survivors": [
    {
      "file": "src/engine.py",
      "line": 42,
      "description": "replace `>` with `>=`",
      "missed_tests": ["test_threshold_boundary"]
    }
  ]
}

CI integration

The recommended way to run mutation testing in CI is through the workflow generated by pretender ci generate github:

pretender ci generate github

This writes .github/workflows/pretender.yml. The generated file includes a check step and a mutation gate step:

- name: pretender check
  run: pretender check .

- name: Mutation score gate
  run: pretender mutation . --score-min 60

The --score-min value defaults to 60 and can be set per invocation or via pretender mutation . --score-min <N> in your CI workflow.

Writing Plugins

pretender supports external metric plugins: small programs that analyse source files and emit findings in JSON. pretender runs each plugin as a subprocess, collects its output, and merges the findings into pretender check results alongside the built-in metrics.


Install location

Place plugin manifests (.toml files) in the metrics directory. pretender searches the first directory found in this order:

  1. $PRETENDER_METRICS_DIR (if set)
  2. $XDG_CONFIG_HOME/pretender/metrics/ (if XDG_CONFIG_HOME is set)
  3. ~/.config/pretender/metrics/ (fallback)

Each .toml file in that directory is loaded as one plugin. Non-.toml files and files with parse errors are skipped — a warning is printed to stderr for each invalid file.


Plugin manifest format

A plugin manifest is a TOML file with the following fields:

FieldTypeRequiredDescription
namestringyesPlugin name shown in output (e.g. "ruff")
extensionsarray of stringsyesFile extensions this plugin applies to (e.g. [".py"]) — must include the leading dot
commandarray of stringsyesCommand and arguments to run. The token {files} is expanded to the list of matching file paths
parserstringyesOutput format: "json" (a JSON array) or "json-lines" (one JSON object per line)
[mapping]tableyesField mapping from the plugin’s JSON output to pretender’s finding schema (see below)

[mapping] sub-table

Each mapping value is a dot-separated path into the JSON object emitted by the plugin. For example, "location.row" navigates {"location": {"row": 5}}.

FieldTypeRequiredDescription
pathstringyesJSON path to the source file name
linestringyesJSON path to the line number (integer)
messagestringyesJSON path to the human-readable finding message
codestringnoJSON path to an optional finding code (e.g. "E501")

Finding schema

Each finding pretender emits from a plugin has these fields:

FieldTypeDescription
sourcestringThe plugin name
lineintegerLine number of the finding
messagestringHuman-readable description
codestring or nullOptional finding code

Worked example

The metrics/example/ directory in this repository contains a minimal, runnable plugin — a shell script that emits one JSON finding per violation and a TOML manifest wiring it to pretender. Copy the files and edit the path to install it locally.

Manifest — metrics/example/long-lines.toml

name       = "long-lines"
extensions = [".py", ".js", ".ts", ".rs"]
command    = ["metrics/example/long-lines.sh", "{files}"]
parser     = "json-lines"

[mapping]
path    = "file"
line    = "line"
message = "message"
code    = "code"

Script — metrics/example/long-lines.sh

The script checks for lines exceeding 100 characters and emits one JSON object per violation:

#!/usr/bin/env sh
for file in "$@"; do
  awk -v file="$file" 'length > 100 {
    printf "{\"file\":\"%s\",\"line\":%d,\"message\":\"line exceeds 100 chars (%d)\",\"code\":\"LL001\"}\n",
      file, NR, length
  }' "$file"
done

Installing the example

Use absolute paths in command — relative paths are resolved from wherever pretender check is run, which can vary. Copy both files to the metrics directory:

mkdir -p ~/.config/pretender/metrics
cp metrics/example/long-lines.sh ~/.config/pretender/metrics/
chmod +x ~/.config/pretender/metrics/long-lines.sh

# Update the command path in the manifest before copying
sed 's|metrics/example/long-lines.sh|/home/you/.config/pretender/metrics/long-lines.sh|' \
  metrics/example/long-lines.toml \
  > ~/.config/pretender/metrics/long-lines.toml

# Verify it appears in check output
pretender check src/

The finding will appear in human output as:

src/lib.rs:87  [long-lines] line exceeds 100 chars (112)  LL001

How findings are merged

After each pretender check run, pretender runs every applicable plugin against the files being checked. Findings from plugins are merged with built-in metric findings and displayed in the same output stream. The source field identifies which plugin produced each finding.

Plugins are always loaded from the metrics directory in addition to any built-in metric tools listed in [plugins].metrics in pretender.toml. See the Configuration reference for the [plugins] section.


Real-world examples

ruff (Python linter)

name       = "ruff"
extensions = [".py"]
command    = ["ruff", "check", "--output-format=json", "--select=E501", "{files}"]
parser     = "json"

[mapping]
path    = "filename"
line    = "location.row"
message = "message"
code    = "code"

ESLint (JavaScript / TypeScript)

ESLint’s JSON output nests messages inside per-file objects — one array entry per file, not per finding. pretender’s dot-path mapping does not support array indexing, so a thin wrapper script is required to flatten the output.

Requires: jq — install with your system package manager (brew install jq, apt install jq, etc.).

#!/usr/bin/env sh
# eslint-wrapper.sh
# Requires jq. Flattens ESLint JSON output to one JSON object per finding.
npx eslint --format=json "$@" | jq -c '
  .[] | .filePath as $f | .messages[] |
  {file: $f, line: .line, message: .message, code: .ruleId}
'
name       = "eslint"
extensions = [".js", ".jsx", ".ts", ".tsx"]
command    = ["/path/to/eslint-wrapper.sh", "{files}"]
parser     = "json-lines"

[mapping]
path    = "file"
line    = "line"
message = "message"
code    = "code"

Replace /path/to/eslint-wrapper.sh with the absolute path where you install the wrapper script.

Implementation Status

Current build status for every command and feature. Stubbed items exit with code 2 and a “not yet implemented” message. Partial items are functional but have noted gaps.


Commands

CommandStatusNotes
pretender init✅ ImplementedInteractive and --non-interactive modes
pretender check✅ Implementedhuman, json, sarif output; --staged and --diff-only filters
pretender complexity✅ ImplementedSingle-file, worst-first sort
pretender report✅ Implementedhuman, markdown, html
pretender duplication✅ ImplementedCross-file and single-file
pretender mutation✅ ImplementedPython, Rust, JS, TS — see caveats
pretender hooks install [hook]✅ ImplementedWrites git hook shim (pre-commit, pre-push)
pretender hooks uninstall✅ ImplementedRemoves Pretender-managed shim
pretender ci generate github✅ ImplementedWrites .github/workflows/pretender.yml
pretender explain✅ ImplementedAll built-in metrics
pretender plugins❌ StubTracked: pretender-07m

Partial features

FeatureStatusDetail
check --format junit❌ StubTracked: pretender-t2m
check --format markdown❌ StubTracked: pretender-t2m
ci generate <non-github>❌ StubOnly github is implemented
Mutation testing — Java⚠️ Partial--dry-run works via tree-sitter; PIT is not invoked automatically

Language support matrix

LanguageComplexityMutation
Python✅✅ (mutmut)
Rust✅✅ (cargo-mutants)
JavaScript✅✅ (Stryker)
TypeScript✅✅ (Stryker)
Go✅—
Java✅⚠️ dry-run only
Ruby✅—
C✅—
C++✅—

Status

beta — core works and is dogfooded in anger; surface may shift before stable.

Implemented

CommandStatusNotes
pretender initstableproject config bootstrap
pretender reportstablecomplexity report (cyclomatic, cognitive, abc, params)
pretender ci generate githubstableemit CI quality-check steps
pretender lintbetacomplexity/lint gate over changed code

In progress

  • Theme/docs conformance round (DDL-u8x epic); docs/src restructure.

Mapped to specs

  • Complexity model definitions in docs/ (mutation.md, plugins.md); openspec proposals in-repo.

Dogfooding

  • wai, testaruda, dont (planned pins) install pretender per versions.ddl.toml; testaruda carries a pretender.toml
  • pretender’s own CI runs just ci

C

Reference for which tree-sitter nodes pretender tracks for C files, how each contributes to pretender check metrics, and known measurement gaps. Consult this page when tuning thresholds or diagnosing unexpected scores.

Overview

Analyses .c and .h files using a tree-sitter grammar. Tracks standard C function definitions (not declarations). Header files are included so complexity in inline or static functions defined in headers is measured.

Tracked nodes

Tree-sitter nodeCaptureMetric impact
function_definition@function.definitionDefines a metric scope
if_statement@branch.if+1 cyclomatic, +1 cognitive
for_statement@branch.loop+1 cyclomatic, +1 cognitive
while_statement@branch.loop+1 cyclomatic, +1 cognitive
do_statement@branch.loop+1 cyclomatic, +1 cognitive
case_statement@branch.case+1 cyclomatic, +1 cognitive
binary_expression "&&"@branch.logical.and+1 cyclomatic, +1 cognitive
binary_expression `““`
call_expression@call+1 ABC C-count
assignment_expression@assign+1 ABC A-count
init_declarator (with value)@assign+1 ABC A-count

Assertion patterns

PatternMatches
^assertassert(...) from <assert.h>
^ASSERTCustom assertion macros
^TEST_ASSERTUnity / CUnit style macros

Quirks and limitations

  • Function pointers and macro-defined functions are not tracked as function scopes.
  • goto is not captured as a branch — complexity in goto-heavy code is understated. The smell-weight system flags goto separately.
  • default: in switch is captured as a case_statement and counted as one branch.
  • Header files (.h) are analysed like .c files. Forward declarations (without a body) are not captured; only full definitions are tracked.
  • Preprocessor conditionals (#ifdef, #if) are not parsed by the tree-sitter grammar and do not contribute branch counts.

Example output

src/parser.c  cyclomatic=11  cognitive=14  function_lines=52  params=3
  ↳ parse_token()  cyclomatic=11  [threshold: 10]
include/utils.h  cyclomatic=2  cognitive=2  function_lines=8  params=1

C++

Reference for which tree-sitter nodes pretender tracks for C++ files, how each contributes to pretender check metrics, and known measurement gaps. Consult this page when tuning thresholds or diagnosing unexpected scores.

Overview

Analyses .cpp, .cc, .cxx, .hpp, and .hxx files using a tree-sitter grammar. Tracks both free functions and out-of-class method definitions (qualified identifiers). Range-based for loops and catch clauses each count as branches.

Tracked nodes

Tree-sitter nodeCaptureMetric impact
function_definition (simple identifier)@function.definitionDefines a metric scope
function_definition (qualified identifier)@function.definitionDefines a metric scope
if_statement@branch.if+1 cyclomatic, +1 cognitive
for_statement@branch.loop+1 cyclomatic, +1 cognitive
for_range_loop@branch.loop+1 cyclomatic, +1 cognitive
while_statement@branch.loop+1 cyclomatic, +1 cognitive
do_statement@branch.loop+1 cyclomatic, +1 cognitive
catch_clause@branch.catch+1 cyclomatic, +1 cognitive
case_statement@branch.case+1 cyclomatic, +1 cognitive
binary_expression "&&"@branch.logical.and+1 cyclomatic, +1 cognitive
binary_expression `““`
call_expression@call+1 ABC C-count
assignment_expression@assign+1 ABC A-count
init_declarator (with value)@assign+1 ABC A-count

Assertion patterns

PatternMatches
^assertassert(...) from <cassert>
^ASSERTCustom assertion macros
^EXPECTGoogle Test EXPECT_EQ(...), etc.
^CHECKCHECK(...) (Abseil, glog)

Quirks and limitations

  • Template functions produce one definition entry per instantiation in some grammar revisions; complexity may be overcounted in heavily templated code.
  • Lambda expressions are not tracked as function scopes — their branches count toward the enclosing function’s complexity.
  • Operator overloads (operator+, operator==) are captured as function definitions and metriced like regular functions.
  • ternary_expression (condition ? a : b) is not currently tracked and does not contribute cyclomatic complexity.
  • In-class method definitions (defined inside a class body in a header) use the simple-identifier form and are tracked correctly. Out-of-class definitions (e.g., Foo::bar() in a .cpp file) use the qualified-identifier form and are also tracked.
  • Preprocessor conditionals (#ifdef, #if) are not parsed by the tree-sitter grammar and do not contribute branch counts.

Example output

src/engine.cpp  cyclomatic=10  cognitive=13  function_lines=49  params=3
include/parser.hpp  cyclomatic=5  cognitive=6  function_lines=24  params=2

Go

Reference for which tree-sitter nodes pretender tracks for Go files, how each contributes to pretender check metrics, and known measurement gaps. Consult this page when tuning thresholds or diagnosing unexpected scores.

Overview

Analyses .go files using a tree-sitter grammar. Tracks both top-level functions and method declarations. Go’s select/case and type-switch cases each count as a branch, matching Go’s concurrency and type-dispatch idioms.

Tracked nodes

Tree-sitter nodeCaptureMetric impact
function_declaration@function.definitionDefines a metric scope
method_declaration@function.definitionDefines a metric scope
if_statement@branch.if+1 cyclomatic, +1 cognitive
for_statement@branch.loop+1 cyclomatic, +1 cognitive
expression_case (in switch)@branch.case+1 cyclomatic, +1 cognitive
type_case (in type switch)@branch.case+1 cyclomatic, +1 cognitive
communication_case (in select)@branch.case+1 cyclomatic, +1 cognitive
binary_expression "&&"@branch.logical.and+1 cyclomatic, +1 cognitive
binary_expression `““`
call_expression@call+1 ABC C-count
assignment_statement@assign+1 ABC A-count
short_var_declaration@assign+1 ABC A-count
var_declaration@assign+1 ABC A-count

Assertion patterns

PatternMatches
^assertassert* (testify-style)
^requirerequire.* (testify require package)
^t\.Errort.Error(...), t.Errorf(...)
^t\.Fatalt.Fatal(...), t.Fatalf(...)
^t\.Failt.Fail(), t.FailNow()

Quirks and limitations

  • Go has no else if node — else if chains are parsed as nested if_statement nodes, so each branch is counted independently (which is the intended behaviour).
  • No ternary operator — Go does not have ?:, so no ternary branches are ever counted.
  • default case in a switch is not captured as a branch — only explicit case labels contribute.
  • select blocks used for concurrency can produce high branch counts in functions that multiplex many channels.

Example output

pkg/handler.go  cyclomatic=7  cognitive=9  function_lines=42  params=2
internal/router.go  cyclomatic=11  cognitive=15  function_lines=48  params=3
  ↳ Route()  cyclomatic=11  [threshold: 10]

Java

Reference for which tree-sitter nodes pretender tracks for Java files, how each contributes to pretender check metrics, and known measurement gaps. Consult this page when tuning thresholds or diagnosing unexpected scores.

Overview

Analyses .java files using a tree-sitter grammar. Tracks both method declarations and constructors as function scopes. Enhanced for loops and switch statement groups each count as separate branches.

Tracked nodes

Tree-sitter nodeCaptureMetric impact
method_declaration@function.definitionDefines a metric scope
constructor_declaration@function.definitionDefines a metric scope
if_statement@branch.if+1 cyclomatic, +1 cognitive
for_statement@branch.loop+1 cyclomatic, +1 cognitive
enhanced_for_statement@branch.loop+1 cyclomatic, +1 cognitive
while_statement@branch.loop+1 cyclomatic, +1 cognitive
do_statement@branch.loop+1 cyclomatic, +1 cognitive
catch_clause@branch.catch+1 cyclomatic, +1 cognitive
ternary_expression@branch.ternary+1 cyclomatic, +1 cognitive
switch_block_statement_group / switch_label@branch.case+1 cyclomatic, +1 cognitive
binary_expression "&&"@branch.logical.and+1 cyclomatic, +1 cognitive
binary_expression `““`
method_invocation@call+1 ABC C-count
object_creation_expression@call+1 ABC C-count
assignment_expression@assign+1 ABC A-count
variable_declarator (with value)@assign+1 ABC A-count

Assertion patterns

PatternMatches
^assertJUnit assertNotNull(...), etc.
^AssertAssert.assertEquals(...) (static import)
^verifyMockito verify(...)
^VerifyVerify.that(...)
^expectexpectedException.expect(...)
^failfail("message")

Quirks and limitations

  • instanceof checks inside if conditions are counted as regular if_statement branches — no special handling for pattern-matching instanceof (Java 16+).
  • Lambda expressions are not tracked as function scopes; their branches count toward the enclosing method’s complexity.
  • switch expressions (Java 14+) may not be fully parsed by the current grammar revision — verify with a test file if you rely on switch-expression branch counts.
  • object_creation_expression (new Foo(...)) counts as a call for ABC C-count, which may inflate scores in builder-heavy code.
  • Mutation testing (pretender mutation) does not yet run PIT automatically. See Mutation testing for the manual workflow.

Example output

src/main/java/Service.java  cyclomatic=9  cognitive=13  function_lines=47  params=3
src/main/java/Parser.java  cyclomatic=15  cognitive=21  function_lines=68  params=4
  ↳ parse()  cyclomatic=15  [threshold: 10]

JavaScript

Reference for which tree-sitter nodes pretender tracks for JavaScript files, how each contributes to pretender check metrics, and known measurement gaps. Consult this page when tuning thresholds or diagnosing unexpected scores.

Overview

Analyses .js, .jsx, .mjs, and .cjs files using a tree-sitter grammar. Tracks named functions, arrow functions assigned to variables, and class methods. switch_case nodes each count as a branch.

Tracked nodes

Tree-sitter nodeCaptureMetric impact
function_declaration@function.definitionDefines a metric scope
variable_declarator with arrow_function@function.definitionDefines a metric scope
method_definition@function.definitionDefines a metric scope
if_statement@branch.if+1 cyclomatic, +1 cognitive
switch_case@branch.if+1 cyclomatic, +1 cognitive
for_statement@branch.loop+1 cyclomatic, +1 cognitive
for_in_statement@branch.loop+1 cyclomatic, +1 cognitive
while_statement@branch.loop+1 cyclomatic, +1 cognitive
do_statement@branch.loop+1 cyclomatic, +1 cognitive
catch_clause@branch.catch+1 cyclomatic, +1 cognitive
ternary_expression@branch.ternary+1 cyclomatic, +1 cognitive
binary_expression "&&"@branch.logical.and+1 cyclomatic, +1 cognitive
binary_expression `““`
call_expression@call+1 ABC C-count
assignment_expression@assign+1 ABC A-count
augmented_assignment_expression@assign+1 ABC A-count
variable_declarator (with value)@assign+1 ABC A-count

Assertion patterns

PatternMatches
^assert$Node.js assert(...)
^assert[A-Z]assertEqual(...), etc.
^expect$Jest / Vitest expect(...)
^it$it('...', ...) test blocks
^test$test('...', ...) blocks
^describe$describe('...', ...) blocks

Quirks and limitations

  • Anonymous arrow functions not assigned to a named variable are not tracked as function scopes; their branches count toward the enclosing scope.
  • ?? (nullish coalescing) is not currently captured as a branch and does not contribute to cyclomatic complexity.
  • Optional chaining (?.) is not tracked.
  • switch_case nodes include both case X: and default: labels — default is counted as one branch.
  • Shebangs (#!/usr/bin/env node) are stripped by the grammar and do not affect parsing.

Example output

src/router.js  cyclomatic=8  cognitive=12  function_lines=44  params=3
src/utils.js  cyclomatic=3  cognitive=4  function_lines=18  params=1

Python

Reference for which tree-sitter nodes pretender tracks for Python files, how each contributes to pretender check metrics, and known measurement gaps. Consult this page when tuning thresholds or diagnosing unexpected scores.

Overview

Analyses .py files using a tree-sitter grammar. Tracks all function forms (top-level, methods, nested, decorated) and Python-specific branch constructs including elif, except, and ternary expressions.

Tracked nodes

Tree-sitter nodeCaptureMetric impact
function_definition@function.definitionDefines a metric scope
if_statement@branch.if+1 cyclomatic, +1 cognitive
elif_clause@branch.elif+1 cyclomatic, +1 cognitive
for_statement@branch.loop+1 cyclomatic, +1 cognitive
while_statement@branch.loop+1 cyclomatic, +1 cognitive
except_clause@branch.catch+1 cyclomatic, +1 cognitive
except_group_clause@branch.catch+1 cyclomatic, +1 cognitive
conditional_expression@branch.ternary+1 cyclomatic, +1 cognitive
boolean_operator "and"@branch.logical.and+1 cyclomatic, +1 cognitive
boolean_operator "or"@branch.logical.or+1 cyclomatic, +1 cognitive
call@call+1 ABC C-count
assignment@assign+1 ABC A-count
augmented_assignment@assign+1 ABC A-count

Assertion patterns

The following call patterns are recognised as assertions for the min_assertions metric (test role):

PatternMatches
^assert$assert(expr) call form; the keyword statement assert expr is captured separately by the assert_statement tree-sitter node
^assert[A-Z]assertEqual, assertRaises, assertIn, etc.
^self\.assertself.assertEqual(...), etc.
^pytest\.raises$pytest.raises(...)
^pytest\.warns$pytest.warns(...)

Quirks and limitations

  • Decorators do not contribute to complexity — only the function body does.
  • Comprehensions ([x for x in y if cond]) contain implicit branches but are not currently tracked; complexity may be understated for comprehension-heavy code.
  • match/case (Python 3.10+) structural pattern matching arms are not yet captured; each arm contributes no cyclomatic count.
  • except* (except_group_clause) is captured and contributes the same as a plain except.

Example output

src/parser.py  cyclomatic=12  cognitive=18  function_lines=55  params=3
  ↳ parse_token()  cyclomatic=12  [threshold: 10]

src/parser.py:42  min_assertions=0  [threshold: 1]  (test role)

Ruby

Reference for which tree-sitter nodes pretender tracks for Ruby files, how each contributes to pretender check metrics, and known measurement gaps. Consult this page when tuning thresholds or diagnosing unexpected scores.

Overview

Analyses .rb files using a tree-sitter grammar. Tracks both instance methods and singleton methods (class-level self. methods). Ruby’s unless, until, and rescue each count as branches alongside the more common if and while.

Tracked nodes

Tree-sitter nodeCaptureMetric impact
method@function.definitionDefines a metric scope
singleton_method@function.definitionDefines a metric scope
if@branch.if+1 cyclomatic, +1 cognitive
unless@branch.if+1 cyclomatic, +1 cognitive
elsif@branch.if+1 cyclomatic, +1 cognitive
for@branch.loop+1 cyclomatic, +1 cognitive
while@branch.loop+1 cyclomatic, +1 cognitive
until@branch.loop+1 cyclomatic, +1 cognitive
rescue@branch.rescue+1 cyclomatic, +1 cognitive
when (in case)@branch.case+1 cyclomatic, +1 cognitive
binary "&&"@branch.logical.and+1 cyclomatic, +1 cognitive
binary `““`
binary "and"@branch.logical.and+1 cyclomatic, +1 cognitive
binary "or"@branch.logical.or+1 cyclomatic, +1 cognitive
call@call+1 ABC C-count
assignment@assign+1 ABC A-count
operator_assignment@assign+1 ABC A-count

Assertion patterns

PatternMatches
^assertMinitest assert(...), assert_equal(...), etc.
^expectRSpec expect(...)
^shouldRSpec legacy should syntax
^raise_errorRSpec raise_error matcher

Quirks and limitations

  • Modifier forms (do_something if condition, value unless flag) are parsed as if/unless nodes and count as branches.
  • Both &&/|| and and/or are tracked — each contributes one cyclomatic point. Code mixing both keyword and operator forms may count more than expected.
  • Blocks (do...end, {...}) are not tracked as function scopes.
  • proc and lambda expressions are not tracked as function scopes.
  • case/in pattern matching (Ruby 3.0+) may not be fully parsed by the current grammar revision.

Example output

lib/parser.rb  cyclomatic=8  cognitive=11  function_lines=39  params=2
spec/parser_spec.rb  min_assertions=0  [threshold: 1]  (test role)

Rust

Reference for which tree-sitter nodes pretender tracks for Rust files, how each contributes to pretender check metrics, and known measurement gaps. Consult this page when tuning thresholds or diagnosing unexpected scores.

Overview

Analyses .rs files using a tree-sitter grammar. Tracks fn items (free functions and methods). match arms each count as one branch, reflecting Rust’s exhaustive pattern matching.

Tracked nodes

Tree-sitter nodeCaptureMetric impact
function_item@function.definitionDefines a metric scope
if_expression@branch.if+1 cyclomatic, +1 cognitive
for_expression@branch.loop+1 cyclomatic, +1 cognitive
while_expression@branch.loop+1 cyclomatic, +1 cognitive
loop_expression@branch.loop+1 cyclomatic, +1 cognitive
match_arm@branch.match_arm+1 cyclomatic, +1 cognitive
call_expression@call+1 ABC C-count
assignment_expression@assign+1 ABC A-count
compound_assignment_expr@assign+1 ABC A-count
let_declaration (with value)@assign+1 ABC A-count

Assertion patterns

PatternMatches
^assert$assert!(...)
^assert_eq$assert_eq!(...)
^assert_ne$assert_ne!(...)
^panic$panic!(...)

Quirks and limitations

  • && and || operators are not tracked — logical operators do not contribute to cyclomatic or cognitive complexity for Rust. This reflects Rust’s idiom of chaining results and options rather than using boolean guards.
  • Closures are not tracked as separate function scopes; their branches count toward the enclosing function’s complexity.
  • if let and while let are captured as if_expression and while_expression respectively, contributing one branch each.
  • Wildcard _ arms in match are counted as branches like any other arm.

Example output

src/engine.rs  cyclomatic=9  cognitive=11  function_lines=38  params=2
src/config.rs  cyclomatic=14  cognitive=22  function_lines=61  params=4
  ↳ parse_config()  cyclomatic=14  [threshold: 10]

TypeScript

Reference for which tree-sitter nodes pretender tracks for TypeScript files, how each contributes to pretender check metrics, and known measurement gaps. Consult this page when tuning thresholds or diagnosing unexpected scores.

Overview

Analyses .ts, .tsx, .mts, and .cts files using a tree-sitter grammar. Identical branch and call tracking to the JavaScript adapter; type annotations and generic type arguments are captured but carry no metric weight.

Tracked nodes

Tree-sitter nodeCaptureMetric impact
function_declaration@function.definitionDefines a metric scope
variable_declarator with arrow_function@function.definitionDefines a metric scope
method_definition@function.definitionDefines a metric scope
if_statement@branch.if+1 cyclomatic, +1 cognitive
switch_case@branch.if+1 cyclomatic, +1 cognitive
for_statement@branch.loop+1 cyclomatic, +1 cognitive
for_in_statement@branch.loop+1 cyclomatic, +1 cognitive
while_statement@branch.loop+1 cyclomatic, +1 cognitive
do_statement@branch.loop+1 cyclomatic, +1 cognitive
catch_clause@branch.catch+1 cyclomatic, +1 cognitive
ternary_expression@branch.ternary+1 cyclomatic, +1 cognitive
binary_expression "&&"@branch.logical.and+1 cyclomatic, +1 cognitive
binary_expression `““`
call_expression@call+1 ABC C-count
assignment_expression@assign+1 ABC A-count
augmented_assignment_expression@assign+1 ABC A-count
variable_declarator (with value)@assign+1 ABC A-count
type_annotation@type.annotationNone
type_arguments@type.genericNone

Assertion patterns

Same as JavaScript:

PatternMatches
^assert$assert(...)
^assert[A-Z]assertEqual(...), etc.
^expect$Jest / Vitest expect(...)
^it$it('...', ...) test blocks
^test$test('...', ...) blocks
^describe$describe('...', ...) blocks

Quirks and limitations

  • Type-only constructs (interface, type aliases, enum declarations) are not tracked as function scopes.
  • Type guards (if (x instanceof Y)) are counted as regular if_statement branches — no special handling.
  • ?? (nullish coalescing) and ?. (optional chaining) are not tracked as branches.
  • Decorators on classes and methods do not contribute to complexity.
  • TypeScript-specific syntax uses the same tree-sitter grammar revision as JavaScript (.tsx files are parsed as TSX).

Example output

src/api.ts  cyclomatic=6  cognitive=8  function_lines=31  params=2
src/components/Form.tsx  cyclomatic=4  cognitive=5  function_lines=22  params=1

CLI and Configuration

Purpose

Defines Pretender’s command-line interface, configuration file schema, output formats, role detection, and plugin manifest contracts.

Requirements

Requirement: Init Command

The system SHALL provide pretender init to create pretender.toml, optionally install a pre-commit hook, and optionally generate GitHub Actions configuration. The command SHALL support --non-interactive and --mode <mode>.

In the current MVP, hook installation writes a repository-wide fallback shim that runs pretender check . and documents that --staged --diff-only will replace it when diff filtering is implemented.

Scenario: Non-interactive creates config

  • WHEN pretender init --non-interactive is run in a repository without pretender.toml
  • THEN the system writes a default pretender.toml and exits with code 0

Scenario: Interactive init can install hook and workflow

  • WHEN pretender init is run interactively and the user opts into hook and CI setup
  • THEN the system writes pretender.toml, .git/hooks/pre-commit, and .github/workflows/pretender.yml

Requirement: Check Command

The system SHALL provide pretender check [paths...] as the implemented MVP scan command for explicit file and directory inputs. The command SHALL require at least one path, SHALL recursively scan directories, and SHALL support --format human|json|sarif, --output <path>, and --mode guidance|tiered|gate.

For the current MVP, --staged, --diff-only, and --diff-base <ref> are reserved CLI flags and SHALL exit with code 2 and a not yet implemented message when used.

Mode behavior in the current MVP is:

  • guidance: always exits 0
  • tiered: always exits 0 after annotating yellow/red findings
  • gate: exits non-zero when any file or unit threshold is violated

Every successful pretender check run SHALL persist the report cache used by pretender report, regardless of output format.

Scenario: Human check succeeds on clean file

  • WHEN pretender check path/to/file.py runs on a file with no threshold violations
  • THEN the command exits with code 0

Scenario: Guidance mode does not fail violating file

  • WHEN pretender check path/to/file.py --mode guidance runs on a file with threshold violations
  • THEN the command exits with code 0 after reporting the violations

Scenario: Reserved staged flag is rejected

  • WHEN pretender check path/to/file.py --staged is run
  • THEN the command exits with code 2 and reports that the feature is not yet implemented

Scenario: SARIF output is emitted

  • WHEN pretender check path/to/file.py --format sarif is run
  • THEN stdout contains valid SARIF 2.1.0 JSON for the analysed findings

Requirement: Complexity Command

The system SHALL provide pretender complexity <path> for the current MVP. The command SHALL accept exactly one path and SHALL report cyclomatic complexity per discovered code unit, sorted worst-first.

The current MVP does NOT implement --top <n> or --threshold <n>.

Scenario: Complexity reports worst units first

  • WHEN multiple code units have different cyclomatic scores
  • THEN pretender complexity <path> lists the highest scoring units before lower scoring units

Requirement: Report Command

The system SHALL provide pretender report to render cached results from the last successful pretender check. The command SHALL support --format human|markdown|html and --output <path>.

Scenario: Report reads cached human check results

  • WHEN pretender report --format markdown is run after a successful human-format pretender check
  • THEN the command renders a Markdown report from the cached results

Requirement: Hooks Commands

The system SHALL provide pretender hooks install to install a native pre-commit shim and pretender hooks uninstall to remove a shim previously installed by Pretender.

In the current MVP, the installed shim SHALL run pretender check . and include a comment that diff-only filtering is deferred to future work.

Scenario: Hook install writes fallback shim

  • WHEN pretender hooks install is run
  • THEN .git/hooks/pre-commit contains a Pretender-managed shim that executes pretender check .

Scenario: Hook uninstall removes Pretender shim

  • WHEN pretender hooks uninstall is run after Pretender installed the hook
  • THEN the Pretender-managed .git/hooks/pre-commit file is removed

Requirement: CI Generate Command

The system SHALL provide pretender ci generate github in the current MVP. The generated workflow SHALL install Pretender, run pretender check . --format=sarif --output=pretender.sarif, upload SARIF, append a Markdown report on failure, and fail the job when findings are present.

Providers other than github remain reserved and SHALL exit with code 2 and a not yet implemented message.

Scenario: GitHub workflow includes SARIF upload

  • WHEN pretender ci generate github is run
  • THEN the generated workflow uploads Pretender SARIF through GitHub Code Scanning

Requirement: Configuration Schema

The system SHALL read pretender.toml with tables for [pretender], [thresholds], role-specific threshold tables such as [thresholds.test], [bands], [scope], [execute], [plugins], [output], and [roles]. The mode value SHALL be one of guidance, tiered, or gate. The implicit default role SHALL be app. Unknown config keys SHALL be ignored.

Scenario: Role-specific thresholds override app defaults

  • WHEN a file is assigned role test
  • THEN values under [thresholds.test] override app threshold defaults for that file

Requirement: Role Detection

The system SHALL assign each file a role from app, library, test, script, generated, or vendor using the current MVP resolution order:

  1. explicit pragma found in the first 8 lines
  2. configured [roles] path globs, with the most specific matching glob winning
  3. built-in path heuristics
  4. default app

The current pragma scanner SHALL accept line comments beginning with # or //, and block-comment openings beginning with /*.

Scenario: Pragma wins over path glob

  • WHEN a file declares an explicit Pretender role pragma and also matches a configured role glob
  • THEN the pragma role is assigned

Scenario: Most specific glob wins

  • WHEN a file matches both tests/** and tests/manual/**
  • THEN the more specific glob is assigned

Scenario: Heuristic role is used when no pragma or glob applies

  • WHEN a file path contains /vendor/ and no pragma or configured glob applies
  • THEN the file is assigned role vendor

Requirement: Schema Versioning

The system SHALL not require a config version key. Unknown config keys SHALL be ignored for forward compatibility.

Scenario: Unknown key is ignored

  • WHEN pretender.toml contains an unknown key
  • THEN parsing succeeds and the unknown key has no effect

Requirement: Output Formats

The current MVP SHALL emit human, json, and sarif output for pretender check, and human, markdown, and html output for pretender report. The junit and markdown formats for check are not implemented and are not accepted by the CLI (tracked under pretender-t2m).

Scenario: JSON report is emitted

  • WHEN pretender check path/to/file.py --format json is run
  • THEN stdout contains valid JSON describing file reports and unit metrics

Scenario: SARIF validates

  • WHEN Pretender emits SARIF output from pretender check
  • THEN the output conforms to the SARIF 2.1.0 schema

Requirement: Plugin Manifests

The system SHALL parse data-only plugin manifests made of .scm query files and plugin.toml metadata. In the current MVP, manifest parsing is implemented as a library capability and test fixture, but plugin installation, discovery, and runtime loading are not yet implemented by the CLI.

Scenario: Python plugin manifest parses

  • WHEN the built-in Python plugin.toml is loaded by the manifest parser
  • THEN the manifest is parsed successfully and exposes its query and assertion metadata

Requirement: Reserved Commands

The current MVP CLI SHALL expose the following reserved commands, which currently exit with code 2 and a not yet implemented message: duplication, mutation, plugins, and explain.

Scenario: Reserved command reports not implemented

  • WHEN pretender explain cyclomatic is run
  • THEN the command exits with code 2 and reports that the feature is not yet implemented

Requirement: Language Support

The current MVP SHALL register built-in parsers for C, C++, Go, Java, JavaScript, Python, Ruby, Rust, and TypeScript-family source files. Files with unsupported extensions SHALL be skipped by pretender check, and pretender complexity <path> on an unsupported extension SHALL fail.

Scenario: Rust file is analysed

  • WHEN pretender check example.rs is run
  • THEN Pretender analyses the file with the Rust parser

Scenario: Unsupported complexity input fails

  • WHEN pretender complexity example.txt is run
  • THEN the command exits non-zero because no parser is registered for .txt

cli-core Specification

Purpose

TBD - created by archiving change upgrade-genesis. Update Purpose after archive.

Requirements

Requirement: CLI scaffold uses Guide

The tool’s main.rs CLI setup SHALL use genesis::guide::Guide::builder() when the tool adopts genesis::guide. Adoption is determined by the tool’s maintainer — this spec defines the contract for how to do it.

Scenario: Guide builder replaces ad-hoc setup

  • GIVEN the tool adopts genesis::guide
  • WHEN main.rs is updated to use Guide::builder(...)
  • THEN command handlers SHOULD return Output<T> or use ErrorSink
  • AND cargo test SHALL pass

Scenario: ErrorSink for self-healing errors

  • GIVEN the tool adopts genesis::guide
  • WHEN a command returns an error
  • THEN ErrorSink SHOULD print the error with a suggestion footer
  • AND it SHOULD write to the error scratch (for --from-last-error)

config Specification

Purpose

TBD - created by archiving change upgrade-genesis. Update Purpose after archive.

Requirements

Requirement: Shared config management

The tool SHALL adopt genesis::config for shared config management.

Scenario: config struct implements ConfigFile

  • GIVEN the tool needs config management
  • WHEN the tool adopts genesis::config
  • THEN the tool’s config SHALL implement genesis::config::ConfigFile
  • AND all config file I/O (read, write, parse) SHALL delegate to genesis
  • AND cargo test SHALL pass

Scenario: config registered at startup

  • GIVEN the tool starts up
  • WHEN it initializes
  • THEN it SHALL register its config struct with ConfigRegistry
  • AND it SHOULD use ConfigStore for config discovery and validation

Scenario: dead config code removed

  • GIVEN the tool has adopted genesis::config
  • WHEN the old config parsing code is removed
  • THEN cargo clippy SHALL introduce no new warnings
  • AND cargo test SHALL pass

Universal Code Model

Purpose

Defines the language-neutral model that all Pretender metrics operate on, plus the tree-sitter query contract, model invariants, metric formulas, and compatibility rules.

Requirements

Requirement: Supporting Types

The system SHALL represent source spans, parameters, call sites, and languages in a language-neutral form. Span.lines() SHALL be inclusive and SHALL require end_line >= start_line. CallSite SHALL include a callee name, span, and default ABC smell weight of 1.0.

Scenario: Span line count is inclusive

  • WHEN a span has start_line = 3 and end_line = 5
  • THEN Span.lines() returns 3

Requirement: Module Model

The system SHALL represent each successfully parsed source file as one Module containing path, language, whole-file span, total/code/comment line counts, code units, and imports. In the current MVP, imports are emitted as an empty list by the built-in query engine adapters.

Scenario: One file maps to one module

  • WHEN a supported source file is parsed successfully
  • THEN the engine emits one Module for that file

Requirement: Code Unit Model

The system SHALL represent each function, method, lambda with block body, constructor, or initializer as a CodeUnit with name, kind, span, parameters, body block, and exported flag.

Scenario: Function maps to code unit

  • WHEN a language adapter captures a function definition
  • THEN the universal model contains a CodeUnit for that function

Requirement: Block and Node Model

The system SHALL represent nested scopes as Block values with zero-based nesting depth from the enclosing CodeUnit body root. Node SHALL represent statements, branches, nested blocks, and calls.

Scenario: Root body nesting is zero

  • WHEN a CodeUnit body is created
  • THEN its root block has nesting = 0

Requirement: Branch Model

The system SHALL represent control-flow points as Branch values with kind, span, nesting depth at capture time, and optional logical sequence ID. Branch kinds SHALL include if, else-if, switch case, loop, catch, ternary, logical-and, and logical-or.

Scenario: Logical sequence IDs are scoped to code unit

  • WHEN logical operators are captured in two different code units
  • THEN their sequence IDs are unique only within each enclosing code unit

Requirement: Tree-Sitter Query Contract

Each implemented language adapter SHALL provide a .scm query file whose captures populate the universal model. The current MVP query engine requires captures for @function.definition, @function.name, @function.parameters, and @function.body; it also consumes optional branch, @call, @call.callee, @assign, and @assert.* captures.

Assertion captures SHALL contribute to the per-code-unit assertion count used by role-specific threshold checks.

Scenario: Function captures populate code unit

  • WHEN a query captures @function.definition, @function.name, @function.parameters, and @function.body
  • THEN the adapter creates the corresponding CodeUnit

Requirement: Model Invariants

The system SHALL preserve these invariants: code-unit body nesting starts at 0; nested blocks increment nesting by 1; branches carry nesting depth at capture time; files with tree-sitter parse errors emit a warning diagnostic and return an otherwise-empty Module; identical logical operator sequences at the same nesting level count once for cognitive complexity.

In the current MVP query-engine-backed adapters, code units whose names begin with _ are treated as not exported and other named units are treated as exported.

Scenario: Parse error returns empty module with warning

  • WHEN tree-sitter reports a parse error for a source file
  • THEN the system emits a warning diagnostic and returns a Module with no CodeUnit values

Requirement: Metric Functions

The system SHALL compute metrics as pure functions over the universal model. Cyclomatic complexity SHALL be 1 + branch count. Cognitive complexity SHALL sum branch cognitive weight multiplied by 1 + nesting_at. Function lines SHALL use inclusive span lines. Parameter count SHALL be parameter length. Maximum nesting SHALL be the maximum block nesting value. ABC SHALL compute sqrt(A^2 + B^2 + C^2) using counted assignments, weighted branches, and weighted calls.

In the current MVP, parsed CallSite values are assigned a default smell_weight of 1.0 by the parser, with smell-specific weighting applied later during metric evaluation.

Scenario: Cyclomatic adds one to branches

  • WHEN a code unit has 4 branch nodes
  • THEN cyclomatic complexity is 5

Requirement: Current Adapter Surface

The current MVP SHALL register built-in adapters for C, C++, Go, Java, JavaScript, Python, Ruby, Rust, and TypeScript-family source files.

Scenario: TypeScript adapter is available

  • WHEN a .ts file is analysed
  • THEN the parser registry resolves the TypeScript adapter for that file extension