{"slug": "twelve-of-thirteen-stale-docs-were-kept-alive-only-by-each-other", "title": "Twelve of thirteen stale docs were kept alive only by each other", "summary": "A developer built mdsweep, a dependency-free Node.js tool that scans repositories for stale markdown files left behind by coding agents and sorts them into active, stale, and orphan buckets. Testing across 36 repos with roughly 1,200 markdown files revealed that 12 of 13 old files in one reports folder were kept alive only by circular references among themselves, showing that inbound link counts alone cannot distinguish live documentation from dead citation clusters. The tool moves orphans to a timestamped trash directory with an undo command rather than deleting them.", "body_md": "My coding agents are prolific note-takers. Every non-trivial session leaves a PLAN.md, a SUMMARY.md, a HANDOFF.md, sometimes a FINAL_REPORT_V2.md. Across 36 repos on this machine there are about 1,200 markdown files, and I could not tell you which ones anybody still opens.\n\nThe obvious cleanup is a glob and a date filter:\n\n```\nfind . -name 'PLAN*.md' -o -name 'SUMMARY*.md' -mtime +30 -delete\n```\n\nI did not run that, because I already knew what it would hit. A PLAN.md from May can still be the file a README points at when it explains why the approach changed. Age alone says nothing about whether a file is load-bearing.\n\nSo the next rule: keep anything that something else links to. Count inbound references, and if the count is above zero, the file is still wired into the repo. Old and unreferenced means safe to move.\n\nThat rule survived about a day. Here is the scan that killed it, from a trading repo with a `reports/` folder:\n\n```\nreports/E_F_G_RESULTS.md              116d  stale  refs=1\nreports/FINAL_HONEST_PLAN.md          116d  stale  refs=1\nreports/FUNDING_HARVEST_REPORT.md     116d  stale  refs=2\nreports/HONEST_FINDINGS.md            116d  stale  refs=1\nreports/J_SUMMARY.md                  114d  stale  refs=1\nreports/FINAL_REPORT.md               114d  stale  refs=6\n... 13 files, all 114-116 days old, every one with refs > 0\n```\n\nEvery file passed the \"someone links to it\" test. Then I traced where the links came from:\n\n```\ngrep -rl \"J_SUMMARY\\.md\" --exclude-dir=.git .\n# reports/L1_AUDIT_REPORT.md\n```\n\n`L1_AUDIT_REPORT.md` is in the same folder and is just as dead. I walked all thirteen. Twelve of them had every single inbound link coming from another file inside `reports/`. The folder was citing itself in a circle. Only `FINAL_REPORT.md` had a reference from outside: `mql5/README.md`, and a Python script that actually runs.\n\nA reference count of 1 was not evidence that anyone used the file. It was evidence that the agent wrote two files in the same session and made one mention the other.\n\nI stopped trying to find a rule that decides. The scanner now sorts into three buckets and hands the ambiguous one back to me:\n\n```\nh.grade = h.ageDays <= days   ? 'active'   // touched recently, leave it\n        : h.refs > 0          ? 'stale'    // old but linked, I read these by hand\n        :                       'orphan';  // old and unlinked, movable\n```\n\nAcross those 36 repos: 687 active, 172 stale, 176 orphan. The 172 in the middle are exactly the files the glob would have eaten, and the ones the reference rule would have blessed. They are not a category the tool can resolve, so it doesn't pretend to.\n\nOrphans get moved rather than deleted. `fs.renameSync` puts them in `.mdsweep/trash/<timestamp>/` next to a manifest that records every original path. `mdsweep undo` renames them back. Nothing in your tree gets unlinked; the only `rmSync` in the codebase targets the trash directory itself after a full restore.\n\nThe same scan flags 1,035 of 1,199 files, which is 86%, and that number is almost meaningless. One of the three detection signals is \"untracked by git,\" so a repo with a messy working tree lights up wholesale. The flag rate measures my hygiene, not the tool's precision. The grading is the part that earns its keep.\n\nThe `stale` bucket is also a genuine dead end, not a staging area. The reports folder above proves the tool cannot distinguish a live citation cluster from a dead one, and I do not think heuristics will get there. Thirteen files is a two-minute read for a human. Thirteen hundred would not be, and I have no answer for that yet.\n\nIt is a single `.mjs` file, no dependencies, Node 18+. It reads your repo and prints a table; you have to pass `--apply` before it touches anything.\n\n```\ngit clone https://github.com/szp2005/mdsweep\nnode mdsweep/bin/mdsweep.mjs scan ~/code/your-repo\n```\n\nI built it ([szp2005/mdsweep](https://github.com/szp2005/mdsweep)) for my own repos, and the reports folder above is why the middle bucket exists at all. If your agents also write more markdown than you read, the scan is read-only, so the worst case is that you find out your repos were fine.", "url": "https://wpnews.pro/news/twelve-of-thirteen-stale-docs-were-kept-alive-only-by-each-other", "canonical_source": "https://dev.to/szp2005/twelve-of-thirteen-stale-docs-were-kept-alive-only-by-each-other-2mk8", "published_at": "2026-09-11 02:12:31+00:00", "updated_at": "2026-09-11 02:56:04.743656+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-tools"], "entities": ["mdsweep", "GitHub", "Node.js", "szp2005"], "alternates": {"html": "https://wpnews.pro/news/twelve-of-thirteen-stale-docs-were-kept-alive-only-by-each-other", "markdown": "https://wpnews.pro/news/twelve-of-thirteen-stale-docs-were-kept-alive-only-by-each-other.md", "text": "https://wpnews.pro/news/twelve-of-thirteen-stale-docs-were-kept-alive-only-by-each-other.txt", "jsonld": "https://wpnews.pro/news/twelve-of-thirteen-stale-docs-were-kept-alive-only-by-each-other.jsonld"}}