diff --git a/AGENTS.md b/AGENTS.md
index b36b72460..fbe39343d 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -191,7 +191,9 @@ Period leftover pairs (ADR 0048 / 0049) are computed in
GRM/GPCM score, never invented. Missing cells stay out of the
Gabriel factorization. Closest and farthest post–criterion pairs
persist to `report_leftover_pair` and sit above the member list so
-a click opens that post.
+a click opens that post. Pair-member leftover-map criterion nodes
+(ADR 0126) also open that leftover-pair post; non-pair criteria stay
+non-interactive.
`frontend/` has its own toolchain (Node pinned via `frontend/mise.toml`,
pnpm via Corepack -- do not add a second Node package manager or a
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 41b84d1e9..e4edce05c 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -604,7 +604,8 @@ through opens those DAG posts. Report members include the earliest
open ticket title, status lookup label, and due date when one exists. The home page renders
the actual mean θ, the FIPC delta, the CAT-selected item, leftover
closest/farthest pairs and the leftover interaction map above the
-member list, and the
+member list (click a leftover-map person or pair-member criterion
+node to open that post), and the
PU / corp / thread comparison -- never a placeholder. TEPP is unchanged.
## Phase 6b: Knowledge Graph as a real Ontology + Semantic Layer
diff --git a/CHANGELOG.d/2.12.10-leftover-map-criterion-node.md b/CHANGELOG.d/2.12.10-leftover-map-criterion-node.md
new file mode 100644
index 000000000..4c217cabb
--- /dev/null
+++ b/CHANGELOG.d/2.12.10-leftover-map-criterion-node.md
@@ -0,0 +1,8 @@
+## 2.12.10 — Leftover-map criterion nodes
+
+- Pair-member leftover-map criterion nodes open the leftover-pair post
+ (closest preferred, then farthest). Non-pair criteria stay
+ non-interactive diamonds so the map does not invent a next action
+ (ADR 0126).
+- Keyboard Enter/Space matches leftover-map person nodes. Click does
+ not land leftover focus on Post quality.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 98977ee27..8914339a9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,15 @@ All notable changes to this project are documented here. Format follows
environment, so local OIDC and synthetic-data workflows resolve the same
pinned dependencies as CI.
+## [2.12.10] - 2026-08-23
+
+### Added
+
+- Pair-member leftover-map criterion nodes now open the leftover-pair
+ post (closest preferred, then farthest). Non-pair criteria stay
+ non-interactive. Click does not set leftover focus on Post quality
+ (ADR 0126).
+
## [2.12.7] - 2026-08-23
### Added
diff --git a/docs/adr/0121-persist-leftover-interaction-map.md b/docs/adr/0121-persist-leftover-interaction-map.md
index 9257a160e..213fe5cc4 100644
--- a/docs/adr/0121-persist-leftover-interaction-map.md
+++ b/docs/adr/0121-persist-leftover-interaction-map.md
@@ -40,7 +40,8 @@ fabricated interaction.
Closest and farthest pairs remain ADR 0048 / ADR 0049. The map sits
**above** that pair list on the period-report group. Clicking a
person node opens that post with the same handler as a leftover
-pair. Hidden posts stay hidden: leftover-map persons join
+pair. Pair-member criterion nodes open that leftover-pair post
+([ADR 0126](0126-leftover-map-criterion-node.md)). Hidden posts stay hidden: leftover-map persons join
`source_post` and use the same ABAC gate as members and leftover
pairs. Missing map rows render nothing.
diff --git a/docs/adr/0126-leftover-map-criterion-node.md b/docs/adr/0126-leftover-map-criterion-node.md
new file mode 100644
index 000000000..8e119fe9a
--- /dev/null
+++ b/docs/adr/0126-leftover-map-criterion-node.md
@@ -0,0 +1,60 @@
+# ADR 0126 — Leftover-map criterion nodes open the leftover-pair post
+
+**Decision status:** Accepted
+**Date:** 2026-08-23
+
+## Context
+
+ADR 0121 persists leftover interaction-map coordinates and renders a
+2D Gabriel biplot above leftover pairs. Person (post) nodes are
+buttons that open that post. Criterion (item) nodes are diamonds
+without a next action: a buyer who sees a highlighted closest or
+farthest criterion cannot act on it.
+
+ADR 0125 lands leftover-pair *list* clicks on Post quality with a
+leftover-focus flag. This increment is independent of that landing.
+The map criterion node opens the leftover-pair post only. It does
+not set leftover focus or `aria-current` on Post quality.
+
+A criterion that is not a leftover-pair member has no buyer next
+action. Inventing a click that opens an arbitrary post would
+fabricate a pair.
+
+## Decision
+
+Export `leftoverPairForCriterion(pairs, criterionCode)`. Prefer the
+closest leftover pair for that criterion, then farthest. If none,
+the criterion stays a non-interactive diamond.
+
+When a pair exists, the criterion node is `role="button"`, keyboard
+activable (Enter / Space), and named `Open leftover map criterion:
+{label}`. Activation calls `onSelectPost(pair.post_id)` — the same
+handler as a leftover-map person node and leftover-pair list button
+on this stack.
+
+Do not pass leftover-focus flags. Hidden posts stay hidden because
+the pair's `post_id` is already ABAC-filtered with leftover pairs.
+
+## Consequences
+
+Buyers can click a highlighted leftover-map criterion and read the
+post that sat closest (or farthest) from it after IRT main effects.
+Non-pair criteria remain visual context on the Gabriel biplot.
+
+## Related
+
+Depends on [ADR 0121](0121-persist-leftover-interaction-map.md),
+[ADR 0048](0048-persist-lsirm-leftover-pairs.md), and
+[ADR 0049](0049-leftover-pair-report-ui.md). Independent of leftover
+criterion evaluation landing.
+
+## References
+
+Gabriel, K. R. (1971). The biplot graphic display of matrices with
+application to principal component analysis. *Biometrika, 58*(3),
+453–467. https://doi.org/10.1093/biomet/58.3.453
+
+Jeon, M., Jin, I. H., Schweinberger, M., & Baugh, S. (2021). Mapping
+unobserved item–respondent interactions: A latent space item response
+model with interaction map. *Psychometrika, 86*(2), 378–403.
+https://doi.org/10.1007/s11336-021-09762-5
diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md
index 28c59bd48..ea8a60cce 100644
--- a/docs/storybook-inventory.md
+++ b/docs/storybook-inventory.md
@@ -9,6 +9,7 @@ buyer-facing control you can click before changing product CSS.
| `AnalysisRun/CutoffKnownBody` | Read the cutoff-known sentence, then compare it with the live body below. | `--color-accent-border`, `--space-panel-block`, `--radius-panel`, `CutoffKnownBody` |
| `Analysis/LineageEntityPicker` | Choose which corp to reconstruct, then click Request a lineage reconstruction. | `--space-control-gap`, `--size-control-min`, `--radius-control`, `LineageEntityPicker` |
| `Chrome/PopupCloseButton` | Close the evidence panel or post popup. | `--space-close-inset`, `--font-size-close`, `PopupCloseButton` |
+| `Reports/LeftoverInteractionMap` | Click a leftover-pair criterion diamond to open that pair's post. | leftover-map item/person tokens, `LeftoverInteractionMap` |
Repeated web objects must use `frontend/src/styles/tokens.css` and a module
under `frontend/src/components/`. Do not add a second Node package manager;
diff --git a/frontend/package.json b/frontend/package.json
index cea549188..95c2ab3d6 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
- "version": "2.12.7",
+ "version": "2.12.10",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/frontend/src/App.css b/frontend/src/App.css
index a3816814a..76b3a71b7 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -567,6 +567,10 @@
stroke-width: 1.5;
}
+.leftover-map-item[role="button"] {
+ cursor: pointer;
+}
+
.leftover-map-item rect {
fill: var(--badge-actor-organization-bg);
stroke: var(--badge-actor-organization-text);
@@ -600,6 +604,15 @@
stroke-width: 2.5;
}
+.leftover-map-item[role="button"]:focus {
+ outline: none;
+}
+
+.leftover-map-item[role="button"]:focus rect,
+.leftover-map-item[role="button"]:hover rect {
+ stroke-width: 2.5;
+}
+
.lineage-dag-node:focus {
outline: none;
}
diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx
index 07d5748e3..baf7bdc5a 100644
--- a/frontend/src/App.test.tsx
+++ b/frontend/src/App.test.tsx
@@ -3451,6 +3451,16 @@ describe("App, authenticated", () => {
await waitFor(() => expect(screen.getByText("The full body text.")).toBeInTheDocument());
});
+ it("opens a leftover-map criterion post from the report panel", async () => {
+ stubBackend();
+ render();
+
+ await userEvent.click(
+ await screen.findByRole("button", { name: "Open leftover map criterion: sales-lead" }),
+ );
+ await waitFor(() => expect(screen.getByText("The full body text.")).toBeInTheDocument());
+ });
+
it("opens Event Lineage, Keyman, and evaluation from a report member click", async () => {
stubBackend();
render();
diff --git a/frontend/src/LeftoverInteractionMap.stories.tsx b/frontend/src/LeftoverInteractionMap.stories.tsx
new file mode 100644
index 000000000..b8c527cda
--- /dev/null
+++ b/frontend/src/LeftoverInteractionMap.stories.tsx
@@ -0,0 +1,76 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { LeftoverInteractionMap } from "./LeftoverInteractionMap";
+
+const itemLabel = (code: string) =>
+ code === "sales_lead_specificity" ? "sales-lead" : code.replaceAll("_", " ");
+
+const meta = {
+ title: "Reports/LeftoverInteractionMap",
+ component: LeftoverInteractionMap,
+ args: {
+ persons: [
+ { post_id: "post-1", post_title: "Public post", axis_one: -0.5, axis_two: 0.1 },
+ {
+ post_id: "post-2",
+ post_title: "Specification revision requested",
+ axis_one: 0.8,
+ axis_two: -0.4,
+ },
+ ],
+ items: [
+ { criterion_code: "sales_lead_specificity", axis_one: -0.4, axis_two: 0.05 },
+ { criterion_code: "general_sentiment_negative", axis_one: 1.2, axis_two: -0.9 },
+ { criterion_code: "general_sentiment_positive", axis_one: 0.1, axis_two: 0.7 },
+ ],
+ pairs: [
+ {
+ pair_kind: "closest",
+ post_id: "post-1",
+ post_title: "Public post",
+ criterion_code: "sales_lead_specificity",
+ leftover_distance: 0.12,
+ leftover_residual: 0.4,
+ },
+ {
+ pair_kind: "farthest",
+ post_id: "post-2",
+ post_title: "Specification revision requested",
+ criterion_code: "general_sentiment_negative",
+ leftover_distance: 1.84,
+ leftover_residual: -1.1,
+ },
+ ],
+ itemLabel,
+ onSelectPost: () => undefined,
+ },
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+export const ClosestFarthest: Story = {};
+
+export const CriterionClick: Story = {
+ args: {
+ items: [{ criterion_code: "sales_lead_specificity", axis_one: -0.4, axis_two: 0.05 }],
+ pairs: [
+ {
+ pair_kind: "closest",
+ post_id: "post-1",
+ post_title: "Public post",
+ criterion_code: "sales_lead_specificity",
+ leftover_distance: 0.12,
+ leftover_residual: 0.4,
+ },
+ ],
+ },
+};
+
+export const OriginPad: Story = {
+ args: {
+ persons: [{ post_id: "post-1", post_title: "Public post", axis_one: 0, axis_two: 0 }],
+ items: [{ criterion_code: "sales_lead_specificity", axis_one: 0, axis_two: 0 }],
+ pairs: [],
+ },
+};
diff --git a/frontend/src/LeftoverInteractionMap.test.tsx b/frontend/src/LeftoverInteractionMap.test.tsx
index 2e334e4bf..d6a60c8b2 100644
--- a/frontend/src/LeftoverInteractionMap.test.tsx
+++ b/frontend/src/LeftoverInteractionMap.test.tsx
@@ -1,10 +1,63 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
-import { LeftoverInteractionMap, projectLeftoverMap } from "./LeftoverInteractionMap";
+import {
+ LeftoverInteractionMap,
+ leftoverPairForCriterion,
+ projectLeftoverMap,
+} from "./LeftoverInteractionMap";
+import type { LeftoverPair } from "./api";
const itemLabel = (code: string) => (code === "sales_lead_specificity" ? "sales-lead" : code);
+const closestPair: LeftoverPair = {
+ pair_kind: "closest",
+ post_id: "post-1",
+ post_title: "Public post",
+ criterion_code: "sales_lead_specificity",
+ leftover_distance: 0.12,
+ leftover_residual: 0.4,
+};
+
+const farthestPair: LeftoverPair = {
+ pair_kind: "farthest",
+ post_id: "post-2",
+ post_title: "Specification revision requested",
+ criterion_code: "general_sentiment_negative",
+ leftover_distance: 1.84,
+ leftover_residual: -1.1,
+};
+
+describe("leftoverPairForCriterion", () => {
+ it("prefers the closest leftover pair for a criterion", () => {
+ const both: LeftoverPair[] = [
+ farthestPair,
+ closestPair,
+ {
+ ...farthestPair,
+ pair_kind: "farthest",
+ post_id: "post-9",
+ criterion_code: "sales_lead_specificity",
+ },
+ ];
+ expect(leftoverPairForCriterion(both, "sales_lead_specificity")).toMatchObject({
+ pair_kind: "closest",
+ post_id: "post-1",
+ });
+ });
+
+ it("uses the farthest leftover pair when the criterion is not closest", () => {
+ expect(leftoverPairForCriterion([closestPair, farthestPair], "general_sentiment_negative")).toMatchObject({
+ pair_kind: "farthest",
+ post_id: "post-2",
+ });
+ });
+
+ it("returns null when the criterion is not a leftover-pair member", () => {
+ expect(leftoverPairForCriterion([closestPair, farthestPair], "general_sentiment_positive")).toBeNull();
+ });
+});
+
describe("LeftoverInteractionMap", () => {
it("projects coincident origin points to the map center", () => {
const projected = projectLeftoverMap(
@@ -37,32 +90,66 @@ describe("LeftoverInteractionMap", () => {
{ criterion_code: "sales_lead_specificity", axis_one: -0.4, axis_two: 0.05 },
{ criterion_code: "general_sentiment_negative", axis_one: 1.2, axis_two: -0.9 },
]}
- pairs={[
- {
- pair_kind: "closest",
- post_id: "post-1",
- post_title: "Public post",
- criterion_code: "sales_lead_specificity",
- leftover_distance: 0.12,
- leftover_residual: 0.4,
- },
+ pairs={[closestPair, farthestPair]}
+ itemLabel={itemLabel}
+ onSelectPost={onSelectPost}
+ />,
+ );
+
+ expect(screen.getByRole("group", { name: "Leftover interaction map" })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "Open leftover map post: Public post" })).toBeInTheDocument();
+ await userEvent.click(screen.getByRole("button", { name: "Open leftover map post: Public post" }));
+ expect(onSelectPost).toHaveBeenCalledWith("post-1");
+ });
+
+ it("opens the leftover-pair post from a pair-member criterion node", async () => {
+ const onSelectPost = vi.fn();
+ render(
+ ,
);
- expect(screen.getByRole("group", { name: "Leftover interaction map" })).toBeInTheDocument();
- expect(screen.getByRole("button", { name: "Open leftover map post: Public post" })).toBeInTheDocument();
- await userEvent.click(screen.getByRole("button", { name: "Open leftover map post: Public post" }));
+ const criterion = screen.getByRole("button", { name: "Open leftover map criterion: sales-lead" });
+ expect(criterion).toBeInTheDocument();
+ await userEvent.click(criterion);
+ expect(onSelectPost).toHaveBeenCalledWith("post-1");
+ expect(
+ screen.queryByRole("button", { name: "Open leftover map criterion: general_sentiment_positive" }),
+ ).not.toBeInTheDocument();
+ });
+
+ it("opens a leftover-pair post from a criterion node with the keyboard", async () => {
+ const onSelectPost = vi.fn();
+ render(
+ ,
+ );
+
+ const criterion = screen.getByRole("button", { name: "Open leftover map criterion: sales-lead" });
+ criterion.focus();
+ await userEvent.keyboard("{Enter}");
expect(onSelectPost).toHaveBeenCalledWith("post-1");
});
diff --git a/frontend/src/LeftoverInteractionMap.tsx b/frontend/src/LeftoverInteractionMap.tsx
index 05176ca78..e492325d5 100644
--- a/frontend/src/LeftoverInteractionMap.tsx
+++ b/frontend/src/LeftoverInteractionMap.tsx
@@ -1,4 +1,5 @@
import type { LeftoverMapItem, LeftoverMapPerson, LeftoverPair } from "./api";
+import { t, tf } from "./i18n";
const MAP_WIDTH = 360;
const MAP_HEIGHT = 240;
@@ -29,6 +30,20 @@ function pairKindsFor(
return [...kinds];
}
+export function leftoverPairForCriterion(
+ pairs: LeftoverPair[],
+ criterionCode: string,
+): LeftoverPair | null {
+ let closest: LeftoverPair | null = null;
+ let farthest: LeftoverPair | null = null;
+ for (const pair of pairs) {
+ if (pair.criterion_code !== criterionCode) continue;
+ if (pair.pair_kind === "closest" && closest === null) closest = pair;
+ if (pair.pair_kind === "farthest" && farthest === null) farthest = pair;
+ }
+ return closest ?? farthest;
+}
+
export function projectLeftoverMap(
persons: LeftoverMapPerson[],
items: LeftoverMapItem[],
@@ -100,6 +115,17 @@ function truncateLabel(label: string): string {
return label.length > 22 ? `${label.slice(0, 21)}…` : label;
}
+function activateLeftoverMapNode(
+ event: { key: string; preventDefault: () => void },
+ postId: string,
+ onSelectPost: (postId: string) => void,
+) {
+ if (event.key === "Enter" || event.key === " ") {
+ event.preventDefault();
+ onSelectPost(postId);
+ }
+}
+
export function LeftoverInteractionMap({
persons,
items,
@@ -121,13 +147,13 @@ export function LeftoverInteractionMap({
const itemById = Object.fromEntries(projected.items.map((point) => [point.id, point]));
return (
- Leftover interaction map after main effects
+ {t("Leftover interaction map after main effects")}
diff --git a/frontend/src/i18n.test.ts b/frontend/src/i18n.test.ts
index 5a3afbfc3..2a1e16aa2 100644
--- a/frontend/src/i18n.test.ts
+++ b/frontend/src/i18n.test.ts
@@ -66,13 +66,14 @@ describe("i18n", () => {
});
it.each([
- ["ko", "DEMO은(는) 이벤트 계보의 현재 항목입니다. 다음으로 Keyman과 평가를 읽으세요."],
- ["zh", "DEMO 是事件谱系中的当前记录。接下来查看关键联系人和评估。"],
- ["ja", "DEMOはイベント系譜の現在の記録です。次にキーパーソンと評価を確認してください。"],
- ["vi", "DEMO là bản ghi hiện tại trong Dòng sự kiện. Hãy xem người liên hệ chính và đánh giá tiếp theo."],
- ] as const)("formats dynamic buyer guidance in %s", (locale, expected) => {
+ ["ko", "잔여 맵 평가 항목 열기: sales-lead"],
+ ["zh", "打开残差图评估项:sales-lead"],
+ ["ja", "残差マップの評価項目を開く: sales-lead"],
+ ["vi", "Mở tiêu chí bản đồ phần dư: sales-lead"],
+ ] as const)("translates leftover-map criterion next action in %s", (locale, expected) => {
setLocale(locale);
- expect(tf("{post} is current in Event Lineage. Read Keyman and evaluation next.", { post: "DEMO" })).toBe(expected);
+ expect(tf("Open leftover map criterion: {label}", { label: "sales-lead" })).toBe(expected);
+ expect(t("Leftover interaction map")).not.toBe("Leftover interaction map");
});
});
diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts
index 650acfca8..4ed14c96d 100644
--- a/frontend/src/i18n.ts
+++ b/frontend/src/i18n.ts
@@ -313,6 +313,14 @@ const TRANSLATIONS: Partial>> = {
"Related next action": "관련 대상 다음 작업",
"Ask next action": "Ask 다음 작업",
"Keyman next action": "Keyman 다음 작업",
+ "Leftover interaction map": "잔여 상호작용 맵",
+ "Leftover interaction map after main effects": "주효과 이후 잔여 상호작용 맵",
+ "Open leftover map post: {label}": "잔여 맵 글 열기: {label}",
+ "Open leftover map criterion: {label}": "잔여 맵 평가 항목 열기: {label}",
+ "Open this leftover map criterion to read the leftover pair post: {label}":
+ "이 잔여 맵 평가 항목을 열어 잔여 쌍 글을 읽으세요: {label}",
+ "Open this post on the leftover map: {label}": "잔여 맵에서 이 글을 여세요: {label}",
+ "Criterion: {label}": "평가 항목: {label}",
"Ask seeded question: {question}": "시드 질문: {question}",
"{post} is current in Event Lineage. Read Keyman and evaluation next.":
"{post}은(는) 이벤트 계보의 현재 항목입니다. 다음으로 Keyman과 평가를 읽으세요.",
@@ -651,6 +659,14 @@ const TRANSLATIONS: Partial>> = {
"Related next action": "相关节点操作",
"Ask next action": "Ask 操作",
"Keyman next action": "关键联系人操作",
+ "Leftover interaction map": "残差交互图",
+ "Leftover interaction map after main effects": "主效应后的残差交互图",
+ "Open leftover map post: {label}": "打开残差图帖子:{label}",
+ "Open leftover map criterion: {label}": "打开残差图评估项:{label}",
+ "Open this leftover map criterion to read the leftover pair post: {label}":
+ "打开此残差图评估项以阅读残差配对帖子:{label}",
+ "Open this post on the leftover map: {label}": "在残差图上打开这篇帖子:{label}",
+ "Criterion: {label}": "评估项:{label}",
"Ask seeded question: {question}": "种子问题:{question}",
"{post} is current in Event Lineage. Read Keyman and evaluation next.":
"{post} 是事件谱系中的当前记录。接下来查看关键联系人和评估。",
@@ -989,6 +1005,14 @@ const TRANSLATIONS: Partial>> = {
"Related next action": "関連ノードの操作",
"Ask next action": "Askの操作",
"Keyman next action": "キーパーソンの操作",
+ "Leftover interaction map": "残差インタラクションマップ",
+ "Leftover interaction map after main effects": "主効果後の残差インタラクションマップ",
+ "Open leftover map post: {label}": "残差マップの投稿を開く: {label}",
+ "Open leftover map criterion: {label}": "残差マップの評価項目を開く: {label}",
+ "Open this leftover map criterion to read the leftover pair post: {label}":
+ "この残差マップ評価項目を開いて残差ペアの投稿を読む: {label}",
+ "Open this post on the leftover map: {label}": "残差マップでこの投稿を開く: {label}",
+ "Criterion: {label}": "評価項目: {label}",
"Ask seeded question: {question}": "シードされた質問: {question}",
"{post} is current in Event Lineage. Read Keyman and evaluation next.":
"{post}はイベント系譜の現在の記録です。次にキーパーソンと評価を確認してください。",
@@ -1327,6 +1351,14 @@ const TRANSLATIONS: Partial>> = {
"Related next action": "Thao tác nút liên quan",
"Ask next action": "Thao tác Ask",
"Keyman next action": "Thao tác người liên hệ chính",
+ "Leftover interaction map": "Bản đồ tương tác phần dư",
+ "Leftover interaction map after main effects": "Bản đồ tương tác phần dư sau hiệu ứng chính",
+ "Open leftover map post: {label}": "Mở bài viết bản đồ phần dư: {label}",
+ "Open leftover map criterion: {label}": "Mở tiêu chí bản đồ phần dư: {label}",
+ "Open this leftover map criterion to read the leftover pair post: {label}":
+ "Mở tiêu chí bản đồ phần dư này để đọc bài viết cặp phần dư: {label}",
+ "Open this post on the leftover map: {label}": "Mở bài viết này trên bản đồ phần dư: {label}",
+ "Criterion: {label}": "Tiêu chí: {label}",
"Ask seeded question: {question}": "Câu hỏi mồi: {question}",
"{post} is current in Event Lineage. Read Keyman and evaluation next.":
"{post} là bản ghi hiện tại trong Dòng sự kiện. Hãy xem người liên hệ chính và đánh giá tiếp theo.",
diff --git a/pyproject.toml b/pyproject.toml
index 7e230986f..da7b0ed0a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "lineageweave"
-version = "2.12.7"
+version = "2.12.10"
description = "Reconstructs git-branch-style lineage DAGs from scattered short records using multi-channel score fusion and LLM adjudication."
readme = "README.md"
license = { text = "MIT" }
diff --git a/uv.lock b/uv.lock
index b4288a2c6..0a24808fa 100644
--- a/uv.lock
+++ b/uv.lock
@@ -454,7 +454,7 @@ wheels = [
[[package]]
name = "lineageweave"
-version = "2.12.7"
+version = "2.12.10"
source = { editable = "." }
dependencies = [
{ name = "certifi" },