Keep one invalidation table, and derive the inverse a UI wants

There were two answers in the tree to "which stored bytes stop being valid when
this knob moves", and only one of them was checked.

`flow/address/block-knobs` is per block and asserted by biconditional —
`address-test` re-freezes the take once per knob and requires that the bytes
changed if and only if the key did. `domain/params`'s `:affects` was per area,
had no caller but a test asserting it returned what it was written as, and was
already wrong in both directions on the one entry where the two granularities
disagree: `:aperture-cut` claimed `#{:mouth}`, where it reaches no block, and
omitted the teeth, whose contour bytes it genuinely moves by gating
`condition/interior`'s smoothing. `:blink-cut` claimed `#{:eye}` and reaches no
block either, because a blink is `[:vis]` keys in tier 1.

So `:affects` and `affected-areas` are gone, and `address/knob-roles` is the
derived inverse of the table that is asserted — which is what a parameter panel
actually wants to ask. A knob absent from it invalidates no block, and that is
an answer rather than a gap.

Two new assertions keep the derivation from rotting at either edge: every role
in the table is reachable from some knob, and every knob a block declares is one
the registry defines. The second closes a real hole — `block-descriptor` checks
only that a knob was PASSED, and the freeze's `merge take/knobs` makes that true
of anything spelled like a keyword, so a typo in `block-knobs` would have named a
setting no slider can move.

228 CLJS tests, green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Olive Vaughn 2026-09-28 01:23:08 -04:00
parent 9cd5243983
commit 27bfe18bee
5 changed files with 85 additions and 13 deletions

View file

@ -26,9 +26,6 @@
(is (= :person-4 (get-in scene [:features :eye-8 :subject])))))
(deftest parameter-definitions-drive-the-current-take-defaults
(is (= #{:eye} (params/affected-areas :blink-cut)))
(is (= #{:head :mouth :eye :brow :teeth}
(params/affected-areas :anchor-avg)))
(is (params/valid-settings? :eye {:blink-cut 0.2}))
(is (not (params/valid-settings? :eye {:verts 8}))))

View file

@ -113,6 +113,31 @@
(str "add " id " to block-knobs for " (pr-str role))
(str "remove " id " from block-knobs for " (pr-str role)))))))))))
(deftest the-knob-inverse-is-derived-and-total
;; `invalidates` is the parameter UI's question — "what stops being valid if I
;; drag this" — and the reason it is derived rather than written down is that
;; the thing it is derived FROM is the thing the biconditional above asserts.
;; These two assertions are what keep the derivation honest in the only two ways
;; it could rot.
(testing "every role in the table is reachable from some knob"
(is (= (set (keys address/block-knobs))
(into #{} (mapcat val) address/knob-roles))))
(testing "every knob a block declares is a knob the registry defines"
;; The other direction of the same edge: a typo in `block-knobs` would name a
;; setting no slider can move, and `block-descriptor` only checks that the
;; knob was PASSED, which the freeze's `merge take/knobs` makes true of
;; anything spelled like a keyword.
(is (empty? (remove #(contains? params/definitions %)
(into #{} (mapcat val) address/block-knobs)))))
(testing "a knob that moves no bytes says so, rather than having no answer"
;; Tier 1 knobs. Each one rewrites keys or a framed value on a re-freeze and
;; no block's address changes, which is why the empty set is the right answer
;; and not a missing entry.
(doseq [knob [:blink-cut :iris-size :pupil-size]]
(is (= #{} (address/invalidates knob)) (str knob)))
(is (= #{"teeth"} (address/invalidates :aperture-cut))
"the mouth's threshold reaches the teeth block and no other")))
(deftest the-teeth-table-is-asserted-at-the-descriptor
;; Every knob the teeth block declares must reach its key. The other half — that
;; each one also moves its bytes — needs real pixels, because the crop, the otsu