The build worked.
At least, that's what the tests said.
We'd made several focused changes to ProHelp Viewer. A retired-topic restore problem was working again. The Viewer was correctly handling PageSnip's general Help request and opening the project's home page instead of resurfacing the last topic.
The warnings we'd been cleaning up were gone too.
I worked through the runtime tests, and the new build appeared to be solid.
Then I opened another database.
Not a damaged database.
Not an experimental database.
Not something I'd manually edited or pulled out of an ancient backup directory without knowing where it came from.
It was an existing, valid authoring database using the current main schema version.
ProHelp Viewer failed while opening it.
That was the moment the build stopped working.
The table that wasn't there
The error came down to a missing table named:
prohelp_authoring_variant_state
That table was part of the newer multilingual authoring support.
Databases created or updated through the latest development path had it. The databases I'd been using for the focused tests had it. The code expected it to be present.
This older database didn't have it.
The code saw a schema-4 database and assumed that schema 4 meant the complete current schema-4 structure.
The database had a different story.
It was also schema 4, but it came from an earlier point in the development of that schema. It was valid before the multilingual extension tables were introduced.
Nothing had gone wrong when the file was created.
The current build simply no longer understood one of its own valid historical states.
Save As proved it wasn't a one-file problem
I tried Save As.
The new copy failed for the same reason.
That was useful because it ruled out the comforting explanation that there was something peculiar about one original file.
Save As had done exactly what Save As was supposed to do. It faithfully copied the database.
The trouble was that it faithfully copied the older structure too.
The copy didn't suddenly acquire tables that weren't in the source. When the Viewer opened the copy, the same current code made the same current assumption and failed in the same place.
That exposed a larger hole in the testing.
I hadn't just missed one old database.
I'd missed the entire path by which an older valid structure could continue reproducing itself through normal product behavior.
What I had actually proved
The focused tests had been legitimate.
They proved that the new work hadn't broken the behaviors I was specifically testing.
They showed that retired topics restored correctly. They showed that PageSnip's no-topic Help request reached the right home page.
Those tests mattered.
But they all happened inside databases that already contained the current supporting tables.
What I had proved was:
The new build works with data that already looks the way the new build expects data to look.
That's useful proof.
It just wasn't complete proof.
I hadn't tested the product's history.
The missing dimension was time
Most of the time, we think of a test matrix in terms of features and paths.
Did the button work?
Did the document save?
Did the Reader refresh?
Did the language change?
Did the topic restore?
Did the integration request arrive correctly?
What I hadn't added to the matrix was time.
Did the feature work in a database created after multilingual authoring was added?
Yes.
Did the application still open a valid schema-4 database created before those tables existed?
No.
The code path was correct across the features I'd tested.
It was incorrect across the product's timeline.
That was the real defect.
The AI hadn't done anything unreasonable
This is the kind of failure that's easy to describe as an AI mistake.
The AI had worked on the code. The AI had helped make the changes. The AI had reviewed the schema and the open path.
But the code it could see described the current structure.
The current initialization code created the multilingual tables. The current database contained them. The current feature expected them.
From that evidence, assuming those tables existed wasn't absurd.
What was missing was historical context.
The assignment hadn't sufficiently emphasized that an older, still-valid schema-4 database might predate the multilingual extension tables.
Once the actual failing database and the error were in front of us, the problem became fairly clear.
The AI could inspect where the table was created, where it was used, and why the normal open process hadn't brought older databases forward.
The hard part wasn't typing the repair.
The hard part was recognizing that the current schema definition wasn't the complete specification.
We could've fixed the wrong thing
There were several ways we could've made the immediate error disappear.
We could've caught the missing-table error and skipped the multilingual state.
We could've told users that the older database was incompatible.
We could've created a separate conversion utility.
We could've patched Save As so that copies gained the missing tables, while leaving the original open path broken.
We could've required the user to create a new database and manually move the content.
Any of those approaches might have made one test pass.
None of them addressed the actual contract.
This was a valid authoring database.
Opening an authoring database is where the application needs to determine whether that database contains all the structures required by the current authoring system.
The right repair belonged there.
The next build repaired the history
The failed build was i084.
Because it had failed owner testing, the correction moved to i085.
The repair was deliberately narrow.
When an authoring-master database opens, the Viewer now performs a compatibility check for the multilingual authoring extension structures. If one of the required tables is missing, the application creates it.
It doesn't discard the database.
It doesn't rewrite unrelated content.
It doesn't demand that the user run a separate converter.
It doesn't assume that every schema-4 database has one exact internal history.
It inspects the actual database and adds only what the current application requires and the older database legitimately lacks.
The repair is also idempotent.
That means the application can perform the check again the next time the database opens. If the table is already there, it leaves it alone. If the database contains some extension structures but is missing another, the repair can complete what's missing without treating the whole database as an all-or-nothing case.
That matters because real migrations don't always encounter one perfectly known starting point.
Then we tested the copy again
The older database opened correctly under i085.
Its existing content remained intact.
The multilingual extension structures were added safely.
Then Save As copied the repaired database.
The copy reopened correctly too.
That second part mattered just as much as the first.
Without it, we might have repaired the original open path while leaving a normal product workflow capable of reproducing the incompatible state.
The current databases still opened.
The i084 behavior we wanted to preserve still worked.
The compatibility repair didn't become an excuse to reopen or redesign everything around it.
It did one job: it allowed older valid databases to cross safely into the current application.
A build can pass every new-feature test and still fail the product
That's the part of this experience that has stayed with me.
We tend to test the thing we just changed.
That's understandable. The change is where we expect the risk to be.
But a new build isn't released into an empty room.
It enters a room full of things the old builds created.
Databases.
Settings.
Documents.
Backups.
Copies.
User habits.
Assumptions embedded in saved data.
The new feature may be correct.
The updated screen may be correct.
The new schema may be correct.
The current test database may be correct.
And the build can still be wrong because it no longer understands the product that existed before it.
Save real old data
One practical change from this is that I'm becoming more deliberate about retaining real historical test files.
It's tempting to generate a test database that represents what we believe an old database looked like.
But a file we construct today is influenced by what we know today. We may include a field that didn't really exist. We may initialize something an old version left empty. We may accidentally make the old fixture cleaner and more consistent than the actual product ever was.
A real database created by the earlier software contains the truth.
It contains the exact tables, defaults, omissions, and combinations that the product produced at that time.
That makes it more valuable than our memory of the schema.
For important persistent formats, I want representative databases from meaningful points in the product's history, identified and preserved as compatibility tests.
Not every build.
Not every experiment.
But enough to prove that the path from then to now still exists.
The version number wasn't enough
This also changed the way I think about schema version checks.
A version number is useful, but it can become a substitute for looking.
The failed database reported schema 4.
The working databases reported schema 4.
The code assumed that meant they were structurally identical in every way that mattered.
They weren't.
A schema version often describes a broad contract. Features added during the life of that version may create additional tables or metadata. Optional paths may initialize structures at different times. A repair may have shipped in one build but not another.
So the safer question wasn't only:
Is this schema 4?
It was also:
Does this database actually contain the structures the current schema-4 application requires?
The label told us where to begin.
Inspection told us what was really there.
The failure was useful
I don't enjoy rejecting a build that looked finished.
But this was a good failure to catch.
It happened while I was deliberately opening other existing databases and trying normal workflows. It happened before the compatibility assumption could reach users. It exposed not just a missing table, but a missing category in the test plan.
The code needed a repair.
The process needed one too.
From now on, a change involving persistent data has to answer more than:
- Can the current build create it?
- Can the current build use it?
- Can the current build save and reopen it?
It also has to answer:
- Can the current build open what the previous builds created?
- Can it upgrade that data without losing anything?
- Can the upgrade safely run again?
- Do Save As, backup, restore, and copy operations preserve the corrected state?
- Are two files with the same version number actually guaranteed to contain the same structures?
Those questions add some work.
They also prevent the much larger job of explaining to a user why the software that created their data can no longer open it.
Yesterday's database is part of today's build
The i084 build worked with the databases we'd been using to build i084.
The i085 build worked with those databases and with the valid databases that existed before them.
That's the difference between a feature working and a product continuing to work.
The source code had moved forward.
The schema had moved forward.
The authoring system had moved forward.
The repair made sure the user's data could move forward with them.
Yesterday's database wasn't an edge case outside the specification.
It was part of the specification all along.
We just hadn't tested that part yet.
Related guide
For a broader framework for treating older data, migrations, copies, backups, and compatibility as part of the feature itself, read The Past Is Part of the Specification.