# Writing a codemod
A *codemod* is a structural fix: it rewrites the tool tree (reorder attributes, drop a
redundant one, rewrite an output element) while preserving behaviour. Codemods live in
tier 2, `galaxy-tool-codemod`, and this page is the short version of how to add one. The
authoritative, step-by-step workflow is the `add-codemod` skill in the repository
(`.claude/skills/add-codemod/`); the architecture context is
[`ARCHITECTURE.md`](https://github.com/richard-burhans/galaxy-tool-refactor/blob/main/ARCHITECTURE.md).
## The two principles
1. **TDD first.** Write the failing test, then the minimum code to pass. One test module
per source module.
2. **Detect is the primitive.** A codemod *reports* exactly what it will change as a list
of `Change` objects; the base class derives `apply` from that. You do not write a
separate mutation pass for an ordinary codemod, so detect and fix can never disagree.
The public surface a test drives:
```python
from galaxy_tool_codemod.parse import parse_module
module = parse_module(b"...") # or a Path
changes = MyCodemod().detect(module) # list[Change], non-mutating
MyCodemod().apply(module) # mutates the tree in place
```
## A minimal codemod
This is the real shape of `DropRedundantParamName` (GTR037), which drops a ``
`name` that its `argument` already implies:
```python
from typing import ClassVar
from galaxy_tool_refactor_rules.meta import RuleMeta
from galaxy_tool_codemod.change import Change
from galaxy_tool_codemod.codemod import CodemodCommand
class DropRedundantParamName(CodemodCommand):
meta: ClassVar[RuleMeta] = RuleMeta(
code="GTR037", # the next free code (the registry asserts no collision)
summary="Drop a 'name' that equals the name its 'argument' implies.",
since="0.0.1",
rulesets=frozenset({"default", "iuc", "strict"}),
)
def detect_Param(self, cursor): # dispatch by tag: -> detect_Param
argument = cursor.get_attribute("argument")
name = cursor.get_attribute("name")
if argument is None or name is None or name != argument.lstrip("-").replace("-", "_"):
return
yield Change(
code=self.meta.code,
sourceline=cursor.sourceline,
xpath=cursor.xpath,
message=f"drop redundant name='{name}' (argument '{argument}' implies it)",
mutate=lambda: cursor.delete_attribute("name"),
)
```
The pieces:
- **Subclass `CodemodCommand`** and set `meta: ClassVar[RuleMeta]` with the next free GTR
code, a `summary`, and `since`. `applies_to` defaults to `{"tool"}`; opt into
`"macro"` only for a rule that is generic across both.
- **`detect_` methods** are dispatched by element tag (`` →
`detect_Param`, `` → `detect_ChangeFormat`). Each `yield`s a `Change`
whose `mutate` is a zero-argument closure over `Cursor` primitives:
`set_attribute` / `delete_attribute` / `rename_attribute` / `rename_tag` /
`reorder_attributes` / `reorder_children` / `remove` / `add_child` / `set_text`.
- The fix must hold **by construction for novel tools**, with a Galaxy-source or schema
citation, never "no corpus tool does this." That argument is recorded in a proof doc
(see the [rule proofs](proofs/index.md)); a guard test fails CI if a fixable rule ships
without one.
## Registering it
- **Always** add the class to `catalog.py::coded_codemods()`. This is what the cross-tier
registry enumerates, and it asserts GTR codes do not collide.
- Add it to `canonical.py::canonical_codemods()` **only if** it is safe, idempotent, and
`profile=`-preserving. That is what makes it run under `format` and the `iuc` ruleset.
Mind the order: `FixTypos` runs first, attribute reorders before child reorders.
- No registry edits are needed: `galaxy-tool-refactor-registry` derives its handles from
these two functions.
## Proving it on the corpus
Sweep the new codemod over the tool corpus to check idempotence and post-fix validity,
and to retain any real-world failure as a regression fixture:
```console
$ uv run python -m scripts.corpus_check codemod \
galaxy_tool_codemod.codemods.drop_redundant_param_name:DropRedundantParamName
```
Any tool that breaks is copied into `tests/data/regressions/` and picked up
automatically by `tests/test_regressions.py`. Investigate and fix every retained
failure, then run the pre-PR audit before opening a pull request.
A codemod that must branch on re-validation (like `FixTypos` or `UpgradeToLatest`) is the
exception: it overrides `apply` with bespoke logic and supplies a coarse `detect`. The
`add-codemod` skill covers that variant and the profile-upgrade (`upgrade_vN`) case.