---
title: "MGS4008: unordered same-step write"
description: Fires before a run starts when one target reads files another target of the same ctx.needs chain writes, with no ctx.needs path ordering the reader after the writer.
tags: [MGS4008, race, ordering, ctx.needs, scheduling, deadlock, declarations, doctor]
---

# MGS4008: unordered same-step write

Two targets run inside one step: a composed target's `ctx.needs` chain. One of
them declares that it writes files the other declares that it reads, and nothing
in the chain says which goes first.

```text
[MGS4008] refusing to run . ci: . coverage-badge reads "**/*.go", which . mocks-generate
  writes as "internal/mocks/gen/**/*.go", and nothing orders the two. Both run inside .
  ci's ctx.needs chain, with no needs path between them, so the reader can start before
  the writer finishes and can wedge the run waiting for a slot the writer needs. Declare
  ctx.needs(mocks-generate) in coverage-badge so it runs after the writer, or narrow
  coverage-badge's ctx.readsFiles so it no longer matches what the writer produces.
```

The run is refused **before any target starts**. Nothing has executed, nothing is
half-written, and the fix is a line in a magusfile.

## Why a refusal rather than an ordering

Across steps magus derives the order itself: a step whose declared writes reach
another step's declared reads runs first, and the reader waits on the barrier.
That machinery cannot reach inside a step. A step is one target's body, and the
sequence its body runs is the sequence the body wrote down: `ctx.needs` and
nothing else. A scheduler that reordered chain members would be rewriting the
body.

So there is no order to install, and the two outcomes left are both bad:

- **The reader wins the race.** It hashes and reads the tree as it stood before
  the writer produced anything, and records a result under a key describing files
  that changed a moment later. Nothing about the entry says so.
- **The reader waits.** A chain member that ends up behind the writer waits while
  holding the concurrency slot it was admitted on. When the composers of one gate
  do that together they hold every slot in the pool, and the writers they are
  waiting for can never be admitted. That is
  [MGS3013](../sandbox/MGS3013.md), and before it existed the run simply hung
  until the stall watchdog ([MGS3012](../sandbox/MGS3012.md)) killed it fifteen
  minutes later with every project lock held.

## Resolve it

**Order the reader after the writer.** This is the fix in nearly every case, and
it is one line:

```buzz
export fun coverage_badge(ctx: magus\Context, args: [str]) > void {
    ctx.needs(generate);           // the writer, or whatever composes it
    ctx.readsFiles("**/*.go");
    ...
}
```

Naming the composer (`generate`) rather than each generator is usually right: the
chain already sequences those, and the memo means a target reached twice still
runs once.

A second `ctx.needs` call in the composer's own body is an ordering too. One
call fans its arguments out together and returns when all of them have run, so
`ctx.needs(format); ctx.needs(conventions);` runs `conventions` after everything
`format` reached, and the check reads it that way. Put the reader in a later
call when the sequencing belongs to the composer rather than to the reader.

**Or narrow the reader's declaration.** A reader that never meant to depend on
generated output should say so, and a footprint that matches files the reader
does not care about is over-declared anyway:

```buzz
ctx.readsFiles("coverage/**/*.json");   // not "**/*.go"
```

Both fixes are real answers. The one to avoid is making the overlap invisible by
deleting a declaration the target does rely on: the cache key would then omit an
input the result depends on, which is [MGS1028](../magusfile/MGS1028.md)'s
question rather than this one.

## What it looks at

**Declared footprints on both sides.** A target that names no `ctx.readsFiles`
falls back to the project's source baseline, a whole-project over-approximation.
An overlap through one of those is a guess, and this refuses runs, so a guess is
not enough. Fallbacks are ignored on either side.

**`ctx.needs` reachability, in either direction.** Reader-after-writer is what
this asks you to add. Writer-after-reader clears it too: the reader reading what
was there beforehand is a sequence the author wrote down, and staleness is
[MGS4006](MGS4006.md)'s question. A later `ctx.needs` call in a composer counts
as reaching everything an earlier call ran.

**A file, not a glob.** Two globs can intersect as patterns and never meet on a
path (`**/MAGUS.md` against `cmd/magus/completions/*`). The refusal stands only
on a file in the tree that both patterns match, and on one the reader's key
would hash: a pattern read never descends into a pruned directory (`gen`,
`vendor`, `node_modules`, and whatever the project's spells add), so a writer's
file under one of those is no witness for a pattern. An exact path is.

**One project.** A pair whose reader and writer belong to different projects is
reported as advice rather than refused: the fix is a line in one project's
magusfile, and the sequencing usually belongs to the other project's chain.

## Meeting it before your first gate

`magus doctor` runs the same predicate over every composed target in every
project, as the `same-step-writes` check. It reads declarations only, so it needs
no run and no cache:

```sh
magus doctor
```

A pair the check fails on is one a gate will refuse; a cross-project pair it
advises on is the one a run warns about; a workspace it passes cannot be refused
for this.

## See also

- [MGS3013](../sandbox/MGS3013.md): the wedge this prevents, caught at run time
  when it happens for some other reason.
- [MGS3012](../sandbox/MGS3012.md): the stall watchdog, which is what used to
  end these runs.
- [MGS4006](MGS4006.md): stale generated output, the staleness question.
- [MGS1020](../magusfile/MGS1020.md): two targets declaring one output, the
  ownership violation with no ordering answer at all.
