Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changes/edit-lock-boundaries.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "type": "user-facing", "releaseNoteId": "release:0.21.3" }
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ All notable Loop Studio releases, newest first. Behavioral changes are pinned
in versioned spec documents (see the [README](README.md#technical-reference));
this file is the narrative history, not the contract.

## v0.21.3 — 2026-10-08

A fix release (issues #334 and #335): the Canvas edit lock is exact at every document boundary and refuses every edit, and its button shows the state at a glance, as [`docs/canvas-edit-lock.md`](docs/canvas-edit-lock.md) describes.

- **A new document starts unlocked.** File → New, starting a temporary session without the open diagram, and Delete work data leave an unlocked, empty document. Before, a locked example or file kept the new document locked, even after a reload.
- **Every other document brings its own lock and no history.** A Template, a file, a share link and Open proposal as document open with their own lock (a locked example stays locked) and with an empty undo history: Undo can no longer go back into the previous document. The confirmation before a replacement is the safety net.
- **While locked, nothing edits the document.** The palette, Insert module, Undo and Redo, the data import wizard, a data refresh, renaming a bound table and a revision Apply are disabled, and the stores refuse them too. Selecting, panning, zooming, Focus and the other view settings, Run and Step, export and sharing stay available.
- **The lock button shows its state.** Unlocked is an open padlock with its shackle swung clear of the body; locked is the closed padlock with the same pressed tell as Focus (the system highlight in forced colours). Its name stays "Edit lock" and `aria-pressed` carries the state; the tooltip names the next action.

**No migration.** Files, share links, digests and simulation results are unchanged. One new string (the button's name) and three release-note lines in 18 languages, 16 of them without native review. The informational `meta.tool` string is now `loop-studio/0.21.3`.

## v0.21.2 — 2026-10-07

A fix release (issue #332): a Pool's, a Parameter's and a Register's value and detail rows sit inside the node, as [`docs/node-shell-content-in-vessel.md`](docs/node-shell-content-in-vessel.md) "Follow-up — value and detail rows" describes.
Expand Down
28 changes: 14 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,18 @@ Additional feature-specific design documents (localization, mobile, module
system, large-graph readability, simulation playback, edge routing, data
import, …) live under [`docs/`](docs/).

## Latest — v0.21.2
## Latest — v0.21.3

A fix release: the edit lock is exact, and its button shows the state.

- **A new document starts unlocked**, and every other one brings its own lock and an empty
undo history, so Undo never goes back into the previous document
- **While locked, nothing edits the document**: the palette, Insert module, Undo, Redo and
the data import wait; selecting, viewing, running and exporting stay available
- **The lock button reads at a glance**: an open padlock when you can edit, a closed,
highlighted one when editing is locked

## v0.21.2

A fix release: values and detail rows sit inside their node.

Expand Down Expand Up @@ -170,19 +181,8 @@ Compact nodes: more of a large graph fits in view.
- **Nothing in a file changes**: positions, saved diagrams and simulation results stay the
same; connections attach a few pixels higher

## v0.20.0

Flow colours beyond the canvas.

- **The minimap and the timeline** show a coloured node in its colour; a coloured Pool or
Register draws its timeline line in it, and every other series keeps its own
- **Three templates in colour**: Coffee roastery, the gacha banner and early MMO open with
three colours on their main flows; their results are unchanged
- **Each colour once** in the Inspector: Recent and In this document leave out the colours
already shown above them
- **On a phone**, the read-only Inspector shows a colour as one line: a dot, its name and hex

See [`CHANGELOG.md`](CHANGELOG.md) for the full notes of these releases, v0.19.0 (flow
See [`CHANGELOG.md`](CHANGELOG.md) for the full notes of these releases, v0.20.0 (flow
colours in the minimap and the timeline), v0.19.0 (flow
colours on nodes and connections), v0.18.2 (the
guided tour says each step once), v0.18.1 (one
keyboard contract for every menu), v0.18.0 (the third-party open-source licenses in the About dialog), v0.17.2 (the
Expand Down
37 changes: 24 additions & 13 deletions docs/bundled-module-label-localization.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,11 @@ revision 2, it IS captured and restored alongside `nodes`/`edges` on every
`SidecarBundle` gains one more key, `m`). This is the load-bearing
correction from revision 1: a bare global map, cleared wholesale by
`newGraph`/`loadGraph`/`loadDoc`, cannot be right — those three actions
push the pre-reset document into `past` FIRST, so an Undo back past one of
pushed the pre-reset document into `past` FIRST, so an Undo back past one of
them must restore that point's tracking along with its nodes, not land on
an empty map.
an empty map. (Since #334 / v0.21.3, `docs/canvas-edit-lock.md` §4, only a
revision Apply still does so; New, a Template and another document start
with an empty history.)

- **`lastAppliedLabel`** is the exact string THIS FEATURE itself last wrote
at that node — set at insert time to whatever `cloneModuleDoc` actually
Expand Down Expand Up @@ -446,11 +448,17 @@ contract this implementation is checked against)
against EACH ENTRY's own provenance snapshot, never the live one
(§MLS4.3, revision 2) — so neither can resurrect a stale-language label,
AND a still-managed-at-that-point instance correctly resumes syncing if
Undo lands there, even past a New/Template-load/file-load.
Undo lands there, even past an in-place whole-graph load (a revision
Apply).
11. `newGraph` / `loadGraph` / `loadDoc` all clear the LIVE provenance map
(§MLS4.2) — but the outgoing document's own tracking is preserved in
history via the same `commit()` sidecar mechanism as `nodes`/`edges`
themselves, so an Undo past the reset restores it too (revision 2).
(§MLS4.2). Since v0.21.3 (issue #334, `docs/canvas-edit-lock.md` §4) New,
a Template, a file, a share link and Open proposal as document are
document boundaries that start with an EMPTY history, so the outgoing
document's tracking leaves with it and no Undo can reach it. The one
whole-graph load that stays undoable, a revision Apply (`loadDoc`
`revision-apply`), preserves the outgoing tracking in history via the
same `commit()` sidecar mechanism as `nodes`/`edges` themselves, so an
Undo past it restores it too (revision 2).
12. No new wire field, no new file-format version; provenance does not
survive a save/reload — scoped explicitly to "bundled instances inserted
in the current session, from this feature onward" (§MLS3), never smuggled
Expand Down Expand Up @@ -481,10 +489,12 @@ contract this implementation is checked against)
`bundledModuleId` registers provenance (with the real applied label) for
exactly the inserted node ids; a `needs-v2-consent` refusal registers
nothing; a file-based insert (no `bundledModuleId`) registers nothing;
`newGraph`/`loadGraph`/`loadDoc` clear the LIVE provenance; **[P1]**
New/loadGraph/loadDoc followed by Undo restores the module instance's
provenance along with its nodes; Redo past a reset restores the empty
(new-document) provenance, not the pre-reset instance's; a loaded document
`newGraph`/`loadGraph`/`loadDoc` clear the LIVE provenance; **[P1]** a
revision Apply (`loadDoc` `revision-apply`) followed by Undo restores the
module instance's provenance along with its nodes, and Redo the empty
provenance, not the pre-load instance's; since #334, New / loadGraph /
a `document-boundary` loadDoc leave an empty history, so Undo cannot bring
the instance or its provenance back; a loaded document
that reuses a former host node id is never treated as provenanced;
**[P1, revision 3]** `updateNodeData` detaches provenance immediately on a
real label edit with no switch involved; a same-value patch does not
Expand All @@ -503,8 +513,9 @@ contract this implementation is checked against)
same-locale reselect a no-op, no regression to Template label-switch
behavior) PLUS, from revision 2's review round: **[P1]** a rename to
another locale's official string preserved through EN/KO/JA cycling;
insert → New/loadDoc/loadGraph → Undo → switch still syncs the restored
instance; a loaded document reusing a former host node id is never
insert → a revision Apply → Undo → switch still syncs the restored
instance (since #334 the New / file / Template-style variants check
instead that Undo cannot bring the instance back); a loaded document reusing a former host node id is never
synced; a rename followed by Undo then Redo restores the managed state
matching each history point (pre-rename still syncs, post-rename stays
preserved) — PLUS, from revision 3's review round: **[P1]**
Expand All @@ -520,7 +531,7 @@ contract this implementation is checked against)
| **MLS-D2** | where does the EN canonical label come from? | **`BUNDLED_MODULES[i].doc`** directly (the same source `cloneModuleDoc` already reads for an EN insert) — not a third overlay table, so there is exactly one place each canonical id's English text is authored. |
| **MLS-D3** | prune provenance entries for deleted/detached nodes? | **No.** Left in whatever snapshot they're in — harmless (never looked up for a node that no longer exists in that snapshot's own `nodes` array) and every live-map entry is fully cleared at the next `newGraph`/`loadGraph`/`loadDoc` regardless; pruning per-delete would be extra wiring for no observable benefit. |
| **MLS-D4** | reuse `known.generated.ts` / the Template relabel machinery? | **No** — a static id table is structurally impossible for modules (§MLS2); a small, synchronous, always-resident map is enough here (two modules, under a dozen nodes apiece), so none of the Template path's lazy-dictionary/CI-drift-check machinery is needed. |
| **MLS-D5** | (revision 2) how does provenance survive Undo/Redo past a New/Template-load/file-load? | **A history-aware sidecar**, riding on the exact mechanism `frameSidecar`/`dataImportSidecar` already use — `SidecarBundle` gains a `m` key, captured by `commit()`'s `sidecarNow()` and restored by `undo()`/`redo()`'s `restoreSidecar()`. Rejected: a bare global map cleared by the three reset actions (revision 1's approach) — provably wrong, since it discarded the outgoing document's tracking the instant a reset committed, with no way for Undo to bring it back. |
| **MLS-D5** | (revision 2) how does provenance survive Undo/Redo past a New/Template-load/file-load? (Since #334 / v0.21.3 those three are document boundaries with an empty history; the question now applies only to a revision Apply.) | **A history-aware sidecar**, riding on the exact mechanism `frameSidecar`/`dataImportSidecar` already use — `SidecarBundle` gains a `m` key, captured by `commit()`'s `sidecarNow()` and restored by `undo()`/`redo()`'s `restoreSidecar()`. Rejected: a bare global map cleared by the three reset actions (revision 1's approach) — provably wrong, since it discarded the outgoing document's tracking the instant a reset committed, with no way for Undo to bring it back. |
| **MLS-D6** | (revision 3) when does a real label edit detach provenance — lazily at the next switch, or eagerly at the edit? | **Eagerly**, in `updateNodeData` itself (§MLS4.4). Rejected: lazy-only detection (revision 2's approach) — it cannot distinguish "never edited" from "edited, then edited back to the exact same text" before any switch happens, silently erasing a genuine edit. The lazy check (§MLS3.1 rule 1) still exists as a correct fallback for a label that arrives some OTHER way (e.g. via Undo/Redo restoring an earlier snapshot) — it is not made redundant, just no longer the only path. |

## MLS8. Order this feeds into
Expand Down
92 changes: 92 additions & 0 deletions docs/canvas-edit-lock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Canvas edit lock and document boundaries

Status: shipped in v0.21.3 (issues #334 and #335).

The Canvas edit lock (`uiStore.canvasLocked`) keeps a document from being changed by accident while it is read, explained or run. This page is the one place its contract lives; the code points here.

## 1. What the lock blocks

While the lock is on, every **user edit** of the document is refused:

- adding a node from the palette (a click or a drag onto the canvas);
- Insert module (a bundled module or a module file);
- moving, connecting, reconnecting and deleting nodes and connections, with the mouse or the keyboard (the arrow keys, Delete, Backspace);
- every Inspector field and button, the flow colour, the Inputs panel values, the Register expression;
- saved frames: drawing, moving, resizing, renaming, colouring, deleting, keeping a suggested frame;
- Undo and Redo (the buttons and the keys);
- the data import wizard, a data refresh and renaming a bound table;
- a revision Apply (Review → Apply).

Copy, cut, paste and duplicate of nodes do not exist today. If they are added, copy stays available and the others follow the lock.

## 2. What stays available

- Selection, region select, the read-only Inspector, pan and zoom, the minimap.
- Focus, Filters, the Activity overlay, Pan mode and every other view setting.
- Run, Step, Play, Monte Carlo, the seed, the speed, the Timeline series: they change the run, not the document.
- Export (Graph JSON, Workspace JSON, CSV, a project revision, Make a proposal, Save selection as a module) and creating a share link.
- Opening another document (section 4).
- Unlocking.

## 3. Where it is enforced

Twice, on purpose:

- **The UI** disables each entry point while locked (`disabled`, not hidden), so nothing invites an edit that will not happen.
- **The stores** refuse it anyway. `src/store/editPolicy.ts` registers one guard on `graphStore` (`setEditGuard`); every user-edit action of `graphStore`, the saved-frame changes of `frameStore` and `projectStore.applyProposal` start with it and return without any change: no state, no undo entry, no autosave, no `simulationRev`. Actions that report a result return `{ ok: false, reason: 'locked' }`, which their callers drop silently (the control was disabled first). `onNodesChange` / `onEdgesChange` keep `select` and `dimensions` changes and drop the rest, so selection and layout keep working.

The guard is registered rather than imported because `graphStore` cannot import `uiStore` (`uiStore` → `mcStore` → `graphStore` would be a cycle); `startApp` imports `editPolicy.ts` once for every build (web, portable, PWA). A bare store in a unit test has no guard.

The runtime is outside the guard by construction: `simStore` and `mcStore` never call a document-changing action.

## 4. Document boundaries

Opening another document is not an edit of the open one, so it is allowed while locked. Each whole-document replacement is a **document boundary**:

| Path | Code |
|---|---|
| File → New | `graphStore.newGraph` |
| A temporary session started without the open document | `switchToTemporary(false)` → `newGraph` |
| Delete work data (personal browser) | `deleteWorkData` → `newGraph` |
| A Template (desktop menu, the phone's ⋯ menu) | `loadGraph` |
| A Graph / Workspace / Project revision file | `workspaceIO` → `loadDoc` `document-boundary` |
| A share link | `shareApply` → `loadDoc` `document-boundary` |
| Open proposal as document | `projectStore.openProposalAsDocument` → `loadDoc` `document-boundary` |

At a boundary:

1. **The undo history starts empty.** Undo and Redo are disabled; Undo can never go back into the previous document. The confirmation the app asks before replacing a diagram (unless it is the untouched first-run sample) is the safety net.
2. **The lock is the new document's.** A document whose `recommendedRunConfig.canvasLocked` is `true` opens locked; every other one, a new empty document included, opens unlocked. The value is written once, as part of the swap and before the new graph is set, so a locked Template or file never shows, renders or autosaves an unlocked moment, and the `applyRecommended` the caller runs afterwards finds it already set.

`loadDoc` takes a required `mode` with no default, so a new caller has to say which load it is:

- `{ mode: 'document-boundary', canvasLocked }` — another document, as above;
- `{ mode: 'revision-apply' }` — the open document edited in place by a revision Apply: one undo entry (`SEMANTICS-R.md` R-INV-8), refused while locked.

`loadGraph` (a Template) is always a boundary and takes `canvasLocked` the same way.

A temporary session that takes the open diagram along, and Reset all Loop Studio data (which reloads the app), are not boundaries of this kind.

## 5. The lock control (issue #335)

The Controls rail's lock button reads at a glance, without colour:

- **Unlocked:** an open padlock, its shackle swung to the side; the free end stands clear of the body by a measurable gap at the real 1× size (2 px for the 14 px icon).
- **Locked:** the closed padlock (both legs meet the body) with the rail's pressed tell, the same as the Focus toggle: the soft signal tint, an inset 2 px ring and the signal colour, at least 3 : 1 against the unlocked button. In forced colours: the system `Highlight` / `HighlightText` pair, like every rail toggle.
- **Name and state:** the accessible name is fixed, "Edit lock" (`canvas.lock.name`), and `aria-pressed` carries the state; the tooltip names the next action ("Lock editing — …" / "Unlock editing — …").
- The keyboard focus ring (the global `:focus-visible` outline) stays visible on the pressed button.
- The phone has no lock button (section 6).

## 6. Persistence

The lock is kept in `localStorage` (`loop-studio:canvas-locked`) so a plain reload or a PWA update keeps it. It is never part of the GraphDoc content, the digest, undo or `simulationRev`; a file carries it only as `recommendedRunConfig.canvasLocked`, written by an export while the lock is on.

The phone is view-only (`docs/mobile.md` §MV3a) and has no lock control; a Template opened there still sets the lock a later desktop visit sees.

## 7. Tests

- `src/store/editPolicy.test.ts` — every refused action leaves the document, the frames, the history and the digest unchanged; selection, a Step, export and unlocking still work.
- `src/store/graphStore.boundary.test.ts` — each boundary empties the history; a revision Apply keeps one entry; the lock takes only its final value (no unlocked moment for a locked document).
- `e2e/document-boundary.spec.ts` — the boundaries through the real UI: File → New, a temporary session, Delete work data, a Template, a file and a share link.
- `e2e/canvas-lock.spec.ts` — the controls are disabled and change nothing while locked, the keyboard neither reaches nor runs them; the allowed ones work.
- `e2e/lock-control.spec.ts` — the 1× gap of the open icon and none for the closed one, the fixed name, `aria-pressed` and the tooltip, the pressed tell in light and dark (the same as Focus, at least 3 : 1) and `Highlight` in forced colours, and the focus ring on the pressed button.
Loading
Loading