From b0ecb49bff2cceff878bad4dc21f831bf03be720 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 16:16:40 +0900 Subject: [PATCH] feat: land first Keyman related nodes under the next action (v1.8.0) Opening Public post from Demo Corp members now lands Ada West related nodes under the first-Keyman next action, ahead of Affiliate tree. Home list opens still wait for a Keyman click. --- .../1.8.0-land-first-keyman-related.md | 5 + CHANGELOG.md | 9 + frontend/package.json | 2 +- frontend/src/App.test.tsx | 18 +- frontend/src/App.tsx | 237 ++++++++++-------- lineageweave/__init__.py | 2 +- pyproject.toml | 2 +- uv.lock | 2 +- 8 files changed, 168 insertions(+), 109 deletions(-) create mode 100644 CHANGELOG.d/1.8.0-land-first-keyman-related.md diff --git a/CHANGELOG.d/1.8.0-land-first-keyman-related.md b/CHANGELOG.d/1.8.0-land-first-keyman-related.md new file mode 100644 index 000000000..a3314ec7f --- /dev/null +++ b/CHANGELOG.d/1.8.0-land-first-keyman-related.md @@ -0,0 +1,5 @@ +# 1.8.0 Land first Keyman related nodes + +Open Public post from the landed Demo Corp members and Ada West related +nodes sit under the first-Keyman next action. Home list opens still +wait for a Keyman click. diff --git a/CHANGELOG.md b/CHANGELOG.md index ce0f9137b..1735022bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ 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). +## [1.8.0] - 2026-08-17 + +### Added + +- Opening Public post from the landed Demo Corp members now lands + Ada West related nodes under the first-Keyman next action, ahead + of Affiliate tree. Home list opens still wait for a Keyman click. + No TEPP theta is invented. No cutoff body is invented (ADR 0016). + ## [1.7.0] - 2026-08-17 ### Added diff --git a/frontend/package.json b/frontend/package.json index 47582abbb..037134f3e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "1.7.0", + "version": "1.8.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 74e006b8a..4907de06a 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -1475,6 +1475,7 @@ describe("App, authenticated", () => { screen.queryByRole("status", { name: "Event Lineage next action" }), ).not.toBeInTheDocument(); expect(screen.queryByRole("status", { name: "Keyman next action" })).not.toBeInTheDocument(); + expect(screen.queryByText("Related to Ada West")).not.toBeInTheDocument(); const popup = document.querySelector(".popup-panel"); expect(popup).not.toBeNull(); const evaluation = within(popup as HTMLElement).getByRole("heading", { @@ -2314,12 +2315,22 @@ describe("App, authenticated", () => { expect(keyman.compareDocumentPosition(evaluation) & Node.DOCUMENT_POSITION_FOLLOWING).not.toBe(0); const keymanNext = await screen.findByRole("status", { name: "Keyman next action" }); expect(keymanNext).toHaveTextContent("Ada West is the first Keyman. Read that person next."); + const related = await within(popup as HTMLElement).findByRole("heading", { + name: "Related to Ada West", + }); + expect(within(related.closest(".related-keymen") as HTMLElement).getByText(/Priya Nair/)).toBeInTheDocument(); expect( - evaluation.compareDocumentPosition(keymanNext) & Node.DOCUMENT_POSITION_FOLLOWING, - ).not.toBe(0); + within(popup as HTMLElement).getByRole("button", { name: "Related nodes for Ada West" }), + ).toHaveAttribute("aria-current", "true"); expect( - keymanNext.compareDocumentPosition(affiliate) & Node.DOCUMENT_POSITION_FOLLOWING, + evaluation.compareDocumentPosition(keymanNext) & Node.DOCUMENT_POSITION_FOLLOWING, ).not.toBe(0); + expect(keymanNext.compareDocumentPosition(related) & Node.DOCUMENT_POSITION_FOLLOWING).not.toBe( + 0, + ); + expect(related.compareDocumentPosition(affiliate) & Node.DOCUMENT_POSITION_FOLLOWING).not.toBe( + 0, + ); } finally { HTMLElement.prototype.scrollIntoView = originalScrollIntoView; } @@ -2590,6 +2601,7 @@ describe("App, authenticated", () => { expect(await screen.findByRole("status", { name: "Keyman next action" })).toHaveTextContent( "Ada West is the first Keyman. Read that person next.", ); + expect(await screen.findByRole("heading", { name: "Related to Ada West" })).toBeInTheDocument(); }); it("lets post_admin rebuild the period report", async () => { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1c0c36ac9..fc727d1b3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -563,6 +563,8 @@ function KeymanPanel({ focusPerson, focusEntity, focusTeam, + landFirstKeyman, + afterList, }: { postId: string; accessToken: string; @@ -573,6 +575,8 @@ function KeymanPanel({ focusPerson?: { personId: string; personName: string } | null; focusEntity?: { entityId: string; entityName: string } | null; focusTeam?: { teamId: string; teamName: string } | null; + landFirstKeyman?: boolean; + afterList?: ReactNode; }) { const [related, setRelated] = useState(null); const [selectedName, setSelectedName] = useState(null); @@ -622,6 +626,23 @@ function KeymanPanel({ } } + useEffect(() => { + if (!landFirstKeyman || selectedName || !keymen?.[0]) { + return; + } + const first = keymen[0]; + const requestId = ++relatedRequest.current; + setSelectedName(first.person_name); + setRelated(null); + fetchRelatedKeymen(accessToken, first.person_id) + .then((result) => { + if (requestId === relatedRequest.current) setRelated(result.related); + }) + .catch(() => { + if (requestId === relatedRequest.current) setRelated([]); + }); + }, [accessToken, landFirstKeyman, keymen, selectedName]); + useEffect(() => { if (!focusPerson) return; const requestId = ++relatedRequest.current; @@ -680,7 +701,86 @@ function KeymanPanel({ } } + const relatedBlock = selectedName ? ( +
+

Related to {selectedName}

+ {related === null ? ( +

Loading related nodes...

+ ) : related.length === 0 ? ( +

No related nodes in the visible graph.

+ ) : ( +
    + {related.map((node) => { + const caption = relatedNodeCaption(node); + const key = `${node.node_type_code}:${node.node_id}`; + if (!isKnownRelatedNodeType(node.node_type_code)) { + return
  • {caption}
  • ; + } + switch (node.node_type_code) { + case NODE_POST: + if (!onSelectPost) { + return
  • {caption}
  • ; + } + return ( +
  • + +
  • + ); + case NODE_PERSON: + return ( +
  • + +
  • + ); + case NODE_CORPORATE_ENTITY: + return ( +
  • + +
  • + ); + case NODE_TEAM: + return ( +
  • + +
  • + ); + default: { + const _exhaustive: never = node.node_type_code; + return
  • {_exhaustive}
  • ; + } + } + })} +
+ )} +
+ ) : null; + return ( + <>

Keyman

@@ -698,6 +798,9 @@ function KeymanPanel({ - - ); - case NODE_PERSON: - return ( -
  • - -
  • - ); - case NODE_CORPORATE_ENTITY: - return ( -
  • - -
  • - ); - case NODE_TEAM: - return ( -
  • - -
  • - ); - default: { - const _exhaustive: never = node.node_type_code; - return
  • {_exhaustive}
  • ; - } - } - })} - - )} -
    - )} + {!afterList && relatedBlock}
    + {afterList} + {afterList && relatedBlock} + ); } @@ -1415,31 +1445,34 @@ function PostDetailPopup({ {focusEventLineage && ( - <> - - setEvaluation(rows)} - /> - {keymen?.[0] ? ( -

    - {firstKeymanNextAction(keymen[0].person_name)} -

    - ) : null} - + + setEvaluation(rows)} + /> + {keymen?.[0] ? ( +

    + {firstKeymanNextAction(keymen[0].person_name)} +

    + ) : null} + + } + /> )}
    diff --git a/lineageweave/__init__.py b/lineageweave/__init__.py index fa84379c8..502ca14c2 100644 --- a/lineageweave/__init__.py +++ b/lineageweave/__init__.py @@ -55,4 +55,4 @@ "sentence_excerpts", ] -__version__ = "1.7.0" +__version__ = "1.8.0" diff --git a/pyproject.toml b/pyproject.toml index b256d6676..52d04d724 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lineageweave" -version = "1.7.0" +version = "1.8.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 6c879e6e3..f6526a6a9 100644 --- a/uv.lock +++ b/uv.lock @@ -454,7 +454,7 @@ wheels = [ [[package]] name = "lineageweave" -version = "1.7.0" +version = "1.8.0" source = { virtual = "." } dependencies = [ { name = "certifi" },