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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion app_web/blueprints/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,8 +265,15 @@ def _method_placeholder_en(method_key: str) -> dict:
mimetype="application/json",
)
except Exception as exc:
# Do not leak the raw exception text (filesystem paths / internals) to
# this public endpoint — expose only the exception class name and log the
# full detail server-side (audit R3 D5).
current_app.logger.warning("Failed to load help specs", exc_info=True)
return current_app.response_class(
response=json.dumps({"error": f"Failed to load help specs: {str(exc)}"}, ensure_ascii=False),
response=json.dumps(
{"error": f"Failed to load help specs: {type(exc).__name__}"},
ensure_ascii=False,
),
status=500,
mimetype="application/json",
)
12 changes: 10 additions & 2 deletions app_web/blueprints/sse.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,9 +458,17 @@ def _single_fit_events(
})
return

params = {k: float(v) for k, v in (fit_result.params or {}).items()}
# Serialize mp.mpf parameters/errors as strings at the requested precision,
# not float() — an early float cast rounds to ~17 digits and destroys the
# high-precision result computed at precision=80+ (audit R3 D3). Mirrors the
# desktop worker, which serializes with mp.nstr. NB: format the existing mpf
# directly; wrapping it in mp.mpf(...) at the ambient (low) mp.dps would
# re-quantize it back down to float precision.
import mpmath as mp

params = {k: mp.nstr(v, precision) for k, v in (fit_result.params or {}).items()}
errors = {
k: float(v) for k, v in (fit_result.param_errors_stat or {}).items()
k: mp.nstr(v, precision) for k, v in (fit_result.param_errors_stat or {}).items()
}
yield ("result", {
"model": model_id,
Expand Down
6 changes: 6 additions & 0 deletions app_web/logic/fitting.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import json
import logging
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from typing import Any
Expand Down Expand Up @@ -57,6 +58,8 @@
from shared.uncertainty import parse_uncertainty_format
from datalab_latex.latex_tables_fitting import build_fitting_comparison_latex_block

_logger = logging.getLogger(__name__)


@dataclass
class FitResultBundle:
Expand Down Expand Up @@ -968,6 +971,9 @@ def _render_plot(fit_res):
)
return _encode_b64(plot_bytes)
except Exception:
# Graceful fallback (plot omitted) but surface the cause to server
# logs instead of silently returning None (audit R3 D6).
_logger.exception("Fitting overview plot rendering failed; omitting plot")
return None

with _precision_guard(mp_precision):
Expand Down
8 changes: 6 additions & 2 deletions app_web/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,17 @@ def _component_schemas() -> dict[str, Any]:
"type": "object",
"properties": {
"model": {"type": "string"},
# High-precision fit parameters/errors are emitted as decimal
# STRINGS (via mp.nstr at the requested precision), not JSON
# numbers, to preserve precision beyond float's ~17 digits
# (audit R3 D3). Keep this in sync with the SSE result emitter.
"params": {
"type": "object",
"additionalProperties": {"type": "number"},
"additionalProperties": {"type": "string"},
},
"param_errors_stat": {
"type": "object",
"additionalProperties": {"type": "number"},
"additionalProperties": {"type": "string"},
},
"aic": {"type": "number"},
"bic": {"type": "number"},
Expand Down
6 changes: 5 additions & 1 deletion datalab_core/statistics_compute.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,11 @@ def compute_statistics(
)
)
warning_codes.append("effective_n")
elif not mp.almosteq(W2, mp.mpf("0")):
else:
# W2 is already known > 0 and finite here (the guard above rejects
# W2 <= 0 / NaN). The former almosteq(W2, 0) check wrongly skipped
# effective_n for tiny-but-valid W2, e.g. very large sigmas (audit
# R3 D4). W^2/W2 is well-defined for any positive W2.
effective_n = (W * W) / W2
else:
raise ValueError(_dual_msg("未知的统计模式。", "Unknown statistics mode."))
Expand Down
10 changes: 9 additions & 1 deletion datalab_latex/latex_tables_extrapolation.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from shared.bilingual import _dual_msg, _split_dual
from shared.formula_defaults import DEFAULT_THREE_POINT_FORMULA
from shared.latex_escaping import latex_escape as _escape_latex_text
from shared.extrapolation_engine import (
ExtrapolationOptions,
ExtrapolationResult,
Expand Down Expand Up @@ -305,6 +306,10 @@ def _append_extrapolation_table_block(
latex_content.append("\\begin{table}[!ht]")
base_caption = caption if caption else "Extrapolation results table"
caption_text = f"{base_caption} (Part {block_index})" if total_blocks > 1 else base_caption
# Escape user-supplied caption text before embedding in \caption{} so LaTeX
# specials (& _ $ # % ~ ^ ...) can't break the compile or inject commands —
# mirrors the error-propagation table (audit R3 D1).
caption_text = _escape_latex_text(caption_text)
label = "tab:extrapolation" if block_index == 1 else f"tab:extrapolation-{block_index}"
latex_content.append(f"\\caption{{{caption_text}}}\\label{{{label}}}")
latex_content.append("\\begin{threeparttable}")
Expand Down Expand Up @@ -407,7 +412,10 @@ def generate_latex_table(
)

header_cells = [
"\\multicolumn{{1}}{{c}}{{{0}}}".format(headers[idx] if idx < len(headers) else f"列{idx + 1}")
# Escape user-supplied column headers before embedding (audit R3 D2).
"\\multicolumn{{1}}{{c}}{{{0}}}".format(
_escape_latex_text(headers[idx] if idx < len(headers) else f"列{idx + 1}")
)
for idx in range(column_count)
]
header_row = "$n$ & " + " & ".join(header_cells) + " & \\multicolumn{1}{c}{Extrap.} \\\\\\hline"
Expand Down
163 changes: 163 additions & 0 deletions docs/CODE_REVIEW_2026_R3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# DataLab — Round-3 Full-Package Review (recommendations only)

**Scope:** strict swarm review of the entire package on the post-R2 state
(main's merged audit fixes + PR#76). **No fixes applied** — this is a
recommendation document.

**Method (three independent gates):**
1. **Swarm review** — 84 agents across 16 dimensions (9 defect dimensions +
7 quality/soft dimensions).
2. **Internal adversarial verification** — every defect finding challenged by
2 skeptics + tie-breaker (default *false-positive* unless reproduced from
current code).
3. **External adversarial re-review** — Codex (gpt-5.5, xhigh) re-adjudicated
each surviving defect against the live code; the primary session model
independently re-verified the contested ones. (Second external CLI — Claude
headless / gpt-5.1-codex — was unavailable this run: no headless auth /
not on account; noted for transparency.)

**Funnel:** 28 raw defect findings → 9 survived internal verification →
**8 confirmed** after external re-review (**1 rejected**). Plus **68 soft
recommendations**.

> R2's 17 fixes (F1–F15 + linecache test-isolation + #76 timeout) are already
> merged and were excluded from this round.

---

## Executive summary

| Bucket | Count | Notes |
|---|---|---|
| Confirmed defects | 8 | 2× LaTeX-injection (extrapolation table), 1 numeric, 1 precision-loss, 1 info-leak, 3 silent-error |
| Rejected (external) | 1 | #2 security-shim — production already hard-raises |
| Soft: maintainability | 20 | large files, duplicated helpers, boilerplate |
| Soft: performance | 10 | hp_fitter matrix construction hotspots |
| Soft: testing | 14 | coverage gaps + weak assertions |
| Soft: type-safety | 9 | dynamic attrs on dataclasses, Any leaks |
| Soft: documentation | 7 | missing env-var docs, ARCHITECTURE drift |
| Soft: bilingual | 7 | single-language user strings |

**Overall health:** good. No critical runtime defects survived verification.
The most actionable items are the **two extrapolation-table LaTeX escaping
gaps** (same class as R2's F14, which only fixed the error-propagation table)
and the **SSE high-precision loss**.

---

## Verified defects (external-adjudicated)

### High-value

**D1 — Extrapolation-table LaTeX caption is unescaped (injection / broken compile)**
`datalab_latex/latex_tables_extrapolation.py:309`
User `caption` (web: `app_web/logic/extrapolation.py:174`) is embedded raw in
`\caption{...}`. Sibling tables escape via `_escape_latex_text`; this one
doesn't — R2's F14 fixed only `error_propagation`. Special chars (`& _ $ # % ~ ^ \ { }`)
break compilation or inject commands. **Fix:** escape `caption_text`. Effort: S.
*(Codex CONFIRMED; primary-model CONFIRMED.)*

**D2 — Extrapolation-table column headers are unescaped**
`datalab_latex/latex_tables_extrapolation.py:410`
User headers (`app_web/logic/extrapolation.py:200`) embedded raw in
`\multicolumn{1}{c}{...}`. Same class as D1. **Fix:** escape each header. Effort: S.
*(Codex CONFIRMED; primary-model CONFIRMED.)*

**D3 — SSE fit response drops high precision**
`app_web/blueprints/sse.py:461`
`params = {k: float(v) ...}` / `param_errors_stat` cast mp.mpf → float (~17
digits), destroying results computed at mp_precision=80+. Desktop serializes via
`mp.nstr`. **Fix:** serialize with `mp.nstr(v, precision)`. **Codex refinement:**
`app_web/openapi.py:66` declares these as numbers — update the schema/clients
too, or emit strings consistently. Effort: M. *(Codex CONFIRMED + refinement.)*

### Medium

**D4 — Effective sample size skipped for tiny-but-valid W2**
`datalab_core/statistics_compute.py:380`
After the `W2 > 0` guard (line 372), `elif not mp.almosteq(W2, mp.mpf("0"))`
suppresses `effective_n` for tiny positive W2 (`almosteq(1e-50,0)==True`,
verified). **Fix:** replace the `elif` with `else:` (W2 is already known >0 &
finite). Effort: S. *(Codex CONFIRMED; primary-model CONFIRMED.)*

**D5 — help_specs endpoint leaks raw exception text**
`app_web/blueprints/api.py:269`
Public GET `/api/help_specs` returns `str(exc)` (paths/internals). SSE/pages use
a sanitizer. **Fix:** return `type(exc).__name__`, log full server-side. Effort: S.

### Low (silent error-handling — surface via logging)

**D6** `app_web/logic/fitting.py:970` — plot render swallows all exceptions →
`None` with no log. **Fix:** `logger.exception(...)` before returning None. Effort: S.

**D7** `fitting/runner.py:213` — observed-linear `except ValueError: pass`
loses the fallback reason. **Fix:** record reason in `fallback_history`
(Codex: init `fallback_history` before the `try`). Effort: S.

**D8** `formula_help.py:78` — help-specs load returns `{}` on any OSError/JSON
error with no log. **Fix:** log tried paths + error before the empty fallback. Effort: S.

### Rejected by external review

**~~Dev-mode disables LaTeX sandboxing~~** `app_web/_security_shim.py:92` —
**FALSE_POSITIVE.** Production hard-`raise`s a `RuntimeError` on security-import
failure (`_security_shim.py:42`); the dev fallback logs at ERROR ("DEV UNSAFE
MODE") *and* the fallback `compile_latex_safe` still forces `-no-shell-escape`
(guards `\write18`). Only the heuristic content-filter is absent in dev
fallback — defense-in-depth behind a hard control. Not a real production gap.
*(Codex FALSE_POSITIVE; primary-model confirmed the raise + shell-escape guard.)*

---

## Improvement recommendations (soft — not adversarially verified)

### Maintainability (20)
- `app_desktop/window.py` — monolithic (8 mixins, 150+ methods); several
100–150-line methods (`_on_stats_mode_change:1328`, `_refresh_display_format:2902`,
`_initialize_workspace_tracking:732`, `__init__:476`); a repeated
getattr/hasattr/setVisible visibility pattern (~21×) → extract a
`set_visible_if(attr, cond)` helper.
- Duplicated helpers across statistics modules: `_bool_option` (4 modules),
`_string_option` (3) → hoist to one shared module.
- `shared/plotting.py` (2080 lines, 62 funcs) — split by workflow.

### Performance (10) — concentrated in `fitting/hp_fitter.py`
- `:341` JᵀJ built via repeated row indexing; `:333` redundant model
evaluations during covariance; `:343` repeated `mp.mpf(0)` allocs; `:140`
redundant `mp.nstr` for dedup keys; `:436` double-nested error-propagation
loop without early exit. These are the high-precision hot path — worth
profiling before optimizing.

### Testing (14)
- No tests: `precision_guard` edge/clamp cases (`shared/precision.py:40`),
`_coerce_int`, safe_eval div/mod-by-zero (`shared/expression_engine.py:232`),
`combine_error_components` NaN/Inf (`fitting/hp_fitter.py:63`), constraint DAG
failures (`fitting/constraints.py:133`). Several weak assertions flagged.

### Type-safety (9)
- `fitting/implicit_model.py:171` dynamic attribute attachment to a dataclass;
`fitting/hp_fitter.py:256` `getattr` on a required field. Consider widening
mypy --strict beyond the current 4 roots.

### Documentation (7)
- `docs/web/deploy.en.md` missing `DATALAB_SSE_DISABLE_RATE_LIMIT` and LaTeX
security env vars; `docs/ARCHITECTURE.md` predates `datalab_core/`.

### Bilingual (7)
- Assorted single-language user-facing strings not using `_dual_msg` (details in
the finding set).

---

## Prioritized action list

1. **D1 + D2** (extrapolation LaTeX escaping) — same class as a shipped R2 fix,
small, user-data-driven. Do first.
2. **D3** (SSE precision loss) — defeats the product's core value on the web fit
path; pair with the OpenAPI schema update.
3. **D4** (effective_n) — one-line correctness fix.
4. **D5–D8** (info-leak + silent errors) — small, batchable logging/sanitizing.
5. Soft items — schedule as maintenance; hp_fitter perf only after profiling.

*Generated by the R3 swarm review; verified via internal 2-skeptic gate +
external Codex adjudication + primary-model re-verification. No code changed.*
12 changes: 10 additions & 2 deletions fitting/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,16 @@ def _fit_self_consistent(
result.details["implicit_strategy"] = "observed_linear"
result.details["optimizer_backend"] = "mpmath_qr"
return result
except ValueError:
pass
except ValueError as exc:
# Record why the observed-linear fast path was abandoned so
# the fallback to the general solver is traceable instead of
# silently swallowed (audit R3 D7). observed_linear_skipped is
# appended to fallback_history below.
observed_linear_skipped = {
"from": "observed_linear",
"to": "general_output_space",
"reason": f"observed_linear_failed: {exc}",
}

target_implicit_candidates: list[tuple[mp.mpf, ...]] | None = None
output_inversion_reason: str | None = None
Expand Down
15 changes: 13 additions & 2 deletions formula_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@
from __future__ import annotations

import json
import logging
import sys
from functools import lru_cache
from pathlib import Path
from typing import Any

from shared.formula_defaults import DEFAULT_THREE_POINT_FORMULA

_logger = logging.getLogger(__name__)


_FALLBACK_FUNCTION_HELP: dict[str, str] = {
"zh": "函数帮助暂不可用。",
Expand Down Expand Up @@ -67,14 +70,22 @@ def _substitute_placeholders(value: object) -> object:

@lru_cache(maxsize=1)
def _load_help_specs() -> dict[str, Any]:
for path in _candidate_help_specs_paths():
candidates = _candidate_help_specs_paths()
for path in candidates:
try:
with path.open("r", encoding="utf-8") as fh:
data = json.load(fh)
if isinstance(data, dict):
return data
except (OSError, json.JSONDecodeError):
except (OSError, json.JSONDecodeError) as exc:
_logger.warning("Could not load help specs from %s: %s", path, exc)
continue
# Empty help is a degraded state (no help content anywhere) — surface it
# instead of silently returning {} (audit R3 D8).
_logger.warning(
"help_specs.json not loaded from any candidate path (%s); returning empty help",
[str(p) for p in candidates],
)
return {}


Expand Down
Loading
Loading