Project / what is wrong, missing, or unproven
Known gaps & lessons
The near-term list: things that are broken, half-built, or built on an assumption
nobody has checked, plus the recurring failure modes this project keeps rediscovering.
Two neighbours own the rest — the verification
checklist holds what needs a runClient session, and the
roadmap holds long-range work that was never started.
Gaps are referenced by number elsewhere (“known gap
#13”), so each is anchored as #gap-13; when one closes, delete it
and leave the rest numbered as they are rather than renumbering.
The runClient checklist lives in wiki/verification.html - both the open issues found in-game and what's still unconfirmed. Keep that file current after each session. The long-range backlog (the full gene wishlist, per-allele stack priority, the modder-facing gene API, non-coat and health genes) lives in wiki/roadmap.html, which is priority-ordered and keyed by anchor rather than by section number; this list stays near-term.
Fixed since that session: grey (was "flat near-white, wants a rework") is now the GreyCoat dapple grey - built, unit-tested and sample-baked, not yet seen in-game.
Both 2026-09-01 splash issues are closed - not fixed, superseded. Splash "not reading its own dose" and its "perfect ring" sock edges both went with the gene: MITF and PAX3 have real per-combination outcomes, and WhitePattern.splash is one wobbled waterline whose crossing height on a leg is irregular by construction. CoatRegions.whitenLowerLeg / whitenBlaze now have no callers at all.
Closed 2026-09-05 (built, not play-tested): the face-marking family. All four white loci draw the head from one shared vocabulary now - WhitePattern.faceMarking + FaceMarking, three components plus a width, with star and snip as real detached patches. See the status entry above and wiki/pipeline.html#face-markings. What is left of it is a play-test (does a three-to-five-texel star read as a star at 128px?) and the two follow-ups below.
Fixed 2026-09-05 (built, not play-tested): splash was coming out bare-faced. The face-marking rework made MITF/PAX3 splash scale its face marking with its body strength like KIT does, and a single-copy splash sits near body strength 0.35 - star / snip / nothing territory - so splash horses lost their blaze. WhitePattern.splash now runs the face marking at clamp01(s + SPLASH_FACE_BOOST) (SPLASH_FACE_BOOST = 0.34): one copy -> a blaze, a homozygote or two loci -> a bald face, which is the splash phenotype. Same RNG draw count (only thresholds move), so alignment holds; coat-golden.txt regenerated (splash rows moved). KIT and EDNRB face markings are untouched. New WhitePatternGenesTest.everySplashHorseWearsAFaceMarking. Measured: mitf=SW1/N head coverage went from 0-66 texels (seed-dependent, often near zero) to 125-166; SW1/SW1 and two-locus splash fill the muzzle (bald face). Checklist: wiki/verification.html §0-C.
Fixed 2026-09-06 (built, not play-tested): partial white was browning on black horses. The "odd 1px gold fleck" logged against roan above was not a roan bug and not 1px - it was every partial whitening in the mod, and the two genes that do a lot of it (classic roan and the leopard complex's varnish) were drawing chocolate and tan through a jet-black horse. A black horse stores (red = 1, black = 1): the red is masked by the black rather than absent, so scaling both channels toward zero unmasks it and the sample walks the gradient's gold diagonal. PigmentField.whiten is the shared verb now - it holds the visible red constant, which on a black horse is zero, so the fade runs down the neutral red = 0 column. Measured on a side view: blue roan 194 warm texels -> 65, black leopard 211 -> 59, against the 64 a plain black horse gets from the template's own hoof and nostril shading. Red-based coats did not move (bay roan 1147 -> 1155 warm, and the golden file's only changed rows carry roan=Rn or leopard=LP). Every hard white marking uses the same verb at full strength and is byte-identical. Data-driven genes get it as the new WHITEN op. Checklist: wiki/verification.html §0-F3.
The 2026-09-02 visual genes (2026-09-02, reworked once after owner feedback) - remaining follow-ups, none seen in-game:
- Dun leg barring was a hand-rolled, evenly spaced Y-phase that did not render in-game at all (owner-observed). Rewritten: noise-warped band phase, per-band thickness and strength, a round-the-limb break that turns a ring into a slash, a window centred on the knee / hock, and a separate seed and presence roll per leg. Unverified in-game - see verification §0-D. What is left is that
d1shows only the dorsal stripe, where a real non-dun-1 horse can also carry faint bars and shoulder shadowing. - Silver has no dapples yet - v1 is the dilution only. A deterministic (fixed-seed)
BodyNoisedapple modulation is the obvious next step. The flaxen mane currently reads a little gold rather than pale. EDNRBpaints lethal white but doesn't kill.O/Ohas its own all-white masking outcome and gets a pen, which is the honest half; the foal then lives, because there is no health system. The coat half is done, so this is now waiting on that system rather than on gene work.- Every white-pattern threshold is eyeballed off sample bakes, not play-tested - the
KITstrength ladder especially. The numbers are monotone (a unit test pins that) and measure 5.7 / 11 / 15 / 25 / 25 / 73 / 94 / 100 % coverage, but whether those read as eight distinguishable steps on a 3D horse is the open question. - "White finds white" has never been seen stacked more than two deep. Each white-pattern painter raises its strength by how white the horse already is, which is what makes the two splash loci and
W20mean anything - but a horse carrying tobiano andKITand both splash loci compounds four times. It cannot run away (strength clamps at 1), but it has not been looked at. - The wide white-pattern genes don't compose an eye-safe check - a big
SB1/SB1or a topline-crossing tobiano could in principle wipe the eye texels;CoatRegions.redrawEyesruns last so eyes always come back, but confirm in-game.
Design follow-ups (not just "go look at it"):
- The holy ward cannot be tested in the horse dimension at all, and the pen built to test it was measuring something else. Two independent facts, either of which is enough on its own. One:
GeneWardHandler.isNaturalgates on the spawn reason and deliberately lets spawner blocks, spawn eggs, breeding, structures, dispensers and commands straight through — so the yard's zombie spawner produces mobs a working ward is supposed to ignore, and its distance column proves nothing about warding. Two:debug_pensgeneratesminecraft:the_void, and the void biome carries no mob spawn entries, so natural spawning cannot happen anywhere in that dimension at any light level. Add gap 180 — even in a real world the warding half only bites more than 24 blocks from every player — and the conclusion is that the gene's headline claim has never been tested and cannot be tested where the yard is. What the pen can test, and what §0-BT ranks higher anyway, is the non-interference half: the ward hooks a global high-frequency event, and if its allow-list is wrong a player's mob farm stops producing and nothing says why. The pen is re-signed for that. Half closed 2026-09-13: the dimension has its own biome now (horsegenetics:debug_pens) with zombies in its monster spawner list, so there is finally something in there for a ward to stop — the second reason above is gone. The first stands: the yard's spawner mobs still pass through by design, so the spawner room tests non-interference and nothing else. And the arrangement exists now (§0-DD): two identical dark chambers at the far end of the yard, one warded and one not, 34 blocks apart so the ward cannot reach the control, with a block of cyan wool on the walkway 30 blocks short of both — nearest corner 32.3 blocks, symmetric to 0.00. So the only part of this gap still open is running it. Every hostile spawn now logs its reason as well as its distance to the nearest live ward, which is what makes that reading interpretable at all. - Two thirds of every
saplingspread is thrown away on geometry, and the gene's advertised interval is therefore three times too optimistic.GeneAbilityHandler.spreadpicks one block per interval from a cube ofradiusaround the horse with a vertical offset of −1, 0 or +1. For thesaplingcover — the only one that builds upward — offset −1 is the ground block itself and is not air, and +1 is air with air beneath it, so only offset 0 can ever succeed. Dryad says “roughly once a day”; measured, it planted three times in three hours of continuous running. The same arithmetic quietly applies to the converting covers in reverse (they can only ever hit the two lower offsets). Either the vertical pick should depend on the cover, or the pages should state the real rate. Half-done 2026-09-13: the dryad's page states the real rate now, so nobody is misled, but the waste is still there and it is worse than it was — the locus has nine planting alleles rather than one. The fix is small and deliberately not taken yet, because the vertical offset is shared withmeltand the three ground covers, where the same three candidates are exactly right and changing them would quietly retune verdant and hot blooded as a side effect. - Nothing in the mod bounds an item-dropping gene against vanilla's despawn timer, and the one cap we have may never be reachable. The
produceverb'snearby_capcounts item entities on the ground within six blocks, and egg layer sets it to eight. A copy's interval is 4 000–14 000 ticks; a dropped item despawns at 6 000. So a lone layer's floor clears itself about as fast as it fills, and the guard that exists to stop a carpet of eggs may never once fire in a real game. That is not obviously wrong — the despawn is doing the guard's job — but it means the cap is untested by construction and would not be noticed if it were broken, which is the same shape as every other bug this project has found in its own scaffolding. The test yard stocks four layers in one pen specifically so the cap can be made to fire at all. Worth deciding, once there are readings: either the cap is a real limit and wants an interval that can outpace the despawn, or it is a belt beside a brace and should say so on the verb's page. - Whether
SpontaneousBreedingHandler.LOCAL_CAPactually stops a population is unverified, and the pen that would have answered it has been retired. That the gene breeds is owner-confirmed (2026-09-12). That it stops is not, and it is the half that matters to a save: this is the only gene in the mod that can grow a population while the player is asleep, and an uncapped automatic breeder is the classic way to kill a server. The cap counts horses within eight blocks and refuses above six, so a single pasture should settle — but the count is per-horse and local, so two clusters twenty blocks apart are two separate caps and a large paddock could hold several. The breeding field was dropped from the test yard on the owner's call the same day (“we're done with those tests”), so this is recorded as dropped rather than answered. Putting it back is one call tobuildBreedingField; it wants a long unattended run and a headcount, which the watch now takes automatically every two minutes. - Horses keep the F8 outline after the highlight is off, and there is no way to reach them. Reported from play: “there’s an area of the horse dimension where a bunch of horses had an outline, F8 was not on”.
MobEffects.GLOWINGis applied in exactly one place in this mod —DebugHighlightHandler.glowNear, which gives nearby horses a 60-tick effect and refreshes it every tick while the highlight is on — and removed in exactly one,clearNear, which sweeps the horses near the player when it goes off. The mismatch is the bug: it lights horses by proximity at the time, and clears them by proximity later. Ride away from a lit group and switch off, and nothing ever visits them again. Ordinarily the effect would expire on its own in three seconds, but effect timers do not tick in unloaded chunks, so a horse lit and then left behind holds the outline frozen until somebody walks back into its chunk — which is exactly the “an area where a bunch of horses had an outline” shape of the report. The fix is to track what was lit rather than infer it: keep the set of UUIDsglowNeartouched and clear that set on toggle-off, on logout and on dimension change, instead of re-deriving it from where the player happens to be standing. Cosmetic and debug-only — it cannot affect a release build, wheredebug.toolsis off — but it is actively misleading during exactly the play sessions this tooling exists to serve, since an outline is also how the yard says “look at this one”. - The horse dimension's three protections are switched OFF, and must
go back before 1.0. Opened 2026-09-12, owner's call, while the behaviour genes
are being tested. All three existed to keep the gallery a gallery, and all three made
the test yard - the only place built for watching these genes - unable to show them:
- Non-horse mobs were deleted on arrival
(
HorseGeneticsEventHandler.keepDebugDimensionHorsesOnly), so the holy ward had no spawner output to protect and the pack leader had nothing to lead. - Spreading genes refused to run
(
GeneAbilityHandler.spread), so dryad, and verdant's moss, grass and mycelium, were silent no-ops. This is the one the owner hit: "dryad does not work". - Glow genes placed no light block, so their light could not be judged there either.
RESTORE BEFORE 1.0comment. The version that comes back should not be the version that left: all three treated the whole dimension as a display case, when what they meant was the corridor. The yard is past the corridor's outer wall andDebugPenManager.corridorWallZ()is the line, so a restored rule can protect the gallery and still let the pens be pens. Left off rather than narrowed today because the owner asked for them off, and a half-narrowed rule is the kind of thing that silently blocks the next test instead. - Non-horse mobs were deleted on arrival
(
- The gene creator cannot show a glow, so the one property
it writes blind is the one it writes most often. Opened 2026-09-12 with
glow intensity. A layer's
emissiveis a level now — a number, a knob, the gene's dial — and the creator's preview is a flat unlit sheet, so there is nothing for a lit texel to be brighter than: a glow at 1.0, a glow at 0.05 and a glow written into the wrong layer all draw identically. That is how the glow came to be dropped from the creator's export entirely and stay dropped (fixed the same day, and only found by reading the exporter).wiki/tools/check-glow-control.mjscovers the model — what is written, what survives the tidy, what the validator names — which is the honest substitute, not a fix. A real fix means shading in the preview: light the sheet in the 3D viewport and draw the emissive texels unshaded, which is the only place in either editor where a second material would earn its keep. Until then a glow is authored by arithmetic and confirmed in game (§0-CO). - A third-party Java gene cannot be put back once
clearLoaded()has dropped it. Opened 2026-09-11, with the open registry.clearLoaded()means "back to the built-ins", and it re-reads the shipped gene files on the way out so the drop-in population is restored — that is what its note is about. It cannot do the same for a gene another mod handed us: aSpecGeneis a file we can re-read, a Java gene is an object we were given once. So it clears them and they stay cleared. Nothing in the game calls it — it is the tests' reset, and it also thaws the freeze for the same reason — so this is latent rather than live, and it is recorded because the failure would be quiet: the registry would come back one locus shorter and every genotype code written since would mean something else. If a reload path is ever wanted in game, the fix is to keep the registration callbacks rather than the gene objects, and re-run them. :webis not built by any gate, so it can be broken for a whole session without anything going red. Opened 2026-09-11, having done exactly that. A change toCarrotEffect's shape leftweb/DesignerApicalling a constructor that no longer existed;:neoforge-26.1.2:builddoes not depend on:web, so every build afterwards was green, the tests were green, and the break only surfaced when:web:bakeDesignerAssetswas run by hand at the end of the session. Two commits ofcommon/had shipped in the meantime with a stale wasm beside them, which is the failure gap 45 and the "git status showing common/ changed and wasm unchanged" warning both exist to catch - and neither catches it, because both assume somebody looked. The cheap fix is making the session's green check compile:web; the honest one is a task that fails when the wasm is older than thecommon/it was built from.- The horse browser's gene and allele pickers inherit the roster cap, so they describe what the tab can see rather than what the player owns. Opened 2026-09-11 with the pickers. They are built from the same capped, client-side roster the tab filters over (gap 76), so on a large stable a gene held only by a horse past the cap is not offered - and unlike a missing row, a missing gene is invisible: the list simply does not mention it, and there is no footer saying so. One more consumer of the owned-horse index gap 76 wants, and the one where the cap lies rather than truncating.
- Two goals share a priority and the tie is broken by insertion order, which nothing declares. Opened 2026-09-11.
FoodTemptGoalis registered atCrouchFeedGoal's priority and after it, so on an untamed horse held food the crouch-taming approach wins - both claimMOVE, and vanilla's goal selector breaks a priority tie by the order goals were added. That is a real behaviour resting on an implementation detail of the selector and on the order of two lines inHorseCareHandler.onHorseJoin: reorder them and taming quietly stops working while both goals still look right. It wants either distinct priorities or acanUseon the tempt goal that stands down explicitly when the crouch conditions hold. - Feeding a foal is a bond source and a growth accelerator at the same time, so the two look causally linked. Opened 2026-09-11 from a playtester report - "a horse just randomly got bigger, and he seems to be getting bigger the higher their bond level gets". Investigated and the mod is innocent: there is exactly one write to
Attributes.SCALEin the codebase (HorseRecords.applyTraitsToEntity), it is asetBaseValueso it cannot accumulate, its input is the genotype plus the epigenome plus a config flag - all immutable for a living horse - and the re-resolve is guarded to run once per horse. No horse's scale changes while it is alive. What was seen is a foal reaching adulthood, which is one discrete size jump. The bond correlation is real but shared-cause:HorseCareHandler.onFeedawards bond and does not cancel the interaction, so vanilla's feed also runs, and on a baby vanilla's feed isageUp- 10% off the remaining growth per feed. A player hand-feeding a foal for bond is accelerating its growth with the same click, and a foal in a herd earns double bond (HorseCareHandler'sinHerd() && isBaby()doubling) while ageing at the same rate, which tightens the apparent link further. The owner's call (2026-09-11) is to leave the mechanic alone because it is vanilla; what is missing is that nothing tells the player, so the same report will arrive again. The cheap fix is wording - the bond surface saying that feeding a foal also grows it - rather than code. CoatBakeGoldenTestis structurally blind to a heterozygote. Opened 2026-09-11. The corpus golden walksGenes.codeOrder()- it has no list to fall behind, which is the whole reason it exists - but it bakes one line per gene per variant allele asnew AllelePair(variant, variant). Every horse in it is homozygous, so no combination that only misbehaves with two different alleles can ever move a hash there. That is exactly the shape of the magic zebra defect fixed the same day: one copy drew the full pattern, and the bake golden stayed green through the fix while the pipeline golden moved 48 rows. The pipeline golden does cover heterozygotes, but only for the genotypes on its hand-picked list - which is the coverage hole the bake golden was written to close. So between them the corpus is covered homozygous and the list is covered both ways, and a codominance or dominance defect in any gene off that list is invisible to both. A second bake pass pairing each variant with the gene's default allele would roughly double the file and close it.- The
GOOmask is compared texel by texel and nowhere else. Opened 2026-09-11. A new mask is supposed to come with an answer table inSpecFixtureToolwhen it carries self-contained arithmetic, the wayschema.svgpins the path grammar down point for point; goo's distance field - capsule, tip disc, polynomial smooth-minimum, and the body-unit conversion out of the band's own space - has none. What it does have is a layer inprismatic.json, and that layer was watched go red: a port returning zero fails 43 checks. Three disagreements were tried against it, and the results say exactly how far it reaches: a port returning zero fails 43 checks, and dropping the end-face wrap fails one - but multiplying the field by 0.98, and changingsag's bite from 1.4 to 1.1, both stay green, because no sampled texel sits in the soft edge or between two drips where those show. So the port is proven present, not proven exact, and a future edit to the drip arithmetic can drift without the gate noticing. - No 0.4.1 world has been opened on 0.5.000. Opened 2026-09-11. Released jars have players on them, so this one was built to load an older world rather than refuse it: a saved genotype code is read through
Genotype.readableStored, which drops any segment naming an allele that no longer exists - the only one found by diffing the gene bundles is Cleave'sClv/Clc/Wav/Wac, retired when Cleave became two loci - so that horse comes back with the locus at its default instead of throwing from every later parse (which from an entity tick takes the world down). Two things are unverified: that no Java gene retired a token too (the diff only covered the spec genes), and thatEpigenome.parsetolerates a copy written against a gene's old schema. Opening a 0.4.1 world with a few hundred horses in it on the new jar would settle both. - A written
breed-spawning.tomlkeeps the values it was written with. Opened 2026-09-11. Each shipped breed's section defaults to what its file said when the settings file was first made, so if a later release moves a breed to new biomes or changes its weight, an install that already has the file keeps the old numbers and the change never arrives. Deleting a section puts the current defaults back. The alternative - blank fields meaning "whatever the breed file says" - would keep up with releases but make the file a list of empty boxes, which is the thing it exists not to be. Relatedly, the breed book on a remote server reads the client's own copy of the file, so it can say a breed is common when the server has switched it off. - Breed spawn settings are per install, not per world. Opened 2026-09-11.
breed-spawning.tomlis a COMMON config because the herd biome modifier needs it before any world exists, so every world opened on an install shares one ecology. A world with its own story - "only my breeds, in this save only" - needs a per-world copy. NeoForge's SERVER override would give one for free (<world>/phc/breed-spawning.toml, given the../phc/file name), but a SERVER config is synced to clients and this spec is built from the host's own breeds, so a client with different drop-ins would receive a file its spec does not match. The workable version is probably the mod reading a per-world file itself atServerAboutToStartEvent, which runs after the biome modifiers - so herd biomes would still come from the install file. - A night breed's herds come almost only from new chunks. Opened 2026-09-10.
spawn_timeis honoured when a herd is founded, but vanilla's animal spawn rule wants light, so on chunks already around a player no horse spawns in the dark at all and a night breed is only ever founded by chunk generation after dark. That is written down on the format page rather than fixed; the fix, if one is wanted, is a spawn-placement rule that lets a horse spawn in the dark in a biome whose only wild breeds are night breeds. - Two tests were red on
mainbefore the 2026-09-10 evening intake, and nothing records why. Opened 2026-09-10.GameteBiasTest.epigeneticSpliceRerollsEpigeneticsNotAlleles(“seed 0: alleles unchanged” - an epigenetic splice is changing a foal's alleles, which it exists not to do, or the test's stream assumption has gone stale) andCreatorMetadataRoundTripTest(“the creator should be able to emit every verb the game declares”: 23 declared, 16 in the creator - seveneffectsverbs have been added toAbilityTypewithout their creator half, which is the verb contract drifting). Both fail identically on a clean worktree at1cf48a8, so neither came from the intake. The first is the one that matters: if splicing really does move alleles, a paid-for epigenetic splice is quietly rerolling a foal's coat. - Priority is the paint order and the sidebar family, and
nothing checks either against the hand-written genes. Opened 2026-09-10.
GeneFilesTestkeeps gene files off each other's priorities, but not off the Java genes', and the 100–199 band is dense with those (eye glow 121–125, the yield and behaviour genes 130–182). So dorsal wing ties eye glow at 121, contour at 122, dripwork at 124, nacre saddle ties particle at 150, starburst rainbow dust at 151, bargello lycan at 152 - all broken alphabetically, which is the thing the file-level check exists to prevent. Worse, seven coat genes from the 2026-09-10 afternoon intake sit in 140–149, whichGeneFamilybands as body-stat: coronal, bracketed, witchfire, maelstrom, sunspiral, geode and agatebound are listed in the sidebar under “Magical body-stat genes” beside magic speed and magic health. Moving them is cheap (free slots, the genotype codes in the pipeline golden move, no coat does) but it is re-doing a shipped intake, so it waits for the owner. The check that would stop it recurring: extendshippedGenePrioritiesAreDistinctto every registered gene, and assert that a data-driven gene's family is a coat family. - On
WAVES,axismeans the opposite of what it means everywhere else. Opened 2026-09-10.axisis the direction the wave travels andacrossis the axis the band sits on; onAXIS,axisis the band. Ooze drip and rainbow drip both arrived written the natural way and ran sideways. The descriptions now say so inSpecSchema, the creator and both tables on the making-a-gene page, but the names are still a trap. Fourteen shipped genes useaxis: "Y", across: "X"- a band across X with lobes running up and down - and some of them surely mean it (cleave's vertical bands). None has been audited against its blurb. Renaming the parameters would be the real fix and is a format change. - Ringwork's rings are dead for a different reason than its baseline
line says, and its other two layers are inside out. Opened 2026-09-10.
DeadLayerTest's javadoc anddead-layers.txtput ringwork's two contour rings down to being 0.1 units wide. They are not narrow, they are empty: undermeasure: centroidaCRACKLEmask covers everything farther thangap/2body units from the centre, ringwork wrote its gaps as fractions of the cell (the schema used to describe the field as 0 at the middle and 1 at the rim), and each ring is “outside 0.43 and inside 0.33”. The same misreading makes “the pale ground inside each cell” cover 99.6% of the horse and “the core, on most cells” 88%. It still looks like something, which is why nobody noticed. Contour cells arrived with the identical construction and was rebuilt on bands of wall distance, which is the fix here too. Crazework writes the same fractional gaps and has not been examined. - A mark at the centre of a lattice cell rarely reaches the skin,
because the lattice is three-dimensional. Opened 2026-09-10.
SPOTS,RINGSandCRACKLEall place their cell centres in 3D, and the body surface slices through the cells wherever it happens to lie - so a centre mark of radius r shows only where the skin passes within r of a centre, and how often that happens falls away roughly as r³. Concentric designs lose their middle first: the baseline's ocular pupil, moth wings' pupil, dorsal shield's stud and concentric eyespots' 0.8 band are all this, and the evening intake arrived with three more (agate eye's core, corolla's eye, contour cells' pip), all enlarged to about 1.0 to 1.3 units to get them onto the horse at all. ASPOTSstretchon Z does not help - the centres still jitter through the full depth of the cell. The real fix is an option that snaps a cell's centre onto the surface before measuring, so every cell the skin cuts is cut through its middle; it would be a new parameter on three masks, and so the mask contract three times. - A gene can be carried, expressed, and show nothing - and the icon
tool photographs exactly one horse. Opened 2026-09-10. A marking gated by
PATCHES, or scattered at lowchanceover a small region, is absent on some horses by design. Contour cells shows no patch on about one horse in forty, the icon tool seeds its horse from the gene key, and that horse is one of them - so the gene has no icon, which is the signal the tool reserves for “paints nothing”. Agate eye was worse before itschancewent up (three horses in eight showed no eye). Nothing measures how often an expressing horse is blank; a check that painted forty seeds and reported the blank share would, and would also let the icon tool pick a horse that shows the gene. - Holy ward's radius sits inside the zone where monsters never spawn
anyway. Opened 2026-09-10, found while working out how the owner could test it.
Vanilla never naturally spawns a monster within 24 blocks of a player, and the ward
reaches 8 to 16 blocks (
HolyWardGene.MIN_RADIUS/MAX_RADIUS). So whenever you are standing with your warding horse, the ward is doing nothing you could not already count on; it only matters for a horse at least 24 blocks from every player - a paddock you are not in. That may be exactly the right job for it (guarding the field while you are in the house), but it is not what the name suggests, and a player testing it beside the horse will conclude it is broken. Either the radius goes past 24, or the gene page says plainly what it protects. Owner's call. - The
OnFeedtrigger exists only for built-in genes. Opened 2026-09-10, when the spawner moved off its timer and onto feeding.GeneAbility.Trigger.OnFeedis fired byGeneAbilityHandler.onFeed, butGeneSpecParser, the gene creator’s schema and its parity fixtures do not know it, so a gene file cannot say"trigger": "on_feed". That is deliberate rather than forgotten: adding a trigger to the format is the multi-file change on the contracts list, and it was not worth doing for one gene. It is the first thing to do if a second gene wants to react to being fed. - A fireproof horse crawls through lava, and making it faster needs a
mixin. Opened 2026-09-10. It now floats (a
GRAVITYmodifier), but vanilla lava travel moves at a fixed0.02with half-speed drag and reads no attribute, so nothing short of replacingtravelInLavaor overridingcanStandOnFluidwill speed it up - and this module has no mixins. Owner’s call whether a lava crossing that slow is acceptable, or whether it is worth the first mixin. The behaviour is owner-confirmed, 2026-09-13 — “lava crawl works as intended, both that you float and that the rider doesn’t take damage as long as they’re mounted”. So the float and the rider immunity are settled and only the speed decision is still open. Note the clause that is doing work in that sentence: as long as they are mounted. A rider who dismounts mid-lake is standing in lava, which is the right outcome and worth keeping in mind before anybody “fixes” it. - The broadest coat test only ever varies the first 35 loci, because
its gene mask is a
long. Opened 2026-09-10, pre-existing and found by reading the test while waiting for it.CoatTextureComposerTest.everyBuiltInGeneComboComposesWithoutThrowingAdultAndFoalbuilds each genotype from a bitmask overGenes.codeOrder()— bit set means the gene’s first allele, clear means its last — and writes(1L << n) - 1for “every gene on”. Java masks a shift distance to the low six bits, so withnpast 64 that silently becomes1L << (n & 63). At 291 genes the shift is 35: the “all on” mask is wrong, and all three thousand random masks leave every locus from the 36th onward pinned to its last allele. Nothing goes red — the test only asserts that a bake returns a full sheet and does not throw, so narrower coverage is invisible. It broke the moment the registry passed 64 genes, which was a long time ago, and the single-bit masks added in the loop above it still touch every locus one at a time, which is why it has been quietly useful rather than useless. The fix is ajava.util.BitSetor aboolean[]; the reason it is worth fixing rather than deleting is that this is the one test that would catch a gene that throws only in combination with another.
Also worth knowing: this test is roughly six and a half thousand whole coat bakes, adult and foal, and is on its own a large share of the ten-minute suite. Any fix should sample rather than widen. - “Broadly white however it got there” is an estimate now,
not a measurement. Opened 2026-09-10, with the eye loci. The rule that gives a
horse blue eyes when it is white from two mild alleles stacking used to read the
finished coat — the fraction of mapped texels left with no pigment
at all — and that is exactly the right signal, because no per-locus test can see a
stack. It cannot be used any more: an eye request is
applied when the horse is bred, and there is no coat at that point and no cheap
way to make one.
WhitePatternEyes.whiteScoreis a declared sum instead — each white locus says how much of the horse it takes the pigment off, throughWhiteExtent, using the painters’ own strength constants where there is one to borrow and a hand-set number where there is not (tobiano). The case it will get wrong is a horse whose loci stack in a way a flat sum does not model: two patterns covering the same half of the horse add to more than they cover, and two that avoid each other add to less. Nothing goes red either way; the horse simply has the wrong eyes. A real fix would need the founder roll to bake a throwaway coat, which is far too expensive for what it buys. - Magic sectoral heterochromia is now redundant, and nothing has been decided about it. Opened 2026-09-10. The locus exists to put two colours in one iris; the sector and sector-colour loci now do that per eye, with a named shape, inheritably, and with a far larger outcome space. The magic locus still works and still paints over the top of everything, so nothing is broken — but it is fifteen expressions and a whole gene page describing a thing the model does better one layer down. Either it goes, or it keeps the one property the loci cannot reproduce (its expressing combination is the heterozygote, which is unique in the mod) and its page says so. Owner’s call.
- Red is the one iris colour with no allele behind it. Opened
2026-09-10. The dhampir paints a deep red iris at
EyeColor.RANK_MAGICAL, and nothing else in the mod can be red-eyed: theEyeHuepalette runs brown, green, gold, three blues, white, chaos, invisible. Every other colour a magical gene paints is now reachable as an allele, which makes this the odd one out rather than a design. AddingREDto the iris palette is three lines and a founder weight; the question is whether a red-eyed horse should be breedable at all, or whether it should stay something only the dhampir locus can produce. - Nothing checks that a registered effect verb has a section on the
page that documents them. Opened 2026-09-09.
night_temperandnight_watchwere registered onAbilityTypeand shipped in two loci, and the effects reference did not mention them at all — not marked unbuilt, not marked undocumented, simply absent, which reads exactly like a verb that does not exist. They were found by countingregister(new AbilityType(against the page while merging it, which is not a method. This is the same shape as the gap that producedwiki/tools/check-gene-tabs.mjs, and the same fix works: a check that walksAbilityType’s registry and fails on the first verb with noidon the verb list. It is cheap because the ids are already the verb names. The general form is worth stating: the wiki has checks for links, for tabs and for parity, and none for coverage — a registry growing an entry the docs never hear about is silent in every direction. - The mask and op parameters are written out twice on one page, and
only prose stops them diverging. Opened 2026-09-09 with the merge.
Masks & ops carries the prose table a reader
works from; The prompt carries a terse one-line
form of the same defaults, because that tab has to be pasteable into a model in one
piece. Both were transcribed from
expected.jsonand agreed when written. Nothing compares them. This is strictly better than the old arrangement, where the two copies were on different pages and one could go a week behind without anyone holding both in view — but the failure mode is unchanged, and it is the failure mode that let five masks go missing in the first place. The honest fix is to generate one or both out of the fixture at bake time, the way the gene pages are generated; the cheap one is a node check that both tables name the same mask set with the same defaults. - Hearts on bond, the foal's inherited bond, and crouch-feed's tuning have not been played.
Opened 2026-09-09 with 0.3.8. Taming by hand itself is owner-confirmed (2026-09-10).
CrouchFeedGoalre-tests its four conditions every tick, so the failure mode nobody will notice is the goal starting too easily and pulling horses out of a herd at a distance whenever somebody walks past crouched with wheat. The tuning is all guesses - a 12-block search, a 25-degree gaze cone, a 2-block tolerance on standing still, 45 ticks a mouthful. The taming roll itself is vanilla's own and is the one part not in doubt. Separately: hearts now fire fromawardBondwhenever a point is actually granted, which means they also stop at the daily cap - that is deliberate and is the only signal a player has that the cap exists, but nobody has watched it happen. And a foal born to a tamed dam starts at a quarter of her bond, written straight onto the attachment rather than throughawardBond; if that number reads as too generous it is one division to change. - The phantom saddle is a real saddle, and the only thing stopping it being a duplicator is a sweep.
Opened 2026-09-09 with 0.4.0. Bareback steering at bond tier 3 works by equipping an
actual
Items.SADDLEcarrying aPHANTOM_SADDLEcomponent, because vanilla decides who may steer insideAbstractHorse.getControllingPassenger(), that check isisSaddled(), there is no hook on it and this module has no mixins. Doing it for real buys the whole vanilla mounted path - client prediction, jumping, the correct acceleration - and the first version, which drove the horse from outside on the tick, had none of that. The risk is that the saddle is genuine. A player can open a horse's inventory while mounted, soreclaimsweeps the rider's inventory and their held-cursor stack every tick and destroys anything carrying the component. That closes the route known about; a hopper, a dispenser, a death drop, another mod's inventory-moving block or a second player reaching into the same horse screen are all unexamined. If a phantom saddle ever escapes, it is an ordinary saddle with an invisible texture, which is worse than a free saddle. None of this has been played. - The custom spawn egg makes horses with identical bodies, and that is by design nobody chose.
Opened 2026-09-09 from a report that every horse in a stable had the same speed, health,
jump and size. It was true and it is not a defect: the egg starts every locus at
Gene.defaultAllele(), and an all-baseline genotype resolves to exactlyHorseTraits.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.BodyStatSpreadTestpins both halves. The open question is whether that is the right default. A player using the egg to stock a world gets clones unless they know to add body genes or press Randomize, and nothing on the screen says so. The options are to seed the four body loci on open, to seed them only on Randomize, or to say so in the screen's own text; nobody has decided. - A breed has no temperament, so “disposition” in the new breed entry is assembled prose.
Opened 2026-09-09 with 0.4.0. The breed entry was asked to show disposition alongside
biomes, body and health. There is no such field: a
Breedhas hardiness, a spawn weight and a magical flag, and the line is built from those three. It is honest about what the data supports and it is not what was asked for. Making temperament real is a breed-format change - a field, a schema entry, the writer, forty-nine files - and it would want to mean something mechanically rather than only reading well, or it is flavour text with a schema attached. Not started. - The gene preview row silently truncates at eight, and a gene with more alleles loses the rest.
Opened 2026-09-09 with 0.3.8.
GenePreviews.MAX_SHOTSis 8, chosen so a row cannot turn an entry into a stable, and a locus with more collectable alleles than that simply stops - with no ellipsis, no count, and no indication that what you are looking at is a subset. Which loci this actually bites is a question forGenes.codeOrder()rather than for this page, and the honest fixes are a second row or a “+3 more” tile rather than a bigger constant. - The stall detector was rewritten after it got a horse killed; binding is
confirmed, landing was fixed again and is unplayed. Update 2026-09-10:
the owner confirmed the sign binds and finds the inside of a stall, and reported horses
still arriving in walls. The landing was the centre of the middle floor tile, and
a horse is about 1.4 blocks wide, so in a two-wide stall it overhung a side wall every
time.
StallDetector.landingSpotnow returns the centroid of the floor, checked against the horse's own box withnoCollision, falling back to the nearest tile it fits on; the reported height is measured to the ceiling. The original account follows. Opened 2026-09-09 with 0.3.7, rewritten 2026-09-09 with 0.3.8. The ticket half is confirmed working - the owner bound a sign, spent a ticket and the horse came home. The detector half was wrong twice over and the two compounded: the fill walked 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, blew its budget and reported “not a room”; thenlandingSpot, having found nothing standable in the fallback box, fell through toreturn stall.signPos()- the block the sign itself occupies, flush against a wall, with wall material at head height, and the one position in the routine never checked for anything. The horse suffocated. The search now walks floor columns, never rises, and steps at most one block between neighbours; a door, trapdoor or gate is the edge of the room in every state, because judging a doorway by whether a horse could squeeze through it measures a different stall depending on which way the gate is swinging. Landing is measured live from the sign rather than read back from the record, the chunk is pulled in first, and a stall with nowhere to stand refuses the ticket and does not consume it. What is still unproven: none of it has been played. The doorway-by-identity rule is a judgement about what a player means by “this stall” and could be wrong for a stall whose gate is its only wall; the one-block step limit is a guess about how uneven a real stable floor gets; and the live re-measure means a stall the player has since rebuilt silently becomes a different stall, which is right but will surprise somebody. The graph search itself is now incommon/asStallFillwith tests, which 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. - “Bound ticket” is named after a design that was abandoned. Opened 2026-09-09. The four tickets were specified as binding to one horse each; the owner's call is that they do not bind at all — you click the horse you want sent, and the tier is reach. Three of the four names survived that fine (blank, basic, interdimensional);
bound_ticketdid not, and it now means “within any one world”, which nobody would guess from the word. The tier itself was also picked by reading the recipe ladder rather than by being told — the owner said outright they did not know what the third one should be — so both the name and the rung are open to being changed. Renaming the item is not free: the id is in a recipe, the lang file, a model, and the baked recipe reference. - The checklist hooks are swept, not watched. Opened 2026-09-09 with the checklist. A sweep proves every task is reachable from some call site (
tools/check-progress-tasks.mjs), which is a much weaker claim than that each fires at the right moment. The ones most likely to be wrong are the ones whose hook is a guess at what the player did rather than a direct observation: bonding ticks on a hand-feed that moved the bond, which is the deliberate half of bonding but not all of it; the transfer paper was worse than described here and is fixed — it ticked at the top ofonEntityInteract, 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; it now ticks at the successful end ofsignandredeem, so every refusal above it is a reason it did not; bringing a horse home credits the horse's owner rather than whoever led it through, which is right when they are the same person and silent when they are not; and the three zygosity tasks walk every locus of every foal, so all three will very likely tick on the first foal anybody breeds - which may be the correct amount of teaching or may make them worthless. None of that is knowable without playing it. - Closed 2026-09-09, on the second attempt - and the first attempt is the
lesson. The research shelf's window was reported rough, fixed, written up
as closed, and was still rough when the owner next opened it: “the
research shelf still has text all over the place and you still can't put items in
it”. Two separate faults, neither of which the first pass had looked for.
Four of the fixed captions were wider than the 176px window and drawn with
raw
g.textrather than the fitter that existed three methods away, so they ran out through the frame. And the filing slot had been inactive on the server for the life of the block: itsisActive()read astoreTabfield that only the client ever sets, so the server sat on the field's default offalseforever and every filed paper snapped back to the cursor. Every caption goes throughdrawFittednow and slot activity is side-aware. What is worth keeping is why it was written up as closed: the layout constants had been moved onto the menu, which was the described fix, and nobody had opened the window. A repair that is reasoned about rather than looked at is a repair that has not happened - and this page is where that keeps being rediscovered (see also gap 13). - The gene carrot does not roll 25%, and the owner thinks it does. Updated 2026-09-11: the mechanic moved under this entry. A Known Gene Splice now names both alleles rather than a gene plus a het/hom flag, and defaults to homozygous — so the fed parent contributes the named allele with certainty, and the only uncertainty left is what the other parent brings. Check any prose written against this entry before using it. Opened 2026-09-09 while writing the Getting Started tab. Asked for prose saying a gene carrot gives “a 25% chance of getting that gene” — but that is not the mechanic. A Known Gene Splice sets the fed parent's gamete to
n/GorG/Goutright, so that parent contributes the gene with certainty; what is uncertain is only whether the foal shows it, which depends on the other parent and on whether the gene needs two copies. For a recessive gene against an unrelated mate the visible rate is nearer a quarter, which is probably where the number came from — but for a dominant one it is effectively certain, and the carrot never simply fails. The tab was written to the code, not to the number, and says the parent will pass it on and that showing it depends on the other side. Which of the two is wanted is an open call: a flat 25% roll would be simpler to explain and would make the carrot a gamble rather than a guarantee, at the cost of the current design's one real virtue - that a splice is deterministic enough to plan a breeding around. - The Getting Started tab is the first thing a new player sees and has never been seen. Opened 2026-09-09. Thirteen sections of prose, a row of item icons on most of them, and two live entity portraits — a horseman villager built by setting a profession on a client-side villager, and a cowboy. None of it has been drawn. The specific risks: the villager may render with no profession clothing if
setVillagerDataon an entity that was never added to a level does not stick, in which case the picture teaches nothing and should become the workstation block instead; the portrait scale is computed from bounding-box height and is a guess; and the page's total height drives its own scrollbar, so if the wrap is wrong the bar will be too. It is also the one screen whose failure is worst, because it is what opens first on a fresh install. - Stables were generating inside mountains, and the fix is a guess at the right adaptation. Opened 2026-09-09 from a report against 0.3.2. The three stable structures were
terrain_adaptation: beard_thin, which lays a thin skirt of material under a piece but does not clear what is above it — so a rigid 86-block building projected onto a slope ends up with one end buried. They arebeard_boxnow, which carves the whole bounding box clear. Nobody has watched one generate either way. The other half of the cause is untouched and may matter more: the biome tag includesmeadowandsavanna_plateau, which are mountain biomes in all but name, and no amount of terrain adaptation makes a flat-bottomed stable sit well on a 40-block slope. Ifbeard_boxis not enough, dropping those two fromhas_stableis the next thing to try. - The Equine Research Shelf has never been placed. Opened 2026-09-09 with the block. A new block, block entity, container menu, screen and two payloads, all written in one pass and none of it run: the model is a cube of vanilla bookshelf textures nobody has looked at, the two-tab screen's coordinates are arithmetic rather than measurement, and the whole storage layer - file, withdraw, copy, drop-on-break, save and reload - is unexercised. The parts most likely to be wrong are the ones a compiler cannot see: whether
Slot.isActive()really does gate hit-testing as well as drawing on this version (the tabs depend on it entirely, and a live slot under an invisible one would let a player put a book into the filing slot); whetherPlayerContainerEvent.Openfires late enough forShelfOpenSync's push to arrive; and whetheraffectNeighborsAfterRemovalis the right hook for dropping the books, since it is the successor to a method that changed name across recent versions. A save-and-reload is the single most valuable thing to try: the contents are a gene-key set on the block entity, written throughValueOutput.store, and nothing has round-tripped it. - A shelf can hold a gene the player has never discovered, and nothing decides whether that is right. Opened 2026-09-09. Filing a paper records the gene on the block; it does not touch the player's gene database, and copying does not consult it either. So a paper handed to you by somebody else can be filed and copied without you ever having read one - which is either the point (knowledge is shareable, and the shelf is how you share it) or a hole in the discovery gate, depending on a call nobody has made. The two are one line apart in
ResearchShelfMenu.recomputeResult. It is left permissive on purpose for now, because the restrictive version cannot be tested until somebody has played the loop. - Closed 2026-09-09: the Horse Browser was played and reads correctly without its container. The owner opened it and the verdict was "the H menu looks great", which answers every question below - the tab strip, the surviving tabs and the geometry underneath them all survived losing
leftPos/topPosand the container's render hooks. One thing the conversion did surface later and is fixed: with six tabs the strip ran off the edge of the window, and a tab you cannot see is a feature you have lost, so it scrolls now. Original: Opened 2026-09-09. On the owner's call — "putting crafting in the H menu breaks the flow of normal Minecraft so much it's confusing people" — the browser's Crafting tab lost its 3×3 grid, its result slot and the player inventory, and the screen stopped being anAbstractContainerScreenover a server-synced menu.HorseBrowserMenu,HorseBrowserRecipes,ModMenus,OpenHorseBrowserPayload,SelectBrowserGenePayload,WriteResearchPaperPayloadandResearchPaperWriterare all deleted, and the key opens the screen directly with no server round trip. It compiles and every reference is gone, and that is the whole of what is known. The specific things a compiler cannot check: whether the tab strip, the scroll bars and the three surviving tabs still draw in the right places now thatleftPos/topPosare this screen's own rather than the container's; whetherextractRenderStateorders the chrome under the widgets the wayextractContentsdid; and whether anything else in the mod expected a menu to be open. One behaviour change was caught by reading the base classes rather than by running it — a plainScreenpauses singleplayer and anAbstractContainerScreendoes not — and is overridden back. There may be others of that shape. - The Recipes tab is generated from the jar's recipes, not the server's. Opened 2026-09-09 with the tab.
client/RecipeReferencereadsassets/horsegenetics/recipe_reference.json, whichtools/bake-recipe-reference.mjsgenerates from the real recipe files — so it cannot disagree with what this jar ships, only go stale, which the regenerate table in CLAUDE.md guards. What it can disagree with is a datapack: a pack that changes one of these recipes will be right at the crafting table and wrong in this window, silently. Nothing in the mod does that today and no third party has been given a reason to, but the tab states recipes as fact and has no way to know it is out of date. The honest fix if it ever matters is to read the client's synced recipe display data instead, which is a real piece of unexplored 26.1.2 API (api-notes has nothing on it) and was not worth the risk for a static list the jar already contains. - Closed 2026-09-09: three stables in four generated empty, because the populator was waiting for the wrong chunk. Opened and closed the same day, off the owner’s own play session — “the structures generate, but there are no horses anywhere in the structure”.
StablePopulatorqueues a stable when the chunk holding itsStructureStartloads, and guarded that withnew ChunkPos(box.minX() >> 4, box.minZ() >> 4).equals(chunk.getPos())— a line whose comment said “only the chunk holding the structure’s own start queues it”. That is not what it tests. A jigsaw start piece is placed at its chunk’s minimum block corner and then given a random rotation, andSinglePoolElement.getBoundingBoxrotates aboutBlockPos.ZERO— soCLOCKWISE_90carries the box into −x,COUNTERCLOCKWISE_90into −z, andCLOCKWISE_180into both. Only the unrotated quarter of stables had a box whose minimum corner lay in the same chunk as the start, so only that quarter was ever queued. The other three quarters generated the building and nothing else, forever, silently. The guard was also unnecessary:chunk.getAllStarts()already returns only the starts belonging to that chunk, andStablePopulationData.claim()is what actually makes population happen once per world. It is now deduped by (structure, corner) against the queue itself, which is the same keyclaim()uses, so it cannot disagree with it. A stable now logs when it is queued as well as when it is filled, ungated by debug.announce, because one line without the other is exactly what tells you which half broke — and this bug’s whole signature was a log with thefilledline missing and nothing to say why. Not yet observed working: it is reasoned from the placement code, and what confirms it is a fresh world with a stable in it. - Closed 2026-09-09: the mod narrated itself to players who were only trying to play. Opened by the same session.
DebugAnnounce.ENABLEDwas a hard-codedtrue, so the[Cowboy]and[Horseman]lines — a villager taking the hitch, a horseman founding — went to chat in a normal install. It was hard-coded on purpose, and the reason is still good: it had been!FMLEnvironment.isProduction(), and a session went by with “no debug line ever printed” and no way to tell a shut gate from a code path that never ran. The mistake was not the constant, it was that the constant outlived the assumption printed next to it — “this mod is a dev build with one tester and is not shipped” — which stopped being true the moment a jar was handed to somebody. It is now thedebug.announceserver config: on in a dev run, off in a normal install, switchable in both, which is what you actually want from a player who has a bug to report. The mitigation that came out of the original confusion is kept: the class still logs, once, which way its gate went and whatisProduction()said, so the two failures stay distinguishable. See also gap 140. - Closed 2026-09-09: every launch printed one ERROR per shipped gene file, and none of them meant anything. Opened by reading the owner’s log for something else.
Genesloads the gene files shipped in the jar from its own class initialiser — deliberately, so no caller can readcodeOrder()and get a shorter answer than the genotype code is written against. ThenModGeneSpecs.load()calledGeneSpecLoader.loadAndRegister(dir), which wasregister(fromClasspath().merge(fromDirectory(dir))): it re-read the same files and handed every one back toGenes.register, which throws on a taken key. The caller logs each throw at ERROR, so a normal launch printed 121 lines ofcould not register …: a gene is already registeredbefore the title screen. Nothing was wrong — the first registration is the one that counts and the registry was always correct — and that is the damaging part: it taught the log’s only reader to scroll past a wall of red, and a genuine collision would have been sitting in the middle of it.loadAndRegisteris now the drop-in folder only. A drop-in that collides with a shipped gene still throws and is still reported, because that one is a real mistake. The launch instead says, every time, how many genes shipped, how many were dropped in, and how many segments the genotype code has — which is the first thing worth knowing from a pasted log. - Mostly closed 2026-09-09: Spawn moved under the preview, and the overlap threshold went from 314px to about 258. It was reported as predicted — “Clear genes” and “Make egg” touching in a real window — and the fix was the owner’s: put the Spawn button under the horse instead of in the column. That is both a better home for the one button that does the thing and 24 pixels the column no longer has to find, since the bottom group is three rows now rather than four. Measured rather than guessed this time: the top group ends at 208 and the bottom starts at
height − 74, so they clear at 290 and, oncerightStep()squeezes to its 20px floor, at 270 — which is a 1080p screen at GUI scale 4, the case that produced the report. It is not gone, only pushed out of reach: below about 258 pixels of window the two groups still meet, because seven 20-high buttons need 140 pixels even touching. Nobody has looked at it at that size and the honest fixes are unchanged (scroll the column, or split it). The original entry follows. Original:CustomHorseSpawnScreenhas seven buttons growing down from the top of that column and four pinned to the bottom, and on a short enough window the two groups meet.rightStep()now gives the gutter away to buy room — the roomy 4px whenever it fits, otherwise the largest step that does — but seven 20-high buttons need 140px even touching, and a 1080p screen at GUI scale 4 leaves about 120. So below roughly 290px of window they still overlap, exactly as they did before the guard existed; the guard removed the overlaps between 290 and 314, not the ones below. Nobody has looked at it at that scale, which is why the number is computed rather than measured. The honest fixes both cost more than the change that surfaced this: let the column scroll, or split it across two columns when the window is short. Labels never truncate at any size — only the spacing gives — so what a small window loses is separation, not information. - Every “this is a dev build with one tester” assumption in the codebase is now wrong, and they are not all found. Opened 2026-09-09. Three separate defects this session came from one root: the jar is handed to people who play it. The debug chat lines said so in a comment and shipped switched on; the encoder crash was invisible on the one machine that had ever run it; and CLAUDE.md’s hard rule 6 (“dev only, single tester, no saves worth keeping”) is what made both of those reasonable decisions rather than oversights. Rule 6 is still right about formats — no back-compat shims, no genotype padding, a save is expendable — and it is now wrong about audience. What has not been done is the sweep: nothing has gone looking for the other places that assume nobody else is running this. Worth reading with that question in mind: anything gated on
FMLEnvironment.isProduction()(the three dev-only debug payloads, which look correctly gated and are worth confirming), anything that writes to chat, and anything whose failure mode is “the owner will notice”. - Closed 2026-09-09, after shipping two releases nobody could play: an epigenome code crossed 32 767 characters and the coat packet was writing it on a default-length string. Opened and closed the same day, off an outside bug report — the owner’s friends, who had 0.2.0 working and found that both 0.3.0 and 0.3.1 disconnect on entering a world with
Failed to encode packet 'clientbound/minecraft:custom_payload'.CoatSyncPayloadsends a horse’s genotype and epigenome as text on horse-tracking start, and was writing both with a barebuf.writeUtf(v), whose implicit cap is 32 767 characters. A full epigenome is now 35 304. Minecraft throws rather than truncating and a throw inside the encoder drops the connection, so the first horse that came into view ended the session — which on a fresh world is immediately. What makes this worth writing down is that a test was watching for exactly this and could not see it.EpigenomeSizeTestexists because the same threshold was crossed once before, and it fired then; but it was pointed atSpawnCustomHorsePayload, the one cap with a number written next to it, and an implicit cap has no number to point at. Three other sites had the same latent fault —StoredGenomeonByteBufCodecs.STRING_UTF8(a component that rides every inventory sync, so a preset spawn egg or a genome jar would have kicked whoever picked it up), and the genotype fields on the roster and spawn payloads, already at 5612 of their declared 8192. All caps now come fromGenomeCodeCodecs, the test guards each of them at half-budget, and the genotype guard failed on its first run, which is the whole argument for it. The general lesson is the one gap 13 keeps teaching in a different costume: a limit nobody wrote down is a limit nothing can check, and “the default is surely big enough” is a derived number in disguise. Fixed in 0.3.2; see gap 66 for the size problem underneath, which is untouched. - With
body.sizeoff, every breed’s height target becomes invisible — and a Falabella is only a Falabella because it is small. Opened 2026-09-09 with the setting itself. A breed’sstats.heightin hands is not decoration:Breed.javaturns it into aStatAxis.SCALEband, and the founder pins the size loci to hit it, which is how a Falabella (6–8 hands) comes out visibly different from a Shire. The new switch gates the one attribute write those loci reach the game through, so with it false every breed in the mod stands at exactly the same height. That is the honest consequence of what was asked for and not a bug — the alleles, the band and the resolved number are all still there and still bred for, and the info panel still reports them — but it means the setting costs more than “tack fits properly” for anyone playing with breeds, and nothing in game says so. The two directions out, neither taken: let the setting compress the range toward 1.0 rather than flatten it (a Falabella stays recognisably small while a two-block doorway still works), or leave it binary and say plainly in the config comment that breed height goes with it. What decides between them is whether anyone actually turns it off, so this wants a real opinion before code. - Both gene testers draw a size-scaled preview and neither can know whether the server will honour it. Opened 2026-09-09.
CustomHorseSpawnScreenscales its preview bypreviewTraits().scale()(capped atPREVIEW_SCALE_CAP, with the true figure printed underneath), and the horse designer does the same thing in the browser — where there is no server to ask at all.body.sizeis aSERVERconfig and is not synced, so with it false the egg shows you a horse a head taller than the one the Spawn button produces. Small, and only reachable in creative, but it is the one surface where the setting can actively mislead. The honest fixes are ordered by cost: sync the boolean and drop the preview to 1.0 when it is off; or leave the preview alone and caption it, since it is a genotype viewer and arguably should always show what the alleles say. The designer is the reason not to rush — it has no server, so whatever is chosen has to still make sense with nothing to sync from, and the standing rule that those two screens are one screen in two places means the change lands in both or neither. - Three loci where the mod contradicts its own science tab, and each contradiction is a design call rather than an oversight. Opened 2026-09-09, when the science tabs were written and the two sides could be compared for the first time. SCN4A makes
H/Hlethal at birth; a real HYPP homozygote lives, is more severely affected than a heterozygote, and is merely ineligible for AQHA registration. The mod wants the cliff — HYPP is the one locus where a player makes the mistake with their eyes open — and a studbook has no analogue in a game. HMGA2 pays the pony allele+2.0health per copy, where the only validated health association at that locus is higher insulin and, through it, laminitis risk. The design reason is sound — a locus whose only effect is “worse” is one a player fixes once and never thinks about again — and the direction is still opposite to the evidence. CKM ships as an additive speed locus; its only equine performance claim came from a 2010 Thoroughbred candidate-gene study, failed that same paper’s independent replication sample, and was explicitly non-significant in a later Quarter Horse study. All three are now stated on the pages themselves, which is the point of recording them here: the risk was never that they were wrong, it was that they were undocumented and would be “corrected” one day by someone who had read only one side. A fourth is smaller and is drift rather than a decision — MATP makes pearl commoner than cream, which is backwards, because the two carried separately tuned frequencies from before they were merged onto one locus. - A gene layer that paints nothing at all is invisible to both golden files, and six of them hid there for a month. Opened 2026-09-09 by the gap 103 fix, which is where the cost got paid. Both goldens hash the composed sheet, so they answer “did this gene's output move” - and a mask that selects zero texels has stable output, forever. Six genes had a layer that could never select anything (an amplitude of 77.94 in a normalised space, which displaces the band clean off the horse); they loaded, registered, appeared in both editors, had baked icons, and hashed consistently in
coat-bake-golden.txtthe entire time. The golden was working exactly as designed and could not have said a word. What found them was measuring where each gene paints - changed texels perPart, gene against wild type - which is about twenty lines on top of the machineryCoatBakeGoldenTestalready has, and reading it took one glance because the interesting rows saidNECK=0. The specific mistake is now refused by a guard inGeneFilesTest, but the class is not: a layer can come out empty because its masks do not overlap, because aneedsblock never matches, because a threshold sits past the range its channel actually reaches, or because a scale is sub-texel (which is what killed wing cloak). The fix is a per-gene “this allele paints something, on some part” assertion walkingGenes.codeOrder(), with an explicit exemption list for the loci that legitimately paint nothing - the body-stat genes, the behavioural ones - because that list is short, known, and worth having written down anyway. Until it exists, “the golden is green” means every gene still does what it did, including the ones doing nothing. - The white lock, the reordered modifier band and four reworked genes are all unplayed, and between them they move where half the magical genes paint. Opened 2026-09-08. Extreme white dominant is the first gene in the mod that changes a coat by changing a rule rather than by painting, and the rule runs at every step of phase 3 and again over the overlay - so a bug in it is a bug in every magical gene at once, on the horses that carry it. Its own tests are the two ends (it changes a marked-and-white horse; it changes nothing at all on a horse with no white) and the golden file pins three combinations, but nobody has looked at the edge, which is where it will show: a magical marking stopping at a tobiano boundary should be a hard cut, and if the lock and the marking disagree by a texel about where that boundary is, it will read as a fringe. The same session moved fielded, yalia, voided, opalized and invert to the top of the order and suit, hood and dhampir to the bottom of it, which is a hundred and thirty genes' worth of new stacking combinations that have never been rendered together.
wiki/verification.html. - Three new alleles and two reshaped genes are drawn from a description, and only the description says what they should look like. Opened 2026-09-08. Opalized’s
Opbreads the black channel where the white form reads inverted total pigment; the band it takes (0.70–0.88, with the core above 0.88 cut out for the rim) was chosen by reasoning about the pigment field rather than by looking at a horse, and if it is wrong it will be wrong in a specific way - a bay whose points are black enough to catch it but whose body is not, so the iridescence lands only on the legs and mane. Yalia’sYlois the white form’s last layer on its own, so it is only as good as that layer was, and nothing has ever looked at that layer without the zones beside it. Cleave absorbed Waves as a reversed allele and gained a half-opaque outline built by subtracting a hard-edged tooth field from a wide-edged one - the arithmetic is right in principle and the width (0.085 of the body, about two texels) is a guess. And panda was turned inside out: white is now a band round the belly plus the neck, head and tail, with the shoulder, butt and all four legs keeping the coat. The icon says the layout is right; nothing says the proportions are. - Which horse a gene is photographed on is now decided by two thresholds, and both were fitted to about six genes. Opened 2026-09-08.
CoatVisibilityused to ask "does this gene paint anything at all on a bay"; it now asks two relative questions instead - is this base coat withinENOUGH_OF_BEST(0.6) of the loudest one, and is this combinationBLANK_FRACTION(0.9) unpigmented - and the answers decide every gene icon and every gene page’s opening coat. The rules are right for the cases they were written for (fielded and mushroom needed the first, KIT needed the second) and the second was got wrong twice before it was got right, in both directions: counting distinct colours let a dominant white through, and then counting contrast caught a cremello, a chestnut, a black and a grey along with it. The version that ships reads phase 1 rather than the finished pixels, which is the difference between depigmented and diluted and is not a judgement call - but 0.6 and 0.9 still are, and the only thing checking them is a person looking at a hundred and eighteen icons. A gene that starts being photographed on the wrong horse will do it silently. :common:bakeGeneWikiPagesnever deletes the page of a gene that stopped existing, and the search index goes on serving it. Opened 2026-09-08. The bake writes a page for every registered gene and rewrites the generated spans ofpages.jsandindex.htmlfrom the registry, so a deleted gene drops out of the sidebar and the landing page correctly - but itswiki/gene-*.htmland itswiki/assets/gene-icons/*.pngare simply left on disk. Renamingcollar.jsontosuit.jsonand foldingwaves.jsoninto Cleave leftgene-collar.htmlandgene-waves.htmlbehind, unreachable from any nav and still indexed bybuild-search-index.mjs, so the wiki’s own search returned a full page describing a gene the mod no longer has. Both were found by hand while cutting 0.2.0, not by any check. The bake knows the registry and owns the directory, so it can sweep it: anygene-*.htmlor icon with no gene behind it should be deleted by the task that writes the rest of them, and until it is, a gene rename leaves a lie in the search results.- Closed 2026-09-09, with gap 44: the correlation is gone, and the test that worked around it no longer needs to. Opened 2026-09-08. Seeds 0, 1, 2… were correlated streams and the draw at a given position in them was correlated across the whole set - so each gene, reading its knobs at its own offset in
Genes.codeOrder(), was exposed to wherever its priority put it. Moving synort and opalized slid magic jump onto a bad offset and its mean over 3000 sequential seeds came out 1.7% high, fifteen standard errors, on a distribution that had not changed. With the scramble inSeededRng, the covariance between neighbouring seeds at positions 0, 1, 5 and 20 drops from about 0.013 to about 0.001 - the residual being sampling noise at n=3000. The proof is that the workaround could be deleted.MagicBodyStatGenesTestcarried its own splitmix64 finaliser as the local answer, with a note saying the underlying correlation was this page’s problem rather than the test’s; it has been removed rather than left to double-mix, and the file passes without it. That makes it a tripwire: take the scramble out and those means go back out by the margin above. - Closed 2026-09-08: what a mask’s
partsmeans outside them now depends on its position, and both halves of that were wrong. Opened and closed the same day. First half.maskCoveragereturns 0 for two different reasons — “outside this mask’s parts” and “inside them, and the field reads 0” — andcoverageinverted both, so a first mask painted everything it excluded at full strength. Seven genes led with an inverted, parts-restricted mask; four (iridescent jewel, opal wing veins, elytra veins, webbed) painted 6 176 texels outside their declared parts — every texel they had excluded — and webbed put four times more paint outside its region than inside it. Second half, found by fixing the first. Making an excluded texel stay 0 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, and 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. The rule that serves both: the first mask defines the region, so outside its parts coverage is 0; every later mask modifies it, so outside its parts it contributes its combine’s identity, which is to say it is skipped.PARTSis exempt, because there the list is the mask. The evidence. Twelve icons moved in total. 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 of them 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. A discriminating case was added toexample-genes/prismatic.jsonand checked by deliberately regressing the JavaScript twin: 264 parity checks fail under the old semantics and pass under the new, so this rule is genuinely covered rather than green by default. - Six new effect verbs and the newly-wired
attributeverb have never run in a game. Opened 2026-09-08.charges,breath,on_death,item_drop,mob_auraandcombatwere written against the 26.1.2 sources in one sitting, and so was theattributetranslator that had been logged-once-and-ignored since it was written. Every one of them compiles and every one is flagged unverified in its own comment. The ones most likely to be wrong are the ones that touch a system nothing else in the mod touches: the transient-attribute add/remove reconciliation (doesclearAttributesactually strip a modifier when its condition goes false, or does a horse that once swam keep the bonus for ever?), the air-supply accumulator (doessetAirSupplyabove the max get clamped by vanilla, and does a negative supply drown the horse the way the code assumes?), and the mob-aura shove, which is a velocity nudge rather than a goal and may simply not overcome a pathfinding mob. To be verified has the checklist. - The white-seeking genes were moved onto
LUMA; the black-seeking ones were not. Opened 2026-09-08. The point of theLUMAmask is that “black” and “white” are facts about the colour the gradient chart produced and not about the pigment behind it — the LUT locus can hand a horse a chart whose black corner is violet, and that horse has identical pigment and is not black anywhere. Fielded, yalia, voided and opalized’s white side now ask the chart. Integration, opalized’s dark side and striped mane and tail still ask the pigment throughPIGMENT’sblackanddarknesschannels, so on an alternate LUT they find “black” where the horse plainly is not. Only the white half was asked for, and moving the rest shifts shipped coats for no stated reason — but the inconsistency is now a real one rather than a latent one, and the thresholds are already measured (black 0.807, bay 0.782, chestnut 0.599 ondark) if someone wants to close it. - Phase 1 chains its layers and phase 3 does not, and only phase 1 says so. Opened 2026-09-08 by the coat-generator audit.
SpecPainter.restrictre-reads the pigment field between layers -PigmentView asRead = field.mutableCopy()inside the loop, with a comment explaining that this is what lets aPIGMENTmask chain off what the layer above it did.SpecPainter.tintdoes the opposite: every layer's masks are handed the samecolouraccumulator, the one from before this gene painted anything, and nothing says so anywhere.It went unnoticed while
PIGMENTwas the only coat-reading mask, because a magical layer never moves pigment and so the question could not arise. LUMA made it arise immediately: Nyxborn is specified as "replace all white with black, then star all the black", and the second half cannot see the first. It is worked around there with a union mask - "wherever it is dark, OR wherever it was white" - which is exact, and which nobody would think to write without having been bitten.Two honest fixes. Either
tintchains likerestrictdoes, which is a real semantic change with a JavaScript twin and would move every multi-layer magical gene that reads the coat; or the asymmetry is written down in the format besideLUMA, which is what has been done for now. The second is not a fix, it is a signpost. - Two genes make total claims about white, and only one of them can be right. Opened 2026-09-08 by the coat-generator audit. Extreme white dominant holds a white lock: every white texel is permanent and nothing painted afterwards may change it, at any slot in the order, which is what makes it "extreme" rather than merely high-priority. Nyxborn claims the opposite totality - every white marking the horse carries is painted out in black.
On a horse carrying both, the lock wins and Nyxborn paints nothing at all: the conversion layer is skipped for every locked texel, and on an extreme-white horse that is essentially all of them. That is the correct resolution and it falls out of the design rather than needing a rule - but neither gene's description mentions the other, so the only way to find out is to breed the pair and wonder what went wrong. Both pages now say so. The general problem stands: nothing checks for two genes whose descriptions both promise to be the last word, and the next pair will be discovered the same way.
- Closed 2026-09-08: there is a per-gene coat regression test now. Opened the same day, when a change to what a mask’s
partsmeans altered what twelve genes paint andcoat-golden.txtcame out byte-identical - it exercises 39 of the registry’s genes and 3 of the hundred-odd data-driven ones, so a defect of that size was invisible to the one test whose job is noticing that a coat moved.CoatBakeGoldenTestwalksGenes.codeOrder()instead of a hand-picked list, so it has no list to fall behind: every registered gene, every variant allele, composed through the real gradient and hashed. It covers 203 genes in 623 lines and runs in seconds rather than needing the ten-minute suite, which is the property that matters - the pipeline golden is expensive enough that its cost gets weighed against its value, and this one is not. It hashes; it does not look. Four of those twelve genes were fixed by the change that broke their hashes, so a diff here is a prompt to go and look at the icons, never a verdict. The test says so in its own failure message. - The three coat genes with a compound heterozygote have never had that combination looked at. Opened 2026-09-08. Stained glass’s
Sgv/Sgmdraws one window with all six colours in the tray, and Tron’sTrs/Trglays a gradient tube over a solid one on the same edges. Both are deliberate designs and neither appears in a founder table, so no wild horse has one and the icon baker photographs homozygotes — which means the only pictures that exist of this mod are of combinations these two rows are not. The Tron one in particular is guesswork: twoTOWARD/RAMPlayers at full opacity on the same texels, and which wins is layer order rather than a decision anybody made. - Closed 2026-09-08: the coat golden file was regenerated and the suite is green again. It had been stale across two sessions — forty-six genes between the two imports, and every one of them moves
Genes.codeOrder(), which moves every horse’s epigenome slots, which moves every hash in the file. Regenerated while cutting 0.3.0, which is exactly the “next deliberate suite run” this gap asked to be done at: the file was deleted,:common:testrun (681 tests, 5m33s, the golden the only failure and failing by design because its resource was missing), the fresh file copied back fromcommon/build/, and the suite run again to confirm. The golden is guarding something again — while it was stale a real regression would have been indistinguishable from the expected churn, which is the part that mattered. The lesson to keep: a release is the natural moment for this, because it is the one time the full suite is run anyway. - Thirty-two imported genes have been looked at exactly once, as an icon each. Opened 2026-09-08. The second magic-gene import translated thirty-two sketches into real gene files. The sketches were written against a vocabulary this mod does not have - boxes at explicit coordinates, rings at named centres, arithmetic in JSON,
spreadon ops that have no such parameter - so every one of them is a translation rather than a transcription, and each gene’s ownnotesblock says where its translation knowingly diverges. All thirty-two paint something and all thirty-two were eyeballed as a contact sheet; four were rebuilt after that sheet showed them wrong (banded socks came out as a solid sock, beetle pearl as one blob, opaline zebra as a smudge, webbed as a ground rather than a net). Nothing has been bred, and nothing has been seen stacked with anything else. The ones most likely to be wrong are the ones whose source placed a mark by coordinate and this build places by lattice - ink scroll’s medallions, painted lady’s wing and wing cloak’s sweeps - because where they land is down to the seed, which is the lottery gap 108 is about. - Closed 2026-09-08: the portability rules are enforced by a test instead of written down. Opened the day the TeaVM empty-map trap was found, on the grounds that a new
Map.of()incommon/"compiles, tests green, ships, and fails only in a browser nobody on this end can open a console in".CommonPortabilityTestnow scans the module’s own sources for three things and fails loudly on any of them: a Minecraft / NeoForge / Mojang import (hard rule 1, which nothing had ever checked either),Map.of()orMap.copyOf(outsideCommonMaps, and the two Java 9+ calls that were removed for the TeaVM and Java 8 targets. It is a source scan and it says so. It will not see a rule broken through reflection, and it deliberately allows a non-emptyMap.of(k, v, …)literal — the trap is the empty map, and light ships one that the browser exercises on every preview, so it is empirically fine rather than argued to be. What it catches is the thing that actually happens: somebody typingMap.of()because it is the obvious way to write an empty map. - Auroraband’s band misses the horse on about one seed in six, and it has lost its icon because of it. Opened 2026-09-08. The ribbon is a single
STROKESstroke at a spacing of 22 body units - wider than the barrel - so which part of the lattice lands on the mesh is entirely down to$ribbonSeed. Measured over twelve epigenomes and all six base coats, ten of them repaint 470-1750 texels and two repaint under thirty, which is the difference between a horse wearing an aurora and a horse wearing nothing. The icon baker seeds itself off the gene key, that seed was one of the unlucky ones, andCoatVisibilitycorrectly judged the result invisible - sowiki/assets/gene-icons/auroraband.pngdisappeared and the gene had no picture on the landing page. The icon is back as of 2026-09-08 and the gap is not fixed. What brought it back isCoatVisibilitygaining a second candidate per allele (the heterozygote, for KIT’s sake) and a relative backdrop rule, either of which can move which horse gets photographed; the underlying lottery is untouched, so the next unrelated change to the chooser can take it away again. Treat a returned icon as luck, not as evidence. The fix is a smaller spacing with a shorter length (several candidate bands, one of which is bound to land) rather than a nudged seed, which only moves which horses lose out. Nothing else uses a spacing anywhere near its own part’s size, so this is believed to be the only gene with the problem - worth a sweep forspacingover about 12 when someone is in there. - The gene creator silently drops a
needsblock. Opened 2026-09-08 with the block itself.spec-model.js’s exporter builds each expression field by field, so anything it does not know about is gone the moment a file is loaded into the creator and exported again — and aneedsblock is exactly that. The damage is bounded and the tool is honest about the general case: it says outright that it authors one visible outcome on a two-allele gene and that anything more is hand-edited JSON, and accretion has four combinations claimed across five entries, so it was already outside what the creator round-trips. But nothing warns, and the failure mode is a gene that loads, breeds and paints, with one of its two loci quietly ignored. The cheap fix is one line intidyto carry the field through; the honest one is for the exporter to refuse, or at least complain, when it meets a key it does not model. - Closed 2026-09-09:
CHOICEis a first-class per-horse decision, and quarter no longer fakes one. Opened 2026-09-08. Quarter has to pick which quadrant goes pale and the mask fold has no branch in it - a knob can move a boundary but cannot choose between two of them - so it sampled value noise at a scale of 4000 body units, a hundred times the length of the horse, and stretched the result onto [-200, 201] so the clamp turned it into a hard yes or no. The newCHOICEmask draws an integer in[0, options)from a seed knob and returns 1 where it equalsis: constant across the body and exactly 0 or 1, so both of the trick’s residual defects are gone - the slight drift across the horse, and the 1-in-401 strip that clamped to neither end and produced a half-strength quadrant. It is the only mask that does not read position at all. All 24 of quarter’s noise-as-coin-flip masks were migrated,invert: truebecoming the other outcome said plainly. A 64-vs-32-bit modulus hazard was caught while writing it, and is the reason the example gene usesoptions: 3: the draw is mixed with splitmix64 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. Taking it over all 64 bits in Java and the low word in JS agrees for every power-of-twooptionsand silently disagrees for 3, 5, 6 - so a 2-way example would have been green either way. Withoptions: 3inprismatic.json, reintroducing the mismatch fails 72 parity checks.ChoiceMaskTestpins fairness (within 5% of uniform over 60,000 seeds for 2, 3, 4, 5 and 8 options), independence of neighbouring seeds, reachability, determinism and the single-option boundary. - Closed 2026-09-09: the neck's geometry is read from the neck instead of typed into gene files, and six genes that were painting nothing now paint. 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. The workaround was a
WAVESmask with a sawtooth longer than the horse (wavelength90) at anamplitudeof 77.94, so the displacement never wrapped and the band came out as a tilted plane at2A/λ = tan(60°)— sixty because the neck's own long axis is sixty off horizontal, and the first attempt used thirty, the complement, which tilted the plane the other way and had to be reported twice. The gap said five gene files; it was eleven, and the audit that came with the fix found the reason it had spread so quietly: six of the eleven had copied the two numbers intospace: "part", where they cannot work at all.amplitudeis in whateverspacesays whilewavelengthnever is, so in a normalised space 77.94 displaces the band twenty to forty spans off the horse: measured over every one of the 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. The replacement isspace: "local"onAXIS,WAVESandRAMPalike, backed byHorseSkinGeometry.local: it takes the part's rest-pose pitch back out before normalising, soaxis: "X"is across the part's depth (low is the crest, which is the face the mane sits on) andaxis: "Y"is along its length, which is a collar square to the neck rather than to the world. On an unpitched part it is exactly the old part space, to the bit. The trick turns out to be exactly that coordinate — the fit is affine with a maximum residual of 0.0002 over the whole neck — so 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. Goth moved only because its$depthknob was in body units and is now a fraction. What used to be nothing but two constants is now three tests inPartLocalFrameTest— local equals part space on an unpitched part, differs sharply on the pitched neck, and the old sixty-degree coordinate is still an affine function of the new one with the slope the neck's 7-unit depth implies — plus a corpus guard inGeneFilesTestthat refuses anyWAVESamplitude far above 1 in a normalised space, which is the general form of the mistake all six made. None of it has been seen on a horse: verification §0-AW. - Closed 2026-09-08:
PIGMENT’sspreadmeasures in body space. Opened and closed the same day. It was a disc of texels, which is the right answer inside a face and the wrong one across a UV seam — and the latent problem became an observed one within the hour, when Integration was pointed at it: dilating three units off a bay’s black socks covered the whole horse, because the leg patches sit next to the barrel on the sheet. The fix was cheaper than the gap assumed.HorseSkinGeometry.sampleis a cached grid, not a computation, so the radius is now in body units and the test is done on the body point the sample carries; the texel window is only a bound on the search. Anything that wants a neighbourhood of the coat should copy that shape rather than counting texels. - Closed 2026-09-09: the parser refuses a
perDosethat cannot vary. Opened and half-fixed 2026-09-08 - the five broken files were corrected then, and the check that would have caught them was not written until now, which is the half that mattered.perDoseis indexed by how many copies of the gene's first-declared allele the horse carries. A coloured expression is reached by two copies of the second, so its dose is always 0 and it readsperDose[0]- which authors write as0.0, because a dose of nothing should draw nothing. Achanceof 0 means the mask never fires, and crescents, lasertae, polymoon, stellar and wormholes all shipped with a coloured form that painted nothing at all.GeneSpecParsernow computes the dose of every combination an expression claims, and refuses aperDoseinside one that can only ever be reached at a single dose - naming the dose, the allele it counts, and the constant the triple actually reads. Scanning the corpus first showed zero genes trip it, so this is prevention with no migration behind it. The catch-all expression is exempt, because its claims are not known until every other entry has been read. - Closed 2026-09-09: the parser refuses a band whose
tocan fall below itsfrom. Opened and half-fixed 2026-09-08, same as gap 105: the two broken genes were corrected and the check was not written.BodyStripes.smoothsteptreatsedge1 <= edge0as a hard step in the original direction - it does not reverse the ramp, it removes it. Patina asked for “fade in as the coat gets lighter” withfrom: 0.75, to: 0.35and got “hard on wherever the coat is dark”, the exact opposite with no edge at all. Integration was worse: it pointedtoat a knob whose range straddled its ownfrom, so about half the horses it drew were reversed and nothing about the file looked wrong. So the test is possibility, not certainty - a knob is a range, two knobs are drawn independently, and “this can come out reversed on some horses” is exactly the bug. It runs onAXIS,PIGMENT,LUMAandWAVES, and per-dose where either end is a triple. TheRAMPop is deliberately exempt: itsfrom/toare the ends of a linear interpolation, so putting them the other way round genuinely reverses the gradient and is a thing an author might mean. A corpus scan first found zero of 513 banded masks trip it, 130 of which point at knobs - so, again, prevention rather than migration. - No data-driven gene can set an eye colour. Eleven of the imported magical genes say in their own description that they set or match the eyes - Angler, Aurorae, Glow, Lantern, Kintsugi, Peafowl, Flametouched, Fawn and Element Dusted want the iris in the gene’s own colour, Qhada wants no pupil, Laciano wants a white sclera. A
GeneSpechas no hook intoEyeColorat all: the eye pipeline is driven by hand-written genes implementingEyeContribution, and nothing in the format reaches it, so all eleven ship with ordinary eyes. The shape of the fix is probably aneyesblock on an expression besideeffects- a rank, a colour or hue knob, and whether it takes the iris, the sclera or both. It is the largest single hole in the bulk import. - A gene cannot read another gene’s epigenetics. Eight imported markings are defined in terms of “the speckling colour” - Rex draws its circles in it, Qhada fills the gaps between its ribbons with it, Panark outlines every stripe in it, Emitola works it through the flames. Each means the colour this horse’s Speckling locus drew, and a spec gene cannot ask another gene what it drew, so each carries its own hue knob and two of them on one horse disagree. Knobs are already stored per allele copy and addressed by gene key, so the read is not the hard part; deciding what happens when the gene being read is absent is - falling back to the reader’s own knob would make the dependency invisible, which is worse than not having it.
- The
invertingmarkings only go one way. Seven loci from the akkadorian source are flagged as inverting: over a pale region they should render dark and over a dark one pale. No op is conditional on what is underneath, so all seven paint pale and the polarity never flips. War Mask shows the fix - it needs the same behaviour (near-black on a light coat, grey on a black one) and gets it with two layers whosePIGMENTmasks are inverses of each other. That works for any of the seven and costs one extra layer each; it was left for time, not for a reason. - The two editors’ new controls have never been on a screen. The randomize menu, the padlocks, the family filter and the clipboard/file horse format were all driven from Node against the compiled wasm and from a harness against the editor model, and all of that passes — but nothing has been looked at, in the browser or in game. The risk is layout, not logic: the padlock takes ten pixels off every gene name, the right column gained two split buttons, and the family filter sits in the eight pixels between the header band and the list. See the checklist.
- A copied horse is about 28 000 characters. Almost all of it is the epigenome code, which stores literal values on both allele copies of every gene that has an epigenetic schema, and there are a hundred and seventy genes now. The clipboard takes it and the parse is fast, so nothing is broken; it is simply not a thing you can paste into a chat message, which the old genotype code was. If it matters, the lever is the epigenome code, not the horse file — the file is a dozen fields around two strings.
- Closed 2026-09-09: the coat textures are bounded and a resource reload drops them. Both halves were defects at the shipped resolution and neither had anything to do with scaling.
GeneticCoatTextureFactorykept twoConcurrentHashMaps filled bycomputeIfAbsentand never removed an entry, so a client accumulated one permanently registeredDynamicTextureper distinct genome it had ever rendered — adult and foal counted separately, plus an emissive mask for every glowing one — and released them only inClientLifecycleHandler.onLoggingOut. And there was no client reload listener anywhere in the mod, so F3+T or a pack switch left every baked coat on screen and leftadultTemplate,babyTemplateand theLutSetholding the pixels of whichever pack was active when the first horse rendered: editing the red/black gradient and reloading appeared to do nothing, which is a trap for exactly the person most likely to try it. Both caches are nowTexelBudgetCaches — an LRU whose capacity is texels rather than entries, because an entry is a whole sheet and the number that has to stay true across a change ofHorseSkinGeometry.SHEET_SIZEis the memory, not the horse count — with an entry floor underneath it so the cache can never end up smaller than a frame at a large sheet, which would not merely thrash but could release a texture an already-submitted draw call still wanted. Every removal, eviction andclear()alike, goes through one hook that releases the texture and drops the reverse-map entry, so there is no way left to drop an entry without freeing it; the tripwire on id collisions consequently now covers only ids live at the same time, which is exactly when a collision could do damage.CoatAssetReloadis the reload listener, registered throughAddClientReloadListenersEvent(not theRegisterClientReloadListenersEventthis gap guessed at — that name does not exist in 26.1.2). The policy is unit-tested inTexelBudgetCacheTest, which caught a real off-by-one in the eviction loop; what a test cannot see is on verification §0-AV, and the one number to look for is the line the factory now logs on world exit saying how many coats it held and how many it recycled. - The gene index cannot be regenerated from the folder, because one absence in it is a killswitch. Opened 2026-09-09, after doing exactly that. Twenty-two genes landed at once and
genes/index.jsonwas rebuilt by listing the directory — which is the obvious thing to do, is what the contract’s warning about a hand-kept list invites you to do, and silently re-enabled wing cloak, whose entire killswitch is that one line being absent. It was caught by reading gap 129 while writing this page, not by anything going red: the gene loads, registers, takes a genotype slot and paints its two dozen texels exactly as before. Restoring the absence moved the bake golden by exactly its three lines and no other hash. The gap is that the file now carries two different kinds of information in one list — “these files exist”, which a script can derive, and “this one is switched off”, which it cannot — with nothing distinguishing them. A gene file that is present and unlisted looks identical to one somebody forgot. Two ways out: a"disabled"flag inside the gene file, so the index really is derivable; or a second list beside it naming the switched-off ones, so a rebuild has something to subtract. Until one exists, add to the index by hand and never regenerate it. - Nothing in the coat pipeline can see the camera, so true iridescence is out of reach and three genes ship the structural half of it. Opened 2026-09-09. Carapace Sheen, Kite Bloom and Moonwing all came in asking for a colour that shifts as the horse turns — a beetle’s shell, a morpho wing, a soap film. This is not a mask that has not been written. The coat is a texture baked once per horse and handed to the renderer; no mask or op is ever called with a view direction, and adding one would mean the texture depended on where somebody was standing, which is not a texture. The honest half is
NORMAL, which reads the direction the surface faces: the sheen sits on the planes that turn into the light and stays there. Anything better needs a shader, not a mask — a client-side render layer sampling the normal against the view vector, which is a different subsystem and a much larger change. Recorded so the next intake batch does not spend two tool-gap sections proposing it; the prompt now says so outright. - A knob cannot be offset by a constant, so no gene can paint a colour a fixed distance round the wheel from its own hue. Opened 2026-09-09. Agate wanted each cell’s heart to be its rim’s complement: same horse, same knob, 180 degrees away. A numeric parameter in a gene file is a constant, a
$knobreference, or a per-dose triple — there is no arithmetic, so “$hueplus 180” cannot be written, and a second knob is a second independent draw rather than an offset of the first. Agate ships with both layers on one hue, separated by saturation and lightness, which reads as depth rather than as contrast.RAMP’shueSpanis the nearest thing that exists and it only offsets along the ramp, so it cannot move the start. The cheap fix is one optionalhueOffseton the four colour ops, added tohueafter the knob is read — a constant, so nothing about the value model changes. The expensive one is expressions in parameter slots, which the format has deliberately never had. CHOICEreads like a per-cell branch and is a per-horse one, and the format has no way to say so at load. Opened 2026-09-09. The Opaline import gave each cell of its mosaic one of three tints by putting aCHOICEmask on each of three layers withis: 0,1,2— which is a completely reasonable reading of the name, and would have painted the entire marking one colour, chosen by the seed. Nothing catches it: the file is valid, the layers are valid, and two of the three simply return 0 everywhere. It was caught by reading the import’s prose, which said “each cell”. The pattern is nowRAMP axis="cellId"and the prompt’s rule 11 says out loud whatCHOICEcannot do, but a loader check would be better: several layers of one expression whose only difference is theisof a shared-seedCHOICEis a recognisable shape, and it is either this mistake or a gene drawing three mutually exclusive outcomes, which the format would rather have as three expressions anyway.- Moonwing’s gold band disappears on a horse that is white everywhere, and only in the pearl form. Opened 2026-09-10, pre-existing — measured with the 0.4.1 layers and again with this session’s two gilding layers stripped back out, and it behaves the same both ways, so it is not what the enlargement did. The measurement, counting texels the gene changed to a warm colour: 589 on a bay, 692 for the coloured allele on a dominant-white
W22/Nhorse, and 4 for the pearl allele on the same white horse. The band is not missing so much as drained — the texels it covers come out#c6c3bcand#adaaa4, a pale grey in exactly the right shape and count. The wing itself lands on all three (601 texels every time, the same SVG fill mask) and the dust lands, so nothing about the mask is coat-dependent; what changes is only what the layer had underneath it. The cause is not established.TOWARDwrites a delta against the pre-gene visible colour and the deltas of a gene’s layers sum, which is the first place to look — the pearl fill and the gold stroke overlap along the whole outline, and on a white horse both deltas are negative where on a bay they pull in opposite directions. Worth fixing only if a white moonwing horse turns up and reads as unmarked; a pearl wing on a white coat is close to invisible with or without its binding. The mechanism is established now, and it was found from a different gene. A gene's layers each measure their delta against the colour the GENE started from and the deltas are SUMMED - gap 169. Moonwing's pearl fill and its gold stroke overlap along the whole outline, and on a horse that is already white the fill's delta is near zero while the band's is large, so where they meet the two add to something past the clamp and come back as white. On a bay the fill's delta is enormous, and the same sum lands differently. That accounts for the band inside the fill; what it does NOT yet account for is the outer half of the stroke, which falls outside the fill and should be gold on any coat. So the gap stays open, but it is one measurement from closing rather than a mystery, andDeadLayerTestdoes not catch it because the band is not dead - it is the wrong colour. - Thirty-six of the forty-two new black forms have never been
seen, and the way they fail is invisible to every check that exists. Opened
2026-09-10. The black alleles were produced by one transformation over the white ones
rather than authored one at a time, so what can be checked mechanically was:
every one parses, every one paints more than
CoatVisibility.MIN_TEXELSon a bay, and the bake golden proves none of them disturbed anything that already existed. None of that touches the question. A near-black marking on a black or seal-brown horse is a marking nobody can see, and no test in this repo asserts contrast against the coat underneath -MIN_TEXELScounts texels the gene changed, and changing a near-black texel to a slightly different near-black counts. Six were rendered and looked at; the other thirty-six were not. The horses to put them on are the black and seal-brown ones, and the honest fix if a form turns out invisible is a hand edit to that gene rather than another sweep - the transformer bands were chosen for the set and are wrong for individuals by construction. - The SVG mask flattens every curve to a fixed sixteen segments,
so a drawing's cost is its command count rather than its size. Opened
2026-09-10.
SvgPath.CURVE_SAMPLESis 16 regardless of how long the span is, which is right for a hand-drawn outline of seven big cubics and wrong by two orders of magnitude for a traced one of two hundred and seventy tiny ones: blackwork's ornament flattened to 4368 points and dorsal wing's butterfly to 1637, both over the 1024 ceiling, for drawings that need a few hundred. Both were simplified by hand into polylines and both ship, so nothing is broken - what is missing is that an author cannot tell in advance whether a drawing will fit, and the number they would have to predict is not the one they can see. The fix is adaptive flattening: subdivide a span until its chord error is under a tolerance instead of always sixteen, which costs a traced outline what it is actually worth and costs moonwing nothing. It is not done here because it moves every existing SVG gene's bake golden,expected.json, and the creator's JS twin, which is a five-file change and not something to do on the way past an intake. The intake side is a tool now:node intake/tools/fit-svg.mjs gene.json --writedoes the hand simplification and measures what it moved - seven drawings of the 2026-09-10 evening batch went from 1316–7785 points to 700 (candelabra 1000), with at most 0.41% of any viewBox changing side. The engine fix is still the right one. - Two per-entity costs hid behind comments claiming they were cheap. Opened and closed 2026-09-10, both found by re-reading the same day's work rather than by any check. (a) The world-reading condition flags were sampled all four together, so a caveborn eyesight horse - which needs one block-light read - was paying for a two-thousand-block jukebox search and a sixteen-block entity scan every twenty ticks, for ever. (b) Guardian's handler scanned a sixteen-block box for horses on every hit any player took anywhere in the world and only then checked ownership - while its javadoc asserted it established ownership cheaply first. Both are fixed: flags are computed individually, and the horse tick keeps an owner index the event can miss on with one hash lookup. The lesson is the one worth keeping. Both bugs were written in the same session as the comments claiming they were not bugs, and both were 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 against the hazard did. A comment asserting something is cheap is not evidence that it is, and there is no check anywhere in this repo that would have caught either.
- Three loci have an allele per mob and only two of them read the same table.
Opened 2026-09-10.
MobRosterexists precisely so that leader of the pack and spawner cannot disagree about what the mobs are, and lycan — which established the pattern — still keeps its own copy. The two lists are identical today and will not stay that way: the first time the game adds a creature, somebody will update one. It was not migrated because lycan’s allele order is part of the genotype code and is baked into two checked-in goldens, so moving it rewrites every saved horse - a real change with a real diff rather than a tidy-up, and not something to do in passing during a twenty-six-gene batch. The honest state is: the table exists, it prevents the drift for the two new loci, and the gene that needed it first is the one still outside it. - A guardian cannot answer a player, because 26.1.2 exposes no
server-side PvP flag. Opened 2026-09-10.
Guardian attacks whatever damaged its owner, and the
design called for retaliation against a player attacker to be gated on the
server’s PvP setting.
MinecraftServerin this version has no such accessor under any name the decompiled sources contain, so the handler takes the conservative branch and never targets a player at all. That is defensible on its own terms - a horse joining a fight between two people changes what that fight is without either of them agreeing - but it is a decision forced by an API gap rather than chosen, and it is recorded here so it is not mistaken for the intended design. The right resolution is a mod config option rather than hunting for the vanilla flag. - A gene page whose filename does not match its gene key is
invisible, and nothing fails. Opened and closed 2026-09-10.
GeneWikiToolderives a page filename from the gene key, sohorsegenetics.pack_leaderlooks forgene-pack-leader.html. Four of this batch’s pages were written under readable names instead (gene-eyesight-locus.html,gene-leader-of-the-pack.html, and both weather loci), and the result was four genes missing from the sidebar and the landing page with the build entirely green. The bake does say so - it printsno page of their own, so not listed- but on a line nobody reads unless they are already suspicious, buried under the usual preview chatter. The four pages were renamed and every inbound link updated. The lesson is the same one as gap 13: a link between two things that is checked by nothing will eventually be wrong, and here the check exists but only whispers. Acheck-links --orphansrun does not catch it either, because the pages were reachable from the roadmap. - Half closed 2026-09-10: a check now catches the layers that
get thrown away, but nothing yet catches the ones that merely come out wrong.
Opened 2026-09-10, after dorsal wing shipped from intake painting a solid black horse and
contour shipped painting a white one. Every layer of a magical gene measures its delta
against the colour the gene started from and the deltas are added, so a
TINTof -200 under aTOWARDteal gives teal minus 200. The rule is written down - the prompt's rule 8 - and it is still the easiest thing in this format to get wrong, because every gene that works does so by having disjoint masks and so never demonstrates the cost, and because the symptom never looks like the cause: dorsal wing looked like a mask that was not firing and contour looked like a palette that was too pale.What was built:
DeadLayerTest. Take a layer out; if the horse does not change on any base coat, the layer was not doing anything. It is a measurement rather than a pattern-match, because whether two masks overlap is not decidable by reading them - 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. It runs in 23 seconds and it found 29 dead layers across the shipped genes, none of which failed anything else. Three different mistakes, one measurement: a mask fold that comes out empty, a layer whose delta is clamped away by an overlapping one, and a feature smaller than a texel (ringwork's two contour rings are annuli 0.1 body units wide against a 0.5-unit texel). Validated against the known bug: the pre-fix dorsal wing is flagged on exactly the two layers that were invisible, and the fixed one is clean.What it does not catch, and this is the half still open. Two metrics were built and measured against the whole corpus before this one was chosen, and both failed for the same reason. Divergence from stacking - render the gene the way the engine does and the way the author meant, and diff - fires on about half the corpus including genes that look perfectly good, because a gene with overlapping layers whose author tuned the colours until it looked right is indistinguishable from one whose author did not. Clip pressure ranks pre-fix contour first and second of every expression in the mod, which is exactly right, but ranks dripwork third and crazework fifth, and those are fine - clipping toward a colour that every contributing layer wanted is harmless. There is no metric that separates "relies on summing and looks good" from "relies on summing and looks wrong", because the only difference between them is whether a human liked the result. So contour's kind of failure - a wash rather than a dead layer - is still only catchable by looking at the horse.
The 2026-09-10 evening intake is the case for looking. Eight of its twenty-four genes nested their layers and expected them to stack - corolla, agate eye, taper flame, barred wing, uraniid, trillium, foxglove, ooze drip - and every pale form came out a blank white shape: corolla's blooms were flat discs, taper flame's spark was clamped out.
DeadLayerTestcaught three of the eight, and only the innermost mark of each; the side-view renders caught all eight. The prompt's rule 8 only said that a layer cannot read an earlier one; it now also says that overlapping layers sum, and that a nested marking has to be written disjoint (each layer multiplying in the inverted shape of every layer painted after it, which for hard masks is stacking exactly). That is what all eight were rebuilt with.The root-cause fix, not done and not this session's call: a per-layer
"over": truethat applies the delta accumulated so far before the layer measures. 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. Withover, dorsal wing's original file would have been correct as written. It is a format change and therefore the five-file contract - engine, schema, fixtures, the creator's JS twin, the designer - plus every golden, so it wants deciding rather than doing.The 29 are a ratchet, not a backlog that can rot.
common/src/test/resources/dead-layers.txtfails the test if anything new appears and if anything in it has come alive, so fixing a gene is not done until its line is deleted. One is already gone: dripwork's loose dots, thinned to nothing by this session's own tuning. The file is meant to reach zero, and the worst entries are quarter (nine dead layers, three of four quadrants in three expressions) and ringwork (both contour rings). - Wing cloak is killswitched until it is retooled. Opened and parked 2026-09-09. It paints fewer than
CoatVisibility.MIN_TEXELS(24) out of roughly five thousand mapped texels — enough that the engine can tell it apart from a plain horse, not enough that a person could. The seed scramble did not cause that; it only decided which side of the line this particular draw fell on, and the gene was marginal well before. What made it visible was the icon baker refusing it an icon on every staged backdrop, taking the icon count from 156 to 155. Owner’s call: it wants rewriting rather than the threshold lowering, so it is switched off rather than left in the world in a state nobody can see. The killswitch is one line:"wing_cloak.json"is out ofhorsegenetics/genes/index.json, so nothing registers the gene — no horse carries it, it has no carrot, it is in neither gene editor, and it is off the wiki’s page list. Every file still exists, including the gene JSON and the wiki page, which now carries a banner saying so and is reachable by direct link. Putting the line back and re-running the three bake tasks restores it — and regenerating the index from the folder does the same thing by accident, which happened on 2026-09-09 and is gap 162. Switching it off changed no horse: all 1056 composed-coat hashes in the pipeline golden are byte-identical and only thewing_cloak=n/nsegment left the genotype code, while the bake golden lost exactly its three lines and nothing else — which is also a clean demonstration of gap 47’s fix, since removing a locus no longer disturbs any other gene’s epigenetics. When it is retooled, check it against the scale rule first: under half a body unit is sub-texel, and that is the commonest reason a gene comes out almost invisible. - Closed 2026-09-09 as soon as it was found: the pipeline golden’s documented regeneration path did not work, and failed by restoring a stale file.
CoatPipelineGoldenTest’s javadoc has always said “run the test, copy the file it writes tocommon/build/coat-golden.txtback into place”. It only ever wrote that file when the golden was missing from the classpath, never on a mismatch - so a red run leftbuild/coat-golden.txtholding whatever an earlier run had put there. Copying it back therefore silently reinstated an old golden, and the test went green having compared nothing new. That happened once during this session and was caught only because the file “moved” by zero lines while the test still failed. It now writes the actual on a mismatch too, the wayCoatBakeGoldenTestalready did, and the javadoc no longer tells you to delete the golden first - which was itself a destructive step to put in front of someone who only wants to see what changed. - Closed 2026-09-09:
posed()exists once, ingeometry.js, and parity checks it against the Java. Opened 2026-09-08.HorseSkinGeometry.posed/posedNormalwalk the raw cuboid rather than its bounding box, and the browser needs the same walk to draw a horse whose neck is pitched - so the arithmetic existed twice, in two languages, in two files that were not obviously a pair, with nothing comparing them.geometry.jsnow carries the port, beside the rest of its port of that class, andmodel3d.js’semitPartcalls it instead of writing the flip out again - 528 cuboid corners verified bit-identical across both skins before and after, so the preview mesh did not move.SpecFixtureToolbakes an answer table of every part × face on both skins, andparity.jsasserts it: 924 new checks, taking the gate from 5485 to 6409. The probes are not only corners, which matters more than it sounds: 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 - the exact shape of the UV swap that once hid behind a stale fixture for a day. Face centres and one asymmetric point are in there too. Verified by regressing the port three ways: swapping the body-Y/Z pairing (460 failures), dropping the pitch rotation (402), and flipping one sign in the normal (44). :common:testtakes about nine minutes. It was under one. The golden-coat suite composes a full 128×128 texture per case and every case now runs a hundred-odd genes rather than seventy, most of the new ones with three to seven layers. Nothing is wrong and nothing is being recomputed twice; there is simply a lot more work. If it becomes intolerable the honest lever is the number of cases inCoatPipelineGoldenTest.CODES, not the size of the model.- Grey has no age - and that's now a decision, not a gap. Horse aging is deliberately out of scope (it risks feeling bad for a player attached to a horse), so greying is a rate with no clock to run on. What the mod does instead is read the copy-number dosage (
N/G2throughG3/G3) to pick a progression window, and the horse's own epigenetics pick where in that window it sits - fixed for life. A slow grey is a steel or dapple grey forever, a rapid grey the near-white one. Flea-bitten grey is parked with aging; melanoma is not - it is reported as an informational condition off the copy number and the base coat, never drawn and never simulated (wiki/gene-grey.html#melanoma). The option isn't foreclosed - reopening it means giving the composer a real age input, which today only knows adult vs foal. It is a settled call -wiki/roadmap.html#settled. - Foal geometry is approximate -
Skin.BABYuses rest-pose AABBs and pre-resolved neck/head/ear pivots; markings on the foal face/neck can land loosely. Also the foal mesh has no MANE/MUZZLE part, so bay foal "black up the face" is coarse. Foals are also the top unverified item. - Closed 2026-09-06: the eye channel is finished, and has its own page. Everything this entry used to list as "content, not machinery" is built - blue-eyed creams, the cream-pearl compound's blue-green, champagne's epigenetic amber/hazel/olive (the mod's green and hazel eyes), and one blue eye rather than two. That last one is
EyeSpread: how far a depigmenting claim actually reached, rolled once off the winning locus's own epigenetic seed, which gives complete and sectoral heterochromia from one mechanism. It does put the white loci's eyes in the per-horse fingerprint, and that turned out to cost nothing - the white loci were already non-deterministic.EyePatchContributionis the second hook, for a gene that paints part of an iris. Single source of truth:wiki/eye-colour.html. - White markings beyond splash - the framework is ready (natural + non-deterministic gene). Roan and rabicano have both shipped since; sock distributions are what is left.
- More loci - pearl-cream stacking nuance. Dun, sooty and flaxen have all shipped since this was written.
- Coat realism - outside grey's dapples and sooty's countershading, every gene still samples the gradient flat per pixel (no seasonal coat, and no seasons to have one in).
BodyNoiseis the reusable seam for the next one.Ton a non-deterministic coat still bakes a unique (identical-looking) texture per horse. - Phase 3 now has three inhabitants and the blend question is answered. Magic zebra validated the negative half of the unclamped signed model (
-200%reads black over any coat, dominant white included); pink hair showed that a blind add is not enough on its own - to reach pink on a black mane a fixed delta has to push hard enough to saturate a pale mane to white, so it readsColorView.visibleand returns the delta that walks the texel toward its target. So: straight signed add stays the blend, but the useful magical genes will read first, andmagicalOrder()matters more than §1 assumed.naturalOrder()/magicalOrder()are still hand-written lists- making them derived is the gene-priority work, since shipped.
breedNthfoal names past foal 1 /FamilyTreeScreenscroll mode / stats surfaces / water-riding feel / the epigenome across a save-reload - seewiki/verification.html.- Closed 2026-09-07: both halves. A foal used to copy a parent's per-allele seed exactly, so a closed line converged on one look and no amount of breeding could produce a horse outside what the wild population already contained.
EpiDriftis the answer: every stored value takes a two-sided exponential nudge at each breeding, tuned so half of all drifts move a value under a tenth of a percent of its range and the far tail stays open. Most pairs breed true to the eye; roughly one foal in thirty has one value that visibly moved. (The other half - the epigenome living on the entity, soFamilyTreeScreenhad to invent an ancestor's coat from its UUID - closed 2026-09-03 by moving the epigenome ontoHorseRecord, and the invented-ancestor fallback itself is now deleted rather than merely unused.) What replaces this entry is gap 64: the curve is unplayed. - Partly fixed 2026-09-04; the carrier half is still open. The info panel and the paper dump now read a horse's expressed
Conditions and print their names and sentences, so the prose is finally read by something. What is still unread is the carrier wording - every health locus declares a sentence explaining what one copy means and nothing shows it - plusExpression.masks()outsideGenotypeCatalogandGene.name(). The punnett / expected-foal display is the natural companion and is what turns two carriers from a nasty surprise into a decision. Original entry: Nothing reads the expression table but the coat and the catalogue. Every gene now carries, per combination, a display name and a human-readable sentence saying what it does - written for the gene dictionary and the wiki, and read by neither yet. The obvious consumers: a punnett / expected-foal display, "carrier of X" wording in the info panel (MATP'spearl-carrierand pink hair'spink-carrieralready have the sentence), a generated gene dictionary, andGeneCodeDisplaydeciding what is worth printing.Gene.name()and one moreExpression.name()reader landed 2026-09-04: the rebuilt spawn-egg editor lists genes by their display name and prints the outcome name under any row that expresses. The Horse Browser + the "View Genes" popup (2026-09-05) are the biggest readers yet: they show everyExpression's name and sentence, thewild type/masks/variesflags, and a new gene-levelGene.description()(fromGeneDescriptions, a central table of the built-ins - the first time a gene has a plain-English summary of itself anywhere in code). Still unread: the carrier wording (the health loci's one-copy sentence), and the punnett / expected-foal display that would turn two carriers into a decision. Data-driven genes still have nodescription()(the spec format has no field for it). - Cleanups: rename
DebugPenManager/DEBUG_LEVEL/horsegenetics:debug_pensto non-"debug" names (needs a save-data migration or a one-time reset); name-generation rework; real white-fog dimension effects (needs a client dimension-effects mixin); the strayneoforge.mods.tomlduplicate. - Closed 2026-09-04. The gallery is gone; the catalogue stays as arithmetic.
GenotypeCatalogis no longer materialised (size()is arithmetic,get(i)reads an odometer) because at 2 064 387 entries an eagerList<Genotype>is hundreds of megabytes. That was half the answer; the other half was that a corridor of that many pens is ~7.2 million blocks. A 20 000-pen cap held it for one session and the revert to random pens (wiki/horse-dimension.html) closed it properly - the corridor is a fixed 2 000 pens and does not grow when a gene is added. What is left is a caller note, not a gap:entries()is a lazy view, soentries().stream()still walks and builds all two million - sample it, or ask the arithmetic. Three catalogue tests are seeded sampling for exactly that reason. (The previous #12 -CoatPipelineGoldenTest.overridesilently ignoring an unknown gene, so a case naming a retired gene pinned nothing - is fixed: it throws now, and the five stalecream/pearlcases were re-pointed atmatp=.) - The wiki is now load-bearing, so it can rot.
wiki/api-reference.htmlhand-transcribes public signatures out ofcommon/andwiki/gene-*.htmlhand-transcribes each gene's constants - neither is generated, so both drift silently the moment a signature or a tuning number changes. Nothing checks those. The one place this is now guarded is the gene creator:check-parity.mjscompares its schema mirror and its whole preview engine against the real Java. That is the model for the rest - the cheap version elsewhere is a:common:testthat greps the gene pages for the constants they quote. Until then the other pages are a discipline item, which is why they are in the session-end routine. Confirmed by the 2026-09-04 roadmap audit:wiki/genetics-model.htmlhad drifted exactly this way - its gene table was headed "the eighteen registered genes", omitted all thirteen non-coat loci, and still said lethal white's death "waits on the health system" a day after that system shipped. The roadmap itself was the other half of the same rot: shipped sections were marked shipped rather than deleted, soroadmap.htmlhad quietly become a second description of built behaviour. Both are fixed; the lesson is that "mark it shipped" is not the same as "move it", and only moving it keeps one source of truth. It happened again in the same session the magical utility genes landed: the end-of-session sweep found the gene count, the catalogue size, the raw genotype count and the code-segment length all stale in four different files (roadmap.html,horse-body.html,verification.html, and CLAUDE.md's own pens section) - every one of them a derived number written out by hand. That is the specific shape of this gap and it is now clear enough to act on: the cheap fix is a:common:testthat greps the docs for the handful of numbers the code can compute (Genes.codeOrder().size(),GenotypeCatalog.size(),totalGenotypes()) and fails when a page disagrees. Prose about how a gene works survives a change; a number in prose almost never does. And it is not only the prose. Building the particle locus found thatGenotypeCatalog.totalGenotypes()had already overflowed itslongand wrapped, before this session - so the 3 028 898 126 035 238 912 written into CLAUDE.md,genetics-model.htmland the roadmap was not a stale number, it was a number the code itself was computing wrongly and every doc had faithfully copied. It is aBigIntegernow. The lesson sharpens the one above: a derived number written out by hand is a liability, and a derived number whose computation can silently saturate or wrap is worse, because re-deriving it reproduces the lie. The proposed test - grep the docs for the numbers the code can compute - would not have caught this one; what would is asserting the arithmetic cannot overflow. It happened a third time, and the 2026-09-06 docs pass measured it. The roadmap itself was the biggest offender: shipped sections had been marked shipped instead of moved, so the backlog had quietly become a second description of built behaviour - which is howcarrots.htmlcame to document a splice recipe rule (">= 5 items with a flavour") that the code had replaced withfilled == 4, and a "View splice recipe" button whose payload and handler no longer exist. Also found stale in the same sweep:gene-silver.htmlsaidZ/ZMCOA was waiting on the health system,making-a-gene.htmlsaid status effects were "not built yet" (both shipped 2026-09-04), andindex.htmlstill advertised 38 genes and a genotype count twelve orders of magnitude low. The rule that came out of it: "marked shipped" is not "moved", and a page that describes both built and unbuilt work will always drift on the built half. The roadmap now contains only unbuilt work and is deleted from, not annotated. The grep-the-docs-for-derived- numbers test is still unwritten and is still the cheap fix. A fourth instance, 2026-09-06, and it is a new shape of the same rot: a generated artefact nobody regenerated.wiki/gene-creator/fixtures/expected.jsonis a checked-in snapshot of the Java spec engine, andcheck-parity.mjscompares the JS port against it - so whenHorseSkinGeometry's top/bottom UV swap landed on 2026-09-05 and the fixture was not re-baked, the check went on reporting 3832/48 green while comparing the port against a record of the Java as it used to be. Re-baking surfaced 313 mismatches. The JS port had been a day behind the whole time, drawing every previewed horse with its spine and belly patches exchanged. A snapshot used as an oracle is only an oracle while it is current, and unlike a stale number in prose it fails silently green, which is worse. The creator now runs the check on itself at boot, but that shares the same fixture - the real discipline is that re-baking is part of the check. - Data-driven genes cover markings and dilutions, not everything. The format has no expression language and no way to read another gene, so the three built-ins that genuinely need one still can't be expressed as specs: grey (its remap onto the gradient's neutral column reads the coat's darkness and rewrites both channels together -
PIGMENTmasking gets close but not there), cream/pearl (they read each other's dose), and bay's exact face-follows-legs coupling. Those stay Java, which is fine - the tiers were always meant to bottom out at a real class. What would move the line: adosemask on another gene, and aREMAPop. - Closed 2026-09-06: the creator self-checks parity on boot, and it was built because the gap had already bitten.
js/parity.jsis the single implementation of the check;check-parity.mjsand the page both run it, and the page prints the verdict under the coat sheet.SpecFixtureToolemitsfixtures/expected.jsbeside the.jsonbecause afile://page cannot fetch a sibling file. What the exercise found is the more useful half: the terminal check had been green against a stale fixture while the JS port was a day behind the Java on the top/bottom UV swap. So the residual risk is no longer "nobody ran the script" but "nobody re-baked the snapshot the script compares against"expected.jsonis checked-in Java output, and a stale one is green by definition.bakeSpecFixturesbelongs to the check; see "Build & test".
- Gene
effectsare a thin slice and mostly untested - though less thin since 2026-09-04: the set is eight verbs,healingandspreadjoined it,emittergrew a second colour / a count / adatanumber / five body-site anchors and a registry-backed particle lookup, and built-in genes use the same vocabulary throughAbilityContribution(orEpigeneticAbilityContribution, where the effect varies per horse), so the path is no longer spec-only.attributestill parses but the translator doesn't apply it (logged once).mob_effectis wired (applyMobEffect).glowis wired both sides - the light half is a trailingminecraft:lightblock (janky vs a mixin-based dynamic light: lags a gallop, air-only, orphans on a server crash, skipped in the gallery), the emissive half isEmissiveCoatLayerreading the composed coat.emitter's ownlightkind is still a no-op (useglow);emitterotherwise only does particles;yieldrecognises a fixed handful of output items;walk_on_wateris buoyancy, not a solid plane. Conditions are boolean, not the architecture's 0-1 scalars. There is no trait registry, noon_change, no selectors/auras/pools. The gene creator edits aneffectsblock since 2026-09-06 (all eight verbs, triggers,minDose, and a flatall/anycondition; a nestedwhenis preserved read-only), and its verb table is parity-checked againstAbilityType.healingandspreadare both first drafts:healingcaps atmax_targetsand does a real sphere check, andspreadconverts at most one block a beat off a narrow, hand-written block list - which is the piece most likely to be wrong in play (the risk is a horse eating something a player built with). Only Waterborn's coat + trail are confirmed in-game; Suntouched and every other effect verb are unverified (wiki/verification.html§13, §0a). The full plan iswiki/horse-traits.html. - The tickets are the last inert item, and the art is provisional. Everything else in
item/ModItemsnow does something - seed jars, whistles, stall signs, shearing to gethorse_hair, all five carrots, research papers. The four tickets craft and do nothing: owner's intent is that a ticket teleports its bound horse back to its stall, which is now possible (stalls exist -StallData/StallRecord.center()), it's just not built. Theknown_gene_splice_carrotis still one generic item parameterised by a component, so every gene's carrot looks the same in the inventory. Art: tickets share one texture, whistles share one, stall signs borrowoak_sign, andbraided_rope/hair_clothhave recipes but no textures. Full roster + every recipe:wiki/items.html; art follow-upwiki/verification.html§15. - The stallion seed jar is a first slice, not the full IVF flow. Collection and impregnation are wired (
StallionSeedJarHandler+ thestored_genomecomponent +GenomeSample+HorseBreedingHandler.applyBredFoal). The gate is vanilla love (isInLove()), not one of this mod's breeding carrots (they still do nothing); there's no gestation (the foal appears immediately, like vanilla breeding), and the jar carries no carrot effects. The synthetic sire record uses the stored donor UUID as its pedigree edge, so the family tree may not find the sire node. Gestation is the "genuinely new" piece per the roadmap; the real carrot gate is now buildable (the carrots shipped 2026-09-05). Owner tests 2026-09-02: (a) held jar didn't change in creative -> fixed, transforms in hand now; (b) worked with no breeding-mode requirement -> fixed, both ends now requireisInLove()and consume it.wiki/verification.html§16. - Fixed 2026-09-03 (data-model rewrite), not play-tested. The short genome string now shows data-driven genes:
GeneCodeDisplayderives its trailing gene list fromGenes.codeOrder()(built-ins in a curated display order, thenGenes.loaded()) and derives the "wild type means absent" test fromdominance(), so the shipped Suntouched / Waterborn appear. Confirm in-game (info panel, paper dump, seed-jar tooltip). - Closed 2026-09-09: the repeated HTML ids are gone, and the link checker refuses new ones. Opened 2026-09-05. An
#anchorresolves to the first element with that id, so a repeated id means one of the two sections cannot be linked at all and the other silently answers for it - and every link to it still looks correct, because the id does exist. Eleven of them: eight on the session log (six dated headings plus two “standing build state” sections), two on to be verified (eyesandepigenetics, each used once for an archived section and once for a current one), and one on spawn eggs. Each later occurrence now carries a-2suffix, which is honest rather than pretty and makes both sections addressable.wiki/tools/check-links.mjsnow fails on a duplicate id, so the next one is a red build rather than a page nobody can link to. That check also found the duplicategap-46on this page - two different gaps under one number - which is why the zebra-map entry is now gap 127. - The stall system is detection + storage only. A stall gets defined and persisted (
StallData), but nothing uses it yet: no teleport-to-stall, no "assigned pen" behaviour, no auto-return. Cleanup is thin - a stall only goes away if its exact sign block is broken (BreakBlockEvent); rebuilding a wall elsewhere, or removing the horse, leaves a stale record. The sign is a plain vanillaoak_wall_sign(no marker that it's a stall sign beyond theStallDataentry keyed on its pos). The "debug overlay" is server-emittedHAPPY_VILLAGERparticles on the F7 keybind (one flash per press) + a chat summary - not a persistent client wireframe, which would need aRenderLevelStageEventrenderer (the 26.1.2 render pipeline changed enough that this was deliberately deferred). Flood-fill is air-only and capped atStallDetector.MAX_BLOCKS(512). - Horse care is a first slice, and unplayed.
HorseCareHandler+BondFollowGoal+HorseCareAttachmentcover §7.2 gated healing and §13 bond/herds; details and what's deferred are inwiki/horse-care.html, the in-game checklist inwiki/verification.html§0. Specifics still open: milking (§7.1) is not built (its "full health" rule is the hook into the healing gate); bond has no shearing/sleeping source and there is no console command to set it, so testing the tiers means grinding or temporarily lowering the thresholds; herd alpha is computed-on-demand with nothing consuming it yet; the comfort buff is restated as +1 regen because the mod has no stamina; the healing scan uses block tags so a pack can extendhorse_water/horse_food, but bucket/fluid-source placement inside a waterlogged block only counts via the#minecraft:waterfluid check, not the block tag. Feed-bond fires onEntityInteractfor anyisFoodstack and is not dose/temper-aware. - Closed 2026-09-06: sex-linked inheritance is built, and brindle proves it.
Gene.inheritance()declares the mode;breedWithroutes the sire's copy by the foal's own sex (and still throws the second coin away, so two booleans per gene holds);Gene.sexConsistentkeeps impossible combinations out of the catalogue and out of the random splice; a hemizygous copy lives in a reserved placeholder slot and displays asX-Brn; founder tables are sex-aware.wiki/gene-brindle.htmlandwiki/genetics-model.html#sex-linked. What is left: nothing isY_LINKEDyet (the mirror case is built and waiting for a gene); a punnett square for a sex-linked gene is a different square and the punnett display does not exist at all; and the gene-inspect popup and the paper dump still print the raw pair, so a stallion's brindle locus readsBrn / No copy (Y chromosome)there - only the short form has the prefix. Also still unbuilt, and a different thing entirely: any sexual dimorphism in the coat. Sex paints nothing and deliberately never will, so a stallion's crest would be a separate gene reading this one. - The whole trait / size / health layer is unplayed. Thirteen genes, a new subsystem, four attributes and a death handler, and none of it has been seen in-game - not a scaled horse, not a dying foal, not a refused pairing, not the config. The checklist is
wiki/verification.html§0b, and the quickest way in is the custom spawn egg: adding a health gene defaults it to the affected genotype, so "add PLOD1, pick Foal, spawn" is a one-click lethal foal. Specific unknowns worth naming: a scaled foal (Skin.BABY's projection is already approximate, and nothing has been rendered at 0.88 scale), the rider position on a large and a small horse, whether the info panel still fits (it gained two rows and a condition list), and whether thedeath.attack.horsegenetics.genetic_defectlang key actually resolves rather than showing a raw key. - The stats are purely genetic, with no noise, and that is a decision. Roadmap §6.2 left room for the random roll to survive as environmental jitter on top. It did not: two horses with the same genotype are byte-identical animals. If the numbers ever feel too tidy the place to reopen it is epigenetic variation - drawn from the expressing copy's seed, inherited with the allele, still deterministic - not a fresh die roll. Related: the founder carrier rates (1.4%-4%) are guesses, chosen so a wild horse is healthy and an inbred line is not, and it is entirely possible a player never meets a lethal at those numbers.
- Mostly fixed 2026-09-05 (not play-tested). The milk yield now requires
tamed+full_health, so right-clicking a random wild mare with a bucket no longer swallows the mount; a producing yield on cooldown sends a "nothing to give yet" message instead of a silent cancel; and a stallion / foal now gets the denial branch (kick / message) rather than a no-op. What is still open: the interaction is still cancelled before thetamed/full_healthchecks inGeneYieldHandler(the loopcontinues to the next yield, but if no yield matches nothing is cancelled - so an untamed mare mounts normally now, good; a tamed but hurt mare gets neither milk nor a mount and no message, because the milk yield'swhenfails and there is no denial yield for "tamed mare, not full health"). A "you need to heal her first" denial yield would close it.wiki/verification.html§0. - Two magical genes do something a player cannot see. Milk and verdant both paint nothing, so a lava-bearing horse and a moss-spreading one are indistinguishable from an ordinary horse until you put a bucket under them or watch the floor. That sits badly beside healer, which draws a red stripe precisely so the ability is legible, and beside the rule in
wiki/making-a-gene.htmlthat an effect must be perceivable. The fix is a marking allele in each locus rather than a mark bolted on beside it, which is why it was not done in passing.wiki/verification.html§0a records it as accepted-for-now rather than as a bug. - Closed 2026-09-06:
GenotypeCatalog.size()/get()arelongnow. The leopard complex is a two-outcome coat gene, so it doubled the reduced catalogue to 1 849 688 066, pastInteger.MAX_VALUE- the widening this gap predicted. It was cheap: no production caller indexes the catalogue (the pen corridor is fixed random pens, gap #12), so only tests needed updating -SpecGeneTesttolongarithmetic,GenotypeCatalogTesttolongloop vars,sampleIndices->long[].entries()still returns aList<Genotype>(int-indexed) and now truncates atInteger.MAX_VALUE- documented, and fine, since callers that need the full range useget(long)/size()directly.totalGenotypes()(alreadyBigInteger) went to 1 429 196 794 572 529 076 587 315 200 000. - Two health genes are only half-drawn.
B4GALT7's Friesian dwarfism should shorten the limbs and ribs and leave the head, butAttributes.SCALEis one number for the whole entity - per-part scaling means owning the horse model rather than borrowing vanilla's, so it renders as an overall three-quarter horse.ST14's naked foal is reported and not drawn: phase 1 can only push pigment down, and a de-pigmented mane reads as a white mane, which is a different horse and a worse lie than drawing nothing. Both are inwiki/roadmap.html#health-genes. Also still absent: DMRT3 / gait (animation work), andTraitRule- two genes that only together trigger an outcome (§6.5). CSNB shipped 2026-09-06 with the leopard complex (informational onLP/LP). - Medicine hat and the war shield - not being built, and not a gene. Closed by the owner 2026-09-09, as a decision rather than a fix. A medicine hat is a retention rule on a near-white horse: colour kept as a bonnet over the ears and poll, usually with a shield on the chest. The mod gets the first half for free -
WhitePattern.sabinonever paints the ears, and measured over eight seeds akit=SB1/SB1horse comes out with ears 0% white and the rear 30% of the head 0.4%, so the cap is really there - while the crest and neck behind it go white (mane 99.6%, neck 100%), so on most draws it reads as two dark ears and a smudge rather than a bonnet. That is now the accepted behaviour: “it will roll randomly, I do not care if it does or does not”, and “the hat should not be a unique gene”. Nothing rolls for it, nothing retains for it, and there is no chest region. NoteW22dominant white andO/Olethal white aremaskingoutcomes thatrestrictAll, so they take the ears too - correctly; those were never hats. - The white-pattern audit (2026-09-05) - the big one is now fixed. Root cause of "white over the whole topline" was a UV bug, not a calibration one:
HorseSkinGeometry.faceMapsOfhadFace.TOPandFace.BOTTOMmapped to each other's texture patches, so a painter whitening "from below" hit the spine and left the belly coloured. Swapped 2026-09-05 (owner diagnosed it); measured bold-splash back white ~90% -> ~35%, belly -> 100%.coat-golden.txtregenerated. Also already fixed: face markings under the jaw (shared vocabulary). And splash's own calibration is fixed too (2026-09-06): both painters measure against a topline reference (WhitePattern.toplineHeight=bounds(skin, BODY).yMax()) on a convex ramp, so a single-copy splash stops at the upper cannon (41% legs, 0% belly, 0% back) instead of over the barrel - see the status snapshot. The two residual calibration items are now closed as well (2026-09-06) - both were about the degree of white once it was in the right place, and both were measuring against the whole-horse AABB rather than per-part:- Closed 2026-09-06:
EdnrbGenemeasures against the topline, and got the per-part reference this entry asked for. The barrel band is a fraction ofWhitePattern.toplineHeight(now public) with a per-horse ceiling that never reaches 1.0, and the neck has a band of its own against the neck's own height - which is exactly the "barrel band + a separate neck rule" this bullet said was needed, and the reason usingbounds(skin, Part.BODY)alone was not the fix. Spine white went from ~75% to 0.00 at every seed, and is pinned inWhitePatternGenesTestrather than left as a measurement in prose. The barrel band has since been rebuilt again as a barrel-relative centre rather than a topline-relative floor-and-ceiling, because a floor-and-ceiling band is bottom-anchored and drew a dipped horse whatever it was measured against - seewiki/gene-ednrb.html#paints. coveris calibrated as ifPatchNoise.fieldwere uniform, and it is a bell. The field concentrates near 0.5 and rarely leaves [0.25, 0.70], so acoverrange written as if the field were uniform spends most of itself outside the field's actual spread and delivers blank-or-flooded rather than a range.EdnrbGene's half is genuinely closed now (2026-09-06, second attempt). The first attempt re-picked the constants against measured coverage and this entry recorded it as closed - but re-picking a constant only moves where a bell gets cut, and the gene still painted a dipped horse. The fix that worked was to stop using a constant at all:paintFramescores every eligible texel, sorts, and cuts at the quantile matchingcover, so the knob is an area fraction by construction and cannot drift when the field or the geometry moves. The lesson generalises - a threshold on a noise field is only meaningful relative to that field's own distribution - so the two-pass quantile is the shape the rest of this entry should be fixed in, not another round of re-picked constants.TobianoGene's half is closed too (2026-09-06). Same two-pass quantile, and the vertical reference moved off the whole-horse AABB onto the barrel's own bounds - the topline bias had been delivering about a third of its nominal strength on the back and its full strength on the mane. The knob was re-declared as the range the old code measurably produced (31%-86%) and the two biases re-tuned to hold the back and legs where they were, so the population is unchanged and only individual coats moved;coat-golden.txtregenerated, 12 rows, alltobiano=To/to. Tobiano also had no shape tests at all before this - it has four now, including one that fails against the old bare threshold.What is left of this entry: nothing in the two genes it named. The lesson is general, though, and a first pass over the shared code found one concrete instance - not fixed, because it moves KIT and sabino coats and this was found while closing a different change:
WhitePattern.BODY_JAG_FREQ = 3.3is sub-texel, the same defect frame's jag had. A barrel texel is half a model unit, so 3.3 per unit is a period of 0.61 texels - against frame's old 3.4, which was 0.59 and which turned out to be per-pixel white noise rather than an edge treatment. It is the body-patch jag for the wholeKIT/ sabino path (WhitePatternline ~207), so any raggedness it is supposed to be adding to a sabino's patch margins is very likely aliasing instead.LEG_JAG_FREQ = 1.6is 1.25 texels - marginal, worth looking at in the same pass. Frame's replacement is expressed as periods across the barrel rather than per model unit, which is the shape of the fix.- Still unaudited: whether any other painter cuts a noise field at a constant the way
coverused to. The two-pass quantile inEdnrbGene/TobianoGeneis the pattern to copy where one is found.
- Closed 2026-09-06:
- Breeds + herds + aggro are built but barely played, and the numbers are estimates.
wiki/verification.html§0e is the checklist. The crash (DistanceManager.runAllUpdatesNPE) recurred once and was re-fixed by deferring the entireonHorseJoinbody toserver.execute- needs a sustainedrunClientflight to confirm it holds. #6 (only Unknown breeds) should be fixed (chunk-gen spawns were bailing before the biome lookup;HerdManagerdoes it deferred now) - confirm real breeds appear. Mixed-breed herds (owner-confirmed still broken after the 3rd revision) got a 4th revision 2026-09-05:HerdManagernow flood-fills the whole connected clump and elects a deterministic lowest-UUID lead whose UUID seeds the breed/band draw, so task order and the >32-block spread of aNaturalSpawnerpack can no longer split one clump into two herds. Not yet re-tested in-game - confirm a spread pack now reads as one breed, and that the last member of a very spread pack doesn't fall out as a lone Unknown. The herd systems (HerdManagerclump election,WildHerdGoalcohesion, traditional vs bachelor composition) and wild aggro (HorseAggroHandler- hit one, the herd kicks you; lose LOS, they calm) have had zero play. The load-bearing unknowns: whether horses actually spawn in the ~30 new biomes (the biome modifiers parse at boot but nothing has flown to a taiga to look); whether a herd reads as one breed on the info panel; whether breed shows on the body (a Thoroughbred herd visibly faster, a Falabella herd visibly tiny - the band ->multiplySpeedUnclamped/multiplyScaleUnclampedpath is unit-tested but never rendered); whether the cross / mixed labels come out right through a few generations of breeding. The per-gene pool rates inBreeds.java(how often a breed carries grey, sabino, cream, ...) are eyeballed to "reads as that breed, not a monoculture" and have had no balance pass.add_horse_lonerstargets#minecraft:is_overworldwholesale, so total world horse density is unmeasured and may be too high. Two design calls flagged inwiki/roadmap.html#decisions: Unknown as a cross component ("Friesian x Unknown cross" vs. making Unknown absorbing like Mixed) and the cross stat rule (per-axis average of the parents' bands vs. leaning toward the stronger parent). - The whole carrot / database / shearing / milking pass is unplayed. None of it has been seen in-game - not a carrot fed, not a biased foal, not a sheared horse, not a paper read.
wiki/verification.html§0 is the checklist. The load-bearing unknowns:- Does
NONE/NONEbreeding really match the old draw in-game? The unit test pins byte-identity andcoat-golden.txtis untouched, but a live foal from two un-fed parents has not been checked against a pre-pass foal. - The Known Gene Splice carrot recipe.
KnownGeneSpliceRecipeis aCustomRecipebuilt against 26.1.2 sources without an in-game craft - thematchesheuristic (1 gold carrot, 1 paper, >=1 hair, the rarity item, >=5 filled slots) may be too loose or too strict, and the recipe-book / JEI display of a no-fixed-ingredients special recipe is unknown. Same forCarrotCombineRecipe. - Gene-splice determinism across a reload. It rolls its target gene at breeding time off the foal RNG; two carriers bred repeatedly should give a spread, not the same locus every time - unverified.
- The gene database UI. The browser tab now depends on
ClientGeneDatabasebeing synced (login + on discovery). If the sync misfires the tab shows every gene as undiscovered. The "Write research paper" button's book-scan + payload round-trip is untested. - Loot injection.
add_research_paper.jsontargets 8 chest tables by id; whether those ids are still current in 26.1.2 and whether the modifier fires at the0.18rate is unchecked. The folder isloot_modifiers/(plural, no index file) - a boot with the wrong path logs "Couldn't parse" but does not crash. AnimalTameEvent- is it the right event, and doesgetTamer()give aServerPlayerfor a horse tamed by repeated mounting? If not, taming discovers nothing and only breeding + papers fill the DB.- The stallion-kick damage source (
horse.damageSources().mobAttack) and whetherplayer.hurtServeris the right call for hurting a player here.
- Does
- The sheared look is not built. The shearing mechanic is complete (hair, once/day cooldown, bond, sound, particles) but there is no visual difference on a sheared horse. Settled §21 as a low-fidelity render-layer overlay (no coat bake) driven off the
"shear"cooldown stamp; it needs a sync of ashearedbool (theHorseCareSyncPayloadhas 4 construction sites, so extending it is the fiddly part) and aclient/ShearedHorseLayeron theEmissiveCoatLayerpattern. Deferred rather than done badly. - Closed 2026-09-06: the §19 metadata has creator forms.
blurb,rarity,carrot(enabled / het-hom / flavour) andspliceare all editable, andtidy()drops whatever is still at its default so a gene that ignores them still exports a short file. They do not paint, socheck-parity.mjssays nothing about them; they are guarded instead byCreatorMetadataRoundTripTest, which parses the creator's own export with the realGeneSpecParser. Still hand-edited: a gene with more than two alleles, or more than one visible outcome. The creator authors the two-allele / one-outcome case and says so on screen rather than pretending otherwise - the format allows any number of both. - Almost no built-in gene declares a
splicetable. Re-checked 2026-09-11: the flat claim is no longer true — exactly one built-in (the diet locus) declares one, so the machinery is proven and what is missing is content. Everything below still applies to the rest. Every built-in falls back to the uniform draw over its viable pairs when the Unknown Gene Splice carrot lands on it - which for a 40-allele locus like particle is a near-guaranteed weird outcome, and for a lethal-carrying locus can hand a foal a lethal genotype (the draw is overcanOccurpairs, so an embryonic lethal is excluded, but a birth lethal likeO/Owas reachable). The lethal half is closed 2026-09-06:SpliceSafetyfilters the pool to loci that cannot kill or cost hearts, derived by resolving every combination throughHorseTraitsrather than from a typed list, so a health gene added later is excluded by having done its job. What is left is the flavour complaint - a uniform draw over a 40-allele locus like particle is a near-guaranteed oddity, and a per-geneGene.spliceTable()is still the seam for that. - The green/pink LUT is unplayed, and a LUT is the one kind of art a test cannot judge. Reopened 2026-09-08 for the second palette; the blue/pink one was confirmed working in game and that half is closed.
lutgreenpink.pngis wired exactly aslutbluepink.pngis - the golden file pins aGrnpnk/Grnpnkhorse, a unit test walksVARIANTSrather than naming a palette, and the offline tools now build their LUT set from the registry (LutSet.fromRegistry) instead of each writingbluepinkout by hand - so the machinery is as covered as the first palette’s. None of that says the chart reads well on a horse. A LUT is sampled by "how much red survived" against "how much black survived", so what matters is what the corners and the middle do: a green-through-pink chart with a pale seam down it will put that seam somewhere on every horse, and where it lands is a property of the art rather than of the code. Spawn aGrnpnk/Grnpnkover a black, a bay and a chestnut and look at all three; also check the log has no "could not load alternate LUT" warning, which is the one failure that would render a perfectly ordinary horse and look deliberate. - The Horse Browser crafting tab is an untested GUI. The browser was rebuilt from a client
Screeninto anAbstractContainerScreen+HorseBrowserMenuagainst the 26.1.2 retained-mode screen API, with no way to play-test here. Risks: the fixed-panel slot/label geometry (the Gene DB detail pane now shares a much narrower window and could collide with the inventory or tab strip), a ghost slot-highlight sprite bleeding through on the Gene DB tab (the grid slots are madeisActive() == falsethere, which should suppress hover/render/click, but the highlight path isprivateand only partly reachable),quickMoveStackedge cases, and thebook -> research_paperresult path (result computed inHorseBrowserRecipes.resultFor, consumed in.consume- the "one book, nothing else, a discovered carrot-bearing gene selected" gate is the fiddly part). Server side booted clean (MenuType+ both payloads register).wiki/verification.html§0-B, and re-run §0 inside the new window. - Brindle, tiger eye and the recalibrated white are all unplayed, and the white one is the widest-reaching thing this project has shipped in a while:
PAX3'sSW2is on 90% of founders, so the topline recalibration changes what an ordinary horse looks like. The measurements are in the status snapshot and the checklist iswiki/verification.html§0-G. The specific unknowns worth naming: whether the eight-stepKITladder still reads as eight steps now that the low end is genuinely low; whether the saturating stacking signal still lets the two splash loci visibly stack (it is tuned soSW1/N + SW2/Nreads as a bold splash) while not turning a tobiano near-white; whether brindle's untouched head is a visible seam at the poll; and whether a two-texel amber iris reads as an amber eye at riding distance - which is the single most likely thing to need another pass. GeneCodeDisplay.trailingOrder()was rotten and is now the template for fixing gap #13. It was a hand-written gene list plusGenes.loaded(), so every built-in gene added after it was written vanished from the short form silently - by the time it was caught it had swallowed the leopard complex, the LUT locus, the cutie mark, the particle locus and the three magical body-stat genes, and nothing failed until a catalogue test happened to find two entries with the same label. It now appends every other registered gene in(priority, key)order after the curated head. The lesson is the general one: a derived list written by hand rots, and it rots without an error. The remaining instances of the same shape are the ones gap #13 names -wiki/api-reference.html's hand-transcribed signatures and every derived number typed into prose - and the cheap fix there is still unwritten: a:common:testthat greps the docs forGenes.codeOrder().size(),GenotypeCatalog.size()andtotalGenotypes()and fails when a page disagrees. Three sessions in a row have now found stale derived numbers by hand.wiki/compatibility.htmlis entirely unverified against another mod. Every claim on it is read off this mod's own source and off how NeoForge's registries and events work - not off a play session with a modpack. The renderer conflict is a certainty (only one mod can ownEntityType.HORSE's renderer); everything marked "fine" is a prediction. The page says so, but it is worth repeating here: it is a map of where to look when something breaks, not a compatibility report. The one thing that would make it real is installing this beside a tack mod and a performance mod and seeing what happens.- The horse designer has never been opened, and its GUI has no automated net. The genetics are as safe as they can be - the page runs
common/compiled to wasm, byte-identical to the JVM, so there is nothing to drift. What is unguarded is everything on top:js/gui.js(a canvas redraw ofCustomHorseSpawnScreen, ~450 lines of layout maths),js/scene.js(camera rig, WASD, the sideways framing offset that puts the horse in the gap between the panels) andjs/animation.js(the gait, an approximation by construction- vanilla's
setupAnimneedswalkAnimationPos, which no browser has). Checked headlessly instead: the exported API end to end, and the GUI driven against the real wasm - every right-column button fires its method, a row adds,KIT's eight alleles open a dropdown and a pick lands. That is logic, not looks.wiki/verification.html§0-I. The sharpest test when it is opened is to copy a genotype code out of the page and paste it into the spawn egg in game: the two horses must be identical, and that single check exercises the compile, the facade, the editor model and both UIs at once. What it still cannot show is three Minecraft-side things - cutie-mark item icons and particle emitters (both come out of the game's own registries, and the spawn egg previews both) and emissiveglowtexels. Nothing genetic is missing any more.
- vanilla's
:web:bakeDesignerAssetsis a step it is possible to forget. The wasm inwiki/horse-designer/wasm/is a checked-in build artefact, so a change tocommon/does not reach the page until it is re-baked - the designer would go on running yesterday's mod, confidently. It is the same shape as the creator's stale-fixture trap (gap #13's fourth instance) with one important difference: this artefact is the code rather than a snapshot of its output, so it cannot be subtly wrong, only wholesale old. Agit statusshowingcommon/changed andwiki/horse-designer/wasm/unchanged is the tell. Worth wiring into the session-end routine, or into:common:testas a staleness assertion.- Almost every gene is the same rarity, so almost every gene carrot costs the same. Re-checked 2026-09-11: “no built-in overrides
Gene.rarity()” is now false — seven do, across four of the six tiers. That is seven of some two hundred, so the economy is still effectively flat and the rest of this entry stands, but the override is no longer unused and the fix is content rather than machinery.GeneRarityhas six tiers and the recipe charges the matching item (server/recipe/RarityItems: iron / gold / diamond / emerald / netherite ingot / nether star), but no built-in gene overridesGene.rarity()- all of them fall through toGeneRarity.DEFAULT. So the whole economy is flat: a gold ingot buys a tobiano carrot and a gold ingot buys a magic-health carrot, and the research-paper loot weighting that sorts on the same axis is uniform too. Nothing is broken; the axis is simply unpopulated, and it was invisible until the wiki's per-gene carrot card started printing each gene's tier on its own page. Tiering the genes is the work - and it wants doing as one pass over the registry rather than gene by gene, because the tiers are relative to each other. - Closed 2026-09-09:
SeededRngscrambles its seed.java.util.Random’s seed handling is a single XOR, which is not a mix, so the firstnextFloat()off sequential seeds barely moved - measured over seeds 0..3999 it spanned 0.58 to 0.95, mean 0.77 on the bare constructor and 0.36 to 0.73 through a namespace, never touching either end of [0,1). It cost most of an hour onEyeSpread.roll, whose 62/22/16 split measured as 100/0/0. The constructor now puts the seed through splitmix64’s finaliser beforeRandomever sees it - the same mixSplittableRandomuses, written out becausecommon/targets TeaVM and one day Java 8. The same measurement now gives 0.0002 to 0.9999, mean 0.5043. It changed every horse ever generated, as predicted: 883 of 1056 lines of the pipeline golden and 218 of 623 of the bake golden moved, and the moved set is exactly the genes that store epigenetics. Both goldens, the gene icons and the designer wasm were regenerated in the same change. See gap 117, which was the same root cause seen from the other end. - The designer wasm is not byte-reproducible, so
git statuscannot tell you whether it is stale. Two consecutive:web:bakeDesignerAssetsruns on unchanged sources producewiki/horse-designer/wasm/web.wasmfiles of identical size and different bytes - TeaVM leaves some ordering or padding nondeterministic - so the file shows as modified after every bake whether or not anything about the mod changed.CLAUDE.md's rule still holds in the direction it is written (common/changed and the wasm untouched means the designer is running yesterday's mod); the inverse does not, and treating "wasm modified" as proof of a real change is how a stale one gets committed anyway. The usable signal is the file size, which is stable across re-bakes and moved by a genuine content change - a stale bake caught at the end of this session was 1 103 396 bytes against 1 103 724 for the current sources. Worth fixing at the source if TeaVM exposes a deterministic-output flag; until then, re-bake last, after the final Java edit, because string edits count: anExpression.describe(...)line is compiled into the wasm and shown in the designer. The same defect turned up in the barn bake on 2026-09-07 and was fixed there, which is worth recording because it shows the shape of the fix:bake-barn.pywas writing a fresh gzip header every run, socowboy_barn.nbtrewrote itself with identical content and different bytes. One argument (mtime=0) made two consecutive bakes byte-identical, andgit statuswent from noise to a real staleness signal - which for that artefact is the only one there is. If TeaVM has an equivalent flag, the wasm is the same one-line fix. - The two zebra loci draw the same map from independent seeds, so a horse carrying both wears two patterns that do not line up.
ZebraStripesis shared - natural zebra whitens the gaps in phase 1, magic zebra blackens the bands in phase 3 - but each gene pulls its ownnextLong()off its own allele's epigenetic seed, which is correct for every other purpose and wrong for this one. On aZeb/Zeb Mzeb/nhorse the black bands fall wherever the white ones are not, so the coat is striped twice at two different phases. Nobody has looked at one yet; it may read as a dense, chaotic zebra (fine, even interesting) or as mud. The fix if it is mud is for magic zebra to read the natural locus's seed when the horse carries it - which would be the model's second cross-locus-reading gene after the leopard complex, and wants the same care: it has to go throughGene.coatDependsOn()or the texture key will not know the coat changed.wiki/verification.html§0-Z. - Closed 2026-09-09: a seeded horse no longer moves when an unrelated gene is registered.
Epigenome.fromSeedwalkedGenes.codeOrder()drawing from one shared stream, so registering a locus at priority 68 gave every gene above 68 a different draw and the horse at seed 13 became a different horse. That was free in play - there are no saves - and expensive in tests: registering natural zebra broke three assertions inWhitePatternGenesTest, all by margins under 0.05, none a real regression, and the only signal was a red build after an unrelated-looking change. Each gene now draws from its own generator, namespaced by its key (Epigenome.copiesFor), so adding, removing or re-prioritising a locus moves that locus and nothing else. Two tests inEpigenomeTestpin it: one recomputes every gene from(seed, key)alone and demands the whole horse agrees, the other walks the registry backwards and demands nothing changes. Both were confirmed to fail when the shared stream is put back. The narrower shape survives on purpose: changing how many values one gene declares still reshuffles that gene. That is one gene, the bake golden names it, and it is the change you were making. The prescription in this entry also got applied - two tests calibrated against a specific draw rather than a distribution were restated, having failed on the reseeding for no real reason: the splash dose-response (which was comparing single horses at seed 3, the one unluckiest draw in sixty, where the mean margin is three times what it asserts) and the grey spread (a range over 8 seeds, the statistic most sensitive to a small sample; over 24 it is about 45 against a threshold of 20). - The coat PNGs have a generated second copy, and nothing tells you the two have drifted.
:web:bakeDesignerAssetscopiesredblackgradient.pngandlutbluepink.pngout ofcommon/src/main/resources/intowiki/horse-designer/assets/, where every wiki page that loads the mod reads them - the horse designer, every gene preview window, and now the LUT lab. This is gap #42's problem for the art rather than the wasm, and it is worse in one specific way: the wasm can only be wholesale old, but a gradient can be subtly, plausibly wrong. A page will render every horse against a chart the game has never used and look entirely correct doing it. It was live, and is closed for now: the two had drifted badly during 2026-09-06 (the wiki's copy was neither the committed gradient nor the current work), and a:web:bakeDesignerAssetsrun later the same day brought them back into step - both copies and both committed states now match. That re-bake is also how the risk was demonstrated rather than argued: the copy task overwrote an uncommitted edit sitting in the generated location, which is precisely what that location cannot hold. The gap itself is not fixed - nothing checks that the two match, so they can drift again the moment either is touched. The direction is one-way:common/is the real one, the wiki's is output, and an edit made only to the wiki's copy changes every wiki page, changes no horse in game, and is destroyed by the next bake - which is exactly the trap the owner has hit before. The tell is agit statuslisting thecommon/PNG and not itswiki/copy, or vice versa; there is no check that they match, and a byte comparison after the bake would be a cheap one. Note the golden coat test cannot catch this at all:CoatPipelineGoldenTestcomposes against a synthetic gradient, by design, so art edits never turn it red. - The default gradient has no saturated warm zone, so every pale outcome is a neutral. Restated 2026-09-06 against the new default (
horselut.png), which fixed the specific defect this gap was opened for and left the general one. What is fixed: the old chart's warm ramp held 65–70% saturation out to X 50% and then fell off a cliff - 43% at X 55%, 3% by X 70% - so a dilution nudged slightly right could fall out of gold into grey with nothing in between. The new chart's top row declines smoothly and monotonically instead: 58% at the red edge, 37% at X 15%, 23% at X 35%, 13% at X 55%, 4% by X 75%. No cliff, no discontinuity, and a gene author moving a constant now gets a proportional change. What is not: peak saturation anywhere on that row is 58%, at the far-left brick red, and it only goes down - there is no gold band at any X. So the pale end of every dilution resolves to a cream or a greige rather than a colour, and the traditionally golden coats are the visible cost: palomino is#E5DBC7, buckskin body#B1A590, gold champagne#E9E4D8. Whether that is a defect at all is an art call, not a code one - a desaturated chart is a defensible naturalistic choice, and real horse coats are less saturated than the names for them suggest. It is recorded because it is invisible from the code: nothing inMatpGeneorChampagneGenesays "and this will come out grey", the constants look like a strong dilution either way, and the only way to see it is the LUT lab's footprint overlay. If gold is wanted back, it is a warmer band in the upper middle of the chart, not a constant in a gene. - Closed 2026-09-09: the creator's composer mirror had been dead code, and the composer is now inside the parity gate. Opened 2026-09-05. The game ramps a resolved colour's opacity from
PURE_BLACK_ALPHAup to fully opaque byNEAR_BLACK; the creator's mirror injs/fields.jsstill had the older rule the game itself once had -rgb === 0 ? PURE_BLACK_ALPHA : 0xFF, an exact equality against pure black. It was worse than “drifted”: it had stopped doing anything at all. Black bakes to about#161515on the shipped chart, so that branch never fired, and the creator drew every dark coat at full opacity - darker than the game, on every preview. It is the same equality that broke the game once before, when a LUT edit moved the chart's black corner off#000000; the game was fixed and the mirror was not. The real finding is why nothing caught it.check-parity.mjsrunsrestrict()andtint()and never runscompose()- 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. The composite itself still cannot be compared in the harness (it needs the gradient and the template, and there is no image decoder there), but the arithmetic now is: the fixture carries answer tables fornearBlackAlphaand the shadow lift, and parity checks them. Verified by restoring the old equality and confirming three mismatches, one of them on#161515exactly. - Closed 2026-09-09: measured, and it is fine. Opened 2026-09-07 when the splash and KIT overhauls left two independent loci handing out ordinary white markings with nothing measuring their combined effect.
FounderWhiteRateTestnow walks a founder population: about two founders in three wear white from these loci, mean coverage about 10% of the sheet, and only about 8% are loudly marked. The estimate in this entry was three in four, so it was in the right region and slightly high. That is a defensible naturalistic population - real horse populations are mostly marked - so the test holds the number rather than condemning it, and it is the instrumentMitfGene.WILD_SW1_PERCENTandKitGene.frequencies()should be tuned against, which is what this entry actually wanted. The band is deliberately wide: the point is to catch a population with no white in it or one that reads as a pinto herd, not to pin today’s figure. The measurement has a trap in it worth knowing about. Counting white on a founder directly says 91%, which looks like a catastrophe and is an artefact: on a flat greyscale chart a cremello, a grey and a palomino are pale coats with no marking on them, and a brightness threshold cannot tell a white marking from a white horse. The giveaway was 91% wearing white while only 78% carried either locus. The test composes each founder twice - as rolled, and again with these loci forced wild - and takes the difference, so the base coat cancels. The underlying design point in roadmap §10.2b stands and is unaffected: neither locus should be the source of ordinary markings, and SW1 is standing in for a polygenic system that does not exist. - Two adjacent bay shades may be indistinguishable in game light, and nothing but eyes can say. Shade puts four named bands on one continuous score, and the four were separated by measuring baked sheets - blood
#5E3827, bay#462B1E, liver#2A1B15, seal#231C18on the body, before the template multiply. The gap between liver and seal is 7 luma steps, and the gradient compresses hard toward its black corner, so the dark half of the range is where a band edge is most likely to be a distinction only the code can see. There is no test that can settle it: a difference in hex is not a difference a player notices, and the golden file composes against a synthetic gradient anyway (gap #48). It is arunClientquestion -wiki/verification.html§0-P - and if the answer is “they look the same” the fix isBayCoat.BODY_CURVEand the two endpoints, not the band edges, which are set by the genetics. - The cowboy's barn has never been seen generating, and three assumptions in its placement can only be checked in a world. Opened 2026-09-07 with the villager work. The pool append is proven - the server logs it - but that only says the element went in. The barn does now generate - the owner has seen one - so what is left unproven is: (a) that terminator slots really are the village edge in practice and not just in the pool graph, which is the whole justification for the placement and would need rethinking rather than tuning if it is wrong; (b) was wrong and is fixed: a connector at
y=1landed the barn's foundation in the road's own layer and every barn generated a block buried, steps and all. It is aty=0now, which is where vanilla houses put their entrance connector and why they sit a block proud of the street; the fix itself is unlooked-at. And (c) that a 15×15 piece placed at a 2×3 slot passes the bounding-box check often enough to appear in most villages rather than one in ten — which got harder, not easier, when the horseman's house was baked into the same piece and doubled its depth. The pool weight went 1 → 3 to compensate, and that 3 is arithmetic, not measurement: nobody has counted barns per village at either weight. Nothing automated can answer any of the three, because all three are about what the vanilla generator does with the piece.wiki/verification.html§0-AC. - Getting a herd of horses through a two-block doorway at dusk was a pathfinding bet, and it lost. Closed by deleting the thing that needed it. The cowboy used to ride home at dusk, open four double doors, wait for eleven horses to file through one gap and shut up behind them; it was tried for a day and never made to work. Three independent reasons, any one of which is fatal: a horse over about 1.43 scale cannot path through a two-block doorway at all (the ground pathfinder rounds a mob up to
floor(width + 1)blocks across, so it wants three); a villager who wanders in is a cork, because a village brain has no reason to leave a building at night; and the string shoved each other back out of the entrance, mobs being pushed apart rather than blocked. Each fix exposed the next - a no-push scoreboard team for the third, an eviction sweep for the second, and nothing at all for the first. The owner's call was that a mounted man's bedtime is not worth a subsystem. He now dismounts at dusk, spends the night as an ordinary villager with no custom code, and is walked back to his horse at dawn; he survives on ten times a villager's health rather than on shelter. That removedCowboyDoors,HerdCollision, theSHELTERduty and about five hundred lines. Recorded rather than deleted because the lesson outlives the code: vanilla navigation will not put a large animal through a door a player walks through without noticing, and no amount of goal code changes that. - Two barns in one village is possible, and only the tie-break stops two cowboys. The jigsaw generator cannot express "at most one of this element per structure", so weight-1 in a five-element pool occasionally draws twice. A second barn is harmless - it reads as another stable. A second cowboy would be two herds and two shops in one village, and the only thing preventing it is a lowest-UUID election among cowboys within 128 blocks, which is the same rule
HerdManageruses for a wild herd lead. It is untested here, and its failure mode is silent: you would have to notice two named riders. The 128-block radius is also a guess against a village's own size. - A branded horse's brand outlives its cowboy if the two are ever loaded apart.
CowboyHandler.ownerOfclears the brand when the cowboy cannot be resolved, which is the self-healing path - kill him and his herd goes feral. It resolves by UUID across the loaded level, and the cowboy is persistent and lives in the same building as his horses, so in practice they load together. But "in practice" is the whole guarantee: a horse loaded while its cowboy's chunk is not would be freed by that same self-heal, which is a small exploit (walk to the edge of the loaded region to shake a horse loose) rather than a crash. The tighter fix is a saved-data list of cowboy ids rather than an entity lookup, which is more machinery than the problem has earned so far. - The horseman's tier-4 and tier-5 gene trades are priced against nothing. A random uncommon gene carrot is 18 emeralds and a random epic one is 40 because those numbers sit in a plausible-looking ladder, not because anyone compared them to what the same carrot costs to craft from a research paper and its rarity item (
RarityItems), or to how long it takes to find the paper. The roadmap's own balance argument is that a villager should be more expensive than finding it yourself; whether these are is unmeasured. The cowboy's horse prices used to be cited here as the milder case, on the grounds that they were at least ordered by the same rarity axis the wild herd roll uses. They are not any more: every horse is 1-3 emeralds (HorsePrices.DEFAULT_PRICE) because that is what an early-game purchase should cost, and no breed has yet named the range that would make it dearer. So the cowboy is now priced against a stated intent and the horseman is still priced against nothing, which is one problem fewer and the same problem. - Something disabled the
MOVEcontrol flag on the cowboy's mount, and nobody ever found out what. Closed with the code it applied to.CowboyMountGoalwould start, run five ticks, stop with its rider plainly aboard, and never start again; sincecanContinueToUse()andcanUse()were the same test on that goal, the only path inGoalSelectorthat both stops a goal and blocks its restart without consulting either isgoalContainsAnyFlagsagainstdisabledFlags. ReassertingenableControlFlag(MOVE)once a tick fixed it outright. Vanilla only disables that flag fromMob.leashTooFarBehaviour, which should be unreachable for a horse nobody has leashed, so the culprit was never identified - and the goal has since been deleted along with the rest of the mounted subsystem, so it never will be. Recorded because the shape recurs: a goal that stops for no reason its own predicates can explain has had a control flag disabled underneath it, and no amount of reading the goal will show that. - The horse on a transfer paper is fitted into its slot by three eyeballed numbers.
TransferDeedRendererdraws the mod's HD horse mesh in an item slot, andFIT,LIFTandCENTRE_Zwere derived from the model's own cube offsets on paper rather than from looking at one. The display transforms inmodels/item/signed_transfer_paper.jsonare the same kind of guess - a side-on view was chosen because a flank shows a coat best, which is the whole point of the feature, but the angle and the in-hand scales are untested. A horse that is clipped by the slot edge, standing on the bottom of it, or facing away is a constant, not a design problem. Nothing about the pipeline is in doubt: it compiles and the coat texture is the same one the entity renderer uses. - The cowboy's house could not be built by the cowboy, and it took a day to see why. Closed 2026-09-07. The horseman needs a bed POI and the barn cannot hold one — it is a building made of doors and a villager only ever shuts the one he walked through — so a copy of a vanilla plains house was placed at runtime when the cowboy founded, onto the first patch of flat, clear, untouched ground it could find near the barn. It never appeared once in a real village: the flatness test wanted every column of a 7×7 footprint to agree on its surface height, which is a thing village terrain essentially never does. Loosening the test was the obvious fix and the wrong one, because underneath it sat an ordering the code could not satisfy: the work posts have to stand on the house, the work posts are what make a cowboy, and a cowboy cannot build the thing that made him. The house is baked into the barn's own structure piece now, work posts and villagers and all, and there is no order left to get wrong. Recorded because the shape recurs: when a placement test fails every time, check whether the thing being placed could have been placed at that moment at all — a bad test and an impossible ordering look identical from the log.
- The homestead has never been seen, and the thing that could break it is the thing that used to work. Opened 2026-09-07 with gap 61's fix, and widened again the same day. The barn was a proven, generating piece; taking the house into it roughly doubled its depth, and the walk along the front has since added a column and a layer on top of that. Footprint is the one property the jigsaw generator judges a piece on, so every one of those is a bet against the bounding-box check. So a change made to guarantee the house exists can just as easily mean neither building does. Three things fall out of that and none can be checked outside a world: whether the piece still lands in most plains villages at weight 3; whether a rigid piece that deep sits level, or leaves one of the two buildings cut into a slope; and whether the two work posts, which moved from the barn's end wall to the house's front step and then a column east again when the walk went in, still get claimed by the villagers now standing on the walk in front of them. The first is the one that matters — a subsystem that generates in one village in ten is not a subsystem.
wiki/verification.html§0-AC. - The cowboy can open a door for the first time, and the rule that he only does it by day has a hole in it that nobody has walked into yet. Opened 2026-09-07. He is an
AbstractVillagerrunning goals rather than a villager brain, so until now he had neither a goal to open a door nor a navigator that would route through one;CowboyDoorGoalis both, and it opens both leaves of a double door because one leaf is not a gap to a horse. Three things about it are designed rather than measured. He never closes anything — deliberate, because the herd is following him through and the barn doors standing open is what the doorway headroom was cleared for, but it means a door he opens at noon is open all night. He cannot open one after dark at all, which is what was asked for and is only harmless because nothing walks him home any more: the dusk routine went with the mounted cowboy (gap 55), so there is no behaviour that needs a door at night. If anything ever wants him indoors again, that rule is the first thing it will hit. And "day" isLevel.isBrightOutside(), shared with the gene effects'daytrigger, which reads sky darkening rather than the clock — so a thunderstorm counts as night and he will stand at a shut door until it passes. None of it has been seen in a game.wiki/verification.html§0-AC. - Drift is tuned on paper and has never been bred. Opened 2026-09-07. The curve is what the owner specified and its distribution is unit-tested against its own quantiles, but nobody has bred thirty generations of anything to find out whether a line actually develops character at the rate it should, or whether “one foal in thirty has a visible change” reads as pleasant variety or as noise. The two numbers to turn are
EpiDrift.SCALEandEpiDrift.REPLACE_CHANCE, and they are deliberately the only two. The awkward part is that it cannot be checked quickly: the whole design is that a single generation is imperceptible, so any honest test of the feel is a long breeding project, and a short one will always report “nothing happens” whatever the tuning is. The cheapest proxy is the epigenetics readout on the gene screen — breed a pair, open both, and read the numbers rather than looking at the horses. - The epigenetics readout is unplayed, and it is the only window onto the whole feature. Opened 2026-09-07, and moved the same day:
GeneInspectScreenis deleted and its readout is now the Health, Coat and Other genes tabs ofHorseInfoScreen, formatted byEpigenomeReadoutincommon/. Nothing about it has been seen in a game. Specific unknowns, unchanged by the move: whether a row fits at all (ednrb has nine values and one of them is four per-leg numbers on a single line), whether a seed truncated to six hex digits is useful or just noise, and whether the›marker for the expressed copy reads as anything. The move added one: the Genes tab deliberately shows no values at all, so if the split between “index” and “detail” is wrong, the readout is now one tab further from anyone looking for it. It is still the only place a player can see epigenetics — there is no editing UI in-game, so tinkering still means the horse designer or the spawn egg's code box.wiki/verification.html§0-AG. - An epigenome code is eight thousand characters and nothing budgets for that. Opened 2026-09-07. Storing literal values instead of a seed roughly doubled the code even after dropping the genes that store nothing, and it scales with every value of every varying gene — so adding one wide gene moves it by hundreds of characters. It is written into entity NBT, the horse record, the
stored_genomeitem component, the transfer deed and two network payloads. The first of those cliffs has now been hit — see gap 135, where crossing 32 767 characters made 0.3.0 and 0.3.1 unplayable. The caps that broke are raised, but two of the paths above still have a ceiling nobody chose: the horse record and the transfer deed both cross the wire as NBT, and an NBT string is capped at 65 535 bytes by Java’s modified-UTF-8 writer, which no code in this repo declares and no test watches. A full code is 35 304 characters today, so that one is a little over half spent. The bandwidth is the other half of it and is not a cliff but a cost: 35 KB of text per horse, per tracking client, every time one comes into view.EpigenomeSizeTestguards the one hard limit (theSpawnCustomHorsePayloadcap, raised to 65536) with a 50% margin, but nothing measures the cumulative cost: a world with a few thousand recorded horses now carries tens of megabytes of epigenome text in its save. Nobody has looked at what that does to save size or chunk-load time, and the honest fix if it matters is a compact binary form for storage, keeping the readable text only for the code box and the designer. - Sixteen disorders, none of them play-tested, and two of them behave in a way that looks like a bug. Opened 2026-09-07. The nine new health loci are unit-tested for the things unit tests can reach — Mendelian rates, the founder invariants, the config switch, that two copies of a dominant cost more than one — and not one has been seen in a game. The specific thing to brace for is that a wild-caught horse can now be born unwell: a dominant has no silent carrier, so an allele excluded from founders could never appear at all, and roughly one wild horse in thirty now has PSSM1. That is correct and it will read as a regression. The invariant that actually matters is narrower and still holds: no founder is ever born dying.
wiki/verification.html§0-AF. - Three disorders describe an onset the mod cannot express, and one describes a coat it will not draw. Opened 2026-09-07. The reference distinguishes episodic (HYPP, PSSM1), chronic (HERDA) and progressive (CA) conditions; all four ship as flat costs, because
Conditionhas a severity and no magnitude or onset kind (roadmap.html#decisions, still undecided). CA is the one that loses most — a real CA foal looks normal and worsens over months, and horse aging is deliberately out of scope, so there is no clock for an onset to run on. Separately, grey’s trick is the obvious workaround nobody has tried: read dosage for a window and let the horse’s own epigenetics pick a fixed point in it, which turns “progressive” into “born somewhere on a spectrum” without needing an age at all. And lavender foal syndrome has a coat — the only lethal in the mod whose real presentation includes a colour — which is not drawn, for the same reason ST14’s is not: phase 1 can only remove pigment, so a diluted mane reads as a white one. It is also academic while the foal dies in seconds. - The custom spawn egg was reported broken in a real build, and the report was never reproduced. Opened 2026-09-07. A third party given the jar said only that the “custom egg spawner also does not seem to be working” — no symptom, no log, no game mode. Reading the whole path found no environment gate anywhere in it: no
isProduction()check, the item registered and in a creative tab, its model and texture present, the payload registered unconditionally. So this session fixed the three things that could plausibly produce that sentence and could be fixed blind — the egg now opens its editor on all four right-click gestures rather than two (aiming at a horse used to fall through to vanilla and mount it), the Spawn button says creative only on its face instead of failing silently after the click, and the two silent server-sidereturns now write a chat line and a log line each. None of that is a diagnosis. The likeliest explanation is still the dullest one, that the tester was in survival; the second likeliest is that they aimed at a horse. What would close this is one line from whoever hit it: which of the three it was, or a log with a[Custom Horse]line in it. Until then the honest status is that a bug was reported, three candidate causes were removed, and nobody knows whether it was any of them. - The horse information screen and the breeding preview are both entirely unplayed, and between them they replaced two working surfaces. Opened 2026-09-07. The “i” button, its six tabs and the browser's Breeding preview tab have never been opened in a game. Two things about that are worse than the usual “not looked at”. The grey side panel and
GeneInspectScreenare deleted, not hidden, so if the new screen fails to open there is no longer any way to read a horse's record in game. And the information screen returns to the horse inventory screen by callingsetScreen(parent)after Minecraft has already runAbstractContainerScreen.removed()on it, which callsmenu.removed(player); the container itself is not closed (that isonClose, which is never called) so the menu should still be live, but this is the standard trick rather than a checked one, and it is exactly the kind of thing that half-works — slots that no longer take a click, or an inventory that quietly desyncs. Beyond that: whether the Health tab's per-gene contributions read as an explanation or as a column of small numbers, whether the Punnett square's grouping puts the right genes together, and whether the roster packet is a sensible size for someone with two hundred horses. Updated 2026-09-07: one thing on this list was found by reading rather than playing — both screens painted their panels after the widget pass, so the barn-name box sat under a 94%-opaque fill and the Crafting tab's own button did too. That is fixed, and it is the reason to treat “built against the retained-mode API, never opened” as a real risk rather than a formality: the bug was visible in the source the whole time and nobody had cause to look.wiki/verification.html§0-AG, §0-AI. - A miscarriage's description is a hand-kept table, and it fails quietly. Opened 2026-09-07.
MiscarriageSignsmaps aConditionid to one sentence describing what the loss looked like, and it has two entries because there are two embryonic lethals. Everything else in the disorder layer is derived from the gene; this cannot be, because a description has to be written. The failure mode is the mild one by design — an unlisted condition gets a generic line, so a new lethal gene works the day it is added — but it is silent, and what it fails at is the one property the feature exists for: two lethals that read identically cannot be told apart by the player they are meant to teach. The log line says[no sign written for this condition], which is the only tripwire and only fires when a miscarriage actually happens. A test asserting that everyLETHAL_AT_CONCEPTIONcondition reachable fromGenes.codeOrder()has an entry would close it properly, and is about ten lines. - The LYCAN shift destroys a horse entity and builds a new one, twice a day, and nobody has watched it happen once. Opened 2026-09-07. This is the most invasive thing in the mod by a distance: at dusk
LycanthropyHandlerwrites the horse's wholesaveWithoutIdtag into an attachment on a freshly created animal,discard()s the horse, and adds the animal; at dawn it does the reverse. Everything about that is written against 26.1.2 sources and none of it has run. The specific things that could be wrong and would not announce themselves: whether the NeoForge attachment block really round-trips throughsaveWithoutId/loadfor every one of the mod's attachments (the record, the ancestry link, the bond, the cooldowns, the cowboy's brand); whether re-using the horse'sUUIDon the restored entity is accepted byaddFreshEntityin the same tick the old one was discarded, or whether it needs a tick between; whether a horse that was leashed, in a boat, or wearing armour comes back holding all of it; and what a second mod'sEntityJoinLevelEventhandler does with a wolf that arrives already carrying an attachment. The failure that matters is silent data loss — a horse that comes back at dawn with a fresh record and no pedigree looks fine and is not.wiki/verification.html§0-AH. - A shifted lycanthrope is a real animal, which is the point and is also an unexamined hole. Opened 2026-09-07. The requirement was that it be interactable in all the ways its animal is, so it is one — and that means a were-cow bred to a real cow produces a real calf, a were-sheep can be sheared for real wool, a were-wandering-trader can be traded with, and a were-cat can be tamed by somebody who has no idea it is a horse. None of that has been thought through as economy. The two worth naming: a shifted animal is a breeding partner that costs a player nothing and never runs out of nights, which is at least a novel path to a herd of cows; and a shifted animal can be killed, which kills the horse inside it — so a pedigreed mare can be lost to a creeper while shaped like a chicken, with no message and no body. The second is deliberate (the attachment is not
copyOnDeathon purpose) but it has never been weighed against how much a horse in this mod is worth. Neither is a bug yet; both are decisions nobody has made. - A particle homozygote carries a second look nobody can ever see. Opened 2026-09-07. Now that the locus expresses only on two variant copies,
Rflm/Rflmis the ordinary way to have flames — and its two copies carry two independent epigenetic draws while only slot 0 emits. So the horse's other flame colour is real, is inherited by half its foals, and is invisible on the horse carrying it. That is consistent with every other homozygous gene in the mod (a healer's hidden stripe opacity behaves the same way), and it may even be a feature — a foal's flames changing colour is a surprise with a cause. But it was not chosen, it fell out of the recessive rule, and the alternative was available: emit per expressing copy, so a homozygote trails two flames in two colours from two places. That was rejected because it would makeDst/Dstvisibly denser thanDst/Bflmwhile the two share an expression id, which is a worse inconsistency than this one. Worth revisiting once somebody has actually watched a paddock of them. - My horses sorts and filters on the client, over a capped roster, and the roadmap said not to. Opened 2026-09-07. §18 is explicit: the expensive part is a per-player owned-horse index, sort and filter run server-side over it, and the result paginates — “do not sync every horse's genome to the client to filter it there”. That is exactly what this does.
HorseRosterwalks the whole ancestry table on request, cuts it atHorseRosterPayload.MAX_ENTRIES, sends each entry's full genetic code, and the client filters what it got. It was chosen with eyes open — the breeding preview was already paying that cost for the same rows, the cap is the whole of the difference, andHorseQueryis game-free so the same predicate moves server-side unchanged the day the index exists. What is actually wrong today: a player past the cap silently sees only their most recent generations (the footer says so, in small text, which is not the same as it being obvious); a filter cannot find a horse the cap dropped, so “no horse carriesSB1” can be a lie; and the roster is a request/response with no invalidation, so a horse tamed while the screen is open is missing until Refresh. None of that is measured — nobody has had two hundred horses. - A horse can be held still by a screen, and the only thing stopping that being permanent is a three-second lease. Opened 2026-09-07.
HorseInspectHoldfreezes a horse while its information screen is open, and the freeze is real AI suppression:InspectHoldGoaltakesMOVE,JUMPandLOOKat priority 0, so panic, stroll, the bond follow and the herd are all not running. The lease is what makes that safe — the client re-asserts once a second, the hold lapses 60 ticks after the last word — and the lease is the only thing that makes it safe. Untested: whether a client that stops sending (a lag spike over a second, a dimension change, a paused single-player world where the client still ticks but the server does not) drops the hold and re-takes it visibly, i.e. whether the horse twitches; and what a second player inspecting the same horse does when the first closes their screen. There is also a design question nobody has answered: a held horse still takes damage and can still be pushed off a ledge while unable to path away from it. That is deliberate — the goal is “does not wander off”, not “is invulnerable” — but it has never been weighed against a player leaving the screen open next to a creeper. - Two screens draw a little horse, and they are two pieces of code. Worse since 2026-09-11: the family tree gained Descendants and Overview modes, so the copy in
FamilyTreeScreen.drawHorseModelnow draws many more boxes than it did — and gap 79’s unmeasured per-descendant record fetch is on the same path. Opened 2026-09-07.FamilyTreeScreen.drawHorseModelandclient/HorsePortraitdo the same job - build a render state from a throwaway client-onlyHorse, push aCoatDatainto it, swing it toward the cursor, fall back to a flat coat swatch - with the same magic numbers for the model scale and the look-at angles, copied. They diverge in one deliberate way (the tree keeps aHorseper record, which is right for seven boxes and would be two hundred entities in a table; the portrait keeps one adult and one foal) and in one accidental way (the tree keeps the horse's ownSCALE, the portrait throws it away so a draught horse does not break the grid). The accidental one is the bug waiting to happen: the next change to how a horse is drawn in a screen will be made in one of the two. They should be one class with a size policy argument, and the reason they are not is that the tree's version is entangled with itsuiScaleand the refactor was not worth doing blind in the same session that wrote the second one. - The Offspring tab fetches a full record per descendant, and nothing measures what that costs. Opened 2026-09-07. Each descendant arrives as a whole
HorseRecord- carrying its epigenome, which is around eight thousand characters - because that is what lets the tab draw the horse in its real coat with no second round trip. Capped atMAX_PER_GENERATIONper rung andMAX_GENERATIONSdeep, so the worst case is bounded, but the bound was picked by eye and never weighed: a prolific founder five generations down is the case, and nobody has bred one. The mitigation is the design - it only happens on a Refresh press - which also means the failure mode is a hitch on a click rather than a stutter while playing. The honest fix if it turns out to hurt is the same one gap 76 wants: send a compact row and fetch coats for the rows on screen, which is exactly what the My horses table already does. - Three third-party builds ship in the jar and nobody has read their licences. Opened 2026-09-07. The generated stables are other people's work downloaded from Planet Minecraft - Horse Stable by Seuchendoktor, Stables by Alpha_One_Seventeen, TM U Stable by Tyiarrah - and they are redistributed inside this mod, not linked to. Planet Minecraft submissions carry no uniform licence; some authors state one, most state nothing, and "nothing" is not permission. Author and source are on the wiki page and in the bake script, which is the floor and not the answer. Before this mod is distributed anywhere, each of the three needs its terms read and permission asked where they are unclear - and a build whose author says no has to come out, which is cheap now (delete two files) and expensive once somebody's world has one in it. The same check applies to every stable added later, which is why
bake-stables.pysays a stable is not finished until it is on the page. - The stable populator hangs off a chunk load, which is the wrong event and the only one there is. Opened 2026-09-07. There is no "a structure finished generating" hook, so
StablePopulatorqueues onChunkEvent.Loadof the structure's start chunk, waits 40 ticks, checks every chunk the bounding box spans is loaded, and then fills. Every number in that sentence was picked by reading, not by watching. The specific things that could be wrong and would not announce themselves: whether 40 ticks is enough for a piece that spans six chunks generated over several ticks (too short and horses go into a half-built stable, and the once-only record means they stay there); whethergetAllStartson a freshly generated chunk really carries the start, or only on reload; and what happens to a stable whose start chunk is generated but never loaded because the player skirted it. The mitigation for the last is that the queue re-queues rather than drops, so a return visit still fills it - but a stable the player walks into from the far side may be empty on arrival and populate while they are standing in it, which is the visible symptom to watch for.wiki/verification.html§0-AK. - Closed 2026-09-10: the breed designer has been opened in a browser - the rebuilt, step-by-step one, driven in headless Chrome over the DevTools protocol: every step renders, the viewport sizes itself in both layouts (and no longer forces the page wider than a phone), the herd and the example founder roll, a random pick of markings lands in the file, export names an unnamed breed, a built-in breed imports, and the console is clean. The
DEFAULTplaceholder the old page used is gone with it - an untouched step simply writes nothing. What a browser cannot say, whether the steps read clearly to a person, is on the checklist. - Breed loading is ordered by a lazy initialiser, and nothing enforces the order. Opened 2026-09-07. A breed file is mostly references to genes, so
Breedsmust not read its files until every drop-in gene is registered - and the way that is arranged is that the registry loads lazily, on first access, withModGeneSpecs.load()called beforeModBreedSpecs.load()in the mod constructor. It works, and it is fragile in a way that would not announce itself: anything that touchesBreeds.all()from a static initialiser that runs earlier would freeze the registry with the built-in genes only, and the symptom would be a log line saying a player's breed names a gene they demonstrably have. Nothing checks for that. The honest fix is a "registry sealed" flag that throws on a late registration the wayGeneswould if it had one. - Nothing has ever displayed the word "Spliced". Opened 2026-09-07. The label, its combination rules and
SpliceOutcomeare unit-tested; what is untested is the whole path from a carrot in a hand to a foal with the label on it. In particular the undecidable case is decided by policy, not by evidence: a mare fed a splice for a gene the stallion already carries produces an unmarked foal, and nobody has fed one to check that reads as fair rather than as the carrot having failed. There is also no feedback at the moment of birth — the foal is simply named differently — so a player who was not watching the info panel will not know the splice landed. :web:bakeDesignerAssetsreported up-to-date while the wasm was a version behind. Opened 2026-09-07. Observed once, and the circumstances matter: a:common:cleanhad failed part-way (Windows would not deletecommon/build/libs/common.jar, held by another process), and the nextbakeDesignerAssetsfinished in a second and rewrote nothing even thoughcommon/had changed since the previous bake.--rerun-tasksproduced a differentweb.wasm, so the up-to-date verdict was wrong. The likely cause is the half-deleted build directory leaving Gradle's task state inconsistent rather than a general fault in the task, which is why this is written as a hazard and not as a bug. It matters because the whole regenerate-what-you-invalidate discipline rests on that task being right, and this is the one artefact CLAUDE.md calls out as unable to drift silently. After a failed clean, re-bake with--rerun-tasksand check the file actually changed.- Four breeds are shade-neutral, and the conversion to files is what made it visible. Opened 2026-09-07, pre-existing.
Breed.Builder.shadeAny()'s own javadoc warns about exactly this: shade is a modifier a coat gene reads, soBreedFounderforces it wild on any breed that does not name it, and "a world where every breed horse is shade-neutral and only feral mixed ones vary would be a bug nobody would think to look for". The three agouti helpers all callshadeAny()to prevent it — but Cleveland Bay, Friesian, Przewalski and Quarter Horse were written with.fixed(AGO, ...)directly and so bypassed it. The breed files reproduce the Java faithfully, which is how it was found: a missinghorsegenetics.shadekey is obvious in a 30-line file and was invisible in an 885-line class. It is deliberately not fixed here. Adding the pool changes the shade of every horse of those four breeds in every existing world, which is a decision about how they should look and not a typo to sweep up in a conversion pass. A Friesian is jet black either way; a Quarter Horse is the one to look at. - "Find the stalls" is a block scan and a guess. Opened 2026-09-07. A structure NBT cannot say where a stall is, and requiring a modder to bake data markers into a downloaded schematic would make "drop in an NBT" untrue - so the populator calls a spot a stall when it has a solid floor, two blocks of air, something overhead, and two of four sides shut in by a fence, gate, wall, door or solid block. That will find things that are not stalls (a corner of a tack room, the gap behind a staircase) and miss stalls that are open on three sides. And it is even less observed than this entry claimed: gap 136 shows three stables in four never received horses at all, so any impression of where they end up was formed from at most a quarter of the cases — and possibly none. Nobody has looked at where the horses actually end up in any of the three buildings, and the two that are all-stalls have no field to fall back to, so a bad scan there means horses standing in a corridor rather than horses outside. The honest alternative if it reads badly is a per-stable list of relative positions in the JSON, which costs a modder real work and should not be reached for first.
- The tab split was decided by a rule over headings, and the predicted failure happened on 26 pages. Opened and half-closed 2026-09-07. 442 sections across 69 gene pages were routed by pattern - headings beginning "Why" and anything naming the reference to Science, look-alikes and breeds to Gameplay, everything else to Coding. The default was the safe direction and no prose was lost, which was checked word-for-word against
HEAD. The predicted failure was "a section a player wanted, sitting on Coding", and measuring prose per gameplay tab found it immediately: 26 pages had a gameplay tab of exactly seven words - two headings and the widgets - because they carry neither a look-alike table nor a breed list, and those were the only two things the rule could move. They included some of the most-looked-at genes in the mod: agouti, tobiano, roan, MATP, shade, sooty, leopard, natural zebra. Each has since been given a short plain-language "What it does" written from what that page already asserts; the thinnest gameplay tab is now 44 words and the median 95. What is still unreviewed is the other direction - whether anything landed on Gameplay that should not have, and whether the 307 sections that defaulted to Coding are all correctly there. Nobody has read those, and a rule this blunt over prose this bespoke will be wrong somewhere else too. - Closed 2026-09-09: all 25 pages were written, and the check now runs with an empty exempt list. Opened 2026-09-07; recount and gate added 2026-09-09; closed the same day. A larger figure was briefly feared and is wrong: 157 of 203 gene pages had no science tab, but 131 of those are magical genes, which have no real-world counterpart, and having no science tab there is correct rather than a gap. Only the natural pages counted, and there were 25. They were deliberately not stubbed — a tab reading “not written yet” is worse than an honest absence, and filling one means writing veterinary and population-genetics material rather than generating it. The owner supplied source files for all 25 and all 25 are written: the locus and the variant, the mechanism, the real inheritance table, breed-by-breed frequency where anyone has measured it, and an explicit split between established, inferred and unsupported. Each also states where the mod and the literature disagree and which way — see gap 134 for the three that turned out to be substantive. The check is the part that stays.
wiki/tools/check-gene-tabs.mjsnow holds an empty allowlist, which turns it from “stop the list growing” into a flat rule: every natural gene page carries a science tab, and a new one without one fails immediately. An empty set is the only state in which nothing can quietly rot in it. What the closure created is new work — 17 of the 23 science tabs that predate this pass now read as stubs beside it, and 20 of the 48 natural tabs still name no breed. That is on the roadmap, not here, because it is unbuilt work rather than a defect. - The search corpus is 1.8 MB, checked in, and grows with every word written. Opened 2026-09-07.
wiki/search-index.jsis the text of all 109 pages baked into a script — a script rather than JSON because the wiki is read fromfile://as often as from a server, and there is no server to query at read time. It is loaded lazily on the first keystroke, and GitHub Pages gzips it, so the practical cost is probably fine; nobody has measured it. What is not fine is that it is a generated artefact that fails silently when stale — search quietly stops finding new pages, with no error anywhere. It is in CLAUDE.md’s regenerate table, which is the only thing standing between it and drift. If it becomes a problem the fix is to index headings and ledes rather than full text, at the cost of the thing that made it worth building. - The item pages were audited against the recipe JSON, and the audit found three things. Opened and largely closed 2026-09-07. Eleven
item-*.htmlpages were split out ofitems.htmlwith their gameplay halves rewritten from the old prose rather than from the code, so every recipe on them was second-hand. All 29 recipe files have now been read against them. Every recipe, range and reagent the pages state is correct - the hair chain and its four reversals, the three whistles, the four tickets, the shaped transfer paper, both jars, and all ten carrots including the five themed reagents. What was wrong was absence rather than error: the stall sign’s own recipe (oak sign + horse hair) was written down nowhere, on the old page or the new one, so nothing told a player how to get one; the Cowboy Hitch is a registered item that appeared on no roster at all; and the two workstations collide (gap 92). The first two are fixed. What is still unchecked is the behaviour prose - whistle ranges, the shear cooldown, the stall flood-fill cap - which came from the old page and not from the handlers. - Closed 2026-09-09: the two workstations have different recipes, and a check now refuses a third collision. Opened 2026-09-07.
cowboy_hitch.jsonandhorsemans_table.jsonwerecrafting_shapelessover exactly the same seven items, so putting them in a grid could only ever produce one of the two blocks and the other was unobtainable in survival - which is not a small thing, because those blocks are how both merchant NPCs come to exist. Villagers stated the shared recipe as though it were fine, which is how it survived being noticed for two days after being written down. They now differ along the line the page itself draws between them - “a hitch you tie a horse to and a table you work leather at”: the hitch is rope-heavy (2 cloth + 3 rope + 2 fence) and the table takes the leather (3 cloth + 2 leather + 2 fence). The fix that matters is the check, not the recipe:neoforge-26.1.2/tools/check-recipes.mjsnormalises every shapeless recipe to a sorted ingredient list and every shaped one to pattern-plus-key, and refuses two that occupy the same slot. It runs in milliseconds and would have caught this the day it was written. It also refuses a vanilla recipe with no result, while exempting the mod’s own custom serialisers, whose result is computed in Java - a false positive there would have been worse than no check. - Three of the landing page’s four headline numbers were stale, two of them by orders of magnitude. Opened and corrected 2026-09-07. The hero on
index.htmlclaimed 48 genes (really 69), 4.3×1031 genotypes (really 6.7×1048) and 11,098,128,386 visually distinct coats (really 3,121,348,608,000,004, six orders of magnitude out). Only the breed count was right. They were checked against the built jar -Genes.codeOrder().size(),GenotypeCatalog.totalGenotypes(),GenotypeCatalog.size()- and corrected, with those accessor names left in a comment beside them. This is gap 13 happening again on the most-read page in the repo, and it is the exact failure CLAUDE.md calls a repeat offender: a derived number written into prose is stale the next time the code moves, and nothing anywhere fails when it is. The numbers are the first thing a visitor reads and the claim the whole mod rests on, so being six orders of magnitude wrong about the size of the coat space is not a cosmetic error. Closed 2026-09-08: they are generated.:common:bakeGeneWikiPageswas already rewritingindex.htmlfor the gene cards, so the stats block went in behind the same markers - four accessors rather than four numbers somebody typed. The correction it made on the way in was itself large: 69 genes had become 155, and the distinct-coat count now prints as a floor because it passedLong.MAX_VALUE. The lesson stands even though the instance is closed, and it is gap 13 again: a derived number in prose is stale the next time the code moves, and nothing fails when it is.