Here is a decision record sitting in a project memory file, written eight months ago, with a decision record file reference that no longer resolves to anything:

### 2026-02-14 — Round VAT per line, not per invoice
Decision: Round VAT at the line level, in `src/billing/invoice.ts`.
Why: The accounting export reconciles line by line.
Status: Active

Nothing is wrong with how that is written. The problem is that src/billing/invoice.ts does not exist. It was split into a billing/ module during last month's cleanup, and the rounding logic now lives somewhere inside it, or possibly in a shared tax/ helper, or possibly nowhere because someone decided the ORM should handle it. That path was the only thing tying the reasoning to the code, and it now resolves to nothing. You cannot tell whether the decision still holds, whether it was quietly dropped during the refactor, or whether it survived under a different filename.

What follows is a walk through that hypothetical case: a small billing refactor, one orphaned decision record, and what writing the record differently would have changed. The project and the paths are made up, but the failure is not unusual, and it is distinct from the two decision-record problems people usually plan for.

This is not a stale decision, and not a bloated file

Two failure modes get most of the attention. One is a decision whose content went out of date, where the choice was reversed and the record was never updated. That is a content problem, and the case for writing a reversal record instead of editing the old entry covers how to handle it. The other is a memory file that grew until nobody reads it, which is a volume problem, handled by pruning.

An orphaned decision record fails differently. The content is accurate. The volume is fine. The record is structurally broken, because the path it names is a dead link into your own codebase. And it fails silently: nothing lints a Markdown file against your source tree, so the record keeps reading as authoritative right up until someone acts on it.

What makes it worse is that your assistant does not notice either. Feed that memory entry into a fresh session and you get a stale file reference in AI context, which the assistant handles badly in both directions. Sometimes it goes looking for src/billing/invoice.ts, does not find it, and treats the whole entry as outdated. Sometimes it takes the entry at face value and proposes recreating the file.

A decision record file reference is the most fragile line you write

Writing the path is the right instinct. When you make a decision during a session, the file is open in front of you. The decision is about that file. Writing "in src/billing/invoice.ts" takes four seconds and saves the next reader a grep, which is exactly the trade project memory is supposed to make.

The path also does real work for the reader. "Round VAT at the line level" is abstract until you can see the loop it applies to. A pointer to the code turns a policy statement into something checkable: open the file, confirm the rounding is where the record says it is, move on.

So the answer is not to stop naming files. The answer is to notice that a path is the most volatile thing in the entry. The reasoning behind line-level rounding will be true in three years. The filename is one git mv away from being wrong, and refactors move files for reasons that have nothing to do with your decision.

What the refactor actually did to that pointer

Three outcomes, and they are not equally bad.

Moved. src/billing/invoice.ts became src/billing/invoice/totals.ts. The code is intact, the reasoning holds, and a reader who knows the codebase recovers in under a minute. The record is wrong but harmless.

Merged. The invoice file was folded into a larger billing/pipeline.ts, and the rounding is now three functions deep inside it. The record still points at a real concept but no longer points at a readable location. "See pipeline.ts" is barely more useful than no reference, because the file is 600 lines and the thing you are looking for is a single Math.round call.

Deleted. The logic was replaced. Maybe the ORM rounds now, maybe a tax library does it. Here the broken path is a genuine signal that something happened, and the record may be describing a decision that was overridden without anyone writing it down. This is the one that misleads people, because the entry still says Status: Active.

Only the third case is a decision problem. The first two are navigation problems wearing the same costume, which is why you want a record that degrades gracefully instead of one that just breaks.

Write the reasoning so the path is optional

The test is simple: delete every file path from the entry and read what is left. If it still tells you what was decided and why, the path was a convenience. If the entry becomes meaningless, you wrote a bookmark instead of a decision.

The original entry fails that test. Here is one that passes:

### 2026-02-14 — Round VAT per line, not per invoice
Decision: VAT is rounded on each invoice line, then summed.
  Never sum raw amounts and round the total.
Why: The accounting export reconciles line by line; rounding the
  total produces one-cent mismatches the finance team has to chase.
Scope: Anywhere invoice totals are computed.
  At the time of writing: `src/billing/invoice.ts` (`computeLineTax`).
Status: Active

Four things changed. The decision is stated as a rule about behaviour, not about a file. The reason names the consequence of getting it wrong, which is what tells a future reader whether the decision still matters. The scope is described in terms of the system ("anywhere invoice totals are computed") rather than one location. And the path is marked as a snapshot, with a function name beside it, because computeLineTax is far more greppable than a filename and tends to survive moves that filenames do not.

That last detail is worth taking seriously. Symbol names outlive paths. A function, a type, an error code or a config key gives the reader something to search for when the directory structure has changed underneath them. If the symbol was also renamed, you are back to reading code, but you have narrowed it to a module instead of a repository.

The same discipline applies to CLAUDE.md decision records and anything else an assistant reads as ground truth. Instructions that say "follow the pattern in src/api/handler.ts" break the same way, and they break louder, because the assistant will cheerfully invent a pattern when it cannot find the file.

A pass that takes ten minutes after a reorg

Treat a project memory refactor cleanup as part of the refactor, not as maintenance you will get to later. Right after you move files, before the branch merges:

  1. Grep your memory and instruction files for path-shaped strings. grep -rnE '\[a-z0-9_./-]+.(ts|py|go|rs|sql)`' .prjcontext/ CLAUDE.md AGENTS.md` catches most of them.
  2. For each hit, check whether the file still exists. The ones that do are fine; leave them alone.
  3. For each dead path, decide which of the three cases you are in. Moved or merged: update the path and add the symbol name if there is one. Deleted: find out what replaced it before you touch the record.
  4. If the logic was genuinely replaced, do not patch the path. Write a reversal entry saying the decision was superseded, and what by. Editing the old entry to point at new code hides the fact that a decision changed.

Step 3 is the one people get wrong under time pressure. Repointing a path at the nearest plausible file makes the record look healthy while quietly attaching old reasoning to new code.

Checking what actually happened to a file is easier when the memory and the source moved through the same history. That is the part we built PrjLab for: your .prjcontext/memory/ entries and your source files sit in one repository, every push is an immutable version with a list of changes and a line diff, so you can open the version where src/billing/invoice.ts stopped appearing and read the same push that touched (or did not touch) the decision record beside it. Diffing context between pushes goes into that comparison in more detail, and the getting-started guide covers prj status and prj push if you want to try it on a project you already have.

None of this requires tooling, though. A record that states the rule, the consequence of breaking it, and the symbol to grep for will survive a reorg on its own. The path is there to save the reader a search, and it should be written like something that might not be true next quarter.