diff --git a/AGENTS.md b/AGENTS.md index 764e100cc..e704c9058 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -173,6 +173,8 @@ never a fabricated theta or a local psychometric substitute. Buyer Board **Weekly VOC** is an ISO-8601 week list filter (ADR 0092). Opening that filtered post focuses Event Lineage (ADR 0093). Do not invent a week, a theta, or a cutoff body. +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. ## Tests diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 878bb4069..461ebacba 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -282,7 +282,8 @@ Keycloak issued; `src/App.tsx` renders a git-branch SVG of `GET /api/lineage` (click a node to open that post; `post_admin` can rebuild), the post list with a named Weekly VOC ISO-8601 week filter (ADR 0092; opening that filtered post focuses Event Lineage, ADR 0093), -and a full detail popup: Korean +Calendar commitments use the same Event Lineage focus path (ADR 0094), 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.14.0-calendar-open-event-lineage.md b/CHANGELOG.d/2.14.0-calendar-open-event-lineage.md new file mode 100644 index 000000000..c09605d5e --- /dev/null +++ b/CHANGELOG.d/2.14.0-calendar-open-event-lineage.md @@ -0,0 +1,5 @@ +# 2.14.0 Opening a Calendar commitment focuses Event Lineage + +Calendar names authorized commitments as current 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 a3814399c..a3559c0b9 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.14.0] - 2026-08-19 + +### Added + +- Opening a Calendar commitment now focuses Event Lineage and names Keyman + and evaluation as the next read. Calendar names authorized commitments + as current before that open. Home-list opens do not add that focus or + copy. No TEPP theta is invented. No cutoff body is invented + (ADR 0094 / ADR 0016). + ## [2.13.0] - 2026-08-19 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 81fefe402..40e173ead 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -78,3 +78,10 @@ ISO-8601 week stay; other VOC types and older weeks drop out. The Board names Event Lineage as the next read (ADR 0092). Open a remaining post: Event Lineage takes focus and names Keyman and evaluation next (ADR 0093). A home-list open does not. Do not invent a theta. + +## Calendar open (v2.14.0) + +Open Calendar. Authorized commitments are current. Open a commitment: +Event Lineage takes focus and names Keyman and evaluation next +(ADR 0094). A home-list open does not. Do not invent a theta or a +CalDAV event. diff --git a/docs/adr/0094-calendar-open-focuses-event-lineage.md b/docs/adr/0094-calendar-open-focuses-event-lineage.md new file mode 100644 index 000000000..0b7b1a8b9 --- /dev/null +++ b/docs/adr/0094-calendar-open-focuses-event-lineage.md @@ -0,0 +1,33 @@ +# ADR 0094: Opening a Calendar commitment focuses Event Lineage + +- Status: Accepted +- Date: 2026-08-19 + +## Context + +Board Weekly VOC opens already focus Event Lineage (ADR 0093). Calendar is +the other buyer destination that opens a source post from an authorized +commitment. That open was a home-list open: the popup body appeared and +Event Lineage did not take focus. + +## Decision + +Opening a commitment on Calendar is a `fromCalendar` open. That open reuses +the Event Lineage focus path used by report-member and Weekly VOC opens: + +- Calendar names the next action: authorized commitments are current; open + a commitment 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). Calendar +does not invent a CalDAV event (ADR 0038). + +## Consequences + +- Calendar, Weekly VOC, and report-member opens share one focus contract. +- Closing the popup clears the Calendar open flag. diff --git a/docs/adr/0094-internal-relation-evidence.md b/docs/adr/0095-internal-relation-evidence.md similarity index 96% rename from docs/adr/0094-internal-relation-evidence.md rename to docs/adr/0095-internal-relation-evidence.md index 31d8e8f22..c5e44e4b3 100644 --- a/docs/adr/0094-internal-relation-evidence.md +++ b/docs/adr/0095-internal-relation-evidence.md @@ -1,4 +1,4 @@ -# ADR 0094: Preserve authorized internal evidence for relation verification +# ADR 0095: Preserve authorized internal evidence for relation verification - Status: Accepted - Date: 2026-08-18 diff --git a/docs/adr/0124-operational-controlled-vocabulary-semantic-layer.md b/docs/adr/0124-operational-controlled-vocabulary-semantic-layer.md index 2b1ddbe08..6853f5308 100644 --- a/docs/adr/0124-operational-controlled-vocabulary-semantic-layer.md +++ b/docs/adr/0124-operational-controlled-vocabulary-semantic-layer.md @@ -69,4 +69,3 @@ https://www.w3.org/TR/skos-reference/ World Wide Web Consortium. (2017). *Shapes Constraint Language (SHACL)*. https://www.w3.org/TR/shacl/ - diff --git a/frontend/package.json b/frontend/package.json index 20fda0d5f..c6a4389de 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "2.13.0", + "version": "2.14.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 2991c04c7..5c2c568de 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -2032,6 +2032,57 @@ describe("App, authenticated", () => { expect(screen.queryByRole("status", { name: "Event Lineage next action" })).not.toBeInTheDocument(); }); + it("does not scroll Calendar users away from Event Lineage when related evidence lands", async () => { + const scrolledIds: string[] = []; + const originalScrollIntoView = HTMLElement.prototype.scrollIntoView; + HTMLElement.prototype.scrollIntoView = function () { + scrolledIds.push(this.id); + }; + try { + stubBackend(); + render(); + + await userEvent.click(await screen.findByRole("button", { name: "Calendar" })); + const calendar = await screen.findByRole("region", { name: "Calendar" }); + await userEvent.click( + within(calendar).getByRole("button", { name: "Open commitment for: Public post" }), + ); + + await screen.findByRole("status", { name: "Ask next action" }); + expect(document.getElementById("post-event-lineage")).toHaveFocus(); + expect(scrolledIds).not.toContain("post-ask"); + } finally { + HTMLElement.prototype.scrollIntoView = originalScrollIntoView; + } + }); + + it("opening a Calendar commitment focuses Event Lineage; a home list open does not", async () => { + stubBackend(); + render(); + + await userEvent.click(await screen.findByRole("button", { name: "Calendar" })); + const calendar = await screen.findByRole("region", { name: "Calendar" }); + expect(within(calendar).getByLabelText("Next action")).toHaveTextContent( + "Authorized commitments are current. Open a commitment to read Event Lineage.", + ); + await userEvent.click( + within(calendar).getByRole("button", { name: "Open commitment for: Public post" }), + ); + + await waitFor(() => expect(screen.getByText("The full body text.")).toBeInTheDocument()); + expect(document.getElementById("post-event-lineage")).toHaveFocus(); + expect(screen.getByRole("status", { name: "Event Lineage next action" })).toHaveTextContent( + "Public post is current in Event Lineage. Read Keyman and evaluation next.", + ); + + await userEvent.click(screen.getByRole("button", { name: "Close" })); + const board = screen.getByRole("region", { name: "Board" }); + await userEvent.click(within(board).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(); @@ -2864,6 +2915,8 @@ describe("App, authenticated", () => { await userEvent.click(calendarButton); 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("shows the seeded analysis run on the home page", async () => { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d22e2d821..7bbf21eeb 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -883,6 +883,7 @@ function KeymanPanel({ focusTeam, landFirstKeyman, landFirstRelated, + landOnAsk, afterList, }: { postId: string; @@ -897,6 +898,7 @@ function KeymanPanel({ focusTeam?: { teamId: string; teamName: string } | null; landFirstKeyman?: boolean; landFirstRelated?: boolean; + landOnAsk?: boolean; afterList?: ReactNode; }) { const [related, setRelated] = useState(null); @@ -1004,13 +1006,13 @@ function KeymanPanel({ }, [accessToken, landFirstRelated, related]); useEffect(() => { - if (!landFirstRelated || !landedRelatedName || landedRelated === null) { + if (!landFirstRelated || !landOnAsk || !landedRelatedName || landedRelated === null) { return; } const heading = document.getElementById("post-ask"); heading?.focus(); heading?.scrollIntoView?.({ block: "nearest" }); - }, [landFirstRelated, landedRelatedName, landedRelated]); + }, [landFirstRelated, landedRelatedName, landedRelated, landOnAsk]); useEffect(() => { if (!focusPerson) return; @@ -1709,6 +1711,7 @@ function PostDetailPopup({ liveBodyWarning, knowledgeCutoff, focusEventLineage, + focusAskOnLand, onClose, onSelectPost, onSearch, @@ -1720,6 +1723,7 @@ function PostDetailPopup({ liveBodyWarning?: string | null; knowledgeCutoff?: string | null; focusEventLineage?: boolean; + focusAskOnLand?: boolean; onClose: () => void; onSelectPost?: (postId: string) => void; onSearch?: (query: string) => void; @@ -2365,6 +2369,7 @@ function PostDetailPopup({ focusTeam={focusTeam} landFirstKeyman landFirstRelated + landOnAsk={focusAskOnLand} afterList={ <> void; + onSelectPost: (postId: string, options?: SelectPostOptions) => void; + namedNextAction?: boolean; + focusEventLineageOnSelect?: boolean; }) { const [calendar, setCalendar] = useState(null); const [error, setError] = useState(null); @@ -3190,8 +3200,13 @@ function CalendarPanel({ const caldavNextAction = calendar.calendar_sources?.caldav_next_action; return ( -
-

{t("Calendar")}

+
+

{t("Calendar")}

+ {namedNextAction && commitments.length > 0 ? ( +

+ {t("Authorized commitments are current. Open a commitment to read Event Lineage.")} +

+ ) : null}

{t("CalDAV events")}

{events.length === 0 ? ( @@ -3226,7 +3241,12 @@ function CalendarPanel({