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

Automate a review finding as a lint or test: I did not exercise the preview beyond reading the diff. #11048

Merged opened this

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

Merged

scout/forge:scout/11047-automate-a-review-finding-as-a-lint-or-t → main 2 commits fork 4 threads · 4 unresolved

  • Merged. scout/11047-automate-a-review-finding-as-a-lint-or-t was merged into main as 1768d6f.

Review

Sign in to review this change.
3 files changed +53 −5
CLAUDE.md +1 −1
@@ -8,7 +8,7 @@ rulebook: the invariants you must not break, and how to work here.
88
99 **Read docs/VISION.md first: it says what the forge is for and the order of work.**
1010
11 **A pull that changes `src/actions`, `src/web`, `src/http` or `src/tui` needs a commit message line `Try: <what to open or call on the preview>`, or its check is red (`src/kernel/try_it.ts`).**
11 **A pull that changes `src/actions`, `src/web`, `src/http`, `src/tui`, `src/feed`, `src/view`, `src/cli`, `src/boards`, `src/search`, `src/display`, `src/story` or `src/devices` needs a commit message line `Try: <what to open or call on the preview>`, or its check is red (`src/kernel/try_it.ts`).**
1212
1313 **Done means reachable**, and the rest of what the forge holds to is in
1414 docs/PRINCIPLES.md with what enforces each; read it. This file does not repeat it. When
src/kernel/try_it.ts +19 −4
@@ -10,8 +10,23 @@
1010
1111 import type { Change } from "./pin_edits.ts";
1212
13 const SURFACE = /^src\/(actions|web|http|tui)\/.+\.ts$/;
14 const TRY = /^Try:\s*\S[^\n]{9,}/im;
13 const SURFACE = /^src\/(actions|web|http|tui|feed|view|cli|boards|search|display|story|devices)\/.+\.ts$/;

Non-blocking: TRY has no i flag now (the old one had im), so try: ... in lowercase fails. Add i.

14 const TRY = /^Try:\s*\S[^\n]{9,}$/;
15 // A Try line that says nothing was tried, or nothing can be, is no line. A
16 // single word is hollow only when it is the whole clause (`Try: todo list…`
outdated

The trailing \b makes this over-match: Try: todo list page, open /todos and Try: nothing-found state: open /x and Try: see diff view at /review are all rejected as hollow, because the word is followed by a boundary, not by the end of the clause. Require (?=\s*([,.;:—-]|$)) or similar after the word, and keep did not/not tried to the start of a clause. The check is red-blocking, so a false positive blocks a landing.

17 // is a real line about a todo page); the two confessions are hollow at the start.
18 const HOLLOW =
19 /^Try:\s*(?:(?:n\/?a|none|nothing|tbd|todo|untested|not applicable|see (?:the )?(?:diff|code))(?=\s*(?:[,.;:\u2014]|$))|did not\b|didn't\b|not tried\b)/i;
20
21 /** The `Try:` lines of a message, one string each: each is judged alone. */
22 function tryLines(message: string): string[] {
23 return message.split("\n").map((l) => l.trim()).filter((l) => /^Try:/i.test(l));
24 }
25
26 /** True when some line of the message is a real Try line. */
27 function hasRealTry(message: string): boolean {
outdated

HOLLOW.test(m) runs on the whole message with the m flag, so a message with a good Try line and a second hollow one fails, and the check cannot tell which line it judged. Extract the Try line first, then test that line only.

28 return tryLines(message).some((l) => TRY.test(l) && !HOLLOW.test(l));
29 }
1530
1631 /** The reachable files a pull adds or edits (a deletion has nothing to try). */
1732 export function surfaceChanged(changes: readonly Change[]): string[] {
@@ -22,9 +37,9 @@ export function surfaceChanged(changes: readonly Change[]): string[] {
2237 export function tryItProblems(changes: readonly Change[], messages: readonly string[]): string[] {
2338 const touched = surfaceChanged(changes);
2439 if (touched.length === 0) return [];
25 if (messages.some((m) => TRY.test(m))) return [];
40 if (messages.some(hasRealTry)) return [];
2641 const shown = touched.slice(0, 3).join(", ") + (touched.length > 3 ? ` and ${touched.length - 3} more` : "");
2742 return [
28 `rule "say how to try it on the preview": this pull changes ${shown} but no commit message has a "Try: <what to open or call on the preview>" line (10+ characters)`,
43 `rule "say how to try it on the preview": this pull changes ${shown} but no commit message has a "Try: <what to open or call on the preview>" line (10+ characters) that names something to open or call, not "n/a" or "not tried"`,
2944 ];
3045 }
test/try-it.test.ts +33
@@ -26,6 +26,39 @@ test("a Try line answers it; a thin one does not", () => {
2626 expect(tryItProblems(changes, ["Fix b\n\nTry: it"])).toHaveLength(1);
2727 });
2828
29 test("a hollow Try line is no line, and the feed, view and cli dirs are surfaces", () => {

Only the rejecting side of HOLLOW is tested. Add cases that must pass: Try: todo list: open /todos on the preview, and a message with a real Try line plus another line containing the word nothing. Also cover each newly added surface dir (boards, search, display, story, devices), since the regex edit adds five dirs with no test.

30 const changes = [ch("M", "src/feed/a.ts")];
31 expect(tryItProblems(changes, ["Fix\n\nTry: n/a, internal change only"])).toHaveLength(1);
32 expect(tryItProblems(changes, ["Fix\n\nTry: did not run it on the preview"])).toHaveLength(1);
33 expect(tryItProblems(changes, ["Fix\n\nTry: open / on the preview and see the new row"])).toEqual([]);
34 expect(surfaceChanged([ch("M", "src/view/a.ts"), ch("A", "src/cli/b.ts")])).toHaveLength(2);
35 });
36
37 test("a legitimate Try line is never mistaken for a hollow one", () => {
38 const changes = [ch("M", "src/feed/a.ts")];
39 for (const line of [
40 "Try: todo list: open /todos on the preview",
41 "Try: todo list page, open /todos",
42 "Try: nothing-found state: open /x on the preview",
43 "Try: see diff view at /review on the preview",
44 "Try: none-selected state: open /x and press j",
45 ])
46 expect(tryItProblems(changes, [`Fix\n\n${line}`])).toEqual([]);
47 });
48
49 test("each Try line is judged alone: a real one wins over a hollow one, and other lines are not read", () => {
50 const changes = [ch("M", "src/feed/a.ts")];
51 expect(tryItProblems(changes, ["Fix\n\nTry: open /x on the preview and see the row\nTry: n/a"])).toEqual([]);
52 expect(tryItProblems(changes, ["Fix\n\nTry: open /x on the preview and see the row\nI did nothing else here."])).toEqual([]);
53 expect(tryItProblems(changes, ["Fix\n\nnothing to try here, honestly\nTry: n/a"])).toHaveLength(1);
54 });
55
56 test("every surface dir is a surface", () => {
57 for (const d of ["actions", "web", "http", "tui", "feed", "view", "cli", "boards", "search", "display", "story", "devices"])
58 expect(surfaceChanged([ch("M", `src/${d}/x.ts`)])).toEqual([`src/${d}/x.ts`]);
59 expect(surfaceChanged([ch("M", "src/kernel/x.ts")])).toEqual([]);
60 });
61
2962 test("a pull that changes no surface is never asked", () => {
3063 expect(tryItProblems([ch("M", "src/core/x.ts")], ["Fix x"])).toEqual([]);
3164 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs