How purpose is classified
Every commit gets a purpose โ feature, fix, test, refactor, docs,
config, chore, migration, dependency, or other. It is what the
"work by purpose" panels, the rework rate, and the productivity split by work
type are built on, so it is worth knowing exactly where it comes from.
No model is involved. Purpose is decided by three deterministic rules,
tried in order. The first rule that matches wins. If none matches, the
purpose is other โ never a guess.
It reads the message you already wroteโ
The tool does not add, rewrite, or append anything to a commit message to
classify it. It reads the first line of the message the developer wrote and
looks for a Conventional Commits
prefix โ type(scope)!: description. If one is there, that is the purpose:
| prefix | purpose |
|---|---|
feat | feature |
fix | fix |
test | test |
refactor, style, perf | refactor |
docs | docs |
chore | chore |
ci, build | config |
This is the highest-confidence rule, and it is the only rule that ever
produces feature or fix. Nothing about the files changed or the shape of
the diff can promote a commit to either.
What the tool does write into a commit is the attribution trailer at the bottom โ status, agent, method, and the rest, described in How attribution works. That block is separate from purpose and carries no purpose field. Purpose is computed at sync time and stored alongside the commit; the message is left as the author wrote it.
Then the paths, if every file agreesโ
Without a prefix, the tool looks at the changed files. A category is assigned only when every changed file matches the same pattern. A source file committed together with its test is a mixed change and falls through rather than being labelled by whichever pattern happened to match first.
Patterns, tried in this order โ narrower signals first:
| purpose | every changed file must beโฆ |
|---|---|
dependency | a manifest or lockfile: go.mod, go.sum, package.json, package-lock.json, yarn.lock, pnpm-lock.yaml, Cargo.toml, Cargo.lock, Gemfile, Gemfile.lock, requirements.txt, poetry.lock, pyproject.toml |
migration | on a path containing migration |
test | _test.go, under /test/ or /tests/, containing .test., or ending _spec.rb |
docs | ending .md, under docs/, or a README |
config | under .github/, ending .yml / .yaml / .toml / .gitignore, or a Dockerfile |
Matching is case-insensitive.
Then the shape of the diff, weaklyโ
The last rule has one case: a change touching six or more files with no
other signal is called refactor. Anything smaller with no prefix and mixed
paths stays other. This is deliberately weak โ it exists because a broad
multi-file change with no message convention is more often a restructuring
than anything else, and it is used only when the two stronger rules have
already declined.
What this means when you read a dashboardโ
feature and fix measure the team's commit habit as much as its work.
A team that writes feat: and fix: gets an accurate split. A team that
writes add export button gets other for that commit, and their dashboard
shows few features and a large other slice. That is a signal about
commit-message discipline, not about what the AI did. The fix is on the
team's side: adopt the prefix, and the classification follows without any
change to the tool.
The rework rate is a fix rate, not a defect rate. Every panel that shows
rework counts commits the author labelled fix:. It does not count bugs
found, incidents, or reverts. A team that never writes fix: has a rework
rate of zero, which is not the same as having no rework.
other is honest, not a failure. The design target is 70โ80% of commits
classified with no model involvement. The remainder is reported as other
rather than pushed into a plausible-looking bucket, for the same reason
absence is never reported as zero:
a wrong label that looks right is worse than an honest gap.
Complexity is not measured. Where a dashboard splits work by complexity, it is using the number of files changed as a stand-in, and says so in the panel's tooltip.
Sourceโ
The classifier is internal/purpose/purpose.go in the repository. It is
small enough to read in a few minutes, and reading it is the definitive
answer to "why did this commit get that label".