🐛 Predict typography ownership across variant switches

Correct the independent switch model when only some typography styles
carry into the new variant. Mixed ownership drops the reference, while
fully carried styles retain it. Predict this from the two mains and
prior authored groups, including when the result is corrupt.

F221's regression and ownership controls failed before the correction;
they pass 2 tests / 6 assertions afterward. The original program and
shrink pass. Async frontend replay passes 19 assertions with undo/redo.
Common/frontend lint passes; changed files were formatted.

No production behavior, golden data or benchmark baseline changes.
Fresh 5,000-program gates remain required.

AI-assisted-by: gpt-6
This commit is contained in:
alonso.torres 2026-10-27 09:40:28 +01:00
parent b4271059df
commit 7e2f8b8781
3 changed files with 59 additions and 2 deletions

View File

@ -397,12 +397,24 @@
(if-let [[main _] (source after new-paths new-main-id shape)]
(let [path (get new-paths (:id main))
[old old-main groups] (get old-copies path)
old-main (or old-main (get old-by-path path))]
old-main (or old-main (get old-by-path path))
text-differences (when (= :text (:type main))
(thtx/override-units (:content old-main) (:content main)))
carried-text-units (into #{}
(keep (fn [[group unit]]
(when (and (contains? groups group)
(not (contains? text-differences unit)))
unit)))
text-groups)
mixed-typography? (some #(not= (contains? carried-text-units :typography)
(contains? carried-text-units %))
[:family :weight :size :line-height :spacing :case])]
(reduce-kv
(fn [result group _]
(if-let [unit (get text-groups group)]
(cond-> result
(and (not (contains? (thtx/override-units (:content old-main) (:content main)) unit))
(and (not (and (= unit :typography) mixed-typography?))
(not (contains? text-differences unit))
(not (same-value? main group {:content (:content old)})))
(-> (assoc-in [(:id shape) group] {:content (:content old)})
(assoc-in [(:id shape) :content-group] {})))

View File

@ -388,6 +388,45 @@
(t/is (= {} (get-in result [:result-text :content-group])))
(t/is (false? (shadow/same-value? (:result-text after) :text-content-size expected)))))))
(t/deftest switch-shadow-keeps-a-typography-only-with-its-style-ownership
(doseq [[new-size keep-reference?] [[14 true] [22 false]]]
(let [content #(thtx/content [[["Same text" {:size %}]]])
applied (thtx/content
(thtx/apply-typography [[["Same text"]]] :heading
{:family "Roboto" :weight 700 :italic true :size 22
:line-height 1.6 :spacing 2 :case "uppercase"}))
root {:type :frame :name "Board" :width 100 :height 100 :rotation 0}
text {:type :text :name "Caption" :width 70 :height 25 :rotation 0}
before {:old (assoc root :id :old :shapes [:old-text])
:old-text (assoc text :id :old-text :parent-id :old :content (content 14))
:copy (assoc root :id :copy :shape-ref :old :shapes [:copy-text])
:copy-text (assoc text :id :copy-text :shape-ref :old-text :parent-id :copy :content applied)}
after {:new (assoc root :id :new :shapes [:new-text])
:new-text (assoc text :id :new-text :parent-id :new :content (content new-size))
:copy (assoc root :id :copy :shape-ref :new :shapes [:result-text])
;; A corrupt result must not supply the model's expected values.
:result-text (assoc text :id :result-text :shape-ref :new-text :parent-id :copy
:content (content 99))}
groups #{:content-group :text-content-typography :text-content-family
:text-content-weight :text-content-size :text-content-line-height
:text-content-spacing :text-content-case}
model {:copy-text (zipmap groups (repeat {:content applied}))}
predicted (get (shadow/swap model before after :copy :old :new true) :result-text)
expected-groups (cond-> groups
(not keep-reference?) (disj :text-content-size :text-content-typography))]
(t/is (= expected-groups (set (keys predicted))))
(t/is (false? (shadow/same-value? (:result-text after) :text-content-family
(:text-content-family predicted))))
(when keep-reference?
(t/is (false? (shadow/same-value? (:result-text after) :text-content-typography
(:text-content-typography predicted))))))))
(t/deftest typography-ownership-stays-consistent-when-a-variant-changes-size
(t/is (map? (cg/run {:world [2 0 {:library true :variants true :rich-variants true
:text true :text-styles true :layout nil :layout-copies true}]
:ops [[:text-style 4 8 1] [:switch 1 0 0 0]
[:undo] [:undo] [:redo] [:redo]]}))))
(t/deftest source-library-edits-wait-for-explicit-consumer-acceptance
(let [initial (cg/initial-state [2 1 {:library true :stale-libraries true}])
situation (:situation initial)

View File

@ -337,6 +337,12 @@
:ops [[:swap 0 0 0 0] [:text 23 3 1 0] [:text-style 22 8 1]
[:undo] [:undo] [:undo] [:redo] [:redo] [:redo]]})))
(t/deftest ^:async typography-ownership-stays-consistent-when-a-variant-changes-size
(await (replay {:world [2 0 {:library true :variants true :rich-variants true
:text true :text-styles true :layout nil :layout-copies true}]
:ops [[:text-style 4 8 1] [:switch 1 0 0 0]
[:undo] [:undo] [:redo] [:redo]]})))
(t/deftest ^:async detaching-after-a-nested-switch-keeps-placement-stable-through-sync
(await (replay {:world [2 2 {:library true :variants true :rich-variants true
:text true :text-styles true :layout :flex :layout-copies true}]