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 (
-
-