Scan a repository
CUPID

Paste a git repository URL, or an absolute path to a checkout already on this machine. URLs are cloned read-only; local folders are analyzed in place (no clone, git history included). The code is never executed. Private repositories work with a read-only access token below.

used for this scan only · sent read-only with the clone · never logged
comparison baseline for this repository is kept in this browser · download ·
try sqlite-utilsclick
Scan settings · thresholds & signals (saved in your browser)

These thresholds decide when a function is flagged. The numbers below come from measuring 42,021 functions across 15 major Python projects (listed at right), lined up from simplest to most complex. A percentile pN is the complexity value at position N% in that line-up, so a higher percentile means a more complex function, not a better one: p99 is the messy 1% at the far end, not a mark of quality. Two of the columns are inverse views: median/p90/p95/p99 ask ‘what value sits at this rank?’, while default’s rank asks ‘what rank does CUPID’s default sit at?’

Reading a row (cyclomatic): a typical function (median) scores 2; 90% score 6 or less; 95% score 9 or less. CUPID’s default of 10 is higher than 96% of real functions; that is its rank, the 96th percentile, so it flags only the most complex ~4%. Only the extreme p99 (20) sits above the default, i.e. the rare outliers worth catching. Raise a number to flag less, lower it to flag more, or uncheck a signal to skip it.

CheckDefaultSource / standarddefault's rankmedianp90p95p99
cyclomatic complexity10McCabe 1976: ≤10 = moderate risk96th26920
cognitive complexity15Campbell / SonarSource 2018: default 1596.2th071334
nesting depth4structured-programming convention: ≤499.2th0234
function length (lines)60review convention: fits on a screen96.9th73247101
parameter count5Fowler, Refactoring: data clump97.7th1348
magic numbers8maintainability heuristic: ≤8 literals99.6th0015

Are these thresholds strict or lenient? Strictness is about where the line is drawn in the crowd: a strict bar is a low number that flags ordinary code; a lenient bar is a high number that catches only the worst. Every default here sits above the 90th–95th percentile of real code (cyclomatic 10 > p95 of 9; cognitive 15 > p95 of 13), so the line is drawn at the far-right edge of the distribution: deliberately lenient, catching only outliers. That a high percentile exceeds the default (cyclomatic p99 = 20 > 10) does not make it strict; it just means a rare 1% is messier than the threshold; precisely the code worth flagging.

So the literature thresholds are not stricter than industry practice; they sit above almost all of it. Each default is a risk ceiling from the literature (McCabe’s cyclomatic 10 = ‘moderate risk’, SonarSource’s cognitive 15, review conventions for the rest), not a target. Well-regarded code naturally lands far below it (median cyclomatic 2, median nesting 0), so a default fires on only the most complex few percent; for nesting, the top ~0.8%. They are not ‘too low’; lowering one toward the corpus median would start flagging ordinary, healthy code.

What these metrics measure & where the thresholds come from
cyclomatic complexity (default 10)

Cyclomatic complexity (Thomas McCabe, A Complexity Measure, IEEE TSE 1976) counts the independent execution paths through a function: start at 1 and add 1 for every branch point (if, for, while, and/or, case). It approximates the minimum number of tests needed to cover the function, so a high value means more branches, harder testing, and more places for bugs to hide. McCabe proposed 10 as the 'moderate risk' boundary above which a function should be split; still in wide use, and CUPID's default.

cognitive complexity (default 15)

Cognitive complexity (G. Ann Campbell, SonarSource white paper, 2018) measures how hard a function is for a person to read, not how hard it is to test. Unlike cyclomatic it penalises nesting (each level deep costs more) and flow-breaking structure (break, continue, nested conditionals), so ten flat ifs score far lower than three deeply nested ones. SonarQube ships it as rule S3776 with a default limit of 15, which CUPID adopts.

nesting depth (default 4)

Nesting depth is how many control structures are stacked inside one another (an if inside a for inside a while = depth 3). Deep nesting is the classic target of the guard-clause / early-return style and traces back to structured-programming practice (Dijkstra, Go To Statement Considered Harmful, 1968). There is no single threshold paper; 4 is a long-standing review convention, and the fix is to flatten with early returns rather than indent further.

function length (lines) (default 60)

Function length is simply the line count. The guidance 'a function should do one thing and fit on a screen' is a review convention popularised by Martin Fowler's Refactoring (Extract Function) and Robert C. Martin's Clean Code, not a number from a paper. CUPID's 60 is a screen-sized heuristic, and it only raises severity when the function is also cognitively complex; a long but flat data table is not punished.

parameter count (default 5)

Parameter count flags the 'long parameter list' / 'data clump' code smell from Fowler's Refactoring: when many arguments travel together they usually want to be grouped into an object. It is a design heuristic, not a measured threshold; 5 is the common ceiling CUPID uses.

magic numbers (default 8)

Magic numbers are un-named numeric literals embedded in code (e.g. `if retries > 7`), which hide intent and drift out of sync. Naming them as constants is a maintainability convention found in most style guides; there is no canonical paper or number, so CUPID flags a function only once it carries more than 8 such literals: a prescription, not a firehose.

Reference corpus · 15 projects, by author
  • Guido van Rossum / Python corepython/cpython
  • David Beazleydabeaz/curiodabeaz/ply
  • Simon Willisonsimonw/datasettesimonw/sqlite-utilssimonw/llm
  • Will McGugan / TextualizeTextualize/richTextualize/textual
  • Tom Christie / Encodeencode/httpxencode/django-rest-framework
  • Hynek Schlawackpython-attrs/attrshynek/structlog
  • Łukasz Langa / PSFpsf/black
  • pytest / Holger Krekel lineagepytest-dev/pytest
  • Django Software Foundationdjango/django
Chosen for distinct engineering traditions with long release history, not star count. See docs/python-gold-repos.md.
Signals to run
Complexity
industry: p95=9 · p99=20 · default 10 = top 4% most complex
industry: p95=13 · p99=34 · default 15 = top 3.8% most complex
industry: p95=3 · p99=4 · default 4 = top 0.8% most complex
industry: p95=47 · p99=101 · default 60 = top 3.1% most complex
industry: p95=4 · p99=8 · default 5 = top 2.3% most complex
industry: p95=1 · p99=5 · default 8 = top 0.4% most complex
Performance
Correctness & safety
Style & docs
Architecture graph thresholds
How many repo-local modules may depend on one module before it is treated as a stable API hotspot. Default: 20. Raise it for large monorepos; lower it for small libraries.
How many repo-local dependencies one module may have before it is treated as a composition hotspot. Default: 20. Raise it for large monorepos; lower it for small libraries.
Fan-in plus fan-out required before a mid-instability module is flagged as a central, change-prone hub. Default: 30. Raise it for large monorepos; lower it for small libraries.
Caller count required before a function can be flagged as a function-level hub. Default: 8. Raise it for large monorepos; lower it for small libraries.
Callee count required before a function can be flagged as a function-level hub. Default: 8. Raise it for large monorepos; lower it for small libraries.
Calibration · Engine B · Pillars 2 & 3

Percentile calibration × historical evidence

Norms: every threshold knows where it stands

  • The corpus is chosen by engineering tradition, not star count: CPython, Django, pytest, rich / textual, httpx / DRF, attrs, black… 42,021 functions in total.
  • The industry defaults are already strict: cyclomatic 10 = the corpus's 96th percentile; nesting 4 = the 99.2nd.
  • The key design: a percentile profile only raises the bar, never lowers it (max(default, profile)). The profile's job is to attach percentile evidence to every finding, not to move thresholds.
  • The shipped asset downsamples each distribution to 1,024 points per metric: the percentile shape survives and the file stays tiny.
The shape of evidence: grew ➞ fixed ➞ stayed one function's cognitive complexity across releases threshold 15 (SonarSource S3776) grew: over the limit for ≥2 releases fixed: a confirmed refactor crosses back stayed: held after
The whole arc is confirmed by algorithm: offline, deterministic, zero LLM. The mirror query build_degraded() finds the long-term debt that crossed the threshold and was never fixed.

The effect: CUPID can tell a customer that datasette fixed this exact class of problem in a specific commit, the same way, and the fix held. The recommendation upgrades from “we think” to “history shows”.

Large repositories can take a minute; the history pipeline walks every release (or monthly snapshot). The scan runs on the server, so you can browse the other pages while it works; come back here to reattach to the live progress. How the analysis works →
Scanning…
The scan keeps running if you browse other pages; this panel reattaches when you return. The report opens when the scan completes.