Regenerate the base, keep the hand work, and say when you cannot

The loop the layer design exists for, tested for the first time: correct a
generated channel by hand, turn the generator's knob, and get the new base with
the correction still on it. `replace-feature` already carried `:over` across —
somebody anticipated this — so the feature path needed a test and not a fix.
The head path needed a fix, and there was a second fault of my own making.

`regenerate-head` leaves the head's authored channels alone once somebody has
placed it by hand, and decided that by `(= (:channels old) (:measured old))`.
Sound, until a correction exists: an `:over` layer makes those unequal, so the
FIRST correction anyone made would have stopped the head following
re-measurement for good — the exact opposite of what a layer is for. It compares
the channels without their layers now. The test fails against the old guard,
which is how I know the bug was real and not a story about one.

The other fault was mine, from the commit before this one. An `:offset` whose
shape does not match its base threw, which is right for authored data — the
validator catches it — but WRONG for the case the model actually names: turn the
mouth's `:verts` knob and the re-freeze gives it a different number of points,
so a correction that was correct when it was made stops fitting through nobody's
error, and a throw in the read path takes the stage down.

So a base that has outgrown a correction is a CONFLICT, and a conflict is the
third thing beside applied and discarded. The regeneration records `:conflict`
on the layer; the layer stays exactly where it is; `over-at` skips it, so the
picture is the base meanwhile; and `clip/conflicts` lists them for a view to
offer. A later regeneration that restores the shape clears the mark, so
resolving one can be as simple as putting the knob back.

Deliberately NOT `problems`. A document with a conflict loads, evaluates and
saves — it contains a decision nobody has made yet, and refusing to open it
would be the persistence layer taking a side in an editing question. The
distinction in the validator is one line: a shape mismatch nobody has recorded
is an authoring bug, and one a regeneration recorded is a conflict.

`channel/conflict-with` is the single rule for "can this layer apply to this
base", used by the validator, by `conflicts`, and by the regeneration that marks
them. Only `:offset` can conflict, since `:replace` states a whole value and has
nothing to agree with; a shape that cannot be read yet — an empty key map — is
not a disagreement. `value-shape` answers it without sampling anything.

414 tests, 5,696 assertions, and `:verts` in the test is a real topology change
rather than a synthetic one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-09-30 16:22:02 -04:00
parent 94c0a21de1
commit 72b57e3786
7 changed files with 297 additions and 11 deletions

View file

@ -268,6 +268,22 @@ This is what `docs/design.md` means by an override layer, and it is why
re-freezing is safe: the base is regenerated, the layers are untouched. It is
Blender's NLA blending and AE's effect stack at one property.
WHEN THE BASE OUTGROWS A CORRECTION it is a CONFLICT, which is neither a dropped
layer nor an applied one. Turning `:verts` gives the mouth a different number of
points, and an `:offset` is a row of components that has to match: so the
regeneration records `:conflict` on the layer, the layer stays in the document,
the picture is the base meanwhile, and `clip/conflicts` is the list a view
offers to resolve. Deliberately not `problems` — the document loads and saves
fine, it just contains a decision nobody has made yet. A later regeneration
that restores the shape clears the mark. Only `:offset` can conflict; `:replace`
states a whole value and has nothing to agree with.
A correction is NOT a hand placement. `regenerate-head` leaves the head's
authored channels alone once somebody has placed it by hand, and it compares the
channels WITHOUT their layers to decide: otherwise the first correction anyone
made would stop the head following re-measurement forever, which is the opposite
of what a layer is for.
Layers are what "set it by hand" means for anything measured, and the measured
channel does not need to know. A hand-set gaze is an `:over` on
`[:xform :pos]` of the iris; a hand-set mouth shape is an `:over` on

View file

@ -488,13 +488,27 @@ on the girl's lane reaches across the drawing boundary beneath it and leaves
every frame outside its support identical, and a correction owned by one
exposure travels with that exposure when a hold before it grows.
Regeneration keeps them, which is the obligation the layer design exists to
meet: `rebased` replaces a base and carries its corrections across, and a
correction the new base no longer fits is MARKED rather than dropped or
misapplied — `clip/conflicts` lists those for a view to offer, separately from
`problems`, because a conflict is a decision nobody has made yet and not a
document that will not load. Turning the mouth's `:verts` knob is a real
topology change and is what the test uses. Two latent faults turned up there and
are fixed: `regenerate-head` compared authored channels to measured ones
directly, so the first correction on the head would have stopped it following
re-measurement for good; and an incompatible offset threw in the read path,
which would have taken the stage down on exactly the case the model says to
report.
What is NOT implemented is a command that produces a layer — the doc's Constant
adjustment, Ramp and Return motion — and with it the question of how a view
offers those three over a selected range. Overwrite, the range and retiming
offers those three over a selected range, and how it offers a conflict for
resolution. Overwrite, the range and retiming
commands (blank, trim, move, slip source, retime) and the exposure-sheet view
are also not implemented; a refusal is the current behavior where the model
demands an explicit choice nobody has made yet. The suite stands at 408 tests
and 5,655 assertions, with `frontend/test/browser/sequence.mjs` driving the
demands an explicit choice nobody has made yet. The suite stands at 414 tests
and 5,696 assertions, with `frontend/test/browser/sequence.mjs` driving the
editor through create, hold, overflow, undo, reuse, make unique, duplicate,
split and insert. Rewrite tests that encode superseded behavior rather than
preserving behavior to keep them green.