forward-only change
rule
when the correct target state is known, update every owner and consumer in one change, then delete the obsolete path. no history in authored code: push directly to the latest desirable state. compatibility aliases, fallbacks, dual routes, and deprecated behavior preserve ambiguity and make the next repair harder.
what keeping both paths costs:
- a search for callers finds two versions; an agent editing one misses the other.
- a feature lands in
newFunctionbut notoldFunction— one caller gets it, the other does not. - a refactor touches
oldFunction, creating silent divergence. - a year later no one knows which is correct or what the difference means.
- the second agent fixing a bug rewrites the path they found first, leaving the other broken.
- each new repair doubles the risk: two paths are independent facts instead of one truth.
why
compatibility paths convert one decision into two live facts. every future search, feature, and repair must establish which path is current, and the answer decays with each edit touching only one. deleting the obsolete path in the same change keeps the invariant: a search for a responsibility finds one current implementation — which is what makes search, review, and the next repair cheap.
- inventory callers, imports, contracts, docs, generated inputs, and storage.
- make the target change.
- regenerate artifacts from their owner.
- search for stale names and paths.
- verify the affected boundary.
decide whether a compatibility path is real
forward-only does not mean ignore a required migration boundary: make the target state explicit and remove authored legacy behavior once all owned consumers have crossed it. framework-mandated migration files and generated artifacts remain at their tool boundary — evidence, not a reason to retain old application aliases.
| situation | direction |
|---|---|
| rename an owned public symbol or file | update every caller/import/reference, then remove the retired name. |
| change generated contract input | update the source, regenerate, and remove handwritten shadow types. |
| rename durable schema object | create a forward migration with explicit recovery/down behavior; never edit history. |
| external consumer contract requires a transition | document the approved compatibility window and its deletion owner before retaining it. |
| no external authority requires old behavior | do not keep an alias, fallback, or dual flow. |
nearest AGENTS.md requires compatibility | follow that narrow instruction, record its boundary and deletion condition, and retain nothing else. |
an old path is not proof it deserves support. only an explicit owner instruction or a consumer outside the repository with a real transition need can retain compatibility. "safer", "someone may use it", "it is easier to leave" are not reasons. compatibility is a temporary owned product behavior, not a default engineering precaution.
verification
- capture the retired paths/names before changing them.
- search every one afterward; regenerate owned artifacts; exercise the consumer boundary.
- a legacy match must be either an immutable history record or an explicitly approved transition with an owner and deletion condition.
do not
- keep old exports, paths, flags, redirects, adapters, fallback flows, or aliases without an approved need.
- leave deprecated comments, docs, tests, generated inputs, or feature flags after the forward-only move.
- rewrite historical migrations.
good and bad
bad
export const readWorkspace = readProjectWorkspace;export const readProjectWorkspace = readWorkspaceById;
two public names claim one responsibility. searches, callers, and future edits cannot establish which name is current; the next change lands on the path the author found first while the other silently diverges. each edit touching only one alias widens the split; the eventual repair is an audit of every caller instead of a rename. one decision has become two live facts kept in sync by hand forever.
good
export const readWorkspaceById = async (workspaceId: string) => {return workspaceRepository.readWorkspaceById(workspaceId);};
rename every caller to the selected symbol, verify the boundary, and remove the retired export in the same change. one search for a responsibility finds exactly one live implementation, so review, blame, and the next rename are single-surface operations. no alias survives, so no future edit can partially update the truth, and each forward-only change leaves the codebase smaller to understand than before.