magus v0.3.0 is out. See what's new
¶ View markdown source · ✎ Suggest an edit
2 min read

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 generate then format, and the formatter's bytes are what land on disk. The next generate regenerates unformatted output, sees it differ from what is committed, and fails its drift gate.
  • Run format then generate, 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.
MGS1020magusfileoutputsownershipgenerateddriftformatter
Last updated (a170f9b2)
Glossary

Workspace

The magus root directory that owns a set of projects and shared config; the unit magus operates over. See workspace.

Project

A directory magus recognizes as a unit of work (it has a magusfile); the unit of caching, scheduling, and dependency tracking. See workspace.

Magusfile

The magusfile.buzz that declares a project's targets (as export funs) and binds its spells. See targets.

Target

A named operation (build, test, ...) you invoke with magus run <target>; it may compose a spell's tool-native operations and depend on other targets. See targets.

Cache

The content-addressed store magus consults before running a target, so unchanged work is skipped. See cache.

CI

An ordinary magusfile-defined target you compose yourself with magus\needs - magus does not hardcode its stages. Magus.RunCI treats it specially only in that it strips the rw charm, it is the anchor magus affected ci keys off, and a selected scope with no project declaring it is a load error rather than a silent no-op. See targets.

Snapshot

A point-in-time view of live state - the pool's occupancy or a tick of exported metrics - as opposed to accumulated history. See daemon.

Ownership

An insight lens: author concentration - the primary author and their share, the distinct-author count (the bus factor), and abandonment. See insight.

Conventions

This page uses none of the site's convention markers. The full set is on the conventions page.