Stale Rework Protection Is a Lifecycle Contract

code review governance software delivery state management testing Jul 28, 2026

When a team allows work to be revised and reworked — a document, a plan, a piece of code — there is an easy trap: assuming that "prevent stale overwrites" is a small file-comparison problem. It isn't. It's a lifecycle-contract problem, and treating it as anything smaller tends to produce a fix that looks correct until a specific ordering of events proves otherwise.

The problem

In any workflow where an item can be revised, reviewed, and sent back for rework, there needs to be one trustworthy answer to a deceptively simple question: what is the current version of this thing right now? That answer isn't just "which file is newest." It's a bundle of connected facts:

  • which revision is currently active,
  • which review or audit applies to that revision,
  • what that review concluded (accepted, accepted with noted risk, needs fix, blocked),
  • and what has already been published or routed downstream based on that conclusion.

If those facts live in different places — or worse, get re-derived independently by different parts of the system — then it's only a matter of time before one path acts on stale information while another path has already moved on. The result isn't a crash. It's quieter and worse: an old version of the work silently becomes the "final" one.

What actually happened

An audit of a rework-handling system found this exact gap. Several independent paths each built their own idea of "what's current": automated drafting steps, manual handoff points, a generic context-loading component, identity handling for a later build stage, archive and reset logic, and the routing logic that ran after review. Each of these assembled its own source of truth. None of them shared one contract.

Fixing this properly required five connected pieces, not one patch:

  1. Expected-parent capture and compare-before-publish — before writing a new result, confirm it was built on top of the version that is currently active, not an older one.
  2. Rework-context parity — every path that hands work back for revision must supply the same context, not a partial or differently-assembled one.
  3. Immutable-enough revision identity for build-stage artifacts — identity needs to be resistant to being spoofed or coincidentally matched.
  4. Verified archive integrity — when older versions are archived, confirm the archived content is actually intact, not just that a file or folder exists.
  5. Durable "active version and applicable audit" state — the system needs a persistent record of what's current and what was decided about it, not a value it reconstructs on the fly each time.

The most important discovery came from testing item five in isolation: identity and audit linkage alone weren't enough. The routing logic also needed the normalized outcome of the audit — accepted, accepted with risk, needs fix, or blocked — stored durably. Without that, a process that resumed after an interruption had to re-read transient audit output or simply guess whether to advance, block, or send the item back for rework. The fix added three outcome fields to the existing durable record rather than inventing a new state model — a narrow, additive change to an existing authority boundary rather than a new one.

A second, subtler bug involved order. A rework-attempt limit was being checked before the audit outcome was made durable. That meant an already-exhausted retry counter could block an outcome that should have gone through cleanly — including outcomes that weren't even rework in the first place. Moving the limit check to run only after the durable outcome was recorded, and only on the rework path specifically, preserved the intended behavior.

A third issue involved what "identity" means for a build-stage artifact. Identity based only on commit and file contents missed a real case: a mode-only change (for example, a file becoming executable) on an already-modified tracked file. The fix made the identity fingerprint sensitive to both content and relevant filesystem mode.

None of these were fixed piecemeal. The eventual design introduced one shared "active version" boundary that every consuming stage relies on, and shifted the source of truth for routing decisions from transient audit output to normalized durable state — without changing the intended outcomes those routes were already supposed to produce.

One more subtlety worth naming: the entire active-version record is a single trust surface. A malformed value in one part of that shared record has to block processing everywhere it's used, not just in the stage that happens to be looking at it directly. Validating only the current stage's slice of shared state was not sufficient.

The lesson

"Current version" is not a file. It's a small bundle of facts that must be tracked together and durably: artifact identity, the audit that applies to it, the normalized outcome of that audit, whether and when it was published, and who has the authority to route it next. If any one of those facts is missing, inferred, or independently reconstructed by more than one part of the system, you have not actually solved the stale-overwrite problem — you've just made it less likely to show up in the common case.

The broader principle

Any workflow with retries, revisions, or hand-backs for rework needs one lifecycle contract, defined once and shared by every stage that touches it, rather than each stage inventing its own local check. A few principles generalize well beyond this specific case:

  • Normalize before you route. Routing decisions should be made from durable, normalized state — not from re-reading raw output that might not exist anymore if the process resumes later.
  • Fail closed on missing durable state. If a stage doesn't have the structured fact it needs to move forward safely, the right move is to stop and require the missing fact to be recorded properly — not to infer it, guess it, or patch around it locally.
  • Validate the whole shared surface, not just your corner of it. If several stages read from one shared state record, a malformed value anywhere in that record is everyone's problem.
  • Identity has to match the thing you actually care about. For text-like artifacts, content identity may be enough. For artifacts tied to a working directory or build process, identity may need to account for things like file mode, not just content.
  • Ordinary tests don't prove refusal logic works. The strongest verification came from deterministic tests that intentionally completed work in the "wrong" order, checked that refused content was byte-identical to what it should have been, and — as a final check — temporarily reintroduced the old defect to confirm the new tests actually caught it.

How to apply it

For any system where work moves through drafting, review, and possible rework, it helps to explicitly define, up front:

  • the active artifact identity, and what "identity" means for that artifact type,
  • the expected parent version that new work should be built on,
  • the audit or review that applies to the current version,
  • the durable, normalized outcome of that review,
  • the guard that prevents publishing over a version that's already moved on,
  • and the archive and recovery behavior for superseded versions.

From there, define the full state-transition table before writing code: absent, malformed, stale, superseded, accepted, accepted-with-risk, needs-fix, blocked. Treat every consuming stage as a client of one shared contract with stage-specific identity formats, not as an independent implementer of its own version-tracking logic. And build in discriminatory tests — deterministic overlapping-completion scenarios, negative controls, and a check that your new test actually fails against the old, buggy behavior — as a standard part of verifying any refusal or guard logic, not an optional extra.

Stale-rework protection looks, from the outside, like a small file-selection detail. It's really a question of whether your system has one place it can trust to answer "what's current, and what happened to it" — and whether every stage that needs that answer is required to go through it.