Skip to content

Desktop workbench overhaul: toolbar options, input/constants/result redesign, design-token pass#84

Open
yilibinbin wants to merge 139 commits into
mainfrom
feat/toolbar-options-popup
Open

Desktop workbench overhaul: toolbar options, input/constants/result redesign, design-token pass#84
yilibinbin wants to merge 139 commits into
mainfrom
feat/toolbar-options-popup

Conversation

@yilibinbin

@yilibinbin yilibinbin commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Summary

Desktop GUI overhaul for DataLab's workbench: toolbar-driven options, a redesigned
input/constants/result layout, on-demand LaTeX, a full design-review token pass, plus a
batch of UX polish and bug fixes accumulated on this branch. One Python core, two frontends
— desktop changes only; the web app and compute layers are untouched.

Highlights

Layout / workbench

  • Two-pane workbench (config+data | result) with a result-heavy ~1:3 default ratio.
  • Input data + constants merged into 输入数据 / 常数 sheet tabs; each tab self-contained.
  • Input-area expand/collapse toggle with a smooth 220ms splitter animation.
  • Left column unified into [输入数据] + [one config card] per mode; mode selector on the toolbar.
  • Result overview → compact clickable toolbar status chip; history moved to a toolbar popup.

Input / constants UX

  • Removed the 使用数据文件 checkbox — a non-empty file path now takes precedence over the
    manual editor (which greys/hides live when a path is entered). Same for constants.
  • Constants card gained a 输入常数 title, an N 行 row-count, and a ? help button (matching the
    data card); both input cards now carry a consistent border + padding.
  • Excel-like block copy (Ctrl/Cmd+C → TSV) in the data/constants tables.

LaTeX / result

  • On-demand 生成 TeX / 预览 PDF (no generate-checkbox); tex-rebuild inputs persist in the
    workspace so it works after reopen without recomputing.
  • 不确定度位数 moved to the result panel with live re-render.
  • Fitting model line honours the digit / scientific toggles; long compile-error dialogs no longer
    push OK off-screen; result font-size control live-updates.

Feature removal

  • Removed the 单位标注 (units) feature end-to-end on the UI side (not general enough); the
    display-only backend is left as harmless dead code and old workspaces still open.

Design-review token pass (theme.py)

  • Converged 3–4 near-duplicate hexes per role into semantic tokens
    (text_primary/text_muted/border/card_bg/region_bg/surface_raised/surface_hover).
  • Radius collapsed to a 2-tier scale (RADIUS_CARD=8 / RADIUS_CONTROL=6); one canonical
    CARD_PADDING; formula-preview border fixed; fixed-white tutorial card text pinned dark.

Precision / workspace correctness

  • Lossless mpf workspace serialization (was two-sided precision loss on reopen).
  • Guarded workspace capture against missing data/constants files.

Review gates already passed on this branch

  • Three-model serial adversarial review (Claude → Codex → Gemini) across multiple rounds;
    all confirmed findings fixed (mpf precision, theme-toggle stale styles, animation-fight,
    tutorial contrast, file-precedence live feedback, …). Gemini's agy channel timed out on the
    last two rounds — its lens was covered by direct code inspection and noted as such.
  • /simplify 4-agent cleanup pass (reuse / simplification / efficiency / altitude): radius
    convergence finished, per-keystroke constants refresh gated, dead widget removed.
  • Iterative user acceptance on the live GUI throughout.

Test plan

  • QT_QPA_PLATFORM=offscreen pytest -q — full suite green (a couple of files run separately due
    to an offscreen Qt-teardown hang unrelated to these changes).
  • Per-round targeted suites (theme tokens/spacing, two-pane layout, input/constants tabs,
    constants editor, formula preview, workspace controller, option reachability) all pass.
  • Regression tests added for: file-precedence dimming, expand/collapse restore, formula-preview
    pixmap inset, constants row-count + control-row placement, theme-toggle restyle, mpf round-trip.

Requesting

CodeRabbit review before merge to main.

Summary by CodeRabbit

  • New Features
    • On-demand LaTeX generation with a non-modal preview dialog (TeX source + compiled PDF).
    • History panel now collapses by default, with a toolbar popup; added a compact result overview popover and an always-visible result status strip.
    • Desktop toolbar option controls now open real non-modal dialogs.
  • Bug Fixes
    • Excel-like block copy (Ctrl/Cmd+C → TSV) for table selections; improved formula preview border visibility and refreshed embedded constants/editor styling.
    • Web: fixed docs TOC heading anchors; added rate-limiting for heavy compute POST requests.
  • Refactor
    • Merged two-pane workbench layout with improved resize/height behavior.
    • LaTeX numeric digit grouping supports native vs app-side grouping (engine-adaptive where available); removed TeX/PDF result tabs.

Aaronhaohao and others added 30 commits July 3, 2026 09:34
…s (SWARM P0 F01/F02)

The deployment docs told operators to run `gunicorn/waitress app_web.server:app`,
but app_web/server.py exposes no module-level `app` — only the create_app()
factory — so every documented production command crashed at startup with
"Failed to find attribute 'app'". Fixed across all 5 deployment surfaces
(docs/web/deploy.en|zh.md, docs/DATALAB_WEB_GUIDE.en|zh.md, gunicorn.conf.py)
by switching to the app-factory form: gunicorn `'app_web.server:create_app()'`,
waitress `--call app_web.server:create_app`.

The docs also recommended multi-worker (`-w 4`/`-w 9`) without noting that the
SSE rate-limiter (_RATE_HISTORY) and collab session registry are per-process
in-memory state. Rather than force single-worker (which would serialize ALL
mpmath compute — mpmath is process-global and serialized by _MP_SERIAL_LOCK, so
gunicorn.conf.py deliberately floors workers at 2 and sse.py notes "scale by
processes, not threads"), the multi-worker recommendation is KEPT and the
trade-offs are now documented honestly: the DoS rate budget is per-worker
(≈ RATE_MAX_REQUESTS × workers, or enforce a strict global cap at nginx), and
multi-worker collaboration needs sticky sessions plus a shared store (Redis).

Adds tests/test_deploy_docs_wsgi_targets.py: parses all deployment surfaces,
asserts every documented WSGI target resolves to a Flask app via
werkzeug.import_string, and forbids the bare `app_web.server:app` from
reappearing.

Reviewed: Codex + Gemini 3.1 Pro adversarial (both PASS); full suite 3834 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Multi-dimensional swarm review of the whole package (11 Claude dimension
reviewers + Codex external pass), every finding adversarially verified by 2
independent skeptics (86 candidates → 56 survived), then re-verified line-by-line
against the code (0 overturned) and passed a Codex + Gemini 3.1 Pro external
adversarial review. F01/F02 are marked fixed (landed in the preceding commit).

Analysis document only — the fixes it recommends land as separate changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…specs

Section 1 (architecture) and Section 2 (5-batch implementation plan) for the
desktop Adaptive Workbench redesign. Both passed external dual-model adversarial
review (Codex + Gemini 3.1 Pro): Section 1 over 4 rounds, Section 2 over 5 rounds,
each finding fixed and re-verified against code to zero findings.

Section 1: keep the 3-pane QSplitter; icon rail in a new inner content HBox
wrapping [icon_rail | splitter]; pane 0 hosts a CurrentPageStack (page 0 =
existing config); result rail stretch 0->1; fold via setVisible(False).

Section 2 batches: (1) shell scaffold, zero test breakage; (2) control migration
+ frequency tiering; (3) F19/F04 layout-coupled run/stop fixes; (4) fold-to-widen
+ focus mode + layout memory; (5) polish (F03/F14/empty-state). Each batch: own
worktree, TDD, ruff/mypy, dual-external review, full suite, user-confirmed merge.

Plan/analysis documents only — no production code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… strip

Replaces the abandoned page-switch sidebar with a menu-bar approach that keeps
every config control in place (never reparented/hidden), per a 3-round dual-model
design review.

- menu_options.py: adds icon-ified 计算 (精度 + 并行/资源) and LaTeX menus after
  文件 in build_menu; icons on all existing menus. Menu items are NAVIGATION
  (reveal gate → ensureWidgetVisible → setFocus on the SAME rail widget) plus
  two-way signal-synced checkable QActions for checkboxes (blockSignals-guarded).
  latex_engine_combo deliberately omitted — it is a result-output control that
  lives in the LaTeX result tab and is only reachable after a result exists.
- result_overview_popover.py: a top-level QWidget(Qt.WindowType.Popup) opened on
  clicking the overview card, showing method/value/uncertainty/elapsed/#points
  read from the same state; auto-closes on click-outside. Existing overview
  widgets are NOT reparented.
- result_status_strip.py: a minimal always-visible footer strip (status badge +
  method + elapsed) driven by the shared result-rail refresh; theme.py extended
  so the strip's status badge gets the same colored pill as the overview badge.

Adds tests/test_desktop_option_reachability.py — the hard acceptance criterion:
every config control is reachable via its visible gate, asserted with
isVisibleTo(window) AND unchanged parent() (proven to fail if a control is
hidden). This guards the exact hiding-bug class that sank the prior approach.

Invariants: single parent per control (no reparent/duplicate), no mixin/MRO
change, no shared/ui_specs change, file-size ratchet respected (panels.py 2194 <
2207; new modules <800).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, complete reachability coverage

External dual-model review (Codex + Gemini) found 4 confirmed issues, all fixed:
- Gated checkbox menu actions (dcolumn/caption, gate=latex) toggled a HIDDEN
  control without revealing its gate. _bind_check_action now reveals the gate
  (checks generate_latex_checkbox) on trigger so the control becomes visible —
  no more operating an unseen control. Two-way blockSignals sync intact.
- Reachability test was masking: it forgot caption_edit, use_file_checkbox,
  data_file_edit, manual_table, and the constants control. Added coverage (each
  asserts isVisibleTo + unchanged parent after its real gate). The named
  use_constants_file_checkbox/constants_file_edit don't exist on the live window
  (dead hasattr refs) — covered the real input_constants_editor instead.
- latex_input_precision_spin (输入列位数) was missing from the LaTeX menu; added.
- Removed the unimplemented result_numeric gate docstring; display_digits/
  scientific stay in the result numeric tab (like latex_engine_combo), covered
  by the result-tab reachability test.

Verified non-masking: hiding caption_edit's reveal makes the reachability test
fail (independently reproduced).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
EOF
…-bound controls

Codex review found the hand-written reachability test still masked per-mode
schema-bound controls (power_p_edit, uncertainty_combo, all statistics.* etc.) —
~89 input controls carry datalab_schema_key but only ~20 were listed.

Rewritten to enumerate every schema-bound input widget (QLineEdit/QSpinBox/
QComboBox/QCheckBox/QPlainTextEdit) from the live tree and sweep each for
reachability in its mode/gate: switch to the owning mode, sweep single + pairwise
gate selectors, assert isVisibleTo(window) AND unchanged parent(). Result-only
controls (results.*/latex.engine, hidden until a result exists) asserted as
result-gated. Empty unreachable-allowlist — all 89 are reachable, no production
change. A baked-in test_non_masking_guard force-hides a control and asserts the
sweep fails, so the test provably catches the hiding-bug class on every run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pinBox)

Codex review found the type filter excluded QDoubleSpinBox, masking
pdf.zoom_percent (a live QDoubleSpinBox bound to that key). Fixed: filter is now
QAbstractSpinBox-based (covers QSpinBox + QDoubleSpinBox) plus combo/checkbox/
lineedit/plaintextedit. Enumeration 89 -> 90 (adds pdf.zoom_percent). pdf.* is a
result-only prefix (PDF lives in the result tabs, hidden pre-result) so it is
asserted result-gated. Verified non-masking for the new type: force-hiding
pdf_zoom_spin makes the sweep fail.

QPushButton (command controls) and QTextBrowser (read-only display) are excluded
by a documented principled rule (not a silent type omission); they carry schema
keys for dispatch/display, not editable options, and are covered by their own
tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…stiveness guard

Gemini + my probe found the sweep still masked custom tabular editable inputs
(ParameterTable fitting.*.parameters, DetectedRowsTable root.unknowns,
ConstantsEditor *.units.*). Fixed: _INPUT_TYPES is now capability-based —
Qt value inputs (base classes) PLUS the app's custom editors (ConstantsEditor,
ParameterTable, DetectedRowsTable), imported by class. Enumeration 90 -> 101,
all reachable in their mode (units editors gated by units.enabled/units.mode,
revealed by the pairwise sweep). Empty unreachable-allowlist.

Adds test_no_editable_schema_type_is_excluded: asserts every excluded
schema-keyed type is in a CLOSED _NON_INPUT_SCHEMA_TYPES set
(QLabel/QPushButton/QScrollArea/QTabWidget/QTextBrowser — all non-input, all
documented). A future editable widget type now FAILS this guard instead of
slipping past — ending the type-omission masking class structurally.

Verified non-masking: force-hiding a ParameterTable fails the sweep; injecting a
new editable type (QDateEdit) is flagged unclassified by the guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codex found the shared input_constants_editor is relabelled at runtime
(fitting.custom.constants -> fitting.implicit.constants when fit_model_combo ==
self_consistent), and the sweep captured (widget,key) once before sweeping so it
never observed the dynamic key state. The widget itself is reachable (visible in
that mode) — this was a test-coverage gap, not a hidden control.

Fix: _record() now re-reads each widget's LIVE datalab_schema_key at record time,
so a key that only exists mid-sweep is covered. _DYNAMIC_KEYS_BY_MODE lists the
known runtime-relabelled keys and the per-mode test asserts each becomes reached
during the sweep. Verified meaningful: disabling the live-key re-read makes the
fitting test fail.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…two-way sync)

User feedback: the menu items were navigation shortcuts (click → focus the config
rail), not editable in the menu. Now each value item is a QWidgetAction hosting a
NEW mirror widget (QSpinBox/QComboBox/QLineEdit matching the real control's
range/items) that two-way-syncs to the real in-rail control via signals with
recursion guards. The real control STAYS in the config rail (no reparent →
single-parent invariant + reachability test intact) and the menu shows an
editable copy.

- New menu_option_editors.py: builds the QWidgetAction mirrors + recursion-safe
  sync (mirror→real keeps real's signals live so downstream slots run; real→mirror
  blockSignals-guarded). Gated LaTeX editors reveal_gate before applying. Combo
  mirrors register with the real combo's i18n spec so bilingual relabel works.
- menu_options.py: value items become editors at wire time; checkboxes stay as the
  existing two-way checkable QActions.

Verified: setting a menu mirror changes the real control and vice versa (spin +
combo), no recursion, real controls not reparented (reachability test still 18
passed). Full suite 67 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… (not a silent set)

Gemini review found test_precision_mirror_drives_real_downstream_slot was
trivially true — it asserted the spin's own value (which a silent set also
satisfies) instead of the downstream effect it claims to verify. Fixed: spy on
the real control's valueChanged and assert it FIRED when the mirror was edited,
proving downstream schema/UI slots run. Verified non-trivial: making the
mirror->real set silent (blockSignals) makes the test fail.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The card click filter fired on ANY MouseButtonRelease, so a right- or
middle-click on the overview card spuriously opened the popover. Guard on
Qt.MouseButton.LeftButton. Adds a regression test that drives left/right/
middle releases through the filter and asserts only left opens it (proven to
fail on the unguarded form).

CodeRabbit finding 1 (QAction.NoRole -> scoped MenuRole.NoRole) intentionally
skipped: the whole codebase (13 sites in panels.py) uses the unscoped form;
a lone scoped call here would break consistency (Rule 8). A scoped-enum sweep
is a separate change if desired.
…m off-window QMenu

Replaces the icon-menubar approach (options landed in macOS system menu bar,
off-window; left 选项 panel stayed, no space freed). Dual-model VERDICT: FRAME
(QMenu breaks nested QComboBox; QFrame(Qt.Popup) doesn't). Real controls move
from options_box into two in-window toolbar dropdown popups (计算/LaTeX),
shrinking the rail so the result area maximizes.
…ERDICT: INLINE)

Second adversarial round (Codex+Gemini, live offscreen probes) reversed the
QFrame(Qt.Popup) choice: a QComboBox inside any Qt.Popup can be dismissed by the
macOS Cocoa grab when its dropdown opens — invisible offscreen (always passes CI,
only fails in production on Mac) = unacceptable. INLINE = a normal QWidget child
toggled by the toolbar button: no grab, isVisibleTo(window) meaningful, stable
parent, trivial reachability gate. Row inserts at root_layout index 1 (between
toolbar and splitter).
…选项 rail panel

Low-frequency options (precision, parallel, LaTeX, plots/verbose) move OUT of
the left-rail 选项 QGroupBox INTO two inline toggle panels dropped under the
toolbar, toggled by checkable 计算/LaTeX toolbar buttons next to 运行/停止. The
left rail shrinks so the result area maximizes — the user's original request.

Mechanism = INLINE (dual-model VERDICT: INLINE), NOT a floating Qt.Popup: a
QComboBox inside a Qt.Popup can be dismissed by the macOS Cocoa grab when its
dropdown opens — a bug invisible offscreen (always passes CI, only fails on Mac).
A normal QWidget child toggled setVisible has no grab, keeps isVisibleTo(window)
meaningful and the parent stable, so the reachability sweep just gains a trivial
open-panel gate.

- NEW app_desktop/workbench_options_panel.py: build_options_panel + bind_options_toggle.
- workbench_toolbar.py: two checkable 计算/LaTeX buttons.
- panels.py: reparent the REAL controls (schema keys/signals preserved) into the
  panels; options_panels_row inserted at root_layout index 1; options_box detached
  from the rail (attr kept for the legacy shell-layout test).
- DELETE the off-window QMenu approach (menu_options.py, menu_option_editors.py + 2 tests).
- Tests: new panel behaviour suite; reachability sweep gains an open-panel gate;
  global-options schema audit repointed at the panels; shell-layout contract adds
  the two buttons and drops the stale rail-geometry assertion for the moved spin.

Full desktop suite: 769 passed. ruff clean.
… review — Codex)

Serial adversarial review (Claude→Gemini→Codex) found a real masked gate:
tools/scan_desktop_gui_schema.py still audited window.options_box, which is now
empty (controls moved into the 计算/LaTeX toolbar panels). A required-but-unbound
widget in a panel passed the release schema-scan silently. Repoint the scan at
compute_options_panel + latex_options_panel; add a regression test that strips a
required widget's schema key and asserts the scan flags it (proven to fail against
the old options_box-pointed scanner).

Adjudication: Gemini's 'options_box leaks OS window handles' finding is REFUTED —
a never-shown unparented QGroupBox has WA_WState_Created False and no native
handle (Claude + Codex probes agree). The orphan is a benign dead attr (kept: it
is also registered in _translations, so removal needs more than deleting the attr;
not worth the churn for a harmless empty widget).
…t (CodeRabbit)

The points label used the column count as a fallback whenever the row count was
falsy — so an empty 0-row/N-col tabular result displayed N points instead of 0.
Show the actual row count for tabular states (including 0); keep the column-count
fallback only for non-tabular states. Adds a regression test (proven RED first).
…s (CodeRabbit)

The selector sweep toggled every combo/checkbox, including ones hidden inside a
collapsed options panel — which could mark a downstream control reachable via a
gate the user cannot see (a masking risk). Guard each selector toggle on
isVisibleTo(window) AND isEnabled(). Today harmless (panel combos are leaf
controls gating nothing, and are covered as reachable via the panel-open gate in
the mode-independent test), but this makes the sweep model true user-operability.

Skipped CodeRabbit's paired-order (combinations→permutations) suggestion: an
empirical run showed permutations reaches the identical 32 keys for this UI —
zero coverage gain for double the pairwise runtime.

Full reachability suite: 18 passed (incl. non-masking guard).
…dius

Serial adversarial (Codex FAIL → Gemini FAIL, both on incomplete test audit) +
two independent lead sweeps enumerate the full 3-pane assumption set:
- app code: visual_contract (3-pane), _refresh_main_splitter_left_min_width +
  left_layout aliases (config-rail-anchored), workbench_layout splitter.
- tests: workbench_layout, mode_stack, shell_layout (mode_section order),
  workbench_data_area (mode_section parent), splitter_persistence (invert the
  stale-blob test), workbench_visual_contract (rewrite to 2 regions), screenshots.
Design confirmed sound: merged pane = workbench_workspace_* as left source of
truth, config_rail compatibility-only, mode_combo → toolbar. Ready for TDD.
…2-pane)

The 计算模式 selector (mode_combo) moves from the left-rail mode_section card
onto the in-window toolbar (left, after the DataLab identity label, before 新建).
The toolbar reserves a _toolbar_mode_slot; panels.py inserts the SAME mode_combo
into it once created. mode_section/mode_box kept as detached compatibility attrs,
no longer added to the config rail. _on_mode_change wiring unchanged — verified
all 5 modes still switch mode_stack (live probe + test).

Tests: new test_desktop_mode_selector_on_toolbar.py (mode on toolbar, left of 新建,
5-mode stack switch, mode_section not a rail card); updated shell_layout +
workbench_data_area to the new rail order [input, output_setup, run].
The left config rail merges into the workspace canvas so the splitter holds two
panes: [输入 + 配置, vertically stacked | 结果]. The merged pane is the new
left-pane source of truth; workbench_config_rail/_content survive only as detached
compatibility attributes (never a splitter pane).

App changes:
- workbench_layout.py: build_workbench_main_splitter → 2 panes (workspace | result).
- panels.py: left_layout/left_container/_left_scroll re-anchored to the merged
  workspace pane; sections stacked 输入(top) → 配置(formula/mode_stack) →
  output_setup → run(bottom); _refresh_main_splitter_left_min_width rewritten for
  2 panes and floored at WORKSPACE_CANVAS_MIN_WIDTH (the merged pane holds both
  input and config, so it needs the wider workspace minimum — this fixes a real
  visual-contract violation the GUI scan caught).
- workbench_visual_contract.py: rewritten to a 2-region contract (merged + result;
  drops the config region and the 3-way order assert).
- tools/scan_desktop_gui_schema.py: horizontal-scrollbar gate scans the merged pane.

Tests: new test_desktop_two_pane_layout.py (2 panes, input-above-config,
config_rail-not-a-pane, 2-pane visual contract, merged-pane min-width); updated
the cataloged 3-pane assertions across workbench_layout, mode_stack,
splitter_persistence (stale 3-pane blob now rejected by the 2-pane window),
visual_contract (rewrite), screenshots, redesign_scan, shell_layout,
workbench_data_area, root_solving_ui. Full desktop suite green (832 passed).
…eRabbit)

The compute/latex options-panel audit passed vacuously when a panel attribute was
absent (only present-but-empty panels were checked). A refactor that dropped a panel
entirely would slip through. Now a missing panel attribute is reported as a
schema_binding issue, like an unbound required widget.
4-module change: (1) LaTeX preview dialog with TeX/PDF tabs + copy/save-to-file;
(2) tectonic-only compile (drop local-tex fallback + engine combo); (3) compute +
LaTeX options as QDialogs (real widgets reparented into the dialog, output-path field
removed); (4) result-panel cleanup — 生成TeX/预览PDF buttons replace the TeX/PDF
result tabs, delete redundant bottom 开始执行 + empty output_setup_section, collapse
history by default. Options dialogs hold the REAL schema-keyed widgets (reachability
test forbids hidden state-holders); the LaTeX-preview window uses new display widgets
reusing render/gen logic. Verified against live probe + code.
… findings)

Codex FAIL → all 5 grounded findings addressed:
1. PDF reuse: _render_pdf_preview is coupled to main-window state (zoom/dpi/
   container/last_pdf_path/tab-select) → spec now mandates a PURE pdf_to_images
   helper + dialog-owned render state.
2. results.latex.source is a schema key, not a payload; modes are file-first and
   skip tex when output_path empty → temp-path resolution promoted to MANDATORY.
3. Resolved the spec's fresh-widget vs reparent contradiction: options dialogs
   reparent REAL widgets; the LaTeX-preview window uses fresh widgets + reused logic.
4. Expanded the deletion blast radius (run_button shortcuts/state/lang-restore,
   latex_engine_combo workspace capture/restore + schema, TeX/PDF tabs in
   _RESULT_VIEW_ORDER + scanner, output_setup_section in _config_card_sections).
5. Tectonic-only: auto-install via ensure_tectonic_installed (not the prompt-based
   path); remove worker fallback; rewrite test_desktop_latex_compile_ui.py:114 which
   asserts the old fallback.
…independence

Lead independent recon (while Gemini reviews): (a) shared/pdf_preview_raster.py:234
convert_pdf_to_images already exists — the pure PDF-render helper is wiring, not
net-new; tectonic-only applies to tex→PDF compile, the PDF→images preview rasterize
still uses pdftoppm/gs. (b) app_web has its OWN compile path (latex_security.py,
security.validate_latex_engine) and does NOT import shared/latex_engine.py's desktop
fallback — removing the desktop fallback is verified web-safe.
… schema bump)

Gemini hung 3x (infra failure, not a review signal) — substituted its role with lead
independent verification of its intended items: (2b) statistics/error modes thread
generate_latex/output_path identically → temp-path mandate covers all 5 modes; (3)
dropping latex_engine_combo's optional 'engine' field from workspace capture/restore is
backward-compatible on datalab.workspace.v2 (restore is already null-safe) — NO
schema_version bump; old .datalab with engine=pdflatex loads + compiles via tectonic.
Design gate: Codex FAIL (5 findings all folded) + independent verification = satisfied.
…odule 2)

compile_latex_to_pdf now always uses tectonic (via _ensure_latex_engine('tectonic'),
which resolves bundled/PATH/auto-installed tectonic) — no latex_engine_combo read, no
pdflatex/xelatex fallback, no local-TeX dependency. Missing tectonic surfaces a clear
'download/install failed, check network' error (no local escape hatch). Removed the
now-dead _latex_compile_fallback_candidates + _resolve_latex_engine_no_prompt helpers;
the worker's fallback params are passed None (inert). Rewrote the 3 fallback/explicit-
engine tests to assert tectonic-only: uses tectonic even with local tex present, and
reports an error (starts no worker) when tectonic is unavailable. 19 latex tests pass.

Note: the latex_engine_combo WIDGET removal + workspace-schema cleanup is Module 3/4
(deletion blast radius); this commit makes the compile LOGIC tectonic-only.
…ule 3)

计算/LaTeX toolbar buttons now open resizable, non-modal QDialog windows instead of
inline toggle panels. Each dialog holds the SAME real option controls (reparented once
into the dialog's content), so the run pipeline keeps reading self.<widget> unchanged
and there are no hidden state-holders/mirrors. The reachability sweep opens the dialogs
(a QDialog child is isVisibleTo(window) only while shown) — verified live.

NEW app_desktop/options_dialogs.py (OptionsDialog + build/bind helpers + add_separator).
Deleted app_desktop/workbench_options_panel.py + tests/test_desktop_toolbar_options_panel.py
(superseded by tests/test_desktop_options_dialogs.py).

LaTeX output-PATH field removed from the options UI (path chosen at save-time in the
TeX window, Module 1): output_file_edit/output_browse_button kept as detached widgets
for the save code paths but stripped of their output.latex.path schema binding, so they
are not enumerated as reachable config inputs. Dropped the output_path_field/
output_browse_field specs + lbl_output param. Scanner + reachability gate + global-options
tests updated to the dialogs. 157 shell/data/mode/workspace tests pass; options-dialog +
reachability + schema-scan green; ruff clean.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
app_desktop/history_panel.py (1)

118-166: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reapply the collapse state after language changes _apply_language() rewrites title_label from the registered bare title, so the ▸/▾ prefix is lost on translation switches. Call _apply_history_collapsed() from the language-change path, or keep the arrow out of the registered title text.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app_desktop/history_panel.py` around lines 118 - 166, The collapse indicator
is being overwritten when `_apply_language()` refreshes `title_label`, so the
▸/▾ state is lost after a language switch. Update the language-change flow in
`HistoryPanel` to re-run `_apply_history_collapsed()` after
`_register_texts()`/title rewrites, or change `title_label` registration so it
does not include the arrow prefix; use `toggle_history_collapsed()`,
`set_history_collapsed()`, and `_apply_history_collapsed()` as the hooks to keep
the collapsed state and label in sync.
statistics_utils.py (2)

172-185: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Forward native_group_width in the statistics worker path.

generate_statistics_latex() defaults this to True, so app_desktop/workers_core.py will emit digit-group-size whenever grouped, non-dcolumn LaTeX is generated. The UI path already passes self._engine_supports_group_width(), but the worker path does not, so bundled Tectonic can still hit the unsupported key in background runs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@statistics_utils.py` around lines 172 - 185, The statistics worker path is
not forwarding native_group_width into generate_statistics_latex, so grouped
non-dcolumn LaTeX can still emit digit-group-size for bundled Tectonic. Update
the worker-side call in workers_core.py to pass the engine capability check
result, using the same _engine_supports_group_width logic already used by the UI
path, so generate_statistics_latex only enables native grouping when supported.

496-519: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Propagate the grouping fallback to the remaining statistics writers

generate_statistics_bootstrap_latex, generate_statistics_time_series_latex, and generate_statistics_hypothesis_latex still rely on siunitx for digit grouping; when native_group_width=False and latex_group_size != 3, they fall back to siunitx’s default 3-digit grouping instead of the requested width. Mirror the app-side fallback used by generate_statistics_latex/generate_statistics_latex_batches, and thread native_group_width through the worker callsite too if it still defaults to True.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@statistics_utils.py` around lines 496 - 519, The remaining statistics LaTeX
writers still ignore the requested grouping width when native_group_width is
disabled. Update generate_statistics_bootstrap_latex,
generate_statistics_time_series_latex, and generate_statistics_hypothesis_latex
to use the same app-side grouping fallback as generate_statistics_latex and
generate_statistics_latex_batches instead of relying on siunitx’s default
3-digit grouping, and make sure the worker callsite passes native_group_width
through where these generators are invoked. Use the existing symbols
_statistics_latex_preamble, latex_group_size, and native_group_width to keep the
behavior consistent.
🧹 Nitpick comments (15)
app_desktop/result_overview_popover.py (1)

90-90: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider using CARD_PADDING for layout consistency.

workbench_results.py was just refactored to use CARD_PADDING from theme.py, but this new popover uses hardcoded 12, 10, 12, 10. If the design-token pass is meant to be consistent across result-overview surfaces, consider importing and using CARD_PADDING here too (or a popover-specific token if the different padding is intentional).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app_desktop/result_overview_popover.py` at line 90, The popover layout is
still using hardcoded contents margins instead of the shared design token, which
breaks consistency with the recent padding refactor. Update the result-overview
popover setup in the relevant layout initialization to use CARD_PADDING from
theme.py, or a dedicated popover padding token if the spacing is intentionally
different, and make sure the import and margin assignment stay aligned with the
existing card-style surfaces.
app_desktop/window_statistics_mixin.py (2)

288-349: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate single/multi-batch LaTeX dispatch logic between run-time and on-demand paths.

generate_statistics_latex_on_demand (Lines 319-346) re-implements the same single-vs-multi-batch generate_statistics_latex/generate_statistics_latex_batches argument-building already in _run_statistics_mode (Lines 506-533). Both blocks were updated together for native_group_width this time, but future option additions risk drifting between the two copies.

Consider extracting a shared helper, e.g. _write_statistics_latex(display_batches, rows, sigma_rows, digits, output_path, use_dcolumn, group_size, native, value_col_joined, caption), used by both call sites.

Also applies to: 504-535

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app_desktop/window_statistics_mixin.py` around lines 288 - 349, The
single/multi-batch LaTeX dispatch is duplicated between
generate_statistics_latex_on_demand and the runtime path in
_run_statistics_mode, so future argument changes can drift. Extract the shared
single-vs-batch decision and generate_statistics_latex /
generate_statistics_latex_batches argument assembly into one helper, then call
it from both generate_statistics_latex_on_demand and _run_statistics_mode,
keeping native_group_width, caption, group_size, and value_col_joined handling
in one place.

318-318: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Repeated engine-capability lookup expression across 8+ sites.

self._engine_supports_group_width() if hasattr(self, "_engine_supports_group_width") else True is copy-pasted at every LaTeX generation call site in this file. Extracting a small helper (e.g. self._native_group_width_flag()) would centralize this so future changes to the fallback/probing logic don't need to be replicated everywhere.

♻️ Sketch
+    def _native_group_width_flag(self) -> bool:
+        return self._engine_supports_group_width() if hasattr(self, "_engine_supports_group_width") else True
+
     def generate_statistics_latex_on_demand(self) -> str | None:
         ...
-        native = self._engine_supports_group_width() if hasattr(self, "_engine_supports_group_width") else True
+        native = self._native_group_width_flag()

Also applies to: 519-519, 532-532, 641-641, 743-743, 1058-1058, 1289-1289, 1424-1424

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app_desktop/window_statistics_mixin.py` at line 318, Repeated
engine-capability probing for the group-width flag is duplicated across many
LaTeX generation call sites in window_statistics_mixin.py. Add a small helper on
the same class, such as a private method like _native_group_width_flag(), that
encapsulates the hasattr(self, "_engine_supports_group_width") fallback logic
and returns the computed boolean. Then update the call sites that currently
inline self._engine_supports_group_width() if hasattr(self,
"_engine_supports_group_width") else True to use the new helper instead, keeping
the behavior identical while centralizing the logic.
app_desktop/window_extrapolation_mixin.py (1)

827-831: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cross-module import of private helpers.

_input_units_for_headers/_result_unit_from_units are underscore-prefixed (private) helpers from .workers_core, imported directly into this UI mixin. Consider exposing them as public helpers (or moving to a shared units-utils module) to avoid coupling UI code to workers_core's private surface.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app_desktop/window_extrapolation_mixin.py` around lines 827 - 831, The UI
mixin is importing underscore-prefixed private helpers from workers_core,
creating tight cross-module coupling; update window_extrapolation_mixin to stop
relying on _input_units_for_headers and _result_unit_from_units directly. Move
the shared unit conversion/header logic into a public helper location or a
dedicated units-utils module, then import those public symbols here and keep
workers_core's private surface internal.
app_desktop/result_status_strip.py (1)

17-17: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider exposing _overview_state/_status_badge as public API.

Importing underscore-prefixed private functions from workbench_results creates a fragile cross-module coupling. If these are stable internal contracts shared within the package, consider dropping the underscore prefix or providing public wrapper functions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app_desktop/result_status_strip.py` at line 17, The import in
result_status_strip is reaching into underscore-prefixed private helpers from
workbench_results, which creates a fragile internal dependency. Update
workbench_results to expose _overview_state and _status_badge through a stable
public API, either by renaming them to public symbols or adding public wrapper
functions, and then switch result_status_strip to import and use those public
names instead.
datalab_latex/latex_formatting.py (1)

636-679: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting the shared digit-grouping loop.

The int-part (reversed) and frac-part (forward) loops both re-implement "insert sep every group_size digits" — same pattern already present in add_spacing_to_number above. A small shared helper (_group_digits(digits, group_size, sep, reverse)) would cut duplication across both call sites.

♻️ Suggested consolidation
+def _group_digit_string(digits: str, group_size: int, sep: str, *, from_left: bool) -> str:
+    seq = digits if from_left else digits[::-1]
+    chars: list[str] = []
+    for i, ch in enumerate(seq):
+        if i > 0 and i % group_size == 0:
+            chars.append(sep)
+        chars.append(ch)
+    grouped = "".join(chars)
+    return grouped if from_left else grouped[::-1]
+
 def group_digits_both_sides(number_str: str, group_size: int, sep: str = "\\,") -> str:
     ...
-    grouped_int_chars: list[str] = []
-    for i, ch in enumerate(reversed(int_part)):
-        if i > 0 and i % group_size == 0:
-            grouped_int_chars.append(sep)
-        grouped_int_chars.append(ch)
-    grouped_int = "".join(reversed(grouped_int_chars))
+    grouped_int = _group_digit_string(int_part, group_size, sep, from_left=False)
     result = (sign or "") + grouped_int
     if frac_part is not None:
-        grouped_frac_chars: list[str] = []
-        for i, ch in enumerate(frac_part):
-            if i > 0 and i % group_size == 0:
-                grouped_frac_chars.append(sep)
-            grouped_frac_chars.append(ch)
-        result += "." + "".join(grouped_frac_chars)
+        result += "." + _group_digit_string(frac_part, group_size, sep, from_left=True)
     return result + (tail or "")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@datalab_latex/latex_formatting.py` around lines 636 - 679, The digit-grouping
logic in group_digits_both_sides duplicates the same “insert sep every
group_size digits” pattern used elsewhere, so extract that shared behavior into
a small helper (for example a private digit-grouping function) and reuse it for
both the reversed integer-part loop and the forward fractional-part loop. Keep
group_digits_both_sides focused on parsing sign/int/frac/tail and delegating to
the helper, and align the new helper with the existing add_spacing_to_number
implementation so the grouping behavior stays consistent.
app_desktop/theme.py (2)

258-287: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Formula preview dark background still hardcodes #1f2328 in two places.

formula_preview_surface_style and formula_preview_source_edit_style both special-case "#1f2328" for the dark background rather than resolving it through _tok(), duplicating the exact kind of per-function hex literal this token pass was introduced to eliminate. Consider adding a token role (e.g. formula_bg) so the two functions can't drift from each other.

♻️ Suggested fix
 _TOKENS: dict[str, tuple[str, str]] = {
     ...
     "surface_hover": ("`#eef2f7`", "`#303746`"),  # button/tab hover
+    "formula_bg": ("`#ffffff`", "`#1f2328`"),      # formula preview / source edit background
 }
 def formula_preview_surface_style(*, dark: bool | None = None) -> str:
     dark = is_dark_theme() if dark is None else bool(dark)
-    background = "`#1f2328`" if dark else _tok("card_bg", dark)
+    background = _tok("formula_bg", dark)
 def formula_preview_source_edit_style(*, dark: bool | None = None) -> str:
     dark = is_dark_theme() if dark is None else bool(dark)
-    background = "`#1f2328`" if dark else _tok("card_bg", dark)
+    background = _tok("formula_bg", dark)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app_desktop/theme.py` around lines 258 - 287, The formula preview styles
still duplicate a hardcoded dark background value, so update
`formula_preview_surface_style` and `formula_preview_source_edit_style` to
resolve the background through `_tok()` instead of embedding `#1f2328`. Add or
reuse a dedicated token role such as `formula_bg` in the theme token lookup so
both helpers share the same source of truth and cannot drift apart.

441-452: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Tab-corner radius hardcodes 6px instead of RADIUS_CONTROL.

Same function uses {RADIUS_CARD}px for the pane border a few lines up (line 437); the tab corner radii at lines 449-450 are literal 6px even though RADIUS_CONTROL = 6 exists for exactly this purpose.

♻️ Suggested fix
-    border-top-left-radius: 6px;
-    border-top-right-radius: 6px;
+    border-top-left-radius: {RADIUS_CONTROL}px;
+    border-top-right-radius: {RADIUS_CONTROL}px;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app_desktop/theme.py` around lines 441 - 452, The tab corner radius in the
input data tabs style is hardcoded instead of using the shared control radius
constant. Update the `build_theme`/theme stylesheet section that defines
`QTabWidget#input_data_tabs QTabBar::tab` to replace the literal corner radius
values with `RADIUS_CONTROL` (consistent with the nearby `RADIUS_CARD` usage),
so the tab corners follow the same theme token as other control elements.
docs/SWARM_REVIEW_2026.md (1)

262-269: 📐 Maintainability & Code Quality | 🔵 Trivial

Self-flagged truncation should be resolved before merge.

The doc explicitly notes the __main__/SocketIO finding's recommendation text was truncated in the source JSON and only "reasonably completed" by the report author, asking to "核对原始 finding" before relying on it.

Want me to help track this down as a follow-up so the recommendation text is verified against the original finding before this report is treated as final?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/SWARM_REVIEW_2026.md` around lines 262 - 269, The issue is that the
recommendation text for the `__main__` SocketIO finding was truncated and only
reconstructed from context, so it should not be treated as final. Verify the
original finding source and replace the placeholder wording in this SWARM review
entry with the exact recommendation text, keeping the `__main__`/SocketIO note
consistent with the rest of the report.
statistics_utils.py (1)

184-257: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated app_group/_maybe_group helper.

The same 6-line closure (app_group = ...; _maybe_group) is copy-pasted verbatim between generate_statistics_latex (196-201) and generate_statistics_latex_batches (341-346). Given this PR's own stated rationale elsewhere for centralizing near-duplicate siunitx logic ("5 near-duplicate inline copies... removing them all keeps the format drift-free"), the same argument applies here.

♻️ Extract a shared module-level helper
+def _maybe_group_cell(cell: str, app_group: bool, group_size: int) -> str:
+    from datalab_latex.latex_formatting import group_digits_both_sides
+    if app_group and "\\multicolumn" not in cell and "\\text" not in cell:
+        return "\\text{" + group_digits_both_sides(cell, group_size) + "}"
+    return cell

Then in both generate_statistics_latex and generate_statistics_latex_batches, replace the local closure with functools.partial(_maybe_group_cell, app_group=app_group, group_size=group_size) (or just call _maybe_group_cell(cell, app_group, group_size) directly at each call site).

Also applies to: 334-399

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@statistics_utils.py` around lines 184 - 257, The duplicated
app_group/_maybe_group closure should be removed and centralized so the siunitx
grouping behavior stays consistent across both statistics table generators.
Extract the shared grouping logic into a module-level helper, then update
generate_statistics_latex and generate_statistics_latex_batches to use that
helper instead of defining the same local closure twice; keep the existing
behavior for app_group, group_size, and _maybe_group call sites unchanged.
datalab_latex/sisetup_block.py (1)

26-53: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Docstring not updated for the new emit_digit_group_size parameter.

The Parameters section still only documents group_size and include_dcolumn; the new tri-state emit_digit_group_size (probe result vs. legacy heuristic) isn't described, even though it's a public function used across five call sites.

📝 Add a Parameters entry
     include_dcolumn:
         When ``True`` the document uses dcolumn for alignment; siunitx
         must NOT emit grouping characters or the alignment breaks. The
         block emits the minimal "no grouping" body in that case.
+    emit_digit_group_size:
+        ``True`` -> engine was probed and honours the key; emit unguarded.
+        ``False`` -> probed as unsupported; never emit.
+        ``None`` (default) -> no probe result; fall back to the legacy
+        ``\`@ifpackagelater``` date heuristic.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@datalab_latex/sisetup_block.py` around lines 26 - 53, Update the docstring
for build_sisetup_block to document the new emit_digit_group_size parameter
alongside group_size and include_dcolumn; add a Parameters entry explaining the
tri-state behavior (True emits digit-group-size, False omits it, None falls back
to the legacy heuristic) so the public API matches the implementation used by
the call sites.
app_desktop/table_copy.py (1)

35-42: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

No escaping for embedded tabs/newlines in cell text.

If any cell's text() contains a literal tab or newline, the TSV grid produced here will misalign columns/rows on paste. Low risk for numeric data but worth a defensive check if free-text columns are ever copyable through this path.

🧹 Optional: sanitize cell text before joining
         for col in range(left, right + 1):
             item = table.item(row, col)
-            cells.append(item.text() if item is not None else "")
+            text = item.text() if item is not None else ""
+            cells.append(text.replace("\t", " ").replace("\n", " "))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app_desktop/table_copy.py` around lines 35 - 42, The TSV export in the table
copy path does not escape embedded tabs or newlines in cell text, so pasted data
can misalign rows and columns. Update the copy routine that builds lines from
table.item(...).text() so each cell value is sanitized before joining, replacing
or escaping literal tab/newline characters consistently. Keep the fix localized
to the table copy helper that appends cells and writes to
QApplication.clipboard().setText.
app_desktop/workbench_variable_panel.py (2)

259-270: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Clean up dead "constants" role from _variable_summary_text.

Since _mounts_in_panel_order no longer includes constants, no section will ever have role "constants". The "constants" key in counts and its entry in the iteration tuple are dead code.

♻️ Proposed cleanup
-    counts: dict[str, int] = {"parameters": 0, "constants": 0, "unknowns": 0}
+    counts: dict[str, int] = {"parameters": 0, "unknowns": 0}
-    for role in ("parameters", "unknowns", "constants"):
+    for role in ("parameters", "unknowns"):
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app_desktop/workbench_variable_panel.py` around lines 259 - 270, The
`_variable_summary_text` helper still carries dead handling for the
`"constants"` role even though `_mounts_in_panel_order` no longer produces it.
Remove the unused `"constants"` entry from the `counts` map and drop
`"constants"` from the role iteration in `_variable_summary_text`, keeping only
the active roles so the summary logic matches the current section roles.

218-218: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove stale spec.constants from has_variables check.

Constants are no longer mounted in the variable panel (per _mounts_in_panel_order), so checking spec.constants here is semantically misleading. If a mode has only constants, has_variables is True but no sections are visible — the panel is hidden anyway after unnecessary iteration.

♻️ Proposed cleanup
-    has_variables = bool(spec and (spec.parameters or spec.tables or spec.constants))
+    has_variables = bool(spec and (spec.parameters or spec.tables))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app_desktop/workbench_variable_panel.py` at line 218, Update the
`has_variables` check in `workbench_variable_panel.py` to stop considering
`spec.constants`, since constants are no longer mounted by
`_mounts_in_panel_order`. Keep the visibility logic aligned with the actual
mounted sections by checking only the active variable sources used by the panel
(for example, parameters and tables), so `has_variables` reflects what can
really be shown and avoids the extra empty iteration path.
app_desktop/constants_editor.py (1)

146-168: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Deduplicate refresh_theme_style and set_embedded_in_workbench.

Both methods end with the identical setStyleSheet(constants_editor_style(embedded=...)) + unpolish/polish sequence. Have set_embedded_in_workbench delegate to refresh_theme_style instead of repeating it.

♻️ Proposed dedup
     def set_embedded_in_workbench(self, embedded: bool) -> None:
         embedded = bool(embedded)
         self.setProperty("datalab_constants_embedded", embedded)
         layout = self.layout()
         if layout is not None:
             # Embedded card now has its own border (like the data card) → pad content off it with
             # the shared CARD_PADDING; standalone keeps its tighter 8px inset.
             if embedded:
                 layout.setContentsMargins(*CARD_PADDING)
             else:
                 layout.setContentsMargins(8, 8, 8, 8)
-        self.setStyleSheet(constants_editor_style(embedded=embedded))
-        self.style().unpolish(self)
-        self.style().polish(self)
+        self.refresh_theme_style()

     def refresh_theme_style(self) -> None:
         """Re-apply the (theme-dependent) editor style for the current embedded state. Its style is
         otherwise set once at construction/embedding, so a live light↔dark toggle would leave the
         button colors stale — the theme refresh calls this."""
         embedded = bool(self.property("datalab_constants_embedded"))
         self.setStyleSheet(constants_editor_style(embedded=embedded))
         self.style().unpolish(self)
         self.style().polish(self)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app_desktop/constants_editor.py` around lines 146 - 168, The style refresh
logic is duplicated between set_embedded_in_workbench and refresh_theme_style in
ConstantsEditor. Refactor so set_embedded_in_workbench only updates the embedded
property and layout margins, then delegates to refresh_theme_style for the
shared setStyleSheet/unpolish/polish sequence. Keep refresh_theme_style as the
single place that reads datalab_constants_embedded and reapplies
constants_editor_style.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app_desktop/fitting_latex_writer.py`:
- Around line 115-131: The fallback grouping in fitting_latex_writer.py is
wrapping grouped values in \text{...}, which breaks rendering when app_group
emits them into a plain r column. Update the _maybe_group helper inside the
formatting function so it returns the result of group_digits_both_sides(cell,
_group) directly, keeping the existing multicolumn/text guards but removing the
extra \text wrapper.

In `@app_desktop/window_extrapolation_mixin.py`:
- Around line 728-867: The on-demand LaTeX generator methods can raise directly
and bypass the existing warning flow. Add try/except handling around
generate_root_latex_on_demand, generate_extrapolation_latex_on_demand, and
generate_error_latex_on_demand (or in
generate_latex_for_current_result/open_latex_preview) so malformed stashes or
bad paths are caught and reported via the same QMessageBox.warning path used by
_write_root_latex_if_requested, using the existing method names to keep the fix
localized.

In `@app_desktop/window_fitting_residuals_mixin.py`:
- Around line 518-576: The on-demand LaTeX regeneration helpers are creating a
fresh temporary file on every call via latex_output_path_for_run(True), which
leaks temp files across repeated refreshes. Update
generate_fitting_latex_on_demand and the other generate_*_latex_on_demand
methods it references to reuse or delete the previous per-kind temp path before
creating a new one, storing the last on-demand path on the instance and
unlinking it safely before requesting a replacement. Keep the fix localized
around latex_output_path_for_run, _load_latex_into_editor, and the
generate_fitting_latex_on_demand-style methods so repeated live option changes
do not accumulate files until window close.

In `@app_desktop/window.py`:
- Around line 3092-3125: The dispatch in generate_latex_for_current_result does
not treat the active statistics result as the preferred stash because
_last_result_kind uses statistics_single/statistics_batches while
generate_statistics_latex_on_demand reads the statistics stash key. Update the
routing so statistics result kinds resolve to the statistics stash key (or track
the active stash key separately) before checking builders, and keep the fallback
order unchanged so the current result always wins over older retained stashes.

In `@app_desktop/workspace_controller.py`:
- Around line 259-280: The capture logic in workspace_controller should decide
file-backed sections from path_text, not the use_file checkbox, so save/restore
stays consistent after a restore. Update the source_kind selection and the
file-read branch in the workspace capture path to treat any non-empty path_text
as the file source, matching the restore behavior that preserves the path even
when use_file_checkbox is unset. Keep the existing fallback to
manual_text/manual_table only when path_text is empty, and ensure the file
attachment is still captured whenever a valid path exists.

In `@tests/test_latex_inputs_serialization.py`:
- Around line 120-158: Add import guards for the Qt-based roundtrip test so it
skips cleanly when dependencies are missing: in
test_workspace_roundtrip_lets_generate_tex_work_after_reopen, add
pytest.importorskip checks for pytestqt and PySide6 before using qtbot or
importing ExtrapolationWindow. Keep the guard placement consistent with the
other Qt-dependent tests in this file so the test is only collected when the
required packages are available.

---

Outside diff comments:
In `@app_desktop/history_panel.py`:
- Around line 118-166: The collapse indicator is being overwritten when
`_apply_language()` refreshes `title_label`, so the ▸/▾ state is lost after a
language switch. Update the language-change flow in `HistoryPanel` to re-run
`_apply_history_collapsed()` after `_register_texts()`/title rewrites, or change
`title_label` registration so it does not include the arrow prefix; use
`toggle_history_collapsed()`, `set_history_collapsed()`, and
`_apply_history_collapsed()` as the hooks to keep the collapsed state and label
in sync.

In `@statistics_utils.py`:
- Around line 172-185: The statistics worker path is not forwarding
native_group_width into generate_statistics_latex, so grouped non-dcolumn LaTeX
can still emit digit-group-size for bundled Tectonic. Update the worker-side
call in workers_core.py to pass the engine capability check result, using the
same _engine_supports_group_width logic already used by the UI path, so
generate_statistics_latex only enables native grouping when supported.
- Around line 496-519: The remaining statistics LaTeX writers still ignore the
requested grouping width when native_group_width is disabled. Update
generate_statistics_bootstrap_latex, generate_statistics_time_series_latex, and
generate_statistics_hypothesis_latex to use the same app-side grouping fallback
as generate_statistics_latex and generate_statistics_latex_batches instead of
relying on siunitx’s default 3-digit grouping, and make sure the worker callsite
passes native_group_width through where these generators are invoked. Use the
existing symbols _statistics_latex_preamble, latex_group_size, and
native_group_width to keep the behavior consistent.

---

Nitpick comments:
In `@app_desktop/constants_editor.py`:
- Around line 146-168: The style refresh logic is duplicated between
set_embedded_in_workbench and refresh_theme_style in ConstantsEditor. Refactor
so set_embedded_in_workbench only updates the embedded property and layout
margins, then delegates to refresh_theme_style for the shared
setStyleSheet/unpolish/polish sequence. Keep refresh_theme_style as the single
place that reads datalab_constants_embedded and reapplies
constants_editor_style.

In `@app_desktop/result_overview_popover.py`:
- Line 90: The popover layout is still using hardcoded contents margins instead
of the shared design token, which breaks consistency with the recent padding
refactor. Update the result-overview popover setup in the relevant layout
initialization to use CARD_PADDING from theme.py, or a dedicated popover padding
token if the spacing is intentionally different, and make sure the import and
margin assignment stay aligned with the existing card-style surfaces.

In `@app_desktop/result_status_strip.py`:
- Line 17: The import in result_status_strip is reaching into
underscore-prefixed private helpers from workbench_results, which creates a
fragile internal dependency. Update workbench_results to expose _overview_state
and _status_badge through a stable public API, either by renaming them to public
symbols or adding public wrapper functions, and then switch result_status_strip
to import and use those public names instead.

In `@app_desktop/table_copy.py`:
- Around line 35-42: The TSV export in the table copy path does not escape
embedded tabs or newlines in cell text, so pasted data can misalign rows and
columns. Update the copy routine that builds lines from table.item(...).text()
so each cell value is sanitized before joining, replacing or escaping literal
tab/newline characters consistently. Keep the fix localized to the table copy
helper that appends cells and writes to QApplication.clipboard().setText.

In `@app_desktop/theme.py`:
- Around line 258-287: The formula preview styles still duplicate a hardcoded
dark background value, so update `formula_preview_surface_style` and
`formula_preview_source_edit_style` to resolve the background through `_tok()`
instead of embedding `#1f2328`. Add or reuse a dedicated token role such as
`formula_bg` in the theme token lookup so both helpers share the same source of
truth and cannot drift apart.
- Around line 441-452: The tab corner radius in the input data tabs style is
hardcoded instead of using the shared control radius constant. Update the
`build_theme`/theme stylesheet section that defines `QTabWidget#input_data_tabs
QTabBar::tab` to replace the literal corner radius values with `RADIUS_CONTROL`
(consistent with the nearby `RADIUS_CARD` usage), so the tab corners follow the
same theme token as other control elements.

In `@app_desktop/window_extrapolation_mixin.py`:
- Around line 827-831: The UI mixin is importing underscore-prefixed private
helpers from workers_core, creating tight cross-module coupling; update
window_extrapolation_mixin to stop relying on _input_units_for_headers and
_result_unit_from_units directly. Move the shared unit conversion/header logic
into a public helper location or a dedicated units-utils module, then import
those public symbols here and keep workers_core's private surface internal.

In `@app_desktop/window_statistics_mixin.py`:
- Around line 288-349: The single/multi-batch LaTeX dispatch is duplicated
between generate_statistics_latex_on_demand and the runtime path in
_run_statistics_mode, so future argument changes can drift. Extract the shared
single-vs-batch decision and generate_statistics_latex /
generate_statistics_latex_batches argument assembly into one helper, then call
it from both generate_statistics_latex_on_demand and _run_statistics_mode,
keeping native_group_width, caption, group_size, and value_col_joined handling
in one place.
- Line 318: Repeated engine-capability probing for the group-width flag is
duplicated across many LaTeX generation call sites in
window_statistics_mixin.py. Add a small helper on the same class, such as a
private method like _native_group_width_flag(), that encapsulates the
hasattr(self, "_engine_supports_group_width") fallback logic and returns the
computed boolean. Then update the call sites that currently inline
self._engine_supports_group_width() if hasattr(self,
"_engine_supports_group_width") else True to use the new helper instead, keeping
the behavior identical while centralizing the logic.

In `@app_desktop/workbench_variable_panel.py`:
- Around line 259-270: The `_variable_summary_text` helper still carries dead
handling for the `"constants"` role even though `_mounts_in_panel_order` no
longer produces it. Remove the unused `"constants"` entry from the `counts` map
and drop `"constants"` from the role iteration in `_variable_summary_text`,
keeping only the active roles so the summary logic matches the current section
roles.
- Line 218: Update the `has_variables` check in `workbench_variable_panel.py` to
stop considering `spec.constants`, since constants are no longer mounted by
`_mounts_in_panel_order`. Keep the visibility logic aligned with the actual
mounted sections by checking only the active variable sources used by the panel
(for example, parameters and tables), so `has_variables` reflects what can
really be shown and avoids the extra empty iteration path.

In `@datalab_latex/latex_formatting.py`:
- Around line 636-679: The digit-grouping logic in group_digits_both_sides
duplicates the same “insert sep every group_size digits” pattern used elsewhere,
so extract that shared behavior into a small helper (for example a private
digit-grouping function) and reuse it for both the reversed integer-part loop
and the forward fractional-part loop. Keep group_digits_both_sides focused on
parsing sign/int/frac/tail and delegating to the helper, and align the new
helper with the existing add_spacing_to_number implementation so the grouping
behavior stays consistent.

In `@datalab_latex/sisetup_block.py`:
- Around line 26-53: Update the docstring for build_sisetup_block to document
the new emit_digit_group_size parameter alongside group_size and
include_dcolumn; add a Parameters entry explaining the tri-state behavior (True
emits digit-group-size, False omits it, None falls back to the legacy heuristic)
so the public API matches the implementation used by the call sites.

In `@docs/SWARM_REVIEW_2026.md`:
- Around line 262-269: The issue is that the recommendation text for the
`__main__` SocketIO finding was truncated and only reconstructed from context,
so it should not be treated as final. Verify the original finding source and
replace the placeholder wording in this SWARM review entry with the exact
recommendation text, keeping the `__main__`/SocketIO note consistent with the
rest of the report.

In `@statistics_utils.py`:
- Around line 184-257: The duplicated app_group/_maybe_group closure should be
removed and centralized so the siunitx grouping behavior stays consistent across
both statistics table generators. Extract the shared grouping logic into a
module-level helper, then update generate_statistics_latex and
generate_statistics_latex_batches to use that helper instead of defining the
same local closure twice; keep the existing behavior for app_group, group_size,
and _maybe_group call sites unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8a1aab45-9fc4-4976-90e5-477f92c5b751

📥 Commits

Reviewing files that changed from the base of the PR and between 4aa10ea and 02adc4d.

📒 Files selected for processing (127)
  • app_desktop/constants_editor.py
  • app_desktop/current_page_stack.py
  • app_desktop/fitting_latex_writer.py
  • app_desktop/formula_preview.py
  • app_desktop/history_panel.py
  • app_desktop/history_popup.py
  • app_desktop/latex_inputs_serialization.py
  • app_desktop/latex_preview_dialog.py
  • app_desktop/message_dialogs.py
  • app_desktop/options_dialogs.py
  • app_desktop/panels.py
  • app_desktop/result_overview_popover.py
  • app_desktop/result_status_strip.py
  • app_desktop/root_latex_writer.py
  • app_desktop/shell_layout.py
  • app_desktop/table_copy.py
  • app_desktop/theme.py
  • app_desktop/views/error.py
  • app_desktop/views/fitting.py
  • app_desktop/views/helpers.py
  • app_desktop/views/root_solving.py
  • app_desktop/views/statistics.py
  • app_desktop/window.py
  • app_desktop/window_data_mixin.py
  • app_desktop/window_extrapolation_mixin.py
  • app_desktop/window_fitting_formatters_mixin.py
  • app_desktop/window_fitting_models_mixin.py
  • app_desktop/window_fitting_residuals_mixin.py
  • app_desktop/window_i18n_mixin.py
  • app_desktop/window_latex_compile_mixin.py
  • app_desktop/window_statistics_mixin.py
  • app_desktop/workbench_formula_panel.py
  • app_desktop/workbench_layout.py
  • app_desktop/workbench_results.py
  • app_desktop/workbench_toolbar.py
  • app_desktop/workbench_variable_panel.py
  • app_desktop/workbench_visual_contract.py
  • app_desktop/workers_core.py
  • app_desktop/workspace_controller.py
  • datalab_latex/latex_formatting.py
  • datalab_latex/latex_tables_common.py
  • datalab_latex/latex_tables_error_propagation.py
  • datalab_latex/latex_tables_extrapolation.py
  • datalab_latex/latex_tables_fitting.py
  • datalab_latex/latex_tables_root.py
  • datalab_latex/latex_tables_statistics_grouped.py
  • datalab_latex/latex_tables_statistics_matrix.py
  • datalab_latex/sisetup_block.py
  • docs/DATALAB_WEB_GUIDE.en.md
  • docs/DATALAB_WEB_GUIDE.md
  • docs/SWARM_REVIEW_2026.md
  • docs/superpowers/specs/2026-07-03-adaptive-workbench-section1-design.md
  • docs/superpowers/specs/2026-07-03-adaptive-workbench-section2-plan.md
  • docs/superpowers/specs/2026-07-04-iconified-menubar-design.md
  • docs/superpowers/specs/2026-07-04-toolbar-options-popup-design.md
  • docs/superpowers/specs/2026-07-05-latex-ondemand-4.2-integration-notes.md
  • docs/superpowers/specs/2026-07-05-latex-ondemand-4.4b-result-tabs-removal.md
  • docs/superpowers/specs/2026-07-05-latex-ondemand-4.4d-remove-generate-checkbox.md
  • docs/superpowers/specs/2026-07-05-latex-ondemand-refactor-design.md
  • docs/superpowers/specs/2026-07-05-latex-pdf-window-cleanup-design.md
  • docs/superpowers/specs/2026-07-05-two-pane-layout-design.md
  • docs/superpowers/specs/2026-07-06-engine-capability-grouping.md
  • docs/superpowers/specs/2026-07-08-left-config-card-unify-design.md
  • docs/web/deploy.en.md
  • docs/web/deploy.zh.md
  • gunicorn.conf.py
  • shared/latex_engine.py
  • shared/ui_specs.py
  • statistics_utils.py
  • test_probe_widget_tree.py
  • tests/test_constants_editor.py
  • tests/test_deploy_docs_wsgi_targets.py
  • tests/test_desktop_about_dialog.py
  • tests/test_desktop_bilingual_inventory.py
  • tests/test_desktop_custom_fit_ui.py
  • tests/test_desktop_engine_group_width.py
  • tests/test_desktop_error_propagation_ui.py
  • tests/test_desktop_example_workspace_menu.py
  • tests/test_desktop_global_options_ui.py
  • tests/test_desktop_gui_redesign_scan.py
  • tests/test_desktop_gui_schema_scan.py
  • tests/test_desktop_gui_workflows.py
  • tests/test_desktop_history_collapse.py
  • tests/test_desktop_history_toolbar_popup.py
  • tests/test_desktop_input_constants_tabs.py
  • tests/test_desktop_latex_compile_ui.py
  • tests/test_desktop_latex_ondemand_dispatch.py
  • tests/test_desktop_latex_ondemand_error.py
  • tests/test_desktop_latex_ondemand_extrapolation.py
  • tests/test_desktop_latex_ondemand_fitting.py
  • tests/test_desktop_latex_ondemand_root.py
  • tests/test_desktop_latex_ondemand_statistics.py
  • tests/test_desktop_latex_preview_dialog.py
  • tests/test_desktop_message_dialogs.py
  • tests/test_desktop_mode_selector_on_toolbar.py
  • tests/test_desktop_mode_stack.py
  • tests/test_desktop_option_reachability.py
  • tests/test_desktop_options_dialogs.py
  • tests/test_desktop_result_overview_popover.py
  • tests/test_desktop_result_schema_ui.py
  • tests/test_desktop_result_status_strip.py
  • tests/test_desktop_root_solving_ui.py
  • tests/test_desktop_shell_layout.py
  • tests/test_desktop_statistics_ui.py
  • tests/test_desktop_table_cell_copy.py
  • tests/test_desktop_theme_tokens.py
  • tests/test_desktop_toolbar_status_overview.py
  • tests/test_desktop_two_pane_layout.py
  • tests/test_desktop_workbench_data_area.py
  • tests/test_desktop_workbench_editor_canvas.py
  • tests/test_desktop_workbench_layout.py
  • tests/test_desktop_workbench_results.py
  • tests/test_desktop_workbench_state_ownership.py
  • tests/test_desktop_workbench_variable_panel.py
  • tests/test_desktop_workbench_visual_contract.py
  • tests/test_desktop_workbench_visual_screenshots.py
  • tests/test_file_size_ratchet.py
  • tests/test_fitting_latex_writer.py
  • tests/test_formula_preview_rendering.py
  • tests/test_latex_engine_adaptive_review_fixes.py
  • tests/test_latex_engine_capability.py
  • tests/test_latex_group_size_zero.py
  • tests/test_latex_inputs_serialization.py
  • tests/test_sisetup_block.py
  • tests/test_splitter_persistence.py
  • tests/test_workspace_controller.py
  • tools/scan_desktop_gui_schema.py
💤 Files with no reviewable changes (8)
  • app_desktop/views/statistics.py
  • tests/test_desktop_workbench_state_ownership.py
  • app_desktop/views/fitting.py
  • app_desktop/views/root_solving.py
  • tests/test_desktop_custom_fit_ui.py
  • tests/test_desktop_statistics_ui.py
  • app_desktop/views/helpers.py
  • app_desktop/views/error.py

Comment thread app_desktop/fitting_latex_writer.py
Comment thread app_desktop/window_extrapolation_mixin.py
Comment thread app_desktop/window_fitting_residuals_mixin.py
Comment thread app_desktop/window.py
Comment on lines +259 to 280
# Only claim source_kind="file" when there is an actual path (review P-C): use_file with an
# empty path used to tag the section "file" while capturing the manual table → inconsistent
# state with no attachment. Fall back to the manual kind so save/restore stay coherent.
_text_view = stack is not None and stack.currentIndex() == 1
source_kind = (
"file" if (use_file and path_text) else ("manual_text" if _text_view else "manual_table")
)
if constants and editor is not None and not use_file:
source_kind = "manual_text" if editor.using_text_view() else "manual_table"
canonical: dict[str, Any]
if use_file and path_text:
raw = Path(path_text).read_bytes()
# Guard the file read (review P-A): if the file was moved/deleted/unreadable since the
# user picked it, saving the workspace must NOT crash. Keep the source as "file" and the
# path (so the user can re-point it), but attach empty content rather than raising.
try:
raw = Path(path_text).read_bytes()
except OSError:
raw = b""
decoded_text, encoding = _decode_bytes(raw)
raw_path = f"attachments/sources/{section_name}.bin"
attachments[raw_path] = raw
canonical = {"rows": []}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Capture code must derive file source from path_text, not use_file_checkbox, to match file-precedence restore behavior.

The restore path (line 1926) unconditionally sets use_file_checkbox.setChecked(False) and keeps the file path only if the file still exists. But the capture path still gates on use_file (the checkbox): source_kind = "file" if (use_file and path_text) else ... and if use_file and path_text: for the file read.

After a restore where the file exists, the checkbox is False and the path is set. On the next save, the capture sees use_file=False, so it:

  1. Tags source_kind as "manual_table"/"manual_text" instead of "file".
  2. Does not read the file — no attachment is saved.

On a subsequent restore where the file has since been moved/deleted, the path is cleared and the workspace falls back to the manual editor's data (possibly empty or stale), with no file attachment. The restore comment claims "its CONTENTS were captured as an attachment on save → the workspace stays self-contained," but this only holds for the initial save — after a restore→save cycle, the workspace is no longer self-contained.

The fix is to gate the file source on path_text instead of use_file:

🔒 Proposed fix
     _text_view = stack is not None and stack.currentIndex() == 1
     source_kind = (
-        "file" if (use_file and path_text) else ("manual_text" if _text_view else "manual_table")
+        "file" if path_text else ("manual_text" if _text_view else "manual_table")
     )
     if constants and editor is not None and not use_file:
         source_kind = "manual_text" if editor.using_text_view() else "manual_table"
     canonical: dict[str, Any]
-    if use_file and path_text:
+    if path_text:
         # Guard the file read (review P-A): if the file was moved/deleted/unreadable since the
         # user picked it, saving the workspace must NOT crash. Keep the source as "file" and the
         # path (so the user can re-point it), but attach empty content rather than raising.

This aligns the capture with the restore's file-precedence model: the file path is the sole determinant of whether the section is file-backed, not the legacy checkbox.

A regression test for the second save-restore cycle (restore → save → delete file → restore) should be added to confirm the workspace remains self-contained.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app_desktop/workspace_controller.py` around lines 259 - 280, The capture
logic in workspace_controller should decide file-backed sections from path_text,
not the use_file checkbox, so save/restore stays consistent after a restore.
Update the source_kind selection and the file-read branch in the workspace
capture path to treat any non-empty path_text as the file source, matching the
restore behavior that preserves the path even when use_file_checkbox is unset.
Keep the existing fallback to manual_text/manual_table only when path_text is
empty, and ensure the file attachment is still captured whenever a valid path
exists.

Comment thread tests/test_latex_inputs_serialization.py
- [Critical] fitting_latex_writer: the app-side group fallback wrapped grouped values in
  \text{...} while emitting them into a plain r column → the \, thin-spaces broke TeX
  compilation. Return the grouped string directly (CodeRabbit).
- [Major] generate_latex_for_current_result: _last_result_kind is granular for statistics
  (statistics_single/_batches/_grouped/…) but they share the single "statistics" builder+stash;
  collapse any statistics* kind to "statistics" so the current result wins dispatch instead of
  falling through to a stale stash.
- [Major] open_latex_preview: wrap generate_latex_for_current_result() in try/except (Value/OS/
  Runtime) → surface a warning dialog instead of letting a builder exception escape the button slot.
- [Minor] latex_output_path_for_run(reuse=True): on-demand regeneration reused one temp .tex per
  session instead of leaking a fresh NamedTemporaryFile per call; run path still allocates fresh.
- [Minor] test_latex_inputs_serialization: importorskip pytestqt/PySide6 inside the one qtbot test
  so it skips cleanly (not fixture-not-found) without gating the non-Qt tests.

Refuted (verified against code): capture "not self-contained after restore→save" — the
_FilePathChecked shim makes use_file == (path present), so a restore-where-file-exists→re-save
still records source_kind=file + saves the attachment (reproduced: attachment present). 49 latex/
serialization/on-demand tests pass.
@yilibinbin

Copy link
Copy Markdown
Owner Author

Addressed the CodeRabbit review in 1d8496b. Disposition of each finding:

# File Verdict Action
1 fitting_latex_writer.py:131\text{} wrapper (Critical) Confirmed Return the grouped string directly (removed the wrapper).
2 window_extrapolation_mixin.py:867 — unguarded on-demand LaTeX (Major) Confirmed Wrapped generate_latex_for_current_result() in open_latex_preview with try/except → warning dialog.
3 window_fitting_residuals_mixin.py:576 — temp-file leak (Minor) Confirmed Added reuse=True to latex_output_path_for_run so on-demand regen reuses one temp .tex per session.
4 window.py:3125 — statistics stash key (Major) Confirmed Collapse any statistics* result kind to the statistics builder key in dispatch.
5 workspace_controller.py:280 — capture gates on checkbox (Major) Refuted The use_file_checkbox is a _FilePathChecked shim whose isChecked() == (a file path is present). So after a restore-where-file-exists → re-save, use_file is True, source_kind="file", and the attachment IS saved. Reproduced: the re-saved workspace still contains the file attachment (stays self-contained). CodeRabbit's premise (use_file=False after restore) reflects the old real-checkbox semantics.
6 test_latex_inputs_serialization.py:158 — missing importorskip (Minor) Confirmed Added importorskip("pytestqt")/("PySide6") inside the one qtbot test.

49 latex / serialization / on-demand-dispatch tests pass locally.

…laceholder (CI gate)

CI's runtime bilingual/accessibility gate (test_desktop_bilingual_inventory) failed: the new
data_file_edit / constants_file_edit QLineEdits had only a one-time placeholder and no
tooltip/accessibility affordance, so the gate flagged 48 "lacks tooltip/description/help" items and
24 "placeholder unchanged across zh/en" items. Registered a bilingual tooltip (via _register_text
setToolTip) and switched the placeholder to a registered bilingual text (setPlaceholderText) so it
re-translates on language switch. Gate passes.
…g modal

Root cause of the CI hang (test_desktop_workbench_results.py, serial): commit 5dc9f60 added
job.target_column / job.variable_map access to the fit-batches SUCCESS path (for on-demand-TeX
fidelity), but two test mock jobs didn't carry those attributes. That raised AttributeError in the
success path → the except branch showed a modal QMessageBox.critical, which BLOCKS FOREVER in
offscreen/headless CI (no one clicks OK) → the whole serial run hung with no timeout. (I'd been
masking this locally by --ignore-ing the file.)

Fixes:
- Added an autouse fixture that makes QMessageBox critical/warning/information non-blocking for
  every test in the file — a modal must never block a headless run.
- Gave the two affected mock jobs the target_column + variable_map the success-path stash reads,
  so they exercise their intended (post-processing-error / success) paths instead of failing early.
Full file: 66 passed, no hang.
Full serial CI run (now that the modal hang is fixed) surfaced 4 failures — 3 real quality-gate
violations from this branch's work + 1 order-dependent flake:
- precision guardrail: latex_inputs_serialization used mp.workdps() (forbidden — mp.dps is
  process-global). Switched both mpf-reconstruction sites to precision_guard(_MPF_RECONSTRUCT_DPS,
  clamp_max=MAX_MPMATH_DPS), the sanctioned guard; the S1 lossless round-trip still passes.
- file-size ratchet: theme.py crossed the 800-line soft limit during the design-review token pass
  (baselined it, +1 entry); window.py/panels.py/workspace_controller.py grew past their frozen
  god-file baselines from this feature's approved work (raised to current: 3467/2478/2130).
The formula-preview contrast test passed both in isolation and after these fixes — it was an
order-dependent flake in the long serial run, not a real failure.
…stale error

_update_theme_from_palette is a Qt slot wired to the app-global paletteChanged signal. In the full
test suite, stale ExtrapolationWindow instances from earlier tests stay connected to that signal, so
a later test's setPalette() fires their handlers too — and any window carrying a cached
formula-population RuntimeError re-raised it INTO the Qt event loop ("CALL ERROR: Exceptions caught
in Qt event loop"), failing an unrelated test. A palette change is purely cosmetic; the slot now
catches + logs instead of letting the exception escape. The real population error is still surfaced
at its own call site. Deterministic repro (formula_panel then preview_dialog): was 1 failed → 74
passed.
…m-independent)

CI failed on test_constants_tab_only_in_constant_using_modes: it asserts Chinese tab titles
(["输入数据","常数"]) but a fresh ExtrapolationWindow follows QLocale.system() — zh on a dev Mac,
en on Linux CI — so it got ["Data input","Constants"] on CI. The test never pinned the language.
The _window helper now applies "zh" so every title assertion in the file is deterministic
regardless of the host locale; the retranslate test re-applies "en" after and still passes.
Reproduced the CI failure locally via LANG=en_US.UTF-8 and confirmed the fix under that locale.
…, high)

mp.dps is process-global and each web compute route holds a serial lock while it runs, so an
unbounded precision field (e.g. mp_precision=100000000) set the process dps to 100M and stalled the
worker — a trivial unauthenticated DoS (CSRF token is freely obtainable; there is no auth). The
desktop worker and the SSE path already clamp; the direct compute POST routes did not.

Added _parse_precision() in app_web/logic/common.py that clamps at parse time to
[MIN_MPMATH_DPS, MAX_MPMATH_DPS], and routed the *_mp_precision field of all five compute logic
modules (extrapolation/error_propagation/statistics/root_solving/fitting) through it instead of the
unclamped _parse_int. Regression tests assert the clamp + that every route uses it.
…high)

The fixed-place LaTeX formatters ran at the AMBIENT mp.dps. The on-demand TeX rebuild (生成 TeX)
formats a stashed high-precision result AFTER the run's own precision_guard has closed, so the
ambient dps is the process default (~15) while `places` is 20-200 — the intermediate
mp.power(10,places) / value*factor products silently lost every digit past ~16, corrupting the
toolkit's headline high-precision guarantee (e.g. sqrt(2)@50 places off from digit ~17).

Rather than guard each of the ~dozen on-demand/web callers, fixed it at the ROOT: _round_to_places
and _format_fixed_places now floor their working precision to places + 12 guard digits via
precision_guard, so they are self-protecting regardless of the caller's ambient dps. Verified the
exact audit scenario (60-digit sqrt(2) formatted at ambient dps=15 now matches dps=60 and mpmath's
own reference). 207 latex/format tests pass.
…e save (audit A4, high)

capture_workspace wrote the tex-rebuild stash (_last_latex_inputs) into manifest[latex_inputs], but
the budget trimmer only ever shrank history — never latex_inputs. A high-dps fit with many points
encodes to several MiB, over the 2 MiB manifest budget, so write_workspace raised "manifest exceeds
size limit" and the ENTIRE workspace became unsaveable (risking loss of the user's work). Since the
stash is a best-effort convenience (optional on decode; the user re-runs to regenerate tex on
reopen), capture now measures the manifest and drops latex_inputs when it would exceed the budget,
so no valid save ever hard-fails. Small stashes are retained. Regression test added.
…, medium)

_compute_covariance already guards `noise = chi2/dof if dof > 0 else mp.nan`, but the caller passed
`dof if dof > 0 else 1`, pre-substituting 1 so the guard never fired: for a k-parameter model fit to
exactly k points (dof=0, exact interpolation, chi2~0) it computed noise=chi2/1~0 → sqrt(~0)~0, a
spuriously precise near-zero uncertainty. The linear auto_models path returns NaN in the same case.
Now passes the true dof so the covariance guard yields NaN uncertainties for an exactly-determined
fit, matching the linear path. Regression: 2-param fit to 2 points → dof=0 → NaN errors.
…A7, medium)

The heading-id injection used r"<(h[123])>(.+?)</\\1>" — in a raw string \\1 is a literal backslash+1,
so the closing-tag pattern was the literal text </\1>, which never matches real </h1>. No id
attributes were emitted, so every docs-page TOC in-page link (href="#slug") was dead. Changed \\1 to
a real backreference \1. Regression asserts heading ids are emitted and every TOC anchor resolves to
one.
…t-detail tabs (audit B5, medium)

input_data_tabs_style hardcoded four dark greys (#1c2129/#161a21/#222833/#2a313c) while its sibling
result_detail_card_style derives every surface from _tok(...). The P1 token pass missed these, so in
dark mode the 输入数据/常数 tab strip visibly mismatched the result-detail tab strip across the
splitter — the exact per-role divergence the token pass was meant to remove, and which this
function's own docstring claims it avoids ("mirrors the result-detail tab chrome"). Now resolves
panel_bg→card_bg, tab_bg→surface_raised, tab_hover→surface_hover (identical mapping to the detail
strip). Updated the theme test that pinned the old #1c2129 literal.
The 5 compute routes (/ /error /fit /stats /roots) had only @csrf_protect — no rate limit — while
each runs an mpmath computation holding a process-global serial lock, so an attacker hammering them
starves legitimate users (the SSE limiter covered only the two GET streaming routes). Added a
pages-blueprint before_request that throttles POST (the heavy path) per-IP, reusing the SSE
blueprint's battle-tested sliding-window limiter (and its TESTING / DATALAB_SSE_DISABLE_RATE_LIMIT
bypasses); GET form render is untouched. Over budget → 429. (A1 already bounded per-request cost by
clamping precision.) Also dropped the now-stale theme.py file-size baseline (B5 shrank it back to
800). Regression: POST throttled, GET not, TESTING-bypass preserved.
…he junk auxiliary (audit A6)

In mpmath's Wynn-epsilon table the ODD columns are non-convergent auxiliary entries that diverge to
huge junk magnitudes (~82 for a Leibniz pi/4 sequence). The cancellation_indicator used
|last_row[-2]| (the junk aux) and the 2-element-row error_estimate used |last[-1]-last[-2]| — so the
reported cancellation/error diagnostic was garbage, and (via model_selector) a 3-input auto-fit
could get a wildly inflated statistical uncertainty. Both diagnostics now derive from the proper
convergent gap |last[-1]-last[-3]|; a 2-element last row (3-input sequence) has no previous
convergent so neither is emitted (model_selector falls back to sqrt(noise)). Regression added.
…tative rows (audit A12)

A workspace saved in TABLE view stores both the rows and a (possibly stale) text-view draft.
_restore_constants_editor_state restored that draft via set_raw_text, which stamps it in-sync with
the table — defeating the editor's anti-stale guard, so a later table→text toggle surfaced the stale
draft instead of regenerating from the restored rows. Now the stored text is only restored when the
workspace was saved in TEXT view (where it is authoritative); in TABLE view the rows are
authoritative and the text is left out-of-sync, so a toggle regenerates from them. Regression:
table-view restore regenerates from rows (no stale draft); text-view restore keeps its custom text.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/test_app_web_docs_baseline.py (1)

88-104: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider making the heading-ID and TOC-anchor regexes more robust.

The heading-ID regex <h[123]\s+id="([^"]+)" requires id to be the first attribute after the tag name. If the template ever emits <h2 class="..." id="slug">, the regex won't capture it, potentially causing false failures in the toc_targets <= heading_ids assertion. Similarly, href="#([^"]+)" captures all in-page anchors, not just TOC links — any non-TOC anchor (footnote, nav) pointing to a non-heading element would fail the subset check.

If the current HTML output always puts id first and the page has no non-TOC anchors, the test is fine today. Consider tightening the patterns for long-term robustness.

♻️ Suggested more robust regex patterns
-    heading_ids = set(re.findall(r'<h[123]\s+id="([^"]+)"', html))
+    heading_ids = set(re.findall(r'<h[123]\s[^>]*\bid="([^"]+)"', html))
     assert heading_ids, "no heading id= attributes were emitted (TOC anchors would be dead)"
     # Every TOC anchor (href="`#slug`") must point at a heading id that exists on the page.
-    toc_targets = set(re.findall(r'href="#([^"]+)"', html))
+    toc_targets = set(re.findall(r'<a\s[^>]*\bhref="#([^"]+)"', html))

The \bid= pattern matches id regardless of attribute position, and restricting to <a tags narrows to actual anchor elements.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_app_web_docs_baseline.py` around lines 88 - 104, Make the regex
assertions in test_docs_headings_get_ids_so_toc_anchors_resolve robust to HTML
attribute ordering and anchor context: match heading id attributes using a word
boundary so id need not be first, and restrict href extraction to anchor tags
while preserving fragment matching. Update the patterns so valid headings with
other attributes and relevant TOC links are handled without false failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/test_app_web_docs_baseline.py`:
- Around line 88-104: Make the regex assertions in
test_docs_headings_get_ids_so_toc_anchors_resolve robust to HTML attribute
ordering and anchor context: match heading id attributes using a word boundary
so id need not be first, and restrict href extraction to anchor tags while
preserving fragment matching. Update the patterns so valid headings with other
attributes and relevant TOC links are handled without false failures.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9dfe179a-016a-492c-8ad4-f1e2ed109c22

📥 Commits

Reviewing files that changed from the base of the PR and between 0fe1aeb and c8cfac1.

📒 Files selected for processing (24)
  • app_desktop/theme.py
  • app_desktop/workspace_controller.py
  • app_web/blueprints/docs.py
  • app_web/blueprints/pages.py
  • app_web/logic/common.py
  • app_web/logic/error_propagation.py
  • app_web/logic/extrapolation.py
  • app_web/logic/fitting.py
  • app_web/logic/root_solving.py
  • app_web/logic/statistics.py
  • datalab_latex/latex_formatting.py
  • extrapolation_methods/accelerators.py
  • fitting/hp_fitter.py
  • tests/test_app_web_compute_rate_limit.py
  • tests/test_app_web_docs_baseline.py
  • tests/test_app_web_precision_clamp.py
  • tests/test_constants_text_view.py
  • tests/test_desktop_input_constants_tabs.py
  • tests/test_desktop_theme_tokens.py
  • tests/test_extrapolation_accelerators.py
  • tests/test_file_size_ratchet.py
  • tests/test_fitting_linear_model_sanity.py
  • tests/test_latex_formatting_precision_floor.py
  • tests/test_workspace_controller.py
💤 Files with no reviewable changes (1)
  • tests/test_file_size_ratchet.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • tests/test_workspace_controller.py
  • tests/test_desktop_theme_tokens.py
  • app_desktop/workspace_controller.py
  • tests/test_desktop_input_constants_tabs.py

mp.dps is typed Any, so max(mp.dps, ...) returned Any while the function declares -> int, failing
the core-layer mypy-strict CI gate. Cast int(mp.dps) so the return is typed int. No behavior change;
A3 precision-floor tests still pass.
…ombo

The quadratic (默认三点公式) accelerator was already a recognized
THREE_POINT_METHOD in the compute engine and had a full description in
get_method_description, but the desktop method combo never offered it.
Add the QUADRATIC_PARAMS form_section (empty — the method derives its
three values A/B/C from the data columns, no user params), the
'quadratic' MethodSpec, and place it after 'power_law' in
METHOD_DISPLAY_ORDER (both are three-point methods). Run wiring is
generic, so no branch changes in the mixins, window, or datalab_core.

Update the exact-order combo assertion in test_desktop_extrapolation_ui.
…ckend

Wires the desktop's self_consistent/implicit fitting mode into the web /fit
form by reusing the pure-math fitting.FitRunner() directly (the core session
service deliberately rejects self_consistent, so this mode bypasses it, same
pattern as the existing comparison-mode branch).

- app_web/logic/fitting.py: new _build_self_consistent_problem() helper
  parses/validates the implicit-model form fields (equation, implicit
  variable, output expression, solve options, optional params JSON) and
  builds ModelProblem + ImplicitModelDefinition; a new elif branch in
  _run_fit() calls it then FitRunner().fit(), guarding the core-service
  dispatch with `if fit_res is None:` so the shared LaTeX/CSV/plot tail
  works unchanged for both paths.
- app_web/templates/fit.html + static/js/i18n.js: new self_consistent
  option and field block (equation/variable/output/params/solve-options),
  bilingual i18n keys for both languages.
- shared/ui_specs.py: FITTING_MODEL_FIELD choices corrected from the stale
  3-mode list to the real 7-mode set the frontends offer.
- tests/test_app_web_fitting_self_consistent.py: positive recovery test
  (known implicit model, recovers a=2 exactly) + 3 bilingual validation
  tests.
- tests/test_auto_fit_removed.py: updated the now-obsolete assertion that
  self_consistent was NOT exposed on web.
- tests/test_file_size_ratchet.py: consciously raised app_web/logic/fitting.py's
  baseline (1143 -> 1238) for the new implicit-model helper + branch.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app_web/logic/fitting.py`:
- Around line 254-265: In _build_self_consistent_problem, keep JSON loading
inside the try block but move the params_cfg dictionary validation and
normalized_cfg construction after it, so the intentional ValueError for
non-object JSON is not caught and wrapped again. Preserve exception chaining and
bilingual parsing-error handling for JSON load failures.

In `@shared/ui_specs.py`:
- Around line 360-370: The help metadata is missing the newly supported
self_consistent fit model. Add a self_consistent entry to shared/help_specs.json
matching the model’s identifier and description defined near the self_consistent
UI specification, keeping the desktop and web help content consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d33f42b5-4ca1-492d-ad5f-19e9f73ca94f

📥 Commits

Reviewing files that changed from the base of the PR and between c8cfac1 and caa4e27.

📒 Files selected for processing (9)
  • app_web/logic/fitting.py
  • app_web/static/js/i18n.js
  • app_web/templates/fit.html
  • datalab_latex/latex_formatting.py
  • shared/ui_specs.py
  • tests/test_app_web_fitting_self_consistent.py
  • tests/test_auto_fit_removed.py
  • tests/test_desktop_extrapolation_ui.py
  • tests/test_file_size_ratchet.py
✅ Files skipped from review due to trivial changes (1)
  • tests/test_file_size_ratchet.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • datalab_latex/latex_formatting.py

Comment thread app_web/logic/fitting.py
Comment thread shared/ui_specs.py
Comment on lines +360 to +370
# quadratic ("默认三点公式") derives its three values from the data columns themselves, so it takes
# no tunable parameters (empty group, like shanks/wynn_epsilon). Added so the desktop offers the
# backend's default three-point method that the web already exposes (audit B3).
QUADRATIC_PARAMS = form_section(
key="quadratic_params",
title_zh="默认三点公式",
title_en="Default three-point formula",
fields=[],
visible_when=VisibilityRule.equals("method", "quadratic"),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify help_specs.json was updated for new user-visible parameters
if [ -f shared/help_specs.json ]; then
  echo "=== Checking for 'quadratic' ==="
  rg -n 'quadratic' shared/help_specs.json || echo "No 'quadratic' entries found"
  echo "=== Checking for 'self_consistent' ==="
  rg -n 'self_consistent' shared/help_specs.json || echo "No 'self_consistent' entries found"
else
  echo "shared/help_specs.json not found"
fi

Repository: yilibinbin/DataLab

Length of output: 286


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file list =="
git ls-files 'shared/ui_specs.py' 'shared/help_specs.json'

echo
echo "== ui_specs.py around cited lines =="
sed -n '340,430p' shared/ui_specs.py

echo
echo "== help_specs.json around quadratic =="
sed -n '100,150p' shared/help_specs.json

echo
echo "== search for self_consistent across repo =="
rg -n 'self_consistent|self-consistent|self consistent' shared . --glob '!**/.git/**'

Repository: yilibinbin/DataLab

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== help_specs fitting-related keys =="
rg -n '"(fitting|fit|model|self_consistent|quadratic|custom)"\s*:' shared/help_specs.json

echo
echo "== help_specs around any fitting section =="
python3 - <<'PY'
import json
from pathlib import Path

data = json.loads(Path("shared/help_specs.json").read_text(encoding="utf-8"))
for k in data:
    if any(s in k.lower() for s in ("fit", "model", "quadratic", "custom", "self")):
        print(k)
PY

Repository: yilibinbin/DataLab

Length of output: 281


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ui_specs.py around 440-560 =="
sed -n '440,560p' shared/ui_specs.py

echo
echo "== full help_specs top-level keys =="
python3 - <<'PY'
import json
from pathlib import Path
data = json.loads(Path("shared/help_specs.json").read_text(encoding="utf-8"))
print("\n".join(data.keys()))
PY

Repository: yilibinbin/DataLab

Length of output: 4168


Add the self_consistent help entry

shared/help_specs.json already includes quadratic, but it still has no entry for the new self_consistent fit model added in shared/ui_specs.py:540-544. Keep the desktop/web help data in sync by adding that model description here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shared/ui_specs.py` around lines 360 - 370, The help metadata is missing the
newly supported self_consistent fit model. Add a self_consistent entry to
shared/help_specs.json matching the model’s identifier and description defined
near the self_consistent UI specification, keeping the desktop and web help
content consistent.

Source: Coding guidelines

…istent uncertainty

Two review-chain findings on the B4 self_consistent web branch (Codex +
CodeRabbit, both reproduced):

- CR-1: a non-dict fit_implicit_params JSON raised its bilingual 'must be a
  JSON object' ValueError INSIDE the json.loads try, so the outer except
  re-wrapped it into a doubled '汉语 / English / 汉语 / English' message that
  breaks the locale layer's single ' / ' split. Hoist the isinstance check
  out of the try so only genuine JSON syntax errors get wrapped.

- CX-1: a self_consistent fit with an unused parameter converges on the real
  parameter but leaves a rank-deficient covariance, so uncertainties come back
  NaN. _collect_params then called format_result_with_uncertainty_latex(val,
  nan), which raised a raw, non-bilingual 'cannot convert inf or nan to int'
  and 500-crashed the whole fit response. Guard non-finite sigma at that shared
  sink and report 'N/A' instead (protects every fit mode, not just implicit).

Both covered by new regression tests in test_app_web_fitting_self_consistent.
…lingual error dedup (round-2 review)

Round-2 adversarial review (4 fresh dimensions + per-finding refute-verify,
user-requested re-review) confirmed 5 defects; all reproduced before fixing:

- R2-1 [medium] ROOT FIX: format_value_for_latex_file raised
  'cannot convert inf or nan to int' on non-finite input, discarding an
  otherwise-successful fit at every caller (web _generate_fitting_latex
  params/metrics/diagnostics branches; trigger: dof=0 → NaN reduced χ²).
  The public wrapper now degrades: non-finite sigma → bare value,
  non-finite value → parse-safe \multicolumn literal (works in S and
  dcolumn columns via siunitx_safe_cell).
- R2-4 [medium]: desktop fit LaTeX writer crashed on NaN sigmas from a
  degenerate self_consistent covariance (both engine modes) — healed by
  the same root fix; regression test parametrizes siunitx + dcolumn.
- R2-2 [low]: one response showed the same undefined sigma as 'N/A'
  (table) but 'nan' (CSV, summary). CSV parameter rows and
  summarize_fit_result now render non-finite sigmas as 'N/A'; undefined
  VALUES (metrics/covariance) stay literal 'nan' by convention.
- CR-2 [low]: custom-mode non-dict params JSON double-wrapped its
  bilingual error (twin of CR-1, same file) — isinstance check hoisted
  out of the json.loads try.
- R2-3 [low]: statistics two-column parser double-wrapped its
  'uncertainty is not finite' bilingual error — raise hoisted out of the
  parse try.

The re-run b3-runtime review dimension returned zero findings (desktop
quadratic runtime clean). Verified: 21 targeted + 1255 broad
latex/fitting/statistics tests pass (CI-parity LANG=en), mypy-strict
clean, ruff clean.
…press undefined stat/sys split (Gemini round-2)

Gemini's serial-chain review of 17153d0 confirmed two real gaps (both
independently reproduced before fixing):

- G-1 [high]: the extrapolation and error-propagation table builders call
  the PRIVATE _format_value_for_latex_file directly, bypassing the
  public-wrapper guard — a formula producing nan/inf still crashed those
  tables. The guard now lives at the private function's head (right after
  its own _mp() normalization, using direct mp.isfinite — no extra mpf
  copies, which also resolves Gemini's G-3 style note), and the public
  wrapper's duplicate guard is removed.
- G-2 [low]: with data sigmas, a degenerate fit yields NaN stat AND sys
  errors; summarize_fit_result rendered '± N/A' but then printed
  '(stat nan, sys nan)' — NaN passes truthiness and fails almosteq. The
  stat/sys split is now suppressed for non-finite sys and stat renders
  as N/A.

Verified: 20 targeted + 463 latex-surface tests pass (CI-parity LANG=en),
mypy-strict clean, ruff clean.
…rouping (Codex CX-2)

Codex's final serial-chain review found the remaining hole in the
non-finite-value handling: with native_group_width=False (bundled-engine
app-side grouping, siunitx mode) the extrapolation, error-propagation, and
root table builders blindly wrap every value cell in \text{...} — wrapping
the new \multicolumn{1}{c}{nan} literal cell produces invalid TeX
('Misplaced \omit', reproduced by Codex with an actual lualatex compile).

All three app-group wrap sites now pass \multicolumn cells through
unchanged, mirroring the precedent in the desktop fitting writer's
_maybe_group. Regression test drives generate_latex_table end-to-end with a
NaN result in app-group mode.

Codex also adjudicated the Gemini-round fixes as correct (G-1 guard
placement complete for all paths through the private formatter; G-2
condition correct for 0/nan/±inf).
…kspace

User-reported: after opening an example from the 示例 menu (e.g. 外推示例),
the +列/-列/+行/-行/清除/文本视图 table-editing buttons were dead — clicks did
nothing.

Root cause: example workspaces ship in "file" data-source mode whose
source_path is a RELATIVE path into the bundled examples/ dir
(e.g. examples/extrapolation_richardson.txt). _restore_data_section keeps the
file path when the file still exists (self-contained file-precedence). When the
app runs from the repo root that relative path resolves, so
_update_data_source_visibility grey-disables the whole manual_box (table +
toolbar) — data is treated as coming from the file, not the manual table. The
example rows are already inlined into the table by the restore, so the disabled
toolbar left the user unable to edit visibly-present data. (It only reproduced
when CWD made the relative path resolve, which is why it looked intermittent.)

Fix: on a template open (as_template=True — all example workspaces), drop the
bundled relative file path and fall back to the editable manual table. The
inlined rows are preserved; _update_data_source_visibility re-enables manual_box
once the path edit is empty. Non-template (real user) workspace opens are
unaffected — they keep file mode.

Regression test drives every bundled example through a template open and asserts
the manual box + all six editing buttons are enabled, the rows survive, and a
+列 click actually adds a column. Verified end-to-end under real (cocoa) Qt too.

339 workspace/example tests + 26 example-menu tests pass; ruff + mypy-strict + ratchet clean.
User-reported: the result-detail panel had three LaTeX buttons — 生成 TeX,
预览 PDF, LaTeX 选项 — but 生成 TeX opens the LaTeX preview dialog which itself
carries both a TeX tab AND a PDF tab. So the standalone 预览 PDF button just
duplicated the dialog's PDF tab.

Remove result_preview_pdf_button; the result panel now shows 生成 TeX + LaTeX 选项.
The PDF preview is unchanged — reachable via the PDF tab inside the 生成 TeX
dialog (open_latex_preview builds both tabs; switching to the PDF tab compiles +
shows the PDF).

Updated the button→dialog test to assert the standalone button is gone while the
dialog still exposes a PDF tab; refreshed the stale subtab comment. Verified
end-to-end under real (cocoa) Qt: the row shows only 生成 TeX + LaTeX 选项.

99 result-panel/latex tests pass; ruff + ratchet clean.
…nstants-editor parity)

Follow-up to 288a305: that fix cleared the bundled data file path so the manual
DATA table re-enabled, but left the CONSTANTS editor with the same file-precedence
disable bug. error-propagation.datalab ships its constants in "file" mode
(source_path examples/constants.txt), so opening it as a template left the
constants editor + its +行/-行/清除 buttons dead while the inlined constants rows
sat visible-but-uneditable — the same class of bug the data-table fix cured.

Extend the template-open cleanup to also clear constants_file_edit and refresh the
constants panel (_update_constants_visibility). Verified end-to-end under real
(cocoa) Qt on error-propagation: constants editor + buttons enabled, 8 constants
rows survive; data side unchanged.

Found by the adversarial review's constants-parity finder; the workflow verifier
first mis-refuted it as an unshown-window isVisible() artifact — independent
re-repro with a shown window + isEnabled() confirmed it was real.

Regression test test_example_with_file_mode_constants_keeps_constants_editor_editable
locks it in. 27 example-menu tests pass; ruff + ratchet clean.
…w count

Codex review: the constants-survival check in
test_example_with_file_mode_constants_keeps_constants_editor_editable only
asserted table.rowCount() > 0. The constants editor has min_rows=1, so an empty
restore would still leave one blank row and pass — a future content-loss
regression could slip through. Assert the actual example constant (ALPHA) is
present via editor.rows() instead, so the test fails if the inlined constants are
dropped. Drop the now-unused QTableWidget import.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants