From c05e45d3f766a07ba2c5b115d91c6dbaa0eefff1 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 15 Oct 2026 15:11:36 +0200 Subject: [PATCH] :books: Update the component memories for the audit and fix phase The contract memory says how the code now holds it: the eager cascade and where it lives, the automatic text name, the children of a transformed copy shape, measured text sizes on both renderers, repairs as derived writes, the tolerance of "differs" and the :set-touched operation. The debugging recipes add the override scanner; the data model and the composable test notes follow the changes. AI-assisted-by: claude-opus-5-5 --- .serena/memories/common/component-data-model.md | 2 +- .../common/component-debugging-recipes.md | 5 +++++ .../memories/common/component-sync-contract.md | 15 ++++++++------- .../frontend/composable-component-tests.md | 11 ++++++----- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/.serena/memories/common/component-data-model.md b/.serena/memories/common/component-data-model.md index dab325dec7..e7f062df35 100644 --- a/.serena/memories/common/component-data-model.md +++ b/.serena/memories/common/component-data-model.md @@ -38,7 +38,7 @@ Masters are not normally touched through `set-shape-attr`, but touched flags can ## Cloning paths -`make-component-instance` in `app.common.types.container` produces a clean component copy through `update-new-shape`, dissociating attrs such as `:touched`, `:variant-id`, and `:variant-name` on cloned shapes. +`make-component-instance` in `app.common.types.container` produces a clean component copy through `update-new-shape`, dissociating attrs such as `:touched`, `:remote-synced`, `:variant-id`, and `:variant-name` on cloned shapes. `duplicate-component` in `app.common.logic.duplicate` creates a new component master by cloning existing component shapes, setting component metadata, and applying a position delta. It does not have the same clean-copy semantics as `make-component-instance`, so inherited attrs on the source can matter. diff --git a/.serena/memories/common/component-debugging-recipes.md b/.serena/memories/common/component-debugging-recipes.md index c4d6ce98d1..9df748a952 100644 --- a/.serena/memories/common/component-debugging-recipes.md +++ b/.serena/memories/common/component-debugging-recipes.md @@ -50,6 +50,11 @@ To capture what `update-attrs-on-switch` saw during a real UI swap, patch it tem Runtime patching is faster than adding temporary source instrumentation and avoids recompilation cleanup. Restore the var or reload the frontend when finished. +## Lost propagation in a real file + +- `app.common.logic.override-scan/scan` runs the sync of every component a file uses in memory and lists the copy shapes it would change (`{:page-id :id :name :groups :attrs}`; `:structure` = children differ): values that differ from the near main in an untouched group. It skips text-measured sizes and layout-owned places, and the copies of components `:skip-component?` names. +- In the browser: `debug.scan_overrides()` prints them with `console.table` for the current file, skipping library components changed since the file last synced. + ## Test-side helpers - Use `thf/dump-file file :keys [...]` to print a shape tree with selected keys during common tests. diff --git a/.serena/memories/common/component-sync-contract.md b/.serena/memories/common/component-sync-contract.md index 125d50be1b..7541185de4 100644 --- a/.serena/memories/common/component-sync-contract.md +++ b/.serena/memories/common/component-sync-contract.md @@ -12,7 +12,7 @@ Intended semantics of copies, `:touched` and component sync. This is the spec: w ## Override rule (holds once sync has settled) -- Different means touched: a group whose value differs from the near main (geometry: from the expected geometry) must be touched. +- Different means touched: a group whose value differs from the near main (geometry: from the expected geometry) must be touched. "Differs" uses `gsh/close-attrs?`: numbers, rects, points, position data with a tolerance (1 px for geometry); `:transform`/`:transform-inverse` with `gmt/close?`; path content segment by segment. - Equal does not mean untouched: a group set back to the main's value stays touched. Only reset, update-main, detach, swap or variant switch clear groups; value equality never does. - Only authored writes on copies set groups. Mains are never touched through `set-shape-attr`. - The swap is the only structural override: copy children always follow the main's structure (add, delete, order), and a child with `:swap-group` keeps its own component in that position. @@ -20,10 +20,10 @@ Intended semantics of copies, `:touched` and component sync. This is the spec: w ## Per operation - Edit on a copy shape (authored): sets the attr's group (text content: `:content-group` + the text groups it changes), clears `:remote-synced`. Descendants moved/resized only because of it stay untouched. -- Rename: an explicit rename touches `:name-group` on root or child. Automatic renames (text name following its content) are derived. Untouched names sync from the near main; a touched root name on update-main renames the component. +- Rename: an explicit rename touches `:name-group` on root or child. Automatic renames (text name following its content) are derived: the editors write the name with `:ignore-touched #{:name}`, and sync gives an untouched copy text the name of its own letters when the main's name is automatic. Untouched names sync from the near main; a touched root name on update-main renames the component. - Edit on a main: no touched change on the main; triggers sync. - 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. +- 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`. - 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`). @@ -46,10 +46,11 @@ Intended semantics of copies, `:touched` and component sync. This is the spec: w ## Geometry ownership - Copy root: position is its own; rotation/flip belong to it only when its `:geometry-group` is touched. Resizing it touches the root only. -- Children of a resized or transformed shape: derived (expected geometry), untouched, and still follow later main edits. +- Children of a resized or transformed shape: derived (expected geometry), untouched, and still follow later main edits. Code: a transform touches only the shapes the user transformed (`clp/calculate-ignore-tree` takes their ids; the frontend keeps them in `:workspace-modifiers-ids`); sync places the main subtree of a copy shape with its own geometry like that shape before walking its children. - Layout-owned (never touched; sync writes, then reflow): x/y of non-absolute children of flex/grid frames; width/height on axes sized fill, or auto on a layout frame. -- Grow-owned (never touched; sync writes, then re-measure on SVG and WASM): width+height of auto-width text, height of auto-height text. -- `set-shape-attr`'s `ignore-geometry` does not cover `:width`/`:height`; a derived size write needs `ignore-touched` or per-dimension exclusion. +- Grow-owned (never touched; sync writes, then re-measure on SVG and WASM): width+height of auto-width text, height of auto-height text. Every editor and resize path writes the measured size as derived; on WASM `sync-file` re-measures the auto-grow texts of the current page it wrote (texts on other pages keep the synced size until measured). +- `set-shape-attr`'s `ignore-geometry` does not cover `:width`/`:height`; a derived size write needs `ignore-touched` (transforms get it from the ignore tree). +- Repairs are not overrides: `files/repair` writes with `ignore-touched`; only the text repairs set groups, explicitly. ## Component modified marking @@ -57,5 +58,5 @@ Intended semantics of copies, `:touched` and component sync. This is the spec: w ## Change ops -- `{:type :set-touched}` replaces `:touched` wholesale and only on copies. `{:type :set :attr :touched}` is a raw assoc with no copy guard; prefer `:set-touched`. +- `{:type :set-touched}` replaces `:touched` wholesale and only on copies (an empty set is dropped). The builders write only it (`pcb/update-shapes` turns a `:touched` change into it); `{:type :set :attr :touched}`, a raw assoc with no copy guard, is still applied for stored changes. - Builders that write to copies must emit the inverse `:set-touched` in undo (as `pcb/update-shapes` does). diff --git a/.serena/memories/frontend/composable-component-tests.md b/.serena/memories/frontend/composable-component-tests.md index da726ee07d..a4fb3aba7d 100644 --- a/.serena/memories/frontend/composable-component-tests.md +++ b/.serena/memories/frontend/composable-component-tests.md @@ -100,8 +100,8 @@ Two pure runners (no store, JVM + CLJS): runner and the interpreter's `sync-op?` dispatch on it. A NEW node MUST implement it. User ops record their production changes (`n/record-changes`, nil when nothing changed); the runner throws if a `:user` op records nothing or another kind records changes. After a user op the - runner syncs to a fixpoint (`ch/components-changed` + `generate-sync-file-changes`, one pass - per cascade level, like the frontend watcher) and pushes op + syncs as one undo group. + runner syncs to a fixpoint with the production cascade (`cll/generate-sync-components-cascade`, + the one the frontend `sync-file` commits at once) and pushes op + sync as one undo group. `n/undo` (`(n/undo k)` for k groups) reverts the latest group and moves it to a redo stack; `n/redo` applies it again (interpreter: `dwu/redo`); neither re-syncs. A user op that pushes a group empties the redo stack; one that changed nothing keeps it. The runner validates the file (against the @@ -195,9 +195,10 @@ Two pure runners (no store, JVM + CLJS): A set they build has no `variant-set` record: read it with `n/component-of` and `n/variant-component-ids`; `switch-variant` takes the value as a fn for it. - Tokens: `n/add-tokens` (assembly: an active set of border radius tokens), `n/apply-token`, - `n/unapply-token`, check `n/has-token?`. The interpreter sends `dwta/apply-token`, which resolves - tokens asynchronously and opens no pending work (F54), so `await-step` takes a `ready?` predicate - (`op-ready`: the token shows on the shape) that must hold before it awaits the pending work. + `n/unapply-token`, check `n/has-token?`. The interpreter sends `dwta/apply-token`, which holds its + shapes pending (a `:token` task) while the tokens resolve, so `await-step` waits for it like any + other op. `await-step`'s `ready?` argument remains for waits on something that opens no pending + work (a dialog shown to the user). - `n/paste-to-root target`: the clipboard's copy (shape-refs of nested heads advanced, the skipped ref chain's touched merged) and paste at the root; the interpreter pastes `thp/simulate-copy-shape`'s data. The node, that helper and `copy-selected` all advance through `cld/advance-copied-objects` (`app.common.logic.duplicate`). A duplicate inside a copy is