Why I built it
ThroughSpec came out of a pattern I kept repeating on my own projects. I write requirements carefully at the start. Then I implement in passes, the code moves, and six weeks later the document describes a system that no longer exists. Nobody notices until a change lands on an assumption that stopped being true.
The usual fix is discipline: remember to update the doc. That never works. The least urgent task always loses to the most urgent one. ThroughSpec is the other bet. Make the spec an input to the development loop, not a wiki page beside it, so staying aligned is easier than drifting.
Specs are living contracts, not static documents.
The second problem, which turned out to be the same problem
I was building this inside AI-assisted loops, where drift has a price you can see. Every ambiguous requirement gets re-explained, and every re-explanation is context you pay for again. Vague intent and token waste are the same failure wearing two hats. A canonical, minimal, high-signal spec fixes both at once.
The problem, stated precisely
Drift is not one event. It piles up from three sources that look harmless alone:
- Hidden assumptions. A decision gets made in the editor, never written down, and becomes load-bearing.
- Rushed fixes. A patch changes behaviour that some other part of the system quietly depended on.
- Undocumented behaviour changes. The code is now correct about something the spec never said.
Tests will not catch those. Tests encode the behaviour you already thought of. You catch them by comparing the implementation back against intent, which only works if intent is written down specifically enough to compare against.
How the system is shaped
The app is TypeScript-led and split into three layers with explicit boundaries. Spec parsing, validation, and the actions downstream of them can evolve without dragging each other around.
Authoring surface (UI layer)
Where intent gets captured in structured form rather than as prose in a wiki.
HTML carries 21.3% of the codebase: UI structure plus the documentation surface.
Service and domain layer (TypeScript, 52.2%)
The point where a spec stops being a document and becomes something the workflow can act on.
Lifecycle checkpoints compare implemented behaviour against the spec statement it maps to.
A drift-prevention tool should not be built in a language that lets its own interfaces drift.
Tooling and memory
Helper scripts and analysis pipelines - chosen where scripting ergonomics beat TypeScript's.
Canonical, minimal, high-signal. Chronology is preserved rather than rewritten in place.
Why a decision was made, kept separately from what the decision was.
- UI layer
- Domain logic
- Engineering memory
- Supporting tooling
Explicit boundaries so spec parsing, validation, and downstream actions can evolve independently.
Authoring surface
The UI layer for writing and reviewing specs and seeing current project state. Intent gets captured in a structured form instead of prose in a wiki.
Service and domain layer
Spec interpretation and the validation flow. This is where a spec stops being text and becomes something the workflow can act on.
Tooling layer
Helpers, analysis, and automation support, including Python where scripting ergonomics beat TypeScript's.
Engineering memory
An append-only session knowledge base in claude/context.md and a teaching
journal in claude/learnings.md. Chronology is preserved rather than rewritten.
- TypeScript
- Python
- Next.js
- MCP
- JavaScript
The stack, by weight
| Layer | Choice | What it carries |
|---|---|---|
| Primary language | TypeScript (52.2%) | System behaviour, all typed interfaces and module boundaries |
| Markup and UI | HTML (21.3%) | UI structure and documentation views |
| Supplementary scripting | JavaScript (12.9%) | Runtime and compatibility utilities |
| Tooling and analysis | Python (12.4%) | Helper scripts and analysis pipelines |
| Styling | CSS (1.2%) | Presentation layer |
The split is deliberate. TypeScript carries anything that is part of the system's contract, because a project about explicitness should enforce its own explicitness at compile time. Python gets the jobs where scripting and analysis ergonomics genuinely win, and none of those jobs are load-bearing for system behaviour.
The loop
Structured enough to guide implementation and verification, not just planning.
The mapping is the traceability link. Without it, 'we implemented the spec' is an assertion.
Compare expected behaviour against actual output at an explicit point, not when someone remembers.
Lets a future change be reasoned about against intent rather than against current code.
What the loop is defending against
Made in the editor, never recorded, then quietly becomes load-bearing.
A patch changes something another part of the system depended on.
Tests do not catch these, because tests encode the behaviour you already thought of.
The checkpoint is what makes drift visible while it is still one change deep instead of forty.
The workflow is small on purpose. A heavyweight process is a process people route around.
Define
Write the spec with enough structure to guide both implementation and verification, not just enough to plan. Ambiguity should die before code exists, because that is the only point where resolving it is cheap.
Implement against a spec statement
Every substantial change maps back to a spec statement or an acceptance rule. That mapping is the traceability link. Without it, "we implemented the spec" is an assertion rather than a fact.
Verify at a checkpoint
Compare expected behaviour against actual output at explicit lifecycle points. The checkpoint makes drift visible while it is still one change deep instead of forty.
Record
Append what was decided and why. The decision log lets a future change be reasoned about against the original intent, not against whatever the code currently happens to do.
What makes this work is that the check is institutionalized rather than remembered. Relying on memory is exactly the failure mode the project exists to remove.
Token-lean context
Rejected: full-history replay
Correct and unaffordable. Re-sending settled context buys nothing and costs every iteration.
Noisy duplication is what makes a spec ambiguous in the first place.
Kept: canonical and append-only
Minimal, high-signal context. The opposite of a wiki that has grown five versions of the truth.
Keeps what I believed at the time and what changed my mind - the part in-place edits destroy.
Affordable precisely because the underlying history still exists to derive from.
The cost I accepted
An append-only log stays navigable only if something summarises it on a cadence.
Bad compression drops the reason for a decision while keeping the decision itself.
- Canonical record
- Derived
- Known cost
- Rejected
Ambiguous intent and token waste are the same failure wearing two hats, so one canonical record fixes both.
The append-only engineering memory is what makes AI-assisted iteration affordable. Getting it right needed three rules:
- Keep canonical context minimal. One authoritative place per fact, not five narratives that mostly agree.
- Never re-send the same payload. Repeated prompt and context payloads are the single largest source of waste in an assisted loop, and they buy nothing.
- Prefer derived summaries over full-history replay, where doing so is safe. Replaying everything is correct and unaffordable. Summarising is affordable and needs judgment about what can be dropped.
The failure mode of compression
Aggressive compression hides nuance if it is done badly, and the nuance it hides is usually the reason a decision was made rather than the decision itself. That is the specific risk I am trading against, and it is why the record is append-only: summaries are derived from a history that still exists, not a replacement for it.
Append-only has its own cost. Chronology and auditability come free, but the log grows monotonically, so it needs periodic summarization to stay navigable. I accepted that because the alternative (editing history in place) loses exactly the information that makes the log worth keeping: what I believed at the time, and what changed my mind.
Design rationale
How do you keep implementation aligned with requirements over time? By making specs a first-class input to the development loop instead of a reference document. Every substantial code change maps back to a spec statement or an acceptance rule, and the append-only context plus the explicit learnings log mean I can audit why a decision was made rather than reconstructing it. That cuts down hidden assumptions and makes regressions traceable, because there is a recorded intent to compare a regression against. The important part is that the check is institutionalized rather than remembered.
Why a TypeScript-heavy architecture for this specific problem? Because the philosophy and the type system point the same direction. Static typing gives strong contracts, clearer refactors, and safer module boundaries, which is exactly what a system about explicitness and drift prevention needs from its own implementation. It would be incoherent to build a drift-prevention tool in a language that lets interfaces drift silently. Python stays in the project where scripting and analysis ergonomics are genuinely better, but the primary system behaviour lives in typed interfaces.
What tradeoff did you consciously accept? More upfront structure in exchange for long-term consistency. The system feels stricter than an ad-hoc workflow, and that strictness is the mechanism, not a side effect. Stricter upfront modelling costs real early effort and removes downstream ambiguity. The honest cost is that extra validation steps slow quick experiments, which is a bad trade for a throwaway prototype and a good one for anything that has to survive its own second month.
Why append-only rather than a maintained document? Because a maintained document is a document that quietly loses its own history. Append-only logs preserve chronology and prevent accidental history rewrites, which is what makes them useful for debugging a decision and for explaining how the architecture evolved. The tradeoff is navigability, and the answer to navigability is derived summaries, which is affordable precisely because the underlying record is intact.
A closing note
The framing I settled on is "drift-proof by design": detect ambiguity early, tighten interfaces, and store only what is necessary, append-only wherever historical traceability matters. It is a small idea and it is easy to state, which is exactly why it is worth building a system around. Projects almost never drift because anyone disagreed with the principle. They drift because nothing in the workflow made following it easier than not.
The project is open source at github.com/VishalPatil18/throughspec.
