diff --git a/tradein-mvp/frontend/src/components/trade-in/HeroSummary.tsx b/tradein-mvp/frontend/src/components/trade-in/HeroSummary.tsx
index 0a0134e6..fe1d0ae9 100644
--- a/tradein-mvp/frontend/src/components/trade-in/HeroSummary.tsx
+++ b/tradein-mvp/frontend/src/components/trade-in/HeroSummary.tsx
@@ -10,7 +10,7 @@
* 14.08.2026 (блок 4.1): SHORT_ESTIMATE_DISCLAIMER — см. `lib/legal-copy.ts`.
*/
import { useState } from "react";
-import type { AggregatedEstimate, TradeInEstimateInput, HouseType, RepairState, ConfidenceLevel } from "@/types/trade-in";
+import type { AggregatedEstimate, AvitoImvSummary, TradeInEstimateInput, HouseType, RepairState, ConfidenceLevel } from "@/types/trade-in";
import { asHouseType, asRepairState } from "@/types/trade-in";
import { useActiveBrandSlug, useBrand } from "@/lib/useBrand";
import { sourceDot, sourcePublicLabel } from "@/lib/source-registry";
@@ -18,6 +18,22 @@ import { dealsAsOfLabel } from "@/lib/rosreestr";
import { SHORT_ESTIMATE_DISCLAIMER } from "@/lib/legal-copy";
import { HeroTransparency } from "./HeroTransparency";
+/**
+ * #3339 — подпись «тонкого рынка» под числом оценочной модели площадки.
+ *
+ * Бэкенд (thin_market, #audit-5b) при market_count ниже порога ИСКЛЮЧАЕТ IMV из
+ * headline — число остаётся справочным. Показывать его рядом с итогом молча —
+ * то же самое, что выдать не участвующую в расчёте величину за расчётную.
+ * null (нет флага / старая оценка без поля) → подписи нет.
+ */
+export function thinMarketNote(imv: AvitoImvSummary | null | undefined): string | null {
+ if (imv?.thin_market !== true) return null;
+ const n = imv.market_count;
+ if (n == null) return "тонкий рынок — в расчёт не входит";
+ // После «из» счётная форма родительная у всех чисел, кроме единицы.
+ return `рынок из ${n} ${n === 1 ? "объявления" : "объявлений"} — в расчёт не входит`;
+}
+
interface Props {
estimate: AggregatedEstimate;
input: TradeInEstimateInput;
@@ -175,6 +191,8 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
imv && typeof imv.recommended_price === "number" && imv.recommended_price > 0
? imv.recommended_price
: null;
+ // #3339: число IMV показываем, но при тонком рынке честно называем его вне расчёта.
+ const imvThin = thinMarketNote(imv);
// ── #652: коридор реальных ДКП-сделок (advisory). ₽/м² → млн через площадь. ──
const dkp = estimate.dkp_corridor;
const dkpArea = estimate.area_m2 ?? input.area_m2 ?? 0;
@@ -537,6 +555,7 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
{sourcePublicLabel("avito_imv")}
{formatMln(imvPrice)} ₽
+ {imvThin && {imvThin}}
)}
{estimate.cian_valuation?.sale_price_rub != null && (
@@ -599,6 +618,7 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
style={{ marginTop: 6, fontSize: 12, color: "var(--muted)" }}
>
{sourcePublicLabel("avito_imv")}, оценочная модель: {formatMln(imvPrice)} ₽
+ {imvThin ? ` · ${imvThin}` : ""}
)}
diff --git a/tradein-mvp/frontend/src/components/trade-in/__tests__/thinMarketBadge.test.tsx b/tradein-mvp/frontend/src/components/trade-in/__tests__/thinMarketBadge.test.tsx
new file mode 100644
index 00000000..01c5d7f6
--- /dev/null
+++ b/tradein-mvp/frontend/src/components/trade-in/__tests__/thinMarketBadge.test.tsx
@@ -0,0 +1,83 @@
+/**
+ * #3339 — число, исключённое из расчёта, обязано быть подписано.
+ *
+ * Бэкенд (#3336, thin_market) при market_count ниже порога не пускает IMV в
+ * headline: величина остаётся справочной. На витрине она всё равно стоит рядом
+ * с итоговой ценой — без пометки читается как участвующая в расчёте.
+ *
+ * Проверяется ЗНАЧЕНИЕ подписи (реальный N из market_count) и оба безопасных
+ * состояния: флаг false и полное отсутствие поля (оценка со старого бэкенда).
+ */
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+import { HeroSummary, thinMarketNote } from "../HeroSummary";
+import { FIXTURE_ESTIMATE, FIXTURE_INPUT } from "@/app/ui-preview/estimate/fixture";
+import type { AggregatedEstimate, AvitoImvSummary } from "@/types/trade-in";
+
+// Бренд и блок прозрачности ходят в сеть (TanStack Query) — к подписи отношения
+// не имеют, гасим, чтобы рендер hero был оффлайновым.
+vi.mock("@/lib/useBrand", () => ({
+ useActiveBrandSlug: () => null,
+ useBrand: () => ({ data: null }),
+}));
+vi.mock("../HeroTransparency", () => ({ HeroTransparency: () => null }));
+
+const IMV: AvitoImvSummary = {
+ recommended_price: 10_200_000,
+ lower_price: 9_400_000,
+ higher_price: 11_000_000,
+ market_count: 3,
+};
+
+function estimateWithImv(imv: AvitoImvSummary): AggregatedEstimate {
+ return { ...FIXTURE_ESTIMATE, avito_imv: imv };
+}
+
+function renderHero(imv: AvitoImvSummary) {
+ return render(
+ {}}
+ />,
+ );
+}
+
+describe("thin_market — подпись у числа оценочной модели площадки", () => {
+ it("thin_market=true, market_count=3 → подпись называет реальные 3 объявления", () => {
+ expect(thinMarketNote({ ...IMV, thin_market: true })).toBe(
+ "рынок из 3 объявлений — в расчёт не входит",
+ );
+
+ const { container } = renderHero({ ...IMV, thin_market: true });
+
+ // Число IMV осталось на экране — гасить его нечестно, оно посчитано.
+ expect(container.textContent).toContain("10,20 млн");
+ // Но рядом с ним стоит N из market_count, а не общая формулировка.
+ expect(screen.getAllByText(/рынок из 3 объявлений — в расчёт не входит/).length)
+ .toBeGreaterThan(0);
+ });
+
+ it("thin_market=false → подписи нет (IMV участвует в расчёте)", () => {
+ expect(thinMarketNote({ ...IMV, thin_market: false })).toBeNull();
+
+ const { container } = renderHero({ ...IMV, thin_market: false });
+ expect(container.textContent).toContain("10,20 млн");
+ expect(container.textContent).not.toContain("в расчёт не входит");
+ });
+
+ it("поля нет вовсе (оценка со старого бэкенда) → подписи нет", () => {
+ expect(thinMarketNote(IMV)).toBeNull();
+ expect(thinMarketNote(null)).toBeNull();
+
+ const { container } = renderHero(IMV);
+ expect(container.textContent).not.toContain("в расчёт не входит");
+ });
+
+ it("единственное объявление — счётная форма не ломается", () => {
+ expect(thinMarketNote({ ...IMV, market_count: 1, thin_market: true })).toBe(
+ "рынок из 1 объявления — в расчёт не входит",
+ );
+ });
+});
diff --git a/tradein-mvp/frontend/src/types/trade-in.ts b/tradein-mvp/frontend/src/types/trade-in.ts
index 518adb4a..6bcbd92c 100644
--- a/tradein-mvp/frontend/src/types/trade-in.ts
+++ b/tradein-mvp/frontend/src/types/trade-in.ts
@@ -147,6 +147,10 @@ export interface AvitoImvSummary {
lower_price: number | null; // нижняя граница IMV-коридора, ₽
higher_price: number | null; // верхняя граница IMV-коридора, ₽
market_count: number | null; // объём рынка, на котором построена оценка
+ // #3339 / backend #audit-5b: market_count < avito_imv_thin_market_threshold —
+ // IMV построен на малой выборке и в headline НЕ участвует (display-only).
+ // optional: оценки, посчитанные до деплоя гейта, поля не содержат.
+ thin_market?: boolean;
}
// ── #652: коридор реальных ДКП-сделок Росреестра (advisory, не клампит) ──