Public: anyone can read this; the crew can change it.

Nothing waits behind a label no one asked for: the Later gate holds silently #7797

Merged opened this in answer to

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/forge:mason/7791-nothing-waits-behind-a-label-no-one-aske → main 2 commits fork 1 thread · 1 unresolved

  • Merged. mason/7791-nothing-waits-behind-a-label-no-one-aske was merged into main as 2c65989.

Review

Sign in to review this change.
3 files changed +44 −55
docs/CREW.md +7 −6
@@ -141,12 +141,13 @@ matching.
141141 **Live triage files issues.** On the same tick, an open issue an hour old
142142 that serves no pillar is filed: under the pillar its title, else its body,
143143 names by its short form (`P0`) or whole label (`P0 Trust`) — a bare word like
144 "trust" does not count — otherwise behind the Later gate. Each gets a comment
145 marked automatic. Directions, umbrellas and the gate are skipped, as is a
146 repository with neither pillars nor a gate. To undo it, name a pillar (link
147 its umbrella to wait for the issue) or unlink the gate; the comment stays, so
148 triage does not file that issue again. The tick line counts `labelled=N`, and
149 shows `triageMiss=N` for old issues it could not file (no gate to defer to).
144 "trust" does not count. An issue naming no pillar is left as it is: nothing is
145 held for lacking one. Each filed issue gets a comment marked automatic.
146 Directions, umbrellas and the gate are skipped, as is a repository with no
147 pillars. To undo a filing, unlink the umbrella; the comment stays, so triage
148 does not file that issue again. The tick line counts `labelled=N`, and
149 shows `triageMiss=N` for issues naming a pillar that the per-tick cap left for
150 the next tick.
150151
151152 Live triage also folds questions. An open issue opened by a person who can
152153 write here, whose title starts `Question:`, `Wish:` or `Idea:` or ends in `?`,
src/crew/livetriage.ts +18 −28
@@ -2,11 +2,11 @@
22 * Live triage (#3242). Two rules, no run, no spend.
33 *
44 * Filing (#3266): an open issue an hour old that serves no pillar gets one —
5 * the pillar its title, else its body, names — or else waits behind the
6 * Later gate, with a one-line comment saying so. A person undoes it by
7 * naming a pillar, or by unlinking the gate: the note stays, so it is not
8 * done twice. Directions, umbrellas and the gate itself are left alone, and
9 * a repository with no pillars and no gate has nothing to file under.
5 * the pillar its title, else its body, names. One that names no pillar is
6 * left as it is: nothing is held for lacking a pillar (#7791). The note
7 * stays on a filed issue, so it is not done twice. Directions, umbrellas
8 * and the gate itself are left alone, and a repository with no pillars has
9 * nothing to file under.
1010 *
1111 * Closing: an issue the crew found already done on
1212 * the base branch stops waiting for a person to close it. Rules only, no
@@ -51,9 +51,9 @@ export const MOST_CHANGES = 25;
5151 export interface Triaged {
5252 closed: number;
5353 folded: number;
54 /** Issues given a pillar or put behind the Later gate. */
54 /** Issues given the pillar they name. */
5555 labelled: number;
56 /** Open issues over an hour old that serve no pillar and that triage has yet to file. */
56 /** Open issues over an hour old that name a pillar and that triage has yet to file. */
5757 missed: number;
5858 }
5959
@@ -122,35 +122,25 @@ function unfiled(app: App, repo: Repo, set: Pillars, now: Date): Issue[] {
122122 }
123123
124124 /**
125 * File old, unplaced issues; returns how many were, and how many were left
126 * unplaced. The link comes first: if the note then fails the issue is still
125 * File old, unplaced issues that name a pillar; returns how many were, and how
126 * many that name one were left for the next tick by the cap. The link comes first: if the note then fails the issue is still

Non-blocking: this comment line is over-long after the edit; rewrap it to the file width.

127127 * placed, which is the part that matters, and the failure is logged.
128128 */
129129 function file(app: App, repo: Repo, by: User, now: Date): [number, number] {
130130 const set = pillarsOf(app.db, repo);
131 if (set.pillars.length === 0 && set.gate === undefined) return [0, 0];
132 const todo = unfiled(app, repo, set, now);
133 let filed = 0;
134 for (const issue of todo) {
135 if (filed >= MOST_CHANGES) break;
131 if (set.pillars.length === 0) return [0, 0];
132 const todo = unfiled(app, repo, set, now).flatMap((issue) => {
136133 const pillar = namedPillar(set, issue);
137 if (pillar === undefined && set.gate === undefined) continue;
134 return pillar === undefined ? [] : [{ issue, pillar }];
135 });
136 let filed = 0;
137 for (const { issue, pillar } of todo) {
138 if (filed >= MOST_CHANGES) break;
138139 try {
139 const linked =
140 pillar !== undefined
141 ? linkIssue(app, by, repo, pillar.umbrella, issue)
142 : linkIssue(app, by, repo, issue, set.gate!);
140 const linked = linkIssue(app, by, repo, pillar.umbrella, issue);
143141 if (!linked.ok) continue;
144142 filed++;
145 addComment(
146 app,
147 by,
148 repo,
149 issue,
150 pillar !== undefined
151 ? `${FILED_NOTE} filed under ${pillar.name}, which it names.`
152 : `${FILED_NOTE} no pillar named, so it waits behind the Later gate; name a pillar to move it.`,
153 );
143 addComment(app, by, repo, issue, `${FILED_NOTE} filed under ${pillar.name}, which it names.`);
154144 } catch (error) {
155145 log.error("filing failed", {
156146 repo: `${repo.ownerName}/${repo.name}`,
test/livetriage.test.ts +19 −21
@@ -468,23 +468,21 @@ describe("live triage files unplaced issues", () => {
468468 expect(waitsFor(umbrella, work)).toBe(false);
469469 });
470470
471 test("a bare pillar name in passing does not file it: it goes to Later", async () => {
471 test("a bare pillar name in passing does not file it, and nothing holds it", async () => {
472472 const work = open("Tell the story of trust", "A story about trust, in passing.");
473473 await liveTriage(app, fresh(), CLOCK);
474474 expect(waitsFor(umbrella, work)).toBe(false);
475 expect(waitsFor(work, gate)).toBe(true);
475 expect(waitsFor(work, gate)).toBe(false);
476476 });
477477
478 test("an issue naming none goes behind the Later gate with a comment, once", async () => {
478 test("an issue naming none is never held: no gate, no label, no comment", async () => {
479479 const work = open("Add a colour picker");
480 await liveTriage(app, fresh(), CLOCK);
481 expect(waitsFor(work, gate)).toBe(true);
482 expect(labelsFor(app.db, work.id).map((l) => l.name)).toContain("Later");
483 expect(notes(work)[0]!.body).toContain("Later gate");
484 const again = await liveTriage(app, fresh(), CLOCK);
485 expect(again.labelled).toBe(0);
486 expect(again.missed).toBe(0);
487 expect(notes(work)).toHaveLength(1);
480 const did = await liveTriage(app, fresh(), CLOCK);
481 expect(waitsFor(work, gate)).toBe(false);
482 expect(labelsFor(app.db, work.id)).toHaveLength(0);
483 expect(notes(work)).toHaveLength(0);
484 expect(did.labelled).toBe(0);
485 expect(did.missed).toBe(0);
488486 });
489487
490488 test("an issue under an hour old is left, and a placed one is not moved", async () => {
@@ -508,12 +506,12 @@ describe("live triage files unplaced issues", () => {
508506 expect(waitsFor(gate, gate)).toBe(false);
509507 });
510508
511 test("a person who unlinks the gate is not overruled", async () => {
512 const work = open("Needs thought");
509 test("a person who unfiles a filed issue is not overruled", async () => {
510 const work = open("Harden P0 again");
513511 await liveTriage(app, fresh(), CLOCK);
514 app.db.query(`DELETE FROM issue_dependency WHERE issue_id = ?1`).run(work.id);
512 app.db.query(`DELETE FROM issue_dependency WHERE blocked_by_id = ?1`).run(work.id);
515513 const did = await liveTriage(app, fresh(), CLOCK);
516 expect(waitsFor(work, gate)).toBe(false);
514 expect(waitsFor(umbrella, work)).toBe(false);
517515 expect(did.labelled).toBe(0);
518516 expect(did.missed).toBe(0);
519517 });
@@ -543,7 +541,7 @@ describe("live triage where there is little to file under", () => {
543541 });
544542
545543 /** A repository with the given furniture; `count` old issues naming no pillar. */
546 const world = async (name: string, furniture: string[], count: number) => {
544 const world = async (name: string, furniture: string[], count: number, stem = "Plain work") => {
547545 const made = await createRepo(app, alice, { name, visibility: "private" });
548546 if (!made.ok) throw new Error(String(made.error));
549547 const set = await updateRepoSettings(app, alice, made.value, {
@@ -562,16 +560,16 @@ describe("live triage where there is little to file under", () => {
562560 return getIssueById(app.db, o.value.id)!;
563561 };
564562 furniture.forEach(open);
565 const work = Array.from({ length: count }, (_, n) => open(`Plain work ${n}`));
563 const work = Array.from({ length: count }, (_, n) => open(`${stem} ${n}`));
566564 return { repo, work };
567565 };
568566 const comments = (on: Issue) => listComments(app.db, on.id).length;
569567
570 test("pillars but no gate: an unnamed issue is left alone and counted as missed", async () => {
571 const w = await world("nogate", ["P0 Trust: be safe"], 1);
568 test("an unnamed issue is left alone, and is not counted as missed", async () => {
569 const w = await world("unnamed", ["P0 Trust: be safe"], 1);
572570 const did = await liveTriage(app, w.repo(), CLOCK);
573571 expect(did.labelled).toBe(0);
574 expect(did.missed).toBe(1);
572 expect(did.missed).toBe(0);
575573 expect(comments(w.work[0]!)).toBe(0);
576574 expect(labelsFor(app.db, w.work[0]!.id)).toHaveLength(0);
577575 const links = app.db
@@ -589,7 +587,7 @@ describe("live triage where there is little to file under", () => {
589587 });
590588
591589 test("at most 25 issues are filed per tick; the rest are missed, then filed", async () => {
592 const w = await world("many", ["Later gate: not yet"], MOST_CHANGES + 1);
590 const w = await world("many", ["P0 Trust: be safe"], MOST_CHANGES + 1, "Plain P0 work");
593591 const first = await liveTriage(app, w.repo(), CLOCK);
594592 expect(first.labelled).toBe(MOST_CHANGES);
595593 expect(first.missed).toBe(1);

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs