MGS1002: spell import shadowed
A workspace defines the same spell import (spells/<name>) at two levels where one
directory is an ancestor of the other. Spell imports resolve root-wins: the
copy nearest the workspace root is canonical, so the deeper copy is never loaded.
The deeper definition is dead code, and this ward blocks the run until you resolve
or acknowledge it.
[MGS1002] spell import "spells/hello" is defined at web/studio/spells/hello/spell.buzz
but shadowed by spells/hello/spell.buzz: imports resolve root-wins, so the deeper
spell is dead. Move or rename it, or acknowledge the shadow in magus.yaml
(spells.allow_shadow) with a reason.
see: .../MGS1002.md
Why
A local spell is imported by a path-style name, import "spells/hello". magus
resolves it by walking a spells/ directory at every level from the workspace root
down to the importing magusfile, and the root-most match wins (see
the workspace model). That rule is deliberate: a spell name
means one thing across the workspace, the same way a charm name does.
The consequence is that a spells/hello placed next to a nested project, when a
spells/hello also exists higher up, can never be reached. An author who put it
there expected it to be used; instead the ancestor silently wins. That is a
footgun, not a self-contradiction, so unlike the kind-coherence wards
(MGS5002, MGS5003) it can be
acknowledged rather than only fixed.
Sibling subtrees are not affected: web/spells/hello and api/spells/hello are
not a shadow, because no single project's root-to-leaf path sees both. Only an
ancestor-and-descendant pair triggers this code.
Resolution
Pick one:
-
Rename the deeper spell so it no longer collides (
spells/hello-web). Its import name changes, but it is now reachable. -
Move the shared spell to the level that should own it. If every project should get the deeper behavior, promote it to the workspace root and drop the ancestor copy.
-
Acknowledge the shadow when it is deliberate (a nested project pins a patched copy on purpose). List its import path in
magus.yamlwith a required reason:# magus.yaml spells: allow_shadow: - name: spells/hello reason: web/studio pins a patched hello until the upstream fix landsThe reason is mandatory, so the intent stays auditable.
magus doctorflags anallow_shadowentry whose shadow no longer exists, so stale reasons get pruned.
Shadowing an embedded or declared spell
The same code covers two shadows magus.yaml did not declare:
- A workspace or remote spell carrying an embedded spell's name. Spells register
by name, so a workspace spell whose
mgs_getName()returnsgowould silently bind the embeddedgo. It fails when imported. - A workspace directory at a declared remote path (
ghcr.io/team/spells/lint/in the tree whilemagus.yamldeclares that path with a tag). A remote import never reads the workspace, so the directory is dead. It fails when the workspace loads.
Either way the fix is a declaration, never a file: an override in magus.yaml makes the
replacement visible in the one place a reviewer reads, and the import stays as it is.
spells:
magus/spell/go:
path: spells/go # the workspace copy replaces the embedded go
ghcr.io/team/spells/lint:
path: ghcr.io/team/spells/lint
Or rename the spell, or delete the directory. See Remote spells.
What this is NOT
- Not a name collision between two workspace spells. The ancestor-descendant case
is about the import path (
spells/hello) at two directory levels, not two workspace spells sharing anmgs_getName. A name is only checked against the embedded spells. - Not triggered by sibling reuse. Two projects in different subtrees may each
ship a
spells/hello; only an ancestor-descendant pair is a shadow.