MGS1020: output owned by two targets
Two targets in the same project declare the same output glob, so both write the same bytes and the result depends on which ran last.
[fail] output ownership: 1 output glob(s) declared by more than one target
docs: output glob "gen/**" is declared by format and generate
Why this is not an ordering problem
The instinct is to add a dependency edge and settle the order. That cannot work, and seeing why is the whole point of this code.
Say generate writes gen/** and format rewrites it:
- Run
generatethenformat, and the formatter's bytes are what land on disk. The nextgenerateregenerates unformatted output, sees it differ from what is committed, and fails its drift gate. - Run
formatthengenerate, and the formatting is immediately undone. The formatter's own check fails instead.
Whichever runs last wins, and the loser's gate fails on the next run, at every possible ordering. Ordering resolves a producer and a consumer. Here there are two producers of the same bytes, which is an ownership violation, and no dependency edge fixes it.
The rule
A generated file has exactly one owning target: the one that declares it.
If generated output needs to be formatted, the generator formats it, as the last
thing it does. It still owns the final bytes, and its drift gate compares
formatted output against formatted output. This is why generated Go needs no
special handling here: mockery and protoc emit gofmt-clean output already.
Excluding generated trees from a formatter is the weaker fallback, correct when
nothing else needs to read the output. Every formatter in this workspace does it
(see .markdownlintignore and the !src/gen/** entries in biome.json),
because a lint rule fixed in generated output is a fix in the wrong place: it
belongs in the generator, which will overwrite the edit on its next run anyway.
Fixing it
- Decide which target owns the glob and remove the other's declaration.
- If the second target genuinely needs to touch part of the file rather than
produce it, declare
ctx.modifiesExistingFiles(...)instead. That records a read-and-amend relationship, and magus will neither snapshot, replay, nor clean those bytes. See Files a target edits rather than produces. - If the two really are one step, merge them into one target.
What this does not catch
Only declared writes are visible. A formatter that rewrites a tree without
declaring ctx.writesFiles(...) is invisible to a static check, which is the
reason formatters are also excluded by configuration rather than relying on this
diagnostic alone.
See also
- MGS4002: the cross-project sibling, where two projects declare the same output glob under the same target.
- MGS1019: a generated file that records VCS state, and so restales itself on every commit.
- Cache model: what an output means to the cache.