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
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/8825-bets-are-proposed-from-the-owner-s-notes → main 2 commits fork
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 | ||
| 13 | 13 | - **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. |
| 14 | 14 | |
| 15 | 15 | 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) => | ||
| 43 | 43 | /** |
| 44 | 44 | * The planner's proposals: open issues in no live bet, grouped by their first |
| 45 | 45 | * 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 | |
| 47 | 48 | * the repo's owner or an admin joins a group when its issue is held there, it |
| 48 | 49 | * names one of the group's issues as #N, or it shares two words with the |
| 49 | 50 | * 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[] { | ||
| 120 | 121 | } |
| 121 | 122 | } |
| 122 | 123 | } |
| 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 | } | |
| 123 | 136 | const named = new Set(listBets(app.db, repo.id).filter((b) => b.state !== "cut" && b.state !== "done").map((b) => b.name)); |
| 124 | 137 | const lead = listCrewRoles(app.db, repo.id).find((c) => c.role === "builder")?.agent.user.id ?? null; |
| 125 | 138 | const made: Bet[] = []; |
| 126 | 139 | for (const [theme, issues] of [...themes].sort((a, b) => b[1].length - a[1].length || a[0].localeCompare(b[0]))) { |
| 127 | 140 | 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; | |
| 129 | 142 | named.add(name); |
| 130 | 143 | const journeys = [ |
| 131 | 144 | ...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)", () => { | ||
| 162 | 162 | expect(proposeBets(app, fresh(), NOW)).toEqual([]); |
| 163 | 163 | }); |
| 164 | 164 | |
| 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 | ||
| 165 | 179 | test("the same state gives the same proposals: names, journeys and scopes match across two repositories", async () => { |
| 166 | 180 | const first = await sameState("zz-same-1"); |
| 167 | 181 | const second = await sameState("zz-same-2"); |
new above
offline