We’ve all tried to architect complex systems in a chat window or a scratch repo. It works… until it doesn’t. Context gets lost, “final” decisions drift, and your first runnable thing is already coupled to a framework choice you made on a Tuesday night.
ProtoSpec is my antidote: a tiny language + a lightweight web IDE that lets me design in prose and pseudocode, generate runnable skeletons on demand, and keep the spec itself as the single source of truth. It’s simple on purpose.
Here’s what you get and why it matters.
The pain ProtoSpec fixes
- Design by vibes: Ideas get buried in threads. Later, no one remembers the trade-offs.
- Premature coupling: You commit to a stack before you’ve nailed the seams.
- LLM thrash: Long chats blow context. Copy/paste becomes the “API”.
- Spec rot: Diagrams and docs drift from the code (or never existed).
ProtoSpec turns that chaos into a loop you can run all day: spec → preview → generate → test → repeat.
What is ProtoSpec (in one screen)
- A human-friendly proto language (“ProtoScript”) that reads like clean pseudocode.
- A web IDE that stores specs locally, visualizes flows, and generates runnable projects.
- A versioned canon: grammar, AST, and mapping tables are SemVer’d and baked into a Help panel.
- A pluggable assistant (ChatGPT or Ollama) scoped to your current spec—no more context soup.
A tiny ProtoScript snippet:
record Order { id, items[], total=0, status="new" }
map<string, Order> orders
http POST /orders -> create_order
http GET /health -> health
fn create_order(req): {
let o = { id: req.id, items: req.items, total: sum(req.items), status:"new" }
orders[o.id] = o
emit order.created { id:o.id, total:o.total }
return o
}
fn health(req): { return { ok:true } }
Click Generate and you get a TypeScript/Express app with handlers, routes, a tiny event bus, and tests.
Why this beats starting in a framework
1) Boundaries first, code second
You model records, maps, and events up front. HTTP, queue, and cron bindings are declarations—not commitments.
2) One spec → many targets
The emitter maps ProtoScript to Node/TS today (PHP or game/3D targets are easy to add). Your spec survives platform changes.
3) Always-runnable scaffolds
Every iteration can end in a runnable artifact. That kills analysis paralysis and flushes edge cases early.
4) LLMs that actually help
The IDE can send just the relevant slice of the spec to your assistant (ChatGPT or local Ollama). Short prompts. Better answers.
5) Versioned language, predictable upgrades
Grammar and mappings live under /canon
. The IDE warns when your specVersion
drifts and links you to the exact rule or changelog. If you need it, a compat flag keeps old projects unblocked while you migrate.
6) Specs that double as tests
“Examples” go straight to test stubs. Your spec isn’t a PDF; it’s executable intent.
How the loop feels in practice
- Sketch the shape
Write the problem, context, and a few examples. Declare yourrecord
s,map
s, and IO bindings. - Add just enough behavior
Usefn
,emit
,expect
. Keep bodies short. Push accidental complexity into later passes. - Generate
Get a runnable project with routes and handler stubs; run smoke tests; wire real integrations only when needed. - Refine
Tweak the spec. The emitter re-generates, preserving any custom code behind clearly marked regions. - Ship
When the skeleton is stable, deepen the implementation. The spec continues to serve as your contract.
Show me the benefits (the blunt version)
- Speed: Faster to a running thing with the right boundaries.
- Clarity: The contract is explicit and short. No “hidden” decisions.
- Swapability: Change frameworks without rewriting the spec.
- Onboarding: New devs can read one file and understand the system.
- Testability: Examples compile into tests; events/handlers are first-class.
- LLM leverage: Assistants can work against a stable schema instead of noise.
- Governance: SemVer’d language + Help panel = fewer “what does this mean?” pings.
A tiny example, end to end
Spec (abridged):
record User { id, email, tags[]=[] }
map<string, User> users
http POST /users -> create_user
fn create_user(req): {
let u = { id: req.id, email: req.email, tags: [] }
users[u.id] = u
emit user.created { id:u.id, email:u.email }
return u
}
Generate → Run:
npm run dev
# POST /users {"id":"1","email":"[email protected]"} -> 201 {"id":"1","email":"[email protected]","tags":[]}
# event: user.created emitted
You didn’t pick a DB, auth, or queue yet. That’s the point. Boundaries first.
What ProtoSpec is not
- Not a framework. It won’t pick your ORM or queue. It declares seams.
- Not a silver bullet. If your spec lies, your code will too—faster.
- Not a lock-in. Specs are plain text. Generators are replaceable.
Migration & versioning (without the drama)
We keep a canonical spec registry under /canon
. The IDE:
- reads your
specVersion
, - warns on mismatch,
- offers a compat pack toggle (so old specs still run),
- links you to the exact grammar rule or mapping that changed.
You can upgrade when you’re ready, not when the tool says so.
How to start
- Write a problem section in plain English.
- Add records and maps to capture state.
- Bind HTTP (and later queue/cron) to handlers.
- Add 1–2 examples as lightweight tests.
- Click Generate. Run it. Iterate.
If you need a nudge, the IDE’s Help panel has the grammar, AST typings, mapping tables, and changelog—all versioned.
Final take
ProtoSpec keeps me honest. I still move fast—but I don’t skip the part where we decide what the system is. And when I want help, my assistant isn’t guessing; it’s reading the spec I’m reading.
If you’ve been feeling that your codebase is designing you instead of the other way around, try a week with ProtoSpec. Worst case, you’ll ship a better skeleton. Best case, you’ll ship the right thing on purpose.