Vibe Coding at 438,000 Lines: How I Keep the Project From Drifting
A StratThread case study on running five applications, eleven shared packages, and 174 database migrations through specifications, explicit state authority, verification, and release gates.
I recently asked Codex to measure the StratThread repository.
The result made “this has become a large project” much more concrete. The current project contains 3,574 tracked files, about 290,000 lines of production code, and another 148,000 lines of tests. It spans five independently runnable applications, eleven shared packages, 174 database migrations, and 56 permanent specifications.
Those numbers do not prove quality. They do not prove that every line is necessary. They do show that the project is far beyond the point where one person can safely hold its rules in memory.
I also do not review most of that code line by line. AI handles much of the implementation, refactoring, and test work. My attention goes to product decisions, specification conflicts, verification evidence, and what actually happened during a release.
The interesting question is therefore not whether AI can generate 438,000 lines. It can. The useful question is: when a person stops reading every line, what prevents a locally plausible change from quietly rewriting the whole system?
Not reading code does not mean nobody reads it
In this workflow, the product owner does not use line-by-line review as the main control surface.
The coding Agent still has to inspect the implementation, call sites, consumers, state ownership, tests, and retained decisions. Machines still run type checks, static checks, tests, builds, and release acceptance.
The human answers a different class of questions:
- What observable result should the user get?
- Which business facts must never be rewritten?
- What should happen on failure, conflict, cancellation, or an unknown outcome?
- Which compatibility cost is acceptable?
- Which risk is worth taking now?
If the Agent also refuses to read the existing system and simply lays a new request over it, that is not specification-driven development. It is rearranging an occupied room with the lights off.
What is inside 438,000 lines
This snapshot reads files currently tracked by Git. It excludes dependencies, build output, and untracked files. The counts are physical lines, so comments and blank lines are included.
| Dimension | Current size |
|---|---|
| Git-tracked files | 3,574 |
| Production code | about 289,800 lines |
| Test code | about 147,800 lines |
| TypeScript / TSX | about 365,800 lines |
| Go | about 34,200 lines |
| SQL | about 19,100 lines |
| Rust | about 180 lines |
| Test files | 918 |
| Database migrations | 174 |
| Permanent specifications | 56 files, about 37,600 lines |
Test code is roughly 51% of the production-code line count. That does not mean 51% coverage. It means proving retained behaviour has become a first-class part of the repository rather than a couple of assertions added at the end.
Rust is tiny by line count and still deserves its own row. The Worker loads this native kernel through Node-API for the dense inner loop of A-share chip-distribution replay: price-bin decay, cost remapping, and triangular-distribution accumulation. TypeScript retains input validation, business state, cancellation, paging, and result summaries. Rust owns only the measured compute hotspot.
In seven measured runs on an Apple M4 after two excluded warm-ups, the median complete 4,096-day replay fell from about 1,724 milliseconds on the TypeScript path to about 98 milliseconds through Rust, or roughly 17.5×. That result applies only to chip replay; it does not claim the whole Worker or product became 17.5× faster. Both paths also had to produce exactly equal results.
The kernel is shipped as five hash-verified .node artifacts: macOS arm64, plus Linux x64 and arm64 for both glibc and musl. The roughly 380 MB local Cargo target directory is ignored by Git, while compiled binaries are not counted as text lines. Rust can therefore be almost invisible in a language summary while remaining architecturally important.
StratThread supports a quantitative research and execution loop. A user can discover a market, form a research view, create a strategy, freeze an immutable version, backtest it, run it on paper, and then carefully choose live execution or notifications. The same product also contains an Agent, realtime data, documents, subscription billing, and a separate operations surface.
The difficult part is not merely the number of files. When a browser, a background Worker, a realtime connection, and several datastores all contribute to one result, one of them still needs the final word.
The important part of the architecture is ownership
The diagram combines the primary runtime boundaries, Worker roles, external dependencies, shared packages, state authority, and durable delivery order. Open it to inspect the full-size labels.
StratThread has five primary application boundaries:
| Application | Primary responsibility |
|---|---|
| Web | Astro server rendering, React interaction, and authenticated Actions |
| Realtime | Market observations, progress, invalidation, and notification hints |
| Worker | Backtests, Agent runs, screening, paper trading, live trading, payments, and a Rust Node-API compute hotspot |
| Go Backend | Market ingestion, operations, and controlled custody work |
| Native Wrapper | Optional Electron capabilities around the same Web product |
Every application is replaceable. Restarting an instance must not erase a strategy, task, result, account, or audit record.
Durable business facts belong in PostgreSQL. Queues, leases, sessions, and expiring caches belong in Redis. Confirmed market history belongs in ClickHouse. Images and large objects belong in object storage. A realtime event may tell the page that a result changed; it is never the only copy of the result.
That rule matters more than the framework list. Generated code often reaches for a module-level map, a local file, or a browser singleton because it solves the function in front of it. Once the specification says that any instance may disappear, those shortcuts are invalid before they are written.
A specification is a decision interface, not a product summary
A specification that can safely drive implementation needs to answer at least six kinds of questions:
| Specification content | Failure it prevents |
|---|---|
| Observable user outcome | Shipping a button or endpoint without completing the use case |
| Domain object and stable identity | The same fact acquiring different names and rules at different entry points |
| Authoritative state and retention | Browser, cache, and database all claiming to be correct |
| Allowed and forbidden transitions | Generated code skipping approval, version, or permission boundaries |
| Failure, conflict, and unknown outcomes | Empty data, stale data, or fake success hiding a real fault |
| Reproducible acceptance | Completion being judged by a screenshot or “tests passed” |
“Add a backtest button” is not enough. An implementable contract says who may submit, which exact strategy version is frozen, what is committed before delivery, whether a duplicate produces another run, where a restarted Worker resumes, which evidence survives failure, and which record the page reads at the end.
At that level, the Agent does not have to invent product policy. The owner does not have to search the implementation for hidden decisions.
Every change needs admission before implementation
I use three delivery lanes:
- Fast: maintenance, repair, or refactoring that preserves an explicit user contract.
- Standard: new or changed product behaviour, interaction, API, protocol, or durable meaning.
- Critical: migrations, authentication, authorization, secrets, payments, custody, production infrastructure, and irreversible operations.
Standard and Critical changes update the permanent owning specification before runtime code. A Fast change must cite an existing invariant and regression point. If either is missing or ambiguous, the change becomes Standard.
There is another important stop condition. If two current specifications disagree, or a proposal conflicts with retained history, the Agent cannot resolve it by choosing the newest file, the most specific paragraph, or the implementation that is easiest to patch.
It must present the proposed behaviour, conflicting clauses, current implementation, compatibility options, migration cost, rollback, and the exact decision required from the owner. The human resolves that product decision without choosing a function or data structure.
Let the Agent write code, but not certify itself
A StratThread change follows this path:
user intent
-> risk lane
-> permanent specification or cited invariant
-> pending evidence for requirements, verification, and rollback
-> Agent inspection of implementation, call sites, tests, and history
-> smallest coherent change
-> scoped tests, types, lint, and build
-> fixed release candidate
-> full release suite and specialized safety gates
-> immutable deployment artifact
-> production health, smoke, and telemetry evidence
-> retained release record
Verification is where this discipline is easiest to fake.
“Tests passed” is not evidence. A retained result names the command, boundary, actual outcome, and known limitations. A check that did not run is Not run. A required check missing authorization, credentials, recovery evidence, or an external condition is Blocked. Neither status can quietly become a successful release.
Tests also belong at the cheapest layer capable of proving the contract. Pure domain rules get pure tests. Stores and Application Services receive injected fake repositories. Protocol and storage mapping belong in adapter tests. Browsers are reserved for interaction, focus, responsive layout, and accessibility behaviour that truly needs a browser.
When a field-validation bug requires booting the whole platform, the test boundary is too expensive. Slow evidence gets skipped; skipped evidence eventually becomes a production surprise.
Persist asynchronous work before asking a Worker to run it
Backtests, Agent runs, paper trading, and live trading outlive a browser request. Workers restart. Queue delivery repeats.
The acceptance order therefore looks like this:
validate
-> commit the accepted business record and outbox
-> deliver a stable ID
-> Worker reloads frozen input from authoritative storage
-> commit running checkpoints
-> commit terminal success or failure evidence
-> send realtime or push hints last
Delivering first and persisting second creates a dangerous gap. If the service stops between those actions, the queue owns a message whose task does not exist.
Queue processing assumes at-least-once delivery. The same message arriving twice must reconcile one business fact rather than create two results. A Redis lease coordinates who may work now; a PostgreSQL checkpoint explains where the work can resume.
Stateless does not mean having no state. It means returning state to its proper owner.
The honest guarantee is that failure cannot stay green
Software cannot promise never to fail. Dependencies time out, networks partition, and both people and models misunderstand requirements.
A more useful target is a set of verifiable properties:
- Generated code that violates types, protocols, or known invariants fails before release.
- A PostgreSQL, Redis, or external outage cannot become an empty success response.
- Losing any application instance cannot erase an authoritative business fact.
- Missing a realtime event is recoverable through an authoritative read.
- A migration without exact targeting, compatibility, and recovery evidence is blocked.
- An unhealthy rollout stops, retains evidence, and restores the accepted workload.
I do not require the Agent to be right on its first attempt. I require the engineering system to reject an outcome that has not been shown to be right.
The prompt I use for a change
This is more useful than asking an Agent to “finish the feature and make sure nothing breaks”:
Follow the repository AGENTS.md and current specifications.
First inspect the owning specification, relevant history, callers, consumers,
state ownership, and existing tests. Classify the change as Fast, Standard,
or Critical.
If the current contract is missing or conflicting, or the request changes
security, authorization, payments, schema, state authority, or failure
semantics, stop implementation and present the product decision I must make.
For Standard and Critical work, update the permanent specification and
observable acceptance points before implementing the smallest coherent change.
Preserve all unrelated work.
Run the scoped tests, type checks, lint, and build. Record actual results,
untested surfaces, compatibility, rollback, and index impact. Do not claim
completion or publish while a required check is failing or blocked.
The prompt is not a safety system by itself. It needs real specifications, executable checks, retained evidence, and release permissions behind it. Strict prose with a “deploy anyway” button is still hope with better formatting.
Where human attention goes when it leaves the diff
As the repository grows, I spend more time on questions like these:
- Did a capability enter the right domain, or did one large module absorb another responsibility?
- Does every state have exactly one final authority?
- What happens after an instance restart, duplicate delivery, or late response?
- Does a test remember product behaviour or an accidental shape of generated code?
- Was the evidence produced from the exact release candidate?
- After rollback, which business facts must remain?
Line-by-line review once provided much of the confidence. At 438,000 lines, it is neither realistic nor necessarily effective. Reading one file does not let one person remember the constraints between five applications, several datastores, and more than a hundred migrations.
The more sustainable path is to put human decisions into specifications, turn those specifications into executable gates, and let production evidence close the loop.
A person can stop reading the code. The system cannot simply trust whoever generated it.
I packaged this workflow as an installable Codex Skill: download the stable-vibe-coding Skill. It does not choose the product direction. It makes each change answer ownership, state, failure, verification, and rollback before it can call itself complete.