3 a.m. The pager goes off. The release that shipped six hours ago is hiccuping: every few minutes, the process crashes somewhere inside its shutdown path. Roll back. Sleep. Postmortem.
The cause turns out to be a pull request from a new hire’s first week — a change so small it barely deserved the name. In get_timeout_ms he had spotted a clearly redundant check:
int get_timeout_ms(void) {
if (g_cfg == NULL) { // review: "g_cfg is initialized at startup; it can't be NULL"
return DEFAULT_TIMEOUT; // he deletes the two lines; three approvals; merge; ship
}
return g_cfg->timeout_ms;
}
Three reviewers looked at it and agreed with him: g_cfg is initialized on the third line of main and never reset, so on every normal path the check is dead code. Deleting it isn’t a risk — it’s hygiene.
The crashes say there is an abnormal path. During shutdown, the hot-reload thread frees g_cfg first, and the requests draining out of the system query the timeout after that. The window is tens of milliseconds — too narrow to hit under daytime traffic. At 3 a.m., when the batch jobs all exit at once, it hits every time.
After the rollback, somebody runs git blame on the deleted lines (real output from this article’s demo repository; construction in Appendix D):
afd40ae8 (zhangsan 2016-04-19 03:20:00 +0800 13) if (g_cfg == NULL) {
afd40ae8 (zhangsan 2016-04-19 03:20:00 +0800 14) return DEFAULT_TIMEOUT;
afd40ae8 (zhangsan 2016-04-19 03:20:00 +0800 15) }
April 19, 2016. Three twenty in the morning. The commit message reads: fix: use default timeout during shutdown race (INC-2016-0417).
That was not a redundant check. It was a monument — raised by another engineer, at another 3 a.m., over another core dump, eight years earlier. The monument was standing right there in the code; it just had no inscription. That night, the review process, the coding standard, and the static analyzers all worked exactly as designed. The only thing that failed was this: nobody remembered.
In 1929 G. K. Chesterton stated what our industry now calls Chesterton’s fence: if you find a fence across a road and don’t know what it’s for, I will not let you tear it down. Go away and learn why it was put up; then come back and tell me it serves no purpose. The quote is famous among programmers. Compliance is rare — because no tool performs the “go away and learn why” step for you. That step is a craft.
This article is about that craft: code archaeology. All git output below was produced on Windows 11 with git 2.49.0 and PowerShell. The demo repository is synthetic: thirteen commits with backdated author dates (GIT_AUTHOR_DATE) simulating sedimentation from 2009 to 2023, built by the script in Appendix D; every command can be reproduced as-is (your commit hashes will differ). The demo code also passed compile verification: gateway.c, session.cpp, and net_utils.cpp compile under Visual Studio 2026 (cl 14.51, /W4 /utf-8 /std:c++17), and net_utils.cpp additionally under gcc 13.3 on WSL Ubuntu 24.04 (-Wall -Wextra). The first two files depend on the Win32 API, so Linux-side verification only makes sense for the portable layer — dual-platform claims should apply where they make sense, and only there.
1. The Premise to Demolish: The Meaning of Code Is Not in the Code
Lesson one of reading legacy code is demolishing a premise you never noticed you were holding: the meaning of code lives in the code.
The premise roughly holds for new code. A module you wrote last week still has warm intent in your head, fresh constraints in the review thread, variable names that remember why they were chosen. Reading it really is reading logic.
Legacy code is something else. The authors have left. The constraints that shaped the design (“that customer is still on IE6”, “the box has 2 GB of RAM”, “the billing service falls over if you look at it wrong”) are remembered by no one. The incident that taught everyone a lesson evaporated along with two ticket-system migrations. You are no longer facing logic. You are facing a historical document whose author is permanently unavailable.
Once you accept that, the entire methodology follows, because the relationship between code and history is this:
Every line of code is the fixed point of a chain of intent, constraints, incidents, and compromises. Writing is lossy compression; reading is decompression — and most of the “why” was discarded at compression time.
Or more bluntly: code records the verdict, not the trial.
Readers of my debugging series have met this situation before. In Every Stack Is Innocent: A Differential Diagnosis of the Hung Program (not yet published; link to be added), the most devious kind of hang is the lost wakeup: the wake-up event happened in the past, nothing registered it, and a hundred dumps all show the same “sleeping posture”. Legacy code is the lost-wakeup problem transplanted into source control: the decision happened in the past, nobody logged it, and no matter how many times you read the file, the “why” dimension carries zero information.
So reading a legacy system is not reading comprehension. It is forensics. Like an archaeologist, you reconstruct an unrecorded signal from the redundancy that survived — strata, artifacts, inscriptions, external archives. The next four sections are the four crafts of archaeology, projected onto a codebase.
2. Stratigraphy: Code Isn’t Written, It Settles
Archaeology’s first principle is the law of superposition: undisturbed upper layers are younger than the ones beneath them. And archaeologists have never excavated dirt — they excavate relationships. The Harris matrix, invented by Edward Harris in 1973, draws the stacking and cutting relationships between strata as a directed graph: a single layer contains no date; the date lives in the relationships between layers.
The insight transfers intact: a single file has no history; history lives in the relationships between files and commits. And a real codebase’s strata often shake hands inside a single function. Here is the current state of session.cpp in the demo repository (a real file):
DWORD WINAPI SessionThread(LPVOID lpParam) { // stratum A, 2009: Hungarian notation, Win32 idiom
char szHost[256]; // stratum A: fixed stack buffer
DWORD dwLen;
auto timeout = std::chrono::seconds(30); // stratum B, 2016: a C++11 modernization wave reached here
dwLen = GetPrivateProfileStringA("server", "host", "", szHost, 256, szIniPath); // stratum A: ANSI API
if (dwLen == 0) {
strcpy(szHost, "127.0.0.1");
}
std::string_view sv{szHost}; // stratum C, 2023: a later patch
printf("connecting %.*s\n", (int)sv.size(), sv.data());
return 0;
}
Three eras, fourteen lines. The first dating tool is carbon dating: git blame stamps every line with the commit that last touched it. But blame has a fatal source of contamination, and we have to deal with it first.
The disturbance layer: carbon dating resets
Run blame on the demo repository’s gateway.c as-is (real output):
$ git blame gateway.c
45b56d9b (liwei 2013-02-11 ...) // gateway.c - gateway forwarding and billing upload
...
d2a4901c (bot 2021-11-02 ...) int get_timeout_ms(void) {
d2a4901c (bot 2021-11-02 ...) if (g_cfg == NULL) {
d2a4901c (bot 2021-11-02 ...) return DEFAULT_TIMEOUT;
...
d2a4901c (bot 2021-11-02 ...) Sleep(50);
d2a4901c (bot 2021-11-02 ...) return do_send(host, port, payload, retries);
Nearly every function-body line claims to be from November 2021, authored by a bot. That is the disturbance layer left by the “repo-wide clang-format” commit: one mass reformat flattened a decade of dating information overnight. Archaeology knows this as disturbance — later construction churning the strata. A codebase has more than one such source: license-header sweeps, directory migrations, line-ending conversions.
The fix is to exclude the disturbance from dating. Since git 2.23 you can write the offending hashes into .git-blame-ignore-revs (GitHub’s blame view honors this file natively) and measure again (real output):
$ git blame --ignore-revs-file=.git-blame-ignore-revs gateway.c
45b56d9b (liwei 2013-02-11 ...) int send_to_billing(...) {
45b56d9b (liwei 2013-02-11 ...) int retries = 3;
db8a6e5f (zhangsan 2014-11-21 ...) if (is_internal(host)) {
eb3f2348 (liwei 2016-07-14 ...) if (port == 8080) {
c2daad8e (zhangsan 2017-05-09 ...) if (strcmp(host, "bill-backup") == 0) {
8883138f (zhangsan 2018-11-02 ...) Sleep(50);
The dates snap back into place: Sleep(50) returns to 2018, the NULL guard to 2016, every scar reattaches to its own stratum. (Make it permanent with git config blame.ignoreRevsFile .git-blame-ignore-revs.)
The probe the disturbance can’t stop
The prettier property shows up in this contrast. To find when Sleep(50) entered the repository, use the pickaxe (real output):
$ git log -S "Sleep(50)" --oneline -- gateway.c
8883138 fix: downstream billing rate-limited; 50ms between uploads (INC-2018-1102)
One hit — and notice that the 2021 reformat commit is nowhere in the result. The reason is -S’s semantics: it looks for commits where the number of occurrences of a string changed. Reformatting rearranged the code but added and removed no occurrence of Sleep(50), so to the pickaxe the disturbance layer is transparent. Carbon dating gets reset by disturbance; the probe cores straight through it. Chinese archaeologists core sites with the Luoyang spade, a probe shovel they adopted from tomb raiders; git log -S is that spade. Choose your dating tool by its contamination mechanism — that is stratigraphy’s first practical rule.
Stratigraphy’s last insight is the one that matters most. Once you can read every layer’s date, you notice: bugs rarely live inside a stratum; they live at the interfaces between strata. The 2009 layer assumed the config object was immortal; the 2016 patch armored only the spot that had been burned; the 2023 code built a new floor on the old assumption. It is the same sentence as in the hang-diagnosis article, where “the cycle is in no single stack — it lives in the relationships between stacks”: the disease of a site lives in relationships, not in any single artifact.
3. Typology: Style Is a Timestamp
An archaeologist glances at a potshard’s glaze and fabric and calls out a century. Programmers have the same ability; few use it deliberately. Idiom is a fingerprint of era.
| Artifact (idiom) | Stratum |
|---|---|
NULL, hand-rolled index loops | pre-C++11 |
sprintf into a fixed char[] | before the security awakening |
A-suffixed APIs, ANSI strings | before the Unicode migration |
Hungarian notation (dwLen, szHost) | the old Windows school |
raw new/delete with manual cleanup in try/catch | before RAII reached this team |
macros as constants, #define MAX_RETRY | pre-constexpr, or a C shop |
| error codes + return-value checks | before exceptions were unified, or systems tradition |
(Full table in Appendix C.) Typological dating has three steps: recognize the artifact, assign the era, and then — the actual point — reconstruct the constraints. A “stupid” piece of manual memory management was a rational choice in a year with no smart pointers; a hardcoded 256-byte buffer was generous when hostnames capped at 63 characters.
A warning sign belongs here, because without it this whole article gets read wrong: judging yesterday’s decisions with today’s toolbox is the Whig interpretation of history, and it is just as lazy in a codebase as it is in a textbook. The Whig historian stands on the present and sneers at the past; the Whig programmer stands on SSDs, 64 GB of RAM, and a modern standard library and writes “what is this garbage” in a review. Typology exists to date things, not to look down on them. Before mocking a stratum, ask: with the tools they had that year, would I have done better?
4. Epigraphy: Never Trust a Single Inscription
Strata give you dates; artifacts give you constraints. What an archaeologist prays for is writing — inscriptions. A codebase offers several things that claim to record intent, and none of them is fully trustworthy. Memorize the chain of evidence:
code > tests > commit messages > comments > docs > oral lore
Each rank deserves its own reasoning.
Code ranks first because it is the only witness that cannot lie — it is what it is. But it answers only “what”, and refuses “why”. That refusal is the entire problem.
Tests are living testimony: executable, falsifiable. But tests fossilize too. A case that has been green for eight years proves somebody cared deeply about a behavior eight years ago — not that the behavior still matters today. Date a test before you weigh its testimony.
Commit messages are the contemporary diary — written at the scene, closest in time to the trial. A good one looks like this (real output, git show 8883138):
commit 8883138fec0d4c6548e0f07a8e5d8e5e66cdf936
Author: zhangsan <zhangsan@corp.example>
Date: Fri Nov 2 02:47:00 2018 +0800
fix: downstream billing rate-limited; 50ms between uploads (INC-2018-1102)
What changed, why, and the incident number — all three elements present; this is a fine inscription. The industry majority, sadly, is “fix bug”, “update”, “oops”: a trial that never happened, a verdict with no reasoning.
Comments are later commentary, not original inscription. This is the superstition that needs breaking most. A comment describes the code as it was the moment the comment was written; the code moves on and the comment doesn’t, so comments rot faster than code. The demo repository holds hard evidence (real output, git blame config.h):
45b56d9b (liwei 2013-02-11 ...) // retry at most 3 times
66724cb2 (chenjun 2019-08-15 ...) #define MAX_RETRY 5
The comment says 3; the code says 5. Line-level dating makes it obvious: the comment is six and a half years older than the line it describes — a 2019 commit changed the number and left the sentence behind. Comments are not unreadable; they are inadmissible as primary evidence. When a comment and the code disagree, believe the code — then fix the comment. That fix is your gift to the next archaeologist.
Documentation is a map of an ideal country: it describes the system as it should be. Oral lore — the memory of the old-timers — ranks last, and not because it lacks value; it is often the only source of “why”. It ranks last because memory rewrites itself. The confident “we did it for performance back then” may simply be the version the teller prefers to remember. Oral history is legitimate evidence, but it must be cross-validated: against commits, monitoring archives, the changelog of the external system. Whatever fails to corroborate gets labeled “unverified”, not “believed”.
Finally, a stroke of luck you should learn to recognize: the Rosetta Stone. When you find two parallel implementations of the same logic alive in one repository — the old billing module and its half-migrated replacement, the C parser and its Rust rewrite — you hold a dual inscription. Each is the translation of the other; decoding either one decodes both. Archaeologists cracked Egyptian hieroglyphs off one trilingual stele; you will crack an inscrutable legacy algorithm off a parallel implementation. When you find one, don’t curse the mess — it is the highest-grade ore in the mine.
5. Excavation Discipline: Non-Destructive First
Archaeologists and looters dig the same holes. The difference is exactly one thing: records. Every layer an archaeologist lifts gets photographed, numbered, and surveyed, because excavation is destruction — once a layer is turned, its original relationships are gone forever. Editing legacy code is the same: every line you change destroys historical information it was carrying. That is why the fourth craft is a discipline.
Rule one: photograph before you dig. Michael Feathers gave this a formal name in Working Effectively with Legacy Code: the characterization test. The point is not to assert “correct” but to pin down “current”:
// Photograph the present before touching anything — pin behavior, not truth
TEST(LegacyParser, Characterize_CurrentBehavior) {
EXPECT_EQ(parse_price("1,234"), 1234); // this is what it does today; pin it
EXPECT_EQ(parse_price(""), 0); // possibly a bug — still today's reality
}
The photograph matters because any behavioral drift after you start digging trips an alarm immediately. You are not capturing right and wrong; you are capturing the present.
Rule two: peel one layer at a time. Strata come off one by one, and so do refactors. Touch three layers at once and any resulting problem is unattributable — the same law as “change one variable at a time” in debugging, fighting on a second front.
Rule three: keep a field journal. The hypothesis you spent three days verifying, the dead ends you ruled out, the relationships you excavated — write them down, in a commit message or the wiki. In the 3 a.m. story that opened this article, the chain did not break because the newcomer was reckless; it broke because the engineer in 2016 left half a headstone: the message recorded an incident number, but the incident itself is unreadable today. Assume every word you write is the only inscription the next archaeologist will ever find. What you save tonight is not just code — it is some stranger at 3 a.m., three years from now.
6. Scar-Tissue Reading: Every Ugly Line Is a Monument
Combine the four crafts and you can do the most practical thing of all: read change patterns. Almost every “ugly” spot in legacy code is a fossil. Here is send_to_billing from the demo repository, annotated with the blame results (dates and authors are real):
int send_to_billing(const char* host, int port, const char* payload) {
int retries = 3; // 2013 liwei: the original riverbed
if (is_internal(host)) { // 2014 zhangsan: INC-2014-0331, retries amplified an internal outage
retries = 1;
}
if (port == 8080) { // 2016 liwei: legacy clients hardcode 8080
port = 80;
}
if (strcmp(host, "bill-backup") == 0) { // 2017 zhangsan: dual-write during migration
// TODO(zhangsan): dual-write during migration, remove next release <- "next release", anno 2017
enqueue(payload);
}
Sleep(50); // 2018 zhangsan: INC-2018-1102, downstream rate limit
return do_send(host, port, payload, retries);
}
A ten-line function with five monuments in it. It was never “written”; it grew, incident after incident. Generalize, and you get a fossil identification table:
| Surface feature | What lies buried |
|---|---|
| An ever-lengthening if-chain | One incident or one big customer per branch |
| A defensive “this can’t be NULL” check | It was NULL. In production. |
| Commented-out blocks | Corpses nobody could bear to bury, or cases awaiting rehabilitation |
| feature flags / config switches | A ceasefire treaty from an old political compromise (“make it toggleable for now”) |
| Code labeled “compatible with XX” | A peace treaty with an external system — whose counterparty may no longer exist |
| A six-year-old TODO | A promise perpetually postponed by newer priorities |
| A two-thousand-line function | The main riverbed: sediment deposited faster than anyone dredges |
| Near-identical copy-pasted blocks with small variations | Parallel strata: one logic evolving separately on several branches |
The table has exactly one reading instruction: every ugly line of code might be a monument to some 3 a.m. incident. It just has no inscription. The archaeologist’s job is to carve the inscription back in. Once you do, something counterintuitive happens: understood legacy code usually stops looking ugly. What was ugly was never the code. It was the amnesia.
7. Dating Engineering and Unprovenanced Artifacts
Real sites are rarely as well-inscribed as our demo. Three harsh field conditions:
Unprovenanced artifacts: the repository with erased history. The year the company migrated from SVN to git, somebody took a shortcut and squashed ten years of history into a single commit — initial commit, fifty thousand lines. Blame is annihilated; every line shows the same day. Archaeology calls such finds “unprovenanced”, and the protocol is: abandon line-level dating, fall back to typology (partition the code into era zones by idiom), add external archives (old docs, mail threads, release notes), add oral history. Resolution drops from “line” to “zone” — and that is usually enough.
Hotspot surveying: where to dig first. No site gets fully excavated; archaeologists survey, and so do we, except our survey is data. Adam Tornhill’s Your Code as a Crime Scene — the title alone is the best advertisement this craft ever got — gives the metric: hotspot = change frequency × complexity. Change frequency is one command (real output):
$ git shortlog -sn -- gateway.c
4 zhangsan
2 liwei
1 bot
$ git rev-list --count HEAD -- gateway.c
7
Seven commits and four authors touched gateway.c over the years while config.h slept quietly — the most-changed, most-complex files are the main riverbeds where incidents are buried densest. Spend your excavation budget there.
Behavior coring: when did it change. Blame answers “how old is this line”, but often the real question is “in which stratum did the behavior change”. When exactly did MAX_RETRY go from 3 to 5? Core thirteen strata with binary search (real transcript, trimmed):
$ git bisect start
$ git bisect bad HEAD
$ git bisect good 45b56d9
Bisecting: 4 revisions left to test after this (roughly 2 steps)
$ git bisect run sh -c "git grep -q 'MAX_RETRY 3' -- config.h"
Bisecting: 2 revisions left to test after this (roughly 1 step)
...
66724cb20322f1a5393bd0c80b23ed14a9d391cf is the first bad commit
commit 66724cb20322f1a5393bd0c80b23ed14a9d391cf
Author: chenjun <chenjun@corp.example>
Date: Thu Aug 15 16:20:00 2019 +0800
fix: 3 retries not enough on weak networks; bump to 5
Two probes, and the layer is pinned: August 15, 2019. (One real pit for PowerShell users: when passing a probe command to sh -c, use double quotes outside and single quotes inside, or the arguments get split and bisect silently hands you a wrong answer — I know because I watched it do exactly that while preparing this article.) Bisect is binary coring through strata; any “when did the behavior change” question — when the latency regressed, when that header got polluted — is fair game.
Last, a distinction you must draw: old is not dead. A layer untouched for five years is either stable, or a zombie nobody dares to touch. The discriminator is not age but callers — run reference analysis to see who still uses it. Old code with callers is mature; old code without callers that nobody dares delete is rubble. Enshrine the first. Cart away the second.
8. Case Study: The 50-Millisecond Sleep
Now assemble every craft into one complete excavation. The report: the gateway service’s average latency is 50 ms above design — exactly 50 ms, suspiciously round. Some blame the network, some blame the GC. You don’t guess. You dig.
Step one: triage. A latency exact to the millisecond is almost always a human constant, not system noise. Target: find the constant. Do not start by reading code — the repo is tens of thousands of lines, and reading is the last resort.
Step two: coring. The spade goes straight at the suspect (real output):
$ git log -S "Sleep(50)" --oneline -- gateway.c
8883138 fix: downstream billing rate-limited; 50ms between uploads (INC-2018-1102)
One hit. Read the inscription (quoted in full back in section 4): November 2, 2018, 2:47 a.m., zhangsan, incident INC-2018-1102.
Step three: cross-validation. Take the incident number to the ticket system — no such ticket. The system migrated twice in five years; 2018 tickets were archived as titles only, bodies lost. This is the normal state of real archaeology: damaged inscriptions. Don’t expect a Hollywood ending. Turn to oral history: an engineer who was on the team back then recalls “billing got hammered by an upstream burst once, and they added rate limiting afterwards”. Corroborate: the billing team’s changelog shows a rate-limit overhaul in November 2018. The chain closes. What happened: back then our requests went straight at billing with no throttle, one traffic spike knocked it over, and afterwards the two teams agreed we would self-impose a 50 ms spacing — the stele got erected right on the request path.
Step four: verdict. Does the stele still bear weight? The billing service was rewritten in 2020 with its own rate limiting and circuit breaking; the technical premise of the treaty is gone. But note the remedy — you do not just delete it; that is looting, not archaeology. The proper closure: turn the 50 ms into a configuration item (default keeps the original value), carve the inscription in a comment (both incident numbers: 2018’s cause and 2020’s rewrite), ramp it to zero behind a flag over two weeks, and only then remove it. The whole excavation goes into the final commit message — field journal closed, an inscription for the inscription erected.
Look back at the case: coring took five minutes; cross-validation took two days. The fast, tool-driven parts are the cheap parts. What costs — and what pays — is the chain of evidence.
9. The Limits of Archaeology: When Not to Dig
After eight sections of methodology, one section of anti-methodology, or this article becomes another superstition.
Triage by risk, not curiosity. Code you must change deserves two days of digging; ugly code you merely pass by gets a photograph (a one-line “history unexamined” comment) and you move on. The excavation budget is always scarce. Spend it on the main riverbed, not on every odd stone along the road.
Anti-romanticism. Some weird code has no story. It was written badly at 7 p.m. on a Friday. Typology can give it a date; it cannot give it a meaning. When three layers of digging come up empty, have the courage to say: this is not a site, it is a landfill. Landfills get cleared, not curated.
The other half of the fence. Chesterton’s actual words amount to “find out why it stands there, then decide” — the emphasis is on finding out, not on preserving forever. When all three evidence sources (strata, inscriptions, oral history) come up empty, price the risk: small blast radius → run a feature-flagged experiment and let the experiment answer what archaeology couldn’t; large blast radius → keep digging. “Nobody knows why” is not a permanent exemption. Treating Chesterton’s fence as a permanent pardon turns a guardrail into an excuse.
Rewriting is a rational option — but excavate before you rewrite. When excavation cost durably exceeds rebuild cost, rewriting is right. Rewriting does not exempt you from archaeology: the scar list of the old system must be inherited in full by the new one, or you will earn every scar again. Most “rewrite disasters” in our industry were not caused by rewriting; they were caused by cutting out the appendix and the vaccination scars in one operation — and then the old disease came back.
10. The AI Era: Spades and Archaeologists
This article is written in the noisy years of AI coding, and dodging the topic would be dishonest. My assessment comes in three restrained layers.
AI is excellent at surface survey and typology. Drop an unfamiliar repository on it and it drafts the module map, summarizes, and recognizes the era of every idiom in seconds — precisely the most time-consuming, least “why”-dependent steps of excavation. Use it there. It is a bargain.
AI cannot read history, and it will pretend it can. The “why” is in no text, so the model has no more access to it than you do. But it will not say so — it will hallucinate a plausible-sounding reason from the code’s apparent logic. That is worse than no explanation, because it reads like the truth. The archaeologist’s iron rule doubles in the AI era: any answer to “why” must be backed by an inscription or an external archive, whether it came from a human or a model.
The real shift is the sedimentation rate. AI multiplies the speed of code production, which means future strata will pile up an order of magnitude faster — and the new strata are born without inscriptions, because generation has no trial to record. Commit messages, ADRs, characterization tests — every “archaeability” practice — are graduating from engineering virtue to survival equipment. One sentence to close: AI is a Luoyang spade, not an archaeologist. It cores faster. The reading is still yours.
11. Closing: Be a Good Corpse, Leave a Good Headstone
This series has demolished several premises: the debugging article demolished “the author knows the intent”, the Heisenberg article demolished “observation doesn’t change the system”, the hang article demolished “a hung program is a broken program”. This one demolishes the last and most fundamental: “the meaning of code is in the code”. Crashes lie in space, hangs lie in time, and code is silent about history.
Against silence, the earlier sections offered crafts: stratigraphy, typology, epigraphy, excavation discipline. But crafts are downstream. The upstream is in our own hands. Every engineer does two jobs every day: writing code, and manufacturing future strata. The second job has no KPI, yet it decides how long the stranger at 3 a.m., three years from now, will have to dig.
So the closing is three inscription disciplines:
- Commit messages record why and the incident number, never what. The code speaks the what by itself; if you don’t speak the why, nobody ever will.
- Write ADRs for significant decisions — the decision, the alternatives, the reasons for rejection. An Architecture Decision Record is a deliberate funerary inscription, a Rosetta Stone prepared in advance for future archaeologists.
- Patch inscriptions as you read. Every piece of history you excavate becomes a comment or a commit note. You owe the next archaeologist exactly what the last one owed you.
Most of software engineering is collaboration with people who are not in the room — authors from the past, readers from the future. Code archaeology turns that long-distance collaboration from luck into craft.
The code you write today is the site someone will excavate tomorrow. Be a good corpse. Leave a good headstone.
Appendix A: Field Guide
| Situation | Craft | Tools |
|---|---|---|
| Can’t tell why some code exists | stratigraphy + epigraphy | blame (with ignore-revs) / log -S / oral history |
| Afraid to touch some code | excavation discipline | characterization tests |
| Fixing a bug / changing behavior | scar-tissue reading + behavior coring | log -S / log -G / bisect |
| Rewrite vs. maintain decision | dating engineering + hotspot survey | shortlog / rev-list / reference analysis |
| History squashed away | unprovenanced-artifact protocol | typology zoning + external archives + oral history |
Appendix B: Git Dating Command Reference
All commands verified on git 2.49.0 + PowerShell (output in the article body):
git blame <file> # carbon dating: per-line era
git config blame.ignoreRevsFile .git-blame-ignore-revs # permanently exclude the disturbance layer
git blame --ignore-revs-file=.git-blame-ignore-revs <file> # one-off exclusion
git log -S "string" --oneline -- <file> # the spade: when a string appeared/vanished (occurrence-count semantics; immune to reformatting)
git log -G "regex" --oneline -- <file> # find changes by diff content
git log --follow --oneline -- <file> # trace a file across renames (without it, history breaks at the rename)
git shortlog -sn -- <file> # author distribution of a layer
git rev-list --count HEAD -- <file> # total commits touching a file (change frequency)
git show <hash> # read the inscription: one commit's full evidence
git bisect start / bad / good / run sh -c "..." # binary coring: locate the stratum where behavior changed
Two pits verified the hard way: in PowerShell, pass the probe command to sh -c with double quotes outside and single quotes inside, or bisect silently returns the wrong commit; and -S takes a literal string, so regex metacharacters (like parentheses) need no escaping — which is exactly why it beats -G for precise coring.
Appendix C: Stratigraphic Fingerprint Table (C and C++)
| Artifact | Stratum | Constraint of its era |
|---|---|---|
NULL, 0 as null pointer | pre-C++11 | no nullptr |
Hungarian notation (dw/sz/lpsz prefixes) | old Windows school | weak type system, names compensating |
A-suffixed APIs, ANSI strings | pre-Unicode-migration | the mojibake years |
sprintf, fixed char[N] | pre-security-awakening | buffer overflows not yet front-page news |
raw new/delete, manual cleanup | pre-RAII | smart pointers not yet widespread |
ini files, GetPrivateProfileStringA | pre-config-center | single-machine deployment era |
| error-code returns | pre-unified-exceptions | cross-compiler ABI worries |
| macro constants | pre-constexpr | weak compiler optimization |
Sleep as a synchronization device | pre-condition-variable era | scarce concurrency primitives |
std::auto_ptr | the C++98 to C++11 transition | move semantics not yet born |
Usage: recognize the artifact, assign the era, reconstruct the constraint. And remember the warning sign: this table is for understanding, not for sneering.
Appendix D: How the Demo Repository Was Built
The demo repository is generated by a script: before each commit, set GIT_AUTHOR_DATE / GIT_COMMITTER_DATE (plus author name and email) to backdate it to any era. The core pattern (bash flavor; the full PowerShell script works the same way):
GIT_AUTHOR_NAME=zhangsan GIT_AUTHOR_DATE="2018-11-02 02:47:00 +0800" \
GIT_COMMITTER_NAME=zhangsan GIT_COMMITTER_DATE="2018-11-02 02:47:00 +0800" \
git commit -m "fix: downstream billing rate-limited; 50ms between uploads (INC-2018-1102)"
Thirteen commits in total: 2009 session module, 2010 network helpers, 2013 gateway + config, five scars in 2014/2016/2017/2018, the lying comment in 2019, the repo-wide reformat in 2021 (disturbance layer), the rename in 2022, the string_view patch in 2023. Build it this way and every command in the body and Appendix B reproduces as-is; commit hashes will vary with construction time, which is normal.
Compile verification commands (matching the claims in the article):
cl /nologo /W4 /utf-8 /c gateway.c
cl /nologo /W4 /utf-8 /std:c++17 /EHsc /c session.cpp
cl /nologo /W4 /utf-8 /std:c++17 /EHsc /c net_utils.cpp
gcc -x c -Wall -Wextra -c net_utils.cpp # Linux side: portable file only
Two flags are load-bearing. First, /std:c++17: MSVC defaults to C++14, and the 2023 stratum’s string_view fails outright with “not a member of std”. Second, /utf-8: repository files are BOM-less UTF-8 (the Linux-friendly way to save), and without the flag MSVC decodes them in the local code page. In the Chinese-commented twin of this repository, that swallowed the line containing #include <windows.h>, and Sleep promptly became “undefined; assuming extern returning int” — an encoding problem masquerading as a missing symbol. Both pits are documented because we fell into both.
Related Reading
- The Harder You Look, the Less It’s There: Heisenbugs and the Observer Effect in Debugging – the epistemic source of the forensics methodology
- Bugs Written by AI Look Like Correct Code: An Epistemic Rewrite of Debugging – the series’ starting point: “intent is unknowable”, debugging side
- Non-Invasive Probes: How ASan, TSan, and UBSan Expose Bugs Before They Ever Misbehave – structural evidence over temporal evidence, the same move as epigraphy
- Code Is Turning from Asset to Liability (Chinese) – archaeology as the inventory method for that liability
- Simplicity: The Undervalued Engineering Skill (Chinese) – the best inscription is manufacturing less strata
