Release/0.1.0 audit#2
Merged
Merged
Conversation
…deps to SHAs - pyproject: dynamic version from setuptools_scm (drop static 0.0.1); __init__ reads importlib.metadata. Tag v0.1.0 now drives the version. - Add LICENSE (GPL-3.0-or-later) + license metadata/classifier + README section. SpyDE builds on GPL HyperSpy/PyXEM. - Pin hyperspy/rosettasciio/anyplotlib to the exact commit SHAs already in uv.lock instead of moving branches (anyplotlib@main was the worst risk). uv lock --check passes.
…andler vector_overlay.py bound `log = getLogger(__name__)` but the beam-stop scan's except handler called `logger.debug`, raising NameError and killing the fv-beamstop daemon thread (leaving _beamstop_scanning stuck) instead of degrading gracefully.
When the Python sidecar exits, runner.ts now dispatches a synthetic `backend_exited` message and the renderer shows a non-dismissable overlay (exit code + "restart SpyDE"). Previously the UI froze silently because every sendAction no-ops once the process is gone. Auto-restart is a follow-up.
The progressive navigator creates a SharedMemory via ensure_live_buffer and stashes it as self._nav_shm, but BaseSignalTree.close() never closed or unlinked it, leaking the segment for the process lifetime on every tree close. Close + unlink it (guarded against None/absent and double-unlink), mirroring _stop_progressive_stream's cleanup.
add_plot_window appended every PlotWindow to plot_subwindows, but Session's teardown path (_close_plot/_close_tree -> plot.close() + _forget_window) closes the underlying Plot directly and never went through PlotWindow.close_window(), so the list grew unbounded as windows were opened and closed. Add MDIManager.remove_plot_window and call it from Session._forget_window. Dead close_signal_tree/signal_trees left for the Phase 5 refactor.
__init__ set _recent_files = [] then loaded _settings (which holds the saved "recent_files"), but never copied them back into _recent_files, so the recent-files list was empty on every restart. Restore it after settings load, capped at 20 to match _add_recent's save cap.
beamstop_dilate was in DEFAULTS and coerced by the action, and the live preview honored it, but _do_compute_vectors called _auto_beamstop_from_signal with the hardcoded default dilate (5), so the tuned value did nothing to the real result. Thread params["beamstop_dilate"] (default 5) into the auto-beamstop call. The auto-beamstop helper still reads a single storage-aligned block, so the memory-safety contract is unchanged.
The numba-CUDA find-vectors path defaulted to _subpixel_com_kernel (centre-of-mass), the least-accurate of the three subpixel estimators — and it was the default on every NVIDIA box. Add _subpixel_parabola_kernel, a direct GPU port of the CPU _subpixel_parabola (3-point parabolic vertex per axis, clamped to [-1,1]), and use it at both numba-CUDA subpixel call sites. The CoM kernel is left defined but unused. The kernel arithmetic was validated against the CPU reference (max |cpu - gpu_math| = 9.5e-7 over 200 random surfaces). Actual on-GPU launch (numba JIT) could not be exercised in this environment and needs real-data validation per the CLAUDE.md benchmarking rule.
compute_strain_field is a per-pixel scipy loop (seconds on a real scan) that strain_run and every reference/ring/CIF interaction ran inline on the asyncio main thread, freezing the UI. Offload both the initial fit and StrainController._recompute to a worker thread and marshal the figure build/update back via Session._dispatch_to_main, matching the threaded idiom of center_zero_beam / find_vectors. A generation counter drops superseded recomputes (latest-wins). Falls back to inline when no session / dispatch loop is available (tests). All 16 strain tests pass.
FindVectorsWizard, OrientationWizard and VectorOrientationWizard each held a setTimeout ref cleared only on the NEXT invocation, never on unmount — closing a wizard mid-debounce fired fv_tune / om_refine / vom_refine at a torn-down preview. Add an unmount cleanup effect to each that clears the pending timer.
The `open-external` IPC handler forwarded any renderer-supplied URL straight
to `shell.openExternal`, letting the renderer (or compromised iframe content
routed through it) ask the OS to open arbitrary `file:`/custom-scheme/
`javascript:` URLs. Parse with the WHATWG URL constructor and only proceed
when the protocol is in {https:, http:, mailto:}; reject (console.warn)
unparseable or disallowed URLs.
`_spyde_test_inject` (a generic message injector), `_spyde_test_widgets`, and `_spyde_test_image_sig` were attached to `window` in EVERY build, including a packaged production app — leaving a debug/injection surface in shipped code. Gate them behind `import.meta.env.DEV || !window.electron?.isPackaged`. The main process sets SPYDE_PACKAGED=1 from `app.isPackaged` before the window loads; the preload exposes that as `electron.isPackaged`. This is TRUE in dev (`npm run dev`) and under the Playwright e2e (which launches the BUILT bundle by path, so `app.isPackaged` is false), and FALSE only in `npm run dist` (packaged) — keeping all 11 e2e specs that use these hooks green while removing them from production. Cleanup deletes all three hooks symmetrically.
… exit
stopSpyDE() previously did only `proc?.kill()`, which on Windows kills just the
direct Python child and ORPHANS the Dask worker/nanny grandchildren; it also
ran only on window-all-closed (Cmd-Q / before-quit / Ctrl-C left the backend
running).
runner.ts: stopSpyDE now (1) writes `{type:'quit'}` to stdin for a graceful
asyncio-loop shutdown (app.py handles `quit` → session.shutdown() → clean Dask
teardown), then (2) backstops with a tree-kill if it doesn't exit promptly:
`taskkill /pid <pid> /T /F` on win32, SIGTERM→SIGKILL on posix. Made idempotent
and null-safe (a `stopping` flag, reset in startSpyDE) so overlapping quit paths
don't double-kill.
index.ts: register `app.on('before-quit', stopSpyDE)` and SIGINT/SIGTERM
handlers (stopSpyDE → app.quit → delayed hard-exit) so every quit path tears
the backend down.
Composition with process_guard.py: the Python side installs a Windows
kill-on-close Job Object so the OS reaps the worker tree whenever the backend
process dies for ANY reason. The graceful `quit` here is the preferred clean
path; the Electron-side tree-kill is the backstop for when Electron must hard-
stop the backend before it reaches its own shutdown() (and is the cross-process
reaper on platforms / cases the Job Object doesn't cover).
…heme, enable webSecurity
`webSecurity: false` was set app-wide so figure iframes could load their
`file://` HTML — but that disables same-origin policy for the ENTIRE renderer,
not just figures. Replace it with a dedicated custom protocol so webSecurity
can stay at its secure default.
PATH TAKEN: full custom-protocol fix (not the reduced-scope fallback).
Investigation showed each figure HTML references exactly ONE external resource:
a dynamic `import("file://…/spyde_figure_esm_*.js")` for the shared anyplotlib
JS bundle (no CSS/fonts/http). That bounded surface makes a clean protocol
handler safe to land:
- Register `spyde-fig` as a privileged STANDARD + SECURE + fetch-capable scheme
BEFORE app-ready (module top level).
- `protocol.handle('spyde-fig', …)` in whenReady serves files from the OS temp
dir through a strict basename allowlist (`spyde_fig_*.html` /
`spyde_figure_esm_*.js` only; path-traversal, bad host, and arbitrary reads
return 404). Verified the resolver against real temp files + traversal inputs.
- The figure message handler now writes the HTML and emits a
`spyde-fig://figures/<name>` URL instead of `file://`. It also rewrites the
embedded `import("file://…/spyde_figure_esm_*.js")` to the SAME spyde-fig://
origin — a secure-scheme page can't import a `file://` module (cross-scheme),
and same-origin keeps Chromium's V8 code cache shared across iframes. The
rewrite regex handles both posix `/` and Windows escaped `\` separators;
verified on a real figure HTML (1 match, no file:// import left).
- Removed `webSecurity: false` (back to the secure default).
The renderer's data:-URL test fallback (raw html, no file_url) is unchanged.
NEEDS RUNTIME VALIDATION (can't fully drive the app here): confirm figures
actually render in the iframe via the new scheme + that the ESM dynamic import
resolves at runtime. Build passes; module loads + scheme registers without
throwing under a stubbed-electron smoke test.
anyplotlib 0.1.0 is now on PyPI. Verified in a clean venv that every symbol SpyDE imports works and the public API surface matches the pinned fork commit (3cbbf3d4): _electron is identical, apl is a superset. Switching to `anyplotlib>=0.1.0` removes the last moving-branch git dependency (@main was the worst supply-chain risk). uv lock now resolves it from the PyPI registry; full migrated suite stays green.
… review M1) Enabling webSecurity (Phase 3) blocks the renderer's file:// <img> icon loads from the dev http://localhost origin. Serve package icons through the existing privileged spyde-fig scheme under an `icons` host instead: resolveIconPath only returns a real .svg/.png whose realpath (with .. /symlinks collapsed) lives under a spyde ".../icons/" directory. Verified the resolver accepts real icons and rejects system files, traversal-out, wrong host, and non-image extensions.
setuptools_scm derives the version from git tags (Phase 1). The default actions/checkout fetch-depth:1 has no tags, so builds got an ugly 0.0.postN.devN version. Add fetch-depth:0 to the checkout steps in build.yml and release.yml.
There is no 'all' extra in pyproject.toml — only 'tests' and 'doc'. The '.[doc,all]' install would error on a clean runner. Use '.[doc]'.
The installer ships pyproject.toml + uv.lock and resolves the venv on the user's first launch, so a lock out of sync with pyproject.toml would only fail for the user. Add `uv lock --check` early in each build leg.
Adds `npm run typecheck` running tsc in noEmit mode against
tsconfig.node.json then tsconfig.web.json — the real type gate that was
missing.
Current state: the node leg is CLEAN (0 errors). The web leg reports 9
PRE-EXISTING errors in two files only:
- src/renderer/src/kernel/SpyDEContext.tsx (the `window as
Record<string,unknown>` _spyde_test hook casts + `import.meta.env`)
- ../guides/markdown.tsx (react module path)
These are documented known issues, out of scope for the release audit.
Therefore the script is added but NOT yet wired into CI (a failing
typecheck would make CI red). Wiring it into build.yml is deferred until
those pre-existing errors are fixed (Phase 5/6).
No CI ran the Electron e2e. Add an `e2e` job that runs the default 'electron' Playwright project (synthetic + bundled data, no network — the opt-in 'electron-real' project is excluded). ubuntu-latest + xvfb: cheapest/standard for headless Electron CI, and the default project exercises no Windows-specific reader paths. Sets up uv + pre-syncs the backend env (dev mode spawns `uv run python -m spyde`), npm ci, installs the Playwright chromium browser, then `npm run test:build` (electron-vite build && playwright test). Uploads the report on failure. Marked continue-on-error for now: it launches a real Electron app + Dask cluster and is not yet validated green on a hosted runner. Drop that once confirmed stable.
The Phase-2 strain fix offloads compute_strain_field to a worker thread
and marshals the figure-apply back via Session._dispatch_to_main, with a
generation counter dropping a superseded recompute. The existing tests
only hit the INLINE fallback (duck-typed session, no _dispatch_to_main).
Add test_strain_threaded.py exercising the REAL threaded branch against a
live asyncio loop on a background thread:
- test_recompute_applies_via_dispatch_thread: the field is computed off
the caller thread and applied on the loop thread (genuine cross-thread
marshal, not inline).
- test_superseded_recompute_is_dropped: a slow in-flight recompute whose
generation is bumped by a newer one does NOT apply its stale result
(deterministic gate by call count; exactly one apply lands).
The two fetch-depth comments added this phase used a Unicode arrow; normalize to '->' so the workflow files stay pure-ASCII (avoids surprises for latin-1 readers). Pre-existing Unicode in release.yml's release-notes block left untouched.
The e2e job ran `npm run test:build` with no --project, so Playwright ran ALL projects including the heavy opt-in `electron-real` (real pyxem downloads + screenshot writes) — contradicting the job's own comment. Pin --project=electron so only the synthetic/bundled-data project runs. Verified: lists 97 [electron] tests, zero electron-real.
…_kernel _find_vectors_batch_gpu was never called (only named in a stale comment); _nxcorr_kernel was used only inside its body, so both are orphaned. The live GPU path (_find_vectors_chunk_gpu_impl + _nxcorr_reflect_kernel / _nxcorr_tiled_kernel) is untouched. Also corrected two stale comments/docstrings that referenced the deleted kernel.
These dirs were declared as package-data but nothing loaded them (only toolbars.yaml + metadata_widget.yaml are read at import). Empty __init__.py, no Python imports, no YAML references. Removed the dirs, their pyproject package-data entries, and the stale CLAUDE.md "Configuration Files" / Actions references that documented them as live.
Add spyde/actions/_common.py with reciprocal_radius(), STRAIN_COMPONENTS/ STRAIN_TITLES, and widget_region(). Repoint the duplicated copies: - _reciprocal_radius: orientation_action + vector_orientation_om - ROI widget->region slice: fft_action + line_profile_action - strain component keys: strain_action (dropped its unused _TITLES) strain_display still defines its own component-title dict; deduped next.
Completes the _common dedup: strain_display's component-title dict is now the shared spyde.actions._common.STRAIN_TITLES (was a verbatim copy).
The load_test_*/test_nav_drag/run_test_orientation actions are reachable from the renderer and download real datasets — fine for dev + the Playwright e2e suite (unpacked app), but they shouldn't exist in a shipped build. Gate them on SPYDE_PACKAGED (set by Electron main only when app.isPackaged, inherited by the backend), refusing them with a logged warning in production. Dev + e2e (unpacked) keep them; the migrated suite is unaffected (never sets SPYDE_PACKAGED).
- Add "vite/client" to tsconfig.web.json types so import.meta.env is typed - Declare typed window._spyde_test_* test hooks in electron.d.ts and drop the six `as Record<string, unknown>` casts (DEV/packaged gate intact) - Map react / react/jsx-runtime in tsconfig paths so the shared ../guides/markdown.tsx (imported by the renderer Tour) resolves react's @types from electron/node_modules npm run typecheck now exits 0; npm run build succeeds.
Runs `npm run typecheck` (tsc --noEmit of the Electron main + renderer) as a standalone fast job mirroring the e2e job's Node setup. Gates PRs on the now-green TypeScript compile.
Add electron/src/renderer/src/kernel/protocol.ts defining a discriminated-union `PlotAppMessage` (keyed on `type`) covering every backend message the renderer dispatches, plus an `asPlotAppMessage()` narrow. Rewrite the SpyDEContext message handler to switch over the narrowed `msg.type` so each variant's fields are typed, removing ~64 ad-hoc `as` casts in the dispatch block — including the worst offender `msg as unknown as NavShapePrompt`, now a proper variant that extends NavShapePrompt. Each variant carries an index signature so not-yet-modelled fields stay accessible (as `unknown`); no runtime behaviour change. typecheck and build both still pass.
Behaviour-preserving split of spyde/actions/find_vectors.py into a package: - gpu_runtime.py numba-CUDA/CuPy infra: pools, streams, latches, policy - kernels.py numba @cuda.jit kernels + CuPy/cuFFT NXCORR - detectors.py per-frame cores (NXCORR, DoG, beam-stop, subpixel, intensity) - chunk.py ghost-block chunk pipeline (GPU + CPU + DoG) - orchestrate.py dask batch orchestration (_do_compute_vectors, _nav_chunk_size) __init__.py re-exports the public surface so external import sites (from spyde.actions.find_vectors import X) keep working. No logic changes; the memory-safety contract in _do_compute_vectors and all GPU latch/warmup state are moved verbatim.
…KER) The split moved the @cuda.jit kernels into kernels.py (defined only inside the `try: from numba import cuda`), but chunk.py imported the individual kernel names at top level — so on a no-numba/no-GPU machine the import raised ImportError and the WHOLE package died, breaking the CPU fallback (numba is not a declared dep). Import the kernels MODULE instead and reference _kernels.<name> at call time, always behind the _GPU_KERNELS_AVAILABLE guard. Add a subprocess regression test that imports the package with numba blocked.
Behavior-preserving move of _emit_axes, _set_axis, _set_offset_crosshair into a mixin; Session now inherits from it.
Behavior-preserving move of _load_test_*, _test_nav_drag, _load_test_vectors, _run_test_orientation into a mixin.
Behavior-preserving move of register/unregister_plot, next_window_id, _plot_by_window_id, and the close/forget/resize/figure-event cluster. MDIManager fold DEFERRED (Session keeps self.mdi_manager). Mixins call ipc.emit (not a module-bound emit) so the conftest emit monkeypatch reaches them; same fix applied to the axes/testharness mixins.
Behavior-preserving move of dispatch_action, _dispatch_toolbar_action, _track_action_artifacts, _set_overlay, _set_action_active, _update_vi, plus the _STAGED_HANDLERS table and _TEST_ACTIONS gating. Constants re-imported into session.py for back-compat.
Behavior-preserving move of open_file/open_stack/load_example loaders, the nav-shape prompt round-trip, save methods, and the module-level file helpers (SUPPORTED_EXTS, _path_ext, _is_supported_dataset_path, _dataset_size_bytes, …). Helpers re-imported into session.py so 'from spyde.backend.session import _path_ext' etc. still resolve. session.py drops 2000 -> 445 lines.
_set_axis moved to _session_axes (emits via ipc.emit) and _save_signal
to _session_files (uses _session_files.threading); update the two tests
that patched spyde.backend.session.{emit,threading} to target the new
homes. Pure test-side follow-up to the session.py split.
Replace the matplotlib Agg PNG IPF colour-key triangle with a native anyplotlib figure, mirroring ipf_density.py. build_ipf_key_figure() evaluates the orix IPF colour key (direction2color) over a stereographic grid of the fundamental sector and feeds the per-cell RGB as a pcolormesh colour-string array clipped to the sector boundary, with the white sector outline + [hkl] corner labels. The legend now emits a `figure` message tagged view="ipf_key" (instead of the `ipf_key` PNG-data-url message); the renderer pins that figure's iframe in the corner of the 2-D map (shown only in 2d mode, like before). Removes the ipf_key message type, the IPF_KEY action/state map, and the matplotlib import/savefig from ipf_view.py (orix still needs matplotlib elsewhere). Drops the now-unused ipf_key.png test fixture.
Single checklist capturing the release process the audit established: tag-driven versioning (setuptools_scm + fetch-depth:0), uv lock --check, SHA-pinned git deps, the test/typecheck/build/e2e gates, the manual GPU/distributed/no-numba/ shutdown checks CI can't cover, and the tag-triggered release workflow.
…lectron-updater - pyproject: add the Python 3.13 classifier (CI already tests 3.13); register the `network` pytest marker (was used in test_composition, unregistered) and add `--strict-markers` so a future unregistered marker errors instead of warning. - electron: remove the electron-updater dependency — it was declared but never imported and never even resolved in package-lock (auto-update isn't wired for 0.1.0). Reword the electron-builder.yml publish comment accordingly (the publish block still configures the artifact target + latest.yml; it just isn't read by an in-app updater anymore).
_build_window computes the strain field, sets ctrl.field, and shows the figure; attach() then unconditionally called _recompute(), running the full per-pixel scipy fit a SECOND time on every strain-window open. Skip the initial recompute when ctrl.field is already set (later ref/ring/CIF interactions still recompute). Halves strain-window open latency. (Phase-2 review carryover.)
- center_zero_beam / vector_orientation_om docstrings claimed the "heavy Qt caret" lives in pyxem.py / vector_orientation_action.py — both deleted in the Qt cleanup. Reworded to describe the Electron-native reality. - find_vectors/__init__: removed the dead TYPE_CHECKING `RoundedToolBar` import (targets the deleted toolbars/toolbar.py) and the unused `_FV_BUILT_TOOLBARS` guard; rewrote the docstring (it described the Qt caret UI — this is the compute package). Both were grep-confirmed unused. - CLAUDE.md: drop the nonexistent pyxem.py entry; document the find_vectors package + _common.py.
Figure HTML written to tmpdir (spyde_fig_*.html, served via spyde-fig://) was never removed, so a long session with many re-rendered figures accumulated temp files. Track written paths and rmSync them on every quit path (window-all-closed, before-quit, SIGINT/SIGTERM).
benchmark_find_vectors.py imported _subpixel_com from find_vectors — a symbol that never existed (the CPU subpixel fn is _subpixel_parabola) and was unused in the function body. Pre-existing ImportError on this manual benchmark; remove the dead name. (Flagged by the Phase-5e review.)
…in map) - build_strain_figure docstring said it returns a 4-tuple (fig, fig_id, html, plot2d); it returns a 5-tuple — the glyph_group is also returned (and used by the controller). Document it. - _affine_from_logstrain docstring claimed S = expm(E); the code actually uses the small-strain S = I + E (cheaper, SPD within ±cap), as its own inline comment says. Reword so the docstring matches the implementation.
…atmap assertion logic
…2e assertion test_generate_then_run asserted the Strain window's signal-plot view_label == "εxx" immediately after waiting only on the new-tree count, but the worker (_build_result_windows) adds the tree THEN tags the plot — a race that flaked on slow CI runners (macos-py3.12 / ubuntu-py3.11). Poll for the tagged plot with _wait instead. selector.spec.ts expected the axis-scale cell to render "2.5", but the renderer formats it to 2dp (PlotControlDock fmt = n.toFixed(2)) → "2.50". Update the expectation to the intended value.
…glyph overlay compute_strain_field ran a per-pixel scipy loop — a fresh cKDTree + lstsq + eigh per nav position, ~13k times on a real scan (sped_ag) → ~5.8 s, which is why the Strain window felt slow. Replace it with a single whole-field pass: per-pixel mean-centre, ONE Friedel (±g) KDTree match for every vector at once, per-pixel affine normal equations via scatter-add, a batched 3x3 solve, and a closed-form batched 2x2 polar decomposition (no per-matrix eigh). Matches the old loop to machine precision (max|Δ| ~4e-16) and runs in ~130 ms on 13,312 px — 45x faster. The 5-D/time and non-CSR cases keep the per-pixel reference path. Also remove the white principal-strain ellipse glyphs from the strain view — they cluttered the component map and added per-frame glyph recompute on every reference/component interaction. build_strain_figure/update_strain_view lose the glyph args; StrainController loses .glyph.
…r test test_settle_timer_armed_on_move_and_clears slept a fixed 0.2s then asserted _settle_timer is None — but under CI load the daemon timer thread can take far longer than its nominal 0.05s delay to fire+clear, so it flaked on slow runners (macos-py3.12). Add a _wait poll helper and use it for the timer-cleared and run-count assertions here and in the dispatcher test.
_wait_idle() returns when the dispatcher queue is empty, but the LAST job can still be running its (slow) slice fn + apply at that point — its fixed 0.05s settle sleep is not enough under CI load, so test_latest_position_wins_under_burst and test_newer_submission_coalesces_older_one flaked on slow runners (macos-py3.10: "assert None is not None"). Poll for the observable end state (current_data on (5,5); both positions in `computed`) after _wait_idle instead of trusting that sleep.
…d controls The strain window's Region/CIF/R1-R10 buttons were dead: the window is created from a bare `figure` message (not a registered Plot), so _plot_by_window_id returned None and every strain_set_* handler silently no-oped (the controller was only findable via the source tree's plot). Resolve the StrainController by window_id from a module registry (_CONTROLLERS, populated in attach) so the staged handlers work regardless of plot registration. Reworked controls per design: - Reference is now ALL reference spots with the zero/direct beam excluded (_zero_beam_filtered, |g| > 25% of median). Removed the R1-R10 ring toggles entirely (backend set_rings/_emit_rings, the strain_rings IPC + renderer STRAIN_RINGS state/reducer/prop). - Region/CIF folded into a Method caret (a <select>): Region = relative (crosshair pixel), CIF = absolute (ideal spacings; prompts for the .cif). New strain_set_method handler. - Submit button commits the live field as a new SignalTree — εxx is the signal plot, εyy/εxy/ω are chip-selectable view figures (mirrors the Vector-OM result window via spyde.actions.views). New strain_commit handler + _commit_strain_tree. Tests: reference-excludes-zero-beam, commit-makes-new-tree (resolves controller by window_id with plot=None). strain_lazy.spec.ts asserts the method caret + Submit and that Submit opens a committed window.
…lections on reference change
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This pull request makes several significant improvements to the build, release, and Electron app infrastructure for SpyDE. The main highlights are the introduction of new CI jobs for TypeScript type-checking and Electron end-to-end (e2e) tests, secure handling of figure and icon resources in the Electron app, and enhancements to release and packaging workflows. It also updates documentation to reflect the new release process and licensing..