lnds/kimun
lnds/kimun has 27 GitHub stars and 8 forks. GitHot measured 0 stars in 24 hours. Last checked 2026-11-14 10:33 UTC. Last push 2026-11-04.
What you can do
- Respects .gitignore rules automatically
- Deduplicates files by content hash (identical files counted once)
- Detects languages by file extension, filename, or shebang line
- Supports nested block comments (Rust, Haskell, OCaml, etc.)
Measured star growth
- in 24h
- 0
- in 7 days
- Collecting history
- in 15 days
- Collecting history
- in 1 month
- Collecting history
Net star changes over overlapping periods, not daily totals. Missing measurements are left blank. Stars measure attention, not software quality.
About
Code health from the terminal: a grade for your project, complexity, duplication, git hotspots, ownership and the impact of a change. A CLI written in Rust.
Developer ToolsREADME.md
OriginalKimün (km)
A fast command-line tool for code analysis, written in Rust. Run km score on any project to get an overall health grade (A++ to F--) across five quality dimensions — cognitive complexity, duplication, indentation depth, Halstead effort, and file size — with a list of the files that need the most attention.
$ km score
Code Health Score
──────────────────────────────────────────────────────────────────
Project Score: A (91.2)
Files Analyzed: 142
Total LOC: 19,084
──────────────────────────────────────────────────────────────────
Dimension Weight Score Grade
──────────────────────────────────────────────────────────────────
Cognitive Complexity 32% 84.1 B+
Duplication 21% 100.0 A++
Indentation Complexity 16% 93.2 A+
Halstead Effort 21% 87.5 A-
File Size 10% 100.0 A++
──────────────────────────────────────────────────────────────────
Files Needing Attention (worst per-file scores, excl. duplication)
──────────────────────────────────────────────────────────────────
Score Grade File Issues
──────────────────────────────────────────────────────────────────
57.7 D- ./src/deps/analyzer.rs Cognitive: 36
61.0 D ./src/cogcom/analyzer.rs Cognitive: 35
70.3 C ./src/smells/rules.rs Cognitive: 20
71.6 C ./src/hal/tokenizer.rs Cognitive: 22
71.8 C ./src/loc/mod.rs Cognitive: 19
72.8 C ./src/knowledge/mod.rs Cognitive: 21
73.8 C+ ./src/dups/mod.rs Cognitive: 19
75.0 C+ ./src/cli_help.rs Cognitive: 17
75.6 C+ ./src/test_blocks.rs Cognitive: 17
77.3 B- ./src/walk.rs Cognitive: 16
──────────────────────────────────────────────────────────────────
That is Kimün measuring its own source: 140 files and 18,963 lines of code, tests left out, in 0.11 seconds on an Apple M3 Max. Counting all 235 files of src/, tests included, takes km loc 0.04 seconds and cloc 2.4.
Website: kimun.tools · A two-minute video of the grade, the metrics, what git remembers, the impact of a change and the gate for CI · Also in Spanish.
Beyond the aggregate score, Kimün provides 19 specialized commands:
- Static metrics — lines of code by language (classified as cloc classifies them), duplicate detection (Rule of Three), Halstead complexity, cyclomatic complexity, cognitive complexity (SonarSource), indentation complexity, two Maintainability Index variants (Visual Studio and verifysoft), code smell detection, dependencies between files and their cycles, a comprehensive multi-metric report, and a map of the code as nested circles coloured by any of them.
- Git-based analysis — hotspot detection (change frequency × complexity, Tornhill method), code churn (pure change frequency), code ownership / knowledge maps via
git blame, temporal coupling between files that change together, impact of a diff (diffusion and missing co-changes), per-author ownership summary, and file age classification (Active / Stale / Frozen). - For coding agents — every command that measures can answer in JSON, and
km aiis an optional integration with Claude that runs the tools and writes a narrative report. No other command calls a model.
Note: This repository is not the same as the Rust console note-taking app
kimunby nico2sh. That project is available at https://github.com/nico2sh/kimun.
Installation
cargo install kimun # from crates.io
brew install lnds/kimun/kimun # with Homebrew, on macOS and Linux
Either one installs the km binary. There are also ready-made binaries for macOS (Apple Silicon), Linux (x86_64 and arm64) and Windows (x86_64) on the releases page. From a checkout of this repository, cargo install --path . builds it from source.
Shell completions
Generate and install a completion script for your shell:
# zsh
km completions zsh > ~/.zfunc/_km
# add to ~/.zshrc if not already present:
# fpath=(~/.zfunc $fpath)
# autoload -Uz compinit && compinit
# bash
km completions bash > /etc/bash_completion.d/km
# fish
km completions fish > ~/.config/fish/completions/km.fish
Commands
Two conventions hold for every command:
--top Nlimits the rows of a table (20 by default), and JSON only when given; the totals of a table, theshortandtersesummaries and the CI annotations (github,codeclimate) always cover every file. A totals row over more files than it shows says so:Total (30 files, 20 shown).- Paths are shown relative to the root of the git repository, or to the analyzed directory outside one, without a leading
./, whatever directory km runs from and however the path argument is written, so the output of two commands can be joined.
km loc -- Count lines of code
km loc [path]
Run on the current directory:
km loc
Run on a specific path:
km loc src/
Test files are left out by default, so the totals are lower than those of cloc on the same directory. With --include-tests both count the same files. Below the table, when there is more than one, come the lines of each kind of file: code, docs, infra and data (kinds in the JSON).
Options:
| Flag | Description |
|---|---|
-v, --verbose |
Show summary stats (files read, unique, ignored, elapsed time) |
--by-author |
Break down lines of code by git author (requires a git repository) |
--include-tests |
Count test files and directories too (left out by default) |
--format {table,json,short,terse} |
Output format (default: table) |
Example output:
────────────────────────────────────────────────────────────────────
Language Files Blank Comment Code
────────────────────────────────────────────────────────────────────
Rust 5 120 45 850
TOML 1 2 0 15
────────────────────────────────────────────────────────────────────
SUM: 6 122 45 865
────────────────────────────────────────────────────────────────────
km dups -- Detect duplicate code
Finds duplicate code blocks across files using a sliding window approach. Applies the Rule of Three: duplicates appearing 3+ times are marked as CRITICAL (refactor recommended), while those appearing twice are TOLERABLE.
Test files and directories are excluded by default, since tests often contain intentional repetition.
The occurrences of a group never overlap within a file, so a run of repeated lines (a table of 0xFF) is not a copy of itself. A block grows while every occurrence goes on matching, even when the next lines also appear elsewhere. The duplicated lines of a project are counted once each, however many groups hold them, so they never exceed the code lines.
km dups [path]
Options:
| Flag | Description |
|---|---|
-r, --report |
Show detailed report with duplicate locations and code samples |
--show-all |
Show all duplicate groups (default: top 20) |
--min-lines N |
Minimum lines for a duplicate block (default: 6) |
--include-tests |
Include tests, examples and benchmarks in analysis (excluded by default) |
--max-duplicates N |
Exit with code 1 if duplicate groups exceed this limit (--max-duplicates 0 fails on any duplicate) |
--max-dup-ratio PERCENT |
Exit with code 1 if the duplicated-lines ratio exceeds this percentage (e.g. --max-dup-ratio 5.0) |
--fail-on-increase REF |
Exit with code 1 if the current duplication ratio is higher than at the given git ref (e.g. origin/main). Prevents debt from growing silently in CI |
--format {table,json,short,terse} |
Output format (default: table) |
Example summary output:
────────────────────────────────────────────────────────────────────
Duplication Analysis
Total code lines: 3247
Duplicated lines: 156
Duplication: 4.8%
Duplicate groups: 12
Files with duplicates: 8
Largest duplicate: 18 lines
Rule of Three Analysis:
Critical duplicates (3+): 7 groups, 96 lines
Tolerable duplicates (2x): 5 groups, 60 lines
Assessment: Good
────────────────────────────────────────────────────────────────────
Example detailed output (--report):
────────────────────────────────────────────────────────────────────
[1] CRITICAL: 18 lines, 3 occurrences (36 duplicated lines)
src/parser.rs:45-62
src/formatter.rs:120-137
src/validator.rs:89-106
Sample:
fn process_tokens(input: &str) -> Vec<Token> {
let mut tokens = Vec::new();
for line in input.lines() {
...
────────────────────────────────────────────────────────────────────
[2] TOLERABLE: 12 lines, 2 occurrences (12 duplicated lines)
src/main.rs:100-111
src/cli.rs:200-211
Sample:
match result {
Ok(value) => {
...
────────────────────────────────────────────────────────────────────
Excluded test patterns
By default, km dups skips files matching common test conventions:
- Directories:
tests/,test/,__tests__/,spec/, and .NET test projects, named after what they test (App.Tests/,Orders.UnitTests/,Api.IntegrationTest/) - Examples and benchmarks:
examples/,example/,benches/,bench/,benchmarks/,benchsuite/, code that is run to show or time the project rather than shipped with it - By extension:
*_test.rs,*_test.go,test_*.py,*.test.js,*.spec.ts,*Test.java,*_test.cpp, and more - Rust test modules: a file declared under
#[cfg(test)]without a body (#[cfg(test)] mod testutil;, with or without#[path]), and the modules below it
The same rules apply to every command. Use --include-tests to analyze them as well, and --list-excluded to see which files are left out and why.
Inline tests
Rust keeps tests inside the source file, under #[cfg(test)]. Every command that measures code leaves those items out along with the test files: dups, indent, hal, cycom, cogcom, mi, miv, smells, hotspots, score and the metrics of report. Only the item under the attribute is skipped, so the code after a test module is measured. --include-tests brings them back.
km loc and the line counts of km report still count them: they are lines of the file, as cloc counts them.
Generated files, data and prose
The commands that measure code (dups, indent, hal, cycom, cogcom, mi, miv, smells, score, report, init, hotspots, churn, tc) leave out:
- Generated files, by name (lock files,
*.min.js,*.bundle.js,*.pb.go,*_pb2.py,*.generated.*) or by content:- a comment in the first 20 lines saying the file is generated and not to be edited (Go's
// Code generated ... DO NOT EDIT., protoc); @generated, or C#'s<auto-generated;- the banner of Bison or Flex, or the helpers esbuild puts at the top of a bundle;
- bundler output: a
sourceMappingURLcomment at the end of the file, or a<name>.mapbeside it.
- a comment in the first 20 lines saying the file is generated and not to be edited (Go's
- Data and prose: JSON, YAML, TOML, XML, Markdown, Text, Properties and PO files. Their indentation is structure, not nesting, and they change with every release without being code.
km knowledge and km authors leave out generated files. km loc and km deps read every file: lines are lines, and generated code is still imported.
File kinds
Every file is of one kind:
| Kind | Files |
|---|---|
code |
Source code |
docs |
Markdown, reStructuredText and plain text |
infra |
Terraform, Dockerfiles, and YAML that is infrastructure: GitHub workflows, .gitlab-ci.yml and other CI pipelines, Compose files, Helm charts, Kustomize, Ansible (playbooks/, roles/*/tasks/, …), Kubernetes manifests (apiVersion and kind), CloudFormation templates |
data |
JSON, TOML, XML, the rest of YAML, Properties and PO files |
--kind chooses the kinds a command reads, on every command that reads files: --kind docs, --kind code,infra, --kind all. Without it nothing changes: each command reads what it always has, and infrastructure written in a language of code (Terraform, Dockerfiles) is measured as code. To measure code alone, use --kind code, or set separate = true under [kinds] in .kimun.toml to make that the default; [kinds] can also declare the kind of files by glob. --list-excluded names the files --kind leaves out. The commands that read git history take it too: km churn --kind docs gives the documents rewritten most often, km age --kind docs those nobody touched in a year, km knowledge --kind infra the Terraform modules only one person knows, and km tc --across the documentation and infrastructure that change with the code (Jiang & Adams, MSR 2015, on infrastructure that co-evolves with code). The km docs commands read the documents and km infra smells the infrastructure, whatever --kind says.
km indent -- Indentation complexity
Measures indentation-based complexity per file: standard deviation of indentation depths and maximum depth. Higher stddev suggests more complex control flow.
km indent [path]
Depth is read as structure, not layout. A line that continues a statement is at most one level deeper than the line it continues: arguments aligned under an opening parenthesis, a line after one ending in ::, &&, ||, |>, | or \, and a line starting with ., |, && or ||. Inside a block opened within parentheses (the body of a closure or of an Elixir fn), depths count again from that block. A file with fewer than 10 code lines is not graded (-): the spread of a handful of lines means nothing. Data and prose files are left out.
Options:
| Flag | Description |
|---|---|
--format {table,json,short,terse} |
Output format (default: table) |
--include-tests |
Include tests, examples and benchmarks in analysis (excluded by default) |
km hal -- Halstead complexity metrics
Computes Halstead complexity metrics per file by extracting operators and operands from source code.
km hal [path]
Metrics
| Symbol | Metric | Formula | Description |
|---|---|---|---|
| n1 | Distinct operators | -- | Unique operators in the code |
| n2 | Distinct operands | -- | Unique operands in the code |
| N1 | Total operators | -- | Total operator occurrences |
| N2 | Total operands | -- | Total operand occurrences |
| n | Vocabulary | n1 + n2 | Size of the "alphabet" used |
| N | Length | N1 + N2 | Total number of tokens |
| V | Volume | N * log2(n) | Size of the implementation |
| D | Difficulty | (n1/2) * (N2/n2) | Error proneness |
| E | Effort | D * V | Mental effort to develop |
| B | Bugs | V / 3000 | Estimated delivered bugs |
| T | Time | E / 18 seconds | Estimated development time |
Higher effort, volume, and bugs indicate more complex and error-prone code.
In TypeScript, declarations that run nothing (interface, type aliases, import type, export type { … }, declare) are left out: a file of types alone has no Halstead metrics, so it does not reach km miv or the Halstead dimension of km score.
Options:
| Flag | Description |
|---|---|
--format {table,json,short,terse} |
Output format (default: table) |
--include-tests |
Include tests, examples and benchmarks in analysis (excluded by default) |
--top N |
Rows of the table (default: 20); JSON lists every file unless --top is given |
--sort-by METRIC |
Sort by effort, volume, or bugs (default: effort) |
Example output:
Halstead Complexity Metrics
──────────────────────────────────────────────────────────────────────────────
File n1 n2 N1 N2 Volume Effort Bugs
──────────────────────────────────────────────────────────────────────────────
src/loc/counter.rs 139 116 3130 1169 34367.7 24070888 11.46
src/main.rs 37 43 520 185 4457.0 354743 1.49
──────────────────────────────────────────────────────────────────────────────
Total (2 files) 3650 1354 38824.7 24425631 12.95
Supported languages
Rust, Python, JavaScript, JSX, TypeScript, Go, C, C++, C#, Java, Objective-C, PHP, Dart, Ruby, Kotlin, Swift, Shell (Bash/Zsh), Elixir, Kaikai, Fortran, Pascal (Delphi).
km cycom -- Cyclomatic complexity
Computes cyclomatic complexity per file and per function by counting decision points (if, for, while, match, &&, ||, etc.).
km cycom [path]
Options:
| Flag | Description |
|---|---|
--format {table,json,short,terse,github,codeclimate} |
Output format (default: table). github emits GitHub Actions annotations; codeclimate (alias: gitlab) emits CodeClimate JSON for GitLab Code Quality |
--include-tests |
Include tests, examples and benchmarks in analysis (excluded by default) |
--top N |
Rows of the table (default: 20); JSON lists every file unless --top is given |
--min-complexity N |
Skip files whose most complex function is below N; with --per-function, also hide functions below N (default: 1) |
--per-function |
Show per-function breakdown |
How functions are found
cycom, cogcom and smells share one function detector. A declaration is recognised by the marker of its language (fn, def, func, function, defmacro) or, in the C family, by its shape, over the whole signature even when it spans several lines:
- parameters on several lines, the name alone on the line above them (GNU style), the
{on the next line; - a line that starts inside parentheses left open (the continuation of a condition, a parameter with an attribute) is never a declaration, nor is an annotation (
@Override,[HttpGet]) or an anonymous class (new Adapter() {); - preprocessor directives and the lines a trailing
\joins to them are not code: they neither declare functions nor add complexity; - Go methods are named without their receiver;
- in JavaScript and TypeScript, functions bound to a name (
res.send = function,const f = (x) => {,name: Type = <T>(x) => {) take that name, a function passed as an argument is<anonymous>, and the braces of a regular expression literal are not blocks; - in Elixir, the code inside a
quoteblock is not part of the function that holds it, and the functions it declares are measured on their own.
km cogcom -- Cognitive complexity
Computes cognitive complexity per file and per function using the SonarSource method (2017). Unlike cyclomatic complexity, cognitive complexity measures how difficult code is to understand, penalizing deeply nested structures and rewarding linear control flow.
km cogcom [path]
Options:
| Flag | Description |
|---|---|
--format {table,json,short,terse,github,codeclimate} |
Output format (default: table). github emits GitHub Actions annotations; codeclimate (alias: gitlab) emits CodeClimate JSON for GitLab Code Quality |
--include-tests |
Include tests, examples and benchmarks in analysis (excluded by default) |
--top N |
Rows of the table (default: 20); JSON lists every file unless --top is given |
--min-complexity N |
Skip files whose most complex function is below N; with --per-function, also hide functions below N (default: 1) |
--per-function |
Show per-function breakdown |
--sort-by METRIC |
Sort by total, max, or avg (default: total) |
In brace languages a function ends where its braces balance. A body introduced by = (fn f(x) = expr, as in Kaikai, Kotlin or Scala) has no closing brace of its own: it runs while its braces are open and the following lines are indented deeper than the declaration. km cycom finds functions the same way.
Keywords and braces are read in order along each line, so code written on one line scores as it does on several: if (a) { return 1 } else { return 2 } is 2, and for (v of xs) { if (y) { return v } } is 3. The block of a control keyword opens at the next {, on its line or, Allman style, on the next one; else if nests its block like if and else.
In Elixir, with, unless and receive are structural increments like case, and and, or, && and || count as boolean sequences. For km cycom, each clause (->) of a case, cond, receive or multi-clause fn is a branch, as each case label of C, and with, unless, and, or, && and || add one each.
km mi -- Maintainability Index (Visual Studio variant)
Computes the Maintainability Index per file using the Visual Studio formula. MI is normalized to a 0–100 scale with no comment-weight term.
km mi [path]
Formula
MI = MAX(0, (171 - 5.2 * ln(V) - 0.23 * G - 16.2 * ln(LOC)) * 100 / 171)
Where V = Halstead Volume, G = cyclomatic complexity, LOC = code lines.
Thresholds
| MI Score | Level | Meaning |
|---|---|---|
| 20–100 | green | Good maintainability |
| 10–19 | yellow | Moderate maintainability |
| 0–9 | red | Low maintainability |
Options:
| Flag | Description |
|---|---|
--format {table,json,short,terse} |
Output format (default: table) |
--include-tests |
Include tests, examples and benchmarks in analysis (excluded by default) |
--top N |
Rows of the table (default: 20); JSON lists every file unless --top is given |
--sort-by METRIC |
Sort by mi (ascending), volume, complexity, or loc (default: mi) |
Example output:
Maintainability Index (Visual Studio)
──────────────────────────────────────────────────────────────────────
File Volume Cyclo LOC MI Level
──────────────────────────────────────────────────────────────────────
src/loc/counter.rs 32101.6 115 731 0.0 red
src/main.rs 11189.6 16 241 17.5 yellow
src/loc/report.rs 6257.0 13 185 22.2 green
──────────────────────────────────────────────────────────────────────
Total (3 files) 1157 7.2
The Total row is the MI of the files weighted by their code lines, with any value below 0 taken as 0, as the legacy model of km score takes it: a few small files do not outweigh the bulk of the code.
km miv -- Maintainability Index (verifysoft variant)
Computes the Maintainability Index per file. MI combines Halstead Volume, Cyclomatic Complexity, lines of code, and comment ratio into a single maintainability score.
This is the verifysoft.com variant, which includes a comment-weight term (MIcw) that rewards well-commented code.
km miv [path]
Formula
MIwoc = 171 - 5.2 * ln(V) - 0.23 * G - 16.2 * ln(LOC)
MIcw = 50 * sin(sqrt(2.46 * radians(PerCM)))
MI = MIwoc + MIcw
Where V = Halstead Volume, G = cyclomatic complexity, LOC = code lines, PerCM = comment percentage (converted to radians).
The scale has no lower bound, as verifysoft defines it. The index is computed once per file, and both logarithmic terms grow with the file's size, so a long file of trivial functions can fall below zero while a short file with one tangled function stays moderate; the Total row averages the files. For a scale clamped to 0–100 use km mi; to find tangled functions, km cogcom.
Thresholds
| MI Score | Level | Meaning |
|---|---|---|
| 85+ | good | Easy to maintain |
| 65–84 | moderate | Reasonable maintainability |
| <65 | difficult | Hard to maintain |
Options:
| Flag | Description |
|---|---|
--format {table,json,short,terse} |
Output format (default: table) |
--include-tests |
Include tests, examples and benchmarks in analysis (excluded by default) |
--top N |
Rows of the table (default: 20); JSON lists every file unless --top is given |
--sort-by METRIC |
Sort by mi (ascending), volume, complexity, or loc (default: mi) |
Example output:
Maintainability Index
────────────────────────────────────────────────────────────────────────────────
File Volume Cyclo LOC Cmt% MIwoc MI Level
────────────────────────────────────────────────────────────────────────────────
src/loc/counter.rs 32101.6 115 731 3.6 -16.2 2.8 difficult
src/main.rs 8686.7 14 204 14.6 34.5 68.2 moderate
src/util.rs 2816.9 18 76 9.5 55.4 84.7 moderate
────────────────────────────────────────────────────────────────────────────────
Total (3 files) 1011 22.2
The verifysoft MI has no lower bound: a large file can be far below 0 (ripgrep defs.rs is −206), and it weighs size more than complexity. That is the definition of the variant; km mi is the one bounded to 0–100. The Total row is weighted by code lines with values below 0 taken as 0, as in km mi.
km hotspots -- Hotspot analysis
Finds hotspots: files that change frequently AND have high complexity. Based on Adam Tornhill's method ("Your Code as a Crime Scene").
km hotspots [path]
Formula
Score = Commits × Complexity
Files with high scores concentrate risk — they are both change-prone and complex, making them the highest-value refactoring targets.
By default, complexity is measured by total indentation (sum of logical indentation levels across all code lines), following Tornhill's original method from "Your Code as a Crime Scene". Use --complexity cycom for cyclomatic complexity instead, or --complexity cogcom for cognitive complexity. Documents (--kind docs) are measured by their lines of text whatever the metric, since indentation and branches say nothing about prose: km hotspots --kind docs finds the long documents rewritten most often, such as an agent instruction file that grows every week. Infrastructure keeps the metric asked for, as the indentation of YAML and HCL is its structure.
Requires a git repository. Merge commits are excluded from the count.
Options:
| Flag | Description |
|---|---|
--format {table,json,short,terse} |
Output format (default: table) |
--include-tests |
Include tests, examples and benchmarks in analysis (excluded by default) |
--top N |
Rows of the table (default: 20); JSON lists every file unless --top is given |
--sort-by METRIC |
Sort by score, commits, or complexity (default: score) |
--since DURATION |
Only consider commits since this time (e.g. 30d, 6m, 1y) |
--complexity METRIC |
indent (default, Tornhill), cycom (cyclomatic) or cogcom (cognitive); documents are measured by lines |
Duration units: d (days), m (months, approx. 30 days), y (years, approx. 365 days).
Example output (default — indentation complexity):
Hotspots (Commits × Total Indent Complexity)
──────────────────────────────────────────────────────────────────────────────
File Language Commits Total Indent Score
──────────────────────────────────────────────────────────────────────────────
src/main.rs Rust 18 613 11034
src/loc/counter.rs Rust 7 1490 10430
src/dups/detector.rs Rust 7 1288 9016
src/dups/mod.rs Rust 9 603 5427
src/report/mod.rs Rust 4 998 3992
──────────────────────────────────────────────────────────────────────────────
Score = Commits × Total Indentation (Tornhill method).
High-score files are change-prone and complex — prime refactoring targets.
Example output (--complexity cycom):
Hotspots (Commits × Cyclomatic Complexity)
──────────────────────────────────────────────────────────────────────────────
File Language Commits Cyclomatic Score
──────────────────────────────────────────────────────────────────────────────
src/loc/counter.rs Rust 7 115 805
src/dups/mod.rs Rust 9 44 396
src/main.rs Rust 18 21 378
src/cycom/analyzer.rs Rust 4 92 368
src/dups/detector.rs Rust 7 46 322
──────────────────────────────────────────────────────────────────────────────
Score = Commits × Cyclomatic Complexity.
High-score files are change-prone and complex — prime refactoring targets.
km knowledge -- Code ownership analysis
Analyzes code ownership patterns via git blame (knowledge maps). Based on Adam Tornhill's method ("Your Code as a Crime Scene" chapters 8-9).
km knowledge [path]
Identifies bus factor risk and knowledge concentration per file. Generated files (lock files, minified JS, files that say they are generated, see Generated files, data and prose) are automatically excluded.
Risk levels
| Risk | Condition | Meaning |
|---|---|---|
| CRITICAL | 1 person owns >80% | High bus factor risk |
| HIGH | 1 person owns 60-80% | Significant concentration |
| MEDIUM | 2-3 people own >80% combined | Moderate concentration |
| LOW | Well-distributed | Healthy ownership |
Knowledge loss detection
Use --since to define "recent activity". If the primary owner of a file has no commits in that period, the file is flagged with knowledge loss risk; when nobody committed in the period, every file is. Use --risk-only to show only those files; it needs --since. The closing count is of every file at risk, with how many --top shows.
Bot accounts (dependabot[bot], github-actions[bot]) hold no knowledge: their lines are left out, and a file only bots wrote is not listed. Generated files and data or prose (JSON, YAML, Markdown, text) are left out too.
Who wrote each line
Ownership comes from blame of the file as committed at HEAD. When the git program is installed, kimun runs git blame -w -M -C: a change of whitespace keeps the author of the line, and so does code moved within a file or from another file the same commit changed. Revisions listed in .git-blame-ignore-revs (or in the file blame.ignoreRevsFile names) are skipped, so a mass reformat does not take the lines. Without git, libgit2 blames the file: it skips whitespace changes but neither follows moved code nor ignores revisions. km authors and km loc --by-author read blame the same way.
Options:
| Flag | Description |
|---|---|
--format {table,json,short,terse} |
Output format (default: table) |
--include-tests |
Include tests, examples and benchmarks in analysis (excluded by default) |
--top N |
Rows of the table (default: 20); JSON lists every file unless --top is given |
--sort-by METRIC |
Sort by concentration, diffusion, or risk (default: concentration) |
--since DURATION |
Define recent activity window for knowledge loss (e.g. 6m, 1y, 30d) |
--risk-only |
Show only files with knowledge loss risk |
--summary |
Aggregate by author: files owned, lines, languages, worst risk |
--bus-factor |
Show the project bus factor: how many contributors have to leave before most of the code has no author left |
--bus-factor-threshold PCT |
Share of the code, in lines, the bus factor counts to (default: 50) |
--author NAME |
Show only files owned by this author (case-insensitive substring match) |
Example output:
Knowledge Map — Code Ownership
──────────────────────────────────────────────────────────────────────────────
File Language Lines Owner Own% Contrib Risk
──────────────────────────────────────────────────────────────────────────────
src/loc/counter.rs Rust 731 E. Diaz 94% 2 CRITICAL
src/main.rs Rust 241 E. Diaz 78% 3 HIGH
src/walk.rs Rust 145 E. Diaz 55% 5 MEDIUM
──────────────────────────────────────────────────────────────────────────────
Files with knowledge loss risk (primary owner inactive): 1
src/legacy.rs (Former Dev)
Use --bus-factor to compute how many contributors have to leave before most of the code has nobody who wrote it (the truck factor of Avelino et al., "A novel approach for estimating truck factors", ICPC 2016):
- The authors of a file are those blame gives at least 75% of the lines of its main author. A contributor with a few lines in a file is not one of its authors.
- Contributors leave one at a time, first the one who is an author of the most lines, until more than 50% of the code is in files with no author left. The bus factor is how many left. Files weigh by their lines, so a one-line file does not count as much as the core of the project, and a long tail of occasional contributors does not raise the number.
- With
--since, authors with no commits in that window have already left: the code only they wrote is orphaned before anyone is counted. When that is already more than the threshold, the bus factor is 0. - People are told apart by email, read through
.mailmap, so one address under several names, or several addresses the mailmap joins, is one person. This also holds for--summary,km authorsandkm loc --by-author.
$ km knowledge --bus-factor --since 1y
Project Bus Factor: 2
12% of the code is already in files whose authors have not committed in 1y.
If these 2 contributors leave, 71% of the code is in files with no author left.
Risk: HIGH — two people hold critical knowledge
Authors of a file hold at least 75% of the lines of its main author; files weigh by lines; threshold 50%.
──────────────────────────────────────────────────────────────────────
Rank Author Files Lines Share Orphaned
──────────────────────────────────────────────────────────────────────
1 E. Diaz 41 8420 68.12% 48.3%
2 A. Torres 9 1490 12.06% 71.0%
3 R. Soto 6 940 7.61%
4 M. Rojas (inactive) 3 610 4.94%
──────────────────────────────────────────────────────────────────────
Orphaned is the share of the code left with no author once that contributor and those above have left.
km tc -- Temporal coupling analysis
Analyzes temporal coupling between files via git history. Based on Adam Tornhill's method ("Your Code as a Crime Scene" ch. 7): files that frequently change together in the same commits have implicit coupling, even without direct imports.
km tc [path]
Formula
Coupling strength = shared_commits / min(commits_a, commits_b)
Coupling levels
| Strength | Level | Meaning |
|---|---|---|
| >= 0.5 | STRONG | Files change together most of the time |
| 0.3-0.5 | MODERATE | Noticeable co-change pattern |
| < 0.3 | WEAK | Occasional co-changes |
High coupling between unrelated modules suggests hidden dependencies or architectural issues — consider extracting shared abstractions.
Options:
| Flag | Description |
|---|---|
--format {table,json,short,terse} |
Output format (default: table) |
--top N |
Rows of the table (default: 20); JSON lists every pair unless --top is given |
--sort-by METRIC |
Sort by strength or shared (default: strength) |
--since DURATION |
Only consider commits since this time (e.g. 6m, 1y, 30d) |
--min-degree N |
Minimum commits per file to be included (default: 3) |
--min-shared N |
Minimum commits two files must share to be a pair (default: 3) |
--max-changeset N |
Ignore commits touching more than N files as evidence of coupling (default: 30) |
--min-strength F |
Minimum coupling strength to show (e.g. 0.5 for strong only) |
--across |
Only pairs of files of different kinds: code with documentation, code with infrastructure. Reads every kind but data unless --kind says which |
--include-ext, --exclude-ext, --exclude-dir, --exclude |
Path filters, applied to the paths of history |
Only files that still exist in the working tree are paired, and only code: lock files, manifests, CI YAML and documentation are left out. A sweeping commit (more than --max-changeset files) relates files by accident and is skipped, as is a revert together with the commit it reverts (a message opening with Revert " that names the commit): together they changed nothing. Renames are not followed: a file is paired under the path it has now.
Example output:
Temporal Coupling — Files That Change Together
──────────────────────────────────────────────────────────────────────────────────
File A File B Shared Strength Level
──────────────────────────────────────────────────────────────────────────────────
src/auth/jwt.rs src/auth/middleware.rs 12 0.86 STRONG
lib/parser.rs lib/validator.rs 8 0.53 STRONG
config/db.yaml config/cache.yaml 6 0.35 MODERATE
──────────────────────────────────────────────────────────────────────────────────
12 coupled pairs found (3 shown). Showing pairs with >= 3 shared commits.
Strong coupling (>= 0.5) suggests hidden dependencies — consider extracting shared abstractions.
Note: File renames are not tracked across git history. Renamed files appear as separate entries.
km impact -- Impact of a diff
Measures how far a change reaches, for a PR or for uncommitted work, before it is merged.
What the blast radius is
The blast radius of a change is the part of the system that can behave differently because of it, beyond the files it edits. A change to a function is not only that function: it is every piece of code that calls it, and what calls those. If the function's own tests pass and something that uses it breaks in production, the break was inside the radius and outside the tests.
km impact measures it at two levels, and reports what guards it:
| Level | The radius is | Read from |
|---|---|---|
| Projects | The projects of the repository that depend on the changed ones, directly or through others | Manifests |
| Source files | The files that call the functions that changed, and the files that use those | The dependency graph of the code |
At each level the radius is a count over a total: 3 of 6 projects, 2 of 618 source files. It answers how much of the system to worry about. Next to it comes what is unprotected: the files inside the radius that no test exercises. A wide radius fully covered by tests is a change to make with care; one file in the radius with no test is where it will break without warning, and the report names it.
Two more measures describe the change itself rather than its reach: how spread it is (diffusion), and which files usually change with it and were left out (logical radius).
km impact --since-ref origin/main [path] # the branch you are on
km impact --since-ref main --until-ref feature # a branch, without checking it out
git diff main... | km impact --diff - # a patch
km impact --pr 123 # a GitHub pull request
What is measured
| Source | The change | History ends at | Manifests and files read from |
|---|---|---|---|
--since-ref REF |
From where REF and HEAD diverged to the working tree: committed, uncommitted and untracked changes, and deletions |
That merge base | The working tree |
--since-ref A --until-ref B |
What B brings since it diverged from A, whatever is checked out |
That merge base | The tree of B |
--diff FILE |
A patch in git format, from a file or from stdin (-) |
HEAD, or where --since-ref and HEAD diverged when given |
The working tree |
--pr NUMBER |
A GitHub pull request | As the mode it resolves to | As the mode it resolves to |
It is always the change over the whole repository: path only locates the repository and does not narrow the analysis. Generated files (lock files, minified assets) are left out of every measure.
--pr requires the GitHub CLI (gh) installed and authenticated. kimun runs it as a program; it links no GitHub client and stores no token.
- When the repository has the commits of the pull request, it is measured from them, as between two refs. A pull request merged by squash or rebase, whose head was never fetched, is measured from the patch GitHub serves, read against the tree of the commit that merged it. Its base and that commit are not compared: the base may be many merges behind, and everything merged in between would be counted.
- Otherwise (a pull request from a fork, or one not fetched) its patch is taken from
gh pr diffand measured like any other patch, with a note on stderr.git fetch origin pull/NUMBER/headmakes its commits available.
A patch (--diff, or a pull request without local commits) is measured against the working tree, where it is not applied:
- it must be in git format with the
a/andb/prefixes, asgit diff,git format-patchandgh pr diffprint it, without colors; - for a branch use
git diff main...(three dots):git diff maincompares against the tip ofmain, and shows whatmaingained since as if the branch had undone it. To include uncommitted work,--since-ref mainis the direct way; - a project the patch creates is not known, so its files have unknown reach, and
--affectedlists every project; - if the patch is already applied in
HEAD, pass--since-refso that its own commits are not counted as history.
Blast radius: projects
Which projects of the repository are reached by the diff. Meant for monorepos, where a change to a shared library reaches applications its author may not know.
A project is a directory with a manifest. A project depends on another when its manifest names it as a local dependency; dependencies on registries or other repositories are ignored. A changed file belongs to the nearest project above it. The radius is every project that depends on a changed one, directly or through others.
| Ecosystem | Manifest | Local dependencies read |
|---|---|---|
| Rust | Cargo.toml |
path dependencies, workspace = true resolved through [workspace.dependencies], in [dependencies], [dev-dependencies], [build-dependencies] and their [target.*] forms |
| JavaScript / TypeScript | package.json |
any dependency whose name is another package of the repository (npm, yarn and pnpm workspaces), plus file: and link: |
| Elixir | mix.exs |
path: dependencies and in_umbrella: true |
| Go | go.mod, go.work |
required modules that are another module of the repository, and replace with a directory |
| Python | pyproject.toml |
any requirement whose name is another project of the repository (whatever the case, and whichever of -, _, .), and those given a path in [tool.uv.sources] or in the tables of Poetry; read from [project] (dependencies, optional-dependencies), [dependency-groups], [build-system] requires, [tool.poetry] and the development groups of uv and PDM |
| Terraform | any .tf file: a module is a directory of them, named after it |
module blocks whose source is a local path (./, ../); registry, git and other remote sources are external. A change to a shared module reaches the modules and the stacks or environments that call it |
Blast radius — projects reached through their manifests
──────────────────────────────────────────────────────────────────────────────
3 of 6 projects reached (50%), 2 direct
Changed: libs/core
Changed Reaches Distance Scope Via
libs/core apps/inventory 1
libs/core libs/locker 1
libs/core apps/parcels 2 libs/locker
──────────────────────────────────────────────────────────────────────────────
Changed files outside every project (reach unknown): Makefile
Scope: a
devdependency (dev or test only) reaches the dependent, whose tests use the changed project, and stops there: the changed project is not part of what the dependent ships, so the dependents of the dependent are not reached.buildandoptionaldependencies carry on like runtime ones.Workspace roots: a
Cargo.tomlwith[workspace], apackage.jsonwithworkspaces(or next to apnpm-workspace.yaml), an umbrellamix.exswithapps_path, ago.work, apyproject.tomlwith[tool.uv.workspace]. A change to the manifest or the lock file at such a root (Cargo.lock,package-lock.json,yarn.lock,pnpm-lock.yaml,bun.lock,mix.lock,uv.lock) reaches every project under it at distance 1, with scopeworkspace, and carries on from them. A workspace root is a project itself only when it declares one ([package]in Cargo,app:in mix,[project]in Python); apackage.jsonat a workspace root never is.Inert files reach nothing: a change to documentation (
.md,.mdx,.rst,.adoc,.txt) breaks no build and no test. It does not count as a change to its project, nor as a file of unknown reach..kimun.tomlcan declare more:[impact] inert = ["scripts/**", "notebooks/**"] # globs, relative to the repositoryFiles outside every project (CI workflows, shared configuration) are listed apart. Their reach is unknown, not zero.
--affectedprints the projects whose builds and tests the diff calls for, one per line, and nothing else: the changed and the reached ones. If any changed file is outside every project, it prints all projects and says why on stderr — skipping a test suite is worse than running one too many. It reads only the diff and the manifests, not the history.A repository with a single project gets a line saying this level does not apply, rather than "0 reached".
node_modules,vendor,testdataand.terraformare never searched for manifests, nor aredeps,_buildandtarget(unless they sit undersrc,liborapp, where they are part of the project), nor is afixturesdirectory inside a test directory. An end-to-end suite with its own manifest (test/e2e/package.json) is a project.
Limits:
mix.exsis code and is read as text. A dependency whose path is built at run time (Path.expand(...), string interpolation, a generated list) cannot be attributed; the report names the manifest and how many it missed.- Of Python only
pyproject.tomlis read. A project declared insetup.py(which is code) orsetup.cfgis not seen, nor is a local dependency written inrequirements.txt(-e ../lib). Apyproject.tomlthat only configures tools, with neither[project]nor[tool.poetry], is not a project. Extras (optional-dependencies) areoptional; dependency groups aredev. - Coupling across ecosystems is not visible: a web client and the service whose API it calls have no manifest dependency between them.
- A project nested in another (
assets/package.jsoninside a Phoenix application) has no dependency to or from the one that contains it unless a manifest declares one. - The graph is read from the working tree. The files of a project the diff deletes or moves away belong to no project any more: their reach is unknown, and the manifests still naming it are reported as not read.
Blast radius: source files
Inside the projects the change affects: which source files use what changed, and which of them no test exercises. This is the question "the tests of the module I changed pass; who else calls it?".
The first line is the answer in short: how many files call what changed, and how many of them have no test. Radius is the count of files that call what changed, and of those that use them. Upper bound is what it would be without knowing which functions changed.
Structural radius — source files that use what changed
──────────────────────────────────────────────────────────────────────────────
1 file calls what changed, 1 of them with no test
Changed: lib/booking/insights.ex
Functions: arrange
Radius: 1 of 6 source files (17%): 1 at distance 1
Not in the radius: 1 that refer to the module without calling it
Upper bound, whatever the function: 4 files (67%)
Tests Dependent
none lib/booking_web/controllers/insight_controller.ex
calls Insights.arrange
none lib/booking/export.ex
refers to the module without calling it
──────────────────────────────────────────────────────────────────────────────
No test reaches 1 of the files that call what changed; an integration test is probably missing:
lib/booking_web/controllers/insight_controller.ex
No test in the change exercises a file that uses what changed.
1 more with no test use the module without a call that tells whether the change concerns them.
How it is measured:
The dependency graph of
km depsis read backwards from the changed source files. Every import counts for the radius,typeanddeferredones included: a change to a type breaks those that check against it, and a deferred import runs. A test that imports a file only for its types runs none of it, so it does not protect it.In Elixir the change is narrowed to functions: the lines the diff touches tell which functions changed, and a change to a private function is carried to the public ones that reach it through local calls. A file that uses the module is then one of three: it calls a function that changed, it refers to the module without calling it (a struct, an
import, ausewith nothing to read), or it only calls functions the change leaves alone, and is not listed. A file that imports the whole module may reach a function through a call the reading does not see, so it refers to the module even when the calls seen are left alone. Each changed file is narrowed on its own. Outside the functions, a touchedaliasorrequirechanges none (it only names what the touched functions use), and a touched module attribute changes the functions that read it, private ones carried to the public ones as above. Every line of its value counts, however many it spans, and an attribute whose value reads it (@all @codes ++ ["AR"]) changes with it. The report names the attributes and who reads them (Attributes: @valid_countries (read by valid_countries)). Ause, animport, adefstruct, an attribute no function reads, an attribute the compiler or a library reads when the module compiles (@derive,@behaviour,@before_compile,@primary_key...) and the routes of a Phoenix router (scope,pipe_through,get...) may concern every function: that file is not narrowed, every use of it counts, and the report says which line it was and why. A new file is never narrowed.In Python the change is narrowed to the top-level names of the module: its functions, classes and assignments. A touched method changes its class; a decorator belongs to what it decorates; a name that uses a changed one changes too, private or not, since nothing keeps another file from importing it. A file that uses the module calls what changed when it takes one of those names:
from m import name,m.nameon a module it imports (under its alias too), or, afterfrom m import *, a name it goes on to mention. A module handed over as a value may give any of its names. One imported with nothing read on it only refers to the module. Imports, the docstring, anifor atrythat only chooses imports, and theif __name__ == "__main__":block change no name. Any other code at the left margin runs when the module is imported and may concern every name: the file is not narrowed. Not seen: names reached at run time (getattr, a registry filled by decorators,mock.patch("m.name")), and which callers use the method that changed. A name a package re-exports is followed through its__init__.py, one distance further.The radius starts at the files that call what changed and follows who uses them, file by file. A file that only refers to the module, without a call that tells, is listed but does not carry the radius on: a schema is named by half a project, and following all of that says nothing. Files that only pass through a dependent the change leaves alone are not counted either. Past the first step it follows functions in Elixir: a file is reached when one of its public functions calls a function already reached, directly or through the private functions of its file, and only those functions carry the radius on. Where functions are not known it goes on file by file: another language, an
importwithoutonly:or ause, a module named without a call (a router listing controllers), a call outside every function. Calls are matched by name, not arity, so it stays an estimate from above.--max-distance Nstops the radius N files away from the change, and the report says so (stopped at distance N). The upper bound is what the radius would be if every use of a changed file counted, whatever the function: in a codebase where everything goes through a few contexts it is most of the project, which is why the radius is the number to read.What the change removes is an origin too. A deleted file is read as it was: every file that still uses it is reached, and every use counts (
deleted). An Elixir file whose modules are all defined elsewhere after the change was moved, not deleted. A public function gone from a file that remains (Elixirdef, a top-level name in Python) counts among the functions that changed, so its callers are exposed. The files that still use what was removed are listed apart, since they break when the change is made:N files still use what the change removes, and will break:. Only static uses are seen:apply/3, a function named at run time or a project that is not in the repository escape it.Tests says how the dependent is protected. A test rarely names everything it exercises — a controller or a live view is tested through its route, a helper through the views that use it — so protection comes in degrees:
TestsProtection When a number direct That many test files refer to it, request a route it serves, or sit at the same place in the source and test layout ( lib/a/b.exandtest/a/b_test.exs)namednamed A test carries its name a directory apart ( live/page_live.exandpage_live_test.exs), or the name of the directory it is in (page_live/index.exandpage_live_test.exs). A test at the place of a source file is the test of that file and names no other:app_test.exsbesideapp.exsays nothing of the files inapp/usersusers It has no test of its own, but a file that uses it has one nonenone No test reaches it Test support (
test/support/), configuration and scripts are not tests: a factory refers to everything and would make everything look protected.A file that calls what changed and that no test reaches is unprotected: the change can break it without any test noticing. That is the warning. Dependents are listed from the least protected.
Entry points are told apart. A command-line task or a script is run, not used: no test of something else passes through it, and few have one of their own. With no test they get a line of their own instead of the warning. They are the files under
mix/tasks/,management/commands/,bin/andscripts/that no source file uses, plus what.kimun.tomldeclares:[impact] entry_points = ["**/endpoint.ex"] # globs, relative to the repositoryIn Elixir, a test that requests a path protects the module that serves it. The routes are read from the Phoenix router of the project of the test, with the path and the alias of every
scopearound them;:idmatches any one segment, and a segment the test writes at run time (#{order.id}) matches only a parameter of the route: a request for one order does not protect/orders/new.In Elixir, what a framework relates by convention counts as a use. A Phoenix controller uses the views named after it (
PageControllerandPageJSON,PageHTML,PageView), so the test of the controller protects them. A module uses the components its templates render, whether written in it as~Hor kept in a.html.heexfile beside it (page/index.exandpage/index.html.heex) or in a directory named after it (page_html.exandpage_html/home.html.heex).
It is measured for the languages whose graph reflects usage: C, C++, C#, Elixir, Go, Java, JavaScript/TypeScript, Kaikai, Python, Ruby and Rust. In Go the unit is the package: a change to any file of one reaches every file that imports it. In C and C++ a change to a source reaches the files that include its header. Only the projects the change affects are read; the whole repository when the reach over projects is unknown.
Limits: a test at the same place, or named after a file, may not exercise the call that changed, and one that refers to a file may mock what it calls — the column says a test exists, not that it covers. users is weaker still: it says something that uses the file is tested. Function names are compared without arity. Modules named at run time (apply/3, configuration) or generated by macros are not seen. A request is read when its path is written in the call (live(conn, ~p"/orders")), not when it comes from a variable or a helper, and a route when it is declared on one line.
Diffusion
How spread the change is. Kamei et al. found diffusion among the strongest predictors of a defect-inducing change.
| Measure | Meaning |
|---|---|
| Files changed | Files added, modified, renamed or deleted |
| Directories | Distinct directories holding a changed file |
| Subsystems | Distinct top-level directories (files at the root form one more) |
| Lines added / deleted | Binary files count no lines |
| Entropy | Shannon entropy of the modified lines over the files, divided by its maximum: 0 when one file holds every modified line, 1 when all the files hold the same amount |
Logical radius
Files that usually change with the files of the diff and are not in it — a change that may have been forgotten. It catches coupling the code does not declare: tests, configuration, migrations.
Confidence = shared_commits / commits of the changed file
A file is reported when some changed file reaches --min-confidence with at least --min-shared shared commits. Confidence is directional, unlike the strength of km tc: a file that changed three times, always with one that changed a hundred times, has strength 1.0 but is needed in 3% of the changes to the other.
- History ends at the merge base: the commits of the diff are never evidence for themselves.
- Commits touching more than
--max-changesetfiles are ignored, and the report says how many: a reformat or a rename across the project relates its files to each other by accident. - A changed file with no history (new, or outside
--since) predicts nothing. It is listed apart, so its silence is not read as "no impact". - Files that no longer exist are not reported.
- Test files are always part of the analysis: a test that usually changes with the code is a change worth expecting.
Options:
| Flag | Description |
|---|---|
--since-ref REF |
Git ref to diff against, e.g. origin/main, HEAD. Required unless --diff or --pr is given |
--until-ref REF |
Measure up to this ref instead of the working tree (needs --since-ref) |
--diff FILE |
Measure a patch in git format; - reads stdin |
--pr NUMBER |
Measure a GitHub pull request; requires gh installed and authenticated |
--since DURATION |
Only consider history since this time (e.g. 6m, 1y, 30d) |
--min-confidence F |
Minimum confidence to report a missing file (default: 0.5) |
--min-shared N |
Minimum shared commits to report a missing file (default: 3) |
--max-changeset N |
Ignore commits touching more than N files as evidence (default: 30) |
--max-distance N |
Follow the structural radius at most N files away from the change (default: no limit) |
--affected |
Print only the changed and reached projects, one per line |
--fail-on-stale-docs |
Exit with code 1 when a document that covers or names a changed file was not updated |
--http |
Add the HTTP level: the routes the change removes or changes, and the client calls that ask for them (heuristic) |
--top N |
Show only the top N missing files (default: 20) |
--format {table,json,short,terse,github,codeclimate} |
Output format (default: table). github and codeclimate annotate the documents left behind |
Example output:
Change Impact — diff against main
Blast radius — projects reached through their manifests
──────────────────────────────────────────────────────────────────────────────
Single project (.): no other project to reach; this level does not apply.
──────────────────────────────────────────────────────────────────────────────
Diffusion
Files changed 5
Directories 3
Subsystems 2
Lines added 120
Lines deleted 30
Entropy 0.82 (0 = one file holds the change, 1 = evenly spread)
Logical radius — files that usually change with this diff and are not in it
──────────────────────────────────────────────────────────────────────────────
Missing file Confidence Shared Changes with
──────────────────────────────────────────────────────────────────────────────
src/tc/report.rs 0.80 8/10 src/tc/mod.rs (+1 more)
README.md 0.50 6/12 src/cli.rs
──────────────────────────────────────────────────────────────────────────────
No history before the diff (new or never committed): src/impact/mod.rs
Generated files ignored: 1
When the rows are too wide for a table (long paths), each missing file is listed on a line of its own with its evidence below.
Documentation left behind
The documents that describe what the change touched and were not updated with it: the note of a knowledge base, the runbook, the instructions of an agent that will mislead whoever follows them next. Each document is found by the strongest of three signals:
| Signal | The document describes a changed file when |
|---|---|
declared |
it says so: covers: in its front matter (covers: ["apps/payments/**", lib/billing/invoice.ex]), or [docs] covers in .kimun.toml maps the file to it |
referenced |
it names the file, as km docs refs reads references: a link, or a path in a code span, whole or by its end (deps/graph.rs), or a directory of two names or more above it (lib/billing/). When the change deleted or moved the file named, the reference is dead too: names it, now gone |
co_changed |
history says it usually changes with the file (the logical radius, which then leaves documents to this block) |
A document the change touches is up to date. Changelogs (CHANGELOG, CHANGES, HISTORY) record what was and are never listed, nor the globs of [docs] ignore, such as decision records. Markdown documents are read (.md, .mdx, .markdown), as they are after the change.
Documentation — documents that describe what changed and were not updated
──────────────────────────────────────────────────────────────────────────────
Document Describes Why
──────────────────────────────────────────────────────────────────────────────
kb/payments/refunds.md apps/payments/lib/refund.ex covers apps/payments/**
kb/architecture.md lib/billing/invoice.ex (+1) names it (line 42)
docs/billing/README.md lib/billing/invoice.ex changed together 7 of 9 times
──────────────────────────────────────────────────────────────────────────────
--fail-on-stale-docs exits with code 1 when a declared or referenced document was left behind; history alone does not fail. --format github annotates each changed file with the document to update (a warning, a notice for co_changed), and --format codeclimate gives the same for GitLab.
Copies of changed values
Some values are kept in step by hand: a list of country codes, a map of time zones, the states of an order, copied into another project "in sync" with the first. When the change modifies such a group of values, the rest of the repository is searched for the copy it left behind. A group is a run of consecutive lines holding literal values: quoted strings without interpolation, the words of an Elixir word sigil (~w(CL PE)), atoms (:pending matches "pending") and numbers; values of one character, punctuation and common words (true, nil, ok, id...) are left out. The change's group is read as it is after the change, so every line of it counts, not only the lines touched; the values the change added and took out tell what it was before.
A group of at least 3 values before the change is looked for. A group elsewhere, outside the change and the documents, is a copy when it holds at least 60% of those values (3 at least) and still lacks a value the change added or holds one it took out. A comment within the three lines above it that says it mirrors the changed file (mirrors, copy of, keep in sync, same as...) and names it (the file's stem or a module it defines) puts it first: says it mirrors it.
Copies of changed values — groups outside the change that hold the old ones
──────────────────────────────────────────────────────────────────────────────
apps/billing/lib/codes.ex:3 4 of 5 values of apps/geo/lib/countries.ex:2, says it mirrors it
lacks US
──────────────────────────────────────────────────────────────────────────────
Matched by their values alone: a hint, not a proof.
The rest of the repository is read only when the change modifies a group large enough. Values built at run time, or kept under other names ("Chile" for "CL"), are not seen.
HTTP level
With --http, km impact follows the change across services that talk over HTTP, where neither manifests nor calls relate them: one project serves a route, another calls it with a client. It is heuristic, and the report says so.
- Routes are read from Phoenix routers, with the path and alias of every
scope, the verb (liveis aget,resourcesthe seven routes it declares) and the action behind each. A route is removed when the router no longer declares it, changed when its line was rewritten or the module or action behind it differ, and action when the change affects the controller action that serves it (the functions the structural radius narrowed it to; every action when the file could not be narrowed). - Client calls are read from every file of the repository outside tests: Tesla (
Tesla.get, or a baregetin a module thatuse Tesla), Req (url:or the first literal), Finch (Finch.build(:get, url)) and HTTPoison in Elixir,fetchandaxiosin JavaScript and TypeScript,requestsandhttpxin Python. The path is the literal the call is given: a scheme and host are dropped, an interpolation or a{placeholder}is a parameter, and so is a base URL written at run time ("#{base}/v1/items"asks for/v1/items). - A call asks for a route when its verb fits (or it tells none) and its path matches the end of the route's, segment by segment, a parameter on either side matching any one: the rest of the route may be in the base URL of the client, which configuration holds. A call of a single segment must match the whole route. Each caller says its project, whether it asks for the whole path, and whether a test is named after its file.
- Calls whose path is built at run time (
Req.get!(url: path)) cannot be matched: they are counted as unknown, not dropped.
HTTP level — client calls to the routes the change removes or changes
──────────────────────────────────────────────────────────────────────────────
removed GET /api/v1/items ShopWeb.ItemController.index (apps/shop/lib/shop_web/router.ex:5)
apps/billing/lib/shop.ex:2 Req, billing, no test
action GET /api/v1/items/:id ShopWeb.ItemController.show (apps/shop/lib/shop_web/router.ex:5)
apps/billing/lib/shop.ex:3 Req, billing, asks for /v1/items/:param, no test
──────────────────────────────────────────────────────────────────────────────
Matched by method and the end of the path, over 3 client calls: a heuristic.
Unknown: 1 calls build their path at run time: apps/billing/lib/shop.ex:4
Without --http nothing of this is read, and the JSON gives http: null.
JSON output, for tools and LLMs
--format json carries everything the table shows, and the lists the table cuts short. An agent reviewing a change can read it in this order:
km impact --since-ref origin/main --format json
| Field | Meaning |
|---|---|
source |
The change measured: diff against main, PR #12, patch from stdin |
structural.functions |
Functions the change affects (public ones in Elixir, top-level names in Python), over the files that tell them; null when none could be narrowed |
structural.narrowing[] |
Per changed file: functions, or null with the reason every use of it counts; attributes (name, read_by) when touched module attributes brought functions in |
structural.direct[] |
Every file that uses a changed file: file, exposure (calls, refers, elsewhere), protection (direct, named, users, none), calls, tests, tests_in_diff, entry_point |
structural.unprotected[] |
Files that call what changed and that no test reaches, not even through what uses them — where an integration test is missing |
structural.untested_entry_points[] |
Command-line tasks and scripts that call what changed and have no test; not counted as unprotected |
structural.unknown_without_tests[] |
Files that use the changed module without a call that tells, and have no test |
structural.change_tests_a_dependent |
Whether a test in the change protects a file that uses what changed |
structural.radius |
files, source_files and share (0 to 1): the radius as a number |
structural.reach[] |
The files of the radius, by distance |
structural.cut_at |
The distance --max-distance stopped the radius at, when files lay beyond it; null otherwise |
structural.upper_bound |
Files reached if every use of a changed file counted, whatever the function |
structural.removed[] |
What the change removes, file and function (null when the file is deleted), with the callers that still use it: file, calls, protection |
projects.changed[], projects.reached[] |
Projects holding a changed file, and those reached, each with origin, distance, via, scope |
projects.affected[] |
Projects whose builds and tests the change calls for (what --affected prints) |
projects.outside[] |
Changed files that belong to no project: their reach is unknown |
projects.inert[] |
Changed files that reach nothing: documentation, and what .kimun.toml declares inert |
diffusion |
Files, directories, subsystems, lines and entropy of the change |
logical_radius.missing[] |
Files that usually change with the change and are not in it, with every trigger; documents go to documentation |
documentation.stale[] |
Documents left behind: document, signal (declared, referenced, co_changed), describes[], and covers, line and gone, or shared_commits, commits and confidence |
copies.copies[] |
Groups of values that look like a copy the change left behind (copies.heuristic is always true): file, line, of, of_line, matched, missing (added by the change, absent here), kept (taken out by the change, still here), mentions |
http |
With --http: routes[] (verb, path, router, line, change: removed, changed or action, serves, callers[] with file, line, client, path, project, exact, tested), unresolved[] (calls whose path is built at run time), calls, heuristic; null otherwise |
km ai exposes the command to an LLM as the tool km_impact, and the skill installed by km ai skill documents it.
Shared reads as shared commits over the commits of the changed file.
(+1 more) means another changed file predicts the same missing file; --format json lists every one. --format terse prints the number of missing files.
Note: File renames are not tracked across git history. A file renamed in the diff itself keeps the history of its old path.
km churn -- Code churn analysis
Measures pure change frequency per file from git history (commit count only, no complexity weight). Identifies the most frequently modified files — high churn without a corresponding quality improvement is a maintenance signal.
The rate is commits per month from the file's first commit to now, or from the start of the --since window to now, at least one month. A burst of commits long ago is spread over the time since, so it does not outrank a file that keeps changing. HIGH is above 4 a month, MEDIUM from 1, LOW below.
km churn [path]
Options:
| Flag | Description |
|---|---|
--top N |
Rows of the table (default: 20); JSON lists every file unless --top is given |
--sort-by METRIC |
Sort by commits (default), rate (commits/month), or file |
--since DURATION |
Only consider commits since this time (e.g. 6m, 1y, 30d) |
--format {table,json,short,terse} |
Output format (default: table) |
Example output:
Code Churn — Change Frequency
──────────────────────────────────────────────────────────────────────────────
File Language Commits Rate/mo First Seen Last Seen
──────────────────────────────────────────────────────────────────────────────
src/main.rs Rust 18 3.2 2025-01-10 2026-03-28
src/loc/counter.rs Rust 7 1.3 2025-01-10 2026-02-14
src/dups/detector.rs Rust 7 1.2 2025-02-01 2026-02-20
──────────────────────────────────────────────────────────────────────────────
km smells -- Code smell detection
Detects common code quality issues per file using text-based heuristics (no AST required). Only languages with complexity marker support are analyzed (same set as km cycom: Rust, Python, JS/TS, C/C++, Go, etc.).
km smells [path]
Smell types
| Smell | Description |
|---|---|
long_function |
Function body exceeds --max-lines (default: 50) |
long_params |
Function has more than --max-params parameters (default: 4) |
todo_debt |
TODO, FIXME, HACK, XXX, or BUG in comment lines |
magic_number |
Bare numeric literals in code, other than 0, 1, 2, -1 and values that already have a name |
commented_code |
Two or more consecutive comment lines containing code-like patterns, outside documentation |
long_paramscounts the parameters of the list that follows the function's name (a Go receiver, or the call a function expression is passed to, is not it), separated by its top-level commas: commas inside type arguments (Node<K, V>), patterns (%{...},<<...>>), literals and nested calls separate nothing.magic_numberleaves out declarations, with any modifiers before the keyword (private static final,public const), assignments to upper-case names (MAX_RETRIES = 5), and the data a declaration opens: the members of anenum, a Goconst (...)block, the elements of a table, the alternatives of atypeunion. A literal that is the value of a key (connect: 3,"MAX": 4093) or a parameter default is named too. The body of a function declared with such a keyword (export function,static int f() {) is still read.commented_codeleaves out documentation, where code is an example:///and//!lines,/** */and/*! */blocks (JSDoc, Javadoc, Rust doctests, C# XML doc), and a Go doc comment with a tab-indented example right above a declaration.
Options:
| Flag | Description |
|---|---|
--top N |
Rows of the table (default: 20); JSON lists every file unless --top is given |
--max-lines N |
Maximum function body lines before flagging (default: 50) |
--max-params N |
Maximum parameter count before flagging (default: 4) |
--files FILE |
Analyze only these specific files (repeatable). Useful for scripting |
--since-ref REF |
Analyze only files changed since this git ref (e.g. origin/main, HEAD~1). Ideal for CI |
--format {table,json,short,terse,github,codeclimate} |
Output format (default: table). github emits GitHub Actions annotations; codeclimate (alias: gitlab) emits CodeClimate JSON for GitLab Code Quality |
The table breaks each file's smell count down by type, with one column per smell kind (magic, long, param, todo, comm) and a per-column total in the footer.
Example output:
Code Smells
──────────────────────────────────────────────────────────────────────
File Total magic long param todo comm
──────────────────────────────────────────────────────────────────────
src/loc/counter.rs 12 7 1 0 4 0
src/main.rs 6 2 0 0 4 0
src/dups/detector.rs 3 0 2 1 0 0
──────────────────────────────────────────────────────────────────────
Total (3 files) 21 9 3 1 8 0
km deps -- Dependency graph analysis
Analyzes internal module dependencies by parsing import/use/require statements. Builds a directed graph of file-level coupling and detects cycles using Tarjan's SCC algorithm.
km deps [path]
Supports Elixir (every module referred to in the code, with alias undone; see the notes below), Rust (every path that names a module of the workspace: use, crate::, super::, a child module; see the notes below), Python (import and from … import, absolute and relative; see the notes below), JavaScript/TypeScript (import, export … from, require and import(), relative or through the aliases of tsconfig.json and the packages of the workspace; see the notes below), Go (imports of the packages of the repository, by the module path of each go.mod; see the notes below), C and C++ (#include, as a compiler finds it; see the notes below), Java (the classes a file names, through its package and its imports; see the notes below), C# (the types a file names, through its namespaces and its using directives; see the notes below), Ruby (require_relative, require against the load path, and the constants a file names, as Rails autoloads them; see the notes below), and Kaikai (import a.b.c, including the as and .{…} forms). External dependencies (crates, npm packages, system headers, the Kaikai stdlib) are ignored. With --kind infra, Terraform enters too: a file that calls a local module (source = "../modules/network") uses every .tf file of that directory, as a Go import uses a package, and a local source that leads to no module is unresolved.
Files in any other language are left out of the graph instead of being listed with zero dependencies. The table footer, the unsupported array of the JSON output and the unsupported:N field of the short format say how many files were skipped, so "not measured" is never shown as "no dependencies".
Some imports do not load a module when the importer loads: a TypeScript import type (and import { type A } when every name is a type, and export type … from) and a Python import under if TYPE_CHECKING: take only types, and a Python import inside a function or a dynamic import() runs later. They stay in the graph, marked type or deferred, and count in fan-in and fan-out, since they are still coupling, but they are left out of cycle detection: a cycle is a set of files that need each other to load. The table footer counts them (Imports left out of cycles: …), and the JSON gives type_fan_out and deferred_fan_out per file, which are part of fan_out, and type_edges and deferred_edges in total.
An import that has the form of a project file but resolves to none is reported, so that a missing edge is not read as no dependency: a quoted #include that is no header of the system (often a file generated at build time), a relative JavaScript or TypeScript specifier of a script, a relative Python import, a Go import within a module of the repository. External packages are never listed. The table footer counts them (Unresolved internal imports: N in M files), the JSON lists them per file in unresolved and counts them in unresolved_count, and the short format gives unresolved:N. Rust, Elixir, Java, C# and Kaikai name modules and types rather than files, where nothing tells an internal name from an external one, so they report none.
The JSON lists the edges in edges, each as {"from", "to", "kind"} with kind one of load, compile, deferred or type: those that leave the files listed, so --top and --cycles-only narrow them as they narrow files.
Elixir notes:
- Dependencies are between modules, and most need no import, so every module name in the code counts as a reference: calls, structs,
use,import,require,@behaviour,defimpl. Comments, strings, heredocs and sigils are set aside first, so a doctest is not a dependency. aliasis undone, includingas:, grouped and multi-line forms, and__MODULE__. The statement itself is not a use.- Nested
defmodules are named after the module that contains them, told by indentation asmix formatleaves it. - A module defined in several files (projects made from one template) resolves to the file nearest the one referring to it.
- Comments and literals hold no references, but the code a string interpolates does:
"Total: #{Orders.total(order)}"usesOrders. - A Phoenix controller uses the views named after it (
PageControllerandPageJSON,PageHTML,PageView), and a module uses the components its~Htemplates render. mix.exs,.formatter.exsand the scripts of aconfigdirectory configure the build: they stay in the graph with no dependencies, and the modules they name (groups_for_modules,config :app, App.Endpoint) or define (App.MixProject) make no edges.use Foo, :namerunsFoo.nameanduse FoorunsFoo.__using__. WhenFoois a module of the project, the code of that function, and of the functions it splices in withunquote(helper()), is read as written in the user: its aliases, imports and references are the user's. A live view that doesuse MyAppWeb, :live_viewusesMyAppWeb.CoreComponentsand theLayoutsit names, as the compiler sees it. The quote is read whole, whichever branch the macro takes.import Foo, only: [bar: 1]callsFoo.bar. Withoutonly:, a call with no module (bar(x),&bar/1,<.bar>in a template) that the file does not define calls the function of that name of each module of the project it imports.- A use the file needs to compile is marked
compile: ause, and a call to a macro (defmacro,defguard) of the project, as the compiler expands it into the caller. What aquoteblock holds is compiled where the macro is called, so it makes nocompileuse of the file that writes it; what auseinjects is compiled in the user. Animportorrequirealone recompiles the file only when the exports of the module change (mix xrefcalls thatexport), so it is a plain use. Acompileuse counts in cycles; the table footer counts them (Compile-time uses …), and the JSON givescompile_fan_outper file andcompile_edgesin total. A function called in the body of a module, outside any function (plug Plug.Static, only: MyAppWeb.static_paths()), also runs at compile time, and is not told. - Not seen: modules named at run time (
apply/3, configuration), those generated by macros, and those a Phoenix router names under the alias of ascope. Functions are told by name, not by arity.
Rust notes:
- A dependency is a path:
use crate::git::GitRepo, a qualifiedsuper::analyzer::run(x), a call on a child module (report::print(x)). Groups,as,self, globs and multi-lineuseare read.mod x;only says where a module lives and adds no edge. - Each crate is a tree of modules grown from its root (
src/lib.rs,src/main.rs, a file ofsrc/bin,tests,examplesorbenches,build.rs) by itsmoddeclarations,#[path]included. A path leads to the file of the deepest module it names: the item may be defined there or only re-exported. A module declared twice under opposite#[cfg]s, with a#[path]each, leads to both files. - A crate re-exported under a name (
pub extern crate grep_printer as printer,pub use grep_printer as printer) is followed:grep::printer::Statsleads to the library ofgrep-printer, and the facade uses it. - Other targets of a package reach its library by name (
my_app::ordersfromtests/ormain.rs), and so do the other crates of the workspace. The name comes from[package] name, with-read as_. - A type uses the files that hold its
implblocks, since what they define is reached through the type. - For
km impact, a file that holds#[test]functions has a test of its own; a file oftests/that holds them, or atests.rsmodule, is a test. What a package runs (src/main.rs,src/bin,examples,benches,build.rs) is an entry point. - Comments, strings and
$cratein a macro name nothing. - Not seen: what a macro generates or names,
include!, a library with a[lib] pathornameof its own, a dependency renamed inCargo.toml, and modules behindcfg, which all count. A use only from an inline test module still makes the file a dependent. A test that runs the binary (assert_cmd,CARGO_BIN_EXE_*) names no file, so it protects none. Two packages of the same name in one repository are not told apart.
Python notes:
- Every
import a.bandfrom a.b import cis read, wherever it is written: in a function, underif TYPE_CHECKING:, over several lines in parentheses or with a backslash. One written in a docstring or a comment is not an import. Where it is written is read by indentation: underif TYPE_CHECKING:(orif typing.TYPE_CHECKING:) it istype, in a function (def,async def, a method) it isdeferred, anywhere else it loads, a class body and theelse:ofif TYPE_CHECKING:included. Neither of the first two makes a cycle. - A dotted path names
a/b.pyor the packagea/b/__init__.py; a stub (.pyi) stands for a module with no source. Only the deepest module is used:import a.b.cadds no edge toa/__init__.py. - In
from a.b import c,cis the submodulea/b/c.pywhen that file exists, and a namea.bdefines otherwise. - A relative import starts at the package of the importing file, one level up per extra dot.
- An absolute import is looked up from each directory above the importing file that is not itself a package (it holds no
__init__.py), nearest first, and from thesrcdirectory under each. That covers a project run from its root, asrclayout withtests/beside it, and several projects in one repository. Then come the roots the nearestpyproject.tomlabove declares: where the build backend finds the packages (whereandpackage-dirof setuptools,packagesof Poetry,sourcesandpackagesof Hatch,python-sourceof maturin,package-dirof PDM,module-rootof uv) and what pytest adds (pythonpath). What is found under none of them is external: the standard library, installed packages. - For
km impact,test_*.py,*_test.pyandtests.pyare tests, andconftest.pyis test support, which protects nothing;__main__.py,setup.pyandmanage.pyare entry points, and so is what sits undermanagement/commands. - Not seen: modules named at run time (
importlib,__import__, Django settings andINSTALLED_APPS), roots added at run time (sys.path,PYTHONPATH, a.pthfile) or declared insetup.py,setup.cfgorpytest.ini, and a namespace package spread over several roots. A name a package re-exports leads to its__init__.py, and from there to the module that defines it.
Go notes:
- An import names a package, and a package is a directory: the import depends on every
.gofile directly inside it, not on the ones in its subdirectories, which are other packages. Which file holds what was used is not read, so fan-out counts files: one import of a package of 17 files adds 17. - Imports are read from the code: one written in a comment or a string, such as the example program of a package comment, is not an import. A package cannot import itself, so an import of the file's own directory adds no edge, unless the file is an external test package.
- Test files (
_test.go) are not part of what a package offers, and neither are the files the toolchain ignores (_x.go,.x.go). A test in an external test package (package store_test) that imports its own package depends on the files beside it. - Every
go.modunder the analysed directory is read, so the modules of a repository with several resolve each from its own directory, with or without ago.work. An import of the module path itself is the package at the root of the module. - The files of one package use each other without importing, so there are no edges between them. The import graph is therefore a lower bound on the real dependencies.
- Not seen: build constraints (
//go:build, file names such asx_linux.go), so a file left out of a build still counts, and packages undervendor/. - An import under the path of a module of the repository that names no directory with a package is reported as unresolved.
JavaScript/TypeScript notes:
- Only the import forms are read:
import … from, a bareimport '…',export … from,require('…'),import('…')andimport x = require('…'), several to a line or over several lines. Any other string is no import, so a path given tofs.readFileorpath.resolveis not a dependency. - A relative specifier resolves from the importing file, trying the extensions of JavaScript and TypeScript and the
indexof a directory. A.jsthat does not exist names its TypeScript source (./a.jsisa.ts), and a query such as Vite's?rawnames the same file. - Any other specifier goes first through the
pathsof the nearesttsconfig.jsonorjsconfig.jsonabove the importing file, the most specific pattern winning, then through itsbaseUrl.extendsis followed within the repository, and a directory whose main config has no aliases takes those of the others beside it (tsconfig.app.json). Comments and trailing commas are allowed. - Then a specifier may name a package of the repository, by the
nameof itspackage.json:@acme/uileads to its entry point (source,module,main,types,exports, read fromsrcwhen they point to a build directory, or elsesrc/index), and@acme/ui/buttonto that file of the package or of itssrc. Packages undernode_modulesare not read. What resolves to none of these is external. - A barrel (
index.ts) is a file like any other: its importers depend on it, and it on what it re-exports. import type,export type … from, and an import or re-export whose names are all markedtype(import { type A, type B }) take only types: the compiler erases them, so they aretypeand make no cycle. One name that is a value, or a default import beside them, makes the import load.import()isdeferred: the module loads when the call runs.- A relative specifier that resolves to no file is reported as unresolved when it names a script: no extension or one of JavaScript or TypeScript. A stylesheet or an image is no file of the graph.
- Not seen: the aliases of a bundler config (
resolve.aliasof Vite or webpack),importsofpackage.json(#internal), specifiers built at run time, and.vueand.sveltefiles.
C and C++ notes:
- The include directories of the build (
-I, CMake'sinclude_directories) are not read. A quoted#include "a/b.h"is looked for beside the includer first; then, quoted or not, in each directory above it and in theincludedirectory of each; then wherever a file of the repository ends with that path, when one is nearest to the includer. #include <stdio.h>and any other bare name in angle brackets that is in none of those directories is a system header: no edge, even if the repository holds a file of that name somewhere else.- A file that includes a header uses what its source defines too:
foo.hand thefoo.cbeside it, orinclude/x/foo.handsrc/x/foo.c.km depsshows only the includes the code writes, so a source and a header that include each other's counterpart make no cycle;km impactfollows both, so a change tofoo.creaches whoever includesfoo.h. - Preprocessor conditions (
#ifdef) are not evaluated: every include counts. - A quoted include that resolves to no file of the repository is reported as unresolved, unless it names a header of the C library or of POSIX (
"stdio.h","sys/types.h"), which a quoted include may also reach. An include in angle brackets is never reported.
Java notes:
- A class is known by its package and its name, and lives in the file of its top-level class, wherever the source root is (
src/main/java,src, a module of a multi-module build). - A file uses the classes it names: those of its own package and of each import on demand (
import a.b.*) by their simple name, each class it imports by itself, and any class written with its package (a.b.C). A nested class (a.b.Outer.Inner) and a static import lead to the file of the outer class. An import is a use even when nothing names the class. - When several files declare the same class (two flavors of one library, a generated copy), the one nearest to the file that names it is taken.
- The JDK and libraries declare no file of the repository: their classes are external.
- A field, a variable or a method that has a class's name is not told from it.
C# notes:
- A
usingnames a namespace, not a file. A type is known by its namespace (block-scoped or file-scoped) and its name, and a file uses the types it names. - A file sees by their simple name the types of each namespace it is in and of those around it (
App.WebseesApp), of the global namespace, of eachusing, and of eachglobal usingwritten in its directory or above it. An alias (using M = App.Models;) names a type or a namespace,using staticuses the type it names, and[Audited]names the classAuditedAttribute. - A partial type is declared by each of its files, and a use of it uses them all. When two projects declare a type of the same name, the one nearest to the file that names it is taken.
- Types of .NET and of NuGet packages declare no file of the repository: external. The implicit usings of the SDK bring nothing of the repository.
- An extension method (
thisbefore the first parameter, in a static class) is called on a value without naming its class. A call after a dot (t.IsValueType()) uses the file that declares an extension method of that name, taking as many arguments, in a namespace the caller sees. The receiver's type is not known: a call several files could answer is ambiguous and adds no edge, and an instance method that shares the name and the arity of a visible extension is taken for it. Private extension methods are left out. - A field, a variable or a method that has a type's name is not told from it.
Ruby notes:
require_relativeresolves from the requiring file, and so does a path built from__dir__or__FILE__(File.expand_path('../x', __FILE__),File.join(__dir__, 'x')).require,loadandautoloadlook the path up under thelib,testandspecdirectories of the repository and the autoload roots of Rails (app/*,app/*/concerns), the nearest first; a path found nowhere is a gem or the standard library.- Most Rails code has no
require: a file uses the files that define the constants it names. Constants are always looked up, in any project, since once loaded a constant is global whatever loaded it (require, Zeitwerk, Bundler). A reference is looked up whole from the namespaces it is written in, innermost first (Iteminsidemodule ShopisShop::ItembeforeItem). - Classes and modules nest by indentation, as formatted code has them. A module opened only to hold other definitions is a namespace, not a definition:
module Shoparoundclass Cartdoes not make every reference toShopa use of that file. A constant assigned in a class or module (VERSION = "1",Point = Struct.new(:x)) is a definition. - A constant defined in several files (a reopened class): the file named after it as Zeitwerk names files (
Shop::LineIteminshop/line_item.rb) is the definition. When none is, the nearest definitions to the reference count, all of them when they sit in one directory; spread over directories with none nearer, the reference is ambiguous and left out (km impactcounts it). A constant no file defines is found by its path under the autoload roots. - Shipping code never uses what a test file or a test directory defines; tests see both.
- The core classes of Ruby (
String,Hash,Object…) are never a use, even when a file of the repository reopens them. - Comments, strings, heredocs, regular expressions and
%literals name nothing; the code a string interpolates does. - Not seen:
const_get,sendand other metaprogramming; methods added by reopening a class in a file not named after it, when a reference resolves to the file named after the class; ERB and other templates; custom autoload paths (config.autoload_paths) and collapsed directories; request specs that reach a controller through its route.
Kaikai notes:
import a.b.cnamesa/b/c.kairelative to a package root, not to the importing file. Each ancestor directory of the importing file is tried, nearest first, so runkm depson a directory that contains the package root.- When no such file exists,
import acan name a package directorya/that carries akai.toml, which the compiler searches: the import depends ona/a.kai, or, when the package has no file of its name, on every.kaifile directly inside it. - An import that resolves to no analysed file is external and adds no edge.
- Several statements on one line are separated by
;, soimport a; import breads both; what follows a#is a comment, andimport ?name(a dependency hole) names no module. - Each file is a module of its own. The files of a package do not share names: they import each other like any other module (
import store,import notes.store), so their only edges are their imports, and a change to one reaches the files of its package that import it. Unlike Go, where the files of a package see each other without importing and an import reaches them all.
| Flag | Description |
|---|---|
--format {table,json,short,terse} |
Output format (default: table) |
--cycles-only |
Show only files that participate in a dependency cycle |
--sort-by METRIC |
Sort by fan-out (default) or fan-in |
--top N |
Rows of the table (default: 20); JSON lists every file unless --top is given |
--matrix |
Print the dependency structure matrix instead of the table |
--depth N |
Levels of directories the matrix shows expanded (default: 1) |
--focus DIR |
Expand this directory of the matrix, and the ones above it, alone |
--html FILE |
Write the matrix as an interactive HTML page to this file |
Example output:
Dependency Graph
────────────────────────────────────────────────────────────────────────
File Language Fan-In Fan-Out Cycle
────────────────────────────────────────────────────────────────────────
report_helpers.rs Rust 26 1 no
util.rs Rust 25 2 yes
walk.rs Rust 24 1 no
────────────────────────────────────────────────────────────────────────
Dependency cycles: 14
Cycle 1 (3 files):
cogcom/analyzer.rs
cogcom/detection.rs
cogcom/report.rs
Dependency structure matrix:
--matrix shows which file depends on which as a dependency structure matrix (DSM), as IntelliJ IDEA draws it. Rows and columns are the same directories and files, in the same order; a column carries only the number of its row, so the matrix stays narrow. A cell counts the files of its column that use files of its row. A directory shown collapsed (▸) counts for every file under it; one shown expanded (▾) heads the rows of what it holds. The root is the deepest directory holding every file, and a directory that holds only another one is shown with it (lib/core/).
The children of each directory are ordered so that a sibling comes before the siblings it uses: in a layered system every mark falls below the diagonal. Siblings that use each other form a cycle and stay together, those that use more of it than it uses them first; the rest keep the order of their names. Only uses that load or compile order the rows: type and deferred ones, which make no cycle, are drawn dimmed.
A mark above the diagonal goes against the layering. The list under the matrix gives each one with the uses behind it, five at most, and why it is there: in a cycle between files (! after the count, red on a terminal), against the layering when only the directories form a cycle (yellow), or type-only or deferred (dimmed). There are no colours when the output is not a terminal or NO_COLOR is set.
--depth N expands the directories less than N levels below the root; --focus DIR expands that directory alone and the ones above it, with the path written as km shows it. --cycles-only keeps the files in cycles. --top does not apply to the matrix. With --format json it is given for tools and models: rows in their order (number, path, a directory ending in /, level), groups with the directories shown expanded, cells with row, column, count, mark (cycle, layering, weak or plain) and the uses behind each (from, to, kind, cycle), and above_diagonal as [row, column] pairs. A matrix wider than the terminal is not printed: a lower --depth or --focus narrows it.
--html FILE writes the matrix as one HTML file that needs nothing else, in light and dark themes. It opens as the text would show, and in it a directory expands and collapses with a click on its triangle (or all at once), a click on a name marks what it uses and what uses it in two colours, and a click on a cell lists the uses behind it. The headers stay in place when the matrix scrolls, a cell is shaded by its count, and a name shows its whole path on hover. The terminal width does not limit it.
$ km deps --matrix --focus shop/core
Dependency Structure Matrix
A cell counts the files of its column that use files of its row; ! marks a cycle.
1 2 3 4 5
1 main.py ·
2 ▸ billing/ 1 ·
3 ▸ web/ 1 1 ·
▾ core/
4 db.py 1 · 1!
5 models.py 1 2 1! ·
Above the diagonal: 1 cell
shop/core/models.py → shop/core/db.py: 1, in a cycle
shop/core/models.py → shop/core/db.py
km authors -- Per-author ownership summary
Summarizes code ownership across the project by author. Aggregates git blame data to answer "who knows what?" at the team level — complementing km knowledge (per-file view) with a team-level view. Blame is read as km knowledge reads it (see Who wrote each line). "Last Active" is the date of the author's last commit, whatever it touched.
km authors [path]
Options:
| Flag | Description |
|---|---|
--since DURATION |
Only consider activity since this time (e.g. 6m, 1y, 30d) |
--format {table,json,short,terse} |
Output format (default: table) |
Example output:
──────────────────────────────────────────────────────────────────────
Author Owned Lines Languages Last Active
──────────────────────────────────────────────────────────────────────
E. Diaz 38 8432 Rust, TOML 2026-03-15
R. Ramirez 4 312 Rust 2026-02-10
──────────────────────────────────────────────────────────────────────
km age -- File age analysis
Classifies source files as Active, Stale, or Frozen based on how long ago they were last modified in git history. Helps identify neglected or abandoned code.
km age [path]
Status classification
| Status | Condition | Meaning |
|---|---|---|
| ACTIVE | Modified within --active-days days (default: 90) |
Regularly touched |
| STALE | Between --active-days and --frozen-days (default: 365) |
Neglected |
| FROZEN | Not modified for more than --frozen-days days |
Potentially abandoned |
Options:
| Flag | Description |
|---|---|
--active-days N |
Days threshold for Active status (default: 90) |
--frozen-days N |
Days threshold for Frozen status (default: 365) |
--sort-by METRIC |
Sort by date (oldest first, default), status, or file |
--status FILTER |
Show only files with this status: active, stale, or frozen |
--format {table,json,short,terse} |
Output format (default: table) |
Example output:
──────────────────────────────────────────────────────────────────────────────
File Language Last Modified Days Status
──────────────────────────────────────────────────────────────────────────────
src/legacy/parser.rs Rust 2023-01-15 840 FROZEN
src/util.rs Rust 2024-09-20 197 STALE
src/main.rs Rust 2026-03-01 34 ACTIVE
──────────────────────────────────────────────────────────────────────────────
ACTIVE 12 (modified < 90 days)
STALE 8 (90 days – 365 days)
FROZEN 3 (not modified > 365 days)
km score -- Code health score
Computes an overall code health score for the project, grading it from A++ (exceptional) to F-- (severe issues). Uses only static metrics (no git required).
There are three scoring models: cogcom, the default, built on Cognitive Complexity (5 dimensions); cogcom-0.35, the same dimensions with the weights it had up to v0.35; and legacy, built on the Maintainability Index and Cyclomatic Complexity (6 dimensions).
Non-code files (Markdown, TOML, JSON, etc.) are automatically excluded. Inline test blocks (#[cfg(test)]) are excluded from every dimension, see Inline tests.
km score [path]
km score --model cogcom-0.35 [path] # the weights up to v0.35
km score --model legacy [path] # MI + cyclomatic model
Dimensions and weights (default: cogcom)
| Dimension | Weight | What it measures |
|---|---|---|
| Cognitive Complexity | 32% | SonarSource method, penalizes nesting |
| Duplication | 21% | Project-wide duplicate code % |
| Indentation Complexity | 16% | Stddev of indentation depth |
| Halstead Effort | 21% | Mental effort per LOC |
| File Size | 10% | Full score up to 500 LOC, falling to 0 at 4,000 |
Up to v0.35 File Size weighed 15%, and the others 30%, 20%, 15% and 20%. It weighs less now because size already drives the other dimensions: a large file has more code to be complex, and the project score is a mean weighted by lines of code. Studies of modules find that defects grow less than proportionally with size, so splitting a file does not by itself make it less fault-prone; what size measures well is the cost of reading and changing it. The five points went to the other dimensions in proportion to their weights. --model cogcom-0.35 scores with the former weights, to compare with earlier results or to keep a CI gate where it was.
Experimental model (--experimental)
km score --experimental scores with the Maintainability Index (Visual Studio variant, 0–100) in place of Halstead Effort, at its weight (21%), and prints the score of the default model next to it, from the same measurement. The MI weighs Halstead volume, cyclomatic complexity and size together; a file scores 0 at MI 0, 40 at 5, 60 at 10, 80 at 20 (where Visual Studio turns green), 90 at 30, 97 at 45 and 100 from 60. Unlike the other dimensions, the MI of the project is the plain mean of its files, not weighted by lines: the MI already falls with the size of a file, and weighting it would count size twice. It is for trying the dimension out, not for gates: it cannot be combined with --model or --trend.
On the projects it was tried on, the experimental score stays within about 1.5 points of the default one, in the same order: the MI follows Halstead closely, from which it is largely computed. A project whose worst code sits in a few large files among many small ones scores higher than with the default model (jq: F 51.3 against D 60.5), since each file counts once.
Experimental: Maintainability Index (Visual Studio) in place of Halstead Effort.
Default model: B+ (83.2) → experimental: B (81.8), -1.5
--format json adds a baseline object with the score and grade of the default model; --format short adds a baseline line.
Dimensions and weights (--model legacy)
| Dimension | Weight | What it measures |
|---|---|---|
| Maintainability Index | 30% | Verifysoft MI, normalized to 0-100 |
| Cyclomatic Complexity | 20% | Max complexity per file |
| Duplication | 15% | Project-wide duplicate code % |
| Indentation Complexity | 15% | Stddev of indentation depth |
| Halstead Effort | 15% | Mental effort per LOC |
| File Size | 5% | Full score up to 500 LOC, falling to 0 at 4,000 |
Each dimension is aggregated as a LOC-weighted mean across all files (except Duplication which is a single project-level value). The project score is the weighted sum of all dimension scores.
Grade scale
| Grade | Score range | Grade | Score range |
|---|---|---|---|
| A++ | 97-100 | C+ | 73-76 |
| A+ | 93-96 | C | 70-72 |
| A | 90-92 | C- | 67-69 |
| A- | 87-89 | D+ | 63-66 |
| B+ | 83-86 | D | 60-62 |
| B | 80-82 | D- | 57-59 |
| B- | 77-79 | F | 50-56 |
| F- | 40-49 | ||
| F-- | 0-39 |
Options:
| Flag | Description |
|---|---|
--model MODEL |
Scoring model: cogcom (default), cogcom-0.35 (the weights up to v0.35) or legacy (MI + cyclomatic) |
--experimental |
Score with the Maintainability Index in place of Halstead Effort, against the default model |
--trend [REF] |
Compare current score against a git ref (default: HEAD). Shows change: B- → B (+2.3). Useful for PR review: --trend origin/main |
--fail-if-worse |
With --trend: exit with code 1 if the score dropped by more than --gate-tolerance |
--gate-tolerance POINTS |
Score drop --fail-if-worse allows before failing (default: 0.01 with --gate-scope project, 0.5 with --gate-scope changed). Compares unrounded scores |
--gate-scope {project,changed} |
What --fail-if-worse compares (default: project, the aggregate score). changed looks only at the files the diff touches: it fails if a modified or renamed file ends below the project score at the ref after dropping more than --gate-tolerance, or if the project's duplicated lines grow. Files above the project score, new files and deleted files never fail it, so removing healthy code cannot lower the verdict. The report lists every changed file with its before/after score |
--fail-below GRADE |
With --trend: exit with code 1 if the score is below GRADE (e.g. B-). Overridable via .kimun.toml |
--badge FILE |
Write a badge of the score: an SVG, or the JSON of a shields.io endpoint badge when FILE ends in .json (see Badge) |
--format {table,json,short,terse} |
Output format (default: table) |
--include-tests |
Include tests, examples and benchmarks in analysis (excluded by default) |
--bottom N |
Number of worst files to show in "needs attention" (default: 10) |
--min-lines N |
Minimum lines for a duplicate block (default: 6) |
Example output:
Code Health Score
──────────────────────────────────────────────────────────────────
Project Score: A (91.2)
Files Analyzed: 142
Total LOC: 19,084
──────────────────────────────────────────────────────────────────
Dimension Weight Score Grade
──────────────────────────────────────────────────────────────────
Cognitive Complexity 32% 84.1 B+
Duplication 21% 100.0 A++
Indentation Complexity 16% 93.2 A+
Halstead Effort 21% 87.5 A-
File Size 10% 100.0 A++
──────────────────────────────────────────────────────────────────
Files Needing Attention (worst per-file scores, excl. duplication)
──────────────────────────────────────────────────────────────────
Score Grade File Issues
──────────────────────────────────────────────────────────────────
57.7 D- ./src/deps/analyzer.rs Cognitive: 36
61.0 D ./src/cogcom/analyzer.rs Cognitive: 35
70.3 C ./src/smells/rules.rs Cognitive: 20
71.6 C ./src/hal/tokenizer.rs Cognitive: 22
71.8 C ./src/loc/mod.rs Cognitive: 19
72.8 C ./src/knowledge/mod.rs Cognitive: 21
73.8 C+ ./src/dups/mod.rs Cognitive: 19
75.0 C+ ./src/cli_help.rs Cognitive: 17
75.6 C+ ./src/test_blocks.rs Cognitive: 17
77.3 B- ./src/walk.rs Cognitive: 16
──────────────────────────────────────────────────────────────────
Badge
--badge FILE writes a badge of the score for a README, besides the report: a self-contained SVG in the flat style of shields.io, coloured by the letter of the grade (A green, B light green, C yellow, D orange, F red), or, when FILE ends in .json, the JSON of a shields.io endpoint badge.
km score --badge .github/km-score.svg
[](https://kimun.tools)
A badge in the repository is as fresh as its last run, and a workflow cannot commit to a protected branch. To keep it current, kimun-action does it in GitHub Actions: it installs km, writes both forms of the badge, and with actions/deploy-pages publishes them on GitHub Pages on every push. The README then points at https://OWNER.github.io/REPO/km-score.svg, or at https://img.shields.io/endpoint?url=https://OWNER.github.io/REPO/km-score.json. Its README has the whole workflow.
km score diff — Compare score against a git ref
Extracts the file tree at the given ref, computes the score for both snapshots, and shows a delta table per dimension. Useful for reviewing how commits impact code quality.
km score diff # compare vs HEAD (uncommitted changes)
km score diff --git-ref HEAD~1 # compare vs previous commit
km score diff --git-ref main # compare vs main branch
km score diff --format json # machine-readable output
Options:
| Flag | Description |
|---|---|
--git-ref REF |
Git ref to compare against (default: HEAD) |
--model MODEL |
Scoring model: cogcom (default), cogcom-0.35 or legacy |
--format {table,json,short,terse} |
Output format (default: table) |
--bottom N |
Number of worst files to show (default: 10) |
--min-lines N |
Minimum lines for a duplicate block (default: 6) |
km map -- Enclosure diagram of the code
Draws the code as nested circles, the enclosure diagram of Your Code as a Crime Scene: every directory is a circle holding the circles of its subdirectories and files, a file's area grows with its lines of code, and one metric colours the files. A table ranks files; the map shows where the ones that stand out sit, whether spread out or gathered in one module, and how big that module is next to the rest.
km map --html map.html [path]
km map --html map.html --color-by hotspots --since 6m
km map --format json [path] # the laid out tree, for other tools
The page is one HTML file that needs nothing else, in light and dark themes. A click on a directory zooms into it, a click outside it or on Up zooms out; hovering a file shows its path, its lines and its value. Directory names show when their circle is large enough. The foot of the page credits what it builds on.
The two ends of the scale are vermilion orange and blue (Okabe-Ito), which stay apart for every kind of colour blindness. Patterns draws them without colour as well, lines over the orange files and dots over the blue ones, for a print in black and white; the page prints with its colours and without its controls.
--color-by |
Colours | As |
|---|---|---|
score (default) |
orange below 70, grey from 70 to 90, blue from 90 | km score, per file |
hotspots |
orange by share of the largest hotspot, grey under a tenth of it | km hotspots |
churn |
high orange, medium pale orange, low grey | km churn |
age |
active blue, stale grey, frozen pale orange | km age |
knowledge |
critical orange to low grey; orange when the owner is inactive | km knowledge |
cycles |
orange when the file is in a dependency cycle | km deps |
llm |
blue from 95 (LLM-friendly), grey from 70 (needs care), orange below 70 (LLM risk) | readiness for coding agents, by km score |
Above the map, the share of the lines on each side of the scale (85% an LLM risk · 9% needing care · 5% LLM-friendly), and on it a label with the path of the directory where each side gathers most (Most LLM risk: src/tui/), not the only place of that side: the directory below the root with the most such lines and the largest share of them, a directory that is a part of the code rather than nearly all of it. --color-by llm follows the claim that coding agents work well only on healthy code: CodeScene puts the bar at 9.5 of its Code Health, and 95 is its counterpart on the 0–100 scale of km score, which is measured otherwise. Both thresholds can be changed in .kimun.toml ([map] llm_friendly = 95, llm_risk = 70).
Files without a value (no history, a language with no dependency graph) are drawn faint. The metrics of git need a repository and read history since --since; the thresholds come from .kimun.toml as their commands read them. The layout packs each directory's children largest first, then by name, as d3.pack packs circles, so the same code gives the same map.
| Flag | Description |
|---|---|
--html FILE |
Write the map as an interactive HTML page to this file |
--color-by METRIC |
score (default), hotspots, churn, age, knowledge, cycles or llm |
--since TIME |
Read history since this time (e.g. 6m, 1y, 30d) |
--format {table,json} |
json prints the laid out tree: every node with x, y, r, depth, parent, lines, and for files heat (-1 to 1) and label |
km docs refs -- Dead references in documentation
Documentation refers to the code: files, directories, headings of other documents. When the code moves on, those references go dead and nobody notices, and an instruction file for coding agents that names a file no longer there sends the agent the wrong way. km docs refs reads the Markdown documents of the repository and lists the references that lead nowhere.
km docs refs [path]
km docs refs --since-ref origin/main --fail-on-dead # in CI
km docs refs --format github # annotations on the pull request
| What | Dead when |
|---|---|
Links and images: [text](path), , [label]: path, <a href>, <img src> |
the file does not exist, from the document, or from the repository root for a path that starts with / |
Anchors: path.md#heading, #heading |
the Markdown document has no such heading, named as GitHub names it (## Install is #install), nor an element with that id or name |
Paths in code spans: `src/kind.rs`, `scripts/`, `src/kind.rs:42` |
the repository held that path at some time and no longer does |
A path in a code span is alive when it exists from the document or from the root, or when it is the end of a path of the repository (`deps/graph.rs` names src/deps/graph.rs). It counts as dead only if the history of the repository ever held it, so the layout of an example (`src/lib.rs` in a guide about Rust packages) or a file a plan has yet to create is not taken for one. Outside a git repository, a path is read when its first name is that of a file or directory of the tree. URLs are never fetched; an absolute link without an extension (/docs/score/) is a route of a site built from the documents, not a file. Fenced and indented code, front matter and HTML comments refer to nothing.
With --since-ref, the documents changed since the ref keep every dead reference, and the others only those that name a file the change touched: a pull request that deletes or moves a file learns which documents still name it.
| Flag | Description |
|---|---|
--since-ref REF |
Only documents changed since REF, and references to files changed since it |
--fail-on-dead |
Exit with code 1 when a dead reference is found |
--include-tests |
Read the documents under test directories too (left out by default, as fixtures) |
--format {table,json,short,terse,github,codeclimate} |
Output format (default: table). terse prints the number of dead references |
Example output:
────────────────────────────────────────────────────────────────────
Document Line Reference Problem
────────────────────────────────────────────────────────────────────
docs/guide.md 42 src/parser/old.rs no such path
docs/guide.md 57 ../README.md#install no such heading
tools/README.md 9 baselines/run.sh no such file
────────────────────────────────────────────────────────────────────
3 dead of 512 references, in 2 of 38 documents
The JSON lists documents, references and every dead reference with its document, line, reference, form (link, image or code) and missing (file, heading or path). km ai offers the command to an LLM as the tool km_docs_refs.
km docs links -- The graph of notes
A knowledge base, a docs site or a second brain is a graph of notes, and its health is a property of that graph: notes nothing links to, links that lead nowhere, a few notes everything depends on, groups of notes cut off from the rest. km docs links reads the links between Markdown documents and reports them.
km docs links [path]
km docs links kb/ --top 50
km deps --kind docs --matrix # the same graph with the dependency tools
| Link | Leads to |
|---|---|
[text](path.md) |
the document at that path, from the linking one (from the walked directory for a path starting with /) |
[[Note]], [[kb/Note|text]], ![[Note]] |
a wikilink, as Obsidian, Foam and Logseq resolve it: by path from the root, else by note name, without case or extension; of several notes of that name, the nearest |
URLs and anchors alone link to no note, and a link to a file that is no document is no edge. An absolute link without extension is a route of a site.
| Reported | What |
|---|---|
| Broken links | no file has the path, no note the name |
| Orphans | notes no note links to, besides entry points: README, index, SUMMARY, CLAUDE, AGENTS, CHANGELOG, CONTRIBUTING and the like |
| Hubs | the notes most linked to |
| Islands | groups of notes linked among themselves and to no other group, besides the largest |
With --kind docs, km deps builds the same graph: fan-in and fan-out of each note, cycles, --matrix and its HTML page, and broken links as unresolved imports. Without --kind, documents stay out of the dependency graph as before.
| Flag | Description |
|---|---|
--top N |
Rows of each table (default: 20); JSON lists every entry unless given |
--format {table,json,short,terse} |
Output format (default: table). terse prints the number of broken links |
km docs context -- Files that steer coding agents
CLAUDE.md, AGENTS.md, rules of Cursor and Copilot, skills, agent and command definitions are loaded into the model's context. They grow, they repeat each other and their rules pile up, while models use long contexts worse, and worst what sits in the middle (Liu et al., Lost in the Middle, TACL 2024). km docs context measures them. No model is called.
km docs context [path]
km docs context --max-tokens 8000 # in CI
| Role | Files |
|---|---|
instructions |
CLAUDE.md, CLAUDE.local.md, AGENTS.md, GEMINI.md, read for every task under their directory |
rules |
.cursorrules, .cursor/rules/, .windsurfrules, .github/copilot-instructions.md, .github/instructions/ |
skill, agent, command |
.claude/skills/ (and any SKILL.md), .claude/agents/, .claude/commands/ |
| Measure | What |
|---|---|
| Tokens | approximate: characters / 4 |
| Loads | what an agent working in a directory reads together: the instructions of its family (claude, agents, gemini) from the analyzed directory down to it, so a deep directory shows its real budget |
| Rules | lines of prose that ask (must, never, always, do not, should, debe, nunca, siempre, …): a rough measure of how much is asked |
| Repeated | lines of prose found in more than one context file; a link such as AGENTS.md → CLAUDE.md repeats nothing |
| Dead | the references km docs refs finds dead |
| Commits, Changed | how often each file changed and when last, in a git repository |
--max-tokens N exits with code 1 when a load goes over N tokens. Files ignored by git are not read, and .claude/ often is; --kind does not apply, the exclude flags do.
| Flag | Description |
|---|---|
--max-tokens N |
Exit with code 1 when a load goes over N tokens |
--top N |
Rows of each table (default: 20); JSON lists every entry |
--format {table,json,short,terse} |
Output format (default: table). terse prints the tokens of the largest load |
km docs stale -- Documents behind the code
The audit of a knowledge base over time: the documents whose code changed after them. Where km impact watches one change before it is merged, km docs stale looks at the whole repository as it is.
km docs stale [path]
km docs stale kb/ --top 50
A document describes a file as km impact reads it: it covers it (covers: in its front matter, or [docs] covers in .kimun.toml) or it names it (a link, a path in a code span, whole or by its end, or a directory of two names or more above it). Documents are never described, and changelogs and the globs of [docs] ignore are left out. A document is behind when a file it describes changed after the document last did, in the committed history.
───────────────────────────────────────────────────────────────────────────────
Document Updated Behind Commits Describes
───────────────────────────────────────────────────────────────────────────────
kb/payments/refunds.md 2026-01-10 143d 12 apps/payments/lib/refund.ex (+3)
docs/architecture.md 2026-04-02 51d 3 lib/billing/invoice.ex
───────────────────────────────────────────────────────────────────────────────
2 of 38 documents are behind the code they describe
Behind is the time from the document's last change to the last change of what it describes, furthest first; Commits the distinct commits to those files since the document changed. The JSON lists every document behind, with updated, days_behind, commits and each file with changed and commits.
| Flag | Description |
|---|---|
--top N |
Rows of the table (default: 20); JSON lists every document unless given |
--format {table,json,short,terse} |
Output format (default: table). terse prints the number of documents behind |
km infra smells -- Security smells of infrastructure code
Infrastructure code has its own defects, and the security ones are well catalogued: secrets written into the code, services left open to the whole internet, containers running as root (Rahman et al., The Seven Sins: Security Smells in Infrastructure as Code Scripts, ICSE 2019). km infra smells reads the files of kind infra (Terraform, Dockerfiles, CI pipelines, Compose files, Kubernetes manifests, Ansible playbooks), whatever --kind says, and lists them.
km infra smells [path]
km infra smells --since-ref origin/main --fail-on hard-coded-secret,empty-password # in CI
km infra smells --format github # annotations on the pull request
| Smell | Found |
|---|---|
hard-coded-secret |
a literal assigned to a key named as a secret (password, db_password, clientSecret, api_key, AWS_SECRET_ACCESS_KEY), or a value shaped like a known key: AWS, GitHub, GitLab, Slack, Stripe, Google, private keys. Variables, interpolations and references (var.x, ${{ secrets.X }}, {{ vault_x }}, !Ref) are no literal |
empty-password |
a password set to "" |
admin-by-default |
user: root, runAsUser: 0, privileged: true, runAsNonRoot: false, USER root left as the last user of a Dockerfile |
unrestricted-address |
0.0.0.0/0 or ::/0 |
ci-permissions |
permissions: write-all; a pull_request_target workflow that reads the head of the pull request, running its code with the secrets of the base |
http-without-tls |
an http:// URL to a host outside the machine, the local network, .svc and example domains |
weak-cryptography |
md5 or sha1 in the code |
unpinned |
an image without a tag or on latest, an action not pinned to a full commit SHA |
suspicious-comment |
a comment with TODO, FIXME, HACK, XXX, bug, workaround or insecure |
Infrastructure smells — security smells of infrastructure code
────────────────────────────────────────────────────────────────────────────────────
File Line Smell Detail
────────────────────────────────────────────────────────────────────────────────────
terraform/aws/ec2.tf 15 Hard-coded secret an AWS access key in the code
terraform/aws/lambda.tf 45 Hard-coded secret `access_key` holds a literal
terraform/aws/lambda.tf 46 Hard-coded secret `secret_key` holds a literal
terraform/aws/providers.tf 10 Hard-coded secret `access_key` holds a literal
────────────────────────────────────────────────────────────────────────────────────
29 smells in 12 of 51 files: hard-coded secret 14, unrestricted address 8, unpinned 7
The reading is lexical, line by line: comments are told apart by format (#, // and /* */ in HCL, heredocs as text; whole lines in a Dockerfile; # outside quotes in YAML), and every smell but the last is searched in the code alone. The most severe come first. Secret values are never printed, in any format. The JSON has the shape of km smells: one entry per file with its smells (kind, line, detail).
| Flag | Description |
|---|---|
--since-ref REF |
Only files changed since REF, uncommitted changes included |
--fail-on SMELL,... |
Exit with code 1 when one of these smells is found |
--top N |
Rows of the table (default: 20); JSON lists every file unless given |
--format {table,json,short,terse,github,codeclimate} |
Output format (default: table). terse prints the number of smells; github annotates the lines as warnings (the first five smells) or notices |
km report -- Comprehensive metrics report
Generates a multi-section report combining all static code metrics in a single pass: lines of code, duplicates, indentation, Halstead, cyclomatic complexity, cognitive complexity, and maintainability index.
km report [path]
Options:
| Flag | Description |
|---|---|
--top N |
Show only the top N files per section (default: 20) |
--min-lines N |
Minimum lines for a duplicate block (default: 6) |
--full |
Show all files instead of truncating to top N |
--format {table,json,short,terse} |
Output format (default: table) |
Project configuration (.kimun.toml)
Run km init to analyze your project and generate a calibrated .kimun.toml in one step:
$ km init
Analyzing project... done.
Current state:
avg function length: 38 lines → suggested max_lines = 45
avg param count: 3.2 → suggested max_params = 4
dup ratio: 4.1% → suggested max_dup_ratio = 5.0
health score: B+ → suggested fail_below = B
Write .kimun.toml with these values? [Y/n]
Use --yes to skip the prompt. Add -y in CI to write the file non-interactively.
Alternatively, place a .kimun.toml file manually in the root of your repository to set project-level defaults for thresholds and quality gates. km searches for the file at the git repository root, falling back to the current directory.
CLI flags always take precedence over .kimun.toml, which in turn takes precedence over built-in defaults.
[smells]
max_lines = 30 # flag functions longer than N body lines (default: 50)
max_params = 3 # flag functions with more than N parameters (default: 4)
[dups]
min_lines = 8 # minimum block size for duplication detection (default: 6)
# also applies to `km report` and `km score`
max_duplicates = 10 # CI gate: fail if duplicate groups exceed N
max_dup_ratio = 5.0 # CI gate: fail if duplicated-lines ratio exceeds this %
[score]
model = "cogcom" # scoring model: cogcom (default), cogcom-0.35 or legacy
fail_below = "B-" # CI gate: fail if health score is below this grade
[age]
active_days = 60 # files modified within N days are Active (default: 90)
frozen_days = 180 # files not modified for more than N days are Frozen (default: 365)
[tc]
min_degree = 5 # minimum commits per file to include in coupling analysis (default: 3)
min_strength = 0.5 # only show pairs with coupling strength >= this value
[hotspots]
complexity = "cogcom" # complexity metric: indent (default), cycom, or cogcom
[map]
llm_friendly = 95 # km map --color-by llm: from this score a file is LLM-friendly (default: 95)
llm_risk = 70 # below this score it is an LLM risk (default: 70)
[impact]
inert = ["scripts/**"] # changed files that reach nothing, besides documentation
entry_points = ["**/endpoint.ex"] # files run rather than used, besides tasks and scripts
[docs]
covers = { "apps/payments/**" = "kb/payments/**", "lib/billing/**" = ["kb/billing.md"] } # code and the documents that describe it
ignore = ["docs/adr/**"] # documents that never need to follow the code
[exclude]
dirs = ["vendor", "third_party"] # directory names, at any depth (as --exclude-dir)
extensions = ["sql", "proto"] # file extensions (as --exclude-ext)
patterns = ["legacy/**", "*.min.js"] # globs from the analyzed directory (as --exclude)
[exclude] leaves files and directories out of every command, besides what .gitignore already does. Unlike other values it does not give way to flags: it adds to --exclude-dir, --exclude-ext and --exclude, and --list-excluded shows what it leaves out. Patterns are matched against the path relative to the analyzed directory, as --exclude is.
[kinds] declares the kind of files by glob, against the path relative to the analyzed directory, before what their language and path say. With separate = true, infrastructure is no longer measured as code unless --kind asks for it.
[kinds]
separate = false # true: Terraform and Dockerfiles are not measured as code (default: false)
docs = ["notes/**"] # these files are docs, whatever their extension
infra = ["deploy/**/*.yml"]
All sections and fields are optional — omit any you don't need. A fully documented template is available at .kimun.toml.example.
Features
- Respects
.gitignorerules automatically - Deduplicates files by content hash (identical files counted once)
- Detects languages by file extension, filename, or shebang line
- Supports nested block comments (Rust, Haskell, OCaml, etc.)
- Handles pragmas (e.g., Haskell
{-# LANGUAGE ... #-}) as code - Mixed lines (code + comment) are counted as code, matching
clocbehavior - A comment marker inside a string opens no comment:
let glob = "crates/*";is a line of code. Here kimun parts fromcloc, which reads the/*of a string as the start of a block comment and counts every line up to the next*/as comment. On code that writes globs or paths in strings the difference adds up: on kimun's own sources, tests included,cloccounts 1,719 lines of code as comment.tokeireads them as kimun does - Reads sources in any 8-bit encoding (Latin-1, Windows-1252) whole: bytes that are not UTF-8 are replaced, not taken for the end of the file
Supported Languages
| Language | Extensions / Filenames |
|---|---|
| Bourne Again Shell | .bash |
| Bourne Shell | .sh |
| C | .c, .h |
| C# | .cs |
| C++ | .cpp, .cxx, .cc, .hpp, .hxx, .hh |
| Clojure | .clj, .cljs, .cljc, .edn |
| CMake | .cmake, CMakeLists.txt |
| CSS | .css |
| Dart | .dart |
| Dockerfile | Dockerfile |
| DOS Batch | .bat, .cmd |
| Elixir | .ex |
| EEx | .eex |
| Elixir Script | .exs |
| Erlang | .erl, .hrl |
| F# | .fs, .fsi, .fsx |
| Fortran 77 | .f, .for, .ftn, .f77, .pfo (and upper case) |
| Fortran 90 | .f90, .F90 |
| Fortran 95 | .f95, .F95 |
| Fortran 2003 | .f03, .F03 |
| Fortran 2008 | .f08, .F08 |
| Go | .go |
| Gradle | .gradle |
| Groovy | .groovy |
| Haskell | .hs |
| HTML | .html, .htm |
| HTML EEx | .heex |
| Java | .java |
| JavaScript | .js, .mjs, .cjs |
| jq | .jq |
| JSON | .json |
| JSX | .jsx |
| Julia | .jl |
| Kaikai | .kai |
| Kotlin | .kt, .kts |
| lex | .l, .lex |
| Lua | .lua |
| Makefile | .mk, Makefile, makefile, GNUmakefile |
| Markdown | .md, .markdown |
| Nim | .nim |
| Objective-C | .m, .mm |
| OCaml | .ml, .mli |
| Pascal | .pas, .dpr, .dpk, .lpr, .pp, .pascal |
| Perl | .pl, .pm |
| PHP | .php |
| Properties | .properties |
| Python | .py, .pyi |
| R | .r, .R |
| reStructuredText | .rst, .rest |
| Ruby | .rb, Rakefile, Gemfile |
| Rust | .rs |
| Scala | .scala, .sc, .sbt |
| SQL | .sql |
| Swift | .swift |
| Terraform | .tf |
| Text | .txt |
| TOML | .toml |
| TypeScript | .ts, .tsx, .mts, .cts |
| yacc | .y, .yacc |
| XML | .xml, .xsl, .xslt, .svg, .fsproj, .csproj, .vbproj, .vcxproj, .sln, .plist, .xaml |
| YAML | .yaml, .yml |
| Zig | .zig |
| Zsh | .zsh |
Language-specific notes
- Kaikai —
#[...]opens an attribute, not a#comment, so attributes count as code. Documentation attributes (#[doc("...")], including the multi-line#[doc("""...""")]form) count as comments and are excluded from the complexity, Halstead, and smell analyses. - Elixir — the heredocs of
@moduledocand@doc(""",''', and the~S"""and~s"""sigils) count as comments, as cloc counts them, and are excluded from the complexity, Halstead, and smell analyses. A one-line@doc "..."and any other heredoc count as code. - Python — a triple-quoted string, a docstring above all, counts as comment,
as cloc counts it: its lines are comment unless code shares them
(
SQL = """is code, the lines after it are comment). - C and C++ headers — a
.his C++ when its text says so (a line opening withnamespace,template,class, an access specifier,using,constexpr, or anystd::), and C otherwise. cloc reports every header asC/C++ Header; kimun keeps them with the language that analyzes them. - reStructuredText — a line opening with
..(a directive or a comment) counts as comment; cloc also counts as comment the lines indented under it. - HTML EEx —
<%!-- --%>and lines opening with<% #are comments, as cloc counts them. EEx —<%# %>is a comment. - Fortran — free form (
.f90and later) comments with!; fixed form (.f,.for,.f77) also makes a comment of a line withC,cor*in its first column. OpenMP and OpenACC directives (!$omp,C$OMP,!$acc) and conditional compilation (!$) count as code, as the directives they are; cloc counts them as comments, and in free form it also takes a line that starts withcin the first column (contains,call) for a comment. A backslash escapes nothing in a string. Fortran ignores case, and so do the complexity, Halstead and smell analyses, while the reports name functions as they are written. Functions are the program units (subroutine,function,program) to theendthat closes them, the units they contain included; those of aninterfaceblock declare no function. Cognitive nesting is read from the constructs (if … then,do,select,where) and theendof each, not from indentation, which fixed form lacks. - Pascal (Delphi, Free Pascal) — three kinds of comment:
//,{ … }and(* … *), the last two closed only by their own delimiter.{$…}is a compiler directive and counts as code. Strings are single-quoted, with no backslash escapes; a delimiter inside one opens no comment (cloc does not tell them apart, so it often counts code as comment after a'{'or aCOUNT(*)). Pascal ignores case, as Fortran does. Functions are the routines of the implementation (procedure,function,constructor,destructor,operator, class methods), from their header to theendof their block, local routines included; the declarations of theinterfacesection and of class types, andforwardorexternalones, have none. Headers repeated under conditional compilation are one routine. Cognitive nesting is read from indentation, two columns a level. - Interior lines of multi-line strings (Kaikai triple-quoted literals,
Elixir heredocs) count as code for
km loc, but are excluded from cyclomatic, cognitive, Halstead, and smell analyses: prose and embedded data are not control flow. - Rust raw strings (
r"...",r#"..."#and the forms with more#, with aborcprefix too) end only at a quote followed by as many#as opened them, and span lines. What they hold counts as code forkm loc, whatever it looks like, and is left out of the cyclomatic, cognitive, Halstead, and smell analyses.
Development
cargo build # build debug binary
cargo test # run all tests
cargo clippy # lint (zero warnings required)
cargo tarpaulin --out stdout # coverage report
References
The metrics and methodologies implemented in Kimün are based on the following sources:
Books
- Adam Tornhill, Your Code as a Crime Scene (Pragmatic Bookshelf, 2015). Basis for hotspot analysis (ch. 4–5), temporal coupling (ch. 7), knowledge maps / code ownership (ch. 8–9), and indentation-based complexity as a proxy for code quality.
- Adam Tornhill, Software Design X-Rays (Pragmatic Bookshelf, 2018). Extends the crime scene metaphor with additional behavioral code analysis techniques.
Papers and standards
- Maurice H. Halstead, Elements of Software Science (Elsevier, 1977). Defines the operator/operand metrics: vocabulary, volume, difficulty, effort, estimated bugs, and development time.
- Thomas J. McCabe, "A Complexity Measure", IEEE Transactions on Software Engineering, SE-2(4), December 1976, pp. 308–320. Introduces cyclomatic complexity as a measure of independent paths through a program's control flow graph.
- Paul Oman & Jack Hagemeister, "Metrics for Assessing a Software System's Maintainability", Proceedings of the International Conference on Software Maintenance (ICSM), 1992. Original Maintainability Index formula combining Halstead Volume, cyclomatic complexity, and lines of code.
- Microsoft, Code Metrics — Maintainability Index range and meaning. Visual Studio variant: normalized to 0–100 scale, no comment-weight term.
- Verifysoft, Maintainability Index. Extended MI formula with a comment-weight component (MIcw) that rewards well-commented code.
- Yasutaka Kamei et al., "A Large-Scale Empirical Study of Just-in-Time Quality Assurance" (IEEE TSE 39(6), 2013). Basis for the diffusion measures of
km impact. - Thomas Zimmermann, Andreas Zeller, Peter Weissgerber, Stephan Diehl, "Mining Version Histories to Guide Software Changes" (IEEE TSE 31(6), 2005). Basis for the logical radius of
km impact.
The name
Kimün means "knowledge" or "wisdom" in Mapudungun, the language of the Mapuche people.
The mascot is Madu, a pudú who is a machi: the one who holds the knowledge of her people. She is the sister of Kalku, the mascot of kalku. She explains Kimün in a two-minute video:
License
MIT. See LICENSE.
Explore related projects
codecrafters-io/build-your-own-x
Master programming by recreating your favorite technologies from scratch.
562,434 starspublic-apis/public-apis
A collective list of free APIs
498,889 starsmorluto/rea
Reverse engineer anything with agents, from app behavior down to native binaries.
458,555 starsopenclaw/openclaw
The AI that really does things. Any OS. Any Platform. The lobster way. 🦞
394,422 stars