Skip to content

Concepts

Every lens, table and finding is phrased in terms of five ideas.

Source unit

A source unit is the slice of the source tree Tepyd analyses as a whole (the row in every table, the thing that has or lacks tests). Units come from the units glob patterns, matched against src_root, first-match-wins.

There are two kinds, and the pattern decides which:

Pattern Yields Unit name Where its tests live
"*", "modules/*" package units (directories only) the path, e.g. modules/biz the mirrored directory, tests/<tier>/modules/biz/
"*.py", "core/*.py" module units (files only) the stem, e.g. core/config test_config.py, both in the mirrored directory tests/<tier>/core/ and flat at the tier root

A pattern whose last segment ends in .py yields modules; anything else yields packages. So units = ["*"] stays directories-only and units = ["*.py"] makes each top-level module a unit: the right choice for a flat package with no sub-packages.

Both kinds can be mixed. A layout with sub-packages and top-level modules wants units = ["*", "*.py"], or, to measure at module granularity throughout, something like ["core/*.py", "lenses/*.py", "*.py"].

Two rules keep discovery predictable:

  • Anything whose path contains a part starting with _ or . is skipped: __init__.py, __pycache__, _private/, .hidden/. Module patterns additionally skip test_*.py, *_test.py, conftest.py and setup.py.
  • A container whose children were already claimed is suppressed, so modules/ itself never doubles as a unit once modules/* exploded it.

Tier

A tier is one rung of the pyramid: a directory of tests of a given cost, e.g. tests/a_unit. Tiers are listed cheapest-first in config. The order matters throughout: the first tier is the unit tier, the last is the expensive cap. You can declare any number of tiers, rooted anywhere, including outside tests/.

Unit share

Unit share is the fraction of a unit's test code that lives in the cheapest tier. It is the headline pyramid-health metric, the one a tier's target_share gates.

Shape glyph

The shape glyph is a one-character read on a unit's pyramid, shown in the last column of shape:

Glyph Meaning
▲ healthy: the cheapest tier is the largest and holds ≥ 40 % of test LOC
◇ balanced: neither clearly healthy nor inverted
▼ inverted: the most expensive tier is the largest and unit share < 30 %
· no tests at all

Layer awareness

By default every unit is judged against the same ideal: a broad unit base. That is right for a flat app and wrong for a layered one. In a hexagonal or onion architecture each layer has a natural tier: pure domain is unit-tested, the persistence layer integration-tested, the HTTP edge end-to-end-tested. Judging all three against the unit ideal turns real structure into a wall of findings that are correct by design.

Tell Tepyd which units each tier owns with expects, a list of globs over unit names:

[[tool.tepyd.tiers]]
name = "a_unit"
expects = ["domain", "domain/*", "services", "lib"]   # pure logic

[[tool.tepyd.tiers]]
name = "b_integration"
expects = ["repositories", "infrastructure"]          # the DB-bound layer

[[tool.tepyd.tiers]]
name = "c_e2e"
expects = ["web", "web/*"]                            # the HTTP edge

That one declaration changes three lenses:

  • gaps reports a unit outside a tier's scope as out of scope (n/a), and drops it from that tier's X/Y mirrored figure.
  • shape judges each unit against its expected home: a controller whose home is e2e is ▲ when its tests live at e2e. ▼ marks only test mass sitting above where it belongs. The codebase-wide target_share check likewise sets aside each unit's tests at its expected higher tiers before measuring.
  • report inherits both, so its findings fire only on genuine problems.

Two things expects does not do:

  • It is not an exclusion. A unit with no behaviour to test anywhere, such as a pure Protocol/ports layer, belongs in exclude, which drops it from every tier.
  • It never hides tests you wrote. Existing tests always count as present, even at a tier that did not expect them. Scope governs one thing only: whether an absence is a problem.

With no expects anywhere, every lens falls back to the classic, layer-blind behaviour.