🐛 Preserve main overrides across repeated component updates

F114: updating a nested copy's main cleared the groups transferred by a
previous update whenever the source still carried remote-synced. A later
edit of the inner component could then overwrite those authored values.
Union the main's and copy's groups on every update.

The focused nested regression failed before the fix and now passes.
Common lint, JVM/JS suites, golden masters and format checks pass.
Seeded frontend replay passes 2 tests and 253 assertions.

Benchmark lower-quantile ratios against Phase 1e:
main 10/100/1000 copies: 0.61/0.63/0.67; three levels: 0.61;
chain: 0.56; library: 0.62; many shapes: 0.63; switch: 0.76.
No regression flag.

AI-assisted-by: GPT-6
This commit is contained in:
alonso.torres 2026-10-22 20:28:50 +02:00
parent 4302f3e696
commit 17ec857515
4 changed files with 43 additions and 3 deletions

View File

@ -25,7 +25,7 @@ Intended semantics of copies, `:touched` and component sync. This is the spec: w
- Sync main→copy: every untouched group gets the near main's value (geometry: expected geometry); touched groups keep their values; never sets or clears `:touched`. Main child added → added clean to copies. Main child deleted → copy child removed, overrides or not. Main reorder → copies follow. A swapped sub-head pairs with the main child it replaces (its `:shape-ref`) and takes only that child's placement (position, rotation, flips, constraints, `layout-item-*`); everything else syncs from its own component's main. A main child deleted → its swapped copies go with it. A swap made in the main reaches copies without `:swap-group` and spares those with it.
- Sync cascade is eager: one sync runs to a fixpoint in memory (copies of A, including the A copy in B's main → copies of B → …) and commits once in the triggering op's undo group. The file is never left partly synced. Syncing only the components reported as changed equals a full-file sync. Code: `cll/generate-sync-file-cascade` (first pass as `generate-sync-file-changes`, then `generate-sync-components-cascade` over `cll/changed-components` of each pass); the frontend `sync-file` commits it to the synced file, and a library file's sync then syncs the current file's copies of the library components its cascade changed.
- Reset: the selected head's subtree becomes equal to its near main (expected geometry); all its touched groups cleared, `:swap-group` included: a swapped head turns back into the near main's component in place, keeping its id. Nothing outside that subtree changes.
- Update main (from a top copy head): the copy's differing values go to the main; the copy's touched cleared; other copies then sync and keep their own overrides. A swapped child replaces the main child it stands for, and neither keeps `:swap-group`.
- Update main (from a top copy head): the copy's differing values go to the main; the copy's touched cleared; other copies then sync and keep their own overrides. A swapped child replaces the main child it stands for, and neither keeps `:swap-group`. The main's authored groups are unioned with the copy's even on a repeated update; `:remote-synced` records an earlier update and must never clear those groups (F114).
- Detach: the subtree loses component links, `:touched`, `:remote-synced`. First-level nested heads stay copies, move one nesting level up and inherit the near main's touched groups, so values their old near main overrode become overrides. A swapped head left with no enclosing copy becomes a plain copy of its own component (`:shape-ref` = its main root, no `:swap-group`).
- Instantiate / nest a copy: the new subtree has no touched groups.
- Duplicate a copy: the new subtree keeps the source's values and touched; if its nesting level changes, touched is inherited as in detach. Paste and move out of a copy follow the same rule.

View File

@ -471,9 +471,10 @@
(update-attrs shape-main shape-inst root-main root-inst main-container false)
;; the copy keeps its swap, and the main takes the copy's overrides
(set-touched shape-inst container (when (ctk/swapped? shape-inst) #{:swap-group}))
;; A previous update is not a reset of the main's authored
;; overrides. Repeating it must retain the groups it pushed.
(set-touched shape-main main-container
(when-not (:remote-synced shape-inst)
(set/union (:touched shape-main) (:touched shape-inst)))))
(set/union (:touched shape-main) (:touched shape-inst))))
(ctsl/any-layout? shape-main)
(update-layout-children main-container shape-main shape-inst
#(ctf/get-shape-in-copy container % shape-inst) false)

View File

@ -0,0 +1,38 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns common-tests.logic.comp-remote-synced-test
(:require
[app.common.test-helpers.composable.comp.nodes :as n]
[app.common.test-helpers.composable.comp.runner :as r]
[app.common.test-helpers.composable.comp.setups :as setup]
[app.common.test-helpers.composable.core :as tm]
[app.common.test-helpers.ids-map :as thi]
[clojure.test :as t]))
(t/use-fixtures :each thi/test-fixture)
(t/deftest updating-main-keeps-overrides-authored-since-the-previous-update
(let [middle-copy (fn [s]
(:parent-id (tm/shape-by-id s ((n/nested-head-of "a" 1) s))))
nested-main-rect (fn [s] (n/level-rect s "a" 0))
situation
(r/run-variant
{:setup setup/empty-situation
:operation
(tm/in-sequence
[(n/create-component "a" "#abcdef" :size 100)
(n/make-nested-component "a" 2)
(n/change-property middle-copy :fills "#ff0000")
(n/update-main middle-copy)
;; Push a nested override, then update again for another edit.
(n/change-property (n/main-rect-of "a") :name "Authored name")
(n/update-main middle-copy)
(n/change-property middle-copy :opacity 0.4)
(n/update-main middle-copy)])})
main (tm/shape-by-id situation (nested-main-rect situation))]
(t/is (= "Authored name" (:name main)))
(t/is (contains? (:touched main) :name-group))))

View File

@ -66,6 +66,7 @@
[common-tests.logic.comp-flex-interactions-test]
[common-tests.logic.comp-main-edit-breaks-copy-slots-test]
[common-tests.logic.comp-move-swapped-copies-test]
[common-tests.logic.comp-remote-synced-test]
[common-tests.logic.comp-reset-test]
[common-tests.logic.comp-sync-test]
[common-tests.logic.comp-touched-test]