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

Bets are proposed from the owner's notes as well as open issues, once the bets planner is on main #12238

Merged opened this

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

Merged

scout/forge:scout/8825-bets-are-proposed-from-the-owner-s-notes → main 2 commits fork

  • Merged. scout/8825-bets-are-proposed-from-the-owner-s-notes was merged into main as 3b6d43c.

Review

Sign in to review this change.
3 files changed +31 −2
docs/features/bets.md +2
@@ -13,3 +13,5 @@ A bet is a name, an appetite (a share of the week), what done looks like in a fe
1313 - **Idle forge.** If nothing was ever backed and the oldest proposal is a day old, the top two start at 10%, marked "not backed by the owner". One tap on *Stop it* ends any of them, and the planner does not restart a stopped bet.
1414
1515 Left out: a per-week reset of the appetite window.
16
17 A note of the owner's (or an admin's) whose issue no group holds seeds a bet of its own, named from the note and quoting it, even alone. A visitor's note never does.
src/actions/bets.ts +15 −2
@@ -43,7 +43,8 @@ const backed = (app: App, repo: Repo) => listBets(app.db, repo.id).filter((b) =>
4343 /**
4444 * The planner's proposals: open issues in no live bet, grouped by their first
4545 * label, else the pillar they hold up, else the title words they share (the
46 * theme; no model, the same state gives the same proposals). A note written by
46 * theme; no model, the same state gives the same proposals). A note whose issue
47 * no group holds seeds a bet of its own, named from it. A note written by
4748 * the repo's owner or an admin joins a group when its issue is held there, it
4849 * names one of the group's issues as #N, or it shares two words with the
4950 * group's name and titles; it is quoted in the bet. Visitors' notes never join. Each group becomes a bet with
@@ -120,12 +121,24 @@ export function proposeBets(app: App, repo: Repo, now = new Date()): Bet[] {
120121 }
121122 }
122123 }
124 // A note whose issue no group holds seeds a bet of its own, named from the note.
125 const seeded = new Set<string>();
126 for (const n of notes) {
127 const mine = byNumber.get(n.issue.number);
128 if (mine === undefined || held.has(mine.id)) continue;
129 const theme = oneLine(n.issue.title, NAME_MAX).toLowerCase();
130 if (theme === "" || themes.has(theme)) continue;
131 held.add(mine.id);
132 add(theme, mine);
133 seeded.add(theme);
134 quoted.set(theme, [n.issue.title]);
135 }
123136 const named = new Set(listBets(app.db, repo.id).filter((b) => b.state !== "cut" && b.state !== "done").map((b) => b.name));
124137 const lead = listCrewRoles(app.db, repo.id).find((c) => c.role === "builder")?.agent.user.id ?? null;
125138 const made: Bet[] = [];
126139 for (const [theme, issues] of [...themes].sort((a, b) => b[1].length - a[1].length || a[0].localeCompare(b[0]))) {
127140 const name = oneLine(theme[0]!.toUpperCase() + theme.slice(1), NAME_MAX);
128 if (made.length >= 5 || issues.length < 2 || named.has(name)) continue;
141 if (made.length >= 5 || (issues.length < 2 && !seeded.has(theme)) || named.has(name)) continue;
129142 named.add(name);
130143 const journeys = [
131144 ...issues.slice(0, 3).map((i) => `Someone can ${oneLine(i.title, 100)}`),
test/bets.test.ts +14
@@ -162,6 +162,20 @@ describe("proposing from unlabelled work and notes (#8850)", () => {
162162 expect(proposeBets(app, fresh(), NOW)).toEqual([]);
163163 });
164164
165 test("a note with no group of its own seeds a bet named from it; a visitor's note seeds none", async () => {
166 const into = await blank("zz-seed");
167 open("Export zebra stripes", into);
168 open("Render quantum kettle", into);
169 const idea = open("Teach the crew to brew tea", into);
170 insertNote(app.db, { appId: "app-x", repoId: into.id, issueNumber: idea.number, visitor: `u:${alice.id}`, name: "alice" });
171 const stray = open("Paint the harbour orange", into);
172 insertNote(app.db, { appId: "app-x", repoId: into.id, issueNumber: stray.number, visitor: "v:stranger", name: "visitor-1" });
173 const made = proposeBets(app, getRepoById(app.db, into.id)!, NOW);
174 expect(made.map((m) => m.name)).toEqual(["Teach the crew to brew tea"]);
175 expect(made[0]!.journeys).toContain(`The owner's note: "Teach the crew to brew tea"`);
176 expect(betScopes(app.db, made[0]!.id).map((sc) => sc.number)).toEqual([idea.number]);
177 });
178
165179 test("the same state gives the same proposals: names, journeys and scopes match across two repositories", async () => {
166180 const first = await sameState("zz-same-1");
167181 const second = await sameState("zz-same-2");

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs