🐛 Stop inherited content overrides at swapped references

Walk content refs through intermediate swapped heads when collecting
inherited touched groups. Following the replaced position instead crosses
into another component and gives copies phantom geometry overrides (F165).
Position advancement keeps its separate position-ref walk.

A portable three-level regression fails both chain and touched assertions
before the fix. Swap-group, file and variant suites pass 124 tests and
588 assertions; the shrunk library seed-712 program passes as well.
Common lint and format pass.

Benchmark lower-quantile ratios against Phase 1e: main 10/100/1000 copies
0.48/0.55/0.57, three levels 0.48, chain 0.47, library 0.49, many shapes
0.46, variant switch 0.57. No regression flags.

AI-assisted-by: GPT-6
This commit is contained in:
alonso.torres 2026-10-24 09:17:12 +02:00
parent a37fac788c
commit 357e8a7ff7
3 changed files with 32 additions and 4 deletions

View File

@ -66,3 +66,9 @@ Moving/dropping a shape into a variant container through the move-to-frame path
New main clones must use the same empty-state representation as
`process-operation :set-touched`; otherwise a later move+undo changes
`:touched #{}` to a missing field and cannot restore the exact state (F156).
Content ref chains (get-ref-chain-until-target-ref, including touched walks)
take find-content-ref-shape at every link. A swapped intermediate head
follows its own main; collecting its replaced position's content overrides
creates phantom groups in copies of that swap (F165). Position advancement
still uses advance-shape-ref and find-ref-shape.

View File

@ -1257,16 +1257,16 @@
;; Ref Chains
(defn get-ref-chain-until-target-ref
"The shape `shape` of `container` and its ref chain (each shape's near
main, found in the container of the one before), up to the shape whose id
is `target-ref`, included, or to the end of the chain."
"The shape `shape` of `container` and its content ref chain, up to the
shape whose id is `target-ref`, included, or to the end of the chain.
A swapped head follows its own component, not the replaced position."
[container libraries shape target-ref]
(loop [chain [shape]
current shape
container container]
(if (= (:id current) target-ref)
chain
(if-let [ref (find-ref-shape nil container libraries current :with-context? true)]
(if-let [ref (find-content-ref-shape nil container libraries current :with-context? true)]
(recur (conj chain ref) ref (:container (meta ref)))
chain))))

View File

@ -34,6 +34,7 @@
[app.common.test-helpers.variants :as thv]
[app.common.types.component :as ctk]
[app.common.types.container :as ctn]
[app.common.types.file :as ctf]
[app.common.types.modifiers :as ctm]
[clojure.test :as t]))
@ -877,3 +878,24 @@
swapped (-> initial (r/step (n/swap-shape :card-star :heart)) r/settle)
undone (r/step swapped (n/undo))]
(t/is (= (thn/normalize-file file) (thn/normalize-file (tm/file undone))))))
(t/deftest inherited-content-overrides-stop-at-a-swapped-reference
(let [file (-> (thf/sample-file :file)
(tho/add-two-levels-nested-component-with-copy
:c1 :main1 :main1-child :c2 :main2 :nested-head1
:c3 :main3 :nested-head2 :nested-subhead2 :copy3)
(tho/add-simple-component :other :other-main :other-child))
file (-> (tm/make-situation file)
(r/step (n/change-property :nested-head1 :opacity 0.4))
tm/file
(tho/swap-component-in-shape :nested-subhead2 :other
:propagate-fn #(propagate % :c3)))
page (thf/current-page file)
middle (ths/get-shape-by-id file (first (:shapes (ths/get-shape file :copy3))))
child (ths/get-shape-by-id file (first (:shapes middle)))
libraries {(:id file) file}
chain (ctf/get-ref-chain-until-target-ref page libraries child nil)]
(t/is (= [(:id child) (thi/id :nested-subhead2) (thi/id :other-main)]
(mapv :id chain)))
(t/is (= #{:swap-group}
(ctf/get-touched-from-ref-chain-until-target-ref page libraries child nil)))))