From 00f177df036350ead5259b8e24a4053a4ff3bf93 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sun, 30 Aug 2026 14:53:05 +0500 Subject: [PATCH] =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B4=D0=B8=D0=BD=D0=B3:=20?= =?UTF-8?q?=D0=B8=D0=BC=D1=8F=20=D0=B1=D0=BE=D0=B5=D0=B2=D0=BE=D0=B3=D0=BE?= =?UTF-8?q?=20=D1=81=D0=B5=D1=80=D0=B2=D0=B5=D1=80=D0=B0=20=D1=83=D0=B1?= =?UTF-8?q?=D1=80=D0=B0=D0=BD=D0=BE=20=D0=B8=D0=B7=20=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8C=D1=81?= =?UTF-8?q?=D0=BA=D0=BE=D0=B9=20=D0=BA=D0=BE=D0=BF=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit «Симуляция пробы покрытия на боевой базе (poincare, 30.08.2026)» доезжала до экрана: CITY_COVERAGE_SOURCE собирается в coverage-copy.ts и рендерится в EstimateFlow.tsx:323. На первом экране лендинга строки не видно — она появляется в форме проверки, после выбора города, поэтому grep по отрендеренной главной давал ноль и дефект выглядел несуществующим. Хостнейм посетителю не сообщает ничего: «на боевой базе» несёт весь смысл. Убран из обеих строк-источников (сверка бэктеста и проба покрытия) и из комментария в статьях. Гейт смотрит в ИСХОДНИК, а не в вывод: какой путь рендера показывает строку сегодня, знать не нужно — завтра он будет другим. Список запретов явный, без эвристики «похоже на хостнейм»: она ловила бы domclick.ru и avito.ru в текстах про источники данных. gendsgn.ru в список НЕ входит — это публичный домен B2B, на который лендинг ссылается кнопкой «Для бизнеса». Гейт проверен фальсификацией: возврат хостнейма даёт красное с указанием landing-facts.ts:207, а не пустой список. --- .../__tests__/public-copy-no-infra.test.ts | 90 +++++++++++++++++++ .../mera-public/articles/articles-content.ts | 2 +- .../src/app/mera-public/landing-facts.ts | 4 +- 3 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 tradein-mvp/frontend/src/app/mera-public/__tests__/public-copy-no-infra.test.ts diff --git a/tradein-mvp/frontend/src/app/mera-public/__tests__/public-copy-no-infra.test.ts b/tradein-mvp/frontend/src/app/mera-public/__tests__/public-copy-no-infra.test.ts new file mode 100644 index 00000000..6b7d71e7 --- /dev/null +++ b/tradein-mvp/frontend/src/app/mera-public/__tests__/public-copy-no-infra.test.ts @@ -0,0 +1,90 @@ +/** + * Инфраструктурные имена не должны попадать в файлы пользовательской копии. + * + * 30.08.2026 «Симуляция пробы покрытия на боевой базе (poincare, 30.08.2026)» + * доехала до экрана: `CITY_COVERAGE_SOURCE` собирается в `coverage-copy.ts` и + * рендерится в `EstimateFlow.tsx:323`. На первом экране лендинга её не видно — + * она появляется в форме проверки, после выбора города. Поэтому grep по + * отрендеренной главной давал ноль, и дефект выглядел несуществующим. + * + * Отсюда форма гейта: он смотрит в ИСХОДНИК, а не в вывод. Какой именно путь + * рендера сегодня показывает строку, знать не нужно — завтра он будет другим, + * а хостнейм прода посетителю не сообщает ничего ни на одном из них. + * + * Скан идёт по всему тексту файла, включая комментарии: комментарий рядом с + * копией — заготовка следующей строки копии, и «на боевой базе» в нём ничем + * не хуже имени машины. + */ +import { readdirSync, readFileSync, statSync } from "node:fs"; +import { join } from "node:path"; + +import { describe, expect, it } from "vitest"; + +const ROOT = join(__dirname, ".."); + +/** + * Имена машин, контейнеров и внутренних хостов. Список ЯВНЫЙ, а не эвристика: + * эвристика на «что-то похожее на хостнейм» ловила бы доменные имена в текстах + * про источники данных (domclick.ru, avito.ru), которые тут законны. + * + * `gendsgn.ru` в список НЕ входит намеренно: это публичный домен B2B, на + * который лендинг ссылается кнопкой «Для бизнеса» (`B2B_URL` в content.ts). + * Запрет на него сломал бы рабочую ссылку — граница проходит по «внутреннее + * имя машины» против «адрес, который посетитель и так увидит в браузере». + */ +const BANNED = [ + "poincare", + "beget", + "tradein-postgres", + "tradein-backend", + "tradein-scraper", + "gendesign-postgres", + "gendesign-backend", + "glitchtip", + "forgejo", +]; + +/** Файлы копии: то, из чего собирается видимый посетителю текст. */ +function copySources(dir: string, acc: string[] = []): string[] { + for (const name of readdirSync(dir)) { + if (name === "__tests__" || name === "node_modules") continue; + const full = join(dir, name); + if (statSync(full).isDirectory()) { + copySources(full, acc); + } else if (name.endsWith(".ts") || name.endsWith(".tsx")) { + acc.push(full); + } + } + return acc; +} + +describe("публичная копия не содержит инфраструктурных имён", () => { + const files = copySources(ROOT); + + it("файлы копии вообще найдены — иначе гейт зелёный по построению", () => { + // Без этой проверки сломанный обход каталога дал бы пустой список и + // «нарушений нет» на любом содержимом. + expect(files.length).toBeGreaterThan(20); + expect(files.some((f) => f.endsWith("landing-facts.ts"))).toBe(true); + expect(files.some((f) => f.endsWith("coverage-copy.ts"))).toBe(true); + }); + + it.each(BANNED)("нигде не встречается «%s»", (needle) => { + const hits: string[] = []; + for (const file of files) { + const text = readFileSync(file, "utf8"); + text.split("\n").forEach((line, i) => { + if (line.toLowerCase().includes(needle)) { + hits.push(`${file.slice(ROOT.length + 1)}:${i + 1}`); + } + }); + } + expect( + hits, + `«${needle}» — инфраструктурное имя в файле пользовательской копии. ` + + `Посетителю оно не сообщает ничего: «на боевой базе» несёт весь смысл. ` + + `Строка может не рендериться сегодня и начать рендериться завтра — ` + + `так и вышло с CITY_COVERAGE_SOURCE в EstimateFlow.tsx.`, + ).toEqual([]); + }); +}); diff --git a/tradein-mvp/frontend/src/app/mera-public/articles/articles-content.ts b/tradein-mvp/frontend/src/app/mera-public/articles/articles-content.ts index 7f0bfba8..e9b44953 100644 --- a/tradein-mvp/frontend/src/app/mera-public/articles/articles-content.ts +++ b/tradein-mvp/frontend/src/app/mera-public/articles/articles-content.ts @@ -4,7 +4,7 @@ * ЧИСЛА ИЗ МАКЕТА СНЯТЫ (29.08.2026). Макет утверждал «по нашим данным, * средний разрыв между первой ценой в объявлении и ценой сделки — 4,1%, а у * провисевших дольше трёх месяцев доходит до 9%», «расхождение с кадастровой - * до 40%» и плитки −4,3% / ×2,4 / 83%. Прод-замер (poincare, 29.08.2026) + * до 40%» и плитки −4,3% / ×2,4 / 83%. Прод-замер (29.08.2026) * не подтвердил ни одно: * * | макет | что показал замер | diff --git a/tradein-mvp/frontend/src/app/mera-public/landing-facts.ts b/tradein-mvp/frontend/src/app/mera-public/landing-facts.ts index 176dbe4c..e1ee7300 100644 --- a/tradein-mvp/frontend/src/app/mera-public/landing-facts.ts +++ b/tradein-mvp/frontend/src/app/mera-public/landing-facts.ts @@ -44,7 +44,7 @@ export interface MeasuredValue { /** Общий источник трёх величин ниже — один и тот же прогон сверки. */ const BACKTEST_SOURCE = - "Ручная сверка на проде (poincare, 29.08.2026): прогноз МЕРЫ против цены ДКП " + + "Ручная сверка на боевой базе (29.08.2026): прогноз МЕРЫ против цены ДКП " + "Росреестра по Екатеринбургу, сделки II квартала 2026 года"; /** @@ -204,7 +204,7 @@ export interface CityCoverageMeasure { } export const CITY_COVERAGE_SOURCE = - "Симуляция пробы покрытия на боевой базе (poincare, 30.08.2026): случайные адреса " + + "Симуляция пробы покрытия на боевой базе (30.08.2026): случайные адреса " + "активных объявлений, для каждого — когорта самой ручки /coverage (радиус 1 км, то же " + "число комнат, площадь ±15 %, свежесть 14 дней, дедуп по источнику и адресу), " + "собственный адрес из когорты исключён"; -- 2.45.3