# Production instrument-search fix — handoff notes Date: 2026-09-17. Scope: production search stall on `/api/instruments?q=...` (0 bytes in ~20s). All work confined to this repo; nothing deployed, user DB untouched, no services restarted. ## Root causes fixed 1. **worker/data.py** — the old `_catalog_frame` fetched the ENTIRE A-share stock catalog (`stock_info_a_code_name`) and the ENTIRE ETF live snapshot (`fund_etf_spot_em`) via AKShare on every search query. Those calls are unbounded and stalled inside the worker container until the Rust-side runner timeout fired, so the browser's fetch starved and kept spinning. Replaced with **bounded direct provider suggestion search**: - Eastmoney `https://searchapi.eastmoney.com/api/suggest/get` (`input=`, `type=14`, bounded `count`), JSON, per-call timeout **4s**. - Tencent `https://smartbox.gtimg.cn/s3/?q=&t=all`, text hint `v_hint="sh~600000~name~py~GP-A^..."`, per-call timeout **4s**. - No synthetic data, no full catalogs anywhere in the search path anymore. 2. **worker (main.py/data.py) envelope** — search now prints one JSON object envelope `{"source","status","items","error","providers"}` with explicit `status: ready|failed`. `run_search` exits non-zero on failure. 3. **server/src/worker.rs** — container runtime bound reduced **120s → 12s** (outer bound incl. container startup; provider calls are ≤2×4s inside). The stdout is now parsed as the full JSON envelope; the old code extracted `first[...]..last[...]` which silently turned a failed-status envelope into an empty success. A `failed` status now surfaces as a real error (`search_unavailable`, provider error code in the message), and **only ready responses enter the server-side TTL cache**. 4. **server/src/main.rs** — `instruments()` success source label is now `provider_suggest`; failures keep returning HTTP 200 with `status: unavailable: ...` so the existing UI contract is unchanged. 5. **Frontend** (`DatasetWizard.svelte` + new `lib/instrumentSearch.ts`): - 350 ms debounce on input, prior in-flight queries are **aborted** (AbortController) and stale responses can never repaint. - Hard **AbortController timer (12s)** bound; hung requests surface 搜索超时,请重试 instead of an eternal spinner. - Non-`ok` envelope status / provider failure shows 搜索数据源暂时不可用,请稍后重试 with a **重试** button; the spinner is always cleared (`onFinish` in `finally`). - Result rows carry per-item **provenance** (东财 / 腾讯). ## Asset-class rules (validated identity, never guessed from code shape) Live-probed provider classifications (2026-09-17, both endpoints): | Provider | Field | accepted | |-----------|------------------|-----------------------------| | Eastmoney | `Classify` | `AStock`→stock, `Index`→index, `Fund`→etf (cross-confirmed, see below) | | Eastmoney | `Classify` (excluded) | `Bond`,`HK`,`OTCFUND`, others | | Eastmoney | `MktNum` | `1`→SH, `0`→SZ (others dropped) | | Tencent | hint tag | `GP-A`→stock, `ETF`→etf, `ZS`→index; `LOF`/others excluded | | Tencent | hint market | `sh`/`sz` only (`bj` dropped) | - An asset's class comes only from the provider's own classification field — a numeric code like `000300` is never inferred to be an index by shape (test: `test_search_numeric_code_not_auto_index`). - **Verified live**: Eastmoney `Classify=Fund` is ambiguous — ETF `510300` and LOF `160706` share the same `Classify` and `SecurityType`. Therefore an Eastmoney *Fund* item is only emitted when Tencent presents the same code with tag `ETF`. Tencent alone classifies ETF/LOF unambiguously. When Tencent unavailable, Eastmoney-only fund suggestions are suppressed (`test_search_etf_ambiguous_fund_excluded_without_tencent_confirmation`, `test_search_lof_not_present_when_eastmoney_only`). Guaranteed: no LOF ever surfaces as an ETF. - Per-item provenance is preserved: identical codes may appear once per actual source (`source: eastmoney` / `source: tencent`). ## Live verification (fresh container `strategy-lab-worker:local`, rebuilt bd9e9f06d56b) ``` docker run --rm --network=bridge strategy-lab-worker:local \ python -m worker.main search --query 600000 --limit 5 → {"status":"ready","items":[{"symbol":"600000","...":"浦发银行","asset_type":"stock",...}],...} wall time ≈1.9s (container start included; previously 0 bytes / 20+s) q=510300 → etf SH#510300 (eastmoney+tencent) q=沪深300 → index SH#000300 / SZ#399300 (+ ETFs), providers ok q=160706 → {"items":[]} (LOF correctly suppressed, providers ok, status ready) ``` ## Tests actually run and passing - `server`: `cargo test` → **54 passed**; `cargo build --release` OK. - `worker`: `.venv/bin/python -m pytest tests` (repo root) → **49 passed**, of which new/updated in `tests/worker/test_data.py` (real shapes for stock/ETF/index, Chinese query `浦发银行`/`沪深300`, unsupported classes bond/HK/OTC/LOF, provider failure + `requests.Timeout` → explicit failed status, partial-provider warning) and `tests/worker/test_fetch_cli.py::test_search_cli_envelope_contract`. - `frontend`: `npm run test` (vitest) → **45 passed** across 13 files, including new `src/lib/instrumentSearch.test.ts` (success, unavailable status → Chinese message, provider rejection, hard-timer abort → 超时 wording with spinner cleared, **stale-prior-query never repaints and never hangs**, dispose invalidates); `npm run check` → 0 errors/warnings; `npm run build` → OK. ## Deployment state - Worker image rebuilt exactly as `strategy-lab-worker:local` (context repo root, `-f worker/Dockerfile`): id `bd9e9f06d56b`. - Frontend `dist/` rebuilt from the fixed sources. - Server binary built release but **not deployed / no restarts performed**. - Rollback note: previous worker image tag remains on the host; server changes are code-only (no config/DB changes). ## Known limitation (documented honestly) When Tencent is unavailable and only Eastmoney responds, fund-class suggestions are suppressed (stock/index still return). This trades a slightly narrower ETF list for a hard guarantee that no LOF is presented as an ETF without provider confirmation.