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

The planner's refusals live in one module, read by dependencies, plan and limb replan #5632

Merged opened this

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

Merged

mason/forge:mason/5595-the-planner-s-refusals-live-in-one-modul → main 1 commit fork

  • Merged. mason/5595-the-planner-s-refusals-live-in-one-modul was merged into main as 7d09241.

Review

Sign in to review this change.
7 files changed +56 −5
src/actions/dependencies.ts +2 −1
@@ -18,11 +18,12 @@ import { canRead, canWrite, type Repo } from "../core/repos.ts";
1818 import type { User } from "../core/users.ts";
1919 import { err, ok, type Result } from "../kernel/result.ts";
2020 import type { AccessError } from "./access.ts";
21 import { SELF_WAIT } from "./plan_words.ts";
2122 import { followLink } from "./pillars.ts";
2223
2324 /** What a refused link says, in the words the API and the MCP share. */
2425 export const dependencyWords: Record<string, string> = {
25 self: "an issue cannot wait for itself",
26 self: SELF_WAIT,
2627 cycle: "that would make a loop",
2728 cross_repo: "dependencies stay inside one repository",
2829 not_an_issue: "only issues can depend on issues",
src/actions/limb_replan.ts +2 −1
@@ -24,6 +24,7 @@ import { err, ok, type Result } from "../kernel/result.ts";
2424 import { getIssueById } from "../core/issues.ts";
2525 import type { FleetError } from "./fleet.ts";
2626 import type { LimbIssue } from "./limbs.ts";
27 import { IN_HAND } from "./plan_words.ts";
2728
2829 export const REPLAN_ACTIONS = ["replan", "takeback"] as const;
2930 export type ReplanAction = (typeof REPLAN_ACTIONS)[number];
@@ -145,7 +146,7 @@ export function takeBackLimb(
145146 const pending = pendingFor(app.db, repo, limb.id);
146147 if (pending === undefined) return fail("bad_action", "it is not with the planner");
147148 if (!seen.value.takeBack)
148 return fail("bad_action", "the planner has it in hand already");
149 return fail("bad_action", IN_HAND);
149150 recordTakenBack(app.db, pending.id);
150151 app.bus.append({
151152 kind: "limb.replan_taken_back",
src/actions/plan.ts +2 −1
@@ -28,6 +28,7 @@ import { transact } from "../kernel/db.ts";
2828 import { id as newId } from "../kernel/id.ts";
2929 import { err, ok, type Result } from "../kernel/result.ts";
3030 import { isMirror } from "../origins/write-through.ts";
31 import { SELF_WAIT } from "./plan_words.ts";
3132 import { followLink } from "./pillars.ts";
3233
3334 export const PLAN_MAX = 12;
@@ -226,7 +227,7 @@ function vet(
226227 if (w === item.key)
227228 return refuse(
228229 "self",
229 `"${item.key}": ${"an issue cannot wait for itself"}`,
230 `"${item.key}": ${SELF_WAIT}`,
230231 );
231232 } else {
232233 const found = getIssue(app.db, repo.id, w);
src/actions/plan_words.ts added +12
@@ -0,0 +1,12 @@
1 /**
2 * What the planner's refusals say, said from one place (#5595): a link or a
3 * plan item that would make an issue wait for itself, and a limb whose
4 * re-plan the planner already holds. The API, the MCP, the plan and the
5 * terminal read these, so the words cannot drift between them.
6 */
7
8 /** A link, or a plan item, that names its own issue as what it waits for. */
9 export const SELF_WAIT = "an issue cannot wait for itself";
10
11 /** A re-plan that can no longer be taken back: the planner has been given it. */
12 export const IN_HAND = "the planner has it in hand already";
src/contract/openapi.ts +2 −1
@@ -9,6 +9,7 @@
99
1010 import { CAPACITY_KEYS } from "../core/capacity.ts";
1111 import { LIMIT_KEYS } from "../settings/limits.ts";
12 import { SELF_WAIT } from "../actions/plan_words.ts";
1213
1314 interface Route {
1415 readonly method: "get" | "post" | "put" | "patch" | "delete";
@@ -1009,7 +1010,7 @@ export const ROUTES: readonly Route[] = [
10091010 body: { blockedBy: "integer" },
10101011 refusals: {
10111012 no_such_issue: "`blockedBy` names no issue in this repository (422)",
1012 self: "an issue cannot wait for itself (422)",
1013 self: `${SELF_WAIT} (422)`,
10131014 cycle: "that would make a loop (422)",
10141015 cross_repo: "dependencies stay inside one repository (422)",
10151016 not_an_issue: "only issues can depend on issues (422)",
src/tui/screens/limbs.ts +2 −1
@@ -25,6 +25,7 @@
2525
2626 import { undoFleetOp } from "../../actions/fleet.ts";
2727 import { previewReplan, replanLimb, takeBackLimb } from "../../actions/limb_replan.ts";
28 import { IN_HAND } from "../../actions/plan_words.ts";
2829 import { pendingReplans } from "../../core/limb_replan.ts";
2930 import { applyLimb, previewLimb, type LimbPreview } from "../../actions/limbs.ts";
3031 import { canAdmin, canRead, canSeeOwnerFacts, type Repo } from "../../core/repos.ts";
@@ -197,7 +198,7 @@ export function limbsScreen(
197198 }
198199 const p = seen.value;
199200 if (p.withPlanner && !p.takeBack) {
200 ctx.toast("the planner has it in hand already", "danger");
201 ctx.toast(IN_HAND, "danger");
201202 return;
202203 }
203204 if (!p.withPlanner && p.blocked !== undefined) {
test/plan-words.test.ts added +34
@@ -0,0 +1,34 @@
1 /**
2 * The planner's refusals are said from one place (#5595): each sentence
3 * `actions/plan_words.ts` exports is non-empty, and no file that says it
4 * types its own copy back in.
5 */
6
7 import { describe, expect, test } from "bun:test";
8 import { readFileSync } from "node:fs";
9 import { join } from "node:path";
10 import { IN_HAND, SELF_WAIT } from "../src/actions/plan_words.ts";
11
12 const READERS = [
13 "src/actions/dependencies.ts",
14 "src/actions/plan.ts",
15 "src/actions/limb_replan.ts",
16 "src/tui/screens/limbs.ts",
17 "src/contract/openapi.ts",
18 ];
19
20 const source = (path: string) =>
21 readFileSync(join(import.meta.dir, "..", path), "utf8");
22
23 describe("the planner's words", () => {
24 for (const [name, words] of Object.entries({ SELF_WAIT, IN_HAND })) {
25 test(`${name} says something`, () => {
26 expect(words.trim().length).toBeGreaterThan(0);
27 });
28
29 for (const path of READERS)
30 test(`${path} does not type ${name} out again`, () => {
31 expect(source(path)).not.toContain(words);
32 });
33 }
34 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs