I keep building the same container
oasis containment design
Today I opened the sponge and lemma directories side by side and looked at them for a long time.
One is an agent framework. One is a learning product. Apart from both being written by me alone, they shouldn't have anything to do with each other.
I was supposed to be looking at lemma's shelter drawer — a sidebar on the left of the learning space that sorts a user's notes, canvases and imported files into groups. I wanted to regroup it. I'd been putting that off for almost three weeks, treating it as a UI thing.
Then I realized I wasn't designing a drawer at all.
sponge has a thing called a Slice. It's the parent class of everything in the system. A conversation is a Slice. A tool is a Slice. A pipeline is a Slice.
Each one carries a kind. And kind is just a string. No enum, no schema. Whatever you write is what it is.
I didn't get why at first. Then I found a line I'd written for myself earlier: the base knows nothing about the content. I wrote it as an acceptance criterion, not a slogan. What it means is — the day I want to add a new kind of thing and the base has to change its code to recognize it, the design is dead, however finished it looks.
lemma has a counterpart called Page. Its kind has four values — note, canvas, imported, folder — hardcoded in a tuple. I also put a constraint in the migration, so anything outside those four won't write to the database at all.
Both repos had a sentence sitting in them. I read them the same afternoon and then sat there for a while.
sponge: add a new kind, zero changes to the base.
lemma: this doesn't need a new system. One new interface, one new section.
At first I thought it was a coincidence.
Then I remembered something I did in mid-August. I had planned to put Statuz underneath Sponge. My words at the time: “like a compute engine inside a slice, or eventually the foundation the compute engine sits on.”
Four days later I overruled myself. Statuz became a separate layer that can serve any system, not a Sponge part.
I thought I was fixing an architectural position. Looking back, I was separating two words: part, and container. A part is what gets put inside. A container is the thing that holds parts. That took me four days.
Before that, at the end of March, I built something called Heya. The idea came to me during a school sports meet. It had one goal — my words: “to move the center of gravity.” In Codex or Claude Code the thing your eyes land on is always the project's code. I wanted to move it off.
I've made this move many times. I'm not building different products. I'm doing the same thing in different places, until I couldn't recognize it anymore.
I call it oasis containment design.
Not a framework. Not a platform. Those words answer “what's inside.” I care about a different question: where the edge is. What gets in, where it lives once it's in, who knows what it is, who's allowed to change its shape.
If I can keep one sentence:
One container, one language, one invariant — instead of a set of features and a set of types.
Here are the seven things left after I took all of that apart.
One.Complexity gets eaten by the system, not by the user.
In sponge's editor plan there's a line I wrote for myself: easy to operate is a hard requirement — not fewer things, but complexity absorbed by the system. Then half a line more: deep philosophy, shallow operation, hard engineering, and you need all three.
This is the only counterintuitive part. Containment doesn't save work. It removes no complexity at all. It moves complexity off the user and onto me. Which means a containment design that actually works is harder to write.
Two.The base doesn't know what's inside it.
I don't know what's in there and I don't need to. To add a new kind of thing, I say its name.
There's one way to test this. Add a new thing. How many lines does the base change? Zero.
Three.One base, only.
In lemma I wrote it as: every learning artifact ends up on Page and Block. Lecture notes, generated content, observation records, imported files — same place.
The day a feature tells me it needs its own table, its own engine, its own format — that's when the design starts leaking.
Four.You get exactly what you declare.
You get in by declaring. A component can render exactly the slots it declared and nothing else; it has no reach beyond that. And registering is an action you can undo.
Five.The container is stacked into existence.
It starts empty. Then patch after patch on top, each one more specific. So configuration itself decides behavior. lemma's model routing is one JSON blob. Change the model, change that blob, zero lines of code.
That sounds like nothing. What it means is that no assembled result is hardcoded anywhere.
Six.Two faces, one structure.
Every capability is at once a surface a human clicks and a tool an agent calls, on the same thing underneath. I haven't done this one. sponge says the human editing face and the agent tool face must be symmetric. It's stage three. I haven't started stage three.
Seven.How good a container is depends on what it refuses, not what it holds.
In lemma's canvas plan I wrote a list of refusals: no marquee select, no connectors, no snapping, no nesting, no minimap, and no “general-purpose canvas framework” — there is one implementation and two users.
That's the line between containment and platform-building. Containment isn't “it holds anything.” It's “anything can move in and the container doesn't deform because of it.”
After those seven I went out for a walk.
I turned back halfway along the riverside park. The whole way I was on one question: how many of those seven have I actually kept.
None. So I came back and rewrote the next part.
There's a mastery field in lemma. It's supposed to record how well a user has mastered a given concept — the single most important signal in the whole learning engine. It has sat there for a long time and no code has ever written to it once. I built a room and forgot to let anyone move in.
In sponge I wrote an acceptance line: if two applications can't each grow their own shape without changing the base, the base has been contaminated by one of them. When I wrote it I thought I was setting a rule. I was recording a ditch I'd already fallen into.
lemma's kind — I said above that it's open. Underneath it's a hardcoded tuple with a constraint. I did half a containment and told myself it was an engineering tradeoff. It wasn't. I was lying to myself.
And the courses table. It has no project_id. So today, when you open a learning space, you can't see the courses that belong to it. “Every artifact lands in one place” covers documents and nothing else. The space-to-course link is broken. I knew it was broken. It sat in my backlog for over a month.
So none of the above is a method I succeeded at. It's the shape I finally saw clearly, after failing at it many times.
If any of this looks familiar, here are six questions to ask your own code.
- Add a new kind of thing. How many lines does the base change?
- Count everything your system produces. How many bases do they land on?
- List the things you deliberately refused to build.
- Whatever you can do — can your agent do it.
- Without touching the container, can a second application grow on top of it.
- And the hardest one last: delete application A entirely. How many lines does the base change?
If the answer isn't zero, what you have isn't a container. It's a shell around application A.
sponge's editor is still a planning document.
The canvas is blocked by a gate I wrote myself. The line in the plan: without a content model for slices first, the canvas would only be acting on stub data. I wrote that almost a month ago. The gate is still shut.
I don't have a real implementation yet.
But it's the first time I've seen what it looks like.
(GMT+8 21:47 · 2026-09-14)
← article