Compare commits
No commits in common. "85d91375dc2b633cda51fdb13a1a5490f822b6dd" and "dfa1e27daba59cbf6ca6c2322809ddef56be28be" have entirely different histories.
85d91375dc
...
dfa1e27dab
3 changed files with 16 additions and 133 deletions
|
|
@ -607,8 +607,6 @@
|
||||||
.kvPlaceholder {
|
.kvPlaceholder {
|
||||||
color: var(--text-soft);
|
color: var(--text-soft);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
/* Stub / not-measured values read fainter than live numbers (audit #1871). */
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
}
|
||||||
.kvCaption {
|
.kvCaption {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
/**
|
|
||||||
* ptica-adapt honesty tests — audit-issue #1871 P1.3 «ptica-adapt помечает
|
|
||||||
* заглушки isReal:true».
|
|
||||||
*
|
|
||||||
* The adapter is the single source of truth for the real-vs-placeholder
|
|
||||||
* decision; a stub must NEVER carry `isReal:true`, or the cockpit presents a
|
|
||||||
* fabricated value as a live measurement. These three cases pin the fixed
|
|
||||||
* spots:
|
|
||||||
* 1. adaptPassport → encumbrances is a stub (never from backend yet).
|
|
||||||
* 2. buySignalFromForecast at deficit_index=null → «прогноза нет», not a
|
|
||||||
* real «наблюдать».
|
|
||||||
* 3. adaptBuySignalGauge with a report whose deficit_index=null → the gauge
|
|
||||||
* is not real (no synthetic mid-scale 50 dressed up as live).
|
|
||||||
*/
|
|
||||||
import { describe, expect, it } from "vitest";
|
|
||||||
|
|
||||||
import {
|
|
||||||
adaptBuySignalGauge,
|
|
||||||
adaptLegalDrawer,
|
|
||||||
adaptPassport,
|
|
||||||
adaptPassportDrawer,
|
|
||||||
buySignalFromForecast,
|
|
||||||
} from "@/components/site-finder/ptica/ptica-adapt";
|
|
||||||
import type { ParcelAnalysis } from "@/types/site-finder";
|
|
||||||
import type { ForecastReport } from "@/types/forecast";
|
|
||||||
|
|
||||||
// Tracked mock fixtures (NEXT_PUBLIC_USE_MOCKS source). The dev-only
|
|
||||||
// __preview/ptica/__fixtures__ dir is git-excluded, so it is absent in CI —
|
|
||||||
// these committed mocks keep the suite reproducible on the Linux runner.
|
|
||||||
import forecastFixture from "@/lib/mocks/parcel-forecast.json";
|
|
||||||
import analyzeFixture from "@/lib/mocks/parcel-analyze.json";
|
|
||||||
|
|
||||||
const analysis = analyzeFixture as unknown as ParcelAnalysis;
|
|
||||||
const baseReport = (forecastFixture as { report: unknown })
|
|
||||||
.report as ForecastReport;
|
|
||||||
|
|
||||||
/** A report identical to the fixture but with no deficit_index (thin §22). */
|
|
||||||
function reportWithoutDeficitIndex(): ForecastReport {
|
|
||||||
return {
|
|
||||||
...baseReport,
|
|
||||||
exec_summary: {
|
|
||||||
...baseReport.exec_summary,
|
|
||||||
key_numbers: {
|
|
||||||
...baseReport.exec_summary.key_numbers,
|
|
||||||
deficit_index: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("ptica-adapt honesty (#1871 P1.3)", () => {
|
|
||||||
it("adaptPassport: encumbrances is a stub, never real", () => {
|
|
||||||
const passport = adaptPassport(analysis);
|
|
||||||
expect(passport.encumbrances.isReal).toBe(false);
|
|
||||||
expect(passport.encumbrances.caption).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("buySignalFromForecast: deficit_index=null is not a real signal", () => {
|
|
||||||
const field = buySignalFromForecast(reportWithoutDeficitIndex());
|
|
||||||
expect(field.isReal).toBe(false);
|
|
||||||
expect(field.caption).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("buySignalFromForecast: a real deficit_index yields a real signal", () => {
|
|
||||||
const field = buySignalFromForecast({
|
|
||||||
...baseReport,
|
|
||||||
exec_summary: {
|
|
||||||
...baseReport.exec_summary,
|
|
||||||
key_numbers: {
|
|
||||||
...baseReport.exec_summary.key_numbers,
|
|
||||||
deficit_index: 0.2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(field.isReal).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("adaptBuySignalGauge: report with deficit_index=null → gauge not real", () => {
|
|
||||||
const card = adaptBuySignalGauge(reportWithoutDeficitIndex());
|
|
||||||
expect(card.gauge.isReal).toBe(false);
|
|
||||||
expect(card.gauge.value).toBeNull();
|
|
||||||
expect(card.available).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("drawer «Обременения» rows are stubs, never real (#1871 P1.3)", () => {
|
|
||||||
// The passport-card encumbrances fix must also cover its two drawer mirrors
|
|
||||||
// (adaptPassportDrawer + adaptLegalDrawer), else the drill-down presents the
|
|
||||||
// em-dash stub as a live measurement.
|
|
||||||
const findEncumbrance = (rows: { k: string; field: { isReal: boolean } }[]) =>
|
|
||||||
rows.find((r) => r.k === "Обременения");
|
|
||||||
|
|
||||||
const passportRow = findEncumbrance(adaptPassportDrawer(analysis).registry);
|
|
||||||
expect(passportRow?.field.isReal).toBe(false);
|
|
||||||
|
|
||||||
const legalRow = findEncumbrance(adaptLegalDrawer(analysis).registry);
|
|
||||||
expect(legalRow?.field.isReal).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -159,8 +159,8 @@ export function adaptPassport(a: ParcelAnalysis): PticaPassport {
|
||||||
: placeholder("нет данных ЕГРН"),
|
: placeholder("нет данных ЕГРН"),
|
||||||
vri: vri ? real(vri) : placeholder("нет данных ЕГРН"),
|
vri: vri ? real(vri) : placeholder("нет данных ЕГРН"),
|
||||||
status: status ? real(status) : placeholder("нет данных ЕГРН"),
|
status: status ? real(status) : placeholder("нет данных ЕГРН"),
|
||||||
// encumbrances never arrive from the backend yet — always a stub.
|
// Matches the existing analysis page: encumbrances are not surfaced yet.
|
||||||
encumbrances: notReal("источник ЕГРН"),
|
encumbrances: { value: PLACEHOLDER, isReal: true },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -265,12 +265,11 @@ export interface PticaInvestScore {
|
||||||
* advisory (the forecast itself is advisory). Independent of the overall score,
|
* advisory (the forecast itself is advisory). Independent of the overall score,
|
||||||
* so it surfaces as soon as the report is ready even if scoring is thin.
|
* so it surfaces as soon as the report is ready even if scoring is thin.
|
||||||
*/
|
*/
|
||||||
export function buySignalFromForecast(report: ForecastReport): PticaField {
|
function buySignalFromForecast(report: ForecastReport): PticaField {
|
||||||
const kn = report.exec_summary.key_numbers;
|
const kn = report.exec_summary.key_numbers;
|
||||||
const di = kn.deficit_index;
|
const di = kn.deficit_index;
|
||||||
if (di == null) {
|
if (di == null) {
|
||||||
// No forecast number → honest stub, distinct from a real «наблюдать».
|
return { value: "наблюдать", isReal: true, caption: "advisory · §22" };
|
||||||
return notReal("прогноз недоступен (Сценарии §22)");
|
|
||||||
}
|
}
|
||||||
const word =
|
const word =
|
||||||
di > 0.05 ? "приобретать" : di < -0.05 ? "пропустить" : "наблюдать";
|
di > 0.05 ? "приобретать" : di < -0.05 ? "пропустить" : "наблюдать";
|
||||||
|
|
@ -678,7 +677,7 @@ export function adaptPassportDrawer(a: ParcelAnalysis): PticaPassportDrawer {
|
||||||
? real(a.egrn.last_egrn_update_date)
|
? real(a.egrn.last_egrn_update_date)
|
||||||
: notReal("нет данных ЕГРН"),
|
: notReal("нет данных ЕГРН"),
|
||||||
},
|
},
|
||||||
{ k: "Обременения", field: notReal("источник ЕГРН") },
|
{ k: "Обременения", field: { value: DASH, isReal: true } },
|
||||||
];
|
];
|
||||||
|
|
||||||
const zouitOverlaps = (zouit ?? []).map((z) => ({
|
const zouitOverlaps = (zouit ?? []).map((z) => ({
|
||||||
|
|
@ -1045,7 +1044,7 @@ export function adaptLegalDrawer(a: ParcelAnalysis): PticaLegalDrawer {
|
||||||
? real(formatInt(restr.zouitCount))
|
? real(formatInt(restr.zouitCount))
|
||||||
: notReal("источник НСПД"),
|
: notReal("источник НСПД"),
|
||||||
},
|
},
|
||||||
{ k: "Обременения", field: notReal("источник ЕГРН") },
|
{ k: "Обременения", field: { value: DASH, isReal: true } },
|
||||||
{
|
{
|
||||||
k: "Блокеры (gate)",
|
k: "Блокеры (gate)",
|
||||||
field: gate
|
field: gate
|
||||||
|
|
@ -1938,36 +1937,20 @@ export function adaptBuySignalGauge(
|
||||||
}
|
}
|
||||||
|
|
||||||
const di = report.exec_summary.key_numbers.deficit_index;
|
const di = report.exec_summary.key_numbers.deficit_index;
|
||||||
|
|
||||||
// Report present but no deficit_index → the gauge has no real metric. Mirror
|
|
||||||
// the "no report" branch: muted track, neutral tone, isReal:false. Never dress
|
|
||||||
// a synthetic mid-scale 50 up as a live number.
|
|
||||||
if (di == null) {
|
|
||||||
return {
|
|
||||||
gauge: {
|
|
||||||
value: null,
|
|
||||||
label: "после прогноза",
|
|
||||||
tone: "neutral",
|
|
||||||
isReal: false,
|
|
||||||
footnote: "Scenarios §22",
|
|
||||||
},
|
|
||||||
word: PLACEHOLDER,
|
|
||||||
available: false,
|
|
||||||
caption: "после прогноза (Scenarios)",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map signed deficit_index (roughly −0.5..+0.5) onto a 0..100 gauge value.
|
// Map signed deficit_index (roughly −0.5..+0.5) onto a 0..100 gauge value.
|
||||||
const gaugeValue = Math.max(0, Math.min(100, Math.round(50 + di * 100)));
|
const gaugeValue =
|
||||||
|
di == null ? 50 : Math.max(0, Math.min(100, Math.round(50 + di * 100)));
|
||||||
|
|
||||||
let tone: PticaGauge["tone"] = "neutral";
|
let tone: PticaGauge["tone"] = "neutral";
|
||||||
let word = "Наблюдать";
|
let word = "Наблюдать";
|
||||||
if (di > 0.05) {
|
if (di != null) {
|
||||||
tone = "good";
|
if (di > 0.05) {
|
||||||
word = "Приобретать";
|
tone = "good";
|
||||||
} else if (di < -0.05) {
|
word = "Приобретать";
|
||||||
tone = "bad";
|
} else if (di < -0.05) {
|
||||||
word = "Пропустить";
|
tone = "bad";
|
||||||
|
word = "Пропустить";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue