Munchable is a barcode scanner for people with gut conditions. The public face of its rules engine is a condition guide per condition, for example munchable.app/conditions/coeliac-disease, plus a few hundred single-question pages indexed at munchable.app/answers. The package behind all of those is packages/rules-engine.

It is also the part of the repository that cites a document which is not in the repository, 64 times.

I did not go looking for that. I went looking for something duller.

The audit

Comments name files constantly. "See lib/x.ts". "Generated by scripts/y.mts". "The shape is in types.ts". Those references are the only cross-links a codebase has that no compiler, bundler or linter ever reads. A broken import fails the build. A broken sentence about an import fails nothing.

So the question was: how many are there, and how many still resolve?

The scanner is about thirty lines of Python. Walk every tracked file, keep markdown lines and lines that open a comment, pull out tokens that look like a path with a known extension, then try to resolve each token relative to the file that mentions it and relative to the repository root.

pat = re.compile( r'(?<![\w/@.-])' r'((?:\.{1,2}/)?(?:[\w.-]+/)*[\w.-]+\.(?:ts|tsx|mts|mjs|md|sql|json|yml|patch|png|svg|txt))' r'(?![\w/-])' ) 

Across 552 tracked files and roughly 15,000 comment lines:

distinct paths mentioned 308 total mentions 884 resolve to a real file 289 do not resolve 19 

Nineteen out of 308 reads like a tidy repository. The mention count is where it stops reading that way: those 19 tokens account for 109 of the 884 mentions, because one of them is cited 74 times in comments alone.

Sorting the nineteen

Not every unresolved path is a defect, and the split matters more than the total.

Three are not paths at all. Next.js, next.js and postgres.js are a framework and a driver that happen to end in .js. Eleven mentions, zero problems, and a reminder that an audit which reports a raw number is reporting its own regex.

Six are deliberately absent. A gitignored seed artefact mentioned in .env.example and in the script that reads it. A placeholder argument spelled path-or-url-to-ingredients.full.json. A render output named by the pattern that produces it (pack-peanut-butter.png). A file named as a plan, in a comment that says generated recipes "will land in a sibling recipes-ai.ts". And +html.tsx, an Expo Router convention file, in a comment explaining why the app cannot use it and sets the background colour on the document instead. All five of those sentences are true. The file is missing because the sentence says it is missing.

Three point into a different repository. The pricing and Stripe modules carry comments saying they are kept in step with a sibling product's lib/stripe/checkout.ts and lib/pricing/display-currency.ts. That is a real, useful reference and it is not checkable from here. Cross-repository cross-references are a category, not a bug.

One documents its own deletion, which is the best entry in the list:

 * THIS IS IRREVERSIBLE. It did not use to be: everything deleted was re-seedable * from a local artifact with scripts/seed-import.ts, and that script has been * removed now that the catalog lives in the database. 

A path that does not resolve, in a sentence whose whole point is that it should not.

That leaves the ones that are simply wrong.

The generated header that names a missing generator

Two files begin with a line telling you not to edit them by hand, and naming the script to run instead:

/** * GENERATED by apps/web/scripts/taxonomy-curate.mts / taxonomy-sync.mts * do not edit by hand. 

taxonomy-sync.mts is there. taxonomy-curate.mts is not; the curation side was consolidated into a single operator-run CLI months ago and the file was renamed. Three more mentions of it sit in .env.example, next to the variables it used to read.

This is the worst shape a stale reference can take, because it is an instruction. A comment that merely describes something is read by somebody already looking at the code. A comment that says "run this" is read by somebody who has decided to act.

Same category, one line, elsewhere: a module comment saying lib/rules/curate.ts adds the storage layer on top of it. That directory now holds curate-core.ts and overlay.ts, and nothing called curate.ts.

The deleted plan

And then there is the one with 74 mentions.

$ git log --follow --name-status --pretty='%h %ad %s' --date=short -- PLAN.md 106b23f 2026-09-02 Migrate backend to Next.js, implement OCR and menu mode D PLAN.md d1dcc71 2026-08-31 Initialize Munchable monorepo A PLAN.md 

PLAN.md was committed on 31 August with the first commit, 218 lines and about 4,100 words, and deleted on 2 September in the commit that moved the backend to Next.js. It went out with SETUP.md, 396 lines, in the same commit: 614 deleted lines, two files.

Counting every line in the repository rather than comments alone, it is cited 101 times across 38 files. 64 of those are in the rules engine. The citations are not vague either. They are precise, and they carry section numbers:

/** The digestive conditions Munchable checks (PLAN.md §14). */ 
/** Required legal and regulatory strings (PLAN.md §4, §13; SCREENS.md §0). */ 

Eleven distinct sections get cited, in 104 section references: 14 of the 101 mentions carry no number, and 16 name two or more. The histogram:

§3 9 §11 7 §4 15 §12 1 §5 13 §13 4 §6 37 §14 3 §7 3 §16 5 §9 7 

Section 6 is cited 37 times, and in the deleted file section 6 is the rules engine chapter. Section 4 is the regulatory position and section 13 is the privacy architecture, and both are cited by the one file holding the app's required legal strings, the second line of which is the comment above. Every one of those citations is load bearing in the sense that matters: somebody wrote it down because the reason for the code was somewhere else.

The somewhere else is now only in git show d1dcc71:PLAN.md.

A section number is a different kind of stale

I have written before about a comment pointing at a renamed patch file, and the conclusion there was easy: a fact with no checker is a claim that decays, so stop writing the fact. This is not that.

A stale path can be fixed by reading the code next to it. lib/rules/curate.ts is wrong, and whoever cares can find curate-core.ts in two seconds. A section number cannot be fixed by reading the code, because the whole reason it exists is that the information is not in the code. "§6" is a promise that a specific paragraph exists somewhere and can be checked. When the document goes, the citation does not become inaccurate. It becomes unanswerable.

The failure mode is also inverted. A wrong path annoys the reader immediately. A citation of a deleted document looks completely fine until somebody tries to use it, and the person most likely to try is the person with the least context: a new contributor asking why the engine must stay deterministic, or why a condition list may not leave the device.

Two CI workflows make that concrete. Both end their setup comment the same way:

# Needs the EXPO_TOKEN repo secret - an EAS access token from # https://expo.dev -> account -> Access Tokens. SETUP.md section 2. 

Section 2 of the deleted SETUP.md is indeed the Expo and EAS chapter. The pointer is accurate, and the only reader who needs it is someone setting up the pipeline for the first time, who is exactly the reader who cannot follow it.

What to do with 101 citations

Three options, and I rejected two.

Delete the citations. Tempting, 101 edits, and it throws away real information: that a decision was deliberate, and roughly where its reasoning lived. A bare comment saying "the engine is deterministic" is weaker than one saying it was decided in a written plan.

Rewrite them to point at code. Right for about a third of them, where the code now is the source of truth. Wrong for the rest, because "why" does not live in a module.

Put the document back. This is the one I am taking. It was deleted as superseded, and it was superseded as a build plan: the stack chapter describes an architecture that is two migrations out of date. But the product chapters are not out of date at all, and 101 citations are 101 votes that they are still being used. It returns as a historical record, under a name that says so, with a header saying which chapters have been overtaken and by what. Then every existing citation resolves again without a single one of them being edited.

The rule I would write on the wall: if a comment cites a document by section, that document is a dependency. Treat deleting it like deleting a module with 101 importers.

The one my own scanner could not see

The earlier version of this problem was a comment in the camera screen naming the patch file that adds tap to focus. The patch had moved from expo-camera@57.0.4.patch to 57.0.5, and the comment stayed on 4.

My scanner does not find it. The token is patches/expo-camera@57.0.5.patch, and the lookbehind in that regex rejects a match preceded by @, which was there to stop npm scopes and email addresses from producing fake paths. One character of defensive matching, and the audit is blind to every patch file in the repository, which is the one category of path that already proved it could go stale.

So the honest version of the headline number is: 884 mentions that this regex can see. Every audit ships with a blind spot, and the useful question is not whether yours has one but whether you can say where it is.

If you want to see what the engine those 64 citations describe actually produces, it is readable without installing anything: munchable.app/conditions/coeliac-disease is one condition guide, and munchable.app/answers is the index of the single-question pages. The app itself is linked from munchable.app.