Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
d9972c2
feat(reports): leftover-map person coordinates on leftover-map compar…
seonghobae Aug 31, 2026
de53e86
test(reports): reconstruct #879 comparison post-coordinate contract
seonghobae Sep 12, 2026
fee9f2b
chore(stack): converge #1034 onto repaired #1033
seonghobae Sep 12, 2026
5015f19
chore(stack): converge #1034 onto repaired successor ancestry
seonghobae Sep 13, 2026
a169e68
fix(reports): restore comparison post coordinate badge
seonghobae Sep 15, 2026
49d02bf
fix(reports): wire comparison post coordinate action
seonghobae Sep 15, 2026
de5a547
chore(stack): converge comparison post lane onto #1033
seonghobae Sep 15, 2026
48705cc
chore(stack): adopt report criterion coordinate boundary
seonghobae Sep 15, 2026
1a75983
chore(stack): preserve report criterion boundary in post lane
seonghobae Sep 15, 2026
9cb44bb
chore(stack): adopt comparison criterion boundary
seonghobae Sep 15, 2026
7653023
chore(stack): adopt comparison criterion action
seonghobae Sep 15, 2026
9aecd55
chore(stack): converge comparison post lane onto repaired #1033
seonghobae Sep 15, 2026
89adb85
chore(stack): converge comparison post lane onto localized #1033
seonghobae Sep 15, 2026
9ea8e9e
fix(i18n): compose localized comparison marker names
seonghobae Sep 15, 2026
4078f76
chore(stack): converge repaired #1033 into #1034
seonghobae Sep 15, 2026
708e435
chore(stack): converge #1033 into #1034 without force
seonghobae Sep 15, 2026
603e255
merge: converge comparison post lane onto repaired #1033 parent
seonghobae Sep 15, 2026
0171755
chore(stack): converge #1034 onto repaired #1033
seonghobae Sep 15, 2026
b9dec94
chore(stack): converge #1034 onto current #1033
seonghobae Sep 15, 2026
9946cb4
merge(reports): converge report-axis missingness repair into comparis…
seonghobae Sep 15, 2026
b916ec6
merge(reports): converge #1034 onto #1033 RED contract head
seonghobae Sep 15, 2026
d2e0eb6
chore(stack): converge current #1033 into #1034
seonghobae Sep 15, 2026
f2982ab
chore(stack): converge current #1033 into #1034
seonghobae Sep 15, 2026
c31ae62
chore(stack): converge semantic RED harness onto #1034
seonghobae Sep 16, 2026
bca5135
chore(stack): converge #1033 product repair into #1034
seonghobae Sep 16, 2026
d09dd1c
chore(stack): converge #1034 on repaired tick foundation
seonghobae Sep 16, 2026
f47c1cf
chore(stack): converge #1034 on repaired comparison tick foundation
seonghobae Sep 16, 2026
ee649c9
chore(stack): converge #1034 on repaired comparison-strip tick founda…
seonghobae Sep 16, 2026
6bda129
chore(stack): converge #1034 on repaired report-axis tick foundation
seonghobae Sep 16, 2026
b8b5075
chore(stack): converge #1034 on repaired comparison tick share founda…
seonghobae Sep 16, 2026
c1ad80e
chore(stack): converge #1034 on repaired tick-share foundation
seonghobae Sep 16, 2026
f5b4620
chore(stack): converge #1034 on repaired #1033 contracts
seonghobae Sep 16, 2026
449a0aa
test(reports): execute comparison post accessibility contract
seonghobae Sep 16, 2026
e2b6724
test(reports): preserve non-plottable post open action
seonghobae Sep 16, 2026
825f47a
Merge parent #1033 convergence into #1034
seonghobae Sep 17, 2026
d5b00f0
Merge current #1033 tick evidence into #1034
seonghobae Sep 18, 2026
8b4df5e
Merge #1033 comparison tick contract repair into #1034
seonghobae Sep 18, 2026
4fb7122
merge: converge report-axis tick behavior into post-coordinate lane
seonghobae Sep 19, 2026
8a26c4f
chore(stack): converge repaired comparison tick contract ancestry
seonghobae Sep 19, 2026
bb664b7
merge: converge #1034 on report contract repairs
seonghobae Sep 19, 2026
53950a8
chore(stack): converge #1034 onto #1033 owner stack
seonghobae Sep 20, 2026
17ffe7b
chore(stack): converge #1034 onto current #1033 head
seonghobae Sep 20, 2026
0c1a972
fix(stack): preserve current #1033 tree in #1034 convergence
seonghobae Sep 20, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { LeftoverPair } from "../api";
import { setLocale } from "../i18n";
import { LeftoverMapPlot } from "./LeftoverMapPlot";
import { LeftoverPairList } from "./LeftoverPairList";

const PAIR: LeftoverPair = {
pair_kind: "closest",
post_id: "post-demo-public",
post_title: "Public post",
criterion_code: "sales_lead_quality",
leftover_distance: 0.12,
leftover_residual: 0.4,
leftover_map_person_axis_1: 0.5,
leftover_map_person_axis_2: 0.1,
leftover_map_item_axis_1: 0.5,
leftover_map_item_axis_2: -0.02,
};

const PAIR_WITHOUT_PERSON_COORDINATES: LeftoverPair = {
...PAIR,
leftover_map_person_axis_1: null,
};

describe("LeftoverMapPlot comparison post accessibility", () => {
afterEach(() => setLocale("en"));

it("composes the comparison marker name from existing localized copy", () => {
setLocale("ko");
render(
<LeftoverMapPlot
pairs={[PAIR]}
criterionLabel={() => "sales-lead"}
onSelectPost={vi.fn()}
variant="comparison"
/>,
);

expect(
screen.getByRole("button", {
name: "잔여 지도 비교 그림: 잔여 지도 글 Public post 열기 (ξ (+0.50, +0.10))",
}),
).toBeInTheDocument();
});

it("does not invent a post marker when persisted person coordinates are unavailable", () => {
const { container } = render(
<LeftoverMapPlot
pairs={[PAIR_WITHOUT_PERSON_COORDINATES]}
criterionLabel={() => "sales-lead"}
onSelectPost={vi.fn()}
variant="comparison"
/>,
);

expect(container).toBeEmptyDOMElement();
});

it("keeps the pair-button open action when missing coordinates make the plot non-plottable", () => {
render(
<LeftoverPairList
pairs={[PAIR_WITHOUT_PERSON_COORDINATES]}
criterionLabel={() => "sales-lead"}
onSelectPost={vi.fn()}
/>,
);

expect(screen.queryByLabelText("Leftover-map graphic display")).not.toBeInTheDocument();
expect(
screen.getByRole("button", {
name: "Open leftover closest pair: Public post · sales-lead",
}),
).toBeInTheDocument();
});
});
72 changes: 43 additions & 29 deletions frontend/src/components/LeftoverMapPlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
firstPlottablePairForPost,
layoutLeftoverMapPlot,
leftoverMapComparePlotCriterionBadge,
leftoverMapComparePlotPostBadge,
leftoverMapPlotCriterionBadge,
LEFTOVER_MAP_COMPARE_PLOT_CAPTION,
LEFTOVER_MAP_COMPARE_PLOT_LABEL,
Expand Down Expand Up @@ -148,12 +149,31 @@ function leftoverMapPlotCriterionText(
return `${t("Criterion ζ")} ${badge.values.label} · ζ ${badge.values.item}`;
}

function leftoverMapPlotPostText(
marker: { label: string; axis1: number; axis2: number },
variant: LeftoverMapPlotVariant,
): string {
if (variant === "comparison") {
const badge = leftoverMapComparePlotPostBadge(marker.label, marker.axis1, marker.axis2);
if (badge === null) {
return `${t(LEFTOVER_MAP_COMPARE_PLOT_LABEL)}: ${t("Post ξ")} ${marker.label}`;
}
return `${t(LEFTOVER_MAP_COMPARE_PLOT_LABEL)}: ${tf(LEFTOVER_MAP_PLOT_POST_ACTION, badge.values)}`;
}
const person = formatLeftoverMapCoordinatePair(marker.axis1, marker.axis2) ?? "";
return tf(LEFTOVER_MAP_PLOT_POST_ACTION, {
title: marker.label,
person,
});
}

/**
* Gabriel leftover-map graphic display of persisted ``ξ_{1:2}`` / ``ζ_{1:2}``.
*
* Person markers are posts; item markers are leftover criteria. Report and
* comparison criterion markers keep distinct accessible names while composing
* the already-localized criterion label with persisted ζ.
* the already-localized criterion label with persisted ζ. Comparison post
* markers name only persisted finite person axes with distinct accessible copy.
* Click a post marker to open that post. Caption leftover-map axes with persisted
* Gabriel inertia share when finite, including rank-0 zero-share axes.
* Report and comparison graphic axes additionally name finite, non-negative
Expand Down Expand Up @@ -605,34 +625,28 @@ export function LeftoverMapPlot({
</text>
</g>
))}
{layout.persons.map((marker) => {
const person = formatLeftoverMapCoordinatePair(marker.axis1, marker.axis2) ?? "";
return (
<g
key={`person:${marker.id}`}
className="leftover-map-plot-marker"
role="button"
tabIndex={0}
aria-label={tf(LEFTOVER_MAP_PLOT_POST_ACTION, {
title: marker.label,
person,
})}
onClick={() => openPost(marker.id)}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
openPost(marker.id);
}
}}
>
<circle className="leftover-map-plot-person-hit" cx={marker.x} cy={marker.y} r={22} />
<circle className="leftover-map-plot-person" cx={marker.x} cy={marker.y} r={6} />
<text className="leftover-map-plot-label" x={marker.x + 10} y={marker.y - 10}>
{marker.label}
</text>
</g>
);
})}
{layout.persons.map((marker) => (
<g
key={`person:${marker.id}`}
className="leftover-map-plot-marker"
role="button"
tabIndex={0}
aria-label={leftoverMapPlotPostText(marker, variant)}
onClick={() => openPost(marker.id)}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
openPost(marker.id);
}
}}
>
<circle className="leftover-map-plot-person-hit" cx={marker.x} cy={marker.y} r={22} />
<circle className="leftover-map-plot-person" cx={marker.x} cy={marker.y} r={6} />
<text className="leftover-map-plot-label" x={marker.x + 10} y={marker.y - 10}>
{marker.label}
</text>
</g>
))}
</svg>
</div>
</figure>
Expand Down
24 changes: 24 additions & 0 deletions frontend/src/leftoverMapPlotLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,30 @@ export function leftoverMapComparePlotCriterionBadge(
return { key: LEFTOVER_MAP_COMPARE_PLOT_CRITERION, values: { label, item } };
}

export const LEFTOVER_MAP_PLOT_POST_ACTION_OMITTED =
"Open leftover-map post {title}";

export const LEFTOVER_MAP_COMPARE_PLOT_POST_ACTION =
"Open leftover map comparison graphic leftover-map post {title} at ξ {person}";

export type LeftoverMapPlotPostBadge = {
key: string;
values: { title: string; person: string };
};

/** Name only finite persisted comparison-post person coordinates; never infer ξ from item coordinates. */
export function leftoverMapComparePlotPostBadge(
title: string,
axis1: number | null | undefined,
axis2: number | null | undefined,
): LeftoverMapPlotPostBadge | null {
const person = formatLeftoverMapCoordinatePair(axis1, axis2);
if (person === null) {
return null;
}
return { key: LEFTOVER_MAP_COMPARE_PLOT_POST_ACTION, values: { title, person } };
}

export const LEFTOVER_MAP_PLOT_TICK =
"leftover-map axis {axis} tick {value}";

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Executable contract for comparison-graphic post person-coordinate captions."""

from pathlib import Path
import re


ROOT = Path(__file__).resolve().parents[1]
LAYOUT_SOURCE = ROOT / "frontend" / "src" / "leftoverMapPlotLayout.ts"
PLOT_SOURCE = ROOT / "frontend" / "src" / "components" / "LeftoverMapPlot.tsx"


def test_comparison_graphic_post_names_persisted_person_coordinates() -> None:
"""Comparison post markers use distinct ξ copy without changing report markers."""
layout_source = LAYOUT_SOURCE.read_text(encoding="utf-8")
plot_source = PLOT_SOURCE.read_text(encoding="utf-8")

assert (
'LEFTOVER_MAP_COMPARE_PLOT_POST_ACTION =\n'
' "Open leftover map comparison graphic leftover-map post {title} at ξ {person}";'
in layout_source
)
assert "leftoverMapComparePlotPostBadge" in layout_source
assert "leftoverMapComparePlotPostBadge" in plot_source
assert "LEFTOVER_MAP_PLOT_POST_ACTION" in plot_source
assert re.search(
r'variant\s*===\s*"comparison".*?leftoverMapComparePlotPostBadge.*?LEFTOVER_MAP_PLOT_POST_ACTION',
plot_source,
re.DOTALL,
)


def test_comparison_post_badge_consumes_person_axes_fail_closed_without_item_inference() -> None:
"""Comparison ξ comes only from finite persisted person axes; zero remains explicit."""
layout_source = LAYOUT_SOURCE.read_text(encoding="utf-8")
helper = re.search(
r"export function leftoverMapComparePlotPostBadge\(.*?\n}\n",
layout_source,
re.DOTALL,
)
assert helper is not None
helper_source = helper.group(0)

assert "formatLeftoverMapCoordinatePair(axis1, axis2)" in helper_source
assert "person === null" in helper_source
assert "return null" in helper_source
assert "LEFTOVER_MAP_COMPARE_PLOT_POST_ACTION" in helper_source
assert "leftover_map_item_axis" not in helper_source
assert "Math.sqrt" not in helper_source
assert "Math.abs" not in helper_source
Loading