Write the spec before the code

Every design decision in tcrga — the Python version, the optimisation library, what the algorithm is even trying to maximise — was written down and reasoned about in README.md and PLAN.md before a single line of the package existed. This page explains why that order mattered, especially when the person writing the code some of the time is an AI coding assistant.

What "spec-first" meant here

Before implementation began, this project answered, in writing:

None of this required code. All of it removed ambiguity that would otherwise have surfaced mid-implementation, at a point where undoing a wrong assumption is expensive.

Why this matters more, not less, with an AI coding assistant

A written spec is the artefact that both the person and the model can point at. When a prompt says "add the crossover operator," the model still has to guess what the chromosome looks like, what invariants it must preserve, and what "correct" means for this project — unless that is already written down. With a spec:

Concrete case: checking a dependency before designing around it

The plan for Phase 0 explicitly set out to retire a risk before writing any scorer code: probe whether epytope's TCR-specificity extension — a unified interface to a couple of dozen pre-trained TCR binding predictors — was actually installable and usable. It looked, on paper, like it could shortcut a large chunk of the scoring work.

The probe found that it could not, in the form hoped for. The findings are recorded in full in docs/predictors.md; in short:

Finding this out during planning meant the project could design Phase 3 around torch-free, self-contained scorers (TCRdist distance, positional weight matrices, a gradient-boosted classifier) as the foundation, not a fallback — and treat any future neural predictor integration as a batched, cascade-style addition rather than a per-individual subprocess call. Discovering this mid-implementation, after scorer code had already been written around the wrong assumption, would have cost real time — plausibly days, once you account for the false starts.

A related example lives in docs/environment.md: this project is developed on an Intel Mac, and several dependencies (biopython, numba, llvmlite, torch) have dropped macOS x86_64 wheel support in their latest releases, failing to build from source against the machine's outdated compiler toolchain. That was diagnosed and pinned around — with the exact package versions and reasoning recorded — before it could quietly break an installation step later.

Concrete case: the real dataset is much smaller than the raw export

McPAS-TCR's export lists 40,731 rows, and it would be easy to plan a scorer around a figure that size. Actually loading and cleaning the data told a different story:

None of this changes what the project builds, but it changes what any run of it can honestly claim: a scorer trained against this data has real supervision for a few dozen peptides, not tens of thousands of rows' worth of coverage. Measuring that before writing scorer code, rather than after training something on an assumed-large dataset, is the same habit as the two cases above: check what you actually have before designing around what you hoped you had.

Concrete case: the optimiser cheats if you let it

The very first prototype run of the GA used a deliberately naive placeholder scoring function — something quick to wire up just to prove the engine ran end-to-end. That placeholder rewarded sequences containing more tryptophan (one of the twenty amino acids). It seemed harmless: a simple, fast, obviously-not-final stand-in for a real binding predictor.

Within 25 generations, the algorithm found CKYIWWWWWAVAWHMF — a sequence that is mostly tryptophan repeated — and scored it at a perfect 1.000. This is not a plausible T-cell receptor sequence by any biological measure. It is what happens when you give a search process a score to maximise and the score has a loophole: the algorithm does not know or care what the score is supposed to mean, it only climbs whatever number you hand it. If the shortest path to a high score runs through nonsense, that is the path it takes.

This is worth stating plainly for anyone new to optimisation: an optimiser exploits whatever you actually give it, not what you meant to give it. Any time a scoring function has an unintended shortcut, a sufficiently thorough search will find it. This is sometimes called "reward hacking" or "objective exploitation," and it is a general hazard of automated optimisation, not something specific to this codebase or to genetic algorithms.

The response was to turn the failure into a permanent safeguard rather than just fix the placeholder and move on: that exact scenario — a scorer trivially exploitable by a degenerate sequence — is kept as a regression test. Any future scorer, including real trained ones, is checked against it. That is one of the concrete reasons this project's fitness design (see the landing page) treats plausibility and off-target binding as hard constraints rather than optional extra terms in a weighted sum: a weighted sum can still be gamed by sacrificing one term for another, while a constraint that must be satisfied cannot be traded away.

The takeaway

None of this required exotic process. It required writing down, before coding, what the system should do, what it should never do, and why — then treating a bad result during development as information to encode permanently, rather than a bug to quietly patch. Both habits are just as valuable, arguably more valuable, when part of the implementation work is delegated to an AI coding assistant: the spec is the shared reference that keeps the human and the model pointed at the same target.

See the prompts page for the actual, verbatim prompts that put this into practice on this project.