post: 30 days of agent churn kept 23.5万 of 51.8万 lines (EN + ZH)
Deploy / build (push) Successful in 56s
Deploy / build (push) Successful in 56s
The commit-churn post the owner read and approved today, published in the first-person voice he asked for. - Voice rewrite before publishing: the draft opened on "when managers or recruiters ask how productive an engineer is" and addressed the reader in the second person throughout (you/your 12 -> 2, both in the CTA; ZH 你 5 -> 2). It now reads as a log entry: "Last week I did something I almost never bother to do: I counted my own commits." Headings moved from essay titles to actions. Numbers, the 5-repo table and every claim are unchanged. - Title changed at the owner's request: "Why 500,000 Lines of Commit Churn Only Left 230,000 Lines in Production" -> "I Committed 518,000 Lines in 30 Days. Production Kept 235,000." (ZH: 30 天提交 51.8 万行,生产环境只留下 23.5 万行), pubDate 2026-10-18. - Fixed two real defects found while making the card: the draft claimed "four automated gates" and listed three; and og-gen split titles at the word-count midpoint, breaking at "…Lines in / 30 Days" — the splitter now prefers a sentence boundary near the middle and falls back to the midpoint. - og PNG regenerated for the new title, thumbs and banner WebP re-derived. Measured: card 2 title lines, overflowY 0, measure_og_banner PASS (tags 1 row, banner delta 2), build clean, /posts/ monotonic with this post on top. Also queued the post at the head of the dev.to drip (queue 69 -> 70).
This commit is contained in:
1 parent
2060aaa166
commit
0d7384224d
9 files changed
+325
-23
No files matched your search
@@ -0,0 +1,180 @@
|
||||
---
|
||||
title: "I Committed 518,000 Lines in 30 Days. Production Kept 235,000."
|
||||
description: "Over 30 days, autonomous coding agents added 518,000 lines across 5 repos, yet production kept 235,000. Here is why verification gates matter more than generation."
|
||||
pubDate: "2026-10-18"
|
||||
category: "engineering"
|
||||
tags: ["ai", "git", "devops", "testing", "refactoring"]
|
||||
ogImage: "/og/why-500000-lines-of-commit-churn-only-left-230000-in-production.png"
|
||||
banner: "/banners/why-500000-lines-of-commit-churn-only-left-230000-in-production.png"
|
||||
---
|
||||
|
||||
Last week I did something I almost never bother to do: I counted my own
|
||||
commits. Not the ones I remember writing — the ones the repositories
|
||||
actually recorded. Five production repos on my self-hosted Gitea, thirty
|
||||
days, one person: 865 commits, 518,233 lines added, 130,892 deleted.
|
||||
|
||||
Then I counted what survived. A clean scan of the current revision of my
|
||||
main production application returns 234,976 lines. The video transcoder that
|
||||
gained 58,240 lines in the same window is 43,050 lines long today.
|
||||
|
||||
So where did the other 280,000 lines go? Nothing was lost. They were
|
||||
written, run, judged wrong, and deleted — or replaced by something smaller
|
||||
that did the same job. That loop, not the writing, is what I want to write
|
||||
down, because it changed how I work this year more than any model update
|
||||
did.
|
||||
|
||||
## The numbers I didn't believe
|
||||
|
||||
Here is the exact commit breakdown across the five repositories, between 17
|
||||
September and 17 October 2026:
|
||||
|
||||
| Repository | Commits (30d) | Additions (+) | Deletions (-) | Net Change | Current Total LOC | Primary Stack |
|
||||
|---|---|---|---|---|---|---|
|
||||
| **numerology-report** | 432 | +331,672 | -98,937 | +232,735 | **234,976** | PHP, Markdown, JSON |
|
||||
| **hevc-transcode** | 187 | +58,240 | -15,508 | +42,732 | **43,050** | Python, HTML, Shell |
|
||||
| **dsm-resource-management** | 132 | +58,234 | -14,076 | +44,158 | **368,793** | Python, TypeScript, Astro |
|
||||
| **hoelee-blog** | 88 | +23,603 | -709 | +22,894 | **55,933** | Astro, TypeScript, Markdown |
|
||||
| **hoelee-agent-dist** | 26 | +46,484 | -1,662 | +44,822 | *(Dist)* | Python, Shell, Markdown |
|
||||
| **Total** | **865** | **+518,233** | **-130,892** | **+387,341** | — | — |
|
||||
|
||||
Two lines are easy to check yourself; this is the whole measurement:
|
||||
|
||||
```bash
|
||||
git -C numerology-report log --since=2026-09-17 --until=2026-10-17 \
|
||||
--numstat --pretty=format: | awk '{add+=$1; del+=$2} END {print add, del}'
|
||||
```
|
||||
|
||||
In a human team, churn like that usually means trouble: requirements moving
|
||||
under the code, a rewrite nobody admits to, an architecture that keeps
|
||||
collapsing. None of those applied here. Every one of these five repos is
|
||||
something I run and depend on, and every commit was authored in a session
|
||||
where I could read the diff before it landed.
|
||||
|
||||
The churn is what refactoring looks like when generation is cheap. An agent
|
||||
drafts a slice of the change, I run the gates, the gates disagree, and the
|
||||
slice gets thrown away or rewritten smaller. On the days I got the gates
|
||||
right, the size drop came faster than the features arrived.
|
||||
|
||||
## What the agents actually touch
|
||||
|
||||
Most people's first experience of AI code is a browser tab: paste a function
|
||||
into a chat window, copy the answer back into an editor. The bottleneck
|
||||
there is the clipboard. It also keeps the model far away from the two things
|
||||
that matter — the codebase's own conventions and the ability to run what it
|
||||
wrote.
|
||||
|
||||
So mine run in a terminal, on the machine, with real tools:
|
||||
|
||||
1. **Filesystem and syntax tools.** They read existing modules, patch by
|
||||
fuzzy match instead of overwriting whole files, and run syntax or AST
|
||||
checks before anything is saved.
|
||||
2. **Test and execution runners.** PHPUnit suites, pytest harnesses, and
|
||||
headless Chrome probes, executed in the foreground with the output read
|
||||
back.
|
||||
3. **Version control.** They read `git diff`, stage explicit paths, and look
|
||||
at the history of a file to see why the previous decision was made.
|
||||
|
||||
That combination is fast — an order of magnitude faster than me typing the
|
||||
first draft. It is also, on its own, a way to make a codebase rot faster,
|
||||
which is the part nobody warns you about.
|
||||
|
||||
## Where the bloat comes from
|
||||
|
||||
Unsupervised models love to write code. Give one a small edge case and its
|
||||
instinct is to add a private helper class, clone an existing utility with
|
||||
one extra parameter, or wrap a library in three layers of abstraction that
|
||||
only make sense to the diff that introduced them. Left alone for a week,
|
||||
that is a few thousand lines of confident, plausible, unnecessary code.
|
||||
|
||||
I know because I have that week in my own history. Two things came out of
|
||||
reading these diffs every day:
|
||||
|
||||
- A new class has to earn its place against a standard-library call I
|
||||
already have.
|
||||
- Anything generated is guilty until a deterministic check says otherwise.
|
||||
"Looks correct" from a model is not evidence; a red test is.
|
||||
|
||||
## The gates that actually deleted the code
|
||||
|
||||
During the V2 architecture overhaul of my main production application, more
|
||||
than 98,000 lines went out in thirty days. Almost none of that was me
|
||||
sitting down to delete things. It went because three gates kept rejecting
|
||||
what came back:
|
||||
|
||||
### 1. Deterministic harnesses instead of "does this look right?"
|
||||
|
||||
Asking a model to review its own code mostly produces confidence, not
|
||||
correctness. The useful move is to wire the work to something that cannot be
|
||||
talked out of failing:
|
||||
|
||||
- In the PHP application, every public route parameter has to carry an
|
||||
explicit default, enforced by a reflection probe
|
||||
(`ControllerAutoRouteSafetyTest.php`) — that test exists because a bare
|
||||
route crashed with a 500 in production.
|
||||
- In the report pipeline, a headless PDF extractor counts the rendered
|
||||
Chinese characters before a build is allowed to pass, so a font or layout
|
||||
change that silently drops glyphs fails the build instead of shipping a
|
||||
broken PDF.
|
||||
- In the transcoder, the ledger compares exact byte counts rather than
|
||||
approximating in megabytes, because rounded numbers hid a size regression
|
||||
for weeks.
|
||||
|
||||
If a gate goes red, the agent either fixes the change or drops it. Nothing
|
||||
reaching a commit is exempt from that.
|
||||
|
||||
### 2. Pruning as a standing task, not a cleanup project
|
||||
|
||||
A portion of every session goes to deleting. Can a 200-line helper be
|
||||
replaced by a ten-line call into something that already exists? Is this new
|
||||
mapper duplicating a function that has lived in `app/Libraries/` for a year?
|
||||
The 130,000 deletions above are not an accident of a rewrite; they are
|
||||
scaffolding that outlived its purpose, migration layers with one caller, and
|
||||
logic that arrived twice under two names.
|
||||
|
||||
### 3. Keeping deterministic work deterministic
|
||||
|
||||
The trap I keep seeing in AI-assisted projects is a model being asked to do
|
||||
arithmetic it should never touch. If a shell pipeline, a SQL constraint, an
|
||||
idempotent script, or a cron job can answer it, an LLM has no business
|
||||
there.
|
||||
|
||||
Models are good at drafting an initial implementation, generating a
|
||||
three-language dictionary, or mapping a third-party API's quirks. They are
|
||||
not where I put state transitions, money, or queue scheduling, because a
|
||||
probabilistic answer to a deterministic question is a bug with a delay fuse.
|
||||
|
||||
## What changed in how I work
|
||||
|
||||
For most of my career the physical constraint was typing speed and
|
||||
remembering syntax. Output was the metric, and a productive week looked like
|
||||
a lot of new files.
|
||||
|
||||
Syntax is free now. I can get a 500-line migration with tests drafted while
|
||||
I read the diff of the previous one. So the constraint moved to the three
|
||||
things a model cannot check for me:
|
||||
|
||||
- **Boundary design** — where components separate, and which state is
|
||||
allowed to cross.
|
||||
- **Test architecture** — the harness that catches semantic drift the type
|
||||
checker never sees.
|
||||
- **Pruning discipline** — the willingness to delete 280,000 lines of
|
||||
exploratory, redundant, or dead code so 235,000 lines can go to
|
||||
production.
|
||||
|
||||
Which is why I no longer read a commit graph as a productivity chart. Half a
|
||||
million added lines in a month tells me the tools were running. The 130,000
|
||||
deleted lines, the refactor underneath them, and a suite that is still green
|
||||
tell me whether the month was worth anything.
|
||||
|
||||
## Building something that has to stay up?
|
||||
|
||||
I run **Hoelee Enterprise**, a website design and development studio for
|
||||
Malaysian SMEs. The agent workflow, the test harnesses and the self-hosted
|
||||
infrastructure in this post are the same setup I build and maintain client
|
||||
systems on.
|
||||
|
||||
If you need full-stack engineering, workflow automation, or infrastructure
|
||||
that survives its own maintenance:
|
||||
- **WhatsApp:** [+60 12-797 2969](https://wa.me/60127972969)
|
||||
- **Email:** [[email protected]](mailto:[email protected])
|
||||
- **Website:** [hoelee.com](https://hoelee.com)
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
---
|
||||
title: "30 天提交 51.8 万行,生产环境只留下 23.5 万行"
|
||||
description: "过去 30 天,自主编程 Agent 在 5 个仓库中累计新增了 51.8 万行代码,但最终留存仅 23.5 万行。为什么在 AI 时代,验证闸门与代码剪枝比生成速度重要十倍?"
|
||||
pubDate: "2026-10-18"
|
||||
category: "engineering"
|
||||
tags: ["ai", "git", "devops", "testing", "refactoring"]
|
||||
ogImage: "/og/why-500000-lines-of-commit-churn-only-left-230000-in-production.png"
|
||||
banner: "/banners/why-500000-lines-of-commit-churn-only-left-230000-in-production.png"
|
||||
---
|
||||
|
||||
上周我做了一件平时几乎不会做的事:数了数自己的提交。不是数我记得写过的那些,而是数仓库里真实记录下来的那些。自建 Gitea 上 5 个生产仓库、三十天、一个人:865 次 Commit、新增 518,233 行、删除 130,892 行。
|
||||
|
||||
然后我数了数活下来的部分。对主生产项目的最新版本跑一次干净扫描,有效代码总量是 234,976 行;同一时间窗口里净增 58,240 行的视频转码器,今天整个仓库只有 43,050 行。
|
||||
|
||||
那 28 万行进哪儿去了?没有丢。它们被写出来、跑过测试、被判为不对,然后删掉——或者被一个更小、做同一件事的东西替换掉。我想把这条循环记下来,因为它这一年对我工作方式的影响,比任何一次模型升级都大。
|
||||
|
||||
## 一开始我不信的这几个数字
|
||||
|
||||
以下是 2026 年 9 月 17 日至 10 月 17 日,名下 5 个活跃仓库的精确 Git 统计:
|
||||
|
||||
| 仓库名称 | 30 天 Commit | 累计新增 (+Add) | 累计删除 (-Del) | 净变动 (Net) | 当前存量有效代码/文本 | 核心技术栈 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| **numerology-report** | 432 | +331,672 | -98,937 | +232,735 | **234,976** | PHP, Markdown, JSON |
|
||||
| **hevc-transcode** | 187 | +58,240 | -15,508 | +42,732 | **43,050** | Python, HTML, Shell |
|
||||
| **dsm-resource-management** | 132 | +58,234 | -14,076 | +44,158 | **368,793** | Python, TypeScript, Astro |
|
||||
| **hoelee-blog** | 88 | +23,603 | -709 | +22,894 | **55,933** | Astro, TypeScript, Markdown |
|
||||
| **hoelee-agent-dist** | 26 | +46,484 | -1,662 | +44,822 | *(Dist)* | Python, Shell, Markdown |
|
||||
| **合计** | **865** | **+518,233** | **-130,892** | **+387,341** | — | — |
|
||||
|
||||
其中两行你可以自己复算,这就是完整的量法:
|
||||
|
||||
```bash
|
||||
git -C numerology-report log --since=2026-09-17 --until=2026-10-17 \
|
||||
--numstat --pretty=format: | awk '{add+=$1; del+=$2} END {print add, del}'
|
||||
```
|
||||
|
||||
在人类团队里,这样的更替量通常意味着出事了:需求在代码底下不停移动、有人偷偷在重写、架构反复塌陷。这几种情况我一条都不占——这 5 个仓库都是我每天在跑、离不开的东西,每一次提交都发生在能读到 diff 的会话里。
|
||||
|
||||
生成成本变低之后,这种更替量就是重构的样子:Agent 起草一段改动,我跑闸门,闸门不同意,这段就被丢掉或者改小重写。凡是闸门写对的那几天,代码总量往下掉得比功能上来的速度还快。
|
||||
|
||||
## Agent 实际接手的是什么
|
||||
|
||||
大多数人第一次让 AI 写代码,是在一个浏览器标签页里:把函数粘进对话框,再把答案拷回编辑器。这条路的瓶颈是剪贴板,而且模型离真正重要的两样东西都很远——仓库自己的约定,以及运行它刚写出来的东西的能力。
|
||||
|
||||
所以我的是跑在终端里、跑在这台机器上的,手里有真工具:
|
||||
|
||||
1. **文件与语法工具**:直接读已有模块,用模糊匹配打补丁而不是整文件覆盖,保存前先过语法与 AST 校验。
|
||||
2. **测试与执行器**:PHPUnit 单元测试、pytest 套件、无头 Chrome 探针,在前台跑起来并把输出读回来。
|
||||
3. **版本控制上下文**:读 `git diff`、显式暂存指定路径、翻一个文件的历史看上一个决定为什么这么做。
|
||||
|
||||
这套组合很快,比我亲手打第一版快一个数量级;但它本身也是让代码库腐烂得更快的方法——这一半通常没人提醒。
|
||||
|
||||
## 臃肿是从哪里长出来的
|
||||
|
||||
不受约束的模型天生爱写代码。给它一个细小的边界情况,它的第一反应是新建一个私有辅助类、把一个已有工具函数复制一份改个参数,或者给一个库套上三层抽象——那三层只有在这段 diff 里才讲得通。放它自己跑一周,就是几千行语气笃定、看起来合理、其实没必要的代码。
|
||||
|
||||
我这么讲是因为我自己的历史里就有那一周。每天读这些 diff,最后落成两条规矩:
|
||||
|
||||
- 新建一个类,要跟一个我本来就有的标准库调用竞争位置。
|
||||
- 生成出来的东西默认有罪,直到某个确定性检查说它没问题。模型说「看起来对」不算证据,红的测试才算。
|
||||
|
||||
## 真正把代码删掉的是三道闸门
|
||||
|
||||
在最近一次主生产系统的 V2 架构重构中,三十天里删掉了 98,000 多行。这些几乎不是我坐下来专门去删的,而是三道闸门不断把回来的东西打回,最后留下的就是更小的版本:
|
||||
|
||||
### 1. 用确定性脚手架,而不是问「这段看着对不对」
|
||||
|
||||
让模型审查自己写的代码,多数时候产出的是自信,不是正确。有用的做法是把它接到一个没法被说服成绿灯的东西上:
|
||||
|
||||
- PHP 应用里,所有公开路由参数必须带显式默认值,由专用的反射探针(`ControllerAutoRouteSafetyTest.php`)守着——这个测试存在,是因为一条裸路由在生产上真的 500 过。
|
||||
- 报告生成管线里,无头 PDF 解析器先数一遍渲染出的汉字数量,数不对就不许构建通过;这样字体或排版改动如果悄悄漏字,就是构建失败,而不是发出一份坏 PDF。
|
||||
- 转码队列里,台账比的是精确字节数而不是 MB 估算,因为圆整过的数字把一次体积回退藏了好几个星期。
|
||||
|
||||
闸门红了,Agent 要么改到过,要么把这段撤掉。没有任何东西能豁免。
|
||||
|
||||
### 2. 把剪枝当成日常动作,而不是一次大扫除
|
||||
|
||||
每次会话都会分出一段时间专门删东西:这个 200 行的辅助类,能不能换成一行已有的原生调用?这个新的映射层,是不是在重复 `app/Libraries/` 里放了一年的某个函数?上面那 13 万行删除不是重写的副产品,而是活过头了的脚手架、只有一个调用方的迁移层,以及换了名字出现两遍的同一段逻辑。
|
||||
|
||||
### 3. 确定性的活留给确定性代码
|
||||
|
||||
我在 AI 项目里反复看到的坑,是让模型去做它根本不该碰的计算。如果一条 Shell 管道、一个 SQL 约束、一个幂等脚本或者一个 Cron 任务就能给出答案,那 LLM 就不该出现在那里。
|
||||
|
||||
模型擅长起草初始实现、生成三语词库、摸清三方 API 的脾气;但状态流转、钱、队列调度,我不会交给它——一个确定性问题上概率性的答案,是一颗带延时引信的 bug。
|
||||
|
||||
## 我的工作方式变了什么
|
||||
|
||||
我职业生涯的大部分时间里,物理瓶颈是打字速度和记语法。产出就是指标,一个高效的一周看起来就是新建了很多文件。
|
||||
|
||||
现在语法是免费的。我可以一边读上一次改动的 diff,一边让 Agent 起草一份带单元测试的 500 行迁移。于是瓶颈转移到了模型替我看不了的三件事上:
|
||||
|
||||
- **边界设计**:模块从哪里分开,哪些状态允许跨过去。
|
||||
- **验证体系**:那个能抓住类型检查看不见的语义漂移的脚手架。
|
||||
- **剪枝的决断力**:愿不愿意删掉那 28 万行探索性、冗余、已死掉的代码,只把 23.5 万行送进生产。
|
||||
|
||||
所以我现在不再把提交曲线当生产率图表看。一个月新增 50 万行,只能说明工具在转;真正说明这一个月值不值的是那删掉的 13 万行、底下的重构,和一套仍然全绿的测试。
|
||||
|
||||
## 需要一套能长期不倒的系统?
|
||||
|
||||
我运营 **Hoelee Enterprise**,为马来西亚中小企业做网站设计与系统开发。上面这套 Agent 工作流、测试脚手架和自托管基础设施,就是我给客户交付并长期维护系统时用的同一套东西。
|
||||
|
||||
如果你需要全栈系统研发、业务流程自动化,或者能扛住自己维护周期的自建基础设施:
|
||||
- **WhatsApp:** [+60 12-797 2969](https://wa.me/60127972969)
|
||||
- **Email:** [[email protected]](mailto:[email protected])
|
||||
- **官网:** [hoelee.com](https://hoelee.com)
|
||||
Reference in new issue
Block a user