Public: anyone can read this; the crew can change it.
Remove isSettingFinding, closeSettingFindings and the sentinel_kept table once #2505 is closed #3664
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/3488-remove-issettingfinding-closesettingfind → main 1 commit fork
9 files
changed +10
−118
README.md +4 −6
| @@ -1315,12 +1315,10 @@ until the value is back at the default; nothing else reminds anyone. The | ||
| 1315 | 1315 | JSON API and MCP carry the same fact: a setting away from its default has a |
| 1316 | 1316 | `changed` field (`at`, `by`) in the capacity listing (`listCapacity`). |
| 1317 | 1317 | |
| 1318 | A `setting:<key>` finding filed before that is closed on the next pass with a | |
| 1319 | comment pointing at the capacity page (`closeSettingFindings` in | |
| 1320 | `crew/setting_finding.ts`). Until it closes it is never a crew run: not | |
| 1321 | assigned, and left out of the crew's ready list (`isSettingFinding`). It raises | |
| 1322 | no ask; the capacity page's Put back is the one answer. Both helpers go once no | |
| 1323 | open setting finding is left, with the unused `sentinel_kept` table. | |
| 1318 | The `setting:<key>` findings filed before that are all closed; none raises an | |
| 1319 | ask, and the capacity page's Put back is the one answer. The `sentinel_kept` | |
| 1320 | table they once used is unread; a migration drops it once the additive rule | |
| 1321 | (`src/core/releases.json`) allows a drop. | |
| 1324 | 1322 | |
| 1325 | 1323 | - **Board:** a site admin's forge strip shows the open findings and the last |
| 1326 | 1324 | hunt; a guest or member sees nothing. |
src/crew/held.ts −4
| @@ -19,7 +19,6 @@ | ||
| 19 | 19 | * - `run_cap`: it has used its runs for the day (`guard.ts`). |
| 20 | 20 | * - `parked_twice`: the crew parked it twice; it is the planner's (`parks.ts`). |
| 21 | 21 | * - `red_fix`: a fix for a red main already covers it (`red_fix.ts`). |
| 22 | * - `setting`: a setting's finding, a person's to answer (`setting_finding.ts`). | |
| 23 | 22 | * |
| 24 | 23 | * The direction issue is the planner's standing issue and never a |
| 25 | 24 | * builder's; it is left out, not counted. |
| @@ -40,7 +39,6 @@ import { capReached, runCapOf } from "./guard.ts"; | ||
| 40 | 39 | import { interruptionOf } from "./interrupted.ts"; |
| 41 | 40 | import { PARK_STRIKES, parkedCount } from "./parks.ts"; |
| 42 | 41 | import { coveredByRedFix } from "./red_fix.ts"; |
| 43 | import { isSettingFinding } from "./setting_finding.ts"; | |
| 44 | 42 | |
| 45 | 43 | export const HOLDS = [ |
| 46 | 44 | "running", |
| @@ -52,7 +50,6 @@ export const HOLDS = [ | ||
| 52 | 50 | "run_cap", |
| 53 | 51 | "parked_twice", |
| 54 | 52 | "red_fix", |
| 55 | "setting", | |
| 56 | 53 | ] as const; |
| 57 | 54 | |
| 58 | 55 | export type HoldName = (typeof HOLDS)[number]; |
| @@ -131,7 +128,6 @@ export function heldBy( | ||
| 131 | 128 | if (atRunCap(app, repo, issue)) return "run_cap"; |
| 132 | 129 | if (parkedCount(app.db, repo, issue.id) >= PARK_STRIKES) return "parked_twice"; |
| 133 | 130 | if (coveredByRedFix(app.db, repo.id, issue)) return "red_fix"; |
| 134 | if (isSettingFinding(app.db, issue.id)) return "setting"; | |
| 135 | 131 | return null; |
| 136 | 132 | }; |
| 137 | 133 | const by = Object.fromEntries(HOLDS.map((h) => [h, [] as Issue[]])) as Record< |
src/crew/sentinel.ts +1 −4
| @@ -29,7 +29,6 @@ import { spans } from "../kernel/degraded_history.ts"; | ||
| 29 | 29 | import { QUEUE_WAIT_MAX_MS } from "../kernel/vitals.ts"; |
| 30 | 30 | import { STALE_AFTER_MS } from "../runs/lease.ts"; |
| 31 | 31 | import { isOutageSummary } from "./network.ts"; |
| 32 | import { closeSettingFindings } from "./setting_finding.ts"; | |
| 33 | 32 | |
| 34 | 33 | const log = createLog("sentinel"); |
| 35 | 34 | |
| @@ -712,10 +711,8 @@ export function hunt( | ||
| 712 | 711 | } |
| 713 | 712 | } |
| 714 | 713 | } |
| 715 | if (repo !== undefined && actor !== undefined) { | |
| 714 | if (repo !== undefined && actor !== undefined) | |
| 716 | 715 | closeHeld(app, repo, actor, new Set(findings.map((f) => f.key)), now); |
| 717 | closeSettingFindings(app, repo, actor); | |
| 718 | } | |
| 719 | 716 | // A hunt with nowhere to file is recorded as such, never as a quiet one. |
| 720 | 717 | const unfiled = repo === undefined || actor === undefined; |
| 721 | 718 | app.db |
src/crew/setting_finding.ts deleted −56
| @@ -1,56 +0,0 @@ | ||
| 1 | /** | |
| 2 | * A setting the sentinel found away from its default is the owner's ask | |
| 3 | * (#2029), never a crew run. The finding is filed by a crew member, so the | |
| 4 | * autonomy clock would otherwise count it ready and hand it to a builder. | |
| 5 | */ | |
| 6 | ||
| 7 | import type { Database } from "bun:sqlite"; | |
| 8 | import type { App } from "../app.ts"; | |
| 9 | import { addComment, setIssueState } from "../actions/issues.ts"; | |
| 10 | import { getIssueById } from "../core/issues.ts"; | |
| 11 | import type { Repo } from "../core/repos.ts"; | |
| 12 | import type { User } from "../core/users.ts"; | |
| 13 | import { createLog } from "../kernel/log.ts"; | |
| 14 | ||
| 15 | const log = createLog("sentinel"); | |
| 16 | ||
| 17 | /** Whether this issue is the open finding of a `setting:<key>` the sentinel reported. */ | |
| 18 | export function isSettingFinding(db: Database, issueId: string): boolean { | |
| 19 | return ( | |
| 20 | db | |
| 21 | .query<{ n: number }, [string]>( | |
| 22 | "SELECT 1 AS n FROM sentinel_finding WHERE issue_id = ?1 AND key LIKE 'setting:%'", | |
| 23 | ) | |
| 24 | .get(issueId) !== null | |
| 25 | ); | |
| 26 | } | |
| 27 | ||
| 28 | /** | |
| 29 | * A setting away from its default is a line on the capacity page, not an | |
| 30 | * issue (#3269). Findings filed before that are closed, with a pointer. | |
| 31 | */ | |
| 32 | export function closeSettingFindings(app: App, repo: Repo, actor: User): void { | |
| 33 | const open = app.db | |
| 34 | .query<{ issue_id: string }, []>( | |
| 35 | `SELECT DISTINCT t.issue_id FROM sentinel_finding t JOIN issue i ON i.id = t.issue_id | |
| 36 | WHERE i.state = 'open' AND t.key LIKE 'setting:%'`, | |
| 37 | ) | |
| 38 | .all(); | |
| 39 | for (const t of open) { | |
| 40 | try { | |
| 41 | const issue = getIssueById(app.db, t.issue_id); | |
| 42 | if (issue === undefined || issue.repoId !== repo.id) continue; | |
| 43 | const said = addComment( | |
| 44 | app, | |
| 45 | actor, | |
| 46 | repo, | |
| 47 | issue, | |
| 48 | "A setting away from its default is now one line beside it on the capacity page (/settings/capacity), with Put back; the sentinel files no issue for it. Closing.", | |
| 49 | ); | |
| 50 | if (!said.ok) continue; | |
| 51 | setIssueState(app, actor, repo, issue, "closed", { reason: "done" }); | |
| 52 | } catch (error) { | |
| 53 | log.error("sentinel could not close a setting finding", { issue: t.issue_id, error: String(error) }); | |
| 54 | } | |
| 55 | } | |
| 56 | } |
src/web/pages/boards-held.ts +1 −1
| @@ -32,7 +32,7 @@ export function heldCard(rows: readonly HeldRow[]): Html { | ||
| 32 | 32 | <p class="muted small"> |
| 33 | 33 | Each ready issue a builder could start, or the first filter that holds |
| 34 | 34 | it: a run on it, an open pull, the planner, stalled, its run cap, |
| 35 | parked twice, a red-main fix, a setting's finding. When nothing can | |
| 35 | parked twice, a red-main fix. When nothing can | |
| 36 | 36 | start and builders are free, the planner is asked to re-plan what is |
| 37 | 37 | stalled or capped. |
| 38 | 38 | </p> |
test/asks.test.ts +2 −2
| @@ -276,8 +276,8 @@ describe("the asks page", () => { | ||
| 276 | 276 | expect(card?.short).toContain("its tests passed"); |
| 277 | 277 | }); |
| 278 | 278 | describe("a legacy setting finding (#3314)", () => { |
| 279 | // The sentinel files no setting now (#3269) and closes the ones it filed | |
| 280 | // before; until it does, an open one asks nobody and is answered nowhere. | |
| 279 | // The sentinel files no setting now (#3269); one filed before asks | |
| 280 | // nobody and is answered nowhere. | |
| 281 | 281 | test("raises no ask, and /asks/setting is gone", async () => { |
| 282 | 282 | const alice0 = getUserByName(app.db, "alice")!; |
| 283 | 283 | setCapacity(app.db, "checks.concurrency", "4", alice0.id); |
test/crew-direction-autonomy.test.ts −19
| @@ -169,25 +169,6 @@ afterAll(async () => { | ||
| 169 | 169 | await rm(dataDir, { recursive: true, force: true }); |
| 170 | 170 | }); |
| 171 | 171 | |
| 172 | describe("a setting finding on an autonomous repository (#2029)", () => { | |
| 173 | test("is the owner's ask: the autonomy clock never assigns it and no run starts", async () => { | |
| 174 | const repo = await autonomous(); | |
| 175 | const finding = ok(openIssue(app, alice, repo, { title: "Sentinel: Setting cells.egress is away from its default" })); | |
| 176 | app.db | |
| 177 | .query("INSERT INTO sentinel_finding (key, issue_id, evidence, first_seen, last_seen) VALUES ('setting:cells.egress', ?1, '', ?2, ?2)") | |
| 178 | .run(finding.id, new Date().toISOString()); | |
| 179 | const other = ok(openIssue(app, alice, repo, { title: "Round the corners" })); | |
| 180 | const before = listEvents(app.db, { kind: "issue.assigned" }).filter((e) => e.payload["issue"] === finding.id).length; | |
| 181 | for (let tick = 0; tick < 3; tick++) await driveRepo(app, fresh(repo)); | |
| 182 | const after = getIssue(app.db, repo.id, finding.number)!; | |
| 183 | expect(after.assigneeId).toBeNull(); | |
| 184 | expect(runsOn(repo, after)).toEqual([]); | |
| 185 | expect(listEvents(app.db, { kind: "issue.assigned" }).filter((e) => e.payload["issue"] === finding.id).length).toBe(before); | |
| 186 | // the crew still works what is not an ask | |
| 187 | expect(getIssue(app.db, repo.id, other.number)!.assigneeId).not.toBeNull(); | |
| 188 | }); | |
| 189 | }); | |
| 190 | ||
| 191 | 172 | describe("a reader's issue on an autonomous public repository", () => { |
| 192 | 173 | test("is never assigned, queued or ready, and the map says why", async () => { |
| 193 | 174 | const repo = await autonomous(); |
test/crew-held.test.ts +1 −7
| @@ -179,11 +179,6 @@ describe("the tick says why each ready issue cannot start", () => { | ||
| 179 | 179 | .query("INSERT INTO sentinel_finding (key, issue_id, evidence, first_seen, last_seen) VALUES (?1, ?2, '', 'x', 'x')") |
| 180 | 180 | .run(`degraded:${keyPart(`red:${repo.id}`)}`, fix.id); |
| 181 | 181 | |
| 182 | const finding = issue(repo, "A setting's finding"); | |
| 183 | app.db | |
| 184 | .query("INSERT INTO sentinel_finding (key, issue_id, evidence, first_seen, last_seen) VALUES ('setting:x', ?1, '', 'x', 'x')") | |
| 185 | .run(finding.id); | |
| 186 | ||
| 187 | 182 | const held = heldNow(app, getRepoById(app.db, repo.id)!); |
| 188 | 183 | const named = (h: keyof typeof held.by) => held.by[h].map((i) => i.number); |
| 189 | 184 | expect(held.startable.map((r) => r.issue.number).sort()).toEqual([free.number, fix.number].sort()); |
| @@ -195,10 +190,9 @@ describe("the tick says why each ready issue cannot start", () => { | ||
| 195 | 190 | expect(named("run_cap")).toEqual([capped.number]); |
| 196 | 191 | expect(named("parked_twice")).toEqual([parked.number]); |
| 197 | 192 | expect(named("red_fix")).toEqual([quoting.number]); |
| 198 | expect(named("setting")).toEqual([finding.number]); | |
| 199 | 193 | expect(heldCounts(held).open_pull).toBe(1); |
| 200 | 194 | expect(heldWords(held)).toBe( |
| 201 | "running=1 open_pull=1 planner=1 done_on_main=1 stalled=1 run_cap=1 parked_twice=1 red_fix=1 setting=1", | |
| 195 | "running=1 open_pull=1 planner=1 done_on_main=1 stalled=1 run_cap=1 parked_twice=1 red_fix=1", | |
| 202 | 196 | ); |
| 203 | 197 | // Something can start: the planner is not told the crew is stuck. |
| 204 | 198 | expect(stuckOf(held)).toBeUndefined(); |
test/sentinel.test.ts +1 −19
| @@ -20,7 +20,6 @@ import { listEvents } from "../src/core/events.ts"; | ||
| 20 | 20 | import { getRepoById, type Repo } from "../src/core/repos.ts"; |
| 21 | 21 | import { createRun } from "../src/core/runs.ts"; |
| 22 | 22 | import { createUser, type User } from "../src/core/users.ts"; |
| 23 | import { isSettingFinding } from "../src/crew/setting_finding.ts"; | |
| 24 | 23 | import { recordCap } from "../src/crew/guard.ts"; |
| 25 | 24 | import { |
| 26 | 25 | budgetBreaches, |
| @@ -490,18 +489,6 @@ describe("the signals inside the hour", () => { | ||
| 490 | 489 | setCapacity(app.db, "cells.egress", "enforce", alice.id); |
| 491 | 490 | }); |
| 492 | 491 | |
| 493 | test("an open setting finding from before closes on the next pass, with a pointer to the capacity page (#3269)", () => { | |
| 494 | const old = createIssue(app.db, { repoId: repo.id, authorId: alice.id, title: "Sentinel: Setting checks.concurrency has been away from its default for 9 days" }); | |
| 495 | if (!old.ok) throw new Error(String(old.error)); | |
| 496 | app.db.query("INSERT INTO sentinel_finding (key, issue_id, evidence, first_seen, last_seen) VALUES ('setting:checks.concurrency', ?1, '', 'a', 'a')").run(old.value.id); | |
| 497 | hunt(app, "alice/forge", Date.now()); | |
| 498 | const row = app.db.query<{ state: string; close_reason: string }, [string]>("SELECT state, close_reason FROM issue WHERE id = ?1").get(old.value.id)!; | |
| 499 | expect(row).toEqual({ state: "closed", close_reason: "done" }); | |
| 500 | expect(listComments(app.db, old.value.id).some((c) => c.body.includes("/settings/capacity"))).toBe(true); | |
| 501 | // closed ones are still known as setting findings | |
| 502 | expect(isSettingFinding(app.db, old.value.id)).toBe(true); | |
| 503 | }); | |
| 504 | ||
| 505 | 492 | test("a newly held slot hunts too", () => { |
| 506 | 493 | const run = createRun(app.db, { repoId: repo.id, kind: "deploy" }); |
| 507 | 494 | const l = lastHunt(); |
| @@ -782,11 +769,7 @@ describe("a degraded finding whose cause has stayed closed", () => { | ||
| 782 | 769 | await rm(hdir, { recursive: true, force: true }); |
| 783 | 770 | }); |
| 784 | 771 | |
| 785 | test("23 hours closed stays open; a person's comment keeps theirs at 25, and a setting finding closes", () => { | |
| 786 | // A setting finding, tracked as the sentinel would. | |
| 787 | const s = createIssue(happ.db, { repoId: hrepo.id, authorId: halice.id, title: "Sentinel: Setting x" }); | |
| 788 | if (!s.ok) throw new Error(String(s.error)); | |
| 789 | happ.db.query("INSERT INTO sentinel_finding (key, issue_id, evidence, first_seen, last_seen) VALUES ('setting:x', ?1, '', 'a', 'a')").run(s.value.id); | |
| 772 | test("23 hours closed stays open; a person's comment keeps theirs at 25", () => { | |
| 790 | 773 | const disk = happ.db.query<{ id: string }, []>("SELECT id FROM issue WHERE title LIKE '%disk%'").get()!; |
| 791 | 774 | addComment(happ.db, { issueId: disk.id, authorId: halice.id, body: "looking" }); |
| 792 | 775 | |
| @@ -801,7 +784,6 @@ describe("a degraded finding whose cause has stayed closed", () => { | ||
| 801 | 784 | expect(said.length).toBe(1); |
| 802 | 785 | expect(said[0]!.body).toContain(new Date(T0).toISOString()); |
| 803 | 786 | expect(state("disk")).toBe("open"); |
| 804 | expect(state("Setting x")).toBe("closed"); | |
| 805 | 787 | }); |
| 806 | 788 | |
| 807 | 789 | test("a finding that comes back is a new issue", () => { |
new above
offline