Most AI coding stories start from a blank page. Mine did too. When I built ScottAdamsSaid, I started with an empty repo, an empty database, and zero users.
Web2Project is the opposite. We forked it from dotProject in November 2007, and I’ve been part of it since day one, including a stretch as its accidental product manager. Some installs started life as dotProject years before the fork and have been upgraded release after release. I know of installs with data going back to 2004. That’s over 20 years of project data, reports, task logs, and files. It’s not something to touch lightly.
For 5.0.0, I used Claude Code to drag it into the present. Between 4.0.2 and 5.0.0 RC2, there are 401 commits touching 728 files, and 334 of those commits carry a “Co-Authored-By: Claude” line. The codebase came out about 900 lines smaller, even after bundling a new UI, and it now requires PHP 8.3 and targets MySQL 8.
Most of it went great. The parts that didn’t were never about the code.
AI is great at fixing old code. The hard bugs in an old app live in the data, and nobody wrote down how the data got that way.
Note: From here on out, I use “we” to refer to Claude and I. Yes, I’m anthropomorphizing it.
Where the AI Did Well
First, the cleanup nobody volunteers for. On October 9 alone, there were 157 commits, and 129 of them were PHPDoc fixes. Deprecated helpers came out. Code that called things PHP 8 removed ($php_errormsg, mysql_get_client_info()) got replaced. The unit tests moved to PHPUnit 11. Every open source project has a pile of this work, and every maintainer puts it off.. myself included.
Next, the error logs and the routes. The list tables logged about 100 warnings per crawl for a divider nobody had configured. When a log is full of junk, nobody reads it, and the real errors hide in there. Web2Project sends every page through index.php, so a crawl of every reachable page became the regression check for each fix. It turned up pages that 500’d, tabs that crashed on first visit, and filters that did nothing.
Finally, security. RC1 removed the eval() that loaded translation files and fixed stored and reflected XSS in the “pick a company” popup. My favorite detail is in that XSS commit: after the fix, a script in a company name still ran on 24 pages, down from 99. Claude reported the 24 instead of declaring victory.
RC2 went further. Web2Project has had a CSRF form token for years. The function that validated it had an empty body. It never checked anything. The search box was open to SQL injection from any logged-in user. The signup form’s duplicate username check sent a redirect and then created the account anyway. All fixed, and signup now has a self-hosted proof-of-work check instead of a third-party captcha.
Swapping the Whole UI in an Afternoon
4.x shipped with three selectable themes: web2project, w2p-snowball, and wps-redmond. Users could pick one, and every page had to render right in all three. 5.0 has one: Bootstrap 5.3.
So how much effort did that take? On October 9, in a single Claude Code session, Claude:
- deleted the two extra themes (1,531 lines gone, plus a padding hack that only existed for wps-redmond)
- added a Bootstrap 5.3 theme with the files bundled locally, no CDN
- replaced the layout tables in the core classes, then in every module, one commit per module
- removed the theme preference and the
host_stylesetting, with migration 063 to clear the old values out of the database
That’s 22 commits, +4,383 and -5,827 lines, and the timestamps run from 2:12pm to 3:04pm. Fifty-two minutes. I had attempted to add a Bootstrap theme a couple of times over the years. After hitting all the sharp edges and life interrupting the effort, I gave up and instead made small incremental changes of standardizing theming and css. While it didn’t get the result I wanted at the time, it did come in useful.
Four days later, four more commits restyled it on the NiceAdmin layout, with a collapsible sidebar and pages that work on a phone.
It was cheap for a boring reason: I had cleaned up the old theming system so the boundaries were clean. Every page already emitted the same CSS classes (std, tbl, list, hilite) and went through the same tab box and title block code. Claude mapped those onto Bootstrap instead of rewriting each page. Then it rendered all 303 crawlable pages in the new theme and confirmed they threw exactly the same PHP warnings as the old one.
It wasn’t free. File-type icons broke, the password strength meter went dead, and the calendars lost their weekend and holiday shading. All three got fixed, but only because someone clicked around.
Code with a clean seam is cheap to replace. Data doesn’t come with one.
Where It Ran Into History
Then we got to the data.
The upgrade manager runs each SQL migration and is supposed to ignore only “this column already exists” errors. Two of its four checks were missing === false, so it ignored every error. The installer and the upgrade page always reported success, no matter what failed.
I know who wrote that line. It was me.
Once errors showed up again, the rest fell out:
- Migrations 050 and 051 clean up tables that only exist on installs converted from dotProject. On every native install they failed, and nobody saw the error.
- Migration 046 was missing a semicolon, which glued two statements together. On upgraded sites,
pref_userstayed a VARCHAR(12). - Migration 060 renamed
visit_datetimetovisit_datetimeinstead of renamingvisit_date. On every upgraded site, recording a forum visit failed. - Migration 056 rebuilds the version history table, but every install has two rows for version 1. On MyISAM, the rebuild failed partway and kept only versions 0 and 1.
- MySQL 8 turns on
ONLY_FULL_GROUP_BYby default. Under it, the crawl hit 156 failing queries and 16 pages returned a 500, including Companies, Tasks, and the project view.
And then we made it worse. During 5.0, we fixed a batch of old migrations (022 through 060) by editing them in place instead of adding new ones. Fresh installs got the fixes. A 4.x site is already at database version 62 and only runs 063 and up, so it never saw them. RC1 shipped with two different schemas reporting the same version number.
RC2 adds migration 070 to close that. To test it, Claude installed 4.0.2 through its own installer, seeded rows with zero dates and bad values, then upgraded on both MariaDB and MySQL 8. Zero migration errors, existing rows kept, and the schema matches a fresh install.
None of that shows up by reading the code. It shows up when you run the code against a real database with real server settings and rows that have survived 20 years of upgrades.
The LDAP bug shows the other half. On a user’s first LDAP login, the contact insert failed (an owner of -1 in an unsigned column), so the user was saved without a contact and disappeared from User Management. Fixing the code took one commit. The users already broken by it needed migration 065 to give them placeholder contacts.
Fixing the code doesn’t fix the rows you’ve already written.
And sometimes the answer was already out there. The timezone bug that moved tasks by the user’s UTC offset had a fix in Klaus Buecher’s fork since 2016. 5.0.0 pulls it in with credit. No model finds that by reading master.
The Commenter Was Right
When I wrote “Developers are Solving The Wrong Problem,” M Furlich pushed back in the comments: most software means nothing without the data behind it, and the hard part comes when a new app has to fit the data an organization already has.
Fair criticism. Web2Project is proof. The code was the easy part. We replaced the entire UI in an hour and spent far longer on the database.
So What Should We Do?
This applies whether you’re modernizing your own app or pointing an agent at one.
First, give the agent a running system. Real data (or a sanitized copy), the strictest database settings you’ll support, and a way to load every page. Claude found half of the bugs above because the server ran in strict SQL mode.
Next, make the system report its own failures. Because of one swallowed error, broken migrations went unnoticed for years. The CSRF check had the same problem: it looked like a safety check and did nothing. An agent can’t fix what the system hides from it, and neither can you.
Next, ask about existing rows on every fix. For each bug, decide whether data already written needs a migration. If it does, write and test that migration before you ship the code fix. And never edit a migration that has already shipped. Add a new one.
Next, test the upgrade, not just the install. Fresh installs passed everything. The bugs lived in databases that started on 3.x, 4.x, or in dotProject. Keep the last release around and upgrade from it every time.
Finally, write the history down where the next reader will look. An agent reads the code and the git log, not an old forum thread or a conference hallway conversation. Most of the 5.0.0 commit messages explain WHY each change was made, including which installs a migration affects. The next person (or agent) to touch those migrations will find the story right there.
AI can clean up 19 years of code in a few weeks and swap the whole UI in an afternoon. It can’t clean up data it doesn’t know exists.
Web2Project 5.0.0 RC2 is out now. If you run it, especially an install that started on 4.x or as dotProject, try the upgrade on a copy of your database and tell us what breaks.

