From bf9daa9810ad31e98cf548334c566b75a7f0cffc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 19:32:42 +0900 Subject: [PATCH 1/7] feat: show Our side / Counterparty on related-node person chips (v0.72.0) Related chips still said "Ada West (Person)" even though the payload already had person_side_code. Attach person_side_label from common_lookup_value and use that caption (and accessible name) so the buyer sees Our side / Counterparty, not the ontology class. --- ARCHITECTURE.md | 4 +++- CHANGELOG.md | 12 ++++++++++ backend/app/knowledge_graph.py | 8 ++++++- backend/tests/test_api.py | 5 +++++ frontend/package.json | 2 +- frontend/src/App.test.tsx | 41 +++++++++++++++++++++++++++------- frontend/src/App.tsx | 16 +++++++++++-- frontend/src/api.ts | 1 + lineageweave/__init__.py | 2 +- pyproject.toml | 2 +- uv.lock | 2 +- 11 files changed, 79 insertions(+), 16 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 617b8b95d..37c10f5b9 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -320,7 +320,9 @@ is the same never-guess-a-parent rule `corporate_hierarchy_resolution` already applies. Entity levels and Keyman sides are labeled from `common_lookup_value` (`Our side`, `Plant`, `Company`) so the popup never shows raw `our_side` / `plant` -codes when a label exists. +codes when a label exists. Related-node person chips use the same +side label (`Ada West (Our side)`), not the ontology class +(`Ada West (Person)`). `GET /api/posts` and `GET /api/posts/{post_id}` include `voc_type_label` / `visibility_label` from `common_lookup_value` so diff --git a/CHANGELOG.md b/CHANGELOG.md index 0096828a2..67e87e9b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ All notable changes to this project are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.72.0] - 2026-08-16 + +### Changed + +- Related-node person chips use the `person_side` lookup label instead + of the ontology class. After `make seed`, walking from Ada West + shows "Priya Nair (Counterparty)" and walking from Demo Corp shows + "Ada West (Our side)" -- not "Ada West (Person)". The payload + already had `person_side_code`; it now also carries + `person_side_label` from `common_lookup_value`. The same caption is + the button accessible name. + ## [0.71.0] - 2026-08-14 ### Added diff --git a/backend/app/knowledge_graph.py b/backend/app/knowledge_graph.py index bb398d141..6b7fc60d9 100644 --- a/backend/app/knowledge_graph.py +++ b/backend/app/knowledge_graph.py @@ -320,6 +320,10 @@ async def hydrate_related_nodes( ) } if corp_ids else {} + side_labels = await labels_for_codes( + conn, [row["person_side_code"] for row in people.values()] + ) + payload: list[dict[str, Any]] = [] for node_type_code, node_id, score in parsed: item: dict[str, Any] = { @@ -329,8 +333,10 @@ async def hydrate_related_nodes( **ontology_annotations(node_type_code), } if node_type_code == NODE_PERSON and node_id in people: + side = people[node_id]["person_side_code"] item["label"] = people[node_id]["person_name"] - item["person_side_code"] = people[node_id]["person_side_code"] + item["person_side_code"] = side + item["person_side_label"] = side_labels.get(side, side) elif node_type_code == NODE_POST and node_id in posts: item["label"] = posts[node_id]["post_title"] elif node_type_code == NODE_CORPORATE_ENTITY and node_id in corps: diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index 74ab44701..1abca6b49 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -883,6 +883,8 @@ def test_related_keymen_use_rwr_and_hide_invisible_posts(client, demo_analyst_to counterpart = by_id[seeded_db["counterpart_person_id"]] assert counterpart["ontology_label"] == "Person" assert counterpart["ontology_iri"].endswith("#Person") + assert counterpart["person_side_code"] == "counterparty" + assert counterpart["person_side_label"] == "Counterparty" own_post = by_id[seeded_db["own_private_post_id"]] assert own_post["ontology_label"] == "Post" @@ -902,6 +904,9 @@ def test_related_corporate_entity_uses_rwr_and_hides_invisible_posts( assert body["entity_name"] == "Test Corp" related_ids = {node["node_id"] for node in body["related"]} assert seeded_db["our_person_id"] in related_ids + our_person = next(node for node in body["related"] if node["node_id"] == seeded_db["our_person_id"]) + assert our_person["person_side_code"] == "our_side" + assert our_person["person_side_label"] == "Our side" assert seeded_db["other_private_post_id"] not in related_ids assert seeded_db["hidden_person_id"] not in related_ids diff --git a/frontend/package.json b/frontend/package.json index 9c84795d9..0f1fa4ce8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "0.71.0", + "version": "0.72.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 415e1419f..72e7c72f1 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -514,6 +514,8 @@ describe("App, authenticated", () => { ontology_iri: "https://contextualwisdomlab.github.io/lineageweave/ontology#Person", ontology_label: "Person", label: "Ada West", + person_side_code: "our_side", + person_side_label: "Our side", relevance: 0.4, }, ], @@ -533,6 +535,8 @@ describe("App, authenticated", () => { ontology_iri: "https://contextualwisdomlab.github.io/lineageweave/ontology#Person", ontology_label: "Person", label: "Priya Nair", + person_side_code: "counterparty", + person_side_label: "Counterparty", relevance: 0.4, }, { @@ -567,6 +571,8 @@ describe("App, authenticated", () => { ontology_iri: "https://contextualwisdomlab.github.io/lineageweave/ontology#Person", ontology_label: "Person", label: "Ada West", + person_side_code: "our_side", + person_side_label: "Our side", relevance: 0.5, }, ], @@ -969,7 +975,12 @@ describe("App, authenticated", () => { await userEvent.click(screen.getByRole("button", { name: "Related nodes for Ada West" })); await waitFor(() => expect(screen.getByText("Related to Ada West")).toBeInTheDocument()); - expect(screen.getByText("Priya Nair (Person)")).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: "Related nodes for Priya Nair (Counterparty)" }), + ).toBeInTheDocument(); + expect(screen.getByText("Related to Ada West").closest(".related-keymen")).not.toHaveTextContent( + "Priya Nair (Person)", + ); await userEvent.click(screen.getByRole("button", { name: "Open related post: Linked post" })); await waitFor(() => expect(screen.getByText("The evidence panel should show exactly this text.")).toBeInTheDocument(), @@ -982,7 +993,9 @@ describe("App, authenticated", () => { await userEvent.click(await screen.findByRole("button", { name: "View post: Public post" })); await userEvent.click(await screen.findByRole("button", { name: "R&R Keyman: Ada West" })); await waitFor(() => expect(screen.getByText("Related to Ada West")).toBeInTheDocument()); - expect(screen.getByText("Priya Nair (Person)")).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: "Related nodes for Priya Nair (Counterparty)" }), + ).toBeInTheDocument(); }); it("opens related nodes from a related corporate entity", async () => { @@ -993,7 +1006,9 @@ describe("App, authenticated", () => { await waitFor(() => expect(screen.getByText("Related to Ada West")).toBeInTheDocument()); await userEvent.click(screen.getByRole("button", { name: "Related nodes for Demo Corp" })); await waitFor(() => expect(screen.getByText("Related to Demo Corp")).toBeInTheDocument()); - expect(screen.getByText("Ada West (Person)")).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: "Related nodes for Ada West (Our side)" }), + ).toBeInTheDocument(); }); it("shows the VOC excerpt under its counterparty, not a detached list", async () => { @@ -1022,7 +1037,9 @@ describe("App, authenticated", () => { await userEvent.click(await screen.findByRole("button", { name: "View post: Public post" })); await userEvent.click(await screen.findByRole("button", { name: "VOC Keyman: Northridge Grid" })); await waitFor(() => expect(screen.getByText("Related to Priya Nair")).toBeInTheDocument()); - expect(screen.getByText("Ada West (Person)")).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: "Related nodes for Ada West (Our side)" }), + ).toBeInTheDocument(); }); it("opens related Keyman nodes from an affiliate-tree person", async () => { @@ -1031,7 +1048,9 @@ describe("App, authenticated", () => { await userEvent.click(await screen.findByRole("button", { name: "View post: Public post" })); await userEvent.click(await screen.findByRole("button", { name: "Affiliate Keyman: Priya Nair" })); await waitFor(() => expect(screen.getByText("Related to Priya Nair")).toBeInTheDocument()); - expect(screen.getByText("Ada West (Person)")).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: "Related nodes for Ada West (Our side)" }), + ).toBeInTheDocument(); }); it("opens related nodes from a Keyman affiliation organization", async () => { @@ -1040,7 +1059,9 @@ describe("App, authenticated", () => { await userEvent.click(await screen.findByRole("button", { name: "View post: Public post" })); await userEvent.click(await screen.findByRole("button", { name: "Keyman affiliation: Demo Corp" })); await waitFor(() => expect(screen.getByText("Related to Demo Corp")).toBeInTheDocument()); - expect(screen.getByText("Ada West (Person)")).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: "Related nodes for Ada West (Our side)" }), + ).toBeInTheDocument(); }); it("opens related nodes from an affiliate-tree organization", async () => { @@ -1049,7 +1070,9 @@ describe("App, authenticated", () => { await userEvent.click(await screen.findByRole("button", { name: "View post: Public post" })); await userEvent.click(await screen.findByRole("button", { name: "Affiliate org: Demo Corp" })); await waitFor(() => expect(screen.getByText("Related to Demo Corp")).toBeInTheDocument()); - expect(screen.getByText("Ada West (Person)")).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: "Related nodes for Ada West (Our side)" }), + ).toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Affiliate org: Northridge Grid" })).not.toBeInTheDocument(); }); @@ -1059,7 +1082,9 @@ describe("App, authenticated", () => { await userEvent.click(await screen.findByRole("button", { name: "View post: Public post" })); await userEvent.click(await screen.findByRole("button", { name: "Counterparty org: Demo Corp" })); await waitFor(() => expect(screen.getByText("Related to Demo Corp")).toBeInTheDocument()); - expect(screen.getByText("Ada West (Person)")).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: "Related nodes for Ada West (Our side)" }), + ).toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Counterparty org: Northridge Grid" })).not.toBeInTheDocument(); }); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1e39a9253..57f5a5185 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -466,6 +466,18 @@ function VocEvidenceSection({ } const NODE_PERSON = "node_person"; + +function relatedNodeCaption(node: RelatedNode): string { + const name = node.label ?? node.node_id; + if (node.node_type_code === NODE_PERSON) { + const side = node.person_side_label ?? node.person_side_code; + if (side) { + return `${name} (${side})`; + } + } + return `${name} (${node.ontology_label ?? node.node_type_code})`; +} + const NODE_POST = "node_post"; const NODE_CORPORATE_ENTITY = "node_corporate_entity"; @@ -677,7 +689,7 @@ function KeymanPanel({ ) : (