diff --git a/AGENTS.md b/AGENTS.md index 18939f8e3..a71928ba8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -177,6 +177,8 @@ Opening a Calendar commitment uses the same focus path (ADR 0094). Do not invent a week, a theta, a cutoff body, or a CalDAV event. Opening a Customer master related post uses the same focus path (ADR 0095). Do not invent a week, a theta, a cutoff body, a CalDAV event, or a customer. +Opening an Ask Agent cited post uses the same focus path (ADR 0096). Do not +invent a cited post. ## Tests diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ee00dcb26..95f840e9b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -284,7 +284,8 @@ rebuild), the post list with a named Weekly VOC ISO-8601 week filter (ADR 0092; opening that filtered post focuses Event Lineage, ADR 0093), Calendar commitments use the same Event Lineage focus path (ADR 0094), Customer master related posts use the same Event Lineage focus path -(ADR 0095), and the full detail popup includes Korean +(ADR 0095). Ask Agent cited posts use the same Event Lineage focus path +(ADR 0096), and the full detail popup includes Korean summary/key-events/R&R, VOC evidence excerpts, an Event Lineage panel (direct vs. indirect links; a link opens that post), the Keyman affiliate tree (resolved ancestors plus unresolved org roots), Keyman + diff --git a/CHANGELOG.d/2.16.0-ask-agent-open-event-lineage.md b/CHANGELOG.d/2.16.0-ask-agent-open-event-lineage.md new file mode 100644 index 000000000..d60ab9945 --- /dev/null +++ b/CHANGELOG.d/2.16.0-ask-agent-open-event-lineage.md @@ -0,0 +1,5 @@ +# 2.16.0 Opening an Ask Agent cited post focuses Event Lineage + +Ask Agent names authorized cited posts as current after an answer and to +open one to read Event Lineage. That open focuses the popup Event Lineage +heading. Home-list opens do not. No TEPP theta is invented. diff --git a/CHANGELOG.md b/CHANGELOG.md index 301c0412b..fbcfc636c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ 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). +## [2.16.0] - 2026-08-19 + +### Added + +- Opening an Ask Agent cited post now focuses Event Lineage and names Keyman + and evaluation as the next read. After an authorized answer, Ask Agent + names cited posts as current before that open. Home-list opens do not add + that focus or copy. No TEPP theta is invented. No cited post is invented +(ADR 0096 / ADR 0039 / ADR 0016). + ## [2.15.0] - 2026-08-19 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index a332cb5fc..8675cec71 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -92,3 +92,10 @@ Open Customer master. Authorized customer entities are current. Open a related post: Event Lineage takes focus and names Keyman and evaluation next (ADR 0095). A home-list open does not. Do not invent a theta or a customer. + +## Ask Agent open (v2.16.0) + +Open Ask Agent. After an authorized answer, cited posts are current. Open +a cited post: Event Lineage takes focus and names Keyman and evaluation +next (ADR 0096). A home-list open does not. Do not invent a theta or a +cited post. diff --git a/docs/adr/0096-ask-agent-open-focuses-event-lineage.md b/docs/adr/0096-ask-agent-open-focuses-event-lineage.md new file mode 100644 index 000000000..5a323fc25 --- /dev/null +++ b/docs/adr/0096-ask-agent-open-focuses-event-lineage.md @@ -0,0 +1,36 @@ +# ADR 0096: Opening an Ask Agent cited post focuses Event Lineage + +- Status: Accepted +- Date: 2026-08-19 + +## Context + +Board Weekly VOC, Calendar, and Customer master opens already focus Event +Lineage (ADR 0093 / ADR 0094 / ADR 0095). Ask Agent is the remaining buyer +GNB destination that opens an authorized cited post. That open was a +home-list open: the popup body appeared and Event Lineage did not take +focus. + +## Decision + +Opening a cited post on Ask Agent is a `fromAskAgent` open. That open +reuses the Event Lineage focus path used by report-member, Weekly VOC, +Calendar, and Customer master opens: + +- After an authorized answer, Ask Agent names the next action: cited posts + are current; open a cited post to read Event Lineage. +- The popup Event Lineage heading takes focus. +- The popup names the opened post as current in Event Lineage and tells + the buyer to read Keyman and evaluation next. + +A Board home-list open does not focus Event Lineage and does not add that +copy. A `?post=` deep link is still a home-list open. + +No TEPP theta is invented. No cutoff body is invented (ADR 0016). Ask Agent +does not invent a cited post (ADR 0039). + +## Consequences + +- Ask Agent, Customer master, Calendar, Weekly VOC, and report-member + opens share one focus contract. +- Closing the popup clears the Ask Agent open flag. diff --git a/docs/adr/0096-internal-relation-evidence.md b/docs/adr/0097-internal-relation-evidence.md similarity index 96% rename from docs/adr/0096-internal-relation-evidence.md rename to docs/adr/0097-internal-relation-evidence.md index 44845b34e..53ce197a2 100644 --- a/docs/adr/0096-internal-relation-evidence.md +++ b/docs/adr/0097-internal-relation-evidence.md @@ -1,4 +1,4 @@ -# ADR 0096: Preserve authorized internal evidence for relation verification +# ADR 0097: Preserve authorized internal evidence for relation verification - Status: Accepted - Date: 2026-08-18 diff --git a/frontend/package.json b/frontend/package.json index f499c9871..96c2b72ba 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "2.15.0", + "version": "2.16.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index a5ea0997b..830187589 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -85,6 +85,7 @@ describe("App, authenticated", () => { pendingTeppRun?: boolean; pluralAffiliations?: boolean; deferMe?: boolean; + deferSecondAsk?: boolean; meFailed?: boolean; postBody?: string; manyCustomerHints?: number; @@ -122,6 +123,7 @@ describe("App, authenticated", () => { contentAfterSummary?: boolean; }): ReturnType & { releaseMe: () => void; + releaseSecondAsk: () => void; releaseGroupRelated: () => void; releaseDemoRelated: () => void; } { @@ -156,6 +158,13 @@ describe("App, authenticated", () => { releaseMe = resolve; }) : Promise.resolve(); + let releaseSecondAsk = () => {}; + const secondAskReady = options?.deferSecondAsk + ? new Promise((resolve) => { + releaseSecondAsk = resolve; + }) + : Promise.resolve(); + let askRequestCount = 0; let releaseGroupRelated = () => {}; let releaseDemoRelated = () => {}; const groupRelatedReady = options?.deferCustomerRelated @@ -248,6 +257,9 @@ describe("App, authenticated", () => { if (url.endsWith("/api/posts/post-1/activity") && method === "GET") { return Promise.resolve(jsonResponse({ events })); } + if (url.endsWith("/api/posts/post-2/activity") && method === "GET") { + return Promise.resolve(jsonResponse({ events: [] })); + } if (url.endsWith("/api/posts/post-1/derive-commitment") && method === "POST") { if (options?.chatUnavailable) { return Promise.resolve( @@ -1278,6 +1290,17 @@ describe("App, authenticated", () => { }), ); } + if (url.endsWith("/api/posts/post-2/summary")) { + return Promise.resolve( + jsonResponse({ + post_id: "post-2", + korean_summary: "연결된 글입니다.", + key_events: [], + roles_and_responsibilities: [], + project_mentions: [], + }), + ); + } if (url.endsWith("/api/posts/post-1/keymen")) { return Promise.resolve( jsonResponse({ @@ -1588,6 +1611,15 @@ describe("App, authenticated", () => { }), ); } + if (url.endsWith("/api/posts/post-2/lineage")) { + return Promise.resolve( + jsonResponse({ + post_id: "post-2", + direct: [{ post_id: "post-1", post_title: "Public post" }], + indirect: [], + }), + ); + } if (url.endsWith("/api/posts/post-1/chat") && method === "GET") { return Promise.resolve( jsonResponse({ @@ -1638,7 +1670,13 @@ describe("App, authenticated", () => { ); } if (url.endsWith("/api/ask") && method === "POST") { - return Promise.resolve( + askRequestCount += 1; + const ready = + options?.deferSecondAsk && askRequestCount === 2 + ? secondAskReady + : Promise.resolve(); + return ready.then(() => + Promise.resolve( jsonResponse({ answer_text: "The cited project is supported by the stored semantic evidence.", cited_post_ids: ["post-2"], @@ -1654,6 +1692,7 @@ describe("App, authenticated", () => { ], source_post_ids: ["post-1", "post-2"], }), + ), ); } if (url.endsWith("/api/customer-master") && method === "GET") { @@ -1751,6 +1790,7 @@ describe("App, authenticated", () => { vi.stubGlobal("fetch", fetchMock); return Object.assign(fetchMock, { releaseMe, + releaseSecondAsk, releaseGroupRelated, releaseDemoRelated, }); @@ -1770,6 +1810,42 @@ describe("App, authenticated", () => { expect(screen.queryByText(/ontology_iri|contextual_orchestrator/i)).not.toBeInTheDocument(); }); + it("hides previous Ask evidence while a new answer is pending", async () => { + const fetchMock = stubBackend({ deferSecondAsk: true }); + render(); + + await userEvent.click(await screen.findByRole("button", { name: "Ask Agent" })); + const ask = await screen.findByRole("region", { name: "Ask Agent" }); + const question = within(ask).getByRole("textbox", { name: "Ask a question" }); + await userEvent.type(question, "Which project?"); + await userEvent.click(within(ask).getByRole("button", { name: "Ask" })); + expect( + await within(ask).findByText( + "The cited project is supported by the stored semantic evidence.", + ), + ).toBeInTheDocument(); + expect(within(ask).getByLabelText("Next action")).toBeInTheDocument(); + + await userEvent.clear(question); + await userEvent.type(question, "Which person?"); + await userEvent.click(within(ask).getByRole("button", { name: "Ask" })); + + expect(within(ask).getByRole("button", { name: "Asking..." })).toBeDisabled(); + expect( + within(ask).queryByText( + "The cited project is supported by the stored semantic evidence.", + ), + ).not.toBeInTheDocument(); + expect(within(ask).queryByLabelText("Next action")).not.toBeInTheDocument(); + + fetchMock.releaseSecondAsk(); + expect( + await within(ask).findByText( + "The cited project is supported by the stored semantic evidence.", + ), + ).toBeInTheDocument(); + }); + it("labels the Customer Master entity level and Keymen side, never the raw lookup code", async () => { // Live UI finding (2026-08-19): read_customer_master() skipped the // common_lookup_value join both endpoints elsewhere already use, @@ -2138,6 +2214,35 @@ describe("App, authenticated", () => { ).toBeInTheDocument(); }); + it("opening an Ask Agent cited post focuses Event Lineage; a home list open does not", async () => { + stubBackend(); + render(); + + await userEvent.click(await screen.findByRole("button", { name: "Ask Agent" })); + const ask = await screen.findByRole("region", { name: "Ask Agent" }); + await userEvent.type(within(ask).getByRole("textbox", { name: "Ask a question" }), "Which project?"); + await userEvent.click(within(ask).getByRole("button", { name: "Ask" })); + expect(await within(ask).findByLabelText("Next action")).toHaveTextContent( + "Authorized cited posts are current. Open a cited post to read Event Lineage.", + ); + await userEvent.click(within(ask).getByRole("button", { name: "Open cited post: Linked post" })); + + await waitFor(() => + expect(screen.getByText("The evidence panel should show exactly this text.")).toBeInTheDocument(), + ); + expect(document.getElementById("post-event-lineage")).toHaveFocus(); + expect(screen.getByRole("status", { name: "Event Lineage next action" })).toHaveTextContent( + "Linked post is current in Event Lineage. Read Keyman and evaluation next.", + ); + + await userEvent.click(screen.getByRole("button", { name: "Close" })); + const boardAfterAsk = screen.getByRole("region", { name: "Board" }); + await userEvent.click(within(boardAfterAsk).getByRole("button", { name: "View post: Public post" })); + await waitFor(() => expect(screen.getByText("The full body text.")).toBeInTheDocument()); + expect(document.getElementById("post-event-lineage")).not.toHaveFocus(); + expect(screen.queryByRole("status", { name: "Event Lineage next action" })).not.toBeInTheDocument(); + }); + it("renders the A-100 fork as a git-style DAG, not a flat edge list", async () => { stubBackend(); render(); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7dd9b3e9b..85eb6cf89 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2546,6 +2546,7 @@ type SelectPostOptions = { fromWeeklyVoc?: boolean; fromCalendar?: boolean; fromCustomerMaster?: boolean; + fromAskAgent?: boolean; }; /** @@ -3597,6 +3598,7 @@ function PostList({ postIdToOpen = null, postOpenFromCalendar = false, postOpenFromCustomerMaster = false, + postOpenFromAskAgent = false, onPostOpened, }: { accessToken: string; @@ -3604,6 +3606,7 @@ function PostList({ postIdToOpen?: string | null; postOpenFromCalendar?: boolean; postOpenFromCustomerMaster?: boolean; + postOpenFromAskAgent?: boolean; onPostOpened?: () => void; }) { const [posts, setPosts] = useState(null); @@ -3625,6 +3628,7 @@ function PostList({ const [openedFromWeeklyVoc, setOpenedFromWeeklyVoc] = useState(false); const [openedFromCalendar, setOpenedFromCalendar] = useState(false); const [openedFromCustomerMaster, setOpenedFromCustomerMaster] = useState(false); + const [openedFromAskAgent, setOpenedFromAskAgent] = useState(false); const [corporateEntities, setCorporateEntities] = useState(null); const [entitiesLoadError, setEntitiesLoadError] = useState(null); const [totalPosts, setTotalPosts] = useState(0); @@ -3682,6 +3686,7 @@ function PostList({ setOpenedFromWeeklyVoc(Boolean(options?.fromWeeklyVoc)); setOpenedFromCalendar(Boolean(options?.fromCalendar)); setOpenedFromCustomerMaster(Boolean(options?.fromCustomerMaster)); + setOpenedFromAskAgent(Boolean(options?.fromAskAgent)); } useEffect(() => { @@ -3689,9 +3694,16 @@ function PostList({ selectPost(postIdToOpen, { fromCalendar: postOpenFromCalendar, fromCustomerMaster: postOpenFromCustomerMaster, + fromAskAgent: postOpenFromAskAgent, }); onPostOpened?.(); - }, [onPostOpened, postIdToOpen, postOpenFromCalendar, postOpenFromCustomerMaster]); + }, [ + onPostOpened, + postIdToOpen, + postOpenFromCalendar, + postOpenFromCustomerMaster, + postOpenFromAskAgent, + ]); function closeSelectedPost() { setSelectedPostId(null); @@ -3701,6 +3713,7 @@ function PostList({ setOpenedFromWeeklyVoc(false); setOpenedFromCalendar(false); setOpenedFromCustomerMaster(false); + setOpenedFromAskAgent(false); const url = new URL(window.location.href); if (url.searchParams.has("post")) { url.searchParams.delete("post"); @@ -4188,7 +4201,8 @@ function PostList({ openedFromReportMember || openedFromWeeklyVoc || openedFromCalendar || - openedFromCustomerMaster + openedFromCustomerMaster || + openedFromAskAgent } focusAskOnLand={openedFromReportMember} onClose={closeSelectedPost} @@ -4447,6 +4461,7 @@ function AskAgentPanel({ if (!normalized) return; setAsking(true); setError(null); + setAnswer(null); try { setAnswer(await askAgent(accessToken, normalized)); } catch (err) { @@ -4482,11 +4497,18 @@ function AskAgentPanel({ {answer.next_action ?

{t(answer.next_action)}

: null} {answer.cited_posts && answer.cited_posts.length > 0 && ( <> +

+ {t("Authorized cited posts are current. Open a cited post to read Event Lineage.")} +

{t("Cited posts")}

    {answer.cited_posts.map((post) => (
  • - {answer.cited_post_evidence?.find((item) => item.post_id === post.post_id)?.facts.length ? ( @@ -4523,6 +4545,7 @@ export default function App({ showLabPanels = false }: { showLabPanels?: boolean }); const [postOpenFromCalendar, setPostOpenFromCalendar] = useState(false); const [postOpenFromCustomerMaster, setPostOpenFromCustomerMaster] = useState(false); + const [postOpenFromAskAgent, setPostOpenFromAskAgent] = useState(false); // Test-only compatibility for legacy analysis-panel coverage; this prop // never forces the panels open outside Vitest. In a real build the // advanced-review section (ADR 0037) is gated on PostList's own @@ -4627,10 +4650,12 @@ export default function App({ showLabPanels = false }: { showLabPanels?: boolean postIdToOpen={postToOpen} postOpenFromCalendar={postOpenFromCalendar} postOpenFromCustomerMaster={postOpenFromCustomerMaster} + postOpenFromAskAgent={postOpenFromAskAgent} onPostOpened={() => { setPostToOpen(null); setPostOpenFromCalendar(false); setPostOpenFromCustomerMaster(false); + setPostOpenFromAskAgent(false); }} /> ) : null} @@ -4641,6 +4666,7 @@ export default function App({ showLabPanels = false }: { showLabPanels?: boolean setPostToOpen(postId); setPostOpenFromCalendar(false); setPostOpenFromCustomerMaster(true); + setPostOpenFromAskAgent(false); setDestination("board"); }} /> @@ -4654,6 +4680,7 @@ export default function App({ showLabPanels = false }: { showLabPanels?: boolean setPostToOpen(postId); setPostOpenFromCalendar(true); setPostOpenFromCustomerMaster(false); + setPostOpenFromAskAgent(false); setDestination("board"); }} /> @@ -4663,6 +4690,9 @@ export default function App({ showLabPanels = false }: { showLabPanels?: boolean accessToken={accessToken} onOpenPost={(postId) => { setPostToOpen(postId); + setPostOpenFromCalendar(false); + setPostOpenFromCustomerMaster(false); + setPostOpenFromAskAgent(true); setDestination("board"); }} /> diff --git a/frontend/src/i18n.test.ts b/frontend/src/i18n.test.ts index 800d4321e..81f4612f7 100644 --- a/frontend/src/i18n.test.ts +++ b/frontend/src/i18n.test.ts @@ -41,6 +41,7 @@ describe("i18n", () => { "All weeks", "Authorized commitments are current. Open a commitment to read Event Lineage.", "Authorized customer entities are current. Open a related post to read Event Lineage.", + "Authorized cited posts are current. Open a cited post to read Event Lineage.", ] as const; it("supports the five product locales", () => { diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index 1ceb891d6..79d2cfc69 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -128,6 +128,9 @@ const TRANSLATIONS: Partial>> = { "권한이 있는 일정이 현재 표시되어 있습니다. 이벤트 계보를 읽으려면 일정을 여세요.", "Authorized customer entities are current. Open a related post to read Event Lineage.": "권한이 있는 고객 엔터티가 현재 표시되어 있습니다. 이벤트 계보를 읽으려면 관련 글을 여세요.", + "Authorized cited posts are current. Open a cited post to read Event Lineage.": + "권한이 있는 인용 글이 현재 표시되어 있습니다. 이벤트 계보를 읽으려면 인용 글을 여세요.", + "Open cited post:": "인용 글 열기:", "Filter by visibility": "공개 여부로 필터", "Sort posts": "글 정렬", "All VOC types": "모든 VOC 유형", @@ -476,6 +479,9 @@ const TRANSLATIONS: Partial>> = { "已授权承诺为当前内容。打开一项承诺阅读事件谱系。", "Authorized customer entities are current. Open a related post to read Event Lineage.": "已授权客户实体为当前内容。打开一篇相关文章阅读事件谱系。", + "Authorized cited posts are current. Open a cited post to read Event Lineage.": + "已授权引用文章为当前内容。打开一篇引用文章阅读事件谱系。", + "Open cited post:": "打开引用文章:", "Filter by visibility": "按公开状态筛选", "Sort posts": "排序文章", "All VOC types": "所有 VOC 类型", @@ -847,6 +853,9 @@ const TRANSLATIONS: Partial>> = { "権限のある約束が現在表示されています。イベント系譜を読むには約束を開いてください。", "Authorized customer entities are current. Open a related post to read Event Lineage.": "権限のある顧客エンティティが現在表示されています。イベント系譜を読むには関連投稿を開いてください。", + "Authorized cited posts are current. Open a cited post to read Event Lineage.": + "権限のある引用投稿が現在表示されています。イベント系譜を読むには引用投稿を開いてください。", + "Open cited post:": "引用投稿を開く:", "Filter by visibility": "公開状態で絞り込み", "Sort posts": "投稿を並べ替え", "All VOC types": "すべての VOC 種類", @@ -1194,6 +1203,9 @@ const TRANSLATIONS: Partial>> = { "Các cam kết được phép đang hiện tại. Hãy mở một cam kết để đọc Dòng sự kiện.", "Authorized customer entities are current. Open a related post to read Event Lineage.": "Các thực thể khách hàng được cấp quyền đang hiện tại. Hãy mở một bài liên quan để đọc Dòng sự kiện.", + "Authorized cited posts are current. Open a cited post to read Event Lineage.": + "Các bài được trích dẫn được cấp quyền đang hiện tại. Hãy mở một bài trích dẫn để đọc Dòng sự kiện.", + "Open cited post:": "Mở bài trích dẫn:", "Filter by visibility": "Lọc theo trạng thái hiển thị", "Sort posts": "Sắp xếp bài viết", "All VOC types": "Tất cả loại VOC", diff --git a/pyproject.toml b/pyproject.toml index 2b9af9ee2..3257416b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lineageweave" -version = "2.15.0" +version = "2.16.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 574c44962..5b2f39e62 100644 --- a/uv.lock +++ b/uv.lock @@ -454,7 +454,7 @@ wheels = [ [[package]] name = "lineageweave" -version = "2.15.0" +version = "2.16.0" source = { editable = "." } dependencies = [ { name = "certifi" },