RetrospectiveSafely Removing the AlgoSu Blog’s Relation Graph and Search
· AlgoSu
- #deletion
- #yagni
- #dead-code
- #refactoring
When you add code, the result is visible. A component appears on screen, tests pass, the build succeeds, and you know it worked.
Deletion is different. You remove a file, run tsc --noEmit (the type check) and fix the errors. The build succeeds. The PR merges. Only when you close out the sprint do you find out CI was broken. A script at the repository root was referencing the file you deleted, and you only ran grep inside a subfolder, so you never saw it.
I removed blog features from AlgoSu twice. AlgoSu has run through hundreds of sprints (short work cycles), and both removals happened shortly after the 190th:
The ADR relationship graph
A diagram (drawn with mermaid) showing how decision records link to each other, with its own graph page plus a small graph beside each record's detail page. Built in the 189th sprint, removed in the 193rd.
ADR search
A search box in the blog header for finding decision records by title or content. It built a search index file (
search-index.json) at build time and searched it in the browser. Built in the 157th sprint, removed in the 201st.
Both times, what I learned wasn't about YAGNI ("you aren't gonna need it") or design mistakes. It was something more practical: how to delete safely.
Problem
Every deletion carries unknown consequences. Is there a method for removing features safely?
Decision
Safe deletion is three steps: classify with grep before starting, remove dead fields along with the feature, and search the whole repository for hidden connections.
Result
Deleting the graph followed all three principles, with a clean result. Deleting search broke the third, and main broke. The difference between the two cases shows what the principles are worth.
Principle 1: Grep First
The first thing to do before deleting a feature is to use grep to separate what only that feature uses from what other parts of the code also use. Get that wrong, and the deletion quietly breaks something next to it.
Before removing search, I started by sorting the i18n keys (the names the UI uses to look up its Korean and English text) with grep:
Search-only (safe to delete)
searchPlaceholder, searchEmpty, and so on. Used only by the search box component (SearchBox)
Shared (must keep)
kindPermanent, metaSprint, and so on. Also used by the ADR cards, category tabs, sprint timeline, and sidebar
Functions were the same. Functions like toSearchDoc, which builds documents for the search index, and the SearchDoc type were search-only, so they went. But functions like buildUrl and groupByKind were still used to render ADR pages, so they stayed.
Removing the graph followed the same pattern. Graph-building functions such as buildGraph and getSubgraph were graph-only, so they were removed. But the CSS tokens for the graph's background and grid (--diagram-bg, --grid) could also be used by diagrams in blog posts and did no harm left in place, so they stayed. The "Related ADR" text links in the sidebar (RelatedLinks) were a component completely separate from the graph, so they were left alone.
The key is doing this classification before deleting anything. Once you start deleting, momentum takes over and "this is probably graph-only too" starts to feel like enough. Running grep first turns that guess into a fact.
Principle 2: Dead Fields Go Too
When you remove a feature, you often find something else: fields that were created while the feature was alive but that nobody actually reads.
When removing search, AdrIndex.searchDocs was exactly that. The function that builds the ADR index filled it on every build, but nothing ever read it. The ADR list, archive, and post pages all read other fields only. It was effectively dead, and I removed it along with search.
AdrDoc.outgoingLinks, when removing the graph, was the same story. It collected the links between documents, and only the graph-drawing functions read it. With the graph gone, the field had no purpose. The function that filled it (extractOutgoingLinks) and the regex only that function used (ADR_LINK_RE) went too.
Leaving a dead field in place does no immediate harm. But months later, reading that code again, you'll wonder "why is this field here?" Dead code muddies context. The moment you're already removing a feature is the most natural time to clean up what it leaves behind.
Principle 3: Breakage Outside
This is the most important principle, and the hardest to believe until it happens to you.
The first PR removing search looked like a self-contained change inside blog/. The search box component, the script that generated the index file (generate-search-index.mjs), the search types and fields, and the search library (minisearch) were all under blog/. I ran grep inside blog/ only. No type errors. next build succeeded. CI was pending.
The PR merged.
And main was broken.
scripts/check-adr-links.mjs, at the repository root, is a script that checks the ADR blog for broken links. It turned out it also checked that search-index.json existed in the build output. Since the index file was no longer generated, the check failed (exit code 2), and the CI job that builds the blog as static pages (Build Blog (SSG)) actually failed.
And yet the merge went through. GitHub's branch protection lets you mark certain checks as required ("this must pass before anything can merge"), and this job wasn't one of them. So even though it failed, it couldn't block the (squash) merge.
What caught it was the checks run by the sprint stop command, /stop. At the end of each sprint it runs check-adr-links.mjs locally, and that run exposed the failure. I had to open a follow-up PR that removed the index-file check from check-adr-links.mjs, leaving only the link check.
The lesson: code that references the file you deleted may live outside the folder where that file lived. The script that generated search-index.json was in blog/scripts/, but the script that checked for it was in the root scripts/. Limit grep to blog/ and that connection is invisible.
When you delete a feature, grep across the whole repository. A git grep for a name finds every reference, wherever it lives. "It's probably only in this folder" is an assumption, not a check.
How it broke outside what was deleted
Deletion as Design
When I removed the graph, the result was clean. Principle 1 used the search feature's i18n keys as its example; when removing the graph, I likewise sorted 44 i18n keys (22 Korean, 22 English) into owned and shared, removed the dead outgoingLinks field together with its extraction function and regex, and confirmed with a repository-wide grep that no references were left. No type errors, build passed, zero issues from the review agent (Critic), CI 37 passed / 0 failed.
Later, when I removed search, I followed the second principle (searchDocs removed) but broke the third. grep was limited to blog/, so the file the root check script depended on was invisible. main merged broken, the /stop checks caught it, and a follow-up PR fixed it.
The contrast between the two cases shows what the principles are for.
If adding code asks "is this needed?", deleting code asks "where is this used?" Removing a feature means discovering that it was never fully on its own. Follow the dependencies grep turns up, and the boundary appears between code that belongs only to the feature and code tied to something else.
Cut precisely along that boundary. That's the art of deletion.
Three Principles
Grep before you delete
Separate what only the feature uses from what is shared, and leave the shared parts alone.
Dead fields die with the feature
Remove fields, types, and parsers used only by the feature being deleted. Leave no dead code behind.
Grep the whole repository
Code that references the deleted file may live outside its folder. Use
git grepacross the whole repo.