Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 4 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,8 +321,10 @@ is the same never-guess-a-parent rule
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. Related-node person chips use the same
side label (`Ada West (Our side)`), not the ontology class
(`Ada West (Person)`). Related-node organization chips use the
side label plus the primary affiliation when one exists
(`Ada West, Demo Corp (Our side)`), not the ontology class
(`Ada West (Person)`). A missing affiliation is omitted, never
guessed. Related-node organization chips use the
entity-level label (`Demo Corp (Company)`), not `Organization`.
Related-node post chips show the post title only, not `(Post)`.

Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ 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.75.0] - 2026-08-16

### Changed

- Related-node person chips include the primary affiliation
organization when one exists. After `make seed`, walking from Ada
West shows "Priya Nair, Northridge Grid (Counterparty)" and walking
from Demo Corp shows "Ada West, Demo Corp (Our side)". A person with
no affiliation keeps the side-only caption -- the org is never
guessed. Resolved catalog orgs win over unresolved names.

## [0.74.0] - 2026-08-16

### Changed
Expand Down
24 changes: 24 additions & 0 deletions backend/app/knowledge_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,8 @@ async def hydrate_related_nodes(

Unknown ids are dropped. Ontology fields are omitted (not faked)
when ``node_type_code`` has no term in lineageweave-kg.ttl.
Person nodes carry the primary affiliation organization when one
exists; a missing affiliation is omitted, never invented.
"""
person_ids: list[str] = []
post_ids: list[str] = []
Expand All @@ -305,6 +307,25 @@ async def hydrate_related_nodes(
person_ids,
)
} if person_ids else {}
affiliations: dict[str, str] = {}
if person_ids:
affiliation_rows = await conn.fetch(
"""
select person_id, affiliated_organization_name, affiliated_corporate_entity_id
from person_affiliation
where person_id = any($1::uuid[])
order by
(affiliated_corporate_entity_id is not null) desc,
affiliated_organization_name
""",
person_ids,
)
for row in affiliation_rows:
person_id = str(row["person_id"])
org = (row["affiliated_organization_name"] or "").strip()
if person_id in affiliations or not org:
continue
affiliations[person_id] = org
posts = {
str(row["post_id"]): row
for row in await conn.fetch(
Expand Down Expand Up @@ -341,6 +362,9 @@ async def hydrate_related_nodes(
item["label"] = people[node_id]["person_name"]
item["person_side_code"] = side
item["person_side_label"] = side_labels.get(side, side)
org = affiliations.get(node_id)
if org:
item["affiliation_organization_name"] = org
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:
Expand Down
8 changes: 8 additions & 0 deletions backend/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,13 @@ def test_related_keymen_use_rwr_and_hide_invisible_posts(client, demo_analyst_to
assert counterpart["ontology_iri"].endswith("#Person")
assert counterpart["person_side_code"] == "counterparty"
assert counterpart["person_side_label"] == "Counterparty"
assert counterpart["affiliation_organization_name"] == "Northridge Grid"
for node in body["related"]:
if node["node_type_code"] != "node_person":
continue
org = node.get("affiliation_organization_name")
if org is not None:
assert org.strip()
own_post = by_id[seeded_db["own_private_post_id"]]
assert own_post["ontology_label"] == "Post"
corp_nodes = [
Expand Down Expand Up @@ -916,6 +923,7 @@ def test_related_corporate_entity_uses_rwr_and_hides_invisible_posts(
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 our_person["affiliation_organization_name"] == "Test Corp"
assert seeded_db["other_private_post_id"] not in related_ids
assert seeded_db["hidden_person_id"] not in related_ids

Expand Down
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
"version": "0.74.0",
"version": "0.75.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
22 changes: 14 additions & 8 deletions frontend/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,7 @@ describe("App, authenticated", () => {
label: "Ada West",
person_side_code: "our_side",
person_side_label: "Our side",
affiliation_organization_name: "Demo Corp",
relevance: 0.4,
},
],
Expand All @@ -537,6 +538,7 @@ describe("App, authenticated", () => {
label: "Priya Nair",
person_side_code: "counterparty",
person_side_label: "Counterparty",
affiliation_organization_name: "Northridge Grid",
relevance: 0.4,
},
{
Expand Down Expand Up @@ -575,6 +577,7 @@ describe("App, authenticated", () => {
label: "Ada West",
person_side_code: "our_side",
person_side_label: "Our side",
affiliation_organization_name: "Demo Corp",
relevance: 0.5,
},
],
Expand Down Expand Up @@ -978,11 +981,14 @@ 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.getByRole("button", { name: "Related nodes for Priya Nair (Counterparty)" }),
screen.getByRole("button", { name: "Related nodes for Priya Nair, Northridge Grid (Counterparty)" }),
).toBeInTheDocument();
expect(screen.getByText("Related to Ada West").closest(".related-keymen")).not.toHaveTextContent(
"Priya Nair (Person)",
);
expect(screen.getByText("Related to Ada West").closest(".related-keymen")).not.toHaveTextContent(
"Priya Nair (Counterparty)",
);
const relatedPanel = screen.getByText("Related to Ada West").closest(".related-keymen");
expect(relatedPanel).toHaveTextContent("Linked post");
expect(relatedPanel).not.toHaveTextContent("Linked post (Post)");
Expand All @@ -999,7 +1005,7 @@ describe("App, authenticated", () => {
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.getByRole("button", { name: "Related nodes for Priya Nair (Counterparty)" }),
screen.getByRole("button", { name: "Related nodes for Priya Nair, Northridge Grid (Counterparty)" }),
).toBeInTheDocument();
});

Expand All @@ -1018,7 +1024,7 @@ describe("App, authenticated", () => {
await userEvent.click(screen.getByRole("button", { name: "Related nodes for Demo Corp (Company)" }));
await waitFor(() => expect(screen.getByText("Related to Demo Corp")).toBeInTheDocument());
expect(
screen.getByRole("button", { name: "Related nodes for Ada West (Our side)" }),
screen.getByRole("button", { name: "Related nodes for Ada West, Demo Corp (Our side)" }),
).toBeInTheDocument();
});

Expand Down Expand Up @@ -1049,7 +1055,7 @@ describe("App, authenticated", () => {
await userEvent.click(await screen.findByRole("button", { name: "VOC Keyman: Northridge Grid" }));
await waitFor(() => expect(screen.getByText("Related to Priya Nair")).toBeInTheDocument());
expect(
screen.getByRole("button", { name: "Related nodes for Ada West (Our side)" }),
screen.getByRole("button", { name: "Related nodes for Ada West, Demo Corp (Our side)" }),
).toBeInTheDocument();
});

Expand All @@ -1060,7 +1066,7 @@ describe("App, authenticated", () => {
await userEvent.click(await screen.findByRole("button", { name: "Affiliate Keyman: Priya Nair" }));
await waitFor(() => expect(screen.getByText("Related to Priya Nair")).toBeInTheDocument());
expect(
screen.getByRole("button", { name: "Related nodes for Ada West (Our side)" }),
screen.getByRole("button", { name: "Related nodes for Ada West, Demo Corp (Our side)" }),
).toBeInTheDocument();
});

Expand All @@ -1071,7 +1077,7 @@ describe("App, authenticated", () => {
await userEvent.click(await screen.findByRole("button", { name: "Keyman affiliation: Demo Corp" }));
await waitFor(() => expect(screen.getByText("Related to Demo Corp")).toBeInTheDocument());
expect(
screen.getByRole("button", { name: "Related nodes for Ada West (Our side)" }),
screen.getByRole("button", { name: "Related nodes for Ada West, Demo Corp (Our side)" }),
).toBeInTheDocument();
});

Expand All @@ -1082,7 +1088,7 @@ describe("App, authenticated", () => {
await userEvent.click(await screen.findByRole("button", { name: "Affiliate org: Demo Corp" }));
await waitFor(() => expect(screen.getByText("Related to Demo Corp")).toBeInTheDocument());
expect(
screen.getByRole("button", { name: "Related nodes for Ada West (Our side)" }),
screen.getByRole("button", { name: "Related nodes for Ada West, Demo Corp (Our side)" }),
).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Affiliate org: Northridge Grid" })).not.toBeInTheDocument();
});
Expand All @@ -1094,7 +1100,7 @@ describe("App, authenticated", () => {
await userEvent.click(await screen.findByRole("button", { name: "Counterparty org: Demo Corp" }));
await waitFor(() => expect(screen.getByText("Related to Demo Corp")).toBeInTheDocument());
expect(
screen.getByRole("button", { name: "Related nodes for Ada West (Our side)" }),
screen.getByRole("button", { name: "Related nodes for Ada West, Demo Corp (Our side)" }),
).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Counterparty org: Northridge Grid" })).not.toBeInTheDocument();
});
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,10 @@ 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;
const org = node.affiliation_organization_name?.trim();
if (side && org) {
return `${name}, ${org} (${side})`;
}
if (side) {
return `${name} (${side})`;
}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export interface RelatedNode {
label?: string;
person_side_code?: string;
person_side_label?: string;
affiliation_organization_name?: string;
entity_level_code?: string;
entity_level_label?: string;
ontology_iri?: string;
Expand Down
2 changes: 1 addition & 1 deletion lineageweave/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@
"sentence_excerpts",
]

__version__ = "0.74.0"
__version__ = "0.75.0"
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "lineageweave"
version = "0.74.0"
version = "0.75.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" }
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.