Engineering Journal

← all posts

Part of Building the 3D Club Scene

One rule for our AI-assisted QA: change the solver or change the gate, never both

Testing & QAAI & agentsProcess

A version-history view of a protected spec file: four ratified revisions in green, and one row highlighted red, UNRATIFIED, flagging a changed tolerance that hasn't been signed off.

When agents do the work and a harness grades it, the harness becomes the most attackable object in the project — not maliciously, just by ordinary drift. Our answer is a single versioned file of ratified acceptance criteria, one rule about which diffs may touch it, and a standing requirement that every tolerance carry a rationale. This is that file, the episode that produced it, and the one circularity still sitting in it that we have not resolved.


The failure mode nobody warns you about

The pipeline this post is about takes one purchased pole-dance take, solves it onto a given body offline, bakes an animation, and runs it through a harness of fourteen kinematic checks: contact exactness, capsule-versus-pole collision against per-character mesh-measured radii, swept between-frame collision, continuity caps, and an identity test. Most of the solver work is done by agents, one task at a time, each measured against frozen baselines.

That arrangement has an obvious failure mode and a subtle one.

The obvious one is a bad fix that the harness catches. That is fine; that is the harness working, and a failed round costs a git checkout plus a written diagnosis.

The subtle one is that a round can improve its score by adjusting what the score means. Nobody has to be dishonest for this to happen. A tolerance gets proposed in a task brief because the old one looks unreachable. A named window gets exempted because "the source performer's own motion does the same thing there." A check gets a --settle default so the solver's first frames don't count. Each of these is defensible in isolation, each arrives inside a diff full of legitimate solver work, and three rounds later the scorecard is green against a definition of good that nobody chose.

We ran into this twice — once with an exemption that had quietly become load-bearing, once with a tolerance that had been borrowed from an unrelated constant — and the second time we stopped and wrote a file.

The rule of record

GATES.md is a single versioned document. Every gate, tolerance, and invariant exemption in the project lives there, each with a one-line rationale and a changelog entry. Four rules govern it:

  1. A work stage may modify the solver OR the acceptance criteria — never both. This is the whole idea. A diff that changes behavior and a diff that changes the definition of correct behavior are different kinds of object and must be reviewed differently.
  2. Agent-proposed tolerances are proposals until ratified by a human. They may be written down, argued for, and measured against. They are not in force.
  3. Every diff touching harness, gate, tolerance, objective, or contact-classification code is read by the orchestrator. Ordinary solver diffs stay agent-reviewed. The scarce human attention goes where the definitions live, not where the work happens.
  4. No exceptions, ever, in any gate — only named review items, in the open.

The current table, roughly as it stands:

gate rule why
spine-line core intrusion 0 frames, absolute this is "body through pole"
point continuity < 18 m/s, no source-relative escape hatch the teleport class
planted sole / foot speed / floor 0 violations with no settle grace plants are exact or broken
limb contact three tiers: hard invariants → semantic correctness → perceptual a human flags clips even where the source is dirty
identity retargeting a take onto its own skeleton is a no-op see below
swept collision 0 between-frame crossings frame sampling has a blind spot
footage none sent for review while a ratified gate is red on the shipped config learned the hard way

One thing that table does not show, and should, in a post about ratification: when the campaign stopped, most of those rows carried the status "ratified-in-practice" rather than a signature, and three of them (the limb-contact standard, the identity gate, and the footage rule) were still sitting at NEEDS RATIFICATION. The file makes that visible on its face, which is most of what the file is for.

Two of those deserve their own paragraphs.

"Source comparison is diagnostic only"

The cheapest way for any retargeting system to dodge blame is the mocap was just like that. And it is often true! The purchased take has a foot that drags, limbs that graze, frames where the performer's own skeleton does things a kinematic checker dislikes.

So we measure the source too, and for a while the comparison was a license: if the target's motion is no worse than the source's under the same test, pass.

That rule is wrong, and the reason is that Spencer flags clips where the source is dirty. A viewer is not comparing your render against the capture. They are looking at a dancer. So the limb-contact standard is now explicitly three-tier — hard invariants first, then semantic correctness (right patch, right contact mode, right timing), then perceptual judgment — and source comparison is demoted to a triage tool: is this defect source-authored, morphology-induced, or solver-induced? That question is worth answering on every finding. It is never a permission slip.

The footage rule, and why it is in a gates file

No footage goes to the human reviewer while any ratified gate is red on the shipped configuration. Diagnostic and probe footage is always allowed, clearly labeled as such.

This looks like process theatre until you have watched a reviewer spend twenty minutes forming an aesthetic opinion about a clip that a check had already failed for a reason nobody mentioned. Human review is the most expensive instrument in the project. Pointing it at output you already know is broken is how you spend it on nothing — and worse, how you teach the reviewer that red gates are negotiable.

The obvious failure mode is a gate that is red for a bad reason, and we have one of those on the record. An absolute-zero core-intrusion gate turned out to be unachievable by the ground truth itself: the source performer's own take scores 432 core-intrusion frames under the same rigid-capsule representation, against our solved output's 248. Two agent tasks burned themselves chasing that zero before anyone measured the source. A wrongly red gate does not only block work, it also holds off the reviewer whose eye has caught every bad instrument in this project so far. What keeps that from being fatal is that probe and diagnostic footage is never blocked, and in practice most rounds shipped their footage under exactly that label. The rule is a queueing discipline for the reviewer's attention, not a wall around it.

The clause has been reinterpreted twice, which is exactly why it is now written in a file with a changelog rather than remembered.

The episode: a no-op that isn't a no-op

The gate that produced the rules is the identity gate, and it is the one we recommend to anyone building a transformation pipeline of any kind.

The test: retarget the take onto its own skeleton. No cross-body math applies, because the target is the source. The output should be the input. It is the fixed-point property, and it is free to check.

Ours drifted 520.19 mm max, 160.24 mm mean per-joint world-track error, peaking on a leg swing at 6.29 s.

The first response was to propose a tolerance: 50 mm max, 20 mm mean. It sounded reasonable. It had also been borrowed from an unrelated warning threshold elsewhere in the codebase, by the admission of the note that proposed it, and nobody had checked whether it was reachable. That is the moment the file got written: a tolerance without a rationale is a number that will eventually excuse something.

Then we spent a probe session explaining the drift instead of tolerating it. The result is one of the more useful things this project has measured:

A companion test made the same point from another angle. Feed the pipeline's own baked output back through it — with the script held byte-identical, so no re-extraction noise can contribute — and it still moves: 397.04 mm max, 95.88 mm mean. At the peak, ten joints (hips, four spine bones, neck, head, both arms, both upper legs) all read exactly 397.04 mm simultaneously, which is the signature of a single root-level discrete decision going the other way, not of per-joint numerical noise.

Anchoring the root solve to the source's own continuous trajectory is the fix that follows from that diagnosis, and it shipped default-on. It took the identity gate from 160.24 to 72.22 mm mean. The max barely moved, 519 mm, and is now dominated by something else entirely, a plant-anchor freeze on the feet. Better than a factor of two on the mean out of one scale correction, and the gate still fails.

The useful part is what all of that does to the tolerance question. The right identity tolerance is not a number from theory and not a number that makes today's build pass. The proposal on the table is max ≤ 100 mm / mean ≤ 40 mm, and it is worth being exact about where that came from, because it is not read off the self-consistency floor. It sits well under that floor. The 397 mm / 96 mm figure only bounds what is reachable without also fixing the root path's own non-idempotence, so 100/40 is a target set against measured quantities rather than derived from them, and it is re-verifiable in one command. It was also written down before the anchor fix landed and has not been re-derived since, which is exactly the debt GATES.md exists to keep visible: at 72.22 mm mean the proposed 40 is now a plausible target rather than an aspiration, and nobody has done the work to say so on the record.

The gate carried one more standing consequence: no coupled-solve stage ships while the drift is unexplained. That one did its job and then discharged. The probes it forced explained the drift, and the coupled-solve stage was dropped on their evidence instead of being commissioned. A gate that blocks work is doing its job; a gate that has been widened to unblock work is furniture.

The exemption sweep

The other half of gate hygiene is periodic amnesty removal.

Early rounds had accumulated named exception windows — most notoriously a "kick-graze" set, added for a genuine reason: the fastest kick sweeps a chord through the pole in two frames, and continuity and hard non-penetration cannot both be absolute when the ask moves that fast. We chose continuity and let the flesh margin absorb a graze.

Then one of those inherited windows turned out to be shielding a foot-through-pole defect that a human could see.

So every baseline table in the project was regenerated with all named gate exceptions deleted, and the exception-free tables became the frozen reference for all three bodies. The verdict logic changed with it: no more silent exclusions inside a check. Where an exclusion is genuinely warranted it is now expressed structurally — as a contact type with a documented permission — and the excluded count is printed in the check's own output rather than subtracted from it.

That last detail is small and it matters: report exclusions, never absorb them. A check that says "0 violations (12 excluded, see windows)" is honest. A check that says "0" is a lie with a good conscience.

The circularity we have not fixed

In the spirit of the file, here is the load-bearing problem still sitting in it.

Some grips on the hardest body are geometrically impossible — her arms are 50 mm shorter than the performer's, and the take asks for two grips 25 cm apart at the same instant. The harness therefore excuses wrist error inside tagged infeasible windows, on the reasonable grounds that no solver quality invents reach.

Those windows are computed by the solver. Specifically, per hand per frame: shortfall = |shoulder(solved root) → pin| − 0.96 × armLength, evaluated at the root position the solver itself chose. And a later stage gates its yield levers to the previous round's window file, iterating until the window set stabilizes — two rounds on the hardest body, one on the others.

Read that again in gate terms: the system being graded supplies the list of frames where its own failures do not count, using a definition that depends on its own output. A worse root strategy produces more infeasibility, which produces more amnesty.

We have not resolved it. What we have done is bound it, and we would rather publish the bound than the reassurance:

None of that makes the definition non-circular. The honest next step is to derive infeasibility from body measurements and the script's own bands rather than from a solved root — a quantity the solver cannot influence — and until that exists, this belongs in GATES.md as a named, ratified circularity with a rationale, not as an implementation detail nobody has to look at.

The part that generalizes

Very little of this is about dancing, and none of it is about three.js.

If you have agents producing work that a harness grades, the harness is now part of your product, and the highest-leverage document in the repository is the one that says what "passing" means. Three things we would do again on day one rather than day thirty:

And keep one gate that nothing is allowed to widen. Ours is the fixed-point test, because it needs no ground truth, no reference footage, and no judgment — just the observation that transforming something into itself should give you back what you started with. It has been the single most informative check in the project, and it has never once passed.