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

Calm pass: issues, the lists and runs show rows a reader can scan, not notes #10415

Merged opened this in answer to

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

Merged

scout/forge:scout/10301-calm-pass-issues-the-lists-and-runs-show → main 6 commits fork 4 threads · 4 unresolved

  • Merged. scout/10301-calm-pass-issues-the-lists-and-runs-show was merged into main as 74de45d.

Review

Sign in to review this change.
14 files changed +85 −186
docs/design/areas/issues-list.md +9
@@ -24,3 +24,12 @@ The side column of an issue (`labelsCard`, `assigneeCard`, `issueTeamsCard`, `de
2424 `src/web/pages/thread.ts`) reads like the pull page's side: a small heading over its rows, `.card.bare`, no box and
2525 no hairline. Chips, forms and empty lines are unchanged; titles stay escaped. The pull page draws the same label and
2626 assignee cards, so it follows. No new class. `test/issue-side-bare.test.ts` holds the shape.
27
28 ## Calm: a row is a title, a state word and a time (#10415)
29
30 On the Issues, Pulls and Runs lists (`src/web/studio/lists.ts`) a row carries its title, its state word and one time.
31 Gone from the row, still on the page it opens: the Flow-first button (still on the issue page, in its header), a pull's pipeline
32 step and the tell it answers, a run's sha, wait reason and duration, who has an issue. A stopped pull, a failed run and a run paused on a limit keep one
33 clamped reason line; an imported issue keeps its origin tag. The issue page drops "nobody has it yet" (the state word says it). Empty states are one line
34 (`Nothing open.`; a set filter is named). First-view budgets fell: issue 76→72 words, issues list 32→30,
35 pulls list 31→24, runs 46→38. No new class.
docs/design/attention-budget.json +12 −12
@@ -49,48 +49,48 @@
4949 },
5050 "issue": {
5151 "390": {
52 "words": 76,
52 "words": 72,
5353 "blocks": 19,
5454 "colours": 2
5555 },
5656 "1280": {
57 "words": 76,
57 "words": 72,
5858 "blocks": 19,
5959 "colours": 2
6060 }
6161 },
6262 "issues list": {
6363 "390": {
64 "words": 32,
65 "blocks": 8,
64 "words": 30,
65 "blocks": 7,
6666 "colours": 2
6767 },
6868 "1280": {
69 "words": 32,
70 "blocks": 8,
69 "words": 30,
70 "blocks": 7,
7171 "colours": 2
7272 }
7373 },
7474 "pulls list": {
7575 "390": {
76 "words": 31,
77 "blocks": 7,
76 "words": 24,
77 "blocks": 6,
7878 "colours": 1
7979 },
8080 "1280": {
81 "words": 31,
82 "blocks": 7,
81 "words": 24,
82 "blocks": 6,
8383 "colours": 1
8484 }
8585 },
8686 "runs": {
8787 "390": {
88 "words": 46,
88 "words": 38,
8989 "blocks": 8,
9090 "colours": 1
9191 },
9292 "1280": {
93 "words": 46,
93 "words": 38,
9494 "blocks": 8,
9595 "colours": 1
9696 }
docs/features/attention.md +4 −4
@@ -25,10 +25,10 @@ reader sees on first view at 390 px and 1280 px: **words**, **text blocks** and
2525 | repository | 116 / 18 / 2 | 116 / 18 / 2 |
2626 | app | 116 / 18 / 2 | 116 / 18 / 2 |
2727 | pull | 133 / 22 / 4 | 133 / 22 / 4 |
28 | issue | 76 / 19 / 2 | 76 / 19 / 2 |
29 | issues list | 32 / 8 / 2 | 32 / 8 / 2 |
30 | pulls list | 31 / 7 / 1 | 31 / 7 / 1 |
31 | runs | 46 / 8 / 1 | 46 / 8 / 1 |
28 | issue | 72 / 19 / 2 | 72 / 19 / 2 |
29 | issues list | 30 / 7 / 2 | 30 / 7 / 2 |
30 | pulls list | 24 / 6 / 1 | 24 / 6 / 1 |
31 | runs | 38 / 8 / 1 | 38 / 8 / 1 |
3232 | run | 34 / 11 / 3 | 34 / 11 / 3 |
3333 | settings | 137 / 32 / 1 | 137 / 32 / 1 |
3434
src/kernel/why_now.ts deleted −37
@@ -1,37 +0,0 @@
1 /**
2 * Retired 2026-11-09: the direction no longer puts the pipeline first —
3 * order is set by the pillars and the captain's ranking — so the gate no
4 * longer asks (`scripts/gate.ts`). A finished, approved pull closed unmerged
5 * on this rule (#10317); nothing reads it now. Kept with its test until the
6 * next sweep of unused modules.
7 *
8 * The direction issue put the pipeline first (#8839), so a new feature
9 * said why it went ahead of it. A feature is a file a pull adds, at any
10 * depth, under an action, a page, an API route, an MCP tool or a terminal
11 * screen; the pull's commits must then say either `Pipeline: #N` (it is
12 * pipeline work) or `Why now: <reason>` (it goes ahead, and why). The check
13 * cannot read the pull description, so the answer lives in a commit
14 * message. It is a nudge, not a proof: it checks a reason was written, not
15 * that it is a good one.
16 */
17
18 import type { Change } from "./pin_edits.ts";
19
20 const FEATURE = /^src\/(actions|web\/pages|http\/api|http\/mcp\/tools|tui)\/.+\.ts$/;
21 const PIPELINE = /^Pipeline:\s*#\d+/im;
22 const WHY_NOW = /^Why now:\s*\S[^\n]{14,}/im;
23
24 /** The files a pull adds that make a new feature. */
25 export function featuresAdded(changes: readonly Change[]): string[] {
26 return changes.filter((c) => c.status.startsWith("A") && FEATURE.test(c.path)).map((c) => c.path);
27 }
28
29 /** One line per problem: none when no feature is added or a commit answers the direction. */
30 export function whyNowProblems(changes: readonly Change[], messages: readonly string[]): string[] {
31 const added = featuresAdded(changes);
32 if (added.length === 0) return [];
33 if (messages.some((m) => PIPELINE.test(m) || WHY_NOW.test(m))) return [];
34 return [
35 `rule "say why it goes ahead of the pipeline": this pull adds ${added.join(", ")} but no commit message has a "Why now: <reason>" line (15+ characters) or a "Pipeline: #N" line`,
36 ];
37 }
src/web/studio/lists.ts +23 −73
@@ -20,11 +20,8 @@ import {
2020 type Label,
2121 } from "../../core/issues.ts";
2222 import { listPulls, openPullsClosing } from "../../core/pulls.ts";
23 import { stepsOfStored, type PathStep } from "../../flow/path.ts";
24 import { canRead, canWrite, type Repo } from "../../core/repos.ts";
25 import { FLOW_LABEL } from "../../core/flow_label.ts";
26 import { flowButton } from "../flow_button.ts";
27 import { stepLink } from "./step_link.ts";
23 import { stepsOfStored } from "../../flow/path.ts";
24 import { canRead, type Repo } from "../../core/repos.ts";
2825 import { waitingCount } from "../../actions/reports.ts";
2926
3027 import { listRepoRuns } from "../../actions/runs.ts";
@@ -60,10 +57,9 @@ import {
6057 time,
6158 } from "../ui.ts";
6259 import type { Run, RunState } from "../../core/runs.ts";
63 import { actorsFor, durationText, issuesFor, runTitleFrom } from "../pages/runs.ts";
60 import { actorsFor, issuesFor, runTitleFrom } from "../pages/runs.ts";
6461 import { originGlyph } from "../pages/thread.ts";
6562 import { changeHref, PAGE_TITLES, pageTitle, tellHref } from "./nouns.ts";
66 import { tellLine, tellsAnswered } from "./branch_tell.ts";
6763 import { knowMergeClosings, tellWords } from "./tell.ts";
6864 import {
6965 pullWords,
@@ -447,18 +443,16 @@ export async function tellsList(ctx: Ctx): Promise<Response> {
447443 .map((issue, i) => ({ issue, info: words[i]! }))
448444 .filter((r) => shown(r.info.word, f));
449445
450 const mayLabel = canWrite(db, repo, ctx.auth.user);
451446 const list =
452447 rows.length === 0
453448 ? empty({
454449 icon: "issue",
455 title: "Nothing here",
456 note: emptyNote(
450 title: emptyNote(
457451 f.word,
458452 "No issue",
459453 record === undefined
460454 ? "Nothing open."
461 : "Nothing open. Tell the crew what you want and it shows up here.",
455 : "Nothing open. Tell the crew what you want.",
462456 ),
463457 })
464458 : listCard(
@@ -472,18 +466,7 @@ export async function tellsList(ctx: Ctx): Promise<Response> {
472466 meta: html`${wordText(info.word)}${when(
473467 issue.originAuthor,
474468 (login) => html`${originTag(login, originGlyph(repo))} · `,
475 )}${issue.assigneeName
476 ? html`${issue.assigneeName} · `
477 : null}${time(issue.updatedAt)}`,
478 ...(mayLabel && issue.state === "open"
479 ? {
480 after: flowButton(
481 `${base}/issues/${issue.number}/flow`,
482 (labels.get(issue.id) ?? []).some((l) => l.name.toLowerCase() === FLOW_LABEL),
483 `${ctx.url.pathname}${ctx.url.search}`,
484 ),
485 }
486 : {}),
469 )}${time(issue.updatedAt)}`,
487470 }),
488471 ),
489472 { extra: "bare" },
@@ -575,11 +558,6 @@ export async function tellsList(ctx: Ctx): Promise<Response> {
575558 return response;
576559 }
577560
578 /** The pull's step beside its row, as a sibling of the row's link: `after`. */
579 function stepAfter(step: PathStep | null | undefined): { after?: Html } {
580 return step == null ? {} : { after: stepLink(step) };
581 }
582
583561 /**
584562 * A pull's title and branch are the writer's words, so inside `[data-pulls]`
585563 * a row wraps them (a long word breaks anywhere) in place of the list's
@@ -620,8 +598,6 @@ export async function changesList(ctx: Ctx): Promise<Response> {
620598 lap("pulls");
621599 const words = await pullWords(wordCtx, fetched);
622600 lap("words");
623 const answered = tellsAnswered(db, repo.id, fetched);
624 lap("tells");
625601 const labels = labelsForIssues(db, fetched.map((pull) => pull.id));
626602 const repoLabels = listLabels(db, repo.id);
627603 lap("labels");
@@ -629,20 +605,22 @@ export async function changesList(ctx: Ctx): Promise<Response> {
629605 .map((pull, i) => ({ pull, info: words[i]! }))
630606 // An open pull is always listed, whatever its word: the tab counts them all.
631607 .filter((r) => f.word === undefined || shown(r.info.word, f));
632 const steps = stepsOfStored(db, repo, rows.map((r) => r.pull));
633 lap("steps");
608
609 // Only a stopped pull's row says why: the step's sentence, one clamped line.
610 const stopped = stepsOfStored(
611 db,
612 repo,
613 rows.filter((r) => r.info.word === "stopped").map((r) => r.pull),
614 );
634615
635616 const list =
636617 rows.length === 0
637618 ? empty({
638619 icon: "pull",
639 title: "Nothing here",
640 note: emptyNote(
620 title: emptyNote(
641621 f.word,
642622 "No pull request",
643 record === undefined
644 ? "Nothing open."
645 : "Nothing open. A pull request is the crew's proposed edit to your app; the crew opens them as it works.",
623 "Nothing open.",
646624 ),
647625 })
outdated

Dead code: record === undefined ? "Nothing open." : "Nothing open." has two identical branches. Pass the string. Same for the runs kind ternary below. Also the stopped pull's reason is gone from the row (principle 1).

648626 : listCard(
@@ -653,11 +631,7 @@ export async function changesList(ctx: Ctx): Promise<Response> {
653631 lead: wordDot(info.word, info.note),
654632 title: titled(pull.title, labels.get(pull.id)),
655633 titleAttr: pull.title,
656 meta: html`${wordText(info.word)}${pull.originAuthor
657 ? originTag(pull.originAuthor, originGlyph(repo))
658 : pull.authorName} ·
659 ${when(answered.get(pull.number), (t) => html`${tellLine(base, t)} · `)}${time(pull.updatedAt)}`,
660 ...stepAfter(steps.get(pull.id)),
634 meta: html`${wordText(info.word)}${when(info.word === "stopped" ? stopped.get(pull.id) : undefined, (step) => html`${noteLine(step.why)} · `)}${time(pull.updatedAt)}`,
661635 }),
662636 ),
663637 );
@@ -747,18 +721,6 @@ function runsHref(
747721 return text === "" ? `${base}/runs` : `${base}/runs?${text}`;
748722 }
749723
750 /** What a queued run waits on, in the words its row says it by: a builder
751 * for an agent run, a slot for a check, the deploy lane for a deploy; a run
752 * paused on a limit says it resumes later. */
753 export function whatRunWaitsOn(run: Run): string {
754 if (run.notBefore !== null) return "paused on a limit, resumes soon";
755 return run.kind === "agent"
756 ? "waiting for a builder"
757 : run.kind === "check"
758 ? "waiting for a check slot"
759 : "waiting for the deploy lane";
760 }
761
762724 /** Whether a run is doing something at this moment — the only runs under
763725 * Working. A deploy is not counted: the flow report's stages are builders,
764726 * reviewers and checks, and the tab's count is held to theirs. */
@@ -782,14 +744,6 @@ export function onRunsList(
782744 return inWordFilter(word, filter);
783745 }
784746
785 /** A check's state in the words its row names it by. */
786 const checkState = (run: Run): string =>
787 run.state === "ok"
788 ? "passed"
789 : run.state === "queued"
790 ? whatRunWaitsOn(run)
791 : run.state;
792
793747 const RUNS_KIND_WORDS: Record<RunsKind, string> = {
794748 agent: "Agent runs",
795749 deploy: "Deploys",
@@ -851,13 +805,10 @@ export async function runsListPage(ctx: Ctx, repo: Repo): Promise<Response> {
851805 rows.length === 0
852806 ? empty({
853807 icon: "bot",
854 title: "Nothing here",
855 note: emptyNote(
808 title: emptyNote(
856809 filter,
857810 "No run",
858 kind === undefined || kind === "agent"
859 ? "Nothing open. A run is one go the crew takes at a job; each one shows up here."
860 : `Nothing open. Every ${kind === "deploy" ? "deploy" : "check"} shows up here while it runs.`,
811 "Nothing open.",
861812 ),
862813 })
863814 : listCard(
@@ -875,12 +826,11 @@ export async function runsListPage(ctx: Ctx, repo: Repo): Promise<Response> {
875826 : icon(run.kind === "check" ? "check" : "rocket"),
876827 title: runTitleFrom(run, actors, issues),
877828 meta:
878 run.kind === "check"
879 ? html`${run.commitOid.slice(0, 7)} · ${checkState(run)}
880 ${when(durationText(run), (d) => html` · took ${d}`)} ·
881 ${time(run.finishedAt ?? run.createdAt)}`
882 : html`${run.kind === "agent" ? html`${who} · ` : null}${run.state === "queued" ? whatRunWaitsOn(run) : noteLine(info.note)}
883 · ${time(run.finishedAt ?? run.createdAt)}`,
829 run.state === "failed"
830 ? html`${noteLine(info.note)} · ${time(run.finishedAt ?? run.createdAt)}`
831 : run.state === "queued" && run.notBefore !== null
832 ? html`${noteLine("paused on a limit, resumes soon")} · ${time(run.createdAt)}`
833 : time(run.finishedAt ?? run.createdAt),
884834 trail: wordBadge(info.word),
885835 });
886836 }),
src/web/studio/tell.ts +1 −3
@@ -540,9 +540,7 @@ async function studioTell(
540540 })}
541541 has it</span
542542 >`
543 : !direction && wordInfo?.word === "told"
544 ? html`<span class="muted">nobody has it yet</span>`
545 : null}
543 : null}
546544 </div>
547545 </div>`;
548546
test/flow-first.test.ts +2 −1
@@ -109,7 +109,8 @@ describe("the flow label goes first", () => {
109109 const rw = createToken(app.db, alice.id, "rw", "rw").token;
110110 const cookie = { authorization: `Bearer ${rw}` };
111111 const list = await (await fetch(`${servers.url}/alice/svc/issues`, { headers: cookie })).text();
112 expect(list).toContain(`action="/alice/svc/issues/${target.number}/flow"`);
112 // The list's rows are title, word and time only (#10415); the button is on the issue page, in its header.
113 expect(list).not.toContain(`action="/alice/svc/issues/${target.number}/flow"`);
113114 const page = await (await fetch(`${servers.url}/alice/svc/issues/${target.number}`, { headers: cookie })).text();
114115 expect(page).toContain(`/alice/svc/issues/${target.number}/flow`);
115116 expect(page).toContain("Flow first");
test/pull-step-surfaces.test.ts +7 −4
@@ -93,8 +93,9 @@ test("the feed's post for a pull says its step, linked to /pipeline", async () =
9393 expect(await get("/feed")).toContain(expected());
9494 });
9595
96 test("the repository's pulls list says each pull's step", async () => {
97 expect(await get("/alice/site/pulls")).toContain(expected());
96 test("the repository's pulls list leaves the step to the pull (#10415)", async () => {
97 expect(await get("/alice/site/pulls")).not.toContain(expected());
98 expect(await get(`/alice/site/pulls/${pull.number}`)).toContain('data-path="review"');
9899 });
99100
100101 const openMore = async (r: Repo, by: User, name: string): Promise<Pull> => {
@@ -109,12 +110,14 @@ const openMore = async (r: Repo, by: User, name: string): Promise<Pull> => {
109110 return opened.value;
110111 };
111112
112 test("a long list says every pull's step and stays quick", async () => {
113 test("a long list is scannable rows with no step beside them, and stays quick", async () => {
113114 for (let i = 0; i < 24; i++) await openMore(repo, alice, `long${i}`);
114115 const started = performance.now();
115116 const html = await get("/alice/site/pulls");
116117 const took = performance.now() - started;
117 expect(html.match(/data-step="review"/g)?.length).toBeGreaterThanOrEqual(25);
118 // The calm pass (#10415): a row is a title, a state word and a time; the step is on the pull.
119 expect(html.match(/class="row-title"/g)?.length).toBeGreaterThanOrEqual(25);
120 expect(html).not.toContain('data-step=');
118121 expect(took).toBeLessThan(500);
119122 });
120123
test/pulls-list-timing.test.ts +2 −2
@@ -38,10 +38,10 @@ function names(res: Response): string[] {
3838 return requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
3939 }
4040
41 test("a repository's pulls list names session, repo, pulls, words, tells, labels, steps and render in order", async () => {
41 test("a repository's pulls list names session, repo, pulls, words, labels and render in order", async () => {
4242 const res = await fetch(`${servers.url}/alice/notes/pulls`, { headers: { authorization: `Bearer ${bearer}` } });
4343 expect(res.status).toBe(200);
4444 await res.text();
4545 expect(res.headers.get("x-content-type-options")).toBe("nosniff");
46 expect(names(res)).toEqual(["app", "session", "repo", "pulls", "words", "tells", "labels", "steps", "render"]);
46 expect(names(res)).toEqual(["app", "session", "repo", "pulls", "words", "labels", "render"]);
4747 });
test/runs-working-tab.test.ts +5 −9
@@ -1,7 +1,7 @@
11 /**
22 * The Runs page's Working tab lists only runs doing something now, and its
33 * count is the flow report's building + review + checking on the same data;
4 * queued runs say what they wait on, finished ones are not under Working (#7719).
4 * queued runs are listed by state, finished ones are not under Working (#7719).
55 */
66
77 import { afterAll, beforeAll, expect, test } from "bun:test";
@@ -89,14 +89,10 @@ test("Working counts what the flow report counts as active, and nothing finished
8989 expect(body).not.toContain("waiting for");
9090 });
9191
92 test("a queued run says what it waits on", async () => {
93 const body = await page("");
94 expect(body).toContain("waiting for a builder");
95 expect(body).toContain("waiting for a check slot");
96 });
97
98 test("a paused run and a queued deploy say what they wait on", async () => {
92 test("a queued run is listed by its state word; only a paused one says why", async () => {
9993 const body = await page("?state=queued");
94 expect(rowCount(body)).toBeGreaterThan(0);
95 expect(body).toContain("queued");
10096 expect(body).toContain("paused on a limit, resumes soon");
101 expect(body).toContain("waiting for the deploy lane");
97 expect(body).not.toContain("waiting for");
10298 });
test/studio-changes.test.ts +17 −9
@@ -340,8 +340,11 @@ describe("the studio, continued", () => {
340340 const all = await (await get("/alice/notes/runs")).text();
341341 for (const link of [agent, check, deploy]) expect(all).toContain(link);
342342 expect(all).toContain("Check of abcdef1");
343 expect(all).toContain("abcdef1 · passed");
344 expect(all).toContain("took ");
343 // A row is title, state word and time (#10415): no sha, no duration beside it.
344 expect(all).not.toContain("abcdef1 · passed");
345 expect(all).toContain("<time");
346 const checkId = /runs\/([^"]+)"/.exec(check)![1];
347 expect(await (await get(`/alice/notes/runs/${checkId}`)).text()).toContain("abcdef1");
345348
346349 const only = async (kind: string) =>
347350 (await get(`/alice/notes/runs?kind=${kind}`)).text();
@@ -683,8 +686,8 @@ describe("the studio, continued", () => {
683686 const tellHtml = await viaTell.text();
684687 expect(tellHtml).toContain("Door test: one tell");
685688 expect(tellHtml).toContain('data-word="told"');
686 // Still waiting, nobody assigned: the one case the line is true (#214).
687 expect(tellHtml).toContain("nobody has it yet");
689 // Still waiting: the state word says so; the page adds no second line (#10415).
690 expect(tellHtml).not.toContain("nobody has it yet");
688691 // The reply, the close and every Details form post to /issues/:number.
689692 expect(tellHtml).toContain(`action="/alice/notes/issues/${n}/comments"`);
690693 expect(tellHtml).toContain(`action="/alice/notes/issues/${n}/assignee"`);
@@ -1319,14 +1322,19 @@ describe("the studio, continued", () => {
13191322 const at = list.indexOf(title);
13201323 return list.slice(at, list.indexOf("</a>", at + 400) + 4);
13211324 };
1322 expect(rowOf("Answers the tell")).toContain(`href="/alice/notes/issues/${n}"`);
1323 expect(rowOf("Answers the tell")).not.toContain("already done");
1324 expect(rowOf("Answers nothing")).not.toContain("for issue");
1325 expect(rowOf("A person's branch")).not.toContain("for issue");
1325 // The calm pass (#10415): a row is title, word and time; the tell it answers is on the pull.
outdated

This negative passes without the change. Assert positively that the pull page carries the tell/step the list dropped.

1326 for (const title of ["Answers the tell", "Answers nothing", "A person's branch"]) {
1327 expect(rowOf(title.replace("'", "&#39;"))).toContain("<time");
1328 expect(rowOf(title.replace("'", "&#39;"))).not.toContain("for issue");
1329 }
1330 expect(rowOf("Answers the tell")).toContain('data-word=');
1331 expect(
1332 await (await get(`/alice/notes/pulls/${answering.number}`)).text(),
1333 ).toContain(`href="/alice/notes/issues/${n}"`);
13261334
13271335 setIssueState(app, alice, repo, told.value, "closed");
13281336 const closedList = await (await get("/alice/notes/pulls")).text();
1329 expect(closedList).toContain("its issue is already done");
1337 expect(closedList).not.toContain("its issue is already done");
13301338 const page = await (
13311339 await get(`/alice/notes/pulls/${answering.number}`)
13321340 ).text();
test/studio-search.test.ts +2 −3
@@ -1248,11 +1248,10 @@ describe("the studio's empty lists", () => {
12481248 await setup("blank");
12491249 for (const [path, words] of [
12501250 ["issues", "Tell the crew what you want"],
1251 ["pulls", "A pull request is the crew"],
1252 ["runs", "A run is one go"],
1251 ["pulls", "Nothing open."],
1252 ["runs", "Nothing open."],
12531253 ] as const) {
12541254 const page = await get(`/alice/blank/${path}`);
1255 expect(page).toContain("Nothing here");
12561255 expect(page).toContain(words);
12571256 expect(page).not.toContain("this filter");
12581257 }
test/web-ui.test.ts +1 −1
@@ -743,7 +743,7 @@ describe("web ui", () => {
743743 await fetch(`${base}/mel/proj/issues?q=nothing-matches-this&state=all`)
744744 ).text();
745745 expect(miss).not.toContain("needs a label");
746 expect(miss).toContain("Nothing here");
746 expect(miss).toContain("Nothing open.");
747747 expect(miss).toContain("nothing-matches-this in the title ✕");
748748
749749 // A LIKE wildcard typed into the box is a literal character, not a glob.
test/why-now.test.ts deleted −28
@@ -1,28 +0,0 @@
1 /** Retired 2026-11-09 with the pipeline-first direction: the gate no longer asks (`scripts/gate.ts`); the pure rule keeps its tests until the module goes. */
2 import { expect, test } from "bun:test";
3 import { featuresAdded, whyNowProblems } from "../src/kernel/why_now.ts";
4
5 const add = (path: string) => ({ status: "A", path });
6
7 test("a new action, page or route is a feature; edits, tests and other files are not", () => {
8 expect(featuresAdded([add("src/actions/x.ts"), add("src/web/pages/y.ts"), add("src/http/api/z.ts")])).toHaveLength(3);
9 expect(featuresAdded([add("src/http/mcp/tools/x.ts"), add("src/tui/app/screen.ts"), add("src/http/api/v1/y.ts")])).toHaveLength(3);
10 expect(featuresAdded([{ status: "M", path: "src/actions/x.ts" }, add("test/x.test.ts"), add("src/core/x.ts")])).toEqual([]);
11 });
12
13 test("a feature with no answer to the direction is refused, naming the rule and the file", () => {
14 const [p] = whyNowProblems([add("src/actions/x.ts")], ["Add x\n\nIt does x."]);
15 expect(p).toContain("say why it goes ahead of the pipeline");
16 expect(p).toContain("src/actions/x.ts");
17 });
18
19 test("a Why now line or a Pipeline line answers it; a thin reason does not", () => {
20 const changes = [add("src/actions/x.ts")];
21 expect(whyNowProblems(changes, ["Add x\n\nWhy now: the owner asked for it in #1234 this week"])).toEqual([]);
22 expect(whyNowProblems(changes, ["Add x\n\nPipeline: #6534"])).toEqual([]);
23 expect(whyNowProblems(changes, ["Add x\n\nWhy now: soon"])).toHaveLength(1);
24 });
25
26 test("a pull that adds no feature is never asked", () => {
27 expect(whyNowProblems([add("test/a.test.ts")], ["Fix a"])).toEqual([]);
28 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs