Project / what was built, when, and why

Session log

The dated record of what each working session built, what it measured, and what it deliberately did not build. It was CLAUDE.md’s “status snapshot” until it grew to 2 900 lines and crowded out the standing rules around it. Newest first. The reasoning captured here is the point — most entries explain a decision that a later session would otherwise re-litigate — but nothing on this page is authoritative about current behaviour. For that, follow the page each entry names.

2026-09-13 — the night's findings turned into a locus, and a shove that lost to a pathfinder

The overnight watch built the previous evening produced its first readings, and almost everything below came out of them rather than out of anybody's judgement. That is the point of the thing and it is worth saying once: three of the four changes in this entry were prompted by a measurement, and the fourth was prompted by a measurement being impossible.

Owner-verified in-game, 2026-09-13

The dryad became nine alleles

The night proved the gene works — a sapling at 00:00:03, six logs and fifty-two leaves by 00:01:09, the first dryad tree in the mod's history. It also proved a sixth of its output was permanent litter: the translator picked a species from pos.hashCode() and one of the six was dark oak, which vanilla grows only from a 2 × 2. A gene planting one sapling at a time in a random spot will never make a square. A dark oak planted at 01:28 was still a sapling at 02:04 while its oak neighbour had been a tree since 00:01, and one position logged ten failed growth attempts in forty-five minutes.

The owner's fix was the better one: put the species on the allele, so a dark-oak horse plants only dark oak and its saplings accumulate, and let the translator cluster them toward each other so the square completes on its own. Then the locus may as well carry the rest of what a dryad could plausibly be — mushrooms, flowers, and bone meal — which makes it nine variants plus the wild type.

Incomplete dominance, and why halving is the whole design

The rule asked for: recessive to the wild type as before, but two different variants both express. The question that decides whether that is a design or an exploit is what “both” means, and the answer is each at half rate. If a mismatch planted two things at full speed, every breeder would keep their lines deliberately mixed and a matched pair would be strictly worse than the thing you get by accident — at a locus whose most interesting allele requires a matched pair. Halving makes it a real choice: specialise for one thing reliably, or mix for variety at the same total pace.

It needed no new machinery, which is the tell that the model was already shaped for it. The interval is epigenetic and lives on the copy, so a heterozygote reads copy(0) and copy(1) separately and doubles each — and GeneEpigenetics.copy(int)'s own javadoc already said it existed for “a codominant gene, where both copies contribute at once”.

One guard for a whole family of bugs

Adding mushrooms and flowers would have reproduced the dark oak bug twice over: a mushroom in daylight pops off next tick, a flower on the wrong ground never exists, and both look exactly like a broken gene. Every planting word now answers BlockState.canSurvive before the block is written — one call, the game's own rule, no light table duplicated in this repo. Note what it does not cover: a dark oak can survive anywhere an oak can, it just cannot grow. That is why the clustering is a separate mechanism, and the distinction is worth keeping straight.

Bone meal, refused in 2026 and granted the same year

This gene's javadoc carried a refusal: “a horse which auto-farms every crop you own, which is an economy lever nobody asked for and very hard to walk back once players have it… recorded here so it is not quietly improved later.” The owner asked for the allele. The note did precisely its job - it made the reversal deliberate rather than silent, which is all a note like that can ever do.

And the objection was answered rather than overruled: the cover refuses crops. What is left is a horse that hurries a wood along, which is the flavour the allele was wanted for and none of the economy the old note feared. It also rolls the block's own isBonemealSuccess, so it is no faster than a player with a stack.

Intimidating: the first change measured against a baseline collected before it

The night said the gene does not do what it claims: six cows placed at three blocks, sixty-four readings, median nearest 4.2 blocks and 63 of 64 inside the eight the gene promises. But it also ruled out the boring explanation — the first reading moved them from 3.0 to 5.6 within a minute, so the aura was firing all along.

The mechanism was wrong, not the wiring. repel was a velocity shove once a second, and a shove loses to a pathfinder: the cow's own navigation walks it straight back, because nothing ever told the cow's AI anything. It issues a path now, toward the far edge of the radius — which is the mechanism this same file had already chosen for the mirror-image case, and followHorse's comment already contained the argument for it. The shove is kept at 0.2, not as the mechanism but as the reading: it is what makes the moment look like being driven off rather than like a cow wandering away.

What makes this one different from every other behaviour change in the project is that there is a number to check it against, collected before the change was made, by a test that runs itself. That has not been true here before.

And one test that could never have worked

The holy ward produced zero spawns in three hours. BaseSpawner.serverTick opens with isNearPlayer and requiredPlayerRange is sixteen blocks, so a vanilla spawner only runs while somebody stands next to it — the one thing an overnight run guarantees nobody is doing. No code changed: the answer is to leave the character in the spawner room, since every other pen is force-loaded and does not care where the player is. Worth recording that §0-CW's control line is what caught it. Without “if there are no monster spawned lines anywhere, stop there”, three hours of no hostile spawns beside a warded horse reads as a triumphant pass.

2026-09-12 — the yard learns to work a night shift, and the log learns to take readings

Two asks, an hour apart, that turned out to be one piece of work. First: “clear the completed tests out of the horse dimension, and add in new areas of pre-tamed horses in their stalls to test anything open”. Then, while that was being built: “please put in a bunch of event listeners … to help you detect what happened from the log. I'm going to leave the game and the horse dimension running over night. Add as many time-reliant tests as possible, focusing on those that don't require direct intervention from me.”

The second one is a different design brief from the one the yard was built to, and it rules out more than it lets in. A pen earns its place now only if its test starts itself, runs on a clock and leaves something readable in the morning. The first two the yard already did. The third did not exist anywhere in the repo.

Why ActionTrace could not do this job

It logs what a person does — a right-click, a mount, a foal — and that is the right design for reconstructing a play session. Every overnight test is the opposite of it: a sapling that appeared, snow that stopped melting, eggs that piled up and then stopped, a population that did or did not plateau. None of those is an event anybody fires. They are differences between two readings, and nothing in the repo took readings. So DebugWorldWatch: each pen registers itself as a named box with the blocks worth counting in it, a scan every ten seconds logs only what changed, and a census every two minutes prints everything whether it moved or not — plus the entity count and the real milliseconds per tick, which is the safety net under the whole thing, because an entity storm is the one failure here that damages a save rather than looking wrong.

The event hooks she asked for sit underneath that and answer the question a delta cannot: why. The best of them is the pair on items. A count says the egg total went from five to eight; the hooks say when each was laid and, crucially, the age of every item that left — because a 6 000-tick despawn and a cap refusing to drop look identical in a count and opposite in a log. Sounds are counted rather than logged: five horses meowing on a 300-tick timer is seven thousand lines a night, which would drown the four that matter. And the hooks throttle themselves at 400 lines a minute, on the principle that the tool built to find a runaway must not be the thing that makes it worse.

The force-load, which is the difference between a test and a wasted night

Minecraft only ticks what is near a player. The yard is 110 blocks deep and its far end is out of range of its near end, so any place to stand leaves part of it asleep — and in the morning that reads exactly like the gene at that end being broken, which is the same trap that cost most of two sessions already (a gene that does nothing and a gene forbidden from doing anything are indistinguishable from inside the game). So entering forces the yard's chunks and leaving releases them, with a sweep at server start for any a crash left behind.

What went in, and what came out

Out: the spontaneous-breeding field and the pack leader's pen, by her call — “we're done with those tests”. In: egg layer (four in one pen, which is the only arrangement that can make the cap fire at all — gap 207), a sound herd of three meowing and two singers, and intimidating with six cows ringed round it. That last one is the design note worth keeping: a repel aura measured by counting penned cows proves nothing, because they cannot leave — four in the far corner and four round its feet are the same number and opposite results. The watch reports the nearest non-horse distance instead.

One question left with the breeding field and is recorded as dropped rather than answered (gap 206): whether LOCAL_CAP actually stops an automatic breeder. The breeding is confirmed; the cap never was.

Three bugs found on the way, none of which anything could have caught

2026-09-12 — a play session that answered five genes, and a test yard that learned to throw things away

The long tail of the previous entry: the owner played it. Five behaviour genes came back confirmed — spontaneous breeding, pack leader, and all three of verdant's alleles — and not one of them had ever been broken. Every one had been unrunnable. The kit only ever handed out mares, so the breeding gene could not have produced a foal on any hotbar it shipped. The dimension deleted every non-horse mob on arrival, so the pack leader's cows and the holy ward's zombies could not exist in the yard built to judge them. And GeneAbilityHandler.spread returned early in that dimension, so the spreading genes were silent no-ops in the one place designed for watching them.

That is a pattern worth naming, because it cost most of two sessions. A gene that does nothing and a gene that is forbidden from doing anything look identical from inside the game, and the instinct on seeing nothing happen is to go and read the gene. The tell in all three cases was in the scaffolding: the kit, the dimension rules, the floor of the pen. So the yard now stocks itself rather than leaning on a hotbar, all three dimension restrictions are off behind gap 204 with RESTORE BEFORE 1.0 at each site, and each growing pen is floored for its own gene — because convert() is a whitelist per cover and grass, famously, only converts bare dirt, which makes the obvious test (a grass gene in a grass field) the one test that can never show anything.

Then the yard threw the answered questions away. Verdant's three pens and the pack leader's are gone. What is left is only the tests that need time rather than attention: the dryad, about a game-day between plantings; a new hot-blooded pen floored with snow and a strip of ice, the ice because it melts to a water source and that is the half of the gene that could flood something; and the breeding field, re-signed from “does it breed” to does it stop. An uncapped automatic breeder is the classic way to kill a server, and no amount of watching answers it — only leaving and counting.

The holy ward got the thing it actually needed, which was arithmetic. Its whole claim is a negative, and a negative cannot be watched: standing in the spawner room counting zombies tells you the spawner works, not whether the ward moved anything, because the ones it stopped are the ones that never appeared. Every hostile spawn is now logged with its distance to the nearest horse whose ward is live — asked of the resolved ability list rather than the genotype, since a carried gene can still be running nothing. A working ward reads as a floor under that column. A broken one reads as spawns at two blocks.

Three bugs out of the same session, all in shipped behaviour rather than scaffolding: spontaneous breeding was putting same-sex pairs in love (hearts over both, never a foal, since the gene was written — and the only visible sign of the gene working is exactly what it looks like when it cannot), the F8 highlight crashed on a horse that had left its team, and pack leader's label promised followers it does not summon. One is still open: horses keep the F8 outline after the highlight is switched off, because it lights them by proximity and clears them by proximity later — and effect timers do not tick in unloaded chunks, so a group left behind holds the outline frozen until somebody walks back. Gap 205, with the fix written down.

2026-09-12 — three quarters of §10, three genes that use it, and one word that had to be neither of the obvious two

Three of the four things left in §10 shipped: erase-down, the expression preview, and glow intensity. The first two turned out to be one feature seen from two ends, which is why they are one commit. What remains of the section is per-region colour ranges.

The dial: naming the knob that means “how much”

The format could always express a gene varying in strength — declare a knob, point every threshold at it — but nothing could find that knob, because “this one is the important one” lived in the author's head. So the creator could not offer a handle for it, and a mask could not default to it. One flag fixes both: "dial": true, at most one per gene, per-horse, never a seed. It changes nothing at paint time.

The word was the hard part, and it was got wrong twice before it was got right. “Expression” is what the owner and the roadmap both call it and it is what a horse person would say — but the format's expressions is already the table of outcomes, so a file would have read "expressions": […] beside "expression": true meaning something else entirely. It was renamed to strength on that reasoning, and strength turned out to be a parameter on every colour op already (how hard it pulls) — found while editing an example gene that had "strength": 75 three lines below the new knob. dial is named after being a handle rather than after what the number means, which is the one reading neither of the others collides with.

pointsMin: a marking that gets smaller, not fainter

A weakly marked horse has a smaller blaze, not a translucent one. Every field mask could say that already (wind a threshold and it covers less); a PATH could not, because a drawing is not a rule — so a drawn shape was the same on every carrier and a gene built from one looked stamped. pointsMin is that shape at the gene's weakest, one point per point, each morphing to its twin as the dial falls.

Morph rather than crossfade, decided before any of it was written. Blending two coverage fields is easier and imposes no authoring constraint, and it is wrong: a hard-edged mark fading into a different hard-edged mark ghosts through the middle rather than shrinking. So the arrays must be the same length, a mismatch is a load error, and the lerp is read through the control points inside the sampler — no second path evaluated, no morphed array allocated, so a PATH with a minimal shape costs what one without costs.

Both shapes are drawn on the one canvas behind a Full / Minimal toggle, with the other as a dashed ghost, because what matters about the minimal shape is how much smaller it is and that is invisible in a second editor elsewhere on the page. Starting one seeds it by shrinking the drawn shape toward its own centre. Every structural edit happens to both arrays, since the loader requires them twinned.

Parity was green about a blend running backwards

With a minimal shape on prismatic's crescent and nothing else, reversing the lerp in the creator's port left check-parity.mjs completely green. A drawing on a flank is a few dozen texels and the fixtures sample four per part; the two sets simply never met. The page had warned about exactly this — the contracts note says to check that a probe case reaches the new thing — and the way it was established was to break the port on purpose and watch the check stay quiet, which is the only way to tell "covered" from "not reached".

There is an answer table now (SpecFixtureTool.pathSection, 101 checks) comparing the morph where it happens, at five points along the dial, straight and curved and filled. The same sabotage now fails it on the first row. Do the sabotage for the next one too: a green parity check is evidence only about what it reaches.

Glow became a level, which reverses an old call on purpose

Emissiveness was one bit per texel, cut at a coverage of 0.5, and GeneSpec said outright that there was no half-lit. That cut was always a stand-in: it existed so a soft mask would not bloom a glow two body units wider than the shape that drew it, and it paid for that with a hard line across a soft edge. The renderer turned out to make the better version free — the emissive pass is BlendFunction.TRANSLUCENT over the coat, read out of RenderPipelines.EYES rather than assumed — so an intensity written as the second texture's alpha is a true dimmer rather than a darker colour.

So CoatOverlay holds a float per texel, a layer's emissive is a Value (true, a number, a $knob, the dial), and the mask's coverage scales it — the author draws the falloff once, in the mask. Two genes over one texel take the brighter rather than summing, since there is nothing above full bright to spend an overlap on. Every glowing gene in the game changed: fifty-odd layers fade at their edges now. §0-CO names the five soft-masked genes to look at and the control group that should be identical.

Particle intensity is deliberately not part of it (owner's call). Particles are the emitter effect, a different system with its own rate and count.

Two bugs found by looking rather than by being told

And then somebody played it, which found four things no test had

The owner ran the client on this machine and worked through the kit. Everything below came out of about twenty minutes of that, and none of it was reachable from the repo: two confirmations, a wrong instruction of mine, a crash, and two tools that do not work.

The features are right. Starburst's emblem “does indeed look like a smooth variety of sizes” - the 45% floor gives a continuum rather than two sizes with a gap, which was the open question and the whole bet of the minimal shape. Lantern shows “different light levels” per horse, Tron has “a glowing diffuse painted along the body for both forms”, gamma, geode and angler read right, and suntouched - the control, which lights whole parts at full alpha and must look untouched - “does indeed look the same as before”. §0-CO is closed on that pair of readings: changed where the masks are soft, identical where the path is old.

“I don't think ember veins has any fall off” - and it should not

The checklist I wrote for §0-CO named five genes to look at and picked them by the wrong property: whether the glow sat behind a field mask rather than a part list. That is not the same question as whether the mask has a soft edge. Measured afterwards, ember veins' lit layers are 0.04 to 0.2 and data rain's are soft bands multiplied by a SPECKLE - hard by construction, so coverage-times-level is level-or-nothing and the change cannot show. The owner found both in a minute.

The rule worth keeping: masks fold by MULTIPLY, so the hardest mask in a layer governs its edge. Geode's glints are a WAVES at 0.09 inside an AXIS at 3.0 and the product is as hard as the 0.09. The softest lit mask in the mod is gamma's SPOTS at 1.8, and gamma is the gene that actually demonstrates the feature. On the format page now, next to the other thing that session taught: a glow has no colour of its own, so Tron's two haloes at an identical 0.25 read differently because one sweeps 300° of hue and the other is flat.

F8 crashed the server, and had been lying about it for four minutes first

Reported as “F8 isn't toggling off, only turning on”, and then, minutes later, “the game just crashed”. One cause. The horse highlight puts herd leads on a scoreboard team and took every nearby horse off it again - but only leads were ever added, and Scoreboard.removePlayerFromTeam(name, team) throws for an entry that is not on that team. It threw on the first ordinary horse it reached.

Fixed by asking the scoreboard first, and by wrapping what the overlay does so that anything else going wrong in there switches the overlay off rather than ending the tick loop. Worth being blunt about: this binding is registered outside the production gate on purpose, because the testers asked for it, so "it is only a dev tool" was never true of it and a released jar could hit this. §0-CQ, with the API asymmetry on api-notes - addPlayerToTeam is forgiving, removePlayerFromTeam is not.

Two tools that do not work, found by using them

Both were fixed the same day. The kit now hands out both sexes wherever a batch breeds, with the rule written into the class so the next author inherits it rather than the bug. And the screen's right column pairs its last two buttons onto one row - six rows instead of seven - which is what pulls the top group off the pinned one at 270px; the designer's copy of those constants was changed with it (hard rule 5). The screen now also says in the log when the two groups collide, because the failure it produces is a button that looks alive, is alive, and cannot be pressed, and that leaves nothing to find. §0-CR.

Then three genes picked it up

A format nothing uses is a format nobody has tested, so the same session put the three features into shipped genes — one each, chosen because the gene was already describing the thing it could not say.

The haloes are flat constants rather than a share of the core, because the format has no arithmetic - "a fifth of the core" is not expressible - and a constant under the dial's own floor keeps the core the brighter of the two on every horse. That is a real limit found by using the feature, and it is the first thing a knob-driven glow wants that it cannot have.

The dial exposed a gene that was already a coin flip

Adding a knob to Starburst made DeadLayerTest fail on "black shards in the spikes", and the obvious reading - the shrunken emblem starved the shards - was wrong. Setting pointsMin equal to points, so the geometry could not have changed, failed identically: what had happened was that the extra knob re-rolled that gene's draw for the one horse the test seeds.

Measuring it settled what was actually true. That layer is a sparse SPOTS field inside a shape a few units across, and over twenty horses it painted nothing on five of them. It had always been a coin flip; the test had only ever seen the one horse, which happened to land. So the fix was the gene, not the dial: 2.8 / 0.5 instead of 3.4 / 0.4 lands on every horse at every size and keeps the density it had at full size. A layer whose existence depends on a seed is one re-roll from dead, and the only thing that finds those is measuring across horses rather than looking at one.

What is not built

2026-09-11 — the two items at the top of the roadmap, both of them

One session, the roadmap's first and second entries. Both were long-standing, both turned out to be smaller than the page implied once the groundwork underneath them had shipped, and both are now waiting on a person rather than on code (§0-CL, §0-CM).

The PATH drawing canvas

A PATH mask's control points are handles on a view of the horse now. Click to add, drag to move, a hollow dot mid-span to insert, right-click or Delete to remove, arrows to nudge; the textarea survives, folded under the numbers, and the two write the same array. It changed no format, no engine and no parity fixture — which is exactly what the entry predicted when it argued for shipping the mask first and the surface after, and is worth recording as a case where sequencing a job that way paid.

Two decisions inside it are the ones a later session might undo without knowing why. The silhouette is the model's own body-space boxes, part by part, because that is precisely the set of (u, v) the mask can reach — a prettier horse drawn by hand would be a drawing of a horse rather than a map of where a point lands. And both axes are drawn at one scale in body units, so the top and front planes letterbox rather than stretch: the alternative fits the box better and makes every circle an ellipse on the animal. The curve is SpecPainter's own Catmull-Rom, reached through two new spec-engine exports rather than a second copy of the maths.

The test found a bug on its first run, and it was in the form layer

Every mask parameter is wrapped in a <label>, which forwards a click anywhere inside it to the first control it contains. The canvas's toolbar sits inside that label, so every click on the horse was also pressing Reverse and the drawing came out backwards one point at a time. It would have been baffling to hit by hand and was obvious from an automated click sequence that printed the array after each step. Controls made of several widgets get a plain block now, not a label.

Verified by driving the real page in headless Chrome — 26 checks covering click placement, drag isolation, insert ordering, deletion, the textarea round-trip, the foal mesh, and one-scale mapping in all three planes. The driver lives in the scratchpad rather than the repo; landing it as a checked-in tool was offered and not taken up, so it is a candidate rather than a gap.

The Java gene API, and an open registry

TwoAlleleGene answers seven of Gene's eight methods from a fluent declaration; AbstractNaturalGene and AbstractMagicalGene leave the subclass restrict() or tint(). The short path is the write-up.

The part worth keeping: AbstractAbilityGene was moved onto the same skeleton. It is where this shape was first noticed, seventeen genes ago, and folding it in is the difference between a generalisation and a second copy of one — it kept its positional constructor so all seventeen call sites were untouched. Mushroom (natural) and healer (magical) were converted as worked examples, and both coat goldens are unmoved, which is what says the conversions preserved behaviour rather than a reading of the diff.

Genes.register takes any Gene now, validates the namespaced key, then freeze()s. The freeze buys no speed — the orderings were already computed once and cached — and the entry that asked for it implied otherwise; what it actually buys is a named failure instead of horses whose loci have all shifted by one segment, which reads as a breeding bug and is not one.

Two things deliberately not built

Also: making-a-gene.html documented geneCarrotHomozygous() as defaulting to false; the code changed to true on the owner's call the day before and the page had not followed. And the designer's wasm was re-baked — common/ moved, so the browser had been running the previous build.

2026-09-11 — a playtester session, and the roadmap stops being fiction

Two halves. The first was a sweep of the roadmap against the code, which found that a large part of it described work already done. The second was a live bug queue from outside playtesters — the first time this mod has had reports from people who did not write it — and most of those turned out to be missing feedback rather than missing behaviour, which is a pattern worth naming.

Three things that were never broken, only silent

“The diet gene doesn't appear to work.” It expresses on about one horse in eight. What it never did was report: healing a horse already at full health is a no-op and the bond award declines silently at max bond or the daily cap, so feeding a healthy, well-bonded horse spent the item, played the sound, showed the hearts and told the player nothing. Indistinguishable from a gene that does nothing. The food is still always eaten — it was never the refusing that was wrong — and the player is told when both halves declined.

“A horse randomly got bigger, and it tracks bond.” Nothing in the mod grows a horse, and the negative is provable: one write to Attributes.SCALE, a setBaseValue so it cannot accumulate, over inputs fixed at birth, guarded to run once. It was a foal reaching adulthood. The bond correlation is a shared cause: feeding awards bond and does not cancel the interaction, so vanilla's feed runs too, and on a baby vanilla's feed is ageUp. Hand-feeding a foal for bond accelerates its growth with the same click. Gap 196.

“An aura of glowing entities round a horse.” MobEffects.GLOWING is applied in exactly one place in the mod — the F8 highlight, which lights every horse within 96 blocks, in a dimension that is wall to wall with pens. The effect carries no particles and no icon and refreshes for as long as it is on, so left on by accident it is indistinguishable from a bug. It times out now and says how to stop itself.

Name tags, and a gate asked on the wrong side

The one report that was a plain defect. HorseInteractionHandler runs on both sides and cancels the name-tag interaction, but only when HorseRecords.hasRealRecord says the horse is ours — and that asks the attachment, which is never synced. Client-side it was always false, so the client never cancelled and did what vanilla does for a tamed horse plus an item: ride it. The rename window opened and the player was put on the horse at the same time. The client does know which horses are ours, in ClientHorseRecordCache, so the question is asked in a Dist.CLIENT subscriber. The stick and clock branches in that same file carry a comment saying to cancel on both sides for exactly this reason.

What the audit found

Part 1 of the roadmap is finished. Of its five “genes drawing the wrong thing”, one was real, one was real and misdiagnosed, one was a decision and two were already built; the attribute verb it called inert has been dispatched for some time, with the class javadoc still saying otherwise. Epigenetic drift — item two of five in “where to start” — is built, written up on breeding, and had only never been deleted. Bareback steering was listed as unbuilt while a playtester was confirming it works. The bond heart particle likewise. On the gaps page, 38 of 183 entries are already marked closed and 41 more need play rather than code, so the real backlog is nearer 97 than 183.

The one that got away

A whole session of green builds over a broken module

Changing CarrotEffect's shape broke web/DesignerApi, and nothing noticed for eleven commits: :web is not a dependency of :neoforge-26.1.2:build, so every build was green, the tests were green, and two commits of common/ shipped with a stale wasm beside them. It surfaced only because the end-of-session routine runs :web:bakeDesignerAssets by hand. Gap 199 - the check that would have caught it does not exist, and gap 45 assumes somebody looked at git status.

Also built

2026-09-11 — three coat defects, and a roadmap that had stopped being true

The session began as “go through the whole roadmap and clear it out”. It turned into something more useful: auditing it against the code first, because a large part of Part 1 turned out to describe work that was already done. Three real defects were fixed on the way, and the page lost two whole entries that had shipped and never been deleted — which is the exact failure its own banner warns about.

What the audit found

Of the five rows under “genes drawing the wrong thing”, one was a real defect, one was a real defect misdiagnosed, one was a design decision, and two were already built. Particle’s carrier case was correct, documented in its own javadoc and asserted by a passing test; light’s glowing hooves are wired end to end — the gold tint and markEmissiveLowerLeg through to EmissiveCoatLayer. Under “behaviour that silently does nothing”, the hurt-mare denial message shipped on 2026-09-10 and was still listed as missing, while sitting in the same page’s own Settled section. Only the inert attribute verb survived the sweep.

The larger find is that epigenetic drift is built — item two of five in “where to start”, whose stated problem was that a foal copies its parent’s seed exactly. EpiDrift nudges every scalar at every breeding on a two-sided exponential, re-rolls seeds and categories on a chance, and clamps to the hard safety bound rather than the design range, so a long-bred line may reach somewhere no wild horse has been. Its own javadoc opens “Before this, inheritance was verbatim.” It shipped without the per-gene opt-in flag the roadmap proposed for the job, and it is written up on breeding, so the write-up had happened and only the deletion was missed. The section is gone, and the Settled entry that still said “verbatim” now says what is true.

Magic zebra is codominant

One Mzeb copy drew the whole pattern, so the locus had nothing between a plain horse and a finished one. It follows natural zebra now: one painter taking a strength, two outcomes, so a dusky parent and its black-striped foal cannot end up wearing different bands. Mzeb/Mzeb passes 1.0 and is byte-for-byte what it always drew; Mzeb/n is a new dusky outcome scaling the subtraction and the opacity together — scaling only the colour would put a dusky band at full opacity over the template, which on a dominant white is a grey stripe as strong as the black one.

The strength is 0.09 because it was picked off a rendered dose ladder, not reasoned about: the first guess of 0.18 read as black on a bay and was not distinguishable from the homozygote. Three base coats by three genotypes in one image settled it in one look, which is the cheapest thing in this repo and is still under-used. 48 Mzeb/n rows of the pipeline golden moved and the 6 Mzeb/Mzeb rows did not.

Particle stops lying about what a horse carries

The owner reopened this one deliberately. Two variant alleles that were not codominant partners used to show the lower-ranked of the pair, which meant a horse trailing dust was either Dst/Dst or Dst over any of the other thirty-nine — identical to look at, and unguessable. The locus asks for agreement now: the same allele twice, or two of one family, and anything else is two carriers and a plain horse. It cost nothing in the wild, because the founder table already excluded exactly these pairs for exactly this reason. The rank rule went with it; declaration order survives only to keep AllelePair canonical so copy(0) and copy(1) address a codominant pair in a fixed order.

The foal sclera — and what it was not

The owner-reported “the coat overwrites the sclera” was real, and both of the roadmap’s suggested causes were wrong: the composer already redraws the eyes after both paint phases, and the coordinates were on the eyes. EYE_RECTS_BABY was 2 texels wide where the eye is 4, on the strength of a comment in the file asserting that the foal template has no bright sclera. It has two columns of one beside each pupil. So every white painter and every magical gene painted over the whites of a foal’s eyes and the redraw put back the black alone.

Worth recording how it was settled, because reasoning about body space has been wrong here before: the fix was checked by dumping the template’s actual pixels and by asking HorseSkinGeometry which columns the head’s faces own, rather than by deriving it. An intermediate claim that the adult right eye was misaligned came from feeding mistyped coordinates to a probe and was withdrawn on reading the source — the adult has been correct since 2026-09-06. Every one of the 528 foal rows in the pipeline golden moved, and not one adult row did, which is what a foal-only fix should look like.

A test that cannot see the bug it is for

CoatBakeGoldenTest stayed green through the whole zebra change, and correctly: it walks every gene but pairs each variant allele with itself (new AllelePair(variant, variant)), so it never bakes a heterozygote and is structurally blind to a codominance defect. That is not a fault in it — it exists to catch a gene whose paint moved — but it is worth knowing which half of the space it covers before trusting a green run to mean a coat did not move.

2026-09-11 — a test kit you can finish, and a mask for liquids

A play session run as a conversation: the owner asked to be set up to test, tested, and reported back, several times over. What the day produced is a test kit that is worked through rather than looked at, three defects found by that testing and fixed, and one new mask in the drawing engine. Nothing below has been seen in a client since the last change; the checklist is the authority on which parts the owner confirmed as they went.

The kit: one hotbar, and a batch is deleted when it is done

Three defects the testing found

Drawings that stand somewhere, and a gene that lied about itself

GOO: the first mask for a liquid

Ooze drip “looks like triangles”, and needed to be “more irregular and curved”. That is not a tuning problem: WAVES is one periodic function, so every lobe is the same length, width and spacing, and at the two or three texels a lobe gets on a barrel a sine is a zigzag. A liquid is not periodic - the whole read of a drip is that one ran further than its neighbour. The owner gave permission to extend the engine for it.

A log line instead of a chore

Checking that a breed carries only the disorders on its sheet meant taming horses one at a time and reading their genes, which the owner declined - reasonably. Every breed founder the server rolls now logs a [breed-health] line of its health genes against its sheet, with a WARN ... NOT ON ITS SHEET for anything the sheet does not list. Two dozen Quarter Horses and two dozen Thoroughbreds later: three HYPP carriers, no warnings, and the answer came out of a grep rather than out of an evening.

Owner-verified in-game

2026-09-11 — every folder in phc/

The owner, after 0.5.000: “change the folder situation to all be in agreement.” Drop-in genes move from config/horsegenetics/genes/ to .minecraft/phc/genes/, beside breeds/ and the three settings files, and nothing of the mod's is in config/ any more (ModGeneSpecs.FOLDER is now ModBreedSpecs.ROOT + "/genes", resolved against the game directory). Both folders' READMEs name the other and the settings. Gap 192 closed.

Shipped as 0.5.001, on the owner's call that a folder move is worth its own release; the notes carry the warning. It was not in the 0.5.000 jar, which is why the warning matters: a gene left in the old folder is not loaded, and a saved horse's segment for it is dropped on load like any unregistered gene's - the player moves the file and restarts before opening the world, or loses that gene from every horse that carried it. The dev run had no drop-in genes, and its stale run/config/horsegenetics* files were deleted (their values had already been copied into run/phc/).

2026-09-11 — 0.5.000: a world decides which breeds it has

The owner's brief for the release: the big thing is custom breeds; before shipping, move the settings somewhere sensible relative to the breeds folder, and let a world set the spawn rate and location of every shipped breed - down to switching all of them off, Feral Mixed included, with a horse that has nothing to be simply not spawning. Commit 94332fb; tagged 0.5.000. Waiting in game: §0-CC.

One folder

breed-spawning.toml

Old worlds

Tests run: BreedSpawnSettingsTest, GenotypeTest, the breed classes, creator parity; :neoforge-26.1.2:build green; a dedicated server booted clean twice. The full :common:test was not run - it would still need to confirm that nothing else reads Breeds in a way the published copies change. The debug world kit was not re-aimed: nothing in §0-CC is an item.

2026-09-11 — a breed is exactly its sheet; the designer draws, asks and explains

The owner's fourteen-point list after walking the new designer, and one principle that fell out of it: now that breeds are designed, magic in a breed is a design decision. Everything is waiting in game.

No stray magic, no background disorders

The designer

The jar

Tests run: the breed classes (founder, plate, parser, breeds, stat curve), all green; the designer driven in headless Chrome. The full :common:test was not run - a full run would still need to confirm nothing outside the breed package rolled a founder expecting stray magic. Gaps 68, 187 and 189 closed.

2026-09-11 — breeds you can drop in, a designer that asks one thing at a time

The owner's brief: the breed designer was overwhelming; break it into steps, keep the options on the left and the horse on the right; let a player make a breed and drop it into a folder with nothing but the jar. And the principle underneath it, now on the philosophy page: a concept is a breed, not a gene - a zombie horse is a zombie breed of ordinary genes (rotted markings, a temper, a low speed score), not one indivisible zombie gene. Everything is waiting in game.

The breed designer, rebuilt

The game side

Tests run: every breed class, both goldens, the gene files, showcase and white-rate - all green. The full :common:test was not run. New gaps 187–189 (188 is still open); gap 83 closed.

2026-09-10 (late) — twenty-four genes out of intake, and what only the renders caught

The whole intake folder: three authoring batches of 7, 7 and 4 genes with a note each, and six more with none. Commits a6d76bd (the batch exactly as it arrived, so the unsimplified traces survive in history) and b3f9e9b (the install). Every gene is waiting in game.

Checked before anything was opened

What had to change on the way in

What the renders showed and the tests did not

All tests were green once the files parsed, apart from twelve dead layers. Then a side-view renderer drew every form of every gene on a bay, and half the batch was wrong in ways no test measures. The renderer was a scratch diagnostic and is not checked in: a hundred lines in the coat.pattern package, making the same calls DeadLayerTest makes (a synthetic bay, SpecPainter.tint for one expression), and plotting every Face.LEFT texel at its body-space (x, y), with seeds and the expression taken from system properties. The icon bake shows one horse per gene from three-quarters; this showed every form, on several horses, from the side, and that difference is the whole of this section. It is worth making a Gradle task.

Every change is written into the gene's own notes, with the reason. DeadLayerTest finished with no new entries. The bake golden gained 84 rows and moved none, and the pipeline golden changed genotype codes only, with no hash moving. Parity and the NeoForge build were green.

Deliberately not done

2026-09-10 (evening) — three play sessions, fixed as they came

The owner played the build three times in one evening and reported as they went; the session was almost entirely answering what they found. Commits 363b3a8 to 759f184.

Owner-verified in game

What was built, and the reason behind each

Lessons

2026-09-10

The eyes stopped being a channel and became thirteen genes

The owner asked for eyes that are “extremely polygenic”, specified locus by locus: iris colour and sclera colour per eye, a heterochromia sector and a colour for it per eye, a glowing iris and a glowing sclera per eye, and a third eye on the forehead that can copy one of the others, mix them, or invent itself. Thirteen loci. Then the harder half: the natural genes that used to paint an iris should stop, and ask the eye loci for one instead.

What made it a design question rather than a port was where the asking lands. The old channel applied a claim at bake time, so a splashed white horse’s blue eyes were paint: real while it had the splash, gone the moment a foal did not inherit it. The owner chose the other answer — the requested allele is written onto the horse when it is made, so a splashed foal genuinely carries MBl/MBl, shows blue eyes, and passes blue eyes on to foals that are not splashed at all. Blue spreads through a herd the way any other allele does. That is a bigger claim than it looks and it is the reason Eyes.force runs in Genome and nowhere else: forcing twice, once with epigenetic midpoints and once with the real numbers, would destroy the horse’s own eye alleles on the first pass and leave the second unable to tell that it had.

The cost that had to be paid for it is on gap 174. The old “this horse is broadly white however it got there” rule measured the finished coat, which is the honest signal and is unavailable at breeding time. It is a declared per-locus sum now, and it will be wrong on horses whose white patterns overlap or avoid each other.

Three real bugs came out of the painting. Two were new and one was old:

And one that had nothing to do with eyes. The weather loci minted allele tokens as T+ and T-, and - is Genotype’s own gene separator — so any horse carrying a “suffers in” allele wrote a genotype code its own parser could not read back. Nothing had caught it because it needs a founder to actually roll one, which is a few percent of a few percent. Adding thirteen loci shifted the founder RNG stream and three unrelated tests went red at once. The suffix is v now.

What was deliberately not done. The eye loci get no wiki/gene-*.html pages: thirteen near-identical pages would be thirteen places for one fact to go stale, and Eye colour is already declared the single source of truth for eyes. The four magical genes that paint eyes directly (dhampir, shadowcreature, light, magic sectoral heterochromia) were left exactly as they were and are now audited on that page — two of them are redundant, and gaps 175 and 176 record the calls the owner has to make. The leopard complex’s white sclera rim was left alone too: it is not an iris colour, and there is no allele meaning “more of the white shows”.

A fourth bug, found by reading a test while waiting for it. CoatTextureComposerTest’s broadest case builds each genotype from a bitmask over Genes.codeOrder() and writes (1L << n) - 1 for “every gene on”. Java masks a shift distance to six bits, so past 64 genes that silently becomes 1L << (n & 63) — at 291 genes, a shift of 35. Three thousand random masks leave every locus from the 36th onward pinned to its last allele, and nothing goes red because the test only asserts that a bake returns a full sheet. It has been wrong since the registry passed 64 genes. Recorded as gap 177 rather than fixed, because the same test is roughly six and a half thousand whole coat bakes and is on its own a large share of the ten-minute suite — widening it without sampling would make that worse.

Every horse in the world changed. Brown is the dominant iris allele, so every horse now has a painted brown iris where it had the template’s flat black. Both goldens moved wholesale, all 203 gene icons were rebaked, and none of it has been seen in game — §0-BU is the checklist, and its first four items are the ones that could be wrong rather than merely unseen.

2026-09-10

Twenty-seven behaviour genes, specified one at a time and then built

The session started as a design conversation about invisible ability genes and finished with the whole family in the mod. It went in that order deliberately, and the order is most of why it worked: every gene was specified, argued over and written up before any of it was built, so building it was transcription rather than invention.

The specification pass

A twenty-nine-gene proposal was walked one gene at a time, in batches of four or five, with the owner ruling on each genuine fork. Two were cut. Moon-sick turned out to be already shipped as the Aga/Aga allele on night temper. Electric cannot be built as described at all, because redstone power comes from blocks and never from entities - the achievable version is pressure plates, which already respond to every horse in vanilla.

Several specifications were wrong in ways only the code could reveal, and finding that out on paper was cheap. A Minecraft sound cannot be started partway through, so singer plays a record’s opening rather than “a section” of it. There is no concept of a “base” anywhere in the mod, so base alarm measures from the horse and stabling becomes the interface. And the mod’s own swimming assist means hydrophobic restores vanilla behaviour rather than adding to it - which flipped it from dominant to recessive, because dominant would have silently changed how every horse already bred behaves.

Each page carries its own hazards

The owner asked for the game-breaking, compatibility and performance risks to be flagged on each gene’s coding tab, and that turned out to be the most useful thing written all session. Five genes can damage a world - spontaneous breeding is uncapped automatic breeding that runs while the player is asleep; holy ward hooks every spawn attempt in the world and can break a mob farm invisibly; spawner can bypass other mods’ protections; egg layer accumulates entities; and the three genes granting a traversal flag to the rider write state onto a player.

Writing those down first meant the implementation answered them by shape rather than by a comment asking the next person to be careful. The ward does not scan for horses at all - the horse tick, which already runs per horse and already holds the ability list, writes its position into a small map and the spawn event walks that. Rider-targeted traversal is re-applied per tick and stores nothing, so there is no state to remove on dismount and it cannot become an invulnerability dupe. Followers are nudged rather than given an injected goal, so a dead horse cannot leave a goal behind on every creature it ever met. And ward entries expire rather than being removed on death, so a missed hook cannot leave a ward suppressing spawns for ever at a position with no horse at it.

What the batch actually cost

The previous estimate on the roadmap - “about one verb per behaviour” - was roughly three times too high. Twenty-six genes needed seven new verbs, two triggers, four condition flags and five widened verbs, because behaviours cluster: four genes wanted sound, five wanted one mob-group filter, three wanted one traversal parameter. Three base classes carry twenty-three of the twenty-six between them, and they were worth more than any individual verb.

Parity earned its keep twice in one session, both times immediately: it caught the creator’s missing condition flags, and then caught MOB_GROUPS being declared below the table that reads it - a JavaScript hoisting bug that produced empty choice lists and would have been invisible in the editor.

Both goldens moved, and neither moved wrongly

None of the twenty-seven paints, and that is measured rather than asserted: all 1056 rows of the pipeline golden kept their rendered-byte hashes and only their genotype keys changed, and the bake golden gained 206 rows with not one existing row moving. That check had to be done keyed by gene and allele - a positional diff claimed 312 rows had moved, which was entirely the artefact of inserting 206 rows into a sorted file.

Three things recorded rather than fudged

A review pass, which found three bugs in the same day's code

Re-reading the finished work before closing turned up a dimension leak in the ward - a horse warding in the Nether would have suppressed Overworld spawns at the same x/z - and two per-entity costs that had been hidden behind comments asserting they were cheap. All three are fixed, and gap 173 records the part worth keeping: every one of them was in code whose hazards had been correctly identified and written down in advance on the gene's own page. Knowing the hazard did not prevent it. Only reading the finished code back against the hazard did, and no check in this repo would have caught any of the three.

The same pass found that hydrophobic only does half of what its page claims - it ejects the rider and does not then head for shore - which is now said on the page rather than discovered in a lake.

Nothing in this batch has been seen in game. §0-BT is the checklist, ordered by what can do damage first.

2026-09-10

Eleven more genes out of intake, none of them duplicates, and the dead-layer check paying for itself on its first batch.

Agatebound, bargello, bracketed, coronal, geode, maelstrom, nacre, starburst, sunspiral, wing margin and witchfire. The batch was checked for duplicates by content rather than by name first: every gene's layer signature — which masks, folded how, painted with what — and the nouns in its blurb, scored against all 159 that already ship. The best match in the whole batch scored 0.13, so nothing was a re-arrival and nothing was skipped.

Two collisions, both resolved rather than skipped

Nacre collided by name with a gene that already ships, and the content check is what settled what to do about it: they score 0.04 against each other. The installed one breaks the coat into hard-edged plates at gold seams; the new one is a marbled saddle with a shifting film along its folds. They have nothing in common but the word, so the new one is Nacre Saddle.

All eleven arrived keyed ixora.* where every other gene in the mod is horsegenetics.*. The format allows a modder their own namespace and should — but a gene shipping inside this mod under a second one breaks every tool that builds a key from a slug, so they were renamed. Recorded in each file's notes rather than done silently.

The check caught nine dead layers before anything shipped

DeadLayerTest was written earlier the same day, and this is the first batch it saw. It failed on nine layers across three genes — every one of which would have shipped, because all eleven files parse, all eleven paint plenty of texels, and ten of the eleven look fine in a render. The nine had three different causes, which is the argument for measuring the symptom rather than pattern-matching the cause:

Diagnosis needed one measurement the check does not make: render each dead layer alone. A layer that paints nothing alone has an empty mask fold; a layer that paints plenty alone and nothing in place has been overruled by its neighbours. Wing margin's veins scored 0 alone, starburst's core 132. That two-way split is worth building into the check's failure message the next time it fires.

And dorsal wing stops clear of the underline

Opening the NORMAL mask to -0.75 at round 0.85 is what carries the butterfly down the flank, but a rounded normal that far open also admits the belly — a downward-facing face reads well above the threshold once it is blended — so the wing ran to the bottom of the barrel. An explicit AXIS on Y now cuts it at y 13.5, about two and a half units above the underline. A height is the right tool for a height: raising the NORMAL instead would have pulled the marking back off the shoulder as well.

What was regenerated

Bundle, icons, 168 gene pages, the designer wasm, both goldens. The bake golden gained 40 rows and moved exactly two — dorsal wing's, from the belly cut. Every pipeline-golden code grew by eleven loci and no horse moved.

2026-09-10

A check for gap 169, and the two metrics that were measured and thrown away first.

Three genes in two days had been brought down by the same thing — a gene's layers each measure their delta against the colour the gene started from and the deltas are added, where every author expects them to stack. This session built the check. Most of the work was finding out which measurement actually separates the broken genes from the good ones, and the two that don't are worth writing down, because both look obviously right until you run them.

What was thrown away, and why

Divergence from stacking. Render the expression twice — once the way the engine does, once applying each layer before the next measures — and diff. It is the most direct possible statement of the bug, and for disjoint layers the two are identical by construction, so it looked like it would have no false positives at all. It fires on about half the corpus. Ringwork diverges on 92% of its painted texels, crazework on 96%, panda on 55%, and all three look good. A gene with overlapping layers whose author tuned the colours until the sum looked right is indistinguishable from one whose author did not.

Clip pressure. How far past the byte the summed delta would have gone. This one ranks pre-fix contour first and second of every expression in the mod, at 89% and 88% of painted texels — exactly right. It also ranks dripwork third and crazework fifth, and those are fine, because clipping toward a colour every contributing layer wanted is harmless: dripwork's layers all want black, so a sum that clamps to black is the intended answer.

The conclusion that shaped the fix

No metric separates “relies on summing and looks good” from “relies on summing and looks wrong”, because the only difference between those two is whether a human liked the result. Any check built on the summing itself is either a gate that fails half the mod or a report nobody reads. So the check had to be built on something objective, and there is exactly one objective thing in the neighbourhood: whether a layer is there at all.

What was built

DeadLayerTest: take a layer out; if the horse does not change on any base coat, the layer was not doing anything. No taste in it, and no pattern-matching either — whether two masks overlap is not decidable by reading them, since they are arbitrary functions of position folded with inversion and subtraction, but it is decidable by evaluating them, which is what the painter already does. 23 seconds, which keeps it inside the working loop; the speed comes from an early exit, since a layer proven alive on the bay never gets re-tested on the other coats.

It catches three unrelated mistakes with one measurement, and none of them fails anything else: an empty mask fold, a layer clamped away by an overlapping one, and a feature smaller than a texel. It found 29 dead layers in the shipped genes. Validated against the known bug by reconstructing the pre-fix dorsal wing: it flags exactly the two layers that were invisible, and the fixed gene is clean.

What it found

The blind spot is declared rather than papered over: 47 layers carry a PIGMENT or LUMA mask, read the coat underneath, and cannot be judged against a synthetic base — moth eyes' six layers read as dead on every base the check builds and are simply unhoused. They are skipped and counted, and a second test asserts the count has not quietly grown.

The root cause is still open

The reason authors keep writing this is that the format cannot say “paint this on top of that”, so they write the thing that looks like it and get summing. A per-layer "over": true that applies the accumulated delta before the layer measures would make dorsal wing's original file correct as written. That is a format change — the five-file contract plus every golden — so it is written up on gap 169 as a decision rather than taken unasked.

2026-09-10

Four genes retuned off looking at them, and the coverage rule of thumb written down.

Four notes from the owner on yesterday's intake, and the interesting thing is that two of the four turned out to be the same defect wearing a different complaint.

“Contour doesn't have enough colour” was not a palette problem

Contour draws a topographic map: six bands of a landscape, each a threshold on one FRACTAL field. Band 0.38 therefore sits entirely inside band 0.30 — the bands are nested. And a magical gene's layers each measure their delta against the colour the gene started from and then sum, so six nested TOWARDs do not paint six bands. They paint one colour, which is whatever six deltas add up to, and six of them add up to white.

Which is what shipped: an almost-white horse with a brown patch. The pastel palette it was given — #e2d5b8, #e8c8e5, #c8eeec — was the author compensating for a wash whose cause they could not see, and adding saturation to those colours would have made the wash worse rather than better. Every band now subtracts the threshold above it and paints only the ring between the two. Once they are disjoint the colours can be what a contour map actually looks like, so they are: a hypsometric ramp from deep green through gold and orange to red-brown.

The coloured form gets a second hue knob rather than more saturation on one. inkHue runs the low ground and highHue the high ground, and they are independent draws — a knob cannot be offset by a constant in this format (gap 164), so a horse can roll two hues that sit next to each other and come out nearly monochrome. That is the honest cost of the missing arithmetic, and it is a good deal less bad than one flat hue.

Three genes in two days, same cause

Dorsal wing painted a solid black horse, contour painted a white one, and both were layers summing where the author expected them to stack. It is gap 169, and the thing that makes it hard to see is that the symptom does not look like the cause: dorsal wing looked like a mask that was not firing, and contour looked like a palette that was too pale. Neither looks like an arithmetic problem, so neither sends you to the rule that explains it.

Dorsal wing wraps down the sides now

The butterfly is drawn half again as wide as the horse and centred on the spine (sizeV 1.5, originV -0.25), so the outer part of each wing falls off the edge of the back and lands on the flank, and the NORMAL mask was opened from 0.15 down to -0.75 at round 0.85 to let it get there. The two numbers have to move together: widening the drawing without opening the NORMAL just clips more of the wing away, and opening the NORMAL without widening the drawing puts bare flank into vertical bands.

Worth being precise about what is on the sides, because it is not a drawing. A top-plane shape is read at (x, z) and a flank face has one constant z, so the whole side shows a single vertical slice of the butterfly stretched downward. On the back you are looking at the drawing; on the flank you are looking at its cross-section running down. It reads as wings folding over the barrel, which is what it is for.

Dripwork: no ground, fewer marks, and runs down the legs

The white ground went first — an ALL layer at 86% opacity, which turned every base coat into the same pale grey before the gene drew anything. That is a lot of horse to spend on a backdrop for a scatter of small marks, and the same argument took the white out of rising sun. The marks are on the BODY and the LEGS now rather than everywhere, and there are fewer of them. The runs are new: STROKES on Y, weighted to the lower half by an AXIS mask, which is what makes them read as ink running toward the hooves rather than as a stripe pattern.

The AXIS went in inverted the first time, which selected everything above the barrel — on a gene restricted to the body and the legs, that is nothing at all, and the layer rendered as though it were not there. Caught by looking at it, which is the only thing that catches it.

Rising sun goes to the top of the order

Priority 123 to 620, which is the top third and above every other marking except invert and tron. It covers the whole horse, so by the rule of thumb below it should be near the bottom. It is at the top on purpose: a rising sun is a poster printed on the horse rather than a marking the horse has, and a poster that other markings draw over is not a poster.

The rule of thumb it breaks

Coverage and priority is new and is deliberately not a rule: the more of a horse a gene covers, the lower its priority should be. Later paints over earlier, so a whole-coat gene painting late erases everything under it and a small mark painting early gets erased by anything bigger. Sorted by coverage, a horse carrying five markings shows five markings.

It ships with its own counter-examples attached, which is the point of writing it as a rule of thumb: rising sun breaks it for the reason above, and fielded breaks it because a gene that reads the coat with LUMA or PIGMENT has to run after whatever it is reading however little it covers. The five priority bands the mod already uses are written out beside it, because slotting a new gene next to whatever it most resembles beats reasoning it out from scratch. The prompt carries a four-line version.

What was regenerated

Icons, the bundle, 157 gene pages, the designer wasm, both goldens. The bake golden moved exactly eight rows — the two alleles of each of the four genes — and nothing was added or lost. Every genotype code in the pipeline golden changed and no horse did, which is rising sun's priority moving it up the code order and nothing else.

The intake folder was empty this session; nothing new to import.

2026-09-10

Fourteen genes out of intake, two drawings that were traced photographs, one that painted itself black, and fourteen founder tables that were four times over budget.

The whole intake folder went in: blackwork, coastline, contour, crazework, dorsal wing, dripwork, dustfall, overcast, ringwork, rising sun, serpentine, shallows, squiggle and teardrop. Priorities 108–124, no clashes, and the batch was hashed before anything was opened (two files once arrived holding a third's content). Twelve of the fourteen needed nothing but installing. The other two, and one thing wrong with all fourteen, are the session.

Two drawings were over the SVG point ceiling

Blackwork's ornament flattens to 4368 points and dorsal wing's butterfly to 1637, against a ceiling of SvgPath.MAX_POINTS = 1024. The ceiling is a cost ceiling — every mapped texel of every skin walks every segment — and it is the guard that exists to stop a traced photograph going into a gene file. These are traced photographs.

What makes them expensive is not the drawing, it is the flattening rule: the engine gives every cubic a fixed sixteen sub-segments, which is right for moonwing's seven big curves and wildly wrong for an outline of two hundred and seventy tiny ones. The drawings do not need them. The viewBox is 1000 wide and a horse is about 150 pixels, so a point every 1.5 units is already finer than a texel by an order of magnitude. Both were flattened the way the engine flattens, run through Ramer–Douglas–Peucker at whatever tolerance hit a 700-point budget, and re-emitted as polylines — one point per vertex where a cubic costs sixteen, and the silhouette identical to well under a texel.

The engine-side alternative is adaptive flattening — subdivide until the chord error is under a tolerance instead of always sixteen — which would let a traced outline in at its natural cost and would also move every existing SVG gene's golden, the spec fixtures and the creator's JS twin. It is gap 168 rather than something to do on the way past.

Dorsal wing painted itself solid black, and the reason is worth knowing

The gene draws a butterfly along the spine: a near-black ground over the whole silhouette, a teal margin band, rust squiggles, hot pink cells inside those. On a horse it was a black horse. 484 texels moved on a bay and not one of them was teal, rust or pink.

A gene's layers do not stack, they SUM

Every layer of a magical gene measures its delta against the colour the gene started from — not against what the layer above it left — and the deltas are added together. So a TINT of -200 on every channel under a TOWARD teal does not give teal. It gives teal minus 200, which is black. Confirmed by deleting the ground layer from the built resources and re-measuring: teal, rust and pink all appear at once.

The prompt has said this since the import — rule 8, “a layer cannot see what the layer above it painted” — and it is still the easiest thing in this format to get wrong, because every gene that does work does so by having disjoint masks and so never shows the author what the rule costs.

The fix is the decomposition the gene's own notes already describe, written as subtraction instead of as stacking: the ground paints the inner shape minus the rust patches, and the rust paints its patches minus the pink cells. Nothing about the drawing, the colours or the nesting moved.

All fourteen founder tables were between three and six times over

The prompt tells an author to declare a rarity tier and let the budget derive the table, precisely because the rates compound across a hundred and sixty genes. All fourteen declared a tier and wrote a table anyway, and every table was 2.9× to 6.3× the tier it declared. Together they claimed 2.87% of founders against a budget of 0.62% — which is not a rounding error, it is another two percentage points of wild horses carrying something magical, on top of what the existing hundred and forty-odd already spend. All fourteen were re-derived from their declared tiers.

The three-form rule, applied for the first time as a rule

These arrived before the rule was written down, and every one of them is a white-or-pale form plus a coloured one. Exactly one qualified: overcast, whose pale form is a four-step grey ramp from #dee2e6 down to #575e69 against the same four steps in one hue — the dark half of the scale simply unused. It has a Ck black sky now, the same four steps between #0d0d0d and #707070.

The other thirteen were judged out, and the judgement is the rule's own “when not to” rather than taste. Dripwork's ink is already black — its #fbfbfb is the ground the graphic sits on, not the marking. Ringwork is achromatic already and runs from #d1d5d6 to #17181b, so there is no unused end to move into, which is the whole premise. The rest — coastline, contour, crazework, dorsal wing, dustfall, serpentine, shallows, teardrop and the others — name three to seven fixed colours, and the palette is the marking.

And four expressions had picked up a pair they should not have

Yesterday's black-allele sweep handed out unclaimed combinations by the rule “a pair belongs to whatever its more dominant allele's homozygote shows”, which is right for the pairs the new allele created and over-reaches on pairs that were deliberately left to the wild-type catch-all. It caught four: Fdf/n on fracture, Tmr/n on marble tobiano, Ylc/n on yalia and Cc/n on overcast, each of which had been wild-type by the author's choice and had quietly started expressing. All four are back, and their founder tables with them — fracture and marble tobiano are recessive genes again, so the whole budget sits on the homozygote where it belongs.

Versioning

Recorded on releases rather than left in a commit message: the next release is 0.5.000, and after that only the last number moves. Three digits, on purpose. The middle number does not move without the owner saying so explicitly — not for a big feature, not for a batch of genes. 0.2.0 to 0.4.1 in two days is what the rule is a reaction to.

What was regenerated

Gene bundle, icons, 157 gene pages, the designer wasm, both goldens (43 rows added to the bake golden, none moved, none removed; the pipeline golden's codes all grew by fourteen loci with no horse moving), the search index. Links, gene tabs, parity and recipes green; :neoforge-26.1.2:build green.

2026-09-10

A black allele for forty-two markings, the rule that produced them written down on the gene page, and Cleave split into two loci.

Every magical marking here that had a white form and a coloured form now has a black one as well — 42 forms across 41 genes. That is not 42 judgement calls; it is one rule applied 42 times, and the rule is the part worth keeping.

White and coloured are two points on one axis with the far end unused

The convention these genes were written to is a white form on allele[0] and a recessive coloured one that draws the same layers with "hue": "$hue" where the white one names #ffffff. Every one of them therefore already knew how to draw itself in a single flat tone, and the whole dark end of the scale was unoccupied. The black form costs one allele and no new vocabulary, and it lands on a set of horses the white form has never read well on — the palominos, cremellos, greys and dominant whites, where a white marking on a white coat is a marking nobody can see.

Re-tone, do not repaint — and measure achromatic as chroma

The black expression is the white one with every achromatic target moved into the dark end of the scale, keeping the order and the spacing of the tones it had, and every saturated target left alone: that colour is the gene rather than its lightness. Moonwing is the clearest result — a graphite wing carrying the same iridescent dust and the same gold binding, because only the wing itself was ever white.

The measurement that has to be chroma

Achromatic is max(r,g,b) - min(r,g,b) under about 0.12, not HLS saturation. Saturation reads backwards at exactly the values that matter, because its denominator collapses near white: #f4f0e8, a pearl six counts off the top of the scale, scores 0.35 and gets protected as though it were a colour. The first pass of this change used saturation and shipped a black moonwing byte-for-byte identical to the pearl one, along with six other genes whose black form was a copy of their white form. It was caught by the transformer printing how many grey steps each gene came out with and seven of them saying zero — which is the argument for making a bulk edit report what it did rather than how many files it touched.

The half that is easy to skip: both achromatic forms get the same depth

A coloured form that separates its layers into a dark ring, a mid field and a bright core, against a white form that paints all three off-white, is not two versions of one marking — it is the good one and the flat one. Where the coloured form spanned a noticeably wider range of lightness, the white form was regraded onto the coloured form's own per-layer ordering, in greys. Three genes qualified. Pavonem is the one that shows why: its coloured form makes the peacock eye's pupil the darkest thing on the horse, and its white form had been painting that pupil the same white as the spots around it, so the eye had no pupil at all.

Dominance: nothing that existed moved

Black sits directly under white and above coloured, in declaration order. Written that way, adding an allele to an existing gene changes nothing that already existed — every pair that had an outcome keeps it, and only the pairs carrying the new token are new. The new pairs were handed out by one rule, an unclaimed pair belongs to whatever its more dominant allele's own homozygote shows, which reproduces the series each file already had and is the only rule that got Cleave's two independent pairs right without anybody typing out 28 combinations.

The proof is the bake golden: 42 rows added, 3 rows moved, none removed — and the three that moved are the three regraded white forms. The pipeline golden did not move at all.

The rule is now on the page, not just in the files

Three forms, where the palette is narrow is the whole of it — the dominance table, the token convention, the re-toning rule with the chroma warning, the depth rule, and when not to. It is a recommendation and it says so: where the colour is the gene, coral bloom's sea-blue and gold or anything whose whole idea is that it is ember, a grey version is a different marking wearing its name. The prompt's CONVENTION block says the same thing to a model, with the slot order and the tone targets spelled out.

And Cleave became two genes

Cleave carried four alleles that crossed two questions together: Clv and Clc for a front-half split, Wav and Wac for a back-half one. So "the same marking in the other colour" and "the same marking on the other half" were the same kind of thing, and adding the black form to it had to add two alleles. They are separate questions and they are two loci now: horsegenetics.cleave says whether there is a cleave and what colour, and cleave_side says which half, wild type being the front. Clvw/Clvw n/n is a white front half; Clvw/Clvw Wve/Wve is the same marking on the back.

It is modelled on accretion + accretion field, which is the same shape already in the mod: the modifier locus has no layers, every horse carries it, and the painting gene asks for it with needs. Two things about it are load-bearing and are written into the file's notes. The conditional expressions must be listed before the unconditional ones, because SpecGene walks the conditionals in file order and only falls through to the plain combination table when none is satisfied. And Wve is dominant — needs asks for one copy — so a foal out of one back-half parent takes the back half.

The old back-half drawing survived the split byte-for-byte: of the six golden rows that moved, all six are the new heterozygous Wve/n horse, and the Wve/Wve one hashes exactly as the old Wav/Wav did.

What was regenerated

Gene bundle, icons, all 143 gene pages, the designer wasm, both goldens, the search index. Links, gene tabs, parity and recipes green; :neoforge-26.1.2:build green.

2026-09-10

Four genes retuned off looking at them, one new gene out of intake, and the gene files’ notes moved to the tab they were always about.

A short session, and every change in it came from the same place: somebody opened the icon sheet and said what was wrong. That is worth saying because three of the four fixes are things no test could have raised — the goldens were green, the parity was green, and every gene was painting exactly what its file said.

Coral bloom’s rim was not a rim

Coral bloom was written as three concentric zones of one tessellation: a bright orange rim in the seam, a deep blue field inside it, a pale crown at the middle. On the horse it was an orange horse with blue blobs. The arithmetic is the whole explanation and it is worth writing down, because the same trap is waiting in every other CRACKLE gene: a wall of width w through cells of size s takes roughly 2w/s of the surface, so at gap: 1.2 in cells of 5.5 to 9.5 the seam is not a hairline round a cell, it is most of the horse, and the cells are the things floating in it. A rim colour is load-bearing in a way a fill colour is not — a fill can be wrong and the picture survives it; a rim being wrong inverts figure and ground.

The fix is to paint nothing in the seam. The base coat shows there instead, which is a colour the horse already owns and which therefore cannot take the picture over however much of the surface it covers. Blue band and pale crown are untouched.

Moonwing, four times the size, and over the back

Moonwing’s viewport went from 0.30×0.20 of the body to 0.62×0.44 and, more to the point, is now deliberately taller than the barrel. That last part is the whole of how it wraps: a side-plane SVG ignores z entirely, so the drawing is a function of x and y and nothing else. Raise its top edge past the barrel’s and the same outline that covers both flanks covers the back between them — no seam, no second placement, and the top of the wing simply clipped off by the horse. The extrusion that was already doing symmetry for free turns out to do wrapping for free as well.

And a gene that reads the coat underneath it

The other half of the moonwing ask was that the pearl allele give other white markings the same treatment — the gold edge and the coloured dust. That is LUMA’s white channel, which is why it exists: not “the pale parts” but the achromatic floor, so a splash reads 1 and a palomino reads near 0.

The rim is the move worth remembering. There is no erode operator and there does not need to be one: the white selection, intersected with the same selection inverted and grown by 1.3 units, is a band exactly that far inside every white border. Growing the NOT-white eats into the white, and the intersection is what was eaten. Both layers run first, before the wing — LUMA reads the coat as it stands when the layer runs, so a wing laid down first would be read back as one of the markings it is supposed to be matching, and edged twice.

Measuring that turned up gap 166, which is not this session’s doing: on a horse that is white everywhere, the pearl form’s gold band comes out grey. Four warm texels against 589 on a bay, and 692 for the coloured allele on the same white horse. It reproduces with the new layers stripped back out, so it predates them; the cause is unestablished and is written down rather than chased.

Three greys instead of one ivory

Iridescent rosette’s pale allele was a single flat ivory annulus and it read as a smudge: a ring with no interior structure has nothing to tell the eye where its edges are. It is now the same three concentric bands the coloured form uses — same radii, same knobs, same registration — with three greys where that one has three hues. The side effect is that the gene’s wiki icon flipped to the coloured allele, because CoatVisibility photographs whichever allele moves the most texels and the darkest grey sits close enough to a bay to lose the count. Noted on verification so it is not reported later as a bug.

Nymphaline

New, out of the intake folder, and the first arrival since the SVG engine landed — which makes its most interesting property that it does not use the SVG mask. Its source was eight posterised colour layers traced off a photograph of a butterfly wing, about 300 KB of path data: past MAX_PATH_POINTS by three orders of magnitude, and the wrong shape of thing regardless, because a traced photograph is a patch of membrane and not an outline, so there is nothing in it to place. It is rebuilt as rules. FAN is the radiating venation, on a pivot that sits on the flank rather than above the back — from above, the barrel subtends about 74 degrees and any spacing wide enough to give a vein a texel of width leaves three veins on the whole side; from the point of the shoulder it subtends nearer 100 and the same spacing gives six. CRACKLE with vertexWeight up is the fine net where the venation breaks down, because a second fan fine enough to read as secondary venation comes out under half a texel wide at every radius on the barrel. Priority 118, with the other whole-coat genes.

The notes were on the wrong tab

Every generated gene page put the gene file’s notes under “About this gene” on the gameplay tab. They are implementation reasoning to the last line — why FAN and not a second SVG, which mask the registration hangs off, what the import asked for and was refused — and the gameplay tab is defined as the one written for a player who does not want the model. They are on the coding tab now, under “Why it is built this way”, which is the same call outcomeSummary already made about the outcome prose, for the same reason. 56 pages moved.

What was regenerated

Both goldens: eleven rows in coat-bake-golden.txt, and every line of coat-golden.txt gained a nymphaline segment. Worth being explicit that no rendered horse in the pipeline golden moved — all 1056 hashes are unchanged and only the genotype codes grew, which is what adding a locus does, and is also the check that the four edited genes are on none of those 39 horses. Then the gene bundle, the icons, the pages, the designer wasm, the search index and links.

The intake folder’s twenty-two markdown sources are deleted — each was checked against the gene it became before anything was removed, and all twenty-two ship. intake/overcast.json arrived during the session and is untouched.

2026-09-09

The engine learns to read SVG, and twenty-two genes land with the ten masks they asked for.

The intake folder held twenty-two gene files and every one of them was written against a vocabulary that does not exist — PAINT ops, CRACKLE with a stretch, DAPPLES with a radius, SPOTS with a count and a centre. That is not a complaint about the files. What each one did carry, and what made the batch worth a day, was a TOOL GAP section saying which measurement its reference needed and why no arrangement of the masks that exist reached it. Ten of those were real, and the session ran in that order: build the tools first, then write the genes that use them.

The SVG mask, which is the one that was not in any tool gap

PATH was the admission that some shapes are not made of a rule — that a marking somebody drew has to arrive as points. This is the admission that follows from it: markings that get drawn get drawn in a drawing program, and what comes out is a d string of cubics, arcs and subpaths inside a viewBox. Re-typing one as sixty-four normalised control points is the step where the drawing stops being the drawing — the holes close up, the arcs become chords, and an evening goes on arithmetic.

So SVG is deliberately the whole of the grammar rather than the convenient part of it. Every path command, including the reflected control point an S and a T carry and the endpoint-parameterised elliptical arc that every exported circle is made of. Subpaths, so a letter O has a hole — and a fill rule, because whether that hole is a hole is written in the artist’s file and cannot be recovered from the geometry. The transform list. viewBox and preserveAspectRatio. A stroke with real caps, joins, a miter limit and a dash pattern measured along the path. SvgPath flattens it once, at load, so per texel it costs what PATH costs however long the string is.

node intake/tools/svg-to-mask.mjs reads an actual .svg and writes the mask blocks: nested <g transform>s flattened into each element, rect/circle/ellipse/line/polyline/polygon converted to path data the way the specification defines them, the presentation attributes carried across, and a warning printed for everything it cannot see — a CSS class, a <use>, an element both filled and stroked. Moonwing is the first gene in the mod that carries a drawing.

Nine more measurements, each because a reference needed one

Two requests refused rather than deferred

Nothing here will ever vary colour by the angle to the camera. The coat is a texture baked once per horse and no mask or op will ever see a viewer, so true iridescence is out of reach and will stay out of reach while the coat is a PNG. NORMAL is the structural half of it — the sheen sits on the planes that turn into the light and stays there — and that is the honest approximation. And a knob cannot be offset by a constant. Agate’s import wanted each cell’s heart to be its rim’s complement, 180 degrees round the wheel; a hue knob is a number the horse drew and there is no way in the format to say “that number plus 180”. Both are now stated in the prompt so the next batch does not spend a section proposing them.

Four things the imports got wrong that would have shipped silently

This is the part worth reading, because none of the four would have gone red.

Two smaller things

wing_cloak.json is missing from genes/index.json on purpose, and this session regenerated the index from the folder and put it back. The absence is the killswitch — gap 129 parked the gene until it is retooled, and the whole switch is that one line not being there. Rebuilding the list by walking the directory is the obvious move, is what the contract’s warning about a hand-kept list invites, and re-enabled it silently: nothing went red, because the gene loads perfectly. It was caught while writing the gaps page and reverted, which moved the bake golden by exactly its three lines and no other hash. The real gap is that the file carries two kinds of information — “these exist”, which a script can derive, and “this one is off”, which it cannot — with nothing telling them apart. Gap 162. Until that is fixed: add to the index by hand, never regenerate it.

Flags can default true, which the SVG mask needs twice: a <path> with no stroke is filled, and a pasted drawing has to be flipped. That is a real change to the creator rather than a cast, because its exporter drops any setting equal to its default — a true default has to be written when it is unticked. The parity check compares flag fallbacks now; it never did when they were all false.

founders.py was re-run over the whole set, as it is designed to be: it budgets the tiers together, so twenty-two new genes re-derived everybody’s numbers. Forty-three existing tables moved, not the twenty-two the commit message guessed at — because the re-derivation also flattened every table that had drifted above its declared tier, and several had, by a factor of seven. Across the mod the share of founders expressing some magical gene went from 16.32% to 13.35% while gaining twenty-two genes. The table and the question of whether those are the right numbers are on verification §0-BM; the fix, if they are wrong, is a gene’s rarity tier and another run, never a hand-edited founders block.

Sources: common/coat/pattern/SvgPath.java, SpecPainter, SpecSchema, wiki/gene-creator/js/svg-path.js, intake/tools/svg-to-mask.mjs, and the twenty-two files in horsegenetics/genes/.

2026-09-09

Four pages about making a gene become one, and the prompt is allowed to say a shape cannot be drawn.

A documentation session, run alongside the play session below and touching none of the same files. It started as “bring the gene-writing page up to date” and turned into a merge, because the reason the page was out of date was structural.

The page had missed five masks, and the reason it missed them is the interesting part

FRACTAL, PATH and CHOICE landed earlier the same day; LUMA, EDGE and spreadFrom the day before. None of them had reached creating-a-gene.html, so its machine-facing tab was handing a model a vocabulary with no lace, no drawn shape, no branch and no way to read the resolved colour — and cheerfully inviting it to approximate. Both schema tables were re-transcribed whole from expected.json rather than patched row by row, which is how they went stale the first time, and the page now says so and gives the one-liner that dumps the fixture.

Also caught up in the same pass: space: "local" in all four places it appears, the parts-by-position rule, that a layer cannot see what the layer above it painted, PATH’s body space where the spine is 0.62 and not 1.0, and the six things the loader now refuses outright — several of which the page still described as traps to be careful about, which they have not been for some time.

“No commentary” was buying a worse failure than it prevented

The pasteable prompt used to end output ONLY the JSON, no commentary. That is right about the ninety per cent of the job that is transcription. It is wrong about the rest, and wrong in a way that hides: a model told to emit JSON and nothing else will always find something to emit, so the answer to “draw a shape this format cannot express” came back as a confident approximation with no sign attached that it was one. Every mask after the first eight exists because a person hit that wall and said which measurement was missing.

It is now conditional and narrow: ship the closest honest approximation, record it in notes, then a fixed-shape TOOL GAP section — the element, how the approximation falls short, why no composition reaches it, and a concrete mask or op design with every parameter and default. Two at most, nothing else. The bar is the one the page already sets for a human author: a different measurement, not different tuning, with PATH and “you have not found the numbers yet” both named as explicitly not gaps.

Then the merge, on the owner’s call that it was spread too thin

The split across four pages was not by subject, it was by accident of when each was written: creating-a-gene.html had the traps, gene-format.html had the parameter that caused them, modding.html had the Java escape hatch, gene-effects.html had the effects vocabulary — and which one you needed depended on already knowing the answer. Making a gene is all of it in eight topical tabs: Start here, Shapes, The file, Masks & ops, Effects, In Java, The prompt, Checks.

The material was moved by line range, not retyped. Four thousand lines of reference retyped by hand is four thousand chances to introduce something nobody would ever find; what is newly written is the connective tissue, and an assembler script refused to emit a page that still named a file it had just deleted.

The tabs are page-localtabs.js has given a panel whose data-tab is not one of the three wiki views its own button since the LUT lab, but nothing had used it at this scale. Two things had to give. sync-page-views.mjs derived views from the panels and would have written [] for a page whose panels are all topical, hiding it from every view at once; a page that names no wiki view is now left alone exactly as an untabbed page is, and this one’s views: ["coding"] is hand-set. And .tab-bar had no flex-wrap: three buttons never needed it, eight do.

The unbuilt architecture was being written twice

Half of gene-effects.html was auras, pools, memory, goals and spawn variants — none of which runs. The plan was to move it to the trait architecture page, and on comparison that page already carried a faithful summary of every one of those sections. So the merge there is not a bulk move: horse-traits gained the anchors the new page links into, the build-order table annotated with what actually shipped against each step, and the divergences catalogue in place of a slice-vs-architecture table that still described five live verbs.

Two things that were simply wrong

Nothing here touched the play session’s files. The eight Java edits in this work are one-line doc comments and test failure messages that named a page which no longer exists — including GeneWikiTool, which is the generator for every gene page, so leaving it would have had the next bake rewrite all of them with a dead link.

0.3.8 – 0.4.0 — the stall that killed a horse, and a long evening of the owner playing while I worked.

An unusual session in shape: the owner was in the game the whole time, so almost everything here came from a report rather than from a plan, and three things were confirmed working within minutes of shipping. The releases run together because they were handed over one at a time as the reports arrived.

The one that mattered

A horse was teleported into a wall and suffocated, and the cause was two bugs that only killed anything together. The stall detector flood-filled air, three blocks above the sign - and a fence is one block tall, so it climbed over the front rail of every real stall, spilled into the aisle, exhausted its cell budget and reported “not a room”. That dropped it into a fallback box straddling the wall. Then landingSpot scanned that box, found nothing standable, and fell through to return stall.signPos() — the block the sign itself occupies, flush against the wall, with wall material at head height. It was the only position in the whole routine never checked for anything, and it was reached exactly when the stall was least understood.

The search now walks floor columns rather than air: it never rises, so it cannot climb a fence, and it steps at most one block between neighbours, so a slab or a carpet is still one room. A door, trapdoor or gate is the edge of the room in every state — judging a doorway by whether a horse could squeeze through it measures a different stall depending on which way the gate happens to be swinging, which is not a property a stall should have. Landing is measured live from the sign instead of read back from the record, so a rebuilt stall is the stall the horse arrives in; the chunk is pulled in first; and a stall with nowhere to stand refuses the ticket without consuming it. A refusal the player can read beats a horse in a wall.

The graph search moved to common/ as StallFill, behind a one-method interface the game implements, with StallFillTest building stalls out of ASCII. That is the cooldown sentinel's lesson applied rather than restated: a piece of pure logic parked on the Minecraft side is a piece of logic nobody can write a five-line test for. The test that would have caught this is six lines and draws an L-shaped room.

Confirmed by the owner, in play

Shearing and milking both work — which closes the cooldown-sentinel gap, since those two had never worked on any horse, ever. Binding a stall sign works. Spending a ticket brings a horse home. Research papers off a book work. The Alleles tab collects. The gene previews turn in 3D and the owner likes them. The cowboy and the horseman read correctly as they/them.

Reports that turned out to be something else

“All of my horses have the exact same speed, health, jump, and size.” True, and not a defect. The custom spawn egg starts every locus at Gene.defaultAllele(), and an all-baseline genotype resolves to exactly HorseTraits.baseline() every time — the rolled epigenome cannot vary it, because epigenetic numbers ride on allele copies and a baseline locus contributes nothing for them to modulate. BodyStatSpreadTest pins both halves so the question is answerable in seconds rather than by reading six gene classes. Whether that is the right default for the egg is now gap 157 and nobody has decided.

“Milking does not work.” It did; the bucket had gone to the inventory rather than the hotbar. Worth recording only because it is a water bucket: there is no milk gene in the mod, the only bucket yield is waterborn, and the checklist task is called “Milk a mare” without saying the mare needs the gene.

The research shelf was written up as fixed and was not. Two faults the first pass had not looked for: four fixed captions were wider than the 176px window and drawn with raw g.text rather than the fitter three methods away, and the filing slot had been inactive on the server for the life of the block because its isActive() read a field only the client ever sets. Every paper filed had been snapping back to the cursor. The layout constants had been moved, which was the described fix; nobody had opened the window.

Built to order

Taming by hand. Crouch, hold food the horse eats, look at it, stay still: it walks over and eats out of your hand, and each mouthful is vanilla's own taming roll. Deliberately the same odds as being thrown off — a second door in, not an easier one — and diet-aware, so a carnivore wants meat and a dhampir does not come.

Bareback riding, twice. First it was made to bond, because vanilla only calls a rider “controlling” once the horse is saddled and the obvious test therefore paid a bareback rider nothing — and bareback is how you talk a wild horse round in the first place. Then it was made to steer, at the top bond tier. The first attempt drove the horse from outside on the tick and felt like it; the owner's suggestion — fake a saddle and take it off on dismount — was better, so it now equips a real saddle carrying a PHANTOM_SADDLE component. The renderer feeds the saddle layer an empty stack, the saddle comes off the tick the rider leaves, and the rider's inventory is swept, because you can open a horse's inventory while mounted and this would otherwise mint saddles.

The breed book (wiki/breed-book.html) and a Breeds tab. The page carries the mod's lore, set out by the owner: none of these horses are native, every breed is an artefact of a collapsed civilisation, gathered up and moved here by a race nobody has a name for, who then went away or stopped coming or is still here and not introducing itself. The horses are thriving without their masters and remain fond of people — which is the in-world reason a wild Shetland will eat out of your hand. The hay portal and the breeding carrots are that race's leftovers: they work, nobody knows why, and the mod never explains the mechanism. The cowboy and the horseman may be the last of them or may have inherited the instructions; the page leaves it open and argues that leaving it open is worth more.

The in-game tab follows the browser's standing rule — it fills in as you meet breeds, because every reference tab here is a record of your game rather than a manual. Only Getting Started and Recipes are complete from the first minute, since those are how you find out what to do at all.

Smaller

What the next session should pick up

Gap 154 (closed 2026-09-10 - see settled decisions) — the Breeds tab ignores creative and did not fill in on a tame. The creative half is certain and small; the taming half has a named prime suspect (a BreedLineage token that is not a bare Breed.id()) and should be checked there first.

0.3.7 — two things you could craft but not use, and two you could do that did nothing.

A repair release, and the repairs came out of somebody playing it and saying what happened. The shape of the day was the same as the last one: less building, more being told.

The bug that had never worked once

“Sheers aren't working. They say that the horse's coat hasn't grown back yet” — on a horse that had never been sheared. Then the same for milking. One cause: HorseCooldownsAttachment.last(key) answers Long.MIN_VALUE for a key that has never fired, and ready was now - last(key) >= cooldown, which overflows to a large negative elapsed time. So the gate said “not ready” for every horse that had never done the thing — and since the stamp is only written on success, that is forever.

Shearing and every gene yield had therefore never worked on any horse in any build, and the dhampir's bite gate was dead the same way without anybody noticing. The fix is one absent check. The part worth keeping is why it survived: the class is Minecraft-side, so there is no test source set it could have been caught in. The same record in common/ would have been a five-line test.

Stalls: the gate was the broken part

“Binding a sign to a horse works, creating a stall / the interior detector definitely doesn't.” StallDetector demanded the sign on the outside face, a room sealed with nothing in it but air, and everything within one block up or down of wherever the sign happened to be. A doorway failed it, a roof beam failed it, a wall torch failed it, hanging the sign from the inside failed it. And stalls are read by exactly one thing — the ticket — so the gate was standing in front of the only feature it existed to serve.

On the owner's call it measures instead of judging: both sides of the wall, !blocksMotion() instead of isAir(), five blocks of headroom, and a plain box in front of the sign when nothing encloses. It cannot refuse. Stall signs has the detail.

Tickets, built on top of it

Inert since they were added. The owner's spec: one-time use, teleport a horse back to its stall, and — against the original design — not bound to a horse. You click the horse you want sent. The tier is reach: basic within the overworld, bound within any one world, interdimensional across them.

The middle rung was read off the recipe ladder rather than specified — the owner said outright they did not know what the third one should be, and an ender pearl to leave the overworld / an eye and blaze powder to leave the world you are in is what the recipes already said. bound_ticket is consequently named after a design that no longer exists (gap 152). Tickets.

Getting Started is a book, not a scroll

Asked for a table of contents; what it wanted was to stop being one long article. It is a section at a time now, the contents list picks one, and the checklist is split into its seven groups rather than thirty-odd boxes in a column. Every section ends with a link to the next, so it still reads straight through — a tutorial you cannot page through would have been a regression from the scroll.

New Bonding chapter, because none of it was written down where a player could reach it: the rates, the fifteen-a-day ceiling and why it is there, and what each tier changes about the animal. Four new checklist tasks — one per bond tier, credited from syncCare so that every path that moves bond funnels through one place, and one for giving a horse a barn name.

Two ticks that fired on the wrong thing

TRANSFER_PAPER was completed at the top of onEntityInteract, before the target was known to be a horse or the held item to be a paper — so right-clicking anything with anything ticked it. Found while reading a stack trace for something else. BUILD_STALL ticked when the sign was bound to the horse, which is a sign in a pocket rather than a stall. Both moved to the act they name.

And the jar was lying about its own version

mod_version in gradle.properties names the jar; neoforge.mods.toml carried a second, hand-edited copy that tells the game. This release built as horsegenetics-0.3.7.jar announcing itself as 0.3.6, and was caught only because the file size came out identical to the previous one. The toml is templated now and processResources declares the property as an input, so there is one place to write it.

What the day cost that was nobody's fault

“Trying to use a nametag crashed my game” — a ClassNotFoundException for HorseProgress, one of our own classes, whose .class file has an mtime of the exact second of the crash. A compileJava run underneath a live runClient. Not a mod bug, and the second time this pattern has cost a debugging session — check the jar or class mtime against the crash timestamp first.

Where the day ended.

Six releases, 0.3.1 through 0.3.6, and the shape of the day was outside testers: almost everything here started as somebody else playing the mod and saying what went wrong. That is new, and it changed what the work looked like — less building, more reading a stranger's stack trace and asking what they actually saw.

Two of the day's bugs were invisible from this machine and obvious from theirs. The encoder crash that made 0.3.0 and 0.3.1 unplayable fired the moment a horse came into render range, which a dev world reaches less reliably than a survival one; the empty stables needed a rotated jigsaw, and three quarters of them were. Neither would have been found by running the game here for another week.

And two of the reports were not bugs at all — a shared horse inventory that was the player's own hotbar showing under every horse, and a crash that was this session rebuilding common.jar underneath a running client. Both cost real time to rule out, and both are now written down (coding notes) because the second one looks exactly like a packaging bug and would be chased again.

What is not done is the playing. The Equine Research Shelf has never been placed, the Getting Started tab has never been drawn, and thirty-one checklist hooks are proven reachable rather than proven correct. Those are gaps 143, 146 and 148, and they are the first thing the next session should ask about rather than build past.

A checklist that teaches the mod, and a collection that makes you want to find one more horse.

The mod now says what there is to do and keeps score. Thirty tasks under the Getting Started prose, in the order somebody learning would meet them, and every unticked one carries a line saying what to actually do — a player staring at an empty box wants an instruction, not a title.

It gates nothing, deliberately. Nothing in the mod asks whether a task is done; somebody who reads the wiki and builds a shelf on their first day ticks two boxes at once and is not stopped. The moment a checklist gates content it stops being advice and starts being homework.

The rule that made it real work

A checklist with an item nobody can tick is worse than a shorter checklist: it reads as a bug and the player cannot tell which one is broken. So every task exists only once something completes it — which meant hooks in thirteen files, each one line, next to whatever event already existed. The enum is in common/ and the hooks are not, so nothing but a sweep can prove they all connect; tools/check-progress-tasks.mjs is that sweep and it fails by name.

It also caught its own bug immediately. Written through a shell heredoc, the check's \b word boundary arrived in the file as a literal backspace, so the regex matched nothing and it cheerfully reported all thirty tasks unwired. A check that silently matches nothing is worse than no check; it is a plain indexOf now, with a comment saying why.

The collection was half-built already

The Alleles tab is every allele in the mod with the unmet ones under question marks, and most of what it needed already existed: the gene database has recorded seen tokens per gene for a long time. What it could not do is record a baseline allele, because an entry in that book is what knows() tests, and creating one off an ordinary horse would have unlocked every gene in the mod.

So the collection is a second set beside the book — a flat geneKey|token per player — and the distinction is the interesting part. Discovering a gene is a gameplay gate; collecting an allele is a record. A plain wild-type allele belongs absolutely in the second and must never be in the first. Sharing one map would have made one of those two behaviours quietly become the other.

And the shelf stopped looking like a different mod

Reported: the craft page's icons overlapped its text and the whole thing looked rough. Both halves were the same mistake — placing things by eye. The list ran to y=96 and the slots sat at y=40, straight through it.

The layout constants now live on the menu, because that is where addSlot needs them, and the screen reads every one; a slot and the well drawn under it can no longer disagree. And the window is drawn in vanilla's idiom rather than the browser's: mid-grey face, white highlight up and left, dark shadow down and right, slots with the bevel inverted, dark text. Drawn, not blitted — the thing that reads as Minecraft is the bevel, not the texture, and four fills mean the window is whatever size the layout needs with no art to keep in step.

Two smaller things while in there. The shelf's item was the classic 26.1.2 trap: its model was in models/item/, which this version ignores in favour of items/, so it drew the missing texture. And an unattached signed transfer paper now falls back to the blank paper's model through a minecraft:condition on the deed component, instead of rendering nothing.

The spawn screen ran out of column, and the fix was to stop using it.

Gap 139 was written the day the right-hand column was widened, saying in as many words that the top and bottom groups would still meet on a short window. They did, and it was reported: “Clear genes” and “Make egg” overlapping.

The owner’s fix is better than the two I had written down. Both of mine (scroll the column, split the column) treated the column as the place buttons go; moving Spawn under the preview asks why the one button that does the thing was queued up with Copy, Paste and Cancel in the first place. The strip between the preview panel and the bottom of the window was empty and always had been. So the bottom group is three rows now instead of four, which is 24 pixels the column no longer has to find.

And this time the threshold is measured rather than asserted. The top group ends at 208 and the bottom starts at height − 74; they clear at 290, and once rightStep() squeezes to its 20-pixel floor they clear at 270 — a 1080p screen at GUI scale 4, which is where the report came from. Below about 258 they still meet, because seven 20-high buttons need 140 pixels even touching. That is written down rather than claimed fixed.

A page that opens first, a copy that takes a minute, and a browser that remembers.

The mod had no way in. Everything it does is documented somewhere on this wiki, which is no help at all to somebody who has just installed it and is standing in a field looking at a horse. So the browser has a Getting Started tab, it is the first thing that opens on a fresh install, and it is the first tab in the strip.

Thirteen sections, in the order a player actually needs them: tame one, notice that you need a mare and a stallion, feed them both a golden carrot, take a gene off a horse with a book, file it on a shelf, buy one from the horseman if you would rather, splice it into a foal, and repeat until the paddock looks the way you pictured. Then the horse dimension, because a door made of hay is worth knowing about.

The pictures are entities and items, not art. Item stacks go through the item renderer and the horseman and the cowboy are built as real entities and drawn the way the roster's horses are - never added to a level, made once, kept. Nothing has to be exported, nothing goes stale when a texture changes, and a resource pack the player has on is reflected. That matters most for the case the page exists for: recognising a villager you have never seen.

The flavour is not decoration

“Some horses are magic” and “breeds belong to biomes” are the two facts that make somebody go and look at a horse rather than walk past it, and both read better as an aside than as a heading. The horseman and the cowboy are written as a pair who built their posts side by side and are, the villages say, married - which is flavour the code half-suggested already, since the two workstations share a texture set on purpose.

One number I did not write down

The brief said a gene carrot gives “a 25% chance of getting that gene”. That is not what the code does. A Known Gene Splice sets the fed parent's gamete outright, so that parent passes the gene on with certainty; what is uncertain is whether the foal shows it, which depends on the other parent and on whether the gene needs two copies. For a recessive against an unrelated mate the visible rate lands near a quarter, which is very likely where the number came from. The tab was written to the code rather than to the number, and gap 145 puts the choice back where it belongs: a flat 25% roll is easier to explain and makes the carrot a gamble, at the cost of the one real virtue the current design has, which is that you can plan a breeding around it.

Copying costs time now

A shelf copy is one iron ingot's smelt - 200 ticks - per rarity tier, so ten seconds for a common gene and a minute for a mythic one. The progress rides on a ContainerData, vanilla's furnace pattern, because the menu already syncs those every tick and a progress bar that lags is worse than no bar. The work is ticked per open menu rather than per block, which is what lets two players at one shelf each copy their own gene onto their own book.

And the browser stays where you left it

It was a fresh Screen every time the key was pressed, so it forgot everything the moment you closed it - you came back to the top of the first tab however deep you had been reading. The remembered position is now static state, which is to say per session: the tab, both scroll positions, both search boxes, the sort, the selections. Only the parts that name a horse are dropped, and only on disconnect, so a UUID from one save can never select something in another.

Stables in mountains

Reported against 0.3.2. The three stables were beard_thin, which lays a skirt of material under a piece without clearing what is above it - so a rigid 86-block building projected onto a slope comes out with one end inside the hill. They carve their whole bounding box now. What is not fixed is the other half of the cause: meadow and savanna_plateau are in the biome tag and are mountains in all but name, and no terrain adaptation makes a flat-bottomed stable sit well on a 40-block slope (gap 147).

A bookshelf that remembers genes, and a portal that finally notices its frame.

The Equine Research Shelf is the other half of the paper loop. A book on a horse gives you a paper for a gene it carries, at random; the shelf is where a paper stops being scarce. File one and that shelf will copy it onto blank books for as long as the original stays in it. Take the original back and the copying stops.

What makes it worth having is that it can be emptied. A one-time unlock would have been simpler and would have made knowledge a flag on a player; a shelf is a thing in the world that somebody could walk into and read, and that you can carry to a friend. Breaking it gives every paper back, deliberately - the alternative is a player never daring to move one.

A set of keys, not a chest

The block entity stores a Set<String> of gene keys. A research paper is completely described by the gene it names, and the shelf holds one of each, so the rule is structural rather than enforced: there is no way to express "two papers for flaxen" in the data, so no code has to remember not to, and a shelf can hold every gene in the mod without an item stack per gene. Keys become items again at exactly two moments - a withdrawal, and the block being removed.

The tabs cost one small discovery: Slot.x and Slot.y are final, so the obvious trick of parking a slot off-screen does not compile. Slot.isActive() is the right mechanism anyway, and better - vanilla consults it for drawing and for hit-testing, which is exactly the pair that must never disagree. A slot you cannot see but can still click is the worst of both.

The portal was never told its frame had gone

Reported plainly: knock a hay bale out of a lit portal and the interior stays. The portal block had no updateShape, so it was only ever removed deliberately. Vanilla's mechanism is the good one and it is barely any code - a portal block that finds itself no longer enclosed returns air, and turning to air is itself a block change, so one broken bale collapses the whole sheet in a cascade and nothing has to know how big the portal was.

Adding it would have broken lighting a portal, and reading vanilla is what caught that. Level.setBlock runs neighbour shape updates unless bit 16 (UPDATE_KNOWN_SHAPE) is set - flag 2 does not set it. Both places that build a portal used flag 2, so each block placed would have asked its neighbours to re-check themselves while the rest of the sheet was still air, and the new rule would have eaten the portal as it was being lit. Vanilla's PortalShape.createPortalBlocks passes 18 for exactly this reason, and now so do both of ours. The bug never existed outside a compiler, which is the useful kind to find.

And the Recipes tab, twice

One row per discovered gene meant the splice carrots outnumbered every other recipe several times over and buried the 27 that are not gene carrots, so the tab gained a two-entry category dropdown defaulting to the small half. Each recipe also gained a line saying what the thing it makes is for, out of the lang file so it is translatable and so a recipe that converts between two items can say which direction it goes - packing a bundle and unpacking one share an output but not a purpose. The gene carrots' line is formulaic, as asked: there are as many of them as there are genes and the only thing that changes is the gene.

The H menu stops crafting, and a book becomes the way a horse tells you something.

"Putting crafting in the H menu breaks the flow of normal Minecraft so much it's confusing people." That is the whole argument and it is a good one: a window bound to a key, that is not a block, that nonetheless crafts, is a fifth thing to learn for no gain. Every recipe on that tab already worked at an ordinary crafting table — both custom ones are registered serialisers with real recipe JSON — so removing it cost nothing but the one thing the grid could uniquely do.

The browser is now a plain Screen. Losing the container was the point rather than a side effect: with no slots there is nothing to drag, nothing to lose on close, and no server menu to keep in step. Seven types went with it — the menu, its recipe class, the menu registry, three payloads and the paper writer — and the key now opens the screen directly instead of asking the server to open a menu. It came to about twenty container-specific references in a 1,400-line file, which was a good deal less than it looked.

One behaviour change was caught by reading vanilla rather than by running it: Screen.isPauseScreen() returns true and AbstractContainerScreen's returns false, so the conversion would have quietly started pausing singleplayer — and a horse you were watching would stop moving whenever you opened its gene list. Overridden back. It is the kind of difference that does not show up in a compiler and does show up as "the mod feels wrong", so gap 141 says plainly that there may be others of that shape.

Recipes, generated rather than queried

The tab that is left is a reference to every recipe the mod adds. Getting the ingredients to draw turned out to be the interesting part: the client has no dependable way to ask for a recipe — data/ is datapack territory and 26.1.2's client-side recipe API hands out display objects rather than recipes, with nothing in api-notes about either. So tools/bake-recipe-reference.mjs reads the real recipe files and writes a flat summary into assets/, where the client reads it straight off the classpath. 26 shapeless, one shaped, and two custom recipes whose inputs live in Java and are described in words instead of drawn wrongly. The trade is written down as gap 142: it describes the recipes this jar ships, not the ones a datapack might have replaced.

A book on a horse

Removing the grid removed the only targeted source of research papers, which would have left the splice chain fed by chest loot alone. The replacement is deliberately not targeted: right-click a horse with a book and you get a paper for one gene that horse carries, chosen at random. Choosing the gene yourself — which is what the old button did — meant a paper was never a discovery, only paperwork once you already knew. Now the horse decides. An entirely ordinary horse has nothing to teach and costs you no book, and discovery is deliberately not required, since reading the paper is the thing that teaches you.

Three smaller ones

The search box was closing the window. Typing "speed" into the gene search hit e and opened the player's inventory. It is a vanilla trap rather than a mistake here: EditBox.keyPressed handles the control keys and returns false for an ordinary letter, because letters arrive separately through charTyped — so the letter falls out of super.keyPressed and the next thing AbstractContainerScreen does is test it against the inventory key. Any screen with a text field and a container behind it has this bug until it says otherwise. A focused box now swallows every key but Escape; the letter still types, because GLFW's character callback is a separate one.

Two mares now say why nothing happened. Vanilla does not know horses have a sex, so a same-sex pair fed breeding carrots goes through the whole courtship and reaches BabyEntitySpawnEvent like any other pair, where the foal was cancelled in silence. The attempt is left exactly as it was — they still try — and it now says so, in one of three lines per sex, to the player who fed them. Light rather than an error: nothing has gone wrong, and a red warning would read as a bug in the mod rather than a fact about horses.

♀ and ♂ after a horse's name, coloured, so sex is readable across a paddock. Appended at render time through RenderNameTagEvent.CanRender and never to the stored name — that name is what the record keeps, what a transfer paper prints and what the rename box shows, and a glyph baked into it would reach all three and then be inherited. A display aid belongs at display time, which is also why the switch is client-side.

A gene says what it does when you point at it — and seventeen of them could not.

Asked for plainly: hovering a gene in the custom spawn egg's list should pop up a blurb describing what it does. The text already existed and already had the right job description — Gene.description() is documented as “one to three plain sentences for the in-game gene browser and tooltips”, with built-ins reading GeneDescriptions and data-driven genes carrying their own blurb.

Except for seventeen of them. The contract says an absent summary resolves to "" and callers should treat that as “no summary available”, which is honest and, on a hover panel, means the gene silently has nothing to say while the one above it does. Coverage was 185 of 202: missing were tiger eye, leopard, PATN1, PATN2, shadowcreature, the ten magic utility loci, and two data-driven genes. All seventeen are written — compressed from each gene's own wiki page rather than invented, which is the only way to write a summary that will not contradict the page it summarises. It is 202 of 202 now, and GeneDescriptionCoverageTest fails by name rather than by count when the next gene arrives without one.

The panel is drawn the way the dropdowns are — fills and text — rather than as a vanilla Tooltip. Two reasons, and the second is the one that decided it: a list row is not a widget on this screen (an unadded row takes its click in mouseClicked), and the browser twin has no vanilla tooltip to mirror. A panel built out of fills exists identically on both sides; a Tooltip would have made the designer's version a reimplementation rather than a copy.

Mirroring it turned up a real difference the two files had been carrying quietly: rowAt() returns a view index in the screen and a gene index in the browser, because the screen's rows carry their own gene and the browser's are bare indices into genes[]. Nothing had ever needed to tell them apart — both are only compared against the same kind of thing — but a panel has to be positioned beside the drawn row, and with a family filter on, the two indices differ. The browser now has a separate rowSlotAt(), named and commented for exactly that, and rowAt() is written in terms of it.

A play session, four reports, and one root cause behind three of them.

0.3.2 launches and makes a world, which was the thing worth knowing. The session that confirmed it then produced four more reports, and reading the log for one of them turned up a fifth nobody had asked about.

The stables generated empty, and the guard that did it was commented as doing something else

“The structures generate, but there are no horses anywhere in the structure.” StablePopulator queues a stable when the chunk holding its StructureStart loads, and guarded that with a comparison between the bounding box's minimum corner and the chunk being loaded — under a comment saying it queued “only the chunk holding the structure's own start”. Those are different tests. A jigsaw start piece is placed at its chunk's minimum block corner and then rotated, and SinglePoolElement.getBoundingBox rotates about BlockPos.ZERO — so three of the four rotations carry the box into the previous chunk in x, z, or both. Only unrotated stables were ever queued. The other three quarters generated the building and stopped.

The guard was not merely wrong, it was unnecessary: getAllStarts() already returns only the starts belonging to that chunk, and StablePopulationData.claim() is what makes population happen once per world. It now dedupes against the queue on the same (structure, corner) key claim() uses, so the two cannot disagree. A stable also logs when it is queued now, not only when it is filled — this bug's entire signature was a missing filled line with nothing to say why, and one line without the other is what tells you which half broke.

Two shipped assumptions, and they are the same assumption

The [Cowboy] and [Horseman] lines were going to chat in a normal install, because DebugAnnounce.ENABLED was a hard-coded true. It was hard-coded for a good reason, written down at the time: it had been !FMLEnvironment.isProduction(), and a whole session went by unable to tell “the gate is shut” from “the code never ran”. The constant was not the mistake. The mistake was that the sentence beside it — this mod is a dev build with one tester and is not shipped — stopped being true and nothing went back to look. It is now a debug.announce server config, on in dev, off in a normal install, switchable in both, which is what you want from a player who has something to report. The startup line that says which way the gate went is kept.

That is the second defect this week from the same root, after the encoder crash that was invisible on the only machine that had ever run the mod. It has a gap of its own now (gap 140), because the sweep has not been done: nothing has gone looking for the other places that assume nobody else is running this.

One hundred and twenty-one errors that meant nothing, every launch

Found by reading their log for the stables line. Genes loads the shipped gene files from its own class initialiser, on purpose; then ModGeneSpecs called loadAndRegister, which re-read the classpath and handed every one of them back to a registry that throws on a taken key. Each throw was logged at ERROR. Nothing was actually broken — the first registration is the one that counts — and that is what makes it worth fixing rather than shrugging at: it trains the log's reader to scroll past a wall of red, and a real collision would be sitting in the middle of it. The classpath half is gone; a drop-in that collides still throws and is still reported. The launch now says how many genes shipped and how many segments the genotype code has, every time, which is the first thing worth knowing from a pasted log.

The spawn screen's right column

“WAY too cramped”, and measurably so: the column was 96px wide with a 2px gutter, and "Spawn (creative only)" alone measures 111px. A vanilla button neither shrinks nor wraps its label, so several were drawn straight over the edges. It is 128 wide with a 4px gutter now, and the breed label fits itself to the button rather than being cut at twelve characters — a number that was a guess at what fitted a 96px button and was wrong in both directions.

The interesting part is what it exposed. Seven buttons grow down from the top of that column and four are pinned to the bottom, and widening the gutter makes them meet sooner. rightStep() now spends the gutter to buy room — but seven 20-high buttons need 140px even touching, and a 1080p screen at GUI scale 4 leaves about 120, so below roughly 290px they still overlap exactly as they always did. That is gap 139 rather than a fix, because the honest answers (scroll the column, or split it) are larger than the change that surfaced the problem. The designer took the same numbers, per the standing rule that those two screens are one screen in two places.

Two releases nobody could play, and a test that was watching the wrong cap.

This one arrived from outside, which is new. The owner handed the jar to friends: 0.2.0 worked, and 0.3.0 and 0.3.1 both disconnected on trying to make a worldInternal Exception: io.netty.handler.codec.EncoderException: Failed to encode packet 'clientbound/minecraft:custom_payload'. The stack, once it arrived, named the line exactly: CoatSyncPayload.java:26, and under it String too big (was 35568 characters, max 32767).

CoatSyncPayload is how a horse’s coat reaches the client — the genotype and the epigenome as text, sent when a player starts tracking the entity. It was writing both with a bare buf.writeUtf(v). That overload caps at 32 767 characters, throws rather than truncating, and a throw inside the encoder takes the connection with it. A full epigenome is 35 304 characters today. So the first wild horse to come into render range ended the session, which on a freshly generated world is roughly immediately. Nothing about it is intermittent; it was simply never seen here, because the trigger is a horse coming into view rather than the world load, and a dev world gets entered and quit a lot.

The uncomfortable part is that a test exists for precisely this. EpigenomeSizeTest was written the last time this threshold was crossed, and it fired then, and it is still green. It was watching SpawnCustomHorsePayload’s stringUtf8(131072) — the one cap in the mod with a number written beside it. An implicit cap has no number to watch. The test could only ever have guarded the site that had already been thought about, which is the site least likely to be wrong.

So the fix is not the raised cap, it is GenomeCodeCodecs: two constants and two StreamCodecs that are now the only way a genome code is allowed onto the wire. Four call sites moved onto them — the coat packet, StoredGenome (which was on ByteBufCodecs.STRING_UTF8, i.e. the same 32 767, on a component that rides every inventory sync, so a preset spawn egg or a filled genome jar would have kicked whoever picked one up), and the genotype fields on the roster and spawn payloads. The test now guards each cap at half budget rather than one of them.

The genotype guard failed on its first run, which is the entire argument for having written it: 5612 characters against the 8192 those payloads had been declaring — two thirds spent, on a number that grows with every gene registered, and nothing anywhere would have said so until it did the same thing the epigenome had just done. That cap is 32 768 now.

Shipped as 0.3.2, and both 0.3.0 and 0.3.1 should be treated as withdrawn — there is no way to play them. A world made under either is fine and opens normally, since the fault was in what the server sent and never in what it saved. The size problem underneath is untouched and is still gap 66: 35 KB of text per horse per tracking client is a bandwidth cost nobody has budgeted, and two paths — the horse record and the transfer deed, both of which cross the wire as NBT — still have a 65 535-byte ceiling that no code here declares and no test watches. A little over half spent.

The PATH mask — a shape you drew, stored as points rather than pixels.

The session was paused here, mid-§10, to deal with something else

Stopped at a clean seam rather than abandoned: both masks are complete on both sides, tested, parity-gated, documented, committed and pushed, the build is green and the tree is clean. Nothing is half-written and nothing is waiting to be reverted.

Resume from the roadmap's paused note, which says what the next job is and what question is waiting on the owner first.

The other half of §10, and the piece the drawing tools have to be written against. Every other mask in the vocabulary says what a shape is made of — a lattice of spots, a band of this width, noise at that scale — and lets the geometry and the horse's own numbers decide where it lands. That is the right tool for a marking with a rule behind it, which is nearly all of them. A lightning bolt, a crescent, a brand, a curl on one particular shoulder has no rule behind it: it is a shape, and the only honest way to describe it is to give its outline.

Control points in a plane, stroked as a line or filled as an outline, extruded through the horse. plane picks the two axes — side is the flank and appears on both of them, which is what a marking drawn on a side view should do. space: "body" normalises the points over the horse, so a shape drawn for the adult lands in the same anatomical place on the foal. Full documentation, which is the source of truth, is on the gene file format.

Points rather than a baked stencil was the owner's call from the previous session, and building it confirmed the reasoning: the shape stays procedural, stays legible in the JSON, follows a resized horse, and is the same arithmetic in Java and in the browser. It is the first parameter kind in the format that is neither a number nor a word, so SpecSchema.Kind gained POINTS and the parser gained its errors.

Catmull-Rom, not Bezier, and the reason is the editor rather than the curve. A Bezier's handles do not sit on the curve; Catmull-Rom passes through every control point. Since the creator is going to draw those points as draggable handles, a curve that merely got near them would make the editor lie about its own data. The ends hold their tangent by duplicating the first and last point rather than extrapolating a phantom one, which is what stops a smoothed path from overshooting off the horse and back.

Two things found by building it that were not the job

And one mistake worth keeping

The first PATH examples were written at v around 0.7 — "high on the flank". Rendered, they were floating above the horse's back. body space normalises over the whole-horse box and that box runs to the ear tips, so on the adult mesh 0.33 is the underline and 0.62 is the spine. It is the same trap WhitePattern.toplineHeight exists because of, met again from the other direction: measuring against the whole box is correct for a mask whose author is drawing on the whole horse, it is simply not anatomical. Moved to the barrel, and written into the format page as its own warning rather than left for the next person.

Also worth recording: PathMaskTest failed twice on first run and both were test bugs, not code. One probe point sat exactly on the Catmull-Rom midpoint of a symmetric span, so "the curve should leave the chord" was false at the one place it was checked; the other measured a stroke's softness fade and called it an overshoot. Both are rewritten to sample the spline directly, where neither confound exists.

:common:test not run in full (owner's standing rule). Run: PathMaskTest (13), FractalMaskTest, both coat goldens (neither moved — no registered gene uses either new mask), GeneSpecParserTest, GeneFilesTest, SpecGeneTest, CreatorMetadataRoundTripTest, :neoforge-26.1.2:build, and every wiki and parity check. Re-baked: spec fixtures, creator assets, designer wasm, search index.

A gene can be a fractal — and a stale roadmap sent the session somewhere else first.

The session opened by picking the top item off the roadmap and found two thirds of it already shipped. §1.1 listed three white-pattern calibration defects; the entry for EDNRB’s frame band still cited BAND_LO / BAND_HI, which have not existed since the barrel-and-neck rewrite, and the entry for the cover knobs described a threshold-on-a-bell that both EdnrbGene.paintFrame and TobianoGene stopped being two sessions ago when they became score-sort-and-cut-at-a-quantile. Both had been fixed and written up elsewhere, and neither had been deleted from here, which is the failure mode this page and the roadmap’s own header warn about. Both rows are gone now.

The third row went the other way and was closed by decision. Medicine hat was measured before it was argued about: over eight seeds a kit=SB1/SB1 horse comes out ears 0% white, the rear 30% of the head 0.4% — the cap is genuinely there — against a 99.6% white mane and a 100% white neck, so rendered it reads as two dark ears and a smudge behind them. The owner declined to model it: “it will roll randomly, I do not care if it does or does not”, and “the hat should not be a unique gene”. Nothing retains for it and nothing rolls for it. Gap 29 is now a record of that call rather than a defect.

The FRACTAL mask

With §1 closed the owner picked §10, the visual marking designer, and within it the fractal masks before the drawing tools — the roadmap names them as what peacock and laced tobiano both want, they live in common/, and the drawing tools sit on top of the mask vocabulary rather than beside it. FRACTAL joins the mask vocabulary (GeneSpec.MaskType.values() counts it): value noise summed over octaves with lacunarity, gain and a domain warp, and a shape that reads the summed field as fbm, ridged or billow. It is documented on the gene file format, which is the source of truth for it.

Two things in it are deliberately not the textbook recipe, and the first is the reason the mask is worth a write-up at all.

FractalMaskTest pins both, and measures the naive normalisation alongside so the claim in the comment is not taken on trust: six octaves of the textbook form come out visibly narrower, and the shipped form does not.

Two things looking at it changed

All four contract files moved (SpecSchema, SpecPainter, the creator’s schema.js and spec-engine.js), plus gene-format.html. No new primitive was needed in js/noise.js: the fractal sum is built out of value, which is already ported.

The parity gate was proved to reach it rather than assumed to. Two FRACTAL layers went into example-genes/prismatic.json — a ridged one and a warped fbm one, so both branches are exercised — and then the JS divisor was deliberately broken and check-parity.mjs watched to go red before being restored. A fixture that never runs the new code is green by definition, which is exactly how the UV swap hid for a day.

Settled for the drawing tools, which are not built: a drawn shape will become vector paths in body space — a handful of curve control points in body units — rather than a baked bitmap stencil. It stays procedural, stays small in the gene file, survives a resized horse, and the same maths runs in Java and in the browser. That makes the tool a curve editor rather than a brush, and it wants a PATH-family mask before any UI is written.

Also settled, by reading the code rather than by asking: the roadmap entry that said to decide whether the pipeline needed a scalar-expression channel “before building the UI” was already answered. GeneSpec.Knob plus Value.FromKnob lets any mask parameter be driven by a per-copy stored scalar over a declared range, and Value.PerDose gives a per-dose triple beside it. “Expression scales parts” is expressible today; what is missing is that the creator never shows the channel.

:common:test was not run in full (owner’s standing rule). Run: FractalMaskTest, both coat goldens (neither moved — no registered gene uses the new mask), GeneSpecParserTest, GeneFilesTest, :neoforge-26.1.2:build, check-parity.mjs, check-links.mjs, check-gene-tabs.mjs. Re-baked: spec fixtures, creator assets, the designer wasm, the search index. Unplayed: nothing new in game — no shipped gene uses FRACTAL yet, so there is nothing to look at until a gene does.

Twenty-five loci get the science they were built from — and three of them argue back.

The health layer and most of the natural colour genes have been carrying their real-world basis in a Java doc comment and nowhere a reader could reach it. Gap 89 had that written down as 25 pages missing from the science view, with a check (wiki/tools/check-gene-tabs.mjs) holding the list so it could not grow. The owner supplied source material for all 25. Twenty-three are now written; the backlog is 2.

Each tab is the same shape, because the source files were: the locus and the variant with its coordinates, the mechanism, the real inheritance table, breed-by-breed frequency where anyone has measured it, and an explicit three-way split between what is established, what is strong inference, and what is not supported. That last section is the one worth having — a page that only says what is known reads as more certain than the field is, and several of these loci have a famous unsupported claim attached that a player is more likely to arrive holding than the real one.

The point was never the citations

It was the fourth thing each page now does: state where the mod and the literature disagree, and which way. Until both halves were on one page there was nowhere that comparison could happen, and the risk was never that a divergence was wrong — it was that it was undocumented and would be “corrected” one day by somebody who had read only one side. Three turned out to be substantive enough to earn a gap of their own.

SCN4A kills a horse that lives. The mod makes the HYPP homozygote lethal at birth. A real H/H horse survives, is more severely affected than a heterozygote, and is ineligible for AQHA registration — a studbook rule, which has no analogue in a game. This is the mod’s largest flat contradiction of fact on the health layer, and it is a design call rather than a misreading: HYPP is the one locus where the player makes the mistake with their eyes open, because an H/N horse is visibly unwell and can still be excellent on every other axis. Making the homozygote a cliff is what turns that temptation into a decision. If a severity axis ever lands, H/H becoming much worse but survivable is the fix — and it would cost the gene its cliff.

HMGA2 pays the pony allele in the wrong direction. HEALTH_PER_P is +2.0, on the reasoning that a pony is a hardy horse rather than a broken one and that a locus whose only effect is “worse” is one a player fixes once and never thinks about again. Sound design. The only validated health association at that locus is higher basal insulin and, through it, laminitis risk — so the sign is inverted against the evidence. A metabolic or laminitis system would give the real effect somewhere honest to live; the diet gene is the nearest existing hook.

CKM is a speed locus the literature does not support. Its only equine performance claim came from a 2010 Thoroughbred candidate-gene study, failed that same paper’s own independent replication sample — the combined analysis retained COX4I2, not CKM — and was explicitly non-significant in a later Quarter Horse study. The biochemistry is real and the genotype-to-phenotype link is not. The trade is deliberate: the mod wants three legible speed genes of different sizes so a breeding programme has a gradient with a long tail, and CKM is the smallest of the three precisely because it is the least defensible. It also names its alleles T/C where the marker in the paper is G>A.

A fourth is drift rather than a decision. MATP makes pearl commoner than cream1/22 against 1/30 — where the real ordering is emphatic in the other direction: cream is in 25 of 28 surveyed breeds and reaches 0.42 in Lusitanos, pearl is in 7 and tops out near 0.12 in Iberians. The cause is visible in the source comment: those are “the same numbers the two old genes carried”, from when cream and pearl were separate loci tuned independently. Merging them made the two figures directly comparable for the first time, and nobody had looked.

Everything else that diverges, diverges for one reason

The missing age model, showing through on gene after gene. Cerebellar abiotrophy arrives at six weeks and worsens for months; SCID gives a foal a fortnight of perfect health on its dam’s borrowed antibodies; HERDA is a horse that is fine until you put a saddle on it at two; naked foal syndrome runs from weeks to three years; champagne foals are born blue-eyed and turn amber. All of them are charged flat, from birth. Written out across five pages it reads less like five separate approximations and more like one missing system, which is what it is.

Two pages needed a different kind of honesty. Diet has no real locus behind it at all, so its tab is a real-horse feeding guide judged against the mod’s own item list — and the finding worth having is that Dveg and Dwht are the only narrow diets a real horse could survive on, while hay, the one thing the mod treats as ordinary, is the only item on the whole list a horse actually lives on. SHOX models one allele where the real locus has two overlapping deletions whose compound heterozygote is also affected, which the mod cannot currently represent.

The last two, and what they cost to be sure of

Tiger eye and rabicano came last, because their first source files both arrived holding a byte-identical copy of the mushroom write-up — all three hashed the same. Re-supplied, they turned out to be the two most interesting pages in the batch, for opposite reasons.

Tiger eye is the mod’s cleanest match to its source and asserts one thing the source will not. TE1/TE1 and TE1/TE2 give amber, carriers show nothing, the coat is untouched, TE2 is rarer than TE1 — all of that is exactly right, including the mod setting TE2’s frequency at 0.6× TE1’s against an observed 20-to-8 split. But the mod gives TE2/TE2 its own paler yellow iris, and the literature has one documented TE2 homozygote — a palomino whose eye phenotype could not be separated from the cream allele it also carried. The extrapolation is reasonable and it is an extrapolation.

Rabicano has no gene at all. No causal variant, no locus symbol, no validated inheritance model, no test — the strongest lead is a 1.7 Mb haplotype near KITLG on ECA28, about 160 kb from the transcription start site. The Rb symbol on the coding tab is this mod’s invention, which is now written down where somebody looking for the paper will find it instead. What is striking is that the mod already had the right structure: a dominant allele whose expression is a per-horse roll that can come out at nothing. That is not a compromise between two readings of the evidence, it is the only shape that reproduces both halves of what the literature reports — apparent dominance in pedigrees, and expression ranging down to invisible.

What the closure created

Gap 89 is closed and its roadmap section has been replaced rather than deleted, because finishing the 25 made a second gap visible underneath. Measured against the new tabs, which run 1,200–2,300 words, 17 of the 23 that already existed read as stubs: MC1R is 41 words for the foundational red/black locus, EDNRB is 62 for the frame-overo lethal that four new pages cite as their cautionary example, and MITF is 90 for SW1 — the common splash allele, whose PAX3 twin now has 2,024. And 20 of the 48 natural tabs name no breed at all; some correctly, but leopard complex without Appaloosa and ACAN without the Miniature Horse are the same hole one level down. It is written up, tiered by which pages the new tabs cite and cannot get an answer from, on the roadmap.

The other half of the audit was the tabs that already existed. Measured against the new ones, which run 1,200–2,300 words, the older science tabs are thin: MC1R is 41 words for the foundational red/black locus, EDNRB is 62 for the frame-overo lethal that four new pages cite as the cautionary example, and MITF is 90 for SW1 — the common splash allele, whose PAX3 twin now has 2,024. And 20 of the 48 natural science tabs name no breed at all. Some of those are universal loci where that is correct (shade, sex), but leopard complex without Appaloosa, ACAN without the Miniature Horse and megaesophagus without the Friesian are gaps of the same kind gap 89 described, one level down.

Some players would rather their saddles fit.

An owner's call, and a small one to build: the size loci now answer to a server setting. body.size, true by default; false and every horse renders and collides at 1.0 whatever its alleles say.

The reason it is the one body trait with a switch is the sentence that normally sells it. Speed, health and jump are numbers; Attributes.SCALE is not, because vanilla scales the hitbox from it as well as the model. So a Falabella and a Percheron meet a saddle, a lead, an arrow and a two-block doorway in genuinely different places, and that is a feature in most worlds and a nuisance in some. Server-side for the same reason: a client that disagreed with the server about a horse's size would be aiming at a horse that is not there.

The gate is one expression, in HorseRecords.applyTraitsToEntity — the single place the resolved scale reaches the game. Everything upstream is untouched: the size genes are registered, drawn from the same founder tables, inherited and reported identically, and Traits.scale() still resolves to what the alleles say, so the info panel, the family tree, the browser and the inspect paper all keep showing the real number. Two details were deliberate rather than incidental. It writes 1.0 rather than skipping the write, so flipping the setting off and reloading actually shrinks the big horses back instead of leaving them frozen large. And the other route to SCALE — an effects attribute verb naming scale, which no shipped gene uses but the table has always allowed — answers to the same setting in both applyAttribute and clearAttributes, so a modifier already standing comes off on the next reconcile rather than being stranded on the horse by a setting change.

Two things came out of writing it down that were not in the ask. A breed's height target rides on the same writestats.height in hands becomes a StatAxis.SCALE band, which is the entire reason a Falabella looks like one — so with the setting off every breed in the mod stands the same height. That is the honest consequence and not a bug, but it costs more than "tack fits" and nothing in game says so (gap 132, with the compress-rather-than-flatten alternative written out). And both gene testers scale their preview by the genotype and cannot know whether the server will honour it — the designer has no server to ask at all (gap 133).

The thing this cost, now on api-notes: a ModConfig.Type.SERVER file is written to the instance's config/<modid>-server.toml, and a world's serverconfig/ folder is an optional override of that path rather than where the file is made. The verification entry said the wrong one until the generated file was actually looked at. Checks: §0-AX, and the first of them is that the default is unchanged — a mistake here flattens every world silently.

Ask the neck what shape it is — and six gene layers that had never painted a texel.

Two defects, both picked for being fixable and provable without opening the game.

The coat cache was unbounded, and no reload touched it

Gap 130, opened by the previous session’s resolution analysis and closed here. GeneticCoatTextureFactory filled two ConcurrentHashMaps with computeIfAbsent and never removed an entry, so a client accumulated one permanently registered DynamicTexture per distinct genome it had ever drawn — adult and foal separately, plus a mask for every glowing one — and released them only on logging out. And there was no client reload listener anywhere in the mod, so editing the gradient and pressing F3+T appeared to do nothing at all.

The fix that matters is the shape of the cache, not its size. TexelBudgetCache lives in common/ because the interesting part is a policy rather than a texture: an LRU whose capacity is texels rather than entries. One entry is a whole sheet, so what has to stay bounded as SHEET_SIZE changes is the memory, not the horse count — an entry-count LRU tuned today would quietly be sixteen times the memory at four times the sheet, which is precisely the trap the resolution work is walking toward. A 64-entry floor sits under the budget so the cache can never come out smaller than a frame, which would not merely thrash but could release a texture an already-submitted draw call still wanted. Every removal, eviction and clear() alike, goes through one hook that releases the texture and drops the reverse-map entry. CoatAssetReload is the listener; the event is AddClientReloadListenersEvent, not the RegisterClientReloadListenersEvent the gap guessed at, which is now on api-notes along with the reshaped PreparableReloadListener.reload signature.

Worth recording: the unit test caught an off-by-one in the eviction loop on its first run — it double-counted removals and stopped one entry early.

The neck’s geometry was written out by hand in eleven files, and six of them were dead

Gap 103. A Y band across the adult neck is a collar: it wraps the crest and the throat alike, which is the bug that put goth’s hood on the underside of the neck and had to be reported twice. The workaround was a WAVES mask with a sawtooth longer than the horse (wavelength 90) at an amplitude of 77.94, so the displacement never wrapped and the band came out as a plane tilted to 2A/λ = tan(60°) — sixty because the neck’s own long axis is sixty off horizontal.

The gap said five gene files. It was eleven — and the audit found why it had spread so quietly. amplitude is in whatever space says while wavelength never is, so six of the eleven had copied the pair into space: "part", where 77.94 displaces the band twenty to forty normalised spans off the horse. Measured over all 1280 neck texels, beetle pearl, diamond scutes, duskfall speckle, ink scroll, nebula points and wyrmwood sigils selected exactly zero, on every horse and at every seed. They loaded, they registered, they had icons, and the layer was inert. Nothing could have said so: a mask cannot fail, it can only select nothing.

HorseSkinGeometry.local is the replacement — the inverse of posed’s frame change, giving a body point as fractions along the edges of the part’s actual cuboid, read by AXIS, WAVES and RAMP as space: "local". On the neck, axis: "X" is across its depth (low is the crest, which is the face the mane box sits on) and axis: "Y" is along its length, so a band there is a collar square to the neck rather than to the world — which is what ink scroll and wyrmwood sigils described all along. Details on body space and the gene format.

The old trick turns out to be exactly that coordinate. The fit is affine with a maximum residual of 0.0002 over the whole neck, which is what made the conversion safe rather than hopeful: the five genes whose bands did land converted with a texel-for-texel identical selection, and four of them — cosmic, shark, stars, suit — came back byte-identical in the bake golden. Only goth moved, because its $depth knob was in body units and is now a fraction; the range of hoods is unchanged and an individual horse redraws. Thirteen golden rows moved in total, which is exactly the six revived genes plus goth and nothing else.

What used to be two constants is now assertions. PartLocalFrameTest pins that local equals part space on an unpitched part, differs sharply on the pitched neck, and that the old sixty-degree coordinate is still affine in the new one with the slope the neck’s 7-unit depth implies — and it establishes “the crest” by asking where the mane box is rather than by repeating a number, so it survives the neck being reproportioned. Beside it, GeneFilesTest now refuses any WAVES amplitude far above 1 in a normalised space, which is the general form of the mistake all six made; it was checked by breaking a gene on purpose and watching it fire.

The thing worth carrying forward

Both defects had the same shape and it is worth naming: a failure that produces no output at all is invisible to every test that hashes output. The six dead layers were in the bake golden the whole time, hashing consistently, because a mask that selects nothing is stable. What found them was measuring where a gene paints, per part, rather than whether its bytes moved — and that measurement took twenty lines. The same reasoning is why the cache defect needed a texel budget rather than an entry count: both are cases where the number that has to stay true is not the number the code was counting.

The sheet size turned out to be a variable in disguise, eight mods got a verdict, and the whistles got a design.

A documentation session — no Java changed, no gene moved, no golden touched. Three questions asked, and the first one had a better answer than expected.

Scaling the coat sheet is mostly already done

The question was “what breaks at half, eight, sixteen, thirty-two and sixty-four times the current sheet?”. The premise of the question was that resolution is baked in everywhere. It is not. HorseSkinGeometry.faceMapsOf multiplies vanilla’s own 64-space texOffs by TEXELS_PER_UNIT, and HdHorseModel divides by the reciprocal through a per-cube texScale, so the UV layout has always been a function of k and nobody has ever typed a texel coordinate into the geometry tables. CoatBuildContext.size() already exists as the seam a gene should read.

So the answer is a short list rather than a rewrite. The absolute references, all of them:

No gene indexes on texel coordinates at all. Every gene takes (px, py) from forEachTexel as an address to write to and does its arithmetic on the BodyPoint beside it; there is no per-texel hash, no neighbour step, no threshold on px anywhere in genetics/genes/. That is the body-space decision paying off years later, and it is why raising the resolution is supersampling rather than reinterpretation. The genes that are coupled to k are coupled through the three shared sites above, plus the soft one nothing can detect: constants tuned by eye at the current size, which the goldens cannot isolate because they all move at once.

Two things also worth having found. Storage is free — no coat is ever written to disk, so resolution costs nothing in save size, jar size or bandwidth. And OptiFine is irrelevant: it, ETF and EMF solve the resource-pack selection problem, which a mod that generates its texture does not have.

The point of the exercise: paint on the player’s own pack

The resolution work is the prerequisite for the feature the owner actually wants — read whatever white horse the player’s resource pack ships and paint the genome on that, at the pack’s own resolution. Checked, not assumed: vanilla 26.1.2 ships horse_white.png and horse_white_baby.png at 64² under exactly the two paths this mod shadows in its own namespace, and the ResourceManager the factory already uses resolves through the active pack stack. So it costs a namespace change, plus an un-mirroring pass to give the adult sheet’s four legs and two ears their own patches — the foal needs none, because vanilla’s baby model already gives each leg a distinct patch — plus making the sheet size a variable. Two tiers, no fallback chain: a pack that ships our namespaced template gets it used verbatim, otherwise we derive from minecraft:.

The undecided part is left undecided on the page: a pack whose “white” horse is cream would tint every coat in the game, because the composite is a multiply that assumes the template is shading and nothing else. Normalise it, accept it as the pack’s intent, or gate the feature — owner’s call, flagged rather than guessed.

All of it is on the new texture resolution page, which is now the single source of truth for the subject.

Eight mods, and the finding that the blocker is usually a version

Compatibility had the accidental half — what happens when two mods meet and neither knows about the other. It now has the deliberate half: what a patch would actually have to do. It decomposes into four seams (entity identity, the mesh and its UVs, the renderer, gameplay overlap), and the recurring finding across SWEM, Icy’s Better Horses, Horseman, Horse Powered, Horse Combat Controls, AstikorCarts, armour-only mods and the Fresh Animations pack is that the overlap is nearly always one subsystem and the blocker is nearly always a version rather than a technical conflict. Of the eight, three are co-installable with 26.1.2 today.

Two ideas came out of it worth more than the verdicts. User-written model remaps: HorseSkinGeometry is already a table of pivots, boxes and 64-space texOffs, and an OptiFine/EMF .jem is very nearly the same table — so describing someone else’s horse mesh is a file, not a fork, and every gene and both editors would work on it unchanged because no gene has ever known what mesh it was painting. What it cannot express is non-box geometry, which is exactly what blocks the SWEM patch. Gaits: the mod resolves speed, jump and health and then says nothing about movement, which throws away a real heritable breed-defining trait it already has the inputs for. A GaitContribution resolving in common/ to a named gait plus a tempo, with a thin per-mod animation mapping on the NeoForge side, would make the gene work shared across every detailed-leg mod instead of written once per mod.

Callable horses: bind the whistle, not the player

The Callable Horses mod binds one personal horse per player to a keybind. Taking the feature, not the shape — a player here has a stable, and picking one favourite is the choice this mod exists to make hard. The binding goes on the item: ModDataComponents.BOUND_HORSE already exists on the stall sign and its javadoc already anticipated being reused, so a bound whistle is that component on a whistle, bound by right-clicking the horse the way every other item in this mod claims one. Many horses, many whistles, and the component’s name field puts the horse’s name on the tooltip for free.

Keybinds: the item is the interface. The mod has exactly one production keybind (H, the browser) and everything else is an item and a click. Callable Horses needs two keys because it has nowhere to put the binding; we do not. At most one optional convenience key, registered the way HorseBrowserKeyBindings does it.

Writing it up settled three questions the roadmap was already carrying, which is the part worth recording. Bond gates the bind — a whistle that binds to a barely-tamed horse makes bond meaningless. Path if it can, teleport if it cannot, using the range BondFollowGoal already walks. And the three whistle tiers become loaded chunk / any distance / any dimension, which finally gives the echo whistle an identity that is not just a bigger number, and puts the configuration in the crafting ladder rather than in a config file. Against the tickets, the split is clean: a whistle brings the horse to you, a ticket sends it to its stall — the interdimensional ticket and the echo whistle being the same idea pointed opposite ways.

Declined deliberately: respawning a dead horse. A horse here is a genome and a pedigree and the breeding layer rests on death being real. The one genuine unknown is finding an entity in an unloaded chunk — HorseRoster already documents that whereabouts are entity facts — and the answer is an index in the per-world horse database, which the browser, the stall records and the tickets all want as well. So it is shared infrastructure to be scoped on its own, not whistle work.

Found on the way

Gap 130, in two halves and both present-day defects at the current resolution. The generated coat textures are never released until logout — clear() does exactly the right thing and its only caller is the logout handler — so a session accumulates one registered DynamicTexture per genome it has ever drawn. Harmless now, and the first thing that stops the sheet ever getting bigger. And there is no client reload listener anywhere in the mod, so F3+T leaves every baked coat on screen and leaves the templates and the LutSet holding the old pack’s pixels — which means editing the gradient and reloading appears to do nothing, and which is a hard prerequisite for the resource-pack feature above.

Nothing was built, so nothing new is waiting in verification. The compatibility verdicts are read off the other mods’ public listings rather than their source, and the two that matter most — whether Horse Powered’s mob capture preserves data attachments, and whether Icy’s replaces the renderer for its breed coats — are exactly what a listing does not answer. They are stated on the page as checks, not conclusions.

The random number generator was never mixing its seed, and two goldens had to move to fix it.

Eight gaps, taken as four pieces of work. The largest one changed every horse in the mod.

Three gaps, one root cause

SeededRng wrapped java.util.Random, whose seed handling is a single XOR against a constant. That is not a mix, and three separate entries on the gaps page turned out to be the same fact seen from different angles:

The seed now goes through splitmix64's finaliser before Random sees it, and each gene draws from its own generator namespaced by its key. The first measurement becomes 0.0002 to 0.9999, mean 0.5043; neighbour covariance drops from about 0.013 to about 0.001.

The convincing evidence was a deletion. MagicBodyStatGenesTest carried its own splitmix64 finaliser as a local workaround, with a comment saying the real correlation was the gaps page's problem rather than the test's. That workaround was removed rather than left to double-mix, and the file passes without it - which is the fix demonstrating itself, and leaves the test as a tripwire if anyone takes the scramble back out.

What it cost, and the check that nearly went wrong

It changed every horse ever generated, exactly as the gap said it would: 883 of 1056 lines of the pipeline golden and 218 of 623 of the bake golden, plus 154 gene icons, the designer wasm and the spec fixtures. The check that mattered was not the count but the membership: the genes that moved are exactly the genes that store epigenetics, with acan and b4galt7 correctly untouched.

Regenerating the pipeline golden turned out to be impossible as documented (gap 128). Its javadoc said "run the test, copy the file it writes"; it only wrote that file when the golden was missing, never on a mismatch - so build/coat-golden.txt held an older run's output, and copying it back reinstated a stale golden while the test went green. The tell was a file that had "moved by zero lines" while the test was still failing, which cannot both be true.

Two tests then failed for no real reason, and both were the fragility gap 47 describes - a claim about a distribution pinned to specific draws. Neither was widened without measuring first: the splash dose response was comparing single horses at seed 3, which after the reseeding became the single unluckiest draw in sixty (+0.08 where the mean is +0.43), and the grey spread was a range over 8 seeds - the statistic most sensitive to a small sample, since it depends only on the two extremes. Over 24 seeds it is about 45 against a threshold of 20. Both now state their claim over a distribution.

The last ungated mirror

Gap 100 is closed and the creator's geometry port is inside the parity gate. posed() existed twice - in HorseSkinGeometry and again in model3d.js's emitPart - in two languages, in two files that were not obviously a pair. It now exists once in geometry.js, with emitPart calling it; 528 cuboid corners were verified bit-identical before and after, so the preview mesh did not move. The fixture bakes every part × face on both skins: 924 new checks, 5485 to 6409.

The probes are deliberately not only corners. A corner is a fraction of 0 or 1 on both axes, and several ways of getting the axis pairing wrong agree at every corner and disagree everywhere else - which is the exact shape of the UV swap that once hid behind a stale fixture for a day. Proved by regressing the port three ways: swapping the body-Y/Z pairing (460 failures), dropping the pitch rotation (402), flipping one sign in the normal (44).

A per-horse choice, and a mirror bug caught before it shipped

Gap 102: the mask fold has no branch in it, so quarter faked one by sampling noise at a scale of 4000 body units - a hundred times the length of a horse - and amplifying it until the clamp turned it into a yes or no. The new CHOICE mask is the real thing: an integer drawn once per horse, constant across the body, exactly 0 or 1. Both of the trick's residual defects go with it - the slight drift across the body, and the 1-in-401 strip that clamped to neither end and produced a half-strength quadrant.

Writing it turned up a 64-vs-32-bit modulus hazard, and that is why the example gene uses options: 3. The draw is mixed and the modulus taken over the low 32 bits only, because the creator mirrors this in JavaScript where a 64-bit remainder needs long division the u64 helpers do not have. Java over all 64 bits and JS over the low word agree for every power-of-two number of options and silently disagree for 3, 5 and 6 - so a two-way example would have been green either way. With a three-way choice in prismatic.json, reintroducing the mismatch fails 72 parity checks.

Wing cloak switched off

Parked rather than patched, at the owner’s call: it paints under two dozen texels out of roughly five thousand, which is invisible to a person and was marginal long before the reseed moved it across the icon baker’s floor. It wants retooling, not a lowered threshold.

The killswitch is one line"wing_cloak.json" out of index.json — because that file is the only thing that registers a gene. Everything else is derived and fell out on the next bake: the designer’s genes.json, the generated spans of pages.js and index.html, the landing card. Every file still exists, and the wiki page carries a banner explaining the state and how to undo it.

It changed no horse, and proving that was worth the two minutes. All 1056 composed-coat hashes in the pipeline golden came back byte-identical — only the wing_cloak=n/n segment left the genotype code prefix, which is why all 1056 lines looked like they had moved. The bake golden lost exactly its three lines and nothing else. That second fact is also a clean demonstration of gap 47’s fix: before this session, removing a locus would have redrawn every gene registered after it.

Two measurements, and one gap that is a writing job

Gap 53 is measured and the answer is reassuring: about two founders in three wear white from the two ordinary-marking loci, mean coverage about 10%, only about 8% loudly marked. The estimate on the page was three in four, so it was close and slightly high. The measurement has a trap in it: counting white on a founder directly says 91%, because on a flat greyscale chart a cremello, a grey and a palomino are pale coats with no marking on them. The giveaway was 91% wearing white while only 78% carried either locus. The test composes each founder twice and differences them, so the base coat cancels.

Gap 89 was recounted rather than fixed. The recorded 24 is 25; a briefly-feared 157 is wrong, because 131 of those are magical genes where having no science tab is correct. None of the 25 has an orphaned real-world section that could simply be moved, so it is genuinely a writing job and wants the owner - filling those tabs means writing veterinary material, not generating it. wiki/tools/check-gene-tabs.mjs does the code-side half: it holds the 25 as a written-out allowlist and fails if a new natural gene page arrives without a science tab, or if one on the list gains a tab and is not removed from it.

Two parser refusals, three live bugs, and the discovery that the composer was never inside the parity gate.

A code-only audit pass: things findable and fixable without opening the game.

The parser now refuses what has already shipped broken

Two gaps had been half-fixed on 2026-09-08 - the broken files were corrected and the checks were never written, which is the half that stops it happening again.

Both were scanned across the corpus before being written, and both trip zero shipped genes - 513 banded masks, 130 of them pointing at knobs. So these are prevention with no migration behind them, which is the cheapest moment to add a rule. Six tests cover the two rejections and the three shapes that must keep working.

Three live bugs

The composer was never inside the parity gate

The creator’s mirror of the composer had rgb === 0 ? PURE_BLACK_ALPHA : 0xFF - an exact equality against pure black, which is what the game used to have too. The game was changed to a ramp when a LUT edit moved the chart’s black corner off #000000; the mirror was not. So the branch had stopped matching anything at all: black bakes to about #161515, the equality never fired, and the creator drew every dark coat at full opacity.

The finding is not the line, it is why nothing caught it. check-parity.mjs runs restrict() and tint() and never runs compose() - the magical cases are seeded with a synthetic flat colour field rather than resolved through a chart - so the entire composite path had no net whatsoever. The gate looked comprehensive and had a hole the exact size of the bug.

The composite itself still cannot be compared in the harness: it needs the gradient PNG and the white template, and there is no image decoder in Node there. The arithmetic can be, and the arithmetic is what drifted - so the fixture now carries answer tables for nearBlackAlpha and the shadow lift, generated from Java, and parity checks them. Verified by restoring the old equality and watching three checks fail, one of them on #161515 exactly.

What this says about the other mirrors

There are four JavaScript ports of common/ in the creator - spec-engine, schema, geometry and the composer in fields.js - and until today the gate covered two of them. Gap 100 is the same shape and still open: geometry.js never received posed(), and the arithmetic lives one directory over in model3d.js. It is now a small job rather than an open question, because the technique exists: export the function, bake a table of Java’s answers into the fixture, compare. That is what closed the composer.

Two genes that make a horse frightening, a per-gene coat test, and a category audit that found the taxonomy was sorting on the wrong axis.

The night loci

Two new mechanical genes, and between them they are the mod’s first deliberate attempt at unsettling rather than pretty. Night temper has eight variants in two families — four that hunt and four that flee, each naming who it feels that about. Night watch has five, and they are a ladder rather than a list: stares from anywhere through walls, closes to ten blocks first, watches only while it can see you, moves to where you cannot see it, stands directly behind you. Any of the five silences the horse’s footfalls at night, which is what makes the locus work at all — the behaviours are unsettling in proportion to how little warning you get, and hoofbeats are warning.

The temper locus supersedes the watch locus whenever it has something to act on, because a horse that both stalks you and flees from you is not two behaviours, it is a bug. That rule could not live in common/: whether there is a cow within sixteen blocks is a question only the running game can answer. So the handler resolves the temper first and withholds the watch ability if it found a target, and the goal reports canUse() == false and stands down instead of fighting for the navigation.

Both founder tables are entirely heterozygous (owner’s call), which nothing else in the registry does. Every feral horse carries exactly one variant and none of them expresses it, so the plainest outcome each locus has is the one that cannot be caught. It broke a test, and the test was right to break: GenotypeTest asserted that a maximal founder roll lands every gene on its default homozygote, and these two have no such row at all.

The coat pass

Nine genes, all from the owner’s own list. Three were painting a ground over the whole horse before marking it — coccinella black, shieldback green, tribal ward pale — which made every carrier the same animal underneath, the opposite of what a coat gene here is for. All three grounds are gone.

Removing them broke two genes, and the break is the interesting part. Shieldback and tribal ward each had a “coloured” allele whose only job was to vary the ground. With the ground gone the two alleles came out byte-identical — a locus with two names for one outcome. Both now point their unused hue knob at the marking instead, which is what “a colour the line carries” should always have meant on a gene whose ground was never the interesting part.

The category audit found the taxonomy sorting on the wrong axis

The complaint was concrete: hood and magic item drop were in the same category. The cause was general. Magical families are derived from priority bands, and MAGIC_CORE had become a 24-gene dumping ground holding coat markings, hair colour, milk buckets, spawn eggs and a horse that stares at you through walls.

Priority is a real fact about a gene that paints and an accident about one that does not. For a painter it says where in the stack it lands, and the bands were laid out as families precisely because the paint order and the taxonomy want the same grouping. A gene that paints nothing has a priority only because every gene needs a slot in the genotype code, so banding on it grouped genes by when they happened to be written.

So the non-painting magicals are named explicitly now, by what they do — yield and death, behaviour, trails and emissions, and the body stats that already had their own band — and the bands go back to meaning what they were built to mean. It is the same split the naturals already make, where anything invisible is a health locus; this is that idea applied to the other half of the registry, with the groups spelled out because “invisible” covers more ground here.

The coat baking test

CoatBakeGoldenTest closes gap 124: every registered gene, every variant allele, composed through the real gradient and hashed. 203 genes in 623 lines, and it runs in seconds rather than needing the ten-minute suite — which is the property that matters, because the pipeline golden is expensive enough that its cost gets weighed against its value and this one is not. It was checked by nudging one gene’s strength by a single point and confirming it fails.

It hashes; it does not look. Four of the twelve genes that broke their hashes yesterday were fixed by the change that broke them, so a diff here is a prompt to go and look at the icons, never a verdict — and the test says so in its own failure message.

What the audit of the generator turned up

Also: shadowcreature’s page had no preview widget at all — it was written from the template for a gene that paints nothing, and shadowcreature paints plenty. That is why its previewer “was not loading”.

2026-09-08

An overnight upkeep pass: a link checker, the invert bug measured and fixed, and two rules that now enforce themselves.

Left to do “whatever upkeep seems necessary” with the long jobs unattended. Four things, and the second is the one that mattered.

A link checker, and two links it caught

wiki/tools/check-links.mjs is new: every internal href and src across the wiki must resolve to a file that exists, and every #fragment must find an id on the page it points at. Nothing had ever checked this, and the wiki is hand-written HTML with several thousand cross-links added in two bulk sittings.

It found two real breaks, both mine from the previous two commits: magic item drop linked to a page called item-preset-horse-spawn-egg.html that does not exist (the preset egg is documented on spawn eggs), and the session log pointed at known-gaps.html#gap-107, which had been closed and deleted. The fragment half is the part most likely to rot, because an id is invisible in the rendered page.

It also has a --orphans mode for the other half of gap 115. It reports none: the two gene pages not in pages.js (shade and accretion field) are deliberately absent, because GeneWikiTool.foldedInto omits a locus that paints nothing and is documented inside the page of the gene that depends on it. Both are linked from their parent. Not a bug, and worth having established rather than assumed.

The invert bug: measured, then fixed

Gap 120 was opened the same day it was found, on stained glass, and left for the owner on the grounds that fixing it moves ten shipped coats. Measuring it settled the question. A throwaway probe rendered every gene that leads with an inverted, parts-restricted mask and counted texels inside and outside its declared parts:

gene                   inside  OUTSIDE parts it paints but never declared
iridescent_jewel       5600    6176    [HEAD, MUZZLE, MANE, TAIL, EARS, all four LEGS]
opal_wing_veins        5600    6176    [HEAD, MUZZLE, MANE, TAIL, EARS, all four LEGS]
elytra_veins           3874    6176    [HEAD, MUZZLE, MANE, TAIL, EARS, all four LEGS]
webbed                 1481    6176    [HEAD, MUZZLE, MANE, TAIL, EARS, all four LEGS]
moth_mantle            7208    4568    [MUZZLE, MANE, EARS, all four LEGS]
lace                   8322    2008    [MUZZLE, MANE, TAIL, EARS]
fracture               4751    520     [MUZZLE]

6 176 is every texel the gene excluded, and webbed was putting four times more paint outside its declared region than inside it. That is not a curiosity to leave for a decision; the before-and-after render is a horse that had been dipped in white with a net on its barrel, versus a bay with a net on its barrel.

The first fix was one condition — a texel the parts test ruled out stays 0 through the invert — and it was half right. It fixed the four, and it broke the opposite idiom: galaxy’s black field and tribal ward’s pale one are both “a ground, with a cut aimed at one part”, written as a broad first mask and a narrow inverted second one. Zeroing outside the cut deleted the ground everywhere the cut did not reach. Tribal ward went from a painted white horse to a plain bay with swirls on it, which is visibly not what its own blurb describes.

The rule that serves both is that a mask’s parts mean different things depending on where the mask sits. The first mask defines the layer’s region, so outside its parts coverage is 0 and an invert does not bring it back. Every later mask modifies that region, so outside its parts it has nothing to say and contributes its combine’s identity — it is skipped. PARTS is exempt, because there the list is the mask rather than a restriction on it.

Twelve icons moved. Galaxy and tribal ward came back byte-identical to before any of this. Webbed and the other three stayed fixed. And five more moved that nobody had been looking at — masked, ehretia, peafowl, snow cloud and war mask — every one a layer that had been losing coverage on a part it explicitly listed. Masked’s “mask over the head and ears” was not reaching the ears. Ehretia’s “dorsal stripe from withers to tail” was not reaching the tail. Those are the genes that quietly paid for the second half of the bug.

The parity check was green about all of it by default, which is the part worth remembering. example-genes/prismatic.json had no layer whose second mask was narrower than its first, so nothing in the fixtures could tell the two semantics apart. A discriminating layer was added, and then checked by deliberately regressing the JavaScript twin: 264 parity checks fail under the old rule and pass under the new one. Re-baking a fixture that cannot reach the change is the same trap as not re-baking it at all.

It also explains an entry already on this page. Gap 119 records webbed being rebuilt after a contact sheet showed it as “a white ground with brown islands rather than a net”. That was this bug all along, and the rebuild had treated the symptom.

Two hard rules that now enforce themselves

CommonPortabilityTest scans common/’s own sources for a Minecraft / NeoForge / Mojang import, for Map.of() or Map.copyOf( outside CommonMaps, and for the two Java 9+ calls that were removed for the TeaVM and Java 8 targets. Nothing had ever checked hard rule 1 or hard rule 2, and both fail in ways the compiler cannot see — a green build, a shipped mod, and the damage in a browser or in a backport that does not exist yet. That closes gap 116.

Writing it turned up the boundary worth recording: the check was too strict at first and flagged light’s six-entry Map.of(…). The trap is the empty immutable map specifically, a populated one gets a sized table, and that gene paints — so the browser exercises it on every preview and would have died on it the way the ALL mask did. Allowed, and the test says why.

And the long job

Full :common:test and a regenerated golden coat file, since ten genes changed what they paint. The bakes were re-run first, including :web:bakeDesignerAssets, because common/ moved and the designer runs the real engine rather than a port of it.

0.3.0, and the release procedure earning its keep on the first run.

Tagged 0.3.0 for the mechanical-gene batch — a minor bump rather than a patch, because eight new behavioural loci and six coat genes are features and the genotype code moved again. Both version strings, the releases page, the tag and the GitHub release object.

The procedure’s first step caught something on its first real use. “Re-run every bake and confirm it moved nothing” turned up a modified wiki/gene-creator/js/examples.js: :common:bakeCreatorAssets copies example-genes/prismatic.json into the creator’s example list, and the previous session had edited prismatic (to exercise the new LUMA and EDGE masks for the parity check) without re-running that bake. The creator would have shipped an example gene missing the two masks the release is about. The regenerate table in CLAUDE.md only said “the coat PNGs or the name tables” for that task; it now names example-genes/ too, in the same row rather than a new one.

The golden coat file was regenerated, which is what gap 118 said to do at the next deliberate suite run and closes it. It had been two sessions stale — forty-six genes across the two imports, each of which moves Genes.codeOrder() and so every horse’s epigenome slots. The suite is genuinely green at this tag rather than green-except-one-known-failure, which is the state a tag ought to be in and had not been.

Fourteen genes, a mask that asks the gradient chart what colour it made, and eight loci that paint nothing at all.

The brief was “a bunch more mechanical genetic toggles, so a breed can be described by what its horses do”, plus six coat genes. It came to fourteen genes, two new masks, six new effect verbs and one correction to the effect format that had been wrong since it was written.

The mask that started it: LUMA

Nyxborn was specified as “replace all white with black, then put tiny white stars over all the black”, and the request added: use the LUT to decide what black is. That is not a detail, it is a different question from the one the format could ask. PIGMENT reads melanin — a pair of levels that mean nothing until a gradient chart turns them into a colour. “Black” and “white” are facts about that colour, and the LUT locus can hand a horse a chart whose black corner is violet. Such a horse has identical pigment everywhere and is no longer black anywhere.

So the format gained LUMA, which thresholds the resolved colour — what phase 2 got out of the chart, plus whatever the magical genes before this one painted. Magical genes only: the natural phase is what decides the pigment the chart is handed, and the overlay pass has already spent the accumulator, so the parser refuses it in both rather than let it read zero.

Its channels were picked off measurement rather than guessed. A throwaway probe baked nine representative coats through the real gradient and printed the readings, and the useful finding was that white is not light: a bald white texel reads 1.000 on the achromatic floor, a perlino 0.871, an old grey 0.698, a palomino 0.475 — while all four are “pale”. On a palomino “the white markings” and “the pale parts” are different sets of texels, and only one of the two channels tells them apart. The same probe gave the black end: black 0.807, bay 0.782, chestnut 0.599, which is why Nyxborn’s stars land on a bay and not on a chestnut.

The four genes that were hunting for the horse’s own white through pigment — fielded, yalia, voided and opalized — were moved onto it, which is the second half of what was asked for. The three that hunt for black through pigment (integration and opalized’s dark side) were left alone: nobody asked, and moving them would shift shipped coats for no stated reason. Worth doing deliberately one day.

Two more pieces of vocabulary

The six coat genes

Nyxborn, stained glass, Tron, ocular and bloodstained are gene files. Shadowcreature could not be: its three halves land in three different phases, and the gold eyes with no sclera can only be reached from the overlay pass, because CoatRegions.redrawEyes restores the eyes from the template as the last act of the bake.

Every one was baked as an icon and looked at, and four were wrong on the sheet in ways no test would have caught:

Eight loci that paint nothing

Swim speed, water breathing, fighter, on death, item drop, mob aura, meat and milk volume. Three shapes between them:

The founder rule, and its one exception

The standing rule for a magical locus with an invisible carrier is that the wild population sits on the combinations that show — item drop, mob aura, meat and the coat genes all follow it. Magic on death does not, at the owner’s explicit instruction: carriers only, never affected, so a feral horse never craters the ground it dies on. The argument for the standing rule assumes the outcome is something you would want to find, and an exploding horse is not; a player who has never heard of the gene should not lose a stable to it.

Six effect verbs, and one thing that was simply wrong

charges, breath, on_death, item_drop, mob_aura and combat, all translated and all flagged unverified in their own comments. attribute was also finally wired — it had been parsed and logged-once since it was written.

Its list named swim_speed, and there is no such attribute. Vanilla has never had one; the thing that means “how fast does this mob move in water” is water_movement_efficiency, Depth Strider’s attribute. It had gone unnoticed because the verb was never executed, so the name never had to resolve to anything. That is worth remembering about any verb sitting in the format unbuilt.

The interesting one is charges. Milk volume has to reach the milk gene’s three fillings, and the two loci cannot see each other’s epigenome — a gene is handed its own values and nobody else’s, deliberately. So the verb names a yield kind rather than a gene, the milk locus tags all three of its fillings "milk", and a future gene producing some other fluid is governed for free instead of by somebody remembering. Charges divide the cooldown rather than banking uses, so “three times a day” is three fillings spread across the day — which needs no new stored state at all.

Two tripwires fired, both correctly

The golden coat file is knowingly a run further behind. Fourteen genes change the registry order, which changes every horse’s epigenome slots, which changes every hash in it — and regenerating it needs a full :common:test. Known gaps has it.

A gene file can hold its own reasons now, and thirty-two genes arrived to test that.

Two things, and the first is why the second is worth having. The gene format gained a notes block — on the gene and on each expression, an array of paragraphs, printed on the generated wiki page as written. JSON has no comments, so everything an author actually knew (why the wavelength is 90, which two layers must stay in that order, what the source description asked for and where the build knowingly does something else) used to go on a hand-written page and drift from the gene the first time either moved. It lives beside the layers now. See the format and creating a gene, which spells out which of the three prose fields a sentence belongs in.

Then thirty-two genes off the intake folder, in three waves as they were dropped in. Every one is a translation rather than a transcription: the sketches were written against a vocabulary this mod does not have — boxes at explicit coordinates, rings at named centres, arithmetic inside JSON, spread on ops that have no such parameter, pigment ops on magical genes — and each gene’s own notes block now records what it asked for and what was done instead. That is the first real use of the field and the reason it was added.

Invert's preview window: an empty map, in the browser only.

Gap 107 is closed, and the cause was not in the gene. TeaVM’s empty immutable map traps with remainder by zero on the first get — it hashes the key into a table it never sized — so Map.of() and Map.copyOf() are correct on a JVM, correct in the game, and a live grenade in the wasm every wiki page runs. Invert is the only gene in the registry with an ALL mask; an ALL mask takes no parameters; its Params map was therefore empty, and the painter’s very first lookup on it killed the bake. Everything else about the gene was fine, which is why a session had already checked every value it consumes from Java and found nothing.

A gene may now say which picture is its picture.

Which allele combination and which base coat a gene is illustrated on is measured — bake the candidates, keep the loudest — and that stays the default, because it means no list goes stale when a gene starts or stops painting. But loudest and most recognisable are not always the same thing. Flametouched’s homozygote is a whole-horse ember gradient and shouted down the flames the gene is named for; Patina moves more texels on a tobiano than on the plain bay it reads better on. Both now declare a preview block, which the icon baker, the gene page’s opening coat and the preview window’s opening outcome all obey. Two genes out of a hundred and fifteen; the thresholds behind the measurement are gap 114.

A second palette, and the tools stopped keeping their own list.

The LUT locus has a green-through-pink allele beside the blue-pink one. The gene was built to grow and did: a row in LutGene.VARIANTS carries the allele, the expression table, the carrier wording and the founder odds. What did not follow from that row was the offline tooling — the icon baker, the wiki baker, the sample tool and the golden test each wrote Map.of("bluepink", ...) out by hand, four copies of a list none of them knew when to update. They call LutSet.fromRegistry now, which is the same walk the game has always done.

Smaller, and all owner's calls.

Not done: the coat golden file is a run behind — thirty-two genes moved every hash in it, and regenerating it means a full suite run, which the owner has asked not be done casually. The next deliberate run will fail on CoatPipelineGoldenTest and that failure is expected; gap 118 has the recipe.

Released 2026-09-08: 0.2.0, and two gene pages that outlived their genes.

Cutting a release rather than building anything, and the interesting part is what the process caught. The tag is 0.2.0, not the 1.1 that was asked for: the only previous tag is 0.1.0, there is no 1.0, and to be verified is still the authority on how little of this has been seen in a game — a 1.x on a mod where most of what is built has never been rendered in a client would be a claim the project cannot support. Owner’s call, taken on that reasoning.

Built 2026-09-08: a gene that changes the rule instead of the coat, and the paint order rebuilt round it.

Owner-driven, and the whole session hangs off one observation: the mod’s magical markings are painted after the natural ones and mostly cover them, so a horse carrying both a tobiano and something loud reads as the loud thing alone. Three separate requests were about that, at three different levels.

Built 2026-09-08: which horse a gene is photographed on, got right on the third try.

Three owner reports - Fielded shown on a plain bay, mushroom wanting a chestnut, KIT’s icon a featureless white horse - are all the same question, and CoatVisibility answers it for both the icon baker and every gene page’s preview window. Two rules changed, and the second one is worth reading before touching it again.

Built 2026-09-08: the punnett square asks who the parents are, and three other wiki changes.

Built 2026-09-08: a “Creating a gene” page, written twice - once for a person and once for a model.

Owner-driven. The page carries two tab panels rather than two pages, so the sidebar has one entry and a reader picks the register they want.

Built 2026-09-08: a third pass over the registry, and two more masks caught painting the wrong thing.

Owner-driven, another twenty-odd notes off the gene pages. Most of it is placement and colour and is written up on each gene's page; what is worth keeping is below.

Built 2026-09-08: Pavonem is an actual ocellus, because the owner drew one.

Owner-driven, and the one item on the review that was flagged rather than guessed - the note said “flag that one if you need me to walk you through it” and it was taken up. What came back was an SVG with the proportions in it, which settled in one message what several passes of guessing would not have.

Built 2026-09-08: a gene page's preview opens on a coat the gene can actually be seen on.

Owner-driven, one request - “set the default preview model for fielded to tobiano bay”. Written up in CoatVisibility and in gene-preview.js's header.

Built 2026-09-08: the owner reviewed the whole registry gene by gene, and the review found more than the fixes did.

Owner-driven, roughly thirty notes taken while reading the gene pages, arriving as a running commentary rather than a list. Written up on each gene's page; the two engine changes are on the gene format.

Built 2026-09-08: twenty-four gene fixes, four new painters, one polygenic locus — and a mask bug that had been eating half of fifteen genes.

Owner-driven, a list of twenty-four notes taken from looking at the horses. Written up on each gene's own page; the vocabulary additions are on the gene format and the cross-locus block on the genetics model.

Built 2026-09-08: the seven magical family index pages are deleted — the landing page was already the index.

Owner-driven, one request. Written up in GeneWikiTool; the grouping itself is unchanged and still common/genetics/GeneFamily.

Built 2026-09-08: the gene icons were being drawn through a sheared camera, and every gene has one now — or honestly none.

Owner-driven, five requests in one, and one of them had to be answered twice. Written up in GeneIconTool and HorseSkinGeometry.posed; the card markup is GeneWikiTool.card.

Built 2026-09-08: the wiki groups genes the way the editors do, every gene page is listed in both places, and neither list is written by hand.

Owner-driven, four requests in one. Written up on the spawn eggs and in GeneWikiTool; the grouping itself is common/genetics/GeneFamily.

Built 2026-09-08, NOT yet looked at in game: the gene editor grows a randomize menu, per-gene locks and a family filter — on both screens.

Owner-driven, one request. The custom horse spawn egg and the browser designer are one screen in two places (hard rule 5), so all of it landed twice — and the parts that are genuinely the same rule rather than two parallel ones stopped being mirrored and moved into common/. Written up on the spawn eggs; checklist there.

Built 2026-09-08, NOT yet looked at in game: eighty-four magical genes, imported in bulk out of process/.

Owner-driven, one overnight request. Every gene is a JSON file — none of them is a Java class. Each has its own generated page, gathered at the time by seven family indexes — ground and strong white, fields and regions, spots and rings, speckle and dust, lines and strokes, mane and tail, colour modifiers. (Those index pages were later deleted as redundant to the landing page.) Checklist §0-AN. Gaps 9497.

2026-09-07

Built 2026-09-07, NOT yet looked at: the wiki became three views — Gameplay, Coding, Science — and got a real search.

Owner-driven, one request with four things in it. Design: every gene page, every item page, the landing page. Checklist §0-AM. Gaps 8891.

Audited afterwards, same day. Two of the gaps this session opened were checkable in the repo without a browser, so they were.

Built 2026-09-07, NOT yet looked at: breeds became JSON files, and got spawn eggs, a designer, and a splice mark.

Owner-driven, one request with four things in it. Design: the breed file format, breeds, items, carrots. Checklist §0-AL. Gaps 8385.

Built 2026-09-07, NOT yet looked at: three stables that generate with rare horses already in them.

Owner-driven, one request with three buildings attached. Design and attribution: generated stables. Checklist §0-AK. Gaps 8082.

2026-09-07: deleted the “HYPP is not built” notes, some sessions after HYPP was built.

Prompted by the owner asking whether HYPP had been added. It had — registered, unit-tested, with its own page and a nav entry. What had not happened was deleting the notes saying it had not.

Built 2026-09-07, NOT yet looked at: five themed random splice carrots.

Owner-driven, one request. Design: the five themed splices. Recipes: items. Checklist §0-AJ.

Built 2026-09-07, NOT yet looked at: the stable as a table, and an information screen you can read.

Owner-driven, ten requests in one pass. Design: roadmap § the horse browser, breeding § client sync. Checklist §0-AI. Gaps 76, 77.

Built 2026-09-07, NOT yet looked at: a horse that is a wolf until dawn, a horse that kicks up rainbows, and a particle locus that stopped showing on one copy.

Owner-driven, three requests. Design: LYCAN, rainbow dust, the particle locus going recessive. Checklist §0-AH. Gaps 7375.

Built 2026-09-07, NOT yet looked at: an information screen behind an “i”, a breeding preview, and a mare that miscarries.

Owner-driven, five requests in one session. Design: breeding § client sync, § the miscarriage, § every draw written down. Checklist §0-AG.

Built 2026-09-07, NOT yet looked at: the rest of the reference’s disorders, and the first dominant ones.

Closes the roadmap’s §12 gene table. Design: the two shapes a disorder comes in; a page each under Health genes. Checklist §0-AF.

Built 2026-09-07, NOT yet looked at: epigenetics become literal numbers, foals drift, and a breed stops chasing its own horses.

Owner-driven, and the largest single change to the genetics model so far. Design: the genetics model, breeding § drift, breeds § founder-only. Checklist §0-AE.

Built 2026-09-07, NOT yet looked at: the walk moves to road level and out in front of the steps, and the cowboy learns to open a door.

Owner-driven, from what he could see in game: the path was in the wrong place, the beds faced the wrong way, and the man could not get into his own barn. Design: Villagers § the barn and § he can work a door. Checklist §0-AC.

An unattended session

The owner queued a run of gene references and left: “don’t ask questions or interrupt this session … use your best judgement, and try to adhere to existing systems as much as possible. Put questions in the verification file.” Every entry below therefore has a Questions for the owner block in its verification section, and every one of them is a real open call rather than a summary.

The standing rule for the whole run: each process/*info.md reference was absorbed into that gene’s wiki page and the file deleted, and anything a reference proposed that would have needed a new system was declined and written down as declined. process/ is now empty and gone. Nothing here has been seen in a game.

Built 2026-09-07, partly looked at: the cowboy gets off the horse, and the barn grows a house.

A playtest round rather than a build round — the owner played the villagers and reported what he saw, repeatedly, and most of what follows is deletion. Design: Villagers & transfer papers. Checklist §0-AC.

Built 2026-09-07, NOT yet looked at: the test world lands you in a plains village, and the cowboy says hello.

Tooling for the entry below, written straight after it, because testing the two villagers started with a hike and that was most of the cost of testing them at all.

Built 2026-09-07, NOT yet looked at: the cowboy rides out — two villagers, and papers that sell a horse.

The roadmap’s §19 villager, a second character the owner asked for alongside it, and the item that connects them. Design: Villagers & transfer papers. Checklist §0-AC.

Built 2026-09-07, NOT yet looked at: KIT gains the booster group, and it is the common part of the locus.

Four new alleles at KIT, taking it from eight to twelve. Checklist §0-AB.

Built 2026-09-07, NOT yet looked at: the two splash loci swap roles - the common allele was on the wrong gene.

MITF and PAX3. Checklist §0-AA.

Built 2026-09-07, NOT yet looked at: grey becomes a copy number, and gets four rates instead of one.

Grey - three alleles, and the mod's first coat gene that is also a health gene. Checklist §0-Z.

Built 2026-09-07, NOT yet looked at: dun's mane and tail get the midtstol.

Dun - a dark band down the centre of the long hair with pale guard hair either side. Checklist §0-Y.

Built 2026-09-07, NOT yet looked at: sunshine and snowdrop, two more dilution alleles at MATP.

MATP gains sun and sno. Checklist §0-X.

Built 2026-09-07, NOT yet looked at: manchado - white with islands in it, and its own locus.

The rare Argentine pattern: broad clean white over the topline with rounded islands of base colour inside it. Checklist §0-W.

Built 2026-09-07, NOT yet looked at: rabicano - dominant to inherit, a roll to see.

White ticking from the tail dock and flank forward. Checklist §0-V.

Built 2026-09-07, NOT yet looked at: pangare, sooty's mirror - and the medial helper gap #51 asked for.

The pale muzzle, eye rings, belly, flanks and inner legs of an Exmoor or a Fjord. Pangaré; checklist §0-U.

Built 2026-09-07, NOT yet looked at: sooty - a dark cape, made by declining to remove pigment.

Sooty; checklist §0-T.

Built 2026-09-07, NOT yet looked at: flaxen - a chestnut's pale mane, as a dosage rather than as folklore.

Flaxen; checklist §0-S.

Built 2026-09-07, NOT yet looked at: W4, the Camarillo white - a second all-white allele at KIT.

Built 2026-09-07, NOT yet looked at: the dhampir, a recessive whose carrier is the advertisement.

Dhampir; checklist §0-R.

Built 2026-09-07, NOT yet looked at: a diet locus - twelve narrow appetites, and a channel other genes override.

Diet; checklist §0-Q.

Built 2026-09-07, NOT yet looked at: bay is a shade continuum - blood, bay, liver and seal off one score.

Shade, a new locus that paints nothing; agouti rewritten. Checklist §0-P.

2026-09-06

Built 2026-09-06, NOT yet looked at: a shadow pass at the end of phase 3 - nothing magical is painted true black.

Owner: “true black should never be painted on a horse, because then you lose all the painted-on shading, and it just looks bad. This is especially noticeable with the magic zebra gene.” ColorField.liftShadows, called after the magical loop; checklist §0-O, mechanism on Pipeline.

Built 2026-09-06, NOT yet looked at: four dilutions set to measured restriction targets.

Owner, now reading numbers straight off the probe: single cream to 20% red / 29% black restricted, classic pearl to 20% / 60%, double dilute to 47% / 95%, champagne to 31% / 80%. Set in MATP and champagne; checklist §0-M.

Built 2026-09-06, NOT yet looked at: the LUT chart can be clicked, and searched by colour.

Owner: clicking the chart should show “the amount of restriction of red and black in that location”, and a box to type a colour code into that finds the closest point on the LUT and reports the same thing. Both mark the spot. On the lab; checklist §0-L.

Built 2026-09-06, NOT yet looked at: a new default gradient, and the coat palette changed under every gene at once.

Owner supplied horselut.png and asked for it as the new default LUT. Installed into all three places the chart lives; gap #49 restated against it. Checklist §0-M.

Built 2026-09-06, NOT yet looked at: three dilutions retuned off the footprint overlay, and the gradient turns out to be the real constraint.

Owner, reading the new overlay: prl/prl should sit “basically all the way at the top of the lut … nearly to white”, cream should be “MUCH closer to the top”, and champagne “should reduce much more red than it does”. Retuned in MATP and champagne; gap #49 is what the exercise uncovered.

Built 2026-09-06, NOT yet looked at: the LUT chart shows which part of itself a coat reads.

Owner, on the lab below: draw on the gradient “where it shows what area of the LUT it's pulling from for that specific coat … that'll also help me tune all the other genes.” It does now. The overlay; checklist §0-L.

Built 2026-09-06, NOT yet looked at: a page for trying a gradient on the real pipeline before shipping it.

Owner: a page to “upload and view different LUTs”, with pre-built buttons for the base coats, then the dilutions, then one white gene. wiki/gene-lut.html?view=lab, plus the widget under wiki/lut-lab/. Checklist §0-L.

Built 2026-09-06, NOT yet looked at: two striped genes stopped sharing a painter, and a third one was written.

Owner: the zebra and brindle genes “shouldn’t use the same painter, and they both currently look awful.” Both true, and the two halves are the same fact. Brindle is white now; magic zebra reads a real zebra’s body map; and the natural counterpart it had been promising since 2026-09-02, zebra striping, exists. Checklists §0-Z and §0-G.

Built 2026-09-06, NOT yet looked at: an art edit switched off a rendering rule, and every bay went dead black.

Owner: “the black markings on bay are pure black when they shouldn’t be, they should be dark but not #000000.” True, and nothing to do with agouti — it was the same session’s LUT edit reaching a place nobody would look. Fixed in CoatTextureComposer; coat-golden.txt regenerated, wasm re-baked. Checklist §0-D.

Built 2026-09-06, NOT yet looked at: dun re-derived from a field guide — grullo goes blue, and the leg bars stop being bracelets.

Owner handed over a reference write-up and two specific complaints: the leg markings did not look correct, and dun on black should be grulla, a grey-blue. Both were real and both had the same root. The whole locus is rebuilt around one dilution and one mask; :common:test green, :neoforge-26.1.2:build green, parity green against freshly baked fixtures, coat-golden.txt regenerated, creator and designer assets re-baked. Closes the dun row on roadmap §1; checklist §0-D.

Built 2026-09-06, NOT yet looked at: white fading through both pigments was browning every marking on a black horse.

Owner: “it’s fading through both black and red pigments, leading to brown edges on otherwise black horses … if we’re doing edging on a black marking, it removes ALL red pigment to do the edging, so the edges should be shades of grey.” That is now PigmentField.whiten, one verb every white marking goes through, plus a WHITEN op so data-driven genes get it too. :common:test green (470), :neoforge-26.1.2:build green, parity green against freshly baked fixtures, coat-golden.txt regenerated (66 rows), wasm re-baked. Closes the “odd 1px gold fleck” line on known gaps; checklist §0-F3.

Built 2026-09-06, NOT yet looked at: tobiano’s coverage knob was lying too, and fixing it changed nothing visible.

Owner’s call, after frame: bring tobiano in line without changing how it works. It had the same defect — a cover knob cutting a bell-shaped noise field at a constant — but it landed on a different part of the bell and so never produced a broken-looking horse. A range written as 0.400.56, which reads like a 1.4× swing, sat across the steepest part of the field and delivered 31%–86% coverage: a 2.8× swing. Frame’s sat outside the field’s spread and came out blank-or-flooded; tobiano’s sat across the middle and came out over-sensitive. :common:test green, :neoforge-26.1.2:build green, parity green, coat-golden.txt regenerated (12 rows, all tobiano=To/to), wasm re-baked. Closes the second half of #30.

Built 2026-09-06, NOT yet looked at: frame overo was drawing a dipped horse, for three separate reasons.

Owner, on the frame rebuild earlier the same day: “the way the markings are drawn is totally wrong … yours is still looking like the horse was dipped.” It was — white rising to a frayed waterline with the coloured coat reduced to scraps. Frame is big splotches across the middle of the side, framed by colour above and below. Three independent faults made the dipping, each individually plausible, and all three had to go. :common:test green, :neoforge-26.1.2:build green, parity green, coat-golden.txt regenerated (18 rows, all ednrb=O/N), wasm re-baked. gene-ednrb.html#paints.

Built 2026-09-06, NOT yet looked at: the two roans stopped being the same pattern at two strengths.

A region-by-region reference on classic roan and varnish roan, read against the code, said the same thing about both: they had converged. Classic roan had a front-to-back gradient that left a dark shoulder, which is a varnish trait; varnish had the same noise frequency and the same gradient and no anatomy at all, which made it a weaker classic roan. The two are told apart by shape, not by how much white there is, and now they are. :common:test green, :neoforge-26.1.2:build green, parity green, coat-golden.txt regenerated with three new roan cases, wasm re-baked.

Built 2026-09-06, NOT yet looked at: frame overo re-derived from the reference, and it closes a real defect.

Partly superseded the same day by the dipped-horse fix above. The topline reference below is still the fix it says it is; the shape it built on top of that — a white belly, a band open from below, five-to-one anisotropy — drew a dipped horse and has been replaced. The belly assertion described here was the test that had to go.

The owner supplied a locus-by-locus description of what frame overo actually looks like on a horse. Read against it, the painter got two things right (jagged margins, a bold face) and most of the rest wrong. Every constant moved, the outcome id is frame-overo rather than frame, and the shape is now written as a list of the reference's claims with the code that answers each — gene-ednrb.html#paints. :common:test green, :neoforge-26.1.2:build green, parity green, coat-golden.txt regenerated, wasm re-baked.

Built 2026-09-06, NOT yet looked at: green and hazel eyes, both heterochromias, and a magic locus that only exists in the heterozygote.

The eye channel was built two sessions ago with one colour for both eyes and a note that said the rest was “content, not machinery”. It was not: one blue eye is not a colour, and neither is a wedge. This session finished the channel and gave it its own page, Eye colour & heterochromia, which is now the single source of truth — pipeline.html#eye-colour and the tiger-eye page both shrank to pointers. :common:test green, :neoforge-26.1.2:build green, parity green, coat-golden.txt regenerated with nine new eye cases, wasm re-baked.

Built 2026-09-06, NOT yet opened in a browser: gene pages show the gene, and how to make its carrot.

Two drop-in widgets, each one <div> and one <script> on a page, both running the same wasm the horse designer does. :common:test green, :neoforge-26.1.2:build green, parity 3868/48, coat-golden.txt untouched. Three commits, in that order.

Follow-ups 2026-09-06 on the designer, from the owner's first look.

Seven items; :common:test 439 green, :neoforge-26.1.2:build green, parity 3868/48.

Built 2026-09-06, NOT yet opened in a browser: the horse designer runs the mod itself.

wiki/horse-designer/ is now the browser twin of the custom horse spawn egg, and it runs common/ compiled to WebAssembly by TeaVM - not a port of it, the actual bytecode - so all 48 genes are live and the coat is byte-identical to the game's. :common:test 439 green, :neoforge-26.1.2:build green, creator parity 3868/48, coat-golden.txt untouched. Six pieces:

Built 2026-09-06, NOT yet looked at in a browser: wiki/horse-designer/ - a second browser tool that stands a horse in a grass field; plus the vanilla model tables folded into wiki/body-space.html and the loose javahorsemodelinfo.md deleted.

No Java changed: :common:test 439 green, :neoforge-26.1.2:build green, creator parity 3868/48, coat-golden.txt untouched. Four pieces:

Fixed 2026-09-06: the gene creator was dead on open, drawing bounding boxes instead of a horse, and had been previewing every coat with the spine and belly texture patches swapped.

:common:test 439 green (+3 CreatorMetadataRoundTripTest), :neoforge-26.1.2:build green, parity 3868/48 (up from 3832 - the effect vocabulary is checked now too). No game code changed; coat-golden.txt untouched. Six pieces:

Fixed 2026-09-06, from the first play session against the sex-linked + eye-colour + white-recalibration work: one eye rendered backwards, and the custom spawn egg could build a Brn/Brn stallion.

:common:test 436 green, :neoforge-26.1.2:build green, creator parity 3832/48. Two independent bugs, both owner-caught inspecting the new eye-colour work:

Built 2026-09-06, NOT yet play-tested: sex-linked inheritance + brindle, the eye-colour channel + tiger eye, a cutie-mark modifier hook, the splice blacklist, the Feral Mixed rename, and the white-belly recalibration.

:common:test 436 green (+20 BrindleGeneTest, +11 EyeColorTest, +6 SpliceSafetyTest, +4 CutieMarkGeneTest), :neoforge-26.1.2:build green, creator parity 3832/48 untouched, runServer boots clean (50 segments, 48 built-in + the 2 shipped spec genes). Old saves will not parse (50 vs 48). Seven pieces:

Docs 2026-09-06, no behaviour change: the roadmap was rewritten and renumbered, and the built work moved onto its own pages.

No Java touched; :common:test 405 green, :neoforge-26.1.2:build green. Shape of it:

Built 2026-09-06, NOT yet play-tested: the leopard complex (appaloosa) - three new loci + a cross-locus-read hook; plus the custom-egg Breed button becomes a dropdown.

:common:test 405 green (+14 LeopardComplexGeneTest), :neoforge-26.1.2:build green, runServer boots clean (48 segments). Old saves will not parse (48 vs 45 code segments). Shape of it:

2026-09-05

Built 2026-09-05, NOT yet play-tested: spawn-egg preview controls + a many-allele dropdown + a Randomize button; cutie-mark placement/rotation tuned.

:common:test unaffected (391), :neoforge-26.1.2:build green. Client-only, CustomHorseSpawnScreen + CutieMarkLayer.

Built 2026-09-05, NOT yet play-tested: name-tag rename window.

Right-clicking a horse (with a real record) with any name tag - not just an anvil-renamed one - now opens client/HorseRenameScreen: two fields (first / last name) prefilled from ClientHorseRecordCache, a Rename + Cancel button. Confirm -> RenameHorsePayload -> server re-checks range + a tag still in hand + not both-blank, then HorseRecords.rename and consumes one tag (creative exempt); Cancel consumes nothing. HorseInteractionHandler's name-tag branch now fires for any Items.NAME_TAG, cancels on both sides (no vanilla custom name), and server-side sends OpenHorseRenamePayload (playToClient -> HorseRenameScreen.open). The old handleNameTag (split-on-first-space, needed CUSTOM_NAME) is deleted. Lang: gui.horsegenetics.rename_horse / .rename_confirm. :neoforge-26.1.2:build green, runServer boots clean (both payloads register). Checklist: wiki/verification.html §0-E.

Built 2026-09-05, NOT yet play-tested: the cutie-mark gene + Horse Browser splice-recipe rework.

:common:test 391 green, :neoforge-26.1.2:build green, runServer boots clean (45 segments, no errors). Two pieces:

LUT is owner-verified in game 2026-09-05

(see the Owner-verified block).

Built 2026-09-05, NOT yet play-tested: white top/bottom UV swap + two Horse Browser gene buttons.

:common:test 386 green, :neoforge-26.1.2:build green.

Built 2026-09-05, NOT yet play-tested: the LUT gene + the Horse Browser becomes a container menu with a Crafting tab; plus two fixes on owner play-test feedback (browser UI redesigned, splash face-marking boosted).

:common:test 386 green, :neoforge-26.1.2:build green, runServer boots clean (44 segments, loaded 2 data-driven gene(s), menu type + payloads register with no error). Pieces:

2026-09-05 follow-ups to the carrot pass, NOT yet play-tested

Built 2026-09-05, NOT yet play-tested: shearing + milking rules + the whole carrot family + the gene database + research papers

(roadmap wiki §§7, 12, 14, 16, 19). Big multi-system pass. :common:test 379 green, :neoforge-26.1.2:build green, parity 3832/48, runServer boots clean (43 segments, still - format 3 is additive). Shape of it:

Built 2026-09-05, NOT yet play-tested: the Horse Browser (H page), first slice, + a per-horse gene popup.

A new common/genetics/GeneDescriptions (a one-paragraph, plain-English summary of every one of the 41 built-in genes) behind a new Gene.description() default - the authoritative gene docs stay in wiki/gene-*.html; this is the glanceable version, and a data-driven gene returns "". :common:test still green (no test touches it). Three new client screens / handlers in neoforge-26.1.2/client/:

Built 2026-09-05, partly play-tested: breeds + herds + wild aggro; Test gene removed.

A common/breed/ package, 49 real-world breeds + UNKNOWN, biome-weighted wild-herd spawning (traditional harems + bachelor bands), cross/mixed breeding labels, wolf-style herd aggro, and a batch of dev tools. The Test gene was deleted (TestGene + TestCoatPattern gone) - it had served its purpose and looked ugly. Genotype code 44 -> 41 segments (43 in-game with suntouched + waterborn), coat-golden.txt regenerated (the three test=T/t golden cases dropped; CoatTextureComposerTest / GeneCodeDisplayTest / GenotypeTest / GeneticCodeCombinerTest / SpecGeneTest retargeted off Genes.TEST), GenotypeCatalog.size() 462 422 019 -> 462 422 018, totalGenotypes() /3 to 5 881 468 290 421 930 356 326 400 000. Machinery is wiki/breeds.html and wiki/horse-care.html; the shape of it:

Built 2026-09-05, NOT yet play-tested: magic speed, magic health and magic jump - three more genes on the MagicSizeGene pattern.

MagicSpeedGene (horsegenetics.magic_speed, priority 141, Swift/Sluggish/n), MagicHealthGene (horsegenetics.magic_health, 142, Hardy/Frail/n), MagicJumpGene (horsegenetics.magic_jump, 143, Springy/Leaden/n). 42 built-in genes, 44 in-game. Shape of it:

Built 2026-09-05, NOT yet play-tested: PAX3 SW2 is the ordinary horse - 90% of founders carry one copy.

Owner's call, and it is how minimal splash works in life: a mild splash allele is near-ubiquitous, and what one copy buys a horse is roughly what most horses look like. Pax3Gene's founder table went from hardyWeinberg(SW2 2%, SW4 0.5%) to a written-out 90% SW2/N / 1% SW4/N / 9% N/N.

Built 2026-09-05, NOT yet play-tested: face markings become a family - star, stripe and snip.

All four white loci now draw the head from one shared vocabulary, WhitePattern.faceMarking(epi, skin, strength, jag) + WhitePattern.FaceMarking, which closes the standing "every locus draws the same centreline stripe" gap. Machinery is wiki/pipeline.html#face-markings; the shape of it:

2026-09-04

Built 2026-09-04, NOT yet play-tested: the particle locus - forty alleles on one gene.

ParticleGene (horsegenetics.particle, priority 150), the largest gene in the mod by a wide margin: 40 variant alleles + n, 861 combinations, 87 outcomes. 39 built-in genes, 41 in-game. A horse trails a particle as it moves - flames, souls, snow, hearts, portal motes. Machinery is wiki/gene-particle.html; the shape of it:

Built 2026-09-04, NOT yet play-tested: seven magical utility genes, designed as a set.

The point of them is combination - broad epigenetic ranges and independent loci, so that a ten-times healer with a striped mane that spreads moss is a horse nobody wrote a line of code for. 38 built-in genes, 40 in-game at the time (39 / 41 now, with the particle locus). Machinery is wiki/gene-milk.html and its six siblings; the shape of it:

Built 2026-09-04, NOT yet play-tested: the horse dimension goes back to random pens

(roadmap §8, done). The genotype gallery is retired before anyone ever walked it.

Built 2026-09-04, NOT yet play-tested: the custom horse spawn egg's editor was rebuilt

(roadmap §9, most of what was left). client/ CustomHorseSpawnScreen is now gene list left / live 3D horse centre / controls right, and the horse that spawns is the horse you were looking at.

Built 2026-09-04, NOT yet play-tested: the trait / size / health system, and the death of the random stat roll (roadmap Tier 2 §6.1, Tier 3 §7, §4.3, §4.4, §6.2-6.4).

Thirteen new genes, a new common/trait/ package, and the removal of the last non-genetic randomness on a horse. 31 built-in genes, 33 in-game. Machinery is wiki/horse-body.html; the shape of it:

Built and owner-verified in-game 2026-09-04: the white-pattern rewrite - four real loci replace four made-up genes.

WhiteGene, SplashGene, SabinoGene and FrameGene are deleted. In their place, named for the genes they model: KitGene (horsegenetics.kit, priority 76, eight alleles), MitfGene (horsegenetics.mitf, 78, four), Pax3Gene (horsegenetics.pax3, 79, three) and EdnrbGene (horsegenetics.ednrb, 74, two). Still 18 built-in genes, 20 in-game. The rule the owner set: only alleles at exactly the same locus share a gene - so tobiano (an inversion near KIT, not a KIT variant) and roan (region-mapped, causal change unresolved) stay their own genes and compose freely with everything. Landing with it:

2026-09-03

Built 2026-09-03, NOT yet play-tested: dun becomes a three-allele locus (roadmap §4.1).

DunGene now carries D / d1 / d2 - six combinations, three outcomes - and d1 is the allele that draws the dorsal stripe without diluting anything, so a horse can carry primitive markings and not be a dun. Landing with it:

Built 2026-09-03, NOT yet play-tested: sex is a gene (roadmap §5.3, first half).

SexGene (horsegenetics.sex, priority 1 - the first gene in codeOrder()), alleles X/Y: X/X is a mare, X/Y a stallion, Y/Y cannot occur. 18 built-in genes, 20 in-game. Landing with it:

Built 2026-09-03, NOT yet play-tested: the combination-table rewrite (roadmap Tier 1 §2, Tier 2 §5.1/§5.2).

DominancePattern is deleted. A gene no longer declares a dominance label; it declares an Expression per distinct outcome - id, display name, a human-readable description, a wildType flag ("this combination changes nothing"), a masks flag, a deterministic flag and its own paint function - plus one function expressionOf(AllelePair) mapping any combination to one of them. Several pairs sharing an expression is what "dominant" meant; only the double-variant landing on a non-wild-type outcome is what "recessive" meant; two variant alleles each with an outcome plus a third for the pair of them is codominance. Works for any number of alleles with no special case. Landing with it:

Built 2026-09-03, NOT yet play-tested: the epigenome lives on HorseRecord.

HorseRecord gained epigenomeCode beside geneticCode (plus genotype() / epigenome() / genome() / hasGenome() / withGenome()), and data/HorseCoatAttachment + the horsegenetics:horse_coat attachment are deleted. Both are heritable facts assigned once at birth, so storing the genotype in two places was one fact twice - and keeping the epigenome off the record is why FamilyTreeScreen had to invent an ancestor's coat from its UUID. It now draws the real coat (closing old known gap #9's second half). HorseRecords.newFounder rolls the whole genome; HorseBreedingHandler / StallionSeedJarHandler read the parent genome off the record; the record attachment gained copyOnDeath. HorseRecordCodecs serialises epigenome_code as an optional field defaulting to "", which is the hasGenome() sentinel.

Built 2026-09-03, owner-verified: the earlier data-model rewrite (roadmap Tier 1, §2).

Every Gene now declares int priority(); codeOrder() / naturalOrder() / magicalOrder() are all derived by one sort on (priority, key) over built-ins + SpecGenes together (no hand-written lists, loaded genes interleave by priority). The genotype / epigenome code strings became gene-keyed and tolerant (<geneKey>=<a>/<b>; missing gene = wild type, unknown gene = dropped, "" = wild type) - see "The genetics model" below. coat-golden.txt regenerated: every deterministic coat is byte-identical (the pipeline is untouched); non-deterministic rows shifted because Epigenome.random / fromSeed now draw per-gene seeds in the new codeOrder() - a stored epigenome code round-trips unchanged, only the seed-derived stand-in moved. :common:test 195 green, :neoforge:build green, runServer boots clean (20 segments with the two shipped spec genes). NeoForge needed no source changes - it delegates all code parsing to common/. Closes old known-gap #18 (GeneCodeDisplay now derives its gene list, so spec genes show). Owner-verified in-game 2026-09-03: wild horses spawn and render correctly, right-click paper genome dump works. Still unconfirmed: bred foal, seed-jar round-trip, a spec gene actually showing in the display (needs a horse carrying Suntouched/Waterborn) - wiki/verification.html §0.

Standing build state

common/

neoforge-26.1.2/

runServer

runClient

2026-09-02

Built 2026-09-02, NOT yet play-tested

the seven remaining visual natural genes (roadmap §§4.1-4.2), all hand-written Genes in common/genetics/genes/:

Built 2026-09-02, NOT yet play-tested

the gameplay-layer items (roadmap wiki §§11-19, first slice). 19 new Items in item/ModItems: horse_hair + horse_hair_bundle (4 hair ↔ 1 bundle, roadmap §12.2's first two rungs), four breeding carrots (epigenetic_splice/gene_splice/stabilizer/magnifier), one generic known_gene_splice_carrot (per-gene parameterisation needs a data component - deferred), placeholder_gene_book (literal name "PLACEHOLDER GENE BOOK", stands in for the research paper), empty_seed_jar + stallion_seed_jar (both SeedJarItem, tooltip from the stored_genome component), four tickets, three whistles, and stall_sign + bound_stall_sign. The 17 non-sign items have owner-supplied textures (tickets share one, whistles share one); the two stall signs borrow minecraft:item/oak_sign - per-tier / real art is a follow-up in wiki/verification.html §15. New item/ModCreativeTabs registers one Horse Genetics tab holding all of them (20 with the custom spawn egg, which also still shows in vanilla Spawn Eggs). The dev test-world hotbar (server/DebugTestWorldHandler) gives the spawn egg on slot 0 (tools shifted to 1-6) and one of every new item in the main inventory. Recipes are datapack JSON under data/horsegenetics/recipe/ (singular). Tickets are inert (stall-teleport needs stall blocks that don't exist); the carrots do nothing yet. The whistles work - see below.

Built 2026-09-02, NOT yet play-tested

the whistles (item/WhistleItem). Right-click anywhere to teleport every tamed horse you own within range (basic 16 / golden 32 / echo 64 blocks), same dimension, not ridden, to a grid of spots beside you; ~3 s use cooldown, a chime, a chat count. Leashed horses are unleashed and come; a horse already within 3 blocks or one you are riding is left alone. This is the owner's "area recall" reading of roadmap §11; the bond-gated version waits on bond, and "what echo adds" beyond range is still open. Checklist wiki/verification.html §17.

Built 2026-09-02, NOT yet play-tested

the stall system (roadmap §11). A new bound_horse data component (data/BoundHorse), a stall_sign / bound_stall_sign item (item/StallSignItem, texture borrowed from minecraft:item/oak_sign), a server-global data/StallData SavedData of data/StallRecords (one per bound horse), a flood-fill server/StallDetector, and server/StallDebug (the "debug overlay" - a particle wireframe + chat summary). Flow: right-click a horse with a blank stall_sign (server/StallSignHandler) -> it becomes a bound_stall_sign carrying that horse's UUID + name; right-click the outside face of a wall with the bound sign -> StallSignItem.useOn drops a real oak_wall_sign with the horse's name and flood-fills the block behind that wall (this layer ± 1, air only, ≤ MAX_BLOCKS 512) - an enclosed area becomes that horse's stall, its outline flashed with HAPPY_VILLAGER particles. Breaking the sign (BreakBlockEvent) releases the stall. Dev keybind F7 (key.horsegenetics.show_stalls -> RequestStallHighlightPayload, dev-gated in ModNetworking) re-flashes every stall's outline near the player + prints a summary. No teleport-to-stall yet (that's the tickets, still inert); no client-side persistent wireframe (particles only). Checklist wiki/verification.html §18.

Built 2026-09-02, NOT yet play-tested

the stallion seed jar first slice (roadmap §15.1). New common/genetics/GenomeSample (a Genotype + Epigenome detached from a horse as code strings, with breedInto(mareGenome, rng)); new data/StoredGenome + data/ModDataComponents registering the horsegenetics:stored_genome data component (persistent Codec + networked StreamCodec); server/HorseBreedingHandler refactored so its foal-building body is a reusable applyBredFoal(...); new server/StallionSeedJarHandler - right-click a tamed adult stallion with an empty_seed_jar → a stallion_seed_jar stamped with his genome, sex, UUID, name, speed/health; right-click a tamed adult mare with a filled jar → a foal bred immediately through applyBredFoal from her live genome + the jar's stored one, jar consumed, mare put on the vanilla breeding cooldown. Both ends require the horse to be in breeding mode (isInLove() - fed a carrot/apple, works in creative); the op consumes that love state. The jar transforms in the player's hand (creative included). No real breeding-carrot gate (vanilla love is the stand-in), no gestation state (foal is immediate). Partly owner-confirmed 2026-09-02 (collection + tooltip); :common:test 194, :neoforge-26.1.2:build, runServer all green.

Built 2026-09-02, NOT yet play-tested

the horse-care systems - gated healing (roadmap §7.2) plus bond + herds (§13), the first slice of both, sharing one slow tick as the roadmap demands. All in neoforge-26.1.2/, nothing in common/:

2026-09-06

Owner-verified in the browser (2026-09-06): the gene creator opens and renders correctly on the live wiki page.

Owner's words: "that looks correct". Taken as a general confirmation that the tool loads and the horse reads right, not an item-by-item walk of the checklist - the same standing the white-pattern rewrite got on 2026-09-04. So it closes: it opens at all (the expresses ReferenceError is gone), the parity line reads green in the page, and the 3D preview reads as a horse rather than the pile of bounding boxes it was. What that cannot cover, and what stays open in wiki/verification.html §0-H, is everything you have to interact with - typing into a field, the Gameplay forms, adding the eight effect verbs, and loading Waterborn to confirm its effects survive the round-trip.

Owner-verified in-game (2026-09-06)

2026-09-05

Owner-verified in-game (2026-09-05)

2026-09-04

Owner-verified in-game (2026-09-04)

2026-09-03

Owner-verified in-game (2026-09-03)

2026-09-02

Owner-verified in-game (2026-09-02)

2026-09-01

Owner-verified in-game (2026-09-01)

2026-08-30

Owner-verified in-game (2026-08-30)

2026-09-02

Built 2026-09-02, NOT yet play-tested

data-driven genes and the rebuilt gene creator. A gene that fits the format is now a JSON file dropped in config/horsegenetics/genes/ - no Java, no rebuild - and wiki/gene-creator/ is the tool that writes it, previewing the gene on a 3D horse over any of 15 base coats before you export. Nothing about the eleven built-in genes changed: no gene file ships by default, so the registry, the genotype code, the gallery numbers and coat-golden.txt are all untouched. See "Data-driven genes" below; in-game checklist in wiki/verification.html.

Built 2026-09-02, partly play-tested

gene effects - a data-driven gene can carry Minecraft-specific behaviour alongside its coat layers. Six verbs at the time, eight since 2026-09-04 (traversal, attribute, emitter, mob_effect, yield, glow, healing, spread), each with an optional boolean when and a minDose. emitter grew a color2, a count, a data number and five body-site anchors when the particle locus landed. common/ parses and validates all of them (GeneAbility records / one AbilityType per-verb declaration / a generic GeneSpecParser.readAbility / HorseAbilities, unit-tested); the NeoForge translator (server/GeneAbilityHandler, server/GeneYieldHandler) executes traversal + emitter + mob_effect + yield + glow. attribute is the one verb parsed but not executed yet (logged once). mob_effect resolves the id against the registry and keeps a hidden/ambient effect topped up on the self / rider target on its refresh beat (duration refresh + 20, so a when going false lets it decay - no explicit removal). glow has two independent halves: light (0-15) maintains a trailing minecraft:light block server-side (moved on block change, cleared on EntityLeaveLevelEvent / when false, skipped in the horse dimension, air-only placement), and parts (a body-region list - a new AbilityType Kind.PARTS parsed through PartGroups.expand) drives client/EmissiveCoatLayer, which redraws those coat parts full-bright over the base coat via a second baked texture. walk_on_water is an approximation (surface buoyancy, not a solid plane). Two shipped genes exercise it: Waterborn (traversal + emitter + yield) - neon-blue mane/tail streaks + blue particle trail owner-confirmed in-game (2026-09-02), walk_on_water and the tamed-mare milking not yet - and Suntouched (glow + emitter) - light 12 + emissive gold mane + a gold-dust aura, the emissive mane and the area lighting both owner-confirmed in-game (2026-09-02). Reference: wiki/gene-effects.html (verbs + the "add an effect" contract), wiki/gene-waterborn.html + wiki/gene-suntouched.html (the genes), wiki/horse-traits.html (the wider architecture); checklist wiki/verification.html §13.

Play-tested 2026-09-02, works

the custom horse spawn egg (item/ModItems -> CUSTOM_HORSE_SPAWN_EGG). A plain Item reusing the vanilla minecraft:item/horse_spawn_egg texture (identical icon), in the Spawn Eggs creative tab. Right-clicking it (client/CustomHorseSpawnEggClient cancels the interaction) opens client/CustomHorseSpawnScreen. The egg, the editor and the spawn were all confirmed working, and two follow-ups were fixed the same day: a full-screen dim in extractRenderState was drawing over the buttons (widgets render during super.extractRenderState), and the gene list could run off the bottom of a short screen. The editor itself was rebuilt 2026-09-04 - see the entry at the top of this list; what carries over is the egg, the interaction cancel, and the two drawing lessons. All custom horses get their body from their genotype like any other horse.

Behaviour change 2026-09-02: Waterborn + Suntouched ship loaded.

To make the data-driven-effects work testable in-game, example.waterborn and example.suntouched are registered via a classpath gene index - neoforge-26.1.2/src/main/resources/horsegenetics/genes/index.json (["suntouched.json", "waterborn.json"]) + the two files beside it, which GeneSpecLoader.fromClasspath() picks up in the mod constructor. These are the first (and so far only) gene files to ship, breaking the "no gene ships by default" invariant on purpose (the owner OK'd it): the in-game genotype code is now 20 segments (13 at the time), GenotypeCatalog/the gallery are ~4x (each shipped DOMINANT two-allele gene doubles them), and shorter saved horses won't parse. :common:test is unaffected - the index lives in the neoforge module's resources, not on the common test classpath, so Genes stays at 11 built-ins there (17 now) and coat-golden.txt + SpecGeneTest's BUILT_IN_GENES still hold. The horse dimension will be overhauled later regardless.

Built 2026-09-02, glow owner-confirmed in-game

Suntouched (example.suntouched, allele Sntch/n, DOMINANT magical, wildOdds 128, priority 210), plus the mob_effect and glow verb translators (mob_effect has no shipped user and is unverified). Suntouched is a spec gene shipped as its own file: one deterministic coat layer (PARTS on HAIR x TOWARD gold #ffcf47 at 88%) plus an effects block of glow (light: 12, parts: ["HAIR"]) and an emitter (gold #ffcf47 dust, interval 6, body anchor). It is the worked example for glow: server/GeneAbilityHandler.reconcileGlow maintains the light block and client/EmissiveCoatLayer + GeneticCoatTextureFactory.getOrCreateEmissive draw the full-bright mane. mob_effect was wired in the same pass (applyMobEffect) but nothing shipped uses it now. See wiki/gene-suntouched.html; checklist wiki/verification.html §13.

Docs 2026-09-02, no behaviour change

the Docs/*.md -> wiki conversion. All five markdown docs are gone; their content lives in wiki/*.html (see the Docs-split section above), the four Javadoc comments that named them were repointed, and Gene Dict.md was split into a page per gene - all 11 now documented, where only 4 were before (and two of those pointed at a CreamPearlGene.java that has never existed). Two pages are new rather than converted: wiki/modding.html (how to write a gene) and wiki/api-reference.html (class abstractions), which is the modder-facing documentation the roadmap assumed would exist. wiki/nav.js builds every sidebar from one array, so a new page is one line. No Java behaviour changed - only Javadoc text - and :common:test (153) and :neoforge-26.1.2:build are both green. Nothing to play-test.

Built 2026-09-02, NOT yet play-tested

the first two magical genes - magic zebra (Mzeb, dominant, 1/100 per allele) and pink hair (Pihr, recessive, 1/12 per allele). Both are phase-3 genes; details in wiki/gene-*.html, in-game checklist in wiki/verification.html. They take the registry to 11 genes, which moves a lot of derived numbers: the code string is 11 segments, breedWith draws 22 booleans, and the gallery goes from 434 pens / 1 519 blocks to 1 730 pens / 6 055 blocks of corridor (long since overtaken - see the white-pattern entry). Sample bakes look right (stripes read black over cremello and over dominant white; pink manes keep their strand shading on black, chestnut and perlino alike); nothing seen in-game.

Built 2026-09-02, behaviour-neutral by construction

the three-phase pigment pipeline (wiki/roadmap.html §1). Phase 3 is now a signed, uncapped ColorField that magical genes add into, both gene hooks are pure (read-only views in, a contribution out), and CoatBuildContext no longer carries scratch space. No coat changed: CoatPipelineGoldenTest hashes 20 genotypes × 3 seeds × adult/foal and every one is byte-identical to the pre-refactor bake, so there is nothing new to play-test - it's groundwork for the magical genes. Nothing in neoforge-26.1.2/ needed touching, which was the test of whether the refactor stayed inside common/.

2026-09-01

Built 2026-09-01, NOT yet play-tested

per-allele epigenetics and the dapple-grey rework. Epigenetics moved off the horse and onto the allele copy (Epigenome / Genome / AlleleEpigenetics, each copy carrying a priority + epigeneticSeed, inherited unchanged by a foal); GreyGene now renders a real dapple grey through the new GreyCoat + BodyNoise; and bay's leg black is a uniform per-horse extent with per-leg jitter instead of the old low-biased single number. Compiles, 138 common tests pass, sample bakes look right, nothing seen in-game yet - checklist in wiki/verification.html.

Built 2026-09-01, never play-tested, and superseded 2026-09-04

the genotype gallery rework of the horse dimension - one pen per visually distinct genotype, the entrance tally sign, and the per-gene distinctness metadata. Reverted to random pens before anyone walked it. What survives from it: the per-pen genotype sign, the pair per pen, and the entity-only teardown that leaves blocks standing.

Built 2026-09-01, NOT yet play-tested

the dev test-world auto-delete

shutdown (and sweeps leftovers on the next start), so the button stops filling run/saves. See "Running the game".

Standing build state

Open issues + NOT verified in-game

see wiki/verification.html. The two newest items are the top of the list, and they are the same play session: PAX3 SW2 on 90% of founders (does a herd still read as a population rather than one horse repeated - and does it read as socks or as four white legs, which would be gap #30 rather than the frequency) and the face-marking family (does a three-to-five-texel star read as a star at 128px, does a snip land on the nostrils, is the star above the eyes given the head's approximate rest-pose projection). After those: the particle locus (§0c - forty particle ids, six body sites, none of it seen; the emitter-style ones and the ten Bedrock substitutions are the likeliest to read badly), the seven magical utility genes (§0a - and inside that, the walking animation of a scaled horse and whether the wild size spread reads right across a herd), the trait / health layer (§0b), the random pens and the rebuilt spawn egg, then foals (only spot-checked). Update it after each runClient.

Machine caveat (this dev laptop)

hybrid graphics (NVIDIA RTX 3050 Ti + AMD integrated). java.exe/javaw.exe are pinned to the NVIDIA GPU and the FML splash is disabled, or the JVM hard-crashes in the AMD GL driver. See "Running the game".