From bf9daa9810ad31e98cf548334c566b75a7f0cffc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 19:32:42 +0900 Subject: [PATCH 01/10] 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({ ) : ( diff --git a/frontend/src/RelatedNodeChip.stories.tsx b/frontend/src/RelatedNodeChip.stories.tsx new file mode 100644 index 000000000..2155be874 --- /dev/null +++ b/frontend/src/RelatedNodeChip.stories.tsx @@ -0,0 +1,90 @@ +import type { RelatedNode } from "./api"; +import { RelatedNodeChip } from "./RelatedNodeChip"; +import type { RelatedNodeChipAction } from "./relatedNodeCaption"; + +type RelatedNodeStoryArgs = { + action: RelatedNodeChipAction; + onSelect: (node: RelatedNode) => void; + node: RelatedNode; +}; + +/** + * Storybook inventory for the repeating related-node chip. + * + * Host this file with Storybook 10 (Vite + React) when the later + * token stack lands. Until then the same four states are locked by + * RelatedNodeChip.test.tsx and relatedNodeCaption.test.ts. + * + * Buyer states after seed: + * - Ada West, Demo Corp (Our side) + * - Priya Nair (Counterparty) — two orgs stay omitted + * - Demo Corp (Company) + * - Linked post (title only) + */ +const meta = { + title: "Lineage/RelatedNodeChip", + component: RelatedNodeChip, +}; + +export default meta; + +function node(partial: Partial & Pick): RelatedNode { + return { + node_id: "node-1", + relevance: 0.4, + ...partial, + }; +} + +export const UniqueAffiliation = { + args: { + action: "walk_person", + onSelect: () => undefined, + node: node({ + node_type_code: "node_person", + label: "Ada West", + person_side_label: "Our side", + affiliation_organization_name: "Demo Corp", + }), + } satisfies RelatedNodeStoryArgs, +}; + +export const SideOnlyPluralAffiliations = { + args: { + action: "walk_person", + onSelect: () => undefined, + node: node({ + node_type_code: "node_person", + label: "Priya Nair", + person_side_label: "Counterparty", + }), + } satisfies RelatedNodeStoryArgs, +}; + +export const OrganizationAndPost = { + render: () => ( +
    +
  • + undefined} + node={node({ + node_type_code: "node_corporate_entity", + label: "Demo Corp", + entity_level_label: "Company", + })} + /> +
  • +
  • + undefined} + node={node({ + node_type_code: "node_post", + label: "Linked post", + })} + /> +
  • +
+ ), +}; diff --git a/frontend/src/RelatedNodeChip.test.tsx b/frontend/src/RelatedNodeChip.test.tsx new file mode 100644 index 000000000..4a46aa216 --- /dev/null +++ b/frontend/src/RelatedNodeChip.test.tsx @@ -0,0 +1,66 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it, vi } from "vitest"; +import type { RelatedNode } from "./api"; +import { RelatedNodeChip } from "./RelatedNodeChip"; +import { + UniqueAffiliation, + SideOnlyPluralAffiliations, +} from "./RelatedNodeChip.stories"; + +function node(partial: Partial & Pick): RelatedNode { + return { + node_id: "node-1", + relevance: 0.4, + ...partial, + }; +} + +describe("RelatedNodeChip", () => { + it("keeps the unique-affiliation caption inside the walk name", () => { + const caption = "Ada West, Demo Corp (Our side)"; + render( + undefined} + />, + ); + expect(screen.getByRole("button", { name: `Related nodes for ${caption}` })).toHaveTextContent( + caption, + ); + }); + + it("does not invent a primary org on a side-only chip", () => { + const caption = "Priya Nair (Counterparty)"; + render( + undefined} + />, + ); + expect(screen.getByRole("button", { name: `Related nodes for ${caption}` })).toHaveTextContent( + caption, + ); + expect(screen.queryByText(/Northridge/)).not.toBeInTheDocument(); + }); + + it("opens the post when the buyer clicks a title-only chip", async () => { + const onSelect = vi.fn(); + render( + , + ); + await userEvent.click(screen.getByRole("button", { name: "Open related post: Linked post" })); + expect(onSelect).toHaveBeenCalledTimes(1); + expect(onSelect.mock.calls[0][0].node_id).toBe("post-1"); + }); +}); diff --git a/frontend/src/RelatedNodeChip.tsx b/frontend/src/RelatedNodeChip.tsx new file mode 100644 index 000000000..97a27d7a2 --- /dev/null +++ b/frontend/src/RelatedNodeChip.tsx @@ -0,0 +1,34 @@ +import type { RelatedNode } from "./api"; +import { + relatedNodeCaption, + relatedNodeChipAccessibleName, + type RelatedNodeChipAction, +} from "./relatedNodeCaption"; +import "./relatedNodeTokens.css"; + +/** + * One related-node chip. Caption, tokens, and accessible name stay + * one contract so the walk inventory matches the Figma chip library + * (ADR 0002 / 0014). + */ +export function RelatedNodeChip({ + node, + action, + onSelect, +}: { + node: RelatedNode; + action: RelatedNodeChipAction; + onSelect: (node: RelatedNode) => void; +}) { + const caption = relatedNodeCaption(node); + return ( + + ); +} diff --git a/frontend/src/relatedNodeCaption.test.ts b/frontend/src/relatedNodeCaption.test.ts new file mode 100644 index 000000000..53d678bdd --- /dev/null +++ b/frontend/src/relatedNodeCaption.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from "vitest"; +import type { RelatedNode } from "./api"; +import { + relatedNodeCaption, + relatedNodeChipAccessibleName, +} from "./relatedNodeCaption"; + +function node(partial: Partial & Pick): RelatedNode { + return { + node_id: "node-1", + relevance: 0.4, + ...partial, + }; +} + +describe("relatedNodeCaption", () => { + it("names a unique affiliation and side", () => { + expect( + relatedNodeCaption( + node({ + node_type_code: "node_person", + label: "Ada West", + person_side_label: "Our side", + affiliation_organization_name: "Demo Corp", + }), + ), + ).toBe("Ada West, Demo Corp (Our side)"); + }); + + it("keeps a side-only chip when no single org is known", () => { + expect( + relatedNodeCaption( + node({ + node_type_code: "node_person", + label: "Priya Nair", + person_side_label: "Counterparty", + }), + ), + ).toBe("Priya Nair (Counterparty)"); + }); + + it("names the organization level, not the ontology class", () => { + expect( + relatedNodeCaption( + node({ + node_type_code: "node_corporate_entity", + label: "Demo Corp", + entity_level_label: "Company", + }), + ), + ).toBe("Demo Corp (Company)"); + }); + + it("shows a post title only", () => { + expect( + relatedNodeCaption( + node({ + node_type_code: "node_post", + label: "Linked post", + }), + ), + ).toBe("Linked post"); + }); +}); + +describe("relatedNodeChipAccessibleName", () => { + it("contains the visible caption for a walk chip", () => { + expect( + relatedNodeChipAccessibleName("Ada West, Demo Corp (Our side)", "walk_person"), + ).toBe("Related nodes for Ada West, Demo Corp (Our side)"); + }); + + it("names the next action on a post chip", () => { + expect(relatedNodeChipAccessibleName("Linked post", "open_post")).toBe( + "Open related post: Linked post", + ); + }); +}); diff --git a/frontend/src/relatedNodeCaption.ts b/frontend/src/relatedNodeCaption.ts new file mode 100644 index 000000000..170f236c2 --- /dev/null +++ b/frontend/src/relatedNodeCaption.ts @@ -0,0 +1,64 @@ +import type { RelatedNode } from "./api"; + +export const NODE_PERSON = "node_person"; +export const NODE_POST = "node_post"; +export const NODE_CORPORATE_ENTITY = "node_corporate_entity"; + +/** + * Decision-facing label for a related-node chip on the #92 walk. + * + * Person chips use the authorized side label and, when exactly one + * organization identity is known, that organization. Multiple + * distinct affiliations stay omitted so a second org is never + * collapsed into an invented primary. Organization chips use the + * entity-level label. Post chips are the title only. + */ +export 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?.trim() || node.person_side_code?.trim(); + const org = node.affiliation_organization_name?.trim(); + if (side && org) { + return `${name}, ${org} (${side})`; + } + if (side) { + return `${name} (${side})`; + } + } + if (node.node_type_code === NODE_CORPORATE_ENTITY) { + const level = node.entity_level_label?.trim() || node.entity_level_code?.trim(); + if (level) { + return `${name} (${level})`; + } + } + if (node.node_type_code === NODE_POST) { + return name; + } + return `${name} (${node.ontology_label ?? node.node_type_code})`; +} + +export type RelatedNodeChipAction = "walk_person" | "walk_entity" | "open_post"; + +/** + * Accessible name for a related-node chip. + * + * The visible caption is contained in the name (WCAG 2.2 Success + * Criterion 2.5.3). Walk chips continue the graph. Post chips open + * the evidence body. + */ +export function relatedNodeChipAccessibleName( + caption: string, + action: RelatedNodeChipAction, +): string { + switch (action) { + case "walk_person": + case "walk_entity": + return `Related nodes for ${caption}`; + case "open_post": + return `Open related post: ${caption}`; + default: { + const _exhaustive: never = action; + return _exhaustive; + } + } +} diff --git a/frontend/src/relatedNodeTokens.css b/frontend/src/relatedNodeTokens.css new file mode 100644 index 000000000..45e91885f --- /dev/null +++ b/frontend/src/relatedNodeTokens.css @@ -0,0 +1,23 @@ +:root { + --related-node-chip-font: inherit; + --related-node-chip-color: inherit; + --related-node-chip-padding: 0; + --related-node-chip-background: none; + --related-node-chip-border: none; + --related-node-chip-text-align: left; + --related-node-chip-cursor: pointer; +} + +.related-node-chip { + background: var(--related-node-chip-background); + border: var(--related-node-chip-border); + padding: var(--related-node-chip-padding); + color: var(--related-node-chip-color); + cursor: var(--related-node-chip-cursor); + font: var(--related-node-chip-font); + text-align: var(--related-node-chip-text-align); +} + +.related-node-chip:hover { + text-decoration: underline; +} diff --git a/lineageweave/__init__.py b/lineageweave/__init__.py index 1710c009e..691380be0 100644 --- a/lineageweave/__init__.py +++ b/lineageweave/__init__.py @@ -35,4 +35,4 @@ "sentence_excerpts", ] -__version__ = "0.75.0" +__version__ = "0.76.0" diff --git a/pyproject.toml b/pyproject.toml index 764ebad72..b10fc3bb7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lineageweave" -version = "0.75.0" +version = "0.76.0" 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 08eab7768..9de5184c3 100644 --- a/uv.lock +++ b/uv.lock @@ -355,7 +355,7 @@ wheels = [ [[package]] name = "lineageweave" -version = "0.75.0" +version = "0.76.0" source = { virtual = "." } dependencies = [ { name = "certifi" }, From 915b3cfcfc30ad698d044af443850270b93dcaf9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 18 Aug 2026 00:03:48 +0000 Subject: [PATCH 10/10] fix(ui): drop Storybook claim and lock raw-code captions 0.76.0 now records the RelatedNodeChip walk behavior only. Caption tests cover missing lookup labels falling back to authorized codes. Co-authored-by: Seongho Bae --- .../0.76.0-related-node-chip-inventory.md | 11 +++++----- CHANGELOG.md | 11 +++++----- frontend/src/relatedNodeCaption.test.ts | 22 +++++++++++++++++++ 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.d/0.76.0-related-node-chip-inventory.md b/CHANGELOG.d/0.76.0-related-node-chip-inventory.md index bcf7c11b6..2c65a9f98 100644 --- a/CHANGELOG.d/0.76.0-related-node-chip-inventory.md +++ b/CHANGELOG.d/0.76.0-related-node-chip-inventory.md @@ -2,9 +2,8 @@ ## Changed -- Related-node walk chips share `RelatedNodeChip` and a Storybook - inventory. After seed, walking from Demo Corp still shows - "Ada West, Demo Corp (Our side)". Priya Nair stays - "Priya Nair (Counterparty)" — two orgs are never collapsed into - an invented primary. Click a chip to continue the walk or open - that post. +- Related-node walk chips share `RelatedNodeChip`. After seed, + walking from Demo Corp still shows "Ada West, Demo Corp + (Our side)". Priya Nair stays "Priya Nair (Counterparty)" — two + orgs are never collapsed into an invented primary. Click a chip + to continue the walk or open that post. diff --git a/CHANGELOG.md b/CHANGELOG.md index c6b49ea80..3c4da3e8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,11 @@ All notable changes to this project are documented here. Format follows ### Changed -- Related-node walk chips share `RelatedNodeChip` and a Storybook - inventory (ADR 0014). After seed, walking from Demo Corp still - shows "Ada West, Demo Corp (Our side)". Priya Nair stays - "Priya Nair (Counterparty)" — two orgs are never collapsed into - an invented primary. Click a chip to continue the walk or open - that post. +- Related-node walk chips share `RelatedNodeChip` (ADR 0014). After + seed, walking from Demo Corp still shows "Ada West, Demo Corp + (Our side)". Priya Nair stays "Priya Nair (Counterparty)" — two + orgs are never collapsed into an invented primary. Click a chip + to continue the walk or open that post. ## [0.75.0] - 2026-08-17 diff --git a/frontend/src/relatedNodeCaption.test.ts b/frontend/src/relatedNodeCaption.test.ts index 53d678bdd..932d74442 100644 --- a/frontend/src/relatedNodeCaption.test.ts +++ b/frontend/src/relatedNodeCaption.test.ts @@ -51,6 +51,28 @@ describe("relatedNodeCaption", () => { ).toBe("Demo Corp (Company)"); }); + it("falls back to raw authorized codes when lookup labels are absent", () => { + expect( + relatedNodeCaption( + node({ + node_type_code: "node_person", + label: "Synthetic Person", + person_side_code: "SIDE_EXTERNAL", + }), + ), + ).toBe("Synthetic Person (SIDE_EXTERNAL)"); + + expect( + relatedNodeCaption( + node({ + node_type_code: "node_corporate_entity", + label: "Synthetic Organization", + entity_level_code: "LEVEL_COMPANY", + }), + ), + ).toBe("Synthetic Organization (LEVEL_COMPANY)"); + }); + it("shows a post title only", () => { expect( relatedNodeCaption(