MGS4007: undeclared source modified
A target finished successfully, and one of the files it declared as a source
had different content afterwards than before. The target did not declare that file
with ctx.modifiesExistingFiles(...).
[MGS4007] .:format modified 1 file it declared as sources; declare them with
ctx.modifiesExistingFiles(...) or stop writing them: types/diagnostic.go
Why this is not a style rule
A cache key is a claim: these inputs produced this result. Sources are hashed before the target runs, so a target that rewrites its own sources invalidates that claim in the same breath it makes it. The stored entry describes a tree that no longer exists, and nothing can reproduce it - not a teammate, not CI, not the same machine a second later.
magus already noticed. recordOutput recomputes the key after every run and
declines to persist the key's explanation when it disagrees, because those lines
would blame the wrong input. That branch has always existed; until MGS4007 it only
whispered at debug level, so the run still passed and the entry was still stored.
The charm trap this was built for
Charms patch a command's arguments. A charm cannot say whether the resulting tool writes, which is exactly how a check turns into a write with nothing in its name changing:
default_charms: [rw] # magus.yaml - every local run carries it
"rw": append(["-w"]) # the op's charm - the tool's autofix flag
Compose those and a target called lint rewrites the tree on a developer's
machine, while CI - which strips default charms - runs the identical target
read-only. Neither run can reveal the other. That divergence is the defect;
the spelling of the charm is not.
So this code is charm-agnostic on purpose. Keying it on the charm was
considered and rejected twice over: a rule that exempts a charm the caller typed
lets a mutating custom charm through merely because someone spelled it out, and a
rule that exempts rw specifically says nothing about the charms a workspace
defines for itself.
Fixing it
Declare the edit, when writing is the point. A formatter genuinely modifies
existing files, and ctx.modifiesExistingFiles is the declaration that says so:
export fun format(ctx: magus\Context, args: [str]) > void {
ctx.modifiesExistingFiles("**/*.go", "go.mod", "go.sum");
go["go-fmt"](ctx);
go["go-mod-tidy"](ctx);
}
Declare it unconditionally, at the top of the target, even when a charm decides
whether the edit lands - the same shape spells/golang/gomod.buzz uses for
go.mod. The globs fold into the source set either way, so the check-only variant
is unaffected.
Or stop writing, when it is not. A target named for checking should not carry a write. Hang the autofix off its own charm rather than a workspace default, so it happens because someone asked:
return Command{bin = "typos", args = args, charms = {
"fix": append(["-w"]), // not "rw": that one is a default charm
}};
Why modifiesExistingFiles rather than writesFiles
writesFiles declares an output: magus snapshots it, replays it on a cache
hit, and magus clean removes it. That is wrong for a file the target amends but
did not author - a replay would restore stale bytes over someone else's edit, the
two-owner hazard MGS1020 reports.
modifiesExistingFiles records read-and-amend instead: the globs join the source
set (so editing the file still invalidates the target that maintains it) and stay
out of the output set (so nothing snapshots, replays, or cleans them).
What it checks, and what it does not
The check runs only after the target succeeded. A failed target has already reported why it failed, and a second diagnostic on top would bury it.
It compares content, not mtimes: a tool that rewrites byte-identical output has changed nothing a cache key can observe, and reporting it would train readers to ignore the code.
A source that disappeared counts - deleting an input is a write, and the key then describes a file that is not there. A source that appeared does not: a target may legitimately produce a file some broad source glob would have matched, which is MGS1028's question rather than this one.
Only declared sources are visible. A target that rewrites a file no glob claims is invisible here, the same limit MGS1020 records for static output checks.