diff --git a/AGENTS.md b/AGENTS.md index c790995c1..32e0b0ab4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -75,12 +75,13 @@ in the same spirit) -- never against real data, per the hard rule above. against a live local stack (`make up`) and self-skip without one -- see [README.md](README.md#local-product-stack-docker-compose). -Period leftover pairs (ADR 0017 / 0018) are computed in +Period leftover pairs (ADR 0017 / 0018 / 0030) are computed in `lineageweave/leftover_pairs.py` from the residual after a real GRM/GPCM score, never invented. Missing cells stay out of the Gabriel factorization. Closest and farthest post–criterion pairs persist to `report_leftover_pair` and sit above the member list so -a click opens that post. +a click opens that post. A calendar commitment that is a leftover +pair also names the leftover criterion next to the due date. `frontend/` has its own toolchain (Node pinned via `frontend/mise.toml`, pnpm via Corepack -- do not add a second Node package manager or a diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index f8a83ceb1..d2146db35 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -122,7 +122,9 @@ flowchart LR `rankweave_client.py`'s default transport raises `RankWeaveNotAvailable`. `GET /api/rankings` then returns `rankweave_not_available` and an empty ranking list. Hidden posts - are omitted from every channel. See ADR 0024. + are omitted from every channel. See ADR 0024. A calendar + commitment that is also a leftover pair names that criterion + (ADR 0030). ## Standards and citations diff --git a/CHANGELOG.d/0.89.0-leftover-calendar.md b/CHANGELOG.d/0.89.0-leftover-calendar.md new file mode 100644 index 000000000..d3a9d4998 --- /dev/null +++ b/CHANGELOG.d/0.89.0-leftover-calendar.md @@ -0,0 +1,8 @@ +# 0.89.0 — Leftover badge on a matching calendar commitment + +## Added + +- Calendar commitments that are leftover pairs name the leftover + criterion (ADR 0030). After `make seed`, the leftover commitment + reads **Closest leftover · sales-lead** next to the due date; + click opens that post. An empty calendar stays fail-closed. diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bfcaa28f..551e6d84a 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). +## [0.89.0] - 2026-08-18 + +### Added + +- Calendar commitments that are leftover pairs name the leftover + criterion (ADR 0030). After `make seed`, the leftover commitment + reads **Closest leftover · sales-lead** next to the due date; + click opens that post. An empty calendar stays fail-closed. + Never invent a fused score or a theta. + ## [0.75.0] - 2026-08-17 ### Added diff --git a/docs/adr/0030-leftover-pair-on-calendar-commitment.md b/docs/adr/0030-leftover-pair-on-calendar-commitment.md new file mode 100644 index 000000000..685b181f9 --- /dev/null +++ b/docs/adr/0030-leftover-pair-on-calendar-commitment.md @@ -0,0 +1,58 @@ +# ADR 0030 — Name leftover on a matching calendar commitment + +**Decision status:** Accepted +**Date:** 2026-08-18 + +## Context + +ADR 0018 puts leftover pairs above the period-report member list. +Calendar sits under Rankings and lists due commitments (Allen +intervals). A buyer who starts from Calendar still sees only the +commitment title, post title, status, and due date. The leftover +post is already on that list when it has a due ticket; the row just +does not name the leftover criterion. + +Do not invent a second leftover store. Do not invent a fused score +or a theta. An empty calendar stays **No upcoming commitments**. + +## Decision + +When an authorized leftover pair names a calendar commitment, that +button shows `Closest leftover · {criterion}` or +`Farthest leftover · {criterion}` and includes the same caption in +its accessible name. + +A commitment that is not a leftover pair stays unmarked. A leftover +pair for a hidden post never reaches Calendar (ADR 0017 ABAC). A +report fetch error clears leftover badges and leaves the due list +intact — never an invented pair. + +After `make seed`, the leftover calendar commitment reads **Closest +leftover · sales-lead** next to the due date; click still opens +that post. + +Leftover evidence is the same authorized `leftover_pairs` already +on the period-report payload. + +## Consequences + +Leftover buttons above the member list stay (ADR 0018). Rankings +leftover badges stay on #252 / ADR 0029. Member badges stay on +#234 / ADR 0028. This slice only labels the already-visible +calendar commitment. + +## Related + +Depends on [ADR 0017](0017-persist-lsirm-leftover-pairs.md) and +[ADR 0018](0018-leftover-pair-report-ui.md). + +## References + +Jeon, M., Jin, I. H., Schweinberger, M., & Baugh, S. (2021). Mapping +unobserved item–respondent interactions: A latent space item response +model with interaction map. *Psychometrika, 86*(2), 378–403. +https://doi.org/10.1007/s11336-021-09762-5 + +Allen, J. F. (1983). Maintaining knowledge about temporal intervals. +*Communications of the ACM, 26*(11), 832–843. +https://doi.org/10.1145/182.358434 diff --git a/frontend/package.json b/frontend/package.json index 575b7c586..51401bfd3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "0.75.0", + "version": "0.89.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index a32a26403..6756cf914 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -1330,6 +1330,76 @@ describe("App, authenticated", () => { await waitFor(() => expect(screen.getByText("The full body text.")).toBeInTheDocument()); }); + it("names leftover criterion on a matching calendar commitment", async () => { + stubBackend({ + calendarCommitments: [ + { + issue_ticket_id: "ticket-a100", + post_id: "post-1", + ticket_status_code: "open", + ticket_status_label: "Open", + ticket_title: "Send Northridge Grid the revised quote", + assigned_account_id: null, + due_date: "2026-01-12", + commitment_summary: null, + created_at: "2026-01-01T00:00:00Z", + updated_at: "2026-01-01T00:00:00Z", + post_title: "Public post", + }, + { + issue_ticket_id: "ticket-b200", + post_id: "post-2", + ticket_status_code: "open", + ticket_status_label: "Open", + ticket_title: "Send Westfield Power the revised specification", + assigned_account_id: null, + due_date: "2026-01-14", + commitment_summary: null, + created_at: "2026-01-02T00:00:00Z", + updated_at: "2026-01-02T00:00:00Z", + post_title: "Specification revision requested", + }, + { + issue_ticket_id: "ticket-c300", + post_id: "post-9", + ticket_status_code: "open", + ticket_status_label: "Open", + ticket_title: "Confirm Riverbend delivery window", + assigned_account_id: null, + due_date: "2026-01-16", + commitment_summary: null, + created_at: "2026-01-03T00:00:00Z", + updated_at: "2026-01-03T00:00:00Z", + post_title: "Riverbend calendar commitment", + }, + ], + }); + render(); + + const leftoverCommitment = await screen.findByRole("button", { + name: /open commitment for: public post \(closest leftover · sales-lead\)/i, + }); + expect(leftoverCommitment).toHaveTextContent("Send Northridge Grid the revised quote"); + expect(leftoverCommitment).toHaveTextContent("Closest leftover · sales-lead"); + expect(leftoverCommitment).toHaveAccessibleName( + "Open commitment for: Public post (Closest leftover · sales-lead)", + ); + expect( + await screen.findByRole("button", { + name: /open commitment for: specification revision requested \(farthest leftover · negative\)/i, + }), + ).toHaveTextContent("Farthest leftover · negative"); + const unmarked = screen.getByRole("button", { + name: /^open commitment for: riverbend calendar commitment$/i, + }); + expect(unmarked).toHaveTextContent("due 2026-01-16"); + expect(unmarked).not.toHaveTextContent("leftover"); + + await userEvent.click(leftoverCommitment); + + await waitFor(() => expect(screen.getByText("The full body text.")).toBeInTheDocument()); + }); + it("shows the calibrated period-report mean theta on the home page", async () => { stubBackend(); render(); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6056e5eb4..703f8fa8e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -52,6 +52,7 @@ import { type PostSummary, type RankingList, type RelatedNode, + type LeftoverPair, type VocEvidence, } from "./api"; import { LineageDag } from "./LineageDag"; @@ -82,6 +83,16 @@ function criterionShortLabel(itemCode: string): string { return CRITERION_SHORT_LABEL[itemCode] ?? itemCode; } +function leftoverRowLabel(pairKind: string): string { + return pairKind === "farthest" ? "Farthest leftover" : "Closest leftover"; +} + +function leftoverCaptionForPairs(pairs: LeftoverPair[]): string { + return pairs + .map((pair) => `${leftoverRowLabel(pair.pair_kind)} · ${criterionShortLabel(pair.criterion_code)}`) + .join("; "); +} + // This popup's layout follows the textual product brief (Korean summary, // key events, R&R, Event Lineage, Keyman, in-popup chat with a sliding // evidence panel) rather than the referenced Figma frame's actual pixel @@ -1373,9 +1384,11 @@ function RankingsPanel({ function CalendarPanel({ accessToken, + leftoverPairs, onSelectPost, }: { accessToken: string; + leftoverPairs: LeftoverPair[]; onSelectPost: (postId: string) => void; }) { const [commitments, setCommitments] = useState(null); @@ -1399,11 +1412,18 @@ function CalendarPanel({

) : (
    - {commitments.map((entry) => ( + {commitments.map((entry) => { + const leftoverForEntry = leftoverPairs.filter((pair) => pair.post_id === entry.post_id); + const leftoverCaption = leftoverCaptionForPairs(leftoverForEntry); + return (
  • - ))} + ); + })}
)} @@ -1425,10 +1454,12 @@ function ReportsPanel({ accessToken, canRebuild, onSelectPost, + onLeftoverPairsChange, }: { accessToken: string; canRebuild: boolean; onSelectPost: (postId: string) => void; + onLeftoverPairsChange: (pairs: LeftoverPair[]) => void; }) { const [grouping, setGrouping] = useState("process_unit"); const [period, setPeriod] = useState("2026-W02"); @@ -1455,9 +1486,13 @@ function ReportsPanel({ setPayload(reports); setIndex(periods); setComparison(compared); + onLeftoverPairsChange(reports.reports.flatMap((row) => row.leftover_pairs ?? [])); }) - .catch((err) => setError(String(err))); - }, [accessToken, grouping, period]); + .catch((err) => { + setError(String(err)); + onLeftoverPairsChange([]); + }); + }, [accessToken, grouping, period, onLeftoverPairsChange]); async function handleRebuild() { setRebuilding(true); @@ -1472,8 +1507,10 @@ function ReportsPanel({ setPayload(reports); setIndex(periods); setComparison(compared); + onLeftoverPairsChange(reports.reports.flatMap((row) => row.leftover_pairs ?? [])); } catch (err) { setError(String(err)); + onLeftoverPairsChange([]); } finally { setRebuilding(false); } @@ -1658,6 +1695,7 @@ function PostList({ accessToken }: { accessToken: string }) { const [graph, setGraph] = useState(null); const [error, setError] = useState(null); const [selectedPostId, setSelectedPostId] = useState(null); + const [leftoverPairs, setLeftoverPairs] = useState([]); const [canRebuild, setCanRebuild] = useState(false); const [rebuilding, setRebuilding] = useState(false); const [rebuildError, setRebuildError] = useState(null); @@ -1690,8 +1728,17 @@ function PostList({ accessToken }: { accessToken: string }) { return ( <> - - + +

Event Lineage

diff --git a/lineageweave/__init__.py b/lineageweave/__init__.py index 1710c009e..f481e5311 100644 --- a/lineageweave/__init__.py +++ b/lineageweave/__init__.py @@ -35,4 +35,4 @@ "sentence_excerpts", ] -__version__ = "0.75.0" +__version__ = "0.89.0" diff --git a/pyproject.toml b/pyproject.toml index 764ebad72..193f3c602 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lineageweave" -version = "0.75.0" +version = "0.89.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 08eab7768..f4a0528d5 100644 --- a/uv.lock +++ b/uv.lock @@ -355,7 +355,7 @@ wheels = [ [[package]] name = "lineageweave" -version = "0.75.0" +version = "0.89.0" source = { virtual = "." } dependencies = [ { name = "certifi" },