August 26, 2026
Project Theseus, Part III: The First Plank

By agsmith
13 min read
In Part I, I asked a question that had been bothering me for a while: what happens when it becomes cheaper to generate the small amount of behavior an application actually needs than it is to safely inherit the much larger implementation somebody else built?
Part II took that idea a little further. I started sketching what a system for doing this might actually look like — specialized agents discovering dependencies, figuring out what an application really expects from them, generating narrower replacements, challenging those replacements, and ultimately producing enough evidence that we could have some confidence the application was still the same application afterward.
I ended that piece with a question I deliberately didn't try to answer.
Does generated code actually improve software supply-chain security? Or are we just replacing shared, visible risk with millions of unique implementations that nobody really understands?
I've been thinking about that question ever since.
And recently, I decided to try something.
The First Plank
A few weeks ago, I picked a mature Java service and started looking through its dependency graph. I wasn't really looking for a particular vulnerability. I was interested in something simpler.
What dependencies does this application carry that it doesn't actually need?
Like most enterprise Java applications that have been around for a while, the dependency graph isn't small. There is Spring Boot and Spring Cloud, Jackson, RabbitMQ, Redis, Hystrix, OpenSAML, Springfox and plenty more underneath them.
Most of that software is there for a reason. The application really does need it, or at least replacing it would be a much larger architectural question than the experiment I wanted to run.
But Gson caught my attention.
com.google.code.gson:gson:2.8.+
It wasn't coming from Spring Boot. It wasn't buried somewhere in a transitive dependency tree. Somebody had explicitly added it.
So I started following where it was used.
Eventually, I ended up at one file, one method and essentially one line of code.
var configs = new Gson().fromJson(
configurationString,
ApplicationConfig[].class
);var configs = new Gson().fromJson(
configurationString,
ApplicationConfig[].class
);That was basically it.
A Spring @Bean method was reading a JSON string from an environment property and turning it into an array of fairly simple Java objects. There were four fields, one of which contained a list of strings. The structure was known ahead of time and the input was tightly bounded.
There wasn't anything wrong with using Gson for this. In fact, using a mature library to parse JSON is exactly the kind of decision most of us would make without thinking twice about it.
But that's what made it interesting.
For this very small piece of behavior, the application had taken on an external software dependency, along with its implementation, release lifecycle, vulnerability history and whatever future remediation obligations might come with it.
It seemed like a good first plank.
Before Replacing Anything
My first instinct was to write the replacement.
That's the easy part now.
Give a modern model the classes, show it the expected JSON structure, tell it that it can't use external dependencies, and it will happily produce an implementation in seconds.
But that wasn't really what I was trying to prove.
One of the things I kept coming back to while writing Part II was that generation isn't the particularly interesting problem anymore. Verification is.
It's easy to generate 100 or 200 lines of code.
It's considerably harder to know whether those 200 lines are actually right.
And if Theseus eventually becomes a system that replaces mature third-party software with generated implementations and considers the job finished because everything compiles, I don't think we've improved anything. We may have made it worse.
We would be replacing code that has been used, tested and examined by potentially millions of people with code that might have existed for thirty seconds.
So instead of writing the replacement, I started writing tests.
I ended up with eleven.
Together they exercised the different behaviors I could find around that configuration: null and blank values, empty JSON, malformed JSON, valid configurations with different combinations of fields, malformed delimiters, normalization behavior and multiple configuration objects in the same input.
Once I felt reasonably good about the coverage, I ran them against the existing Gson implementation.
Three failed.
That was probably the most useful result of the entire experiment.
I Was Wrong
There was no generated code yet, so the failures couldn't have been caused by my replacement.
They were caused by my understanding of the application.
In one case, I had looked at a configuration value and made what seemed like a reasonable assumption about the order of two pieces of information separated by delimiters.
I had them backward.
My test described what I thought the application did. The production implementation described what it actually did.
The test caught the difference before I changed anything.
Another failure was even more interesting.
I had assumed that malformed JSON would result in an empty configuration. That seemed like sensible behavior for this particular application, so that's what I wrote into the test.
Except that wasn't what happened.
Gson threw a JsonSyntaxException, which propagated upward. Because the parsing happened while creating a Spring @Bean, malformed configuration could prevent the application context from loading at startup.
That wasn't a problem introduced by generated code.
It was existing behavior I hadn't understood.
Arguably, it was a latent defect.
So I fixed it in the existing implementation first.
Then I ran the tests again.
This time all eleven passed.
Only then did I start thinking about the replacement.
The Part I Didn't Expect
Going into this, I assumed the replacement implementation would be the interesting artifact.
I'm not sure I believe that anymore.
The tests were the interesting artifact.
Before I could safely replace anything, I had to work out what the application actually expected this tiny piece of software to do. That meant understanding what inputs were valid, what should happen when they weren't, which behaviors were intentional and which ones had simply emerged over time.
Some of that information existed in the code. Some of it existed in configuration. Some of it existed only in assumptions that turned out to be wrong.
And once those behaviors were captured in tests, something changed.
The implementation became a lot less important.
I could replace Gson today. I could replace my replacement six months from now. If the requirements change, another implementation could be generated again.
The tests can remain.
The more I've thought about that, the more I think this may be one of the more important ideas behind Theseus.
Maybe the implementation is disposable because the specification isn't.
The Replacement
With eleven passing tests against a baseline I now understood, I finally wrote the replacement.
The target was intentionally small: a package-private utility class using nothing outside java.util.regex and the standard Java libraries. No third-party JSON library. No Jackson. No JSON-P. Nothing else to pull in.
The reason I was comfortable trying that was the shape of the data itself. It was entirely static and flat — a top-level array containing objects with a few string fields and one string-array field. There was no nesting, no numeric data, no polymorphism and no unknown structure to account for.
For this particular job, a general-purpose JSON parser suddenly felt like a lot of machinery.
A purpose-built parser for the exact shape the application needed came out to about 140 lines, and the core of it looked like this:
final class ApplicationConfigParser {
private static final Pattern STRING_FIELD = Pattern.compile(
"\"(\\w+)\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"");
private static final Pattern RESOURCES_ARRAY = Pattern.compile(
"\"resources\"\\s*:\\s*\\[([^\\]]*)\\]");
private static final Pattern QUOTED_STRING = Pattern.compile(
"\"((?:[^\"\\\\]|\\\\.)*)\"");
private ApplicationConfigParser() {}
static ApplicationConfig[] parse(String json) {
if (json == null || json.isBlank()) {
return new ApplicationConfig[0];
}
try {
return parseArray(json.trim());
} catch (Exception e) {
return new ApplicationConfig[0];
}
}
private static ApplicationConfig[] parseArray(String json) {
if (!json.startsWith("[") || !json.endsWith("]")) {
return new ApplicationConfig[0];
}
List<ApplicationConfig> result = new ArrayList<>();
for (String obj : splitTopLevelObjects(
json.substring(1, json.length() - 1).trim())) {
String trimmed = obj.trim();
if (!trimmed.isEmpty() && !trimmed.equals("null")) {
result.add(parseObject(trimmed));
}
}
return result.toArray(new ApplicationConfig[0]);
}
private static ApplicationConfig parseObject(String obj) {
ApplicationConfig config = new ApplicationConfig();
Matcher fieldMatcher = STRING_FIELD.matcher(obj);
while (fieldMatcher.find()) {
String key = fieldMatcher.group(1);
String value = unescape(fieldMatcher.group(2));
switch (key) {
case "appName":
config.setAppName(value);
break;
case "basePath":
config.setBasePath(value);
break;
case "type":
config.setType(value);
break;
}
}
Matcher arrayMatcher = RESOURCES_ARRAY.matcher(obj);
if (arrayMatcher.find()) {
List<String> resources = new ArrayList<>();
Matcher itemMatcher =
QUOTED_STRING.matcher(arrayMatcher.group(1));
while (itemMatcher.find()) {
resources.add(unescape(itemMatcher.group(1)));
}
config.setResources(resources);
}
return config;
}
}final class ApplicationConfigParser {
private static final Pattern STRING_FIELD = Pattern.compile(
"\"(\\w+)\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"");
private static final Pattern RESOURCES_ARRAY = Pattern.compile(
"\"resources\"\\s*:\\s*\\[([^\\]]*)\\]");
private static final Pattern QUOTED_STRING = Pattern.compile(
"\"((?:[^\"\\\\]|\\\\.)*)\"");
private ApplicationConfigParser() {}
static ApplicationConfig[] parse(String json) {
if (json == null || json.isBlank()) {
return new ApplicationConfig[0];
}
try {
return parseArray(json.trim());
} catch (Exception e) {
return new ApplicationConfig[0];
}
}
private static ApplicationConfig[] parseArray(String json) {
if (!json.startsWith("[") || !json.endsWith("]")) {
return new ApplicationConfig[0];
}
List<ApplicationConfig> result = new ArrayList<>();
for (String obj : splitTopLevelObjects(
json.substring(1, json.length() - 1).trim())) {
String trimmed = obj.trim();
if (!trimmed.isEmpty() && !trimmed.equals("null")) {
result.add(parseObject(trimmed));
}
}
return result.toArray(new ApplicationConfig[0]);
}
private static ApplicationConfig parseObject(String obj) {
ApplicationConfig config = new ApplicationConfig();
Matcher fieldMatcher = STRING_FIELD.matcher(obj);
while (fieldMatcher.find()) {
String key = fieldMatcher.group(1);
String value = unescape(fieldMatcher.group(2));
switch (key) {
case "appName":
config.setAppName(value);
break;
case "basePath":
config.setBasePath(value);
break;
case "type":
config.setType(value);
break;
}
}
Matcher arrayMatcher = RESOURCES_ARRAY.matcher(obj);
if (arrayMatcher.find()) {
List<String> resources = new ArrayList<>();
Matcher itemMatcher =
QUOTED_STRING.matcher(arrayMatcher.group(1));
while (itemMatcher.find()) {
resources.add(unescape(itemMatcher.group(1)));
}
config.setResources(resources);
}
return config;
}
}That's essentially the parsing logic.
The three compiled Pattern constants handle the scalar fields, the resources array and the individual elements inside that array. The parse() entry point deals with null, blank or unparseable input by returning an empty array. Everything after that is mostly mechanical traversal of a structure we already know.
And I think that distinction is important.
This isn't a general-purpose JSON parser.
It isn't trying to be one.
It understands exactly the shape this application reads and essentially nothing else.
I realize there is something slightly uncomfortable about looking at a mature library like Gson, looking at 140 lines of purpose-built parsing code, and deciding the latter might be preferable. For most of my career, I probably would have argued the opposite.
And as general advice, I still would.
If an application needs to parse arbitrary JSON, use a JSON parser.
But that isn't quite the question I'm asking anymore.
The question is whether an application that needs four fields from one completely known structure necessarily needs to inherit all the capabilities, lifecycle and future obligations of a general-purpose implementation.
In this case, I decided it didn't.
What I ended up with is code that does one thing, lives inside one package and can be deleted or regenerated if the requirement ever changes. There is no independent release cycle to follow, no upstream maintainer to wait for and no additional dependency to carry forward.
More importantly, I'm not particularly invested in this implementation.
If someone produces a better 140 lines tomorrow, we can replace it.
If a model produces a better version six months from now, we can replace it again.
If the configuration format changes enough that using a general-purpose parser makes sense again, we can throw the whole thing away and bring one back.
The thing I'm increasingly reluctant to throw away is the behavioral specification we built before writing any of it.
Those eleven tests are what let this code be disposable.
Without them, this is just 140 lines of homegrown parsing code that I hope works.
With them, it's an implementation of a behavior we can independently describe, exercise and challenge.
That difference is starting to feel like the heart of Theseus.
The implementation isn't precious.
The evidence that tells us whether we can safely replace it is.
So I Replaced It
Once the tests were passing against the existing implementation, the actual replacement was fairly uneventful.
I wrote a small package-private parser that used only the Java standard library. No Gson. No Jackson. No JSON-P.
It ended up being around 140 lines.
The parser understands the specific configuration structure this application uses and nothing more. It walks the input, identifies the objects and expected fields, extracts the array values, and turns them into the configuration objects the application already expects.
Some of that parsing uses regular expressions.
I realize that sentence will probably make a few people uncomfortable.
Replacing a mature JSON parser with 140 lines of homegrown parsing code sounds suspiciously like the kind of thing we've spent the last twenty years telling developers not to do.
And as general engineering advice, I still think that's right.
I'm certainly not suggesting that teams start replacing JSON parsers with regular expressions.
What I'm starting to question is whether every application that consumes something that happens to be formatted as JSON necessarily needs a general-purpose JSON parser.
This application doesn't need to understand arbitrary JSON documents. It doesn't need arbitrary nesting, polymorphism, dynamic schemas or an open-ended object model. It needs to understand one small, known configuration structure.
So I wrote exactly that.
Nothing more.
And if that requirement changes someday, I don't feel particularly attached to those 140 lines. They can change too.
That's sort of the point.
Did It Work?
After replacing the Gson call, I ran the eleven tests again.
They passed.
Then I ran the full application test suite.
That passed too.
Finally, I removed Gson from the build and checked the resolved dependency graph.
./gradlew dependencies --configuration compileClasspath | grep gson./gradlew dependencies --configuration compileClasspath | grep gsonNo output.
Gson was gone.
That was satisfying, but the absence of the dependency wasn't really what gave me confidence in the change.
It was the path we had taken to get there.
We could show why the dependency existed. We could show the behavior the application actually required from it. We had tests describing that behavior independently of the replacement, and those tests ran successfully against both implementations. We could run the application's broader regression suite, and we could demonstrate that the dependency itself was no longer present.
None of those things individually proves that the new implementation is perfect.
Together, though, they start to look like evidence.
And evidence is something another engineer can inspect, a security reviewer can challenge, and an auditor can reproduce.
That's a very different proposition from asking an LLM to rewrite something and trusting that it got it right.
So Is It Actually Safer?
This brings me back to the question I left hanging at the end of Part II.
Does generated code improve software supply-chain security?
After doing this once, I don't think generation itself has much to do with the answer.
The replacement isn't safer because AI helped write it. It isn't automatically safer because it's smaller, and removing a third-party dependency doesn't magically make the remaining code secure.
What feels different is that I now understand this particular behavior considerably better than I did before I tried to replace it.
And I have something executable that describes it.
There is also a fair counterargument to all of this that I've been thinking about.
Open-source maintainers have access to these same AI capabilities.
They're going to use agents to find vulnerabilities, generate patches, create tests and push releases faster too. The remediation cycle that historically took weeks or months may eventually take hours.
That's unquestionably good.
But I'm not sure it eliminates the ownership question.
Even if somebody else's remediation happens almost immediately, the application is still coupled to somebody else's implementation, priorities, release decisions and evolution.
Maybe that coupling is absolutely worth it for Spring.
It's almost certainly worth it for cryptography.
It may be worth it for a general-purpose JSON parser too.
But is it worth it for one line that turns a small, fixed configuration string into four fields?
I'm less certain.
Maybe AI doesn't make open source go away.
I don't think it does.
Maybe it just changes the point at which sharing code is worth the dependency.
The CVE We Don't Have to Fix
There is a security consequence here that is easy to overlook.
Gson has had vulnerabilities. CVE-2022–25647, for example, was a high-severity deserialization vulnerability affecting Gson versions prior to 2.8.9. Under certain conditions, deserialization of untrusted data through Gson's internal classes could lead to denial of service.
The version in this application had already moved beyond that particular vulnerability. So I don't want to claim that removing Gson somehow fixed a CVE that was actively present in the application.
What it did was something slightly different.
It removed Gson from the application's vulnerability lifecycle altogether.
If another vulnerability is discovered in Gson tomorrow, this application doesn't need to determine whether it's exploitable. We don't need a security ticket to investigate it, an engineer to trace the affected code path, a dependency upgrade, a regression cycle, a patch window or an exception explaining why the finding doesn't apply.
There is nothing to remediate.
The dependency isn't there.
That distinction has started to matter to me.
Traditional vulnerability management asks us to continuously evaluate the software we've inherited. A CVE appears, scanners find the package, somebody determines whether we're actually vulnerable, and eventually we patch, mitigate or accept the risk.
Theseus asks an earlier question.
Did we need to inherit that software in the first place?
For something like a general-purpose JSON library, the answer will very often be yes. But in this particular application, all of that capability existed to support one narrow and completely known configuration format.
Once that behavior became owned by the application itself, the Gson CVE surface disappeared with the dependency.
Not because we patched it.
Because there was nothing left to patch.
And that may be a more interesting form of remediation.
There Is Another Side to This
Part II raised another problem that I don't want to lose sight of.
If thousands of organizations depend on the same vulnerable library today, that's obviously a supply-chain problem. But it's also a very visible one. The library has a name. It has versions. Eventually it has a CVE. There is a maintainer, a patch and a shared body of knowledge about the problem.
Now imagine those same organizations independently generating their own replacements using the same handful of models.
The source code might be different, but the underlying assumptions might not be.
We could end up with millions of supposedly unique implementations that all contain variations of the same model-induced mistake.
That might actually be harder to reason about than the shared dependency we started with.
I still don't have a clean answer for that.
What this experiment did change for me is where I think the answer might live.
Maybe we don't need to trust the generation layer.
Maybe we need to trust the verification process around it.
If the expected behavior is explicit, if the important security properties are explicit, and if the evidence required to accept an implementation can be reproduced independently, then who — or what — typed the implementation starts to matter a little less.
Human-written code still has to pass.
Open-source code still has to pass.
Generated code still has to pass.
And whatever replaces the generated code someday still has to pass.
Verification-first doesn't guarantee that software is safe.
But it starts moving safety away from where the code came from and toward what we can actually demonstrate about it.
I think that's a much more interesting place to be.
One Plank
I'm trying not to overstate what this experiment proved.
I removed Gson from one application.
That's it.
One dependency that supported one very narrow piece of behavior.
But maybe that's exactly the right place to start.
Mature applications accumulate things like this. A library gets added because somebody needs one utility function. Years pass. Nobody removes it because it works, and removing working software has traditionally carried more risk than simply leaving it alone.
Eventually those decisions become the software supply chain.
AI changes some of those economics.
Not because code suddenly becomes free, but because implementation is becoming cheap enough that understanding and verification may become the dominant costs.
The more I work on Theseus, the more I suspect that's where the interesting engineering problem actually is.
What Comes Next
There is one other thing I realized after finishing this experiment.
I had essentially acted out the architecture from Part II myself.
I found the dependency and traced where it was used. I tried to understand its behavioral contract. I wrote tests to capture that behavior, discovered that some of my assumptions were wrong, corrected them, produced a replacement and then tried to establish enough evidence to convince myself that the replacement hadn't changed the application.
In other words, I played the dependency agent, the contract-discovery agent, the test agent, the implementation agent and the verifier.
That's obviously not Project Theseus.
Not yet.
It's a person manually following the workflow that Theseus is supposed to perform.
So that's where I want to go next.
I want to see whether those responsibilities can actually be separated into bounded agents. Whether one agent can find a candidate dependency while another independently figures out what the application expects from it. Whether tests can be generated without knowing what the replacement will look like. Whether another agent can build the smallest possible implementation, and whether yet another can attack it rather than simply agreeing that it looks good.
And eventually, whether all of that evidence is strong enough that we can make a meaningful decision about replacing a piece of software without trusting any single model — or any single human — to tell us that it's correct.
That's a much harder experiment.
But now there is at least somewhere concrete to start.
The Ship of Theseus wasn't rebuilt in an afternoon. The Athenians replaced its planks one at a time until the question of whether it was still the same ship became a philosophical problem instead of a practical one.
This application still compiles. The tests still pass. As far as we can demonstrate, its behavior hasn't changed.
We replaced one plank.
Now I want to see if the ship can learn how to replace the next one.