Every AI coding assistant I pointed at this codebase made the same mistake within its first five minutes: it edited a generated file that gets overwritten on the next build. Confidently. With a clean diff and a nice explanation. And the moment someone rebuilt the front-end bundle, the change vanished.
The codebase is a classic-ASP app running on Windows/IIS that has been in production for years. Its front-end JavaScript and CSS ship as date-stamped compiled files. Picture a name like ManageRoster260224.js, where 260224 is the build date. That file is build output. The real source lives one directory deeper, under a src/ folder, with the date stripped off: .../scripts/src/ManageRoster.js. You edit the source, the build regenerates the dated file, and the dated file is what the browser actually loads.
A human learns this once and never forgets it, because the first time your change disappears you feel it. An AI assistant does not get that feedback loop. It greps for the symbol you mentioned, finds it in the generated CSS file (because of course the symbol is in there, that’s the compiled output), edits it, and reports done. The grep was technically correct and the result was technically wrong.
Why grep finds the wrong file
The trap is sharper than “there are two copies.” If the dated file and the source file were both sitting in the same folder, any assistant would notice the naming pattern and ask. They aren’t. The references to the compiled path live in server-side includes buried in the page-layer files, not in the HTML you’d think to look at. So when an assistant traces “where does this script get loaded,” it lands on an ASP include pointing at the dated build artifact, follows that path, opens the generated file, and starts typing.
Nothing about that chain looks suspicious in isolation. The include really does reference ManageRoster260224.js. That file really does contain the function. The only thing wrong is the layer: you’re editing the bottom of the stack instead of the top. And there is no compiler error, no failing test, no red squiggle to catch it. The change works in the moment and rots on the next build.
I corrected this in chat over and over. “Don’t edit the dated file, edit the src version.” “Strip the date, look under src/.” Each new session started clean and made the same mistake again, because chat context does not survive a new conversation and a fresh agent inherits none of my hard-won corrections. I was paying the explanation tax on every single session.
The fix is config, not conversation
If I keep saying the same sentence to the AI, that sentence wants to be a rule, not a message. Cursor reads .mdc rule files out of .cursor/rules/, and a rule with a globs field fires automatically on every edit that matches the pattern. It is part of the project, checked into the repo, applied without anyone remembering to mention it.
So I wrote .cursor/rules/edit-js-css-src-only.mdc. The front-matter scopes it:
globs: "**/*.js", "**/*.css"alwaysApply: falseThe body is the part that matters: an explicit “strip the date, insert src/” resolution table, with worked examples so there’s no ambiguity about the transform. The canonical one:
ManageRoster260224.css -> [output-dir]/src/ManageRoster.cssWith that rule enabled for matching files, the source-versus-output convention is available before the edit happens. It no longer depends on me repeating the correction from a prior chat. The convention travels with the code.
That single rule killed the most expensive recurring correction in the project. But once I had the mechanism, the obvious next move was to encode the rest of the tribal knowledge that bites every fresh agent the same way. In the same commit I shipped four more:
asp-page-object-pattern.mdc: thePage.prototypeOO convention this admin uses for its client-side pages, so a new assistant builds in the house style instead of inventing its own structure.bottom-up-function-ordering.mdc: the ordering convention for function definitions in these files.chrome-devtools-mcp.mdc: how to drive the browser for this project when verifying a change.local-dev-domain.mdc: the local development domain, so the assistant tests against the right host instead of guessing.
None of those is profound. Each is one paragraph of “here’s how we do it here” that I would otherwise type into a fresh chat every session.
A prompt is a sentence, a rule is a behavior
A prompt is something you say. A rule is something the tool does. When you put a convention in your prompt, you are hoping you remember to include it, hoping it survives the context window, hoping it carries to the next session. When you put it in a rule with a matching glob, it fires whether or not you’re thinking about it, on the exact files it applies to, every time.
The cost asymmetry is what sells it. Re-explaining a gotcha in chat is cheap per instance and expensive in aggregate: it is a few sentences repeated across sessions, plus the cost of the times the correction is missed. Writing the rule takes upkeep too, but it turns a repeated conversation into a project artifact that can be reviewed and improved.
Codebase conventions that bite humans bite AIs harder, because the AI never gets the painful feedback loop that teaches a human to stop. A new developer edits the generated file once, watches their work disappear, and internalizes the rule permanently in about four seconds of frustration. The AI feels nothing, learns nothing across sessions, and will do it again tomorrow with total confidence. The only durable place to put that knowledge is somewhere the tool reads automatically, right next to the code, in a format that fires on the matching files.
Related
- Writing an AI_CONTEXT.md So Your Assistant Stops Rediscovering Your Codebase: another way to preserve repository knowledge across fresh sessions
- Scope Is the Unit, Not Persona: Rethinking How You Load Context Into AI Agents: how to decide what belongs in project-scoped agent context