24560 Commits

Author SHA1 Message Date
alonso.torres
d1e0f80265 🐛 Keep update-main and deferred text sync in one undo group
Use the same explicit group for inverse sync and every copy sync. Text
reflow can finish after the update transaction closes; one undo must still
restore the main, its copies and the source overrides together (F152).

The five-operation text regression fails before this change. All 12
frontend generated replays pass: 865 assertions. Common and frontend lint
and format pass. Shared benchmark ratios: mains 0.47/0.53/0.55; levels
0.51; chain 0.64; library 0.64; many shapes 0.96; switch 0.68, no flags.

AI-assisted-by: GPT-6
2026-10-28 15:53:09 +01:00
alonso.torres
00f5d621b0 🐛 Remove invalid swaps when a main detaches a nested head
A swapped copy must follow structural changes when its position ceases
to be a component head. Walk that shape normally during sync, remove
its component metadata and swap group, and replace its former component
children with the detached main shape's children.

The regression fails three assertions before the fix. The swap suite
passes 44 tests and 177 assertions, including exact undo. The shrunk
generated detach scenario also passes with its structural model update.

Benchmark vs Phase 1e: main 10/100/1000 0.50/0.57/0.64; levels 0.52;
chain 0.52; library 0.60; many shapes 0.55; switch 0.65. No flags.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
44485aee91 🐛 Clear source overrides after pushing a swapped subtree
When update-main clones a swapped subtree into the main, the new main
owns its authored groups. Clear those groups from every source shape
when relinking it to the clone, instead of clearing only swap-group.

The regression fails on retained source flags before the fix. All five
assertions now pass, including retained geometry and opacity values,
new-main groups and exact undo. The swap suite passes 43 tests and 173
assertions.

Benchmark vs Phase 1e: main 10/100/1000 0.57/0.65/0.73; levels 0.59;
chain 0.59; library 0.61; many shapes 0.65; switch 0.68. No flags.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
f01b260f74 🐛 Keep inherited root orientation through a variant switch
Use the root's effective touched groups after the reference-chain merge
when preserving its orientation. A near main may author the rotation
while the selected copy remains untouched. Checking only the selected
shape drops that inherited override during the switch.

The portable regression fails before the fix and passes four assertions
afterwards. Focused swap and generated checks pass 42 tests and 165
assertions.

Benchmark vs Phase 1e: main 10/100/1000 0.56/0.58/0.67; levels 0.61;
chain 0.57; library 0.53; many shapes 0.69; switch 0.70. No flags.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
9f10e5e7c3 🐛 Keep authored root orientation as a switch override
A switch places the new root in the old orientation before carrying
attributes. When rotation is already equal, the carry step emits no
write and drops its geometry group. Preserve that group for retained
authored root orientation so later sync keeps the visible result.

Extend the rotation regression to check touched and later sync: two
assertions fail before the fix. Focused checks pass 43 tests and 166
assertions, including the two previously shrunk generated regressions.

Benchmark vs Phase 1e: main 10/100/1000 0.64/0.65/0.97; levels 0.49;
chain 0.59; library 0.58; many shapes 0.89; switch 0.60. No flags.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
fbb56e2916 🐛 Keep a plain swap's inherited orientation through sync
A plain swap also retains orientation inherited from its old component.
Record that retained orientation as geometry when it differs from the
new component's expected placement. Parent-derived orientation stays
untouched and size-only overrides still clear.

The regression fails before the fix. The swap suite passes 41 tests /
162 assertions; the seeded MVP property also passes 100 scenarios.

Benchmark vs Phase 1e: main 10/100/1000 0.54/0.57/0.66; levels 0.60;
chain 0.62; library 0.67; many shapes 0.83; switch 0.77. No flags.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
407e14682f 🐛 Preserve swapped placement when its content main rotates
Prepare the content subtree in the swapped head's orientation before
syncing it. A content main rotation must not replace the orientation of
the position this head occupies in its enclosing component.

The focused regression fails two assertions before the fix. Geometry,
reset, swap and oracle suites pass 77 tests / 312 assertions. The original
generated failure also passes after the fix.

Benchmark vs Phase 1e: main 10/100/1000 0.51/0.57/0.57; levels 0.49;
chain 0.49; library 0.54; many shapes 0.72; switch 0.64. No flags.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
0859d7c1a1 🐛 Order grid children when adding a shape
Record grid children in cell order before asynchronous reflow. This
keeps changes and their undo independent of the reflow schedule.

Red child-order regression reproduced. Focused geometry, reset, swap
and runner suites pass 121 tests / 540 assertions.

Benchmark vs Phase 1e: main 10/100/1000 0.51/0.64/0.65; levels 0.50;
chain 0.57; library 0.53; many shapes 0.47; switch 0.59. No flags.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
40ff7a743c 🐛 Keep parent orientation when resetting a nested copy
Resetting a selected nested copy must compose its parent placement even
when that parent remains a component copy.

Red regression reproduced; reset suite passes 16 tests / 63 assertions.

Benchmark vs Phase 1e: main 10/100/1000 0.51/0.64/0.65; levels 0.50;
chain 0.57; library 0.53; many shapes 0.47; switch 0.59. No flags.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
835138749b 🐛 Ignore dot product roundoff when detecting shape flips
A right-angle turn can produce a tiny negative dot product. Compare it
relative to the axis lengths before treating the turn as a reflection.

Red regression reproduced at small, normal and large sizes. Focused
geometry, reset, swap and runner suites pass 121 tests / 540 assertions.

Benchmark vs Phase 1e: main 10/100/1000 0.51/0.64/0.65; levels 0.50;
chain 0.57; library 0.53; many shapes 0.47; switch 0.59. No flags.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
a90c6d1d8c 🐛 Keep authored rotation through a plain component swap
Retain geometry touched when a plain swap keeps authored orientation,
so a later sync preserves it. Size-only and content overrides still clear.

Red regression reproduced; swap suite passes 39 tests / 154 assertions.

Benchmark vs Phase 1e: main 10/100/1000 0.51/0.64/0.65; levels 0.50;
chain 0.57; library 0.53; many shapes 0.47; switch 0.59. No flags.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
59d06a1066 🔧 Allow machine precision geometry noise in composable undo checks
Keep snapshots exact and compare only shape geometry within an absolute
1e-10 tolerance. Reflow recomputed 0.01 as 0.010000000000005116 after undo.
Opacity, touched groups and other authored fields still compare exactly.
Fix the false positive F130 and document the narrow exception.

The regression failed before the fix. Focused comparison and structural
undo checks pass 2 tests with 9 assertions, including negative controls.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
90138f14c6 🐛 Compose child flip flags from the roots during geometry sync
Keep the roots' relative flip state when placing the source subtree.
Several small rotations must give the same child flags after a full sync;
recomputing one large turn could toggle both axes. Fix F139.

The direct regression failed nil/nil to true/true before the fix. Focused
geometry and oracle suites pass 20 tests, 88 assertions.
Benchmark ratios against Phase 1e: main 10/100/1000 0.57/0.64/0.60;
three levels 0.67; chain 0.56; library 0.60; many shapes 0.50;
variant switch 0.64. No regression flag.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
67550ebb5c 🐛 Record user undo changes before starting derived component sync
Hand applied transitions to the component watcher only after the workspace
records their undo entry. Keep the handoff synchronous with the captured
prior file data. This preserves transaction order when nesting triggers
several component syncs. Fix F137.

The minimal replay failed both undo and redo before the fix; its 4 checks
now pass. Existing sync scenarios pass their behavior and undo checks;
60 golden digests need review for the accumulated Phase 4 fixes.
Common benchmark ratios against Phase 1e: main 10/100/1000
0.46/0.52/0.53; levels 0.47; chain 0.51; library 0.51; many 0.49;
switch 0.61. This handoff changes no common benchmark code.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
84affc15c0 🐛 Queue moved component copies before syncing their new dependents
A copy moved from a rotated parent can need its own component's sync even
when that main did not change. Include the moved heads among the sync
inputs so the new parent's copies receive the settled geometry. Fix F135.

The minimal regression failed before the fix. Geometry suite: 6 tests,
55 assertions, green; the shrunk seed 712 case also passes.
Benchmark ratios against Phase 1e: main 10/100/1000 0.46/0.52/0.53;
three levels 0.47; chain 0.51; library 0.51; many shapes 0.49;
variant switch 0.61. No regression flag.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
3ff24dc190 🐛 Preserve remote sync state in inverse child add and delete changes
Make structural inverses ignore touched so undoing derived propagation
keeps the parent's remote-synced flag. Fix F132.

Both add and delete regressions pass exact undo/redo comparisons. Runner
suite: 43 tests, 107 assertions, all green.
Benchmark ratios against Phase 1e: main 10/100/1000 0.46/0.52/0.53;
three levels 0.47; chain 0.51; library 0.51; many shapes 0.49;
variant switch 0.61. No regression flag.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
8a034be8fd 🐛 Remap nested grid reset cells through their near-main references
Use each paired child's shape-ref directly. Resolving it through its own
component can select a different nesting level and move the child into an
earlier auto cell. Fix F134.

The regression moved column 2 to column 1 before the fix; all 3 assertions
now pass. Benchmark ratios against Phase 1e: main 10/100/1000
0.46/0.52/0.53; three levels 0.47; chain 0.51; library 0.51;
many shapes 0.49; variant switch 0.61. No regression flag.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
6831417993 🐛 Keep nested copies in a detached parent's orientation during sync
Express the source subtree in its ordinary parent's orientation before
syncing a surviving nested copy. Detaching a rotated outer instance no
longer makes a later main edit turn the nested copy back. Fix F131.

The focused regression failed on points and rotation before the fix.
Geometry/reset suites: 19 tests, 111 assertions, all green.
Benchmark ratios against Phase 1e: main 10/100/1000 0.46/0.52/0.53;
three levels 0.47; chain 0.51; library 0.51; many shapes 0.49;
variant switch 0.61. No regression flag.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
1ed6476a53 🐛 Normalize newly allocated grid cells in the same assignment pass
Assign positions after adding overflow cells so a second assignment leaves
the grid unchanged. This keeps main and copy cell metadata in agreement.
Fix F129. The red regression now passes; layout suite: 60 tests, 344 asserts.

Benchmark ratios against Phase 1e: main 10/100/1000 0.46/0.52/0.53;
three levels 0.47; chain 0.51; library 0.51; many shapes 0.49;
variant switch 0.61. No regression flag.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
4e7cca7ff4 🐛 Preserve untouched parent grid cells during component swaps
Restore the original cell metadata after replacing a child in place, and
ignore touched while assigning the new instance. Later main cell moves
can then reach the swapped copy. Fix F128.

The regression failed before the fix. Swap suite: 37 tests, 137 assertions.
Benchmark ratios against Phase 1e: main 10/100/1000 0.46/0.52/0.53;
three levels 0.47; chain 0.51; library 0.51; many shapes 0.49;
variant switch 0.61. No regression flag.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
e4e02a4753 🐛 Clear source grid cells when relocating children
Recalculate cell positions in the previous grids after moving children.
The destination update alone left cell references to shapes that moved.

Validation: reset and move suites, 21 tests / 92 assertions. The grid
move regression failed before the fix and checks exact undo.

Benchmark ratios against Phase 1e: main 10/100/1000 copies
0.54/0.56/0.60; three levels 0.52; chain 0.52; library 0.55;
many shapes 0.58; variant switch 0.61. No regression flag.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
e2b47a91fe 🐛 Resolve deleted near mains when resetting nested copies
Include cached deleted mains when resolving the position of a nested
copy. Reset previously passed nil to container lookup and threw.

Validation: reset and move suites, 21 tests / 92 assertions. The new
reset regression failed before the fix and checks undo and redo.

Benchmark ratios against Phase 1e: main 10/100/1000 copies
0.54/0.56/0.60; three levels 0.52; chain 0.52; library 0.55;
many shapes 0.58; variant switch 0.61. No regression flag.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
35b0a893e6 🚧 Save Phase 4 random harness and grid fixes in progress
Save Stage 2 structure, text, geometry and layout generation, independent
oracle checks, frontend seeded replay and plugin visible-value checks.
Include pending grid sync and deletion fixes with focused regressions.

WIP validation: component sync 21 tests / 106 assertions; move shapes
6 tests / 33 assertions. Earlier plugin checks passed 68 full cases,
11 unit tests and types. The final generated replay, golden check,
5000-case gate, full suites and live WASM check remain unfinished.
Stages 3 (swaps/variants) and 4 (library files) have not started.

F124 benchmark ratios against Phase 1e: main 10/100/1000 copies
0.48/0.52/0.54; three levels 0.53; chain 0.56; library 0.52;
many shapes 0.42; variant switch 0.64. No regression flag.
The F125 deletion change still needs its final benchmark.

Save the detailed resume checklist and logs in the local stabilization
plan. Keep that workflow document out of the commit.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
8ae59c56d7 🐛 Match component orientation with its relative transform
Apply the relative orientation matrix directly. Large rotations also
change flip flags, so applying flips before rotation can add 180 degrees.
The text regression fails before this fix at 135 and 225 degrees.

Validation: geometry suite, 4 tests / 50 assertions; existing golden
cases and worlds, 1098 assertions. No existing golden digest changed.

Benchmark lower-quantile ratios against Phase 1e:
main 10/100/1000 copies 0.51/0.59/0.58; three levels 0.53;
chain 0.61; library 0.53; many shapes 0.54; variant switch 0.70.
No regression flag.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
9083f5e232 🐛 Accept zero and negative property-test environment seeds
Require an integer seed while retaining positive counts. Seed zero is a
valid test.check replay seed and must behave like the explicit option.

Validation: PENPOT_GEN_SEED=0 schema and runner suites pass,
44 tests / 119 assertions. The regression failed before the fix.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
a8bad1c8c8 🐛 Keep missing-shape diagnostics readable with mixed labels
Sort labels by their printed value so keyword and fallback string labels
can appear in the same diagnostic. The failing regression now passes.

Validation: schema and runner suites, 44 tests / 119 assertions.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
418a060ce4 🐛 Find the user duplicate after component propagation
Duplicating a nested copy in a main also adds subtrees to that main's
copies. The frontend result lookup counted all of them and threw instead
of returning the user's duplicate. Exclude roots inside component copies.

The regression checks the lookup after the sync cascade creates three
subtrees. Focused common suites pass: 66 tests, 11263 assertions.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
d14cd01374 🐛 Cascade structural edits through deleted component mains
Add, delete and move changes inside a deleted main carry a component
container ID. The change scanner ignored that destination, so it never
synced the deleted component's surviving copies after a structural edit.
Return the container's component ID for all three change kinds.

The integration regression failed with dangling refs before the fix.
It now removes the child from both the stored main and its copies and
checks undo/redo. Focused common suites pass: 66 tests, 11263 assertions.
Common lint passes.

Sync benchmark ratios against the 1e baseline (lower quantile):
main 10/100/1000 copies 0.55/0.65/0.61; three levels 0.52; chain 0.62;
library 0.58; many shapes 0.69; variant switch 0.79. No regression flags.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
ed4767ebfd 🐛 Assign grid cells when moving shapes from the layers panel
Relocate assigned grid cells only when the caller named a target cell.
A layers-panel move left the new child unassigned in the main, while sync
assigned it in copies. Assign all unplaced children after either route.

The regression failed with an empty second cell before the fix. It now
places the moved shape there and checks exact undo restoration. Existing
move tests and common/frontend lint pass.

Sync benchmark ratios against the 1e baseline (lower quantile):
main 10/100/1000 copies 0.53/0.58/0.59; three levels 0.52; chain 0.54;
library 0.67; many shapes 0.65; variant switch 0.68. No regression flags.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
b86ca2f7fd 🐛 Keep intersections accurate for short perpendicular lines
An absolute determinant threshold classified short perpendicular lines
as parallel. Constraint propagation then displaced text in a rotated
nested frame by hundreds of pixels. Compare the determinant relative to
both line lengths, retaining the existing parallel-line fallback.

The component regression failed on points and x/y before the fix. It now
passes, as do direct intersections across sizes and positions, the
geometry tests, and existing composable scenarios.

Sync benchmark ratios against the 1e baseline (lower quantile):
main 10/100/1000 copies 0.47/0.63/0.80; three levels 0.56; chain 0.48;
library 0.50; many shapes 0.45; variant switch 0.56. No regression flags.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
2b31f88797 🐛 Initialize flex line arithmetic before placing the first child
The first layout line starts as nil. JavaScript arithmetic treated its
missing count and bounds as zero, while the JVM threw. Give those three
values zero defaults before calculating gaps and the next line bounds.

A regression failed on the JVM before the fix. Six combinations of row
and column layout with start, around and even spacing now pass. Existing
flex tests pass too; the layout oracle fixtures pass on the JVM.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
ac83604f49 🐛 Restore main orientation when resetting a component copy
Reset used the copy's geometry override to rotate the main before copying
its values, then cleared the override. The copy and its children kept the
old rotation until the next sync. Reset now uses the main's orientation.

The regression failed at 45 degrees before the fix and now passes, with
exact undo restoration. Shared scenarios and golden masters pass:
55 tests, 1866 assertions. No existing golden digest changed.

Sync benchmark ratios against the 1e baseline (lower quantile):
main 10/100/1000 copies 0.54/0.58/0.58; three levels 0.51; chain 0.62;
library 0.51; many shapes 0.47; variant switch 0.61. No regression flags.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
b736424c9d ✨ Generate shrinkable component composition scenarios
Add a shared generator, authored override shadow and independent oracle.
Resolve tuples against current trees, log skips and stable replay paths,
and check propagation, sync equivalence, values and exact undo/redo.
Keep partial sync in the preceding user's undo group.

Validation: 5,000 seeded scenarios passed; common JVM/JS, golden masters,
lint and format passed. Focused oracle and sequence checks also pass.
Production sync is unchanged by this test harness commit.

AI-assisted-by: GPT-6
2026-10-28 15:53:08 +01:00
alonso.torres
17ec857515 🐛 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
2026-10-28 15:53:07 +01:00
alonso.torres
4302f3e696 🐛 Restore copy links before overrides when undoing a detach
The Phase 4 random harness found F113: undo of detaching an overridden
copy restored its touched groups before shape-ref. The copy-only setter
therefore dropped those groups. Append the touched and remote-sync undo
operations after restoring the shape's attributes.

The focused regression failed first and now passes. Common JVM/JS and
all 1499 frontend tests pass. Common lint and targeted format checks pass.
The frontend test build used shadow-cljs directly because Cargo's cache
permissions blocked the wrapper's unrelated WASM rebuild.

Benchmark lower-quantile ratios against Phase 1e:
main 10/100/1000 copies: 0.46/0.52/0.53; three levels: 0.54;
chain: 0.47; library: 0.58; many shapes: 0.68; switch: 0.64.
No regression flag.

AI-assisted-by: GPT-6
2026-10-28 15:53:07 +01:00
alonso.torres
76e90bad36 🐛 Honor generative test size and seed options
Preserve explicit quick-check options and read PENPOT_GEN_NUM and
PENPOT_GEN_SEED when callers leave them unset. Print the final shrunk
counterexample in full so failures can be replayed.

The new regressions failed before the fix and now pass on the JVM.
Common lint and the full JVM/JS suites pass. Format checks pass for
these files; the in-progress Phase 4 harness remains outside this commit.

AI-assisted-by: GPT-6
2026-10-28 15:53:07 +01:00
alonso.torres
fe80b4d3e4 🐛 Cancel WASM thumbnails when leaving the workspace
Cancel the full thumbnail upload/render stream on page or workspace
teardown and on matching thumbnail clearing. Deferred library thumbnails
can otherwise query a main missing from the next WASM scene and abort
the renderer (F112).

Complete the upload observable after its callback so successful requests
also release their subscriptions.

Add async regressions for pending current and library frames, late upload
callbacks, clearing, and successful rendering. They fail before the fix.
Document the thumbnail lifecycle in the frontend memory.

Frontend CI passes lint, 1499 tests / 7272 assertions, and formatting.
Live WASM update-main followed by navigation no longer crashes; an
uninterrupted library request refreshes its PNG thumbnail too.

AI-assisted-by: GPT-6
2026-10-28 15:53:07 +01:00
alonso.torres
a3d75b0e45 🐛 Preserve file destinations through undo and redo
Keep a namespaced file destination on each history change. Replay changes
in order in their target files, including stacked entries and transactions.
Keep payload fields named file-id intact, such as set-tokens-source (F111).

Snapshot the current file and every library in both composable runners.
Name the file in failure signatures; normalize the server synced-at status
stamp alongside modified-at. Update golden cases H and AP for this exclusion.
The library update regression checks both files through undo and redo and
failed against the old code.

Common JVM: 1705 tests; JS: 1633; frontend: 1495; backend: 1610; exporter: 14.
All pass, with lint and formatting checks for both changed modules.

Benchmark vs Phase 1e (lower quantile, ms):
world                 baseline     now   ratio
main-10                  1.790   0.993    0.55
main-100                15.415   9.038    0.59
main-1000              153.086  90.145    0.59
three-levels            35.215  18.989    0.54
chain                   68.931  35.045    0.51
library-500             83.083  85.830    1.03
many-shapes              4.837   3.344    0.69
variant-100             32.052  33.586    1.05
All worlds pass the 10% regression threshold.

AI-assisted-by: GPT-6
2026-10-28 15:53:07 +01:00
alonso.torres
9fbc7dd818 🐛 Keep component sync with each user state transition
Emit changes-applied with the commit's prior file data. Sync each commit
before a later edit can replace its state or separate its undo group.
Allow later commits through while completion barriers finish (F110).

Process layout batches after their buffer closes, so reentrant component
sync cannot lose a pending layout task. Cover three user edits without an
intermediate wait, then every undo and redo. Token and tiny-layout cases
also pass. Re-record golden cases X and AJ for the settled timing.

Frontend: 1495 tests, 7255 assertions pass; lint and format checks pass.
The three-edit regression failed before the fix.

Benchmark vs Phase 1e (lower quantile, ms):
world                 baseline     now   ratio
main-10                  1.790   0.993    0.55
main-100                15.415   9.038    0.59
main-1000              153.086  90.145    0.59
three-levels            35.215  18.989    0.54
chain                   68.931  35.045    0.51
library-500             83.083  85.830    1.03
many-shapes              4.837   3.344    0.69
variant-100             32.052  33.586    1.05
All worlds pass the 10% regression threshold.

AI-assisted-by: GPT-6
2026-10-28 15:53:07 +01:00
alonso.torres
e2740f3be7 🐛 Preserve scaled content styles through component sync
Store cumulative content scale as an optional shape attribute, default 1.
Sync scales the main's styles by the copy/main ratio separately from
constraint resizing. Keep descendants untouched and allow later main style
edits to propagate (F109). Both renderer modifier paths use this field.

Cover a later sync, repeated scaling, main radius edits, reset and its undo.
The original stroke regression failed before the fix. Common JVM/JS and
frontend suites, backend and exporter tests, lint and format checks pass.
Historical scale gestures cannot be reconstructed from old file data.

Benchmark vs Phase 1e (lower quantile, ms):
world                 baseline     now   ratio
main-10                  1.790   0.993    0.55
main-100                15.415   9.038    0.59
main-1000              153.086  90.145    0.59
three-levels            35.215  18.989    0.54
chain                   68.931  35.045    0.51
library-500             83.083  85.830    1.03
many-shapes              4.837   3.344    0.69
variant-100             32.052  33.586    1.05
All worlds pass the 10% regression threshold.

AI-assisted-by: GPT-6
2026-10-28 15:53:07 +01:00
alonso.torres
c0e814421f 🐛 Bound component sync retries per component
Count repeated processing of each component instead of total graph width.
A valid dependency graph with 102 outer components now settles and updates
every copy (F108). The non-settling cascade guard remains bounded.

The regression failed at 100 passes before the fix, then passed with 103
components. Common JVM/JS, frontend, backend and exporter suites pass.

Benchmark vs Phase 1e (lower quantile, ms):
world                 baseline     now   ratio
main-10                  1.790   0.993    0.55
main-100                15.415   9.038    0.59
main-1000              153.086  90.145    0.59
three-levels            35.215  18.989    0.54
chain                   68.931  35.045    0.51
library-500             83.083  85.830    1.03
many-shapes              4.837   3.344    0.69
variant-100             32.052  33.586    1.05
All worlds pass the 10% regression threshold.

AI-assisted-by: GPT-6
2026-10-28 15:53:07 +01:00
alonso.torres
03afe5de80 🐛 Preserve nested overrides when absorbing a component
Keep touched groups and remote sync state when cloning an absorbed main.
New component copies still start clean. Extend the nested absorption test
with head and descendant overrides, then sync the inner component (F107).

Validation: red regression before the fix; common JVM 1705 tests and JS
1633 tests, backend 1610 tests, exporter 14 tests pass.

AI-assisted-by: GPT-6
2026-10-28 15:53:07 +01:00
alonso.torres
de30b8434e 🐛 Drop the uniform structure flag of older copy texts
Copy texts in files older than mid-2025 can hold
:text-content-structure-same-attrs next to :text-content-structure,
which marked a structure change of a text with one style. The text
override migration recomputed the groups and dropped the structure flag
but left this one in :touched, where nothing reads it, and validation
did not report it. It is now one of the legacy text groups, so the
migration and the :legacy-text-touched repair drop it too. Found in
the copy A2 of the QA fixture text-components-propagation.penpot while
re-running PENPOT-2261.

convert-text-overrides-test: the flag goes with the structure flag and
without it. repair-components-test/legacy-text-touched: a copy holding
it is reported and repaired.

AI-assisted-by: claude-opus-5-5
2026-10-28 15:53:07 +01:00
alonso.torres
ee448fd5a1 🐛 Absorb the libraries a move to another team leaves behind
Moving files or a project to another team unlinked every library that
ended up in another team than the file using it, but absorbed nothing:
the copies kept pointing at a library their file could no longer
reach. A move now does what deleting the library does (PENPOT-1540):
it absorbs each such library into the files using it, then unlinks
them; as on deletion, a failed absorption is logged and does not stop
the move. That covers a moved library and the files left behind using
it, a moved file and the libraries it used, and a moved project.

The links checked are those that start or end at a moved file. They
were those of the files in the source and destination projects, so a
file in another project of the old team kept its link to a moved
library across teams.

rpc-management-test: a moved library is absorbed into a file in its
project and into one in a project the move does not touch; a moved
file absorbs its library; a moved project absorbs the library it left
behind.

AI-assisted-by: claude-opus-5-5
2026-10-28 15:53:07 +01:00
alonso.torres
6ce88bd0c1 🐛 Absorb a deleted library into the files that used it
Deleting a shared library removes its links to the files using it at
once, so they stop listing it, and queues the delete-object job, which
absorbs the library into the files it finds through those links. By
then there were none, so nothing was absorbed: the files lost the
library and their copies kept pointing at the deleted file
(PENPOT-1092). Unpublishing absorbs before it unlinks, so it was not
affected.

The delete reads the files using the library before unlinking them
and gives them to the job (:absorb-file-ids), which absorbs into them;
without them, as from the project and team deletions, the job still
finds them through the links.

rpc-file-test/delete-shared-library-absorbs-it-into-the-files-using-it
deletes the library through the RPC and runs the job it queued.

AI-assisted-by: claude-opus-5-5
2026-10-28 15:53:07 +01:00
alonso.torres
9c701da24a 🐛 Store the fragments a library absorption changes
Absorbing a library into a file that uses it (unpublishing or deleting
the library) saved the file without its features. Saving stores the
changed pointer-map fragments only when the features name
fdata/pointer-map, so with that feature the file pointed at a fragment
of components that was never written, and loading it failed with
:fragment-not-found. The file's features now go with the save.

rpc-file-test/unpublish-library-absorbs-it-into-the-files-using-it
unpublishes a library with a copy in another file, then loads that
file. The existing absorption test turns fdata/pointer-map off, so it
missed this.

AI-assisted-by: claude-opus-5-5
2026-10-28 15:53:07 +01:00
alonso.torres
5fdf0c036f 🐛 Keep a committed property name from being sent again
When the variant property panel went away (selecting another layer),
input-with-meta* sent a blur from the last input it had rendered, to
commit an edit still open. After an edit was already committed that
input is gone and keeps the text it had: reordering the properties
and then selecting a variant renamed the property now in that row
after the old one, "Property 1" becoming "Property 2 (1)"
(PENPOT-2588). The input is forgotten once its edit is committed, so
the blur is sent only for an edit still open.

ds-input-with-meta-test mounts the component in a jsdom document: a
committed edit is not sent again when the value changes and the input
goes away; an edit still open is committed.

AI-assisted-by: claude-opus-5-5
2026-10-28 15:53:07 +01:00
alonso.torres
03c6e3b006 🐛 Take the first equally near variant on a switch
A switch picks the variant whose properties are nearest the wanted
ones; between equally near ones it took the last child of the variant
container. Since combine as variants keeps the order of the components
it combines, that is the last of them: switching an empty variant to
"primary" gave primary/icon+test where the QA case (PENPOT-2446)
expects primary/hover, the first primary variant. It now takes the
first child among the equally near ones.

variants-switch-target-test: two variants one value away, in both
orders of the container's children.

Benchmark against the 1e baseline (lower quantile, ms):

  world                               baseline     now  ratio
  deepest-edit-through-three-levels     35.215  16.810   0.48
  edit-through-a-chain-of-dependents    68.931  32.765   0.48
  library-update-into-copies            83.083  39.232   0.47
  main-edit-among-many-shapes            4.837   2.277   0.47
  main-edit-into-10-copies               1.790   0.807   0.45
  main-edit-into-100-copies             15.415   8.054   0.52
  main-edit-into-1000-copies           153.086  81.186   0.53
  variant-switch-into-copies            32.052  17.933   0.56

AI-assisted-by: claude-opus-5-5
2026-10-28 15:53:07 +01:00
alonso.torres
9592b1cb2e 📎 Count only the plugin calls in the plugin registry tests
The registry tests mock the RPC and check every call recorded while the
mock is installed. A component sync deletes the thumbnails of the
frames it changes 200 ms later, so a sync at the end of an earlier test
of the suite can call the mock in that window: under the coverage run,
the frontend suite failed in register-test, a different test each time,
with an extra :delete-file-object-thumbnails call. The mock now records
and answers only the plugin profile commands; any other call gets an
empty answer.

AI-assisted-by: claude-opus-5-5
2026-10-28 15:53:07 +01:00
alonso.torres
c05e45d3f7 📚 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
2026-10-28 15:53:07 +01:00