重构一款“感觉导向”的应用程序:代码虽可读,逻辑却已无迹可寻。
Renovating a vibe-coded app: the code was readable, the reasoning was gone

原始链接: https://www.habchy.dev/writing/renovating-a-vibe-coded-app

在这篇反思中,Yves Habchy 讲述了将一个利用生成式 AI 工具快速构建的“氛围导向型”(vibe-coded)教育平台投入生产环境所面临的挑战。尽管代码本身尚可阅读,但却缺乏文档、背景信息以及清晰的决策脉络。 该项目充斥着技术债务:不完整的数据库迁移、缺失的权限设置,以及包括暴露 API 密钥和未授权端点在内的严重安全漏洞。尽管 Habchy 和自动化审计系统识别并解决了这些技术问题,但核心逻辑依然晦涩难懂。该应用程序包含多种相互冲突的学生评分算法,且没有任何记录说明哪一个是权威的,或者它们为何存在差异。诸如考勤处理等功能虽然已经完全构建,但却处于断开状态,此前的设计决策也毫无记录。 Habchy 总结认为,虽然 AI 工具可以快速生成功能完备的应用程序,但它们无法保留代码背后的“动机”。其结果是一个脆弱的产品:虽然具备技术功能,却丢失了底层的逻辑推理。如果没有开发人员来梳理和记录设计意图,未来的维护将如履薄冰,这证明了“可读”的代码并不能替代架构的清晰性。

Hacker News 最新 | 过往 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 重构一个“氛围感”编码的应用:代码虽易读,思路却已丢失 (habchy.dev) 6 分,yveshabchy 发布于 50 分钟前 | 隐藏 | 过往 | 收藏 | 讨论 | 帮助 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索:
相关文章

原文

By Yves Habchy · September 2026

In the first week of a renovation job I found a function whose name said it cleaned up duplicate citizenship records. I could read it end to end. It grouped records by student and day, compared the first tag on each, and deleted the rest, which means it could delete records that weren't duplicates at all.

The problem is, citizenship wasn't defined anywhere. Not in the schema, not in a comment, not in the client's own description of the product. So it had to be something the business meant, held somewhere other than a table.

What I had was a category of records, a scoring path that read them, and a dedicated tool for cleaning up their duplicates. The rows gave me the rest: tags like warning, asked to leave, character points. Teacher-logged behaviour notes, worth a few points either way in a student's daily score. I settled for that by inference, because nothing in the codebase told me what the word meant. Then I shipped changes to the code that reads those records without ever being sure what they represented. I still can't tell you why it's called citizenship.

The app was built in Lovable over about six months, then handed to us to make production ready in five weeks against a fixed pilot date. It's an education platform: it scores students daily on effort rather than test results, and reports that score to their parents as a letter grade. Supabase for the backend and auth, React on the front end. This was my first time working on a fully vibe-coded app, so my expectations were vague. Exposed API keys, missing auth, the kind of thing you read about on X and Reddit.

I found all of that, and fixing it was the easy part. What cost me was code I could read perfectly and still not say why it existed.

The first job was getting it to run locally. I was invited to the Lovable project and exported the code. I did a quick overview of the backend and found 132 migration files. So I create a fresh Supabase project, point the app at it, run the migrations, and I have the app.

It wasn't that smooth. The second migration failed, calling a function that didn't exist because the extension providing it wasn't installed. I installed the extension into the schema the later migrations expected. Migration thirty-five then failed, because some of the earlier migrations call that same function with a schema prefix and some without, and no single install location satisfies both. The fix was moving the extension and widening the database search path. Two lines, most of an afternoon.

Another migration dropped an access policy from a table no migration creates. The table isn't in the export. It isn't in production either. I marked that migration applied without running it. Another created a trigger an earlier migration had already created, and Postgres has no if-not-exists for triggers, so the second one just fails.

Two days of this and the schema was up. Then the app couldn't read it. Sixty-six of seventy tables had no permissions for any role the application uses; every request came back permission denied. Those grants exist in the original project. They were applied silently at setup, six months earlier, and nothing recorded that it had happened.

With the grants fixed I could finally use the app.

The second job was the audit. I don't read a codebase this size by hand, so I told the agent to analyse it and run one. No formal prompt, just a casual command, because I wanted to see what it would yield.

The first pass came back with 108 findings, 23 critical. They were what you'd expect. A setup endpoint that creates a platform administrator and returns the password, sitting before any authentication check and before the rate limiter. A report endpoint with no authentication at all that will hand you any child's daily scores and their parent-facing summary if you know their ID. A function that deletes an entire school, gated on nothing more than the presence of an authorization header, which the anonymous key in the browser bundle satisfies.

Then we ran the same codebase through something heavier. An orchestrator forbidden from reviewing anything itself, splitting the code into the smallest reviewable units it can find. Every unit goes to two different frontier models in parallel, blind to each other. Anything only one of them reports, or that they rate differently, goes to a second round where each is shown the other's finding and asked to concur, revise, or refute with evidence from the code. Anything still disputed after a third round goes back to the orchestrator, which reads the cited lines and decides. Three passes over everything: correctness, security, production readiness.

It returned 319 findings. Eighteen of the criticals were ones the first pass had missed, including a school administrator being able to promote themselves to platform superadmin, because a policy checked who you were and never checked what you were allowed to become. That register replaced mine as the one we worked from.

Both passes still missed things. Testing manually, I found that any logged-in user can create a school. A student can create one and make themselves its administrator.

All of that was findable. What follows is the part where reading harder didn't help.

An attendance function that sends a teacher's typed note to a language model to work out who was absent. Fully built, permission-checked, wired to a paid AI account, and called from nowhere in the app. I couldn't tell whether it had been retired or never wired up, so I hardened it during the security pass like a live function.

A screen where teachers enter the day's data one row at a time, with a comment saying a bulk control was removed because it caused errors. The bug isn't written down anywhere else, so I can't tell what it was or whether it still exists. Somebody documented that decision, and it still wasn't enough to act on.

The scoring is the one I couldn't fix.

The product gives every student a score each day and shows it to their parent as a letter grade. That number is the product. It's what the schools are paying for.

Four things compute it. A daily calculator and a weekly one, which derive points from goals completed. A general scorer and a batch scorer, which compute something else entirely: a blend of effort, mastery and reliability over rolling two-week and one-month windows. All four write to the same field on the same row. Whichever runs last wins.

They don't agree. The general scorer and the batch scorer weight the same sub-score sixty-forty and forty-thirty-thirty, so the same student gets a different number depending on which one ran. The letter grade boundaries are written out in five separate places. The daily calculator stamps its output with one version label and the other two stamp another, so somebody knew, once, that these were different things.

None of that is hard to see. It's all sitting in the files. What's missing is any record of which one is authoritative. Nothing in the code, the comments or the client's description says.

So I can't pick one. Picking one changes what grade a child gets and what their parent is told about their week. I can't merge them for the same reason, and I can't test my way out, because the test would encode whichever answer I guessed.

I escalated it as a product question and it left my hands.

Six months of building produced an application that runs. That part is real. But it runs with no record of why any of it is the way it is, and the moment someone else has to change it, that's the bill coming due.

I'd expect the tool to keep that record. Not the client. The whole point of these tools is that non-technical people don't need to know what a developer knows, and knowing that the reason behind a decision is worth keeping is exactly the kind of thing a developer knows.

So the code was readable. The reasoning was gone. And the person who could have told me was never told it mattered.


Other writing →

联系我们 contact @ memedata.com