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

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.

See also

  • MGS1020: two targets declaring one output - the ownership violation this one is the runtime counterpart to.
  • MGS1028: a file read but not declared, the under-declaration this one mirrors on the write side.
MGS4007racecachesourcescharmsformatteroutputsdeclarations
Last updated (9edeede5)
Glossary

Workspace

The magus root directory that owns a set of projects and shared config; the unit magus operates over. 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.

Op

A single tool-native command a target composes (long form: operation); the middle of the work hierarchy (Spell to Op to Target). See operations.

Spell

A language/runtime adapter (e.g. go, md) that maps generic targets onto a toolchain's real commands. See spells.

Charm

An execution modifier attached with : (lint:rw) that changes how a target runs, not which one; the built-in rw flips a check-only target to mutate in place, and ci always strips it. See charms.

Ward

A coded diagnostic that inspects a resolved op and nudges or blocks an anti-pattern before it runs. See wards.

Buzz

The language magusfiles are written in (the .buzz engine). See engines.

Cache

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

Affected

The set of projects touched by a change; magus affected <target> runs a target only over them. See affected.

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.