Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d3f5e61
feat: open Calendar commitments onto Event Lineage (v2.14.0)
seonghobae Aug 19, 2026
af2b114
fix: stop auto-drilled Keyman panel from stealing Event Lineage focus
seonghobae Aug 20, 2026
6a63852
fix: keep Calendar scroll with Event Lineage
seonghobae Aug 20, 2026
8a291d7
chore: restack Calendar on current Weekly VOC head
seonghobae Aug 20, 2026
2c0d2be
chore: restack Calendar on current Weekly VOC head
seonghobae Aug 20, 2026
343301e
chore: restack Calendar focus on current Weekly VOC
seonghobae Aug 20, 2026
47d2102
chore: restack Calendar focus on current Weekly VOC
seonghobae Aug 20, 2026
159f2ce
chore: reconcile remote Calendar restack
seonghobae Aug 20, 2026
e3f9fae
chore: restack Calendar on final Weekly VOC
seonghobae Aug 20, 2026
93cb898
chore: restack Calendar on final Weekly VOC
seonghobae Aug 20, 2026
2039785
chore: restack Calendar on final Weekly VOC
seonghobae Aug 20, 2026
c2e7ecd
Merge remote-tracking branch 'origin/feat/board-weekly-voc-open-event…
seonghobae Aug 20, 2026
6f7dcb3
Merge remote-tracking branch 'origin/feat/board-weekly-voc-open-event…
seonghobae Aug 20, 2026
ecb5722
Merge commit '4fdd027e6eb0708d5b78b2acde76e031a25d03bb' of https://gi…
seonghobae Aug 20, 2026
b829464
fix: avoid calendar ADR number collision
seonghobae Aug 20, 2026
611fa08
fix: align embedded image accessibility assertion
seonghobae Aug 20, 2026
507021e
Merge remote-tracking branch 'origin/feat/board-weekly-voc-open-event…
seonghobae Aug 20, 2026
bf2cf37
Merge remote-tracking branch 'origin/feat/board-weekly-voc-open-event…
seonghobae Aug 20, 2026
cae9f69
Merge remote-tracking branch 'origin/feat/board-weekly-voc-open-event…
seonghobae Aug 20, 2026
6e336c5
Merge current board lineage base into calendar lineage
seonghobae Aug 20, 2026
14d62e2
chore: sync lock metadata with release version
seonghobae Aug 20, 2026
cc01b4d
merge: propagate current board lineage base
seonghobae Aug 20, 2026
0e62d30
merge: propagate corrected Weekly VOC lineage base
seonghobae Aug 20, 2026
9f26564
merge: propagate current protected board base
seonghobae Aug 20, 2026
4a05a91
merge: propagate current board branch head
seonghobae Aug 20, 2026
bd1b4d2
merge: propagate current semantic-source parent
seonghobae Aug 20, 2026
848ff4c
Merge branch 'feat/board-weekly-voc-open-event-lineage-v2130' of http…
seonghobae Aug 20, 2026
a71b8a6
Merge remote-tracking branch 'origin/feat/board-weekly-voc-open-event…
seonghobae Aug 20, 2026
221cc94
fix(calendar): keep home commitment opens neutral
seonghobae Aug 20, 2026
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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,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
Expand Down
3 changes: 2 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 +
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.d/2.14.0-calendar-open-event-lineage.md
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
33 changes: 33 additions & 0 deletions docs/adr/0094-calendar-open-focuses-event-lineage.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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
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": "2.13.0",
"version": "2.14.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
53 changes: 53 additions & 0 deletions frontend/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1955,6 +1955,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(<App />);

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(<App />);

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(<App showLabPanels />);
Expand Down Expand Up @@ -2751,6 +2802,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 () => {
Expand Down
54 changes: 44 additions & 10 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,7 @@ function KeymanPanel({
focusTeam,
landFirstKeyman,
landFirstRelated,
landOnAsk,
afterList,
}: {
postId: string;
Expand All @@ -858,6 +859,7 @@ function KeymanPanel({
focusTeam?: { teamId: string; teamName: string } | null;
landFirstKeyman?: boolean;
landFirstRelated?: boolean;
landOnAsk?: boolean;
afterList?: ReactNode;
}) {
const [related, setRelated] = useState<RelatedNode[] | null>(null);
Expand Down Expand Up @@ -965,13 +967,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;
Expand Down Expand Up @@ -1669,6 +1671,7 @@ function PostDetailPopup({
liveBodyWarning,
knowledgeCutoff,
focusEventLineage,
focusAskOnLand,
onClose,
onSelectPost,
onSearch,
Expand All @@ -1680,6 +1683,7 @@ function PostDetailPopup({
liveBodyWarning?: string | null;
knowledgeCutoff?: string | null;
focusEventLineage?: boolean;
focusAskOnLand?: boolean;
onClose: () => void;
onSelectPost?: (postId: string) => void;
onSearch?: (query: string) => void;
Expand Down Expand Up @@ -2286,6 +2290,7 @@ function PostDetailPopup({
focusTeam={focusTeam}
landFirstKeyman
landFirstRelated
landOnAsk={focusAskOnLand}
afterList={
<>
<EvaluationPanel
Expand Down Expand Up @@ -2505,6 +2510,7 @@ type SelectPostOptions = {
knowledgeCutoff?: string;
fromReportMember?: boolean;
fromWeeklyVoc?: boolean;
fromCalendar?: boolean;
};

/**
Expand Down Expand Up @@ -3085,9 +3091,13 @@ function RankingsPanel({
function CalendarPanel({
accessToken,
onSelectPost,
namedNextAction = false,
focusEventLineageOnSelect = false,
}: {
accessToken: string;
onSelectPost: (postId: string) => void;
onSelectPost: (postId: string, options?: SelectPostOptions) => void;
namedNextAction?: boolean;
focusEventLineageOnSelect?: boolean;
}) {
const [calendar, setCalendar] = useState<CalendarResponse | null>(null);
const [error, setError] = useState<string | null>(null);
Expand All @@ -3107,8 +3117,13 @@ function CalendarPanel({
const caldavNextAction = calendar.calendar_sources?.caldav_next_action;

return (
<section className="popup-section lineage-home">
<h2>{t("Calendar")}</h2>
<section className="popup-section lineage-home" aria-labelledby="calendar-title">
<h2 id="calendar-title">{t("Calendar")}</h2>
{namedNextAction && commitments.length > 0 ? (
<p className="board-next-action" role="status" aria-label={t("Next action")}>
{t("Authorized commitments are current. Open a commitment to read Event Lineage.")}
</p>
) : null}
<section className="popup-section">
<h3>{t("CalDAV events")}</h3>
{events.length === 0 ? (
Expand Down Expand Up @@ -3143,7 +3158,12 @@ function CalendarPanel({
<button
className="post-list-item"
aria-label={`${t("Open commitment for:")} ${entry.post_title}`}
onClick={() => onSelectPost(entry.post_id)}
onClick={() =>
onSelectPost(
entry.post_id,
focusEventLineageOnSelect ? { fromCalendar: true } : undefined,
)
}
>
<span className="ticket-title">
{entry.commitment_summary ?? entry.ticket_title}
Expand Down Expand Up @@ -3540,11 +3560,13 @@ function PostList({
accessToken,
showLabPanels = false,
postIdToOpen = null,
postOpenFromCalendar = false,
onPostOpened,
}: {
accessToken: string;
showLabPanels?: boolean;
postIdToOpen?: string | null;
postOpenFromCalendar?: boolean;
onPostOpened?: () => void;
}) {
const [posts, setPosts] = useState<PostSummary[] | null>(null);
Expand All @@ -3564,6 +3586,7 @@ function PostList({
const [landOnComparison, setLandOnComparison] = useState(false);
const [openedFromReportMember, setOpenedFromReportMember] = useState(false);
const [openedFromWeeklyVoc, setOpenedFromWeeklyVoc] = useState(false);
const [openedFromCalendar, setOpenedFromCalendar] = useState(false);
const [corporateEntities, setCorporateEntities] = useState<CorporateEntityRef[] | null>(null);
const [entitiesLoadError, setEntitiesLoadError] = useState<string | null>(null);
const [totalPosts, setTotalPosts] = useState(0);
Expand Down Expand Up @@ -3619,20 +3642,22 @@ function PostList({
setOpenedCutoffIso(options?.knowledgeCutoff ?? null);
setOpenedFromReportMember(Boolean(options?.fromReportMember));
setOpenedFromWeeklyVoc(Boolean(options?.fromWeeklyVoc));
setOpenedFromCalendar(Boolean(options?.fromCalendar));
}

useEffect(() => {
if (!postIdToOpen) return;
selectPost(postIdToOpen);
selectPost(postIdToOpen, postOpenFromCalendar ? { fromCalendar: true } : undefined);
onPostOpened?.();
}, [onPostOpened, postIdToOpen]);
}, [onPostOpened, postIdToOpen, postOpenFromCalendar]);

function closeSelectedPost() {
setSelectedPostId(null);
setOpenedAfterCutoff(false);
setOpenedCutoffIso(null);
setOpenedFromReportMember(false);
setOpenedFromWeeklyVoc(false);
setOpenedFromCalendar(false);
const url = new URL(window.location.href);
if (url.searchParams.has("post")) {
url.searchParams.delete("post");
Expand Down Expand Up @@ -4116,7 +4141,8 @@ function PostList({
openedAfterCutoff ? analysisRunOpenedBodyWarning(openedCutoffIso) : null
}
knowledgeCutoff={openedAfterCutoff ? openedCutoffIso : null}
focusEventLineage={openedFromReportMember || openedFromWeeklyVoc}
focusEventLineage={openedFromReportMember || openedFromWeeklyVoc || openedFromCalendar}
focusAskOnLand={openedFromReportMember}
onClose={closeSelectedPost}
onSelectPost={selectPost}
onSearch={searchBoard}
Expand Down Expand Up @@ -4608,6 +4634,7 @@ export default function App({ showLabPanels = false }: { showLabPanels?: boolean
if (typeof window === "undefined") return null;
return new URLSearchParams(window.location.search).get("post");
});
const [postOpenFromCalendar, setPostOpenFromCalendar] = 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
Expand Down Expand Up @@ -4683,7 +4710,11 @@ export default function App({ showLabPanels = false }: { showLabPanels?: boolean
accessToken={accessToken}
showLabPanels={testOnlyLabPanels}
postIdToOpen={postToOpen}
onPostOpened={() => setPostToOpen(null)}
postOpenFromCalendar={postOpenFromCalendar}
onPostOpened={() => {
setPostToOpen(null);
setPostOpenFromCalendar(false);
}}
/>
) : null}
{destination === "customers" ? (
Expand All @@ -4698,8 +4729,11 @@ export default function App({ showLabPanels = false }: { showLabPanels?: boolean
{destination === "calendar" ? (
<CalendarPanel
accessToken={accessToken}
namedNextAction
focusEventLineageOnSelect
onSelectPost={(postId) => {
setPostToOpen(postId);
setPostOpenFromCalendar(true);
setDestination("board");
}}
/>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/i18n.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ describe("i18n", () => {
"Weekly VOC",
"Filter by ISO week",
"All weeks",
"Authorized commitments are current. Open a commitment to read Event Lineage.",
] as const;

it("supports the five product locales", () => {
Expand Down
Loading