Skip to content

Commit

Permalink
Add a category for the "Needs Edits" label, treated similarly to Agen…
Browse files Browse the repository at this point in the history
…da+.
  • Loading branch information
jyasskin committed Mar 7, 2024
1 parent bdff9a0 commit 2b5a6b4
Show file tree
Hide file tree
Showing 12 changed files with 186 additions and 71 deletions.
40 changes: 19 additions & 21 deletions frontend/src/components/Issue.astro
@@ -1,48 +1,46 @@
---
import { formatRoundAge } from "@lib/formatRoundAge";
import type { IssueSummary } from "@lib/repo-summaries";
import { agendaSLO, slo, sloMap } from "@lib/slo";
import { slo } from "@lib/slo";
import assert from "node:assert";
interface Props {
issue: IssueSummary;
// Show the issue's time on the agenda rather than its prioritized SLO time.
agenda?: boolean;
// Show the issue's time in a particular category rather than its prioritized SLO time.
category?: "agenda" | "needsEdits";
}
const { issue, agenda } = Astro.props;
const { issue, category } = Astro.props;
const { withinSlo, onAgendaTooLong } = slo(issue);
const { withinSlo, untilSlo, categories } = slo(issue);
const categoryInfo = category && categories[category];
const timeToReport = (function () {
if (agenda) {
assert(issue.onAgendaFor);
if (onAgendaTooLong) {
return issue.onAgendaFor.subtract(agendaSLO);
if (categoryInfo) {
if (categoryInfo.untilSlo.sign > 0) {
return categoryInfo.timeUsed;
}
return issue.onAgendaFor;
return categoryInfo.untilSlo.negated();
} else {
if (withinSlo) {
return issue.sloTimeUsed;
} else {
if (issue.whichSlo === "none") {
throw new Error(
`Can't be out of SLO with a 'none' SLO type: ${JSON.stringify(
issue
)}`
);
}
return issue.sloTimeUsed.subtract(sloMap[issue.whichSlo]);
assert.notStrictEqual(issue.whichSlo, "none");
assert(untilSlo);
return untilSlo.negated();
}
}
})();
---

<a href={issue.url}>{issue.title}</a>: {
agenda ? (
onAgendaTooLong ? (
categoryInfo ? (
categoryInfo.untilSlo.sign !== 1 ? (
<span class="error">out of SLO</span>
) : (
) : category === "agenda" ? (
"on the agenda"
) : (
"has needed edits"
)
) : withinSlo ? (
"on maintainers' plate"
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/lib/published-json.ts
Expand Up @@ -7,10 +7,12 @@ export const SummaryJson = z.object({}).catchall(z.object({
urgentViolations: z.number(),
soonViolations: z.number(),
agendaViolations: z.number(),
needsEditsViolations: z.number(),
needTriage: z.number(),
urgent: z.number(),
soon: z.number(),
agenda: z.number(),
needsEdits: z.number(),
other: z.number(),
}));
export type SummaryJson = z.infer<typeof SummaryJson>;
Expand All @@ -25,16 +27,19 @@ export const RepoJson = z.object({
urgentViolations: z.number(),
soonViolations: z.number(),
agendaViolations: z.number(),
needsEditsViolations: z.number(),
needTriage: z.number(),
urgent: z.number(),
soon: z.number(),
agenda: z.number(),
needsEdits: z.number(),
other: z.number(),
}),
triage: IssueSummaryWithSlo.array(),
urgent: IssueSummaryWithSlo.array(),
soon: IssueSummaryWithSlo.array(),
agenda: IssueSummaryWithSlo.array(),
needsEdits: IssueSummaryWithSlo.array(),
other: IssueSummaryWithSlo.array(),
});
export type RepoJson = z.infer<typeof RepoJson>;
3 changes: 3 additions & 0 deletions frontend/src/lib/repo-summaries.ts
Expand Up @@ -20,6 +20,8 @@ export const IssueSummaryInContent = z.object({
whichSlo: SloType,
// For Agenda+ issues, this is how long since the label was most-recently added.
onAgendaFor: checkDuration.optional(),
// For 'Needs Edits' issues, this is how long since the label was most-recently added.
neededEditsFor: checkDuration.optional(),
stats: z.object({
numTimelineItems: z.number(),
numComments: z.number().optional(),
Expand All @@ -32,6 +34,7 @@ export const IssueSummary = IssueSummaryInContent.extend({
createdAt: instant,
sloTimeUsed: duration,
onAgendaFor: duration.optional(),
neededEditsFor: duration.optional(),
});
export type IssueSummary = z.infer<typeof IssueSummary>;

Expand Down
74 changes: 58 additions & 16 deletions frontend/src/lib/slo.ts
Expand Up @@ -9,6 +9,7 @@ export const soonSLO = Temporal.Duration.from({ days: 91 });
// A 5-week SLO to address something on the agenda allows a twice-a-month meeting to miss the item
// once.
export const agendaSLO = Temporal.Duration.from({ days: 35 });
export const editsSLO = Temporal.Duration.from({ days: 14 });

// Keep to at most 25 agenda items.
export const agendaLengthSLO = 25;
Expand All @@ -19,28 +20,51 @@ export const sloMap = {
"triage": triageSLO
} as const;

type CategoryInfo = {
timeUsed: Temporal.Duration;
// This is positive if the issue is within this category's SLO, or negative by the amount the
// issue is out-of-SLO.
untilSlo: Temporal.Duration;
};

export interface SloStatus {
whichSlo: SloType;
withinSlo: boolean;
onAgenda: boolean;
onAgendaTooLong: boolean;
// This is undefined if the issue has no SLO, positive if the issue is within its priority's
// SLO, or negative by the amount the issue is out-of-SLO.
untilSlo?: Temporal.Duration;
// When an issue is in a particular category, that key will be present.
categories: {
agenda?: CategoryInfo
needsEdits?: CategoryInfo
}
};

export function slo(issue: Pick<IssueSummary, "whichSlo" | "sloTimeUsed" | "onAgendaFor">): SloStatus {
const onAgenda = issue.onAgendaFor !== undefined;
const onAgendaTooLong = issue.onAgendaFor !== undefined &&
Temporal.Duration.compare(issue.onAgendaFor, agendaSLO) > 0;
export function slo(issue: Pick<IssueSummary, "whichSlo" | "sloTimeUsed" | "onAgendaFor" | "neededEditsFor">): SloStatus {
let categories: SloStatus["categories"] = {};
if (issue.onAgendaFor) {
categories.agenda = {
timeUsed: issue.onAgendaFor,
untilSlo: agendaSLO.subtract(issue.onAgendaFor),
};
}
if (issue.neededEditsFor) {
categories.needsEdits = {
timeUsed: issue.neededEditsFor,
untilSlo: editsSLO.subtract(issue.neededEditsFor),
}
}
if (issue.whichSlo === "none") {
return { whichSlo: "none", withinSlo: true, onAgenda, onAgendaTooLong };
return { whichSlo: "none", withinSlo: true, categories };
}
const slo = sloMap[issue.whichSlo];

const untilSlo = slo.subtract(issue.sloTimeUsed);
return {
whichSlo: issue.whichSlo,
withinSlo:
Temporal.Duration.compare(issue.sloTimeUsed, slo) < 0,
onAgenda,
onAgendaTooLong,
untilSlo,
withinSlo: untilSlo.sign > 0,
categories,
};
}

Expand All @@ -51,16 +75,21 @@ const zeroDuration = Temporal.Duration.from({ seconds: 0 });
export function cmpByAgendaUsed(a: IssueSummary, b: IssueSummary) {
return Temporal.Duration.compare(b.onAgendaFor ?? zeroDuration, a.onAgendaFor ?? zeroDuration);
}
export function cmpByNeededEditsFor(a: IssueSummary, b: IssueSummary) {
return Temporal.Duration.compare(b.neededEditsFor ?? zeroDuration, a.neededEditsFor ?? zeroDuration);
}

export interface SloGroups {
untriaged: IssueSummary[];
urgent: IssueSummary[];
soon: IssueSummary[];
agenda: IssueSummary[];
needsEdits: IssueSummary[];
triageViolations: IssueSummary[];
urgentViolations: IssueSummary[];
soonViolations: IssueSummary[];
agendaViolations: IssueSummary[];
needsEditsViolations: IssueSummary[];
other: IssueSummary[];
}
export function groupBySlo(issues: IssueSummary[]): SloGroups {
Expand All @@ -69,14 +98,16 @@ export function groupBySlo(issues: IssueSummary[]): SloGroups {
urgent: [],
soon: [],
agenda: [],
needsEdits: [],
triageViolations: [],
urgentViolations: [],
soonViolations: [],
agendaViolations: [],
needsEditsViolations: [],
other: [],
};
for (const issue of issues) {
const { whichSlo, withinSlo, onAgenda, onAgendaTooLong } = slo(issue);
const { whichSlo, withinSlo, categories: { agenda, needsEdits } } = slo(issue);
switch (whichSlo) {
case "urgent":
if (withinSlo) {
Expand All @@ -103,15 +134,26 @@ export function groupBySlo(issues: IssueSummary[]): SloGroups {
result.other.push(issue);
break;
}
if (onAgendaTooLong) {
result.agendaViolations.push(issue);
} else if (onAgenda) {
result.agenda.push(issue);
if (agenda) {
if (agenda.untilSlo.sign < 0) {
result.agendaViolations.push(issue);
} else {
result.agenda.push(issue);
}
}
if (needsEdits){
if (needsEdits.untilSlo.sign < 0) {
result.needsEditsViolations.push(issue);
} else {
result.needsEdits.push(issue);
}
}
}
for (const [key, list] of Object.entries(result)) {
if (key.startsWith('agenda')) {
list.sort(cmpByAgendaUsed);
} else if (key.startsWith('needsEdits')) {
list.sort(cmpByNeededEditsFor);
} else {
list.sort(cmpByTimeUsed);
}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/triage-labels.ts
Expand Up @@ -3,3 +3,4 @@ export const soon = { name: "Priority: Soon", color: "#fbca04" } as const;
export const eventually = { name: "Priority: Eventually", color: "#c2e0c6" } as const;
export const needsFeedback = { name: "Needs Reporter Feedback", color: "#5319e7" } as const;
export const agenda = { name: "Agenda+", color: "#000000" } as const;
export const needsEdits = {name: "Needs Edits", color: "#5319e7"}
41 changes: 40 additions & 1 deletion frontend/src/pages/[org]/[repo].astro
Expand Up @@ -9,7 +9,9 @@ import {
agendaLengthSLO,
agendaSLO,
cmpByAgendaUsed,
cmpByNeededEditsFor,
cmpByTimeUsed,
editsSLO,
groupBySlo,
soonSLO,
triageSLO,
Expand Down Expand Up @@ -42,6 +44,8 @@ const {
triageViolations,
agenda,
agendaViolations,
needsEdits,
needsEditsViolations,
soon,
soonViolations,
urgent,
Expand All @@ -53,6 +57,8 @@ soon.push(...soonViolations);
soon.sort(cmpByTimeUsed);
agenda.push(...agendaViolations);
agenda.sort(cmpByAgendaUsed);
needsEdits.push(...needsEditsViolations);
needsEdits.sort(cmpByNeededEditsFor);
untriaged.push(...triageViolations);
untriaged.sort(cmpByTimeUsed);
---
Expand Down Expand Up @@ -111,6 +117,17 @@ untriaged.sort(cmpByTimeUsed);
</li>
) : null
}
{
needsEdits.length > 0 ? (
<li>
<a href="#need-edits">
{needsEditsViolations.length
? `${needsEditsViolations.length} issues with out-of-SLO pending edits`
: `${needsEdits.length} issues with pending edits`}
</a>
</li>
) : null
}
{
agenda.length > 0 ? (
<li>
Expand Down Expand Up @@ -181,6 +198,28 @@ untriaged.sort(cmpByTimeUsed);
) : null
}

{
needsEdits.length > 0 ? (
<>
<h2 id="need-edits">Need Edits</h2>
<p>
Try to apply the
{/* prettier-ignore */}
<a href={`${import.meta.env.BASE_URL}about#needs-edits`}>edits
requested in these issues</a>
within <Duration d={editsSLO} weeks />.
</p>
<ul>
{needsEdits.map((issue) => (
<li>
<Issue {issue} category="needsEdits" />
</li>
))}
</ul>
</>
) : null
}

{
agenda.length > 0 ? (
<>
Expand All @@ -196,7 +235,7 @@ untriaged.sort(cmpByTimeUsed);
<ul>
{agenda.map((issue) => (
<li>
<Issue {issue} agenda />
<Issue {issue} category="agenda" />
</li>
))}
</ul>
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/pages/[org]/[repo].json.ts
Expand Up @@ -30,16 +30,19 @@ export const GET: APIRoute = ({ props }) => {
urgentViolations: groups.urgentViolations.length,
soonViolations: groups.soonViolations.length,
agendaViolations: groups.agendaViolations.length,
needsEditsViolations: groups.needsEditsViolations.length,
needTriage: groups.untriaged.length,
urgent: groups.urgent.length,
soon: groups.soon.length,
agenda: groups.agenda.length,
needsEdits: groups.needsEdits.length,
other: groups.other.length,
},
triage: groups.triageViolations.map(issue => Object.assign(issue, outOfSloObj)).concat(groups.untriaged),
urgent: groups.urgentViolations.map(issue => Object.assign(issue, outOfSloObj)).concat(groups.urgent),
soon: groups.soonViolations.map(issue => Object.assign(issue, outOfSloObj)).concat(groups.soon),
agenda: groups.agendaViolations.map(issue => Object.assign(issue, outOfSloObj)).concat(groups.agenda),
needsEdits: groups.needsEditsViolations.map(issue => Object.assign(issue, outOfSloObj)).concat(groups.needsEdits),
other: groups.other,
} satisfies RepoJson;
return new Response(JSON.stringify(summary));
Expand Down

0 comments on commit 2b5a6b4

Please sign in to comment.