The Art of Deletion

retrospectivedeletionyagnidead-coderefactoring

When you add code, the result is visible. A component appears on screen, tests pass, the build succeeds — you know it worked.

Deletion is different. You remove a file, fix the type errors tsc --noEmit surfaces, the build succeeds, you deploy. Then CI breaks three days later. A script in the root directory was referencing the file you deleted, and you only ran grep inside a subdirectory, so you never saw it.

I deleted features twice in AlgoSu. The ADR relationship graph, built in Sprint 189, was removed in Sprint 193. The ADR search, built in Sprint 157, was removed in Sprint 201. Both times, what I learned wasn't about YAGNI or design failures — it was something more practical: how to delete safely.


Problem

Every deletion carries unknown consequences. Is there a methodology for removing features safely?

Principle 1: Grep Before You Delete — Owned vs. Shared

The first thing to do before deleting a feature is to use grep to distinguish assets the feature owns from assets shared with other parts of the codebase. Get that classification wrong, and a deletion silently breaks something adjacent.

Before starting on Sprint 201 (removing search), I split the i18n keys with grep:

  • Search-only (safe to delete): searchPlaceholder · searchAriaLabel · searchEmpty — referenced only inside SearchBox
  • Shared (must preserve): kindPermanent · kindTopic · kindSprint · metaSprint — also used in adr-card · adr-category-tabs · sprint-timeline · adr-meta-sidebar

The same logic applied to lib functions. toSearchDoc · toPlainText · the SearchDoc type were search-only → removed. But buildUrl · groupByKind · mapBySprint · filterAdrsByTopic were still needed across ADR rendering → preserved.

Sprint 193 (removing the graph) followed the same pattern. buildGraph · getSubgraph · filterAdjacency · mergeTargets were graph-only → removed. But the --diagram-bg and --grid CSS tokens could still be used by in-post mermaid diagrams, so they stayed. The "Related ADR" text link list in the sidebar (RelatedLinks) was an entirely independent component from the mermaid graph, so it was untouched.

The key is doing this classification before any deletion begins. Once you start deleting, momentum sets in and "this is probably graph-only too" starts feeling like enough. Running grep first turns that guess into a fact.


Principle 2: Dead Fields Die with the Feature

When you remove a feature, you often discover something alongside it — fields that were created while the feature was alive, but that nobody ever actually reads.

In Sprint 201, AdrIndex.searchDocs was exactly that. buildAdrIndex populated it on every build, but nowhere in the codebase was it ever read. adr/page.tsx · archive/page.tsx · post-page.tsx all accessed only .all · .byKind · .bySprint. The field was effectively dead. When the search feature was removed, this field was cleaned up alongside it.

AdrDoc.outgoingLinks in Sprint 193 was the same story. It was data generated exclusively for buildGraph and mergeTargets. Once the graph was gone, the field lost its purpose entirely. The extraction logic (extractOutgoingLinks) and its dedicated regex (ADR_LINK_RE) were removed together.

Leaving a dead field in place causes no immediate harm. But months later, reading that code again, you'll wonder "why is this field here?" Dead code pollutes context. The moment you're already removing a feature is the most natural time to clean up its leftovers.


Decision

Safe deletion is three steps: grep-classify before starting, remove dead fields alongside the feature, and probe for hidden coupling across the entire repository.

Principle 3: What Breaks Lives Outside What You Deleted

This is the most important principle, and the hardest to believe before you experience it firsthand.

The first PR removing search in Sprint 201 looked like a self-contained change inside blog/. SearchBox component · generate-search-index.mjs script · SearchDoc type · AdrIndex.searchDocs field · the minisearch dependency — all of it was under blog/. I ran grep within blog/. tsc passed. next build succeeded. CI was pending.

The PR merged.

main was broken.

The root scripts/check-adr-links.mjs had logic to verify the presence of the build artifact search-index.json, exiting with code 2 if it was missing. Since search-index.json was no longer being generated, this gate now failed on every run. The CI Build Blog (SSG) job actually failed.

And yet the merge went through. This job wasn't listed as a required check in branch protection — so even though it failed, it couldn't block a squash merge.

What caught it was the /stop gate. The sprint-close workflow runs check-adr-links.mjs locally, and that run surfaced the failure. A follow-up PR was needed to remove the search-index validation block from check-adr-links.mjs, leaving only the internal link integrity check.

The lesson is straightforward. Code referencing the file you deleted may live outside the directory where that file lived. The script that generated blog/public/adr/search-index.json was under blog/scripts/, but the gate that verified its existence was in the root scripts/. Scoping grep to blog/ makes this connection invisible.

When you delete a feature, grep scope must be the entire repository. A git grep on the symbol name will find every reference, no matter where it lives. "It's probably only in this directory" is an assumption, not a verification.


Result

Deleting the graph followed all three principles — clean result. Deleting search violated the third — main broke. The difference between the two cases is the value of the principles.

Deletion Is Design

When I deleted the graph (Sprint 193), the result was clean. I classified 44 i18n keys (22 KR · 22 EN) as owned or shared, removed the dead outgoingLinks field together with extractOutgoingLinks and ADR_LINK_RE, and verified with a full-repo grep. tsc 0, build passed, Critic 0 issues, CI 37 pass / 0 fail.

When I deleted search (Sprint 201), I followed the second principle (searchDocs removed) but violated the third — grep was scoped to blog/, and the root gate's dependency was invisible. main merged broken. The /stop gate caught it. 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 is a process of proving it was never fully self-contained. Following grep results as they map out dependencies reveals the boundary between code you own outright and code with ties to something else.

Cutting along that boundary precisely — that's the art of deletion.


Three Principles

  1. Grep before you delete — Classify owned vs. shared assets and leave shared ones untouched.
  2. Dead fields die with the feature — Remove fields, types, and parsers whose only consumers are inside the feature being deleted. Leave no dead code behind.
  3. Grep scope is the entire repository — References to the file you deleted may live outside its parent directory. Use git grep and open the whole repo.

Related ADRs

Decision records behind this post

sprint-157

ADR md → Human-Friendly HTML Dual Output Automation (blog integration + KR/EN UI + content i18n infra)

Keep ADR md files as LLM-friendly SSOT while automatically generating an HTML site that lets humans quickly grasp decision flow, impact, and context

sprint-189

Blog UI/UX revamp — Graph legend/filter + Category 7-class + ADR topic auto-classification (Phase 5)

5-phase blog revamp complete — post category 7-class, ADR topic frontmatter auto-classification (KR SSOT + EN injection), and graph legend/filter (WCAG AA) round out the portfolio navigation UI.

sprint-193

Full Removal of the Blog ADR Graph Feature

Remove the mermaid-based ADR relationship graph added in Sprint 189 (the dedicated /adr/graph page + the mini graph in the detail sidebar) in full. Delete 5 graph-only components, lib logic (buildGraph/getSubgraph/filterAdjacency), dead data (outgoingLinks), 44 i18n keys, and fixtures F7/F8 — while preserving the graph-independent 'Related ADRs' text links, blog body mermaid, and the --diagram-bg/grid tokens. tsc 0, build (routes gone), 7 gates no-regression, Critic 0 findings, CI #339 37 pass / 0 fail.

sprint-201

Remove Blog ADR Search (SearchBox/MiniSearch)

Completely removed the MiniSearch-based client-side full-text search (SearchBox) from the blog ADR site header. The search — introduced in Sprint 157 (minisearch dependency + build-time search-index.json generation) — is independent of ADR rendering (list/detail/archive/topics), so removal has no impact on other ADR display features. Before starting, grep separated search-only assets from shared assets — removed only search-only assets (minisearch dependency, search* i18n keys, SearchDoc type, AdrIndex.searchDocs dead field, toSearchDoc/toPlainText) while preserving shared assets (kind*/metaSprint i18n keys, buildUrl/groupByKind/mapBySprint/filterAdrsByTopic functions). The first PR #355 (d4660b0) cleaned up blog/ internals, but because the grep scope was limited to blog/, it missed that the root scripts/check-adr-links.mjs validates the (now-deleted) search-index.json existence (exit 2) → CI #355's Build Blog (SSG) job was actually a failure, but since that job is not a branch-protection required check, the squash merge proceeded and main was merged in a broken state. The /stop gate's local check-adr-links run caught it → a follow-up PR removed the search-index validation in check-adr-links.mjs + tidied the ci.yml paths-filter comment. tsc 0 · next lint 0/0 · build 329 pages (search-index.json not regenerated) · check-adr-links KR/EN exit 0 · doc-refs 363 0 broken · grep residue 0 · Critic (Codex) Critical/High 0.

Related Posts