Changelog¶
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[0.7.2] - 2026-08-28¶
Changed¶
- Runs are faster again — 12.4s to 1.9s on an 800-file repository. A symbol metric parses a file only when its text contains the symbol's last part, since no counted occurrence exists without it; a regex metric computes line offsets only for files its pattern matched; and each file is read and decoded once per run rather than once per metric. Nothing reported changes, except that a file both unparsable and free of the symbol is now skipped without a warning — it contributes zero either way.
[0.7.1] - 2026-08-23¶
Added¶
- Templates for the suppressions every project was still writing by hand:
coverage.pragma_comment,pytest.skip_marks,pytest.xfail_marks,mypy.strictness_holesandpython.cast_used.mypy.strictness_holesreads the ini spellings too, sincemypy.iniandsetup.cfgwrite= Falseand only TOML has a formatter settling its spacing. - The ruff pack covers the rest of ruff's suppression comments, split by how
much each one covers:
ruff.ignore_commentfor# ruff: ignore[...]over a line or a statement,ruff.suppressed_rangesfor the# ruff: disable[...]that opens a block, andruff.file_exemptionsfor# ruff: noqaand# ruff: file-ignore[...]. A whole-file exemption is not the admission a single# noqais, so it is not the same number.
Changed¶
python.todo_commentscountsXXXandHACKalongsideTODOandFIXME;ruff.noqa_commentandruff.noqa_spreadmatch a# noqawith no rule code, which silences every rule on the line and was going uncounted. A repository holding either sees the number rise on upgrade.
[0.7.0] - 2026-08-21¶
Added¶
- The interactive TUI measures with the screen already up, rather than leaving the terminal blank until the table is ready. A progress bar counts files while it walks the tree — where there is no total to be a proportion of — then metrics as it measures them, naming the one it is on. Under it, a line of Sims-flavoured nonsense about code quality, turning over on a timer and whenever the phase changes. A run that finishes quickly never draws it, since a screen that flashes up and vanishes is worse than a beat of stillness. Quitting part-way stops the run instead of waiting it out.
Changed¶
- Runs are around four times faster on a project carrying a virtualenv:
tingle staton this repo went from ~3.6s to ~0.85s, and nothing it reports changed. The walk now skips the always-excluded directories instead of descending into them and then rediscovering, once per metric, that they match nothing — 26,581 files walked here to measure 102. It reads directory entries directly rather than throughPath.rglob, which builds a path object per entry and stats every one. And each distinct set of ranges resolves once per run rather than once per metric, where before the metrics sharing a range — most of them — each rescanned the tree to arrive at the same answer. A.venvnested inside a package is not the project's own and is still measured, exactly as before.
[0.6.0] - 2026-08-20¶
Added¶
- A metric can build on a template instead of stating everything itself:
base = "tingle.builtins.ruff.noqa_comment"is a whole metric — type, pattern, name, group, description — and any key the entry states wins over it.tingle librarylists what is on offer,tingle library --expandprints each one as the config it stands for, andtingle add --base <path>writes the two-line entry. - Templates for the tools most projects already run:
black,codespell,import_linter,mypy,pylint,python,ruff,taplo,unittest_mock. extra_<param>extends a template's list rather than replacing it, soextra_ignore_lineskeeps whatever the template already excused and adds to it. The plain param still replaces.[templates.<name>]declares a template in the config file itself. One with no type is a mixin — a sharedignore_linesset or pair of ranges — usable by metrics of different types, and it may build on an imported template in turn.- Templates are ordinary Python: a package of
MetricTemplateinstances at an import path, needing tingle and nothing else, so a team can publish its own and name it the same way. tingle's own pack is reached by that loader, not from the inside. A pack may nest as deeply as it likes, and where a module declares__all__that is what it publishes. - A
toml_list_lengthkey that carries on past an array of tables means it once per entry:tool.importlinter.contracts.ignore_importscounts the excused imports across every contract.
Changed¶
tingle.tomlmeasures itself through the template library, which changes nothing it counts.any-usesnow sits undertypingrather than ungrouped, since a template carries a group and there is no way to take one back.tingle librarylists the templates a pack offers that verify, and reports the rest, rather than refusing to list anything when one is broken.
[0.5.0] - 2026-08-16¶
Added¶
--group NAMEnarrowstingle,stat,checkandreportto the metrics under one group heading, the way--metric NAMEnarrows them to one metric. Both repeat and read as a union, so--group linting --metric locmeasures a group plus one metric from outside it; a name no metric or group carries is a usage error.tingle listnames each metric's group, so the values--grouptakes can be read off the CLI instead of out of the config file.- The interactive TUI is a sortable table: group headers, metrics and their
located hits are rows in one outline instead of a three-level accordion.
Sort by group, name, type, value or score —
gntvcascending,GNTVCdescending. Sorts stack, sonthentgives type-major order with names ordered inside each type, and asking for a stacked key the other way up turns it over in place. The column deciding the order carries ▲ or ▼, and a line under the table names the whole stack. Sorting by anything butgroupdrops the group headers and names each metric's group on its own row instead;0clears the stack and brings them back. valueandscoreare separate sorts: the raw count, and the same number against the metric's own guide. Onlyscorecompares metrics whose guides differ.- Search the table with
/. Case-sensitive substring, matched against a metric's name, its group's, its description and its range names — and against the path of every occurrence, whether or not it is on screen, so a fully folded tree is still searchable. A metric found by name is left as you had it; found by description or range, it opens on those words; found through its files, it opens showing only the files that matched. Enter keeps the query and hands the rows back, Esc restores the outline untouched. - A metric's description, the ranges it measures over and the error it failed with are rows under it, shown when it is unfolded.
- A GitHub Actions composite action,
fancysnake/tingle/actions/metrics-history, that recordsstat --jsonon a branch and publishes its history as one chart per metric group — a line per metric, on a logarithmic axis that still has room for a count of 0. Its other half,.../metrics-history/publish, fetches that chart into a site being built, for repositories whose Pages source is already a build artifact and so cannot serve the data branch. Both are documented under History; tingle's own history is published this way, at https://tingle.fancysnake.dev/history/chart/.
Changed¶
- A
--metricor--groupname the config does not carry is reported asusage error:rather thanconfig error:. The file is valid; the typo is on the command line. - TUI: folding a metric now hides its description, which the accordion kept visible at rest.
- A file is called binary on a NUL in its first 8,000 bytes, the window git's own differ uses; it was 8,192.
- An untracked file that is not UTF-8 now counts as fully added, the way git would diff it. Whether a metric may read it is still decided when the metric runs.
Fixed¶
- TUI: the ▲/▼ on the sorted column was cut off whenever that column's values were narrower than its heading.
- TUI: opening a hit no longer blocks the table while the editor is being handed the file, and an editor that will not open it says so in a notification instead of ending the session.
Removed¶
Occurrence.sort_key, which ordered hits by path, then line, then note. Nothing asked for that order.
[0.4.1] - 2026-08-09¶
Added¶
regex_spreadandsymbol_spread: the searchesregex_countandsymbol_usesrun, counting the files a thing appears in rather than the number of times it is written. A file with forty matches counts once.- In a diff the spread types compare whole-file presence against the merge-base rather than counting touched lines, so rewriting a file that already matched nets zero while one new file that matches is +1 — the metric to gate on for containment rather than removal, since it does not fail every bug fix to legacy code. They match full text on both sides, so they carry no multi-line caveat in diff mode.
Changed¶
symbol_usesside-scoped diff warnings now reada.py: base side: skipped (syntax error: ...), with the colon every other side warning already used.
[0.4.0] - 2026-07-16¶
Added¶
- Severity emoji (🎉 🦠 🚧 🚨 🔥 💀) on every value, ranking it against a
guide on a logarithmic ladder. With no guide set, one is derived from
the size of the codebase (one unit per 100 lines), so debt is read as a
density. Pin one with
[display] guide, or per metric withguide;[display] loc_rangenames the range those lines are counted over. - Group headers show the sum of their metrics, ranked against the summed guides. In the TUI a group summing to zero starts folded, unless it holds an errored metric.
- The summary tables read as an outline — a group name heads its indented
metrics, blocks ruled apart — replacing the
Groupcolumn. Every value's emoji is aligned into one column, the numbers space-padded beneath it. ignore_linesonregex_countandsymbol_uses: regexes matched against the line a hit sits on, excusing hits that are not debt —ANYin an assertion counts,"form": ANYdoes not.over_linesonfile_count: counts only files longer than the gate. In a diff, a file growing past the gate is new debt, one refactored back under it is debt paid off.descriptionon any metric, shown intingle report, the JSON and the TUI.tingle add --descriptionwrites one.tingle checkprints a line when it passes, instead of exiting silently.- In the TUI, Space or Enter on an occurrence opens it in VS Code — the file at its line, in the window you are already in. Works from VS Code's integrated terminal; elsewhere the key says there is no editor.
Fixed¶
- TUI: clicking empty space no longer moves focus off the metric rows, leaving the arrow keys scrolling instead of navigating.
[0.1.0] - 2026-07-12¶
Initial release: tingle.toml (or [tool.tingle] in pyproject.toml),
named ranges of include/exclude globs, metric groups, and the three commands.
Added¶
tingle stat— the summary table.tingle report— every occurrence, file and line, plus--coberturafor CI consumers that read coverage XML (GitLab, Jenkins, diff-cover). Both take--json,--diff,--base,--configand--metric. Baretingleon a terminal opens the same results as a foldable accordion; a non-TTY gets the static summary.tingle check— the CI gate: it measures the branch likestat --diff, then exits 1 if the metrics worsened, printing only the lines the branch added under the metrics that grew.[check] policy = "sum"(default) fails when the metrics grow in total,"any"when a single metric does;ignore = [...]names metrics that may grow;--policyoverrides for one run.--diff [--base REF]— the impact of the current branch against the merge-base with a base branch, diff-cover style. Line-scoped metrics count occurrences on added vs removed lines, value metrics report the delta between the merge-base and now. The base resolves as--base>[diff] base>main, with anorigin/<base>fallback. Uncommitted changes count; untracked files count as fully added.- Metric types:
regex_count(regex matches),symbol_uses(AST references to a bare or dotted Python symbol),toml_list_length,toml_table_array(entries of an array of tables, labelled by a configurable field;explode = truefans a list-valued label out into one count per element),ini_list_length,file_count,line_count. - Every metric reports where its hits are, and diff results carry signed
added/removed occurrences, so the list metrics show which entries
changed.
report --jsonincludes them and the per-filedetails;stat --jsonstays values-only, like its table. - Config authoring:
tingle add TYPE [VALUE](validate-before-write, auto-generated names, repeatable--range,--param key=value,--group),tingle init,tingle list/tingle list --types. - Reports on stdout, warnings and per-metric errors on stderr; exit codes: 0
ran, 1 metric failure, 2 config/usage error. Metric values never affect the
exit code,
checkbeing the one command whose exit code reflects the measurements, and one broken metric does not stop the run. - Python 3.11 through 3.14; the test matrix runs on all four.
- GLIMPSE architecture (pacts/specs/mills/links/gates/inits) enforced with import-linter; strict mypy.