@harborppm/cli 0.49.0 → 0.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/app.mjs +54 -2
- package/lib/cli.mjs +67 -20
- package/lib/format.mjs +9 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -155,9 +155,11 @@ harbor risk "TITLE" --project P|--program P [--impact 1-5] [--probability 1-5]
|
|
|
155
155
|
harbor issue "TITLE" --project P|--program P [--impact 1-5] [--owner NAME]
|
|
156
156
|
[--due DATE]
|
|
157
157
|
harbor decision "TITLE" --project P|--program P [--owner NAME] [--due DATE]
|
|
158
|
+
[--turned-down T] [--look-again DATE] [--supersedes "OLD"]
|
|
158
159
|
harbor risks [words] [--kind risk,issue,decision] [--project P] [--closed]
|
|
159
160
|
harbor raid "TITLE" [--status S] [--owner me|none|NAME] [--impact 1-5] [--probability 1-5]
|
|
160
|
-
[--due DATE|none] [--update TEXT] [--mitigation T] [--resolution T]
|
|
161
|
+
[--due DATE|none] [--update TEXT] [--mitigation T] [--resolution T]
|
|
162
|
+
[--turned-down T] [--look-again DATE|none] …
|
|
161
163
|
one risk, issue or decision in full, or change it
|
|
162
164
|
harbor review [WORDS] [--mine|--owner NAME] [--portfolio P] [--done]
|
|
163
165
|
the review: programs worst first, with next moves
|
package/dist/app.mjs
CHANGED
|
@@ -2069,7 +2069,8 @@ function normalizeRaidItem(r) {
|
|
|
2069
2069
|
updates: Array.isArray(r.updates) ? r.updates : [],
|
|
2070
2070
|
scoreHistory: Array.isArray(r.scoreHistory) && r.scoreHistory.length ? r.scoreHistory : void 0,
|
|
2071
2071
|
...kind === "risk" && typeof r.happened === "boolean" ? { happened: r.happened } : {},
|
|
2072
|
-
...kind === "decision" ? decisionFields(r) : {}
|
|
2072
|
+
...kind === "decision" ? decisionFields(r) : {},
|
|
2073
|
+
...ids$1(r.followerIds).length ? { followerIds: ids$1(r.followerIds) } : {}
|
|
2073
2074
|
};
|
|
2074
2075
|
}
|
|
2075
2076
|
/** A revisit condition that still says something, or null. */
|
|
@@ -14719,6 +14720,24 @@ var init_assignment = __esmMin((() => {}));
|
|
|
14719
14720
|
//#region src/lib/slices/tasks.ts
|
|
14720
14721
|
function tasksSlice({ set, get, write }) {
|
|
14721
14722
|
return {
|
|
14723
|
+
followWork: (personId, taskIds, raidIds = []) => {
|
|
14724
|
+
const tasks = new Set(taskIds);
|
|
14725
|
+
const raid = new Set(raidIds);
|
|
14726
|
+
const add = (ids) => ids?.includes(personId) ? ids : [...ids ?? [], personId];
|
|
14727
|
+
write({
|
|
14728
|
+
tasks: get().tasks.map((t) => tasks.has(t.id) && t.assigneeId !== personId ? {
|
|
14729
|
+
...t,
|
|
14730
|
+
followerIds: add(t.followerIds)
|
|
14731
|
+
} : t),
|
|
14732
|
+
raidItems: get().raidItems.map((r) => raid.has(r.id) && r.ownerId !== personId ? {
|
|
14733
|
+
...r,
|
|
14734
|
+
followerIds: add(r.followerIds),
|
|
14735
|
+
updatedAt: stamp()
|
|
14736
|
+
} : r)
|
|
14737
|
+
});
|
|
14738
|
+
const who = get().people.find((p) => p.id === personId);
|
|
14739
|
+
get().log(`added ${who?.name ?? "someone"} as a second pair of hands on ${taskIds.length + raidIds.length} item${taskIds.length + raidIds.length === 1 ? "" : "s"}`, "/people");
|
|
14740
|
+
},
|
|
14722
14741
|
addTask: (t, opts) => {
|
|
14723
14742
|
if (!opts?.allowOverCap) {
|
|
14724
14743
|
if (columnWip(get(), t.projectId, t.status, 1)) return "";
|
|
@@ -25377,9 +25396,30 @@ function dockCompare(data, projectId, changes, now) {
|
|
|
25377
25396
|
pace: plan.pace,
|
|
25378
25397
|
shift: a && b ? dayNumber(b) - dayNumber(a) : null,
|
|
25379
25398
|
load,
|
|
25380
|
-
stale: plan.stale
|
|
25399
|
+
stale: plan.stale,
|
|
25400
|
+
notes: dockNotes(plan.project, realTasks, dockTide, now)
|
|
25381
25401
|
};
|
|
25382
25402
|
}
|
|
25403
|
+
/**
|
|
25404
|
+
* What to know before trusting the shift: the pace is read from weeks of
|
|
25405
|
+
* finished work, so a short or lumpy record (work marked done in one go) or
|
|
25406
|
+
* a due date that holds the finish can leave the dates where they were.
|
|
25407
|
+
*/
|
|
25408
|
+
function dockNotes(project, realTasks, docked, now) {
|
|
25409
|
+
if (docked.kind !== "forecast") return [];
|
|
25410
|
+
const notes = [];
|
|
25411
|
+
const weekly = weeklyFinished(realTasks, project.startDate, now);
|
|
25412
|
+
const finished = weekly.reduce((a, b) => a + b, 0);
|
|
25413
|
+
const busiest = Math.max(0, ...weekly);
|
|
25414
|
+
if (weekly.length < 2) notes.push("The pace comes from under two weeks of finished work, so treat the dates as rough.");
|
|
25415
|
+
else if (docked.rough) notes.push(`Only ${finished} task${finished === 1 ? "" : "s"} finished in the last ${weekly.length} weeks, so treat the dates as rough.`);
|
|
25416
|
+
if (weekly.length >= 2 && finished >= 4 && busiest * 2 > finished) notes.push(`${busiest} of the ${finished} tasks finished lately were marked done in one week, so the pace jumps from week to week and a few tasks more or less may not move the dates.`);
|
|
25417
|
+
if (docked.heldBy?.dueDate) {
|
|
25418
|
+
const name = taskKey(docked.heldBy, project) ?? docked.heldBy.title;
|
|
25419
|
+
notes.push(`${name} is due ${shortDate(docked.heldBy.dueDate)}, after the pace would finish, so its due date sets the finish and work done before then doesn't move it.`);
|
|
25420
|
+
}
|
|
25421
|
+
return notes;
|
|
25422
|
+
}
|
|
25383
25423
|
/** "11 days sooner", "3 days later", "Same day", or null without both forecasts. */
|
|
25384
25424
|
function shiftText(shift) {
|
|
25385
25425
|
if (shift == null) return null;
|
|
@@ -25573,6 +25613,10 @@ function tryLines(data, project, changes, cmp, today) {
|
|
|
25573
25613
|
const end = docked.project.endDate;
|
|
25574
25614
|
if (end >= today && real.tide.kind === "forecast" && docked.tide.kind === "forecast") out.push(row(`Making ${shortDate(end)}`, end === real.project.endDate ? chanceText(real.tide.chanceByEnd) : "-", chanceText(docked.tide.chanceByEnd)));
|
|
25575
25615
|
if (cmp.pace !== 1) out.push(row("Crew pace", "100%", `${Math.round(cmp.pace * 100)}%`));
|
|
25616
|
+
if (changes.length && cmp.notes.length) {
|
|
25617
|
+
out.push("");
|
|
25618
|
+
for (const n of cmp.notes) out.push(` Note: ${n}`);
|
|
25619
|
+
}
|
|
25576
25620
|
if (cmp.load.length) {
|
|
25577
25621
|
out.push("", " Due in the next six weeks:");
|
|
25578
25622
|
for (const l of cmp.load) {
|
|
@@ -25621,6 +25665,14 @@ async function tryHere(opts) {
|
|
|
25621
25665
|
lines
|
|
25622
25666
|
};
|
|
25623
25667
|
if ((opts.apply || opts.save) && changes.length === 0) throw new HarborError("There are no changes to apply or save. Add some, like --drop RL-4.");
|
|
25668
|
+
if (opts.apply && !opts.confirm) {
|
|
25669
|
+
const gone = changes.flatMap((c) => {
|
|
25670
|
+
if (c.kind !== "drop") return [];
|
|
25671
|
+
const t = data.tasks.find((x) => x.id === c.taskId);
|
|
25672
|
+
return t ? [`${taskKey(t, project) ?? t.id} ${t.title}`] : [];
|
|
25673
|
+
});
|
|
25674
|
+
if (gone.length) throw new HarborError(`--apply removes ${gone.length === 1 ? "this task" : `these ${gone.length} tasks`} from the harbor:\n${gone.map((g) => ` ${g}`).join("\n")}\nTo remove ${gone.length === 1 ? "it" : "them"}, run it again with --confirm. Nothing was changed.`);
|
|
25675
|
+
}
|
|
25624
25676
|
if (opts.apply) {
|
|
25625
25677
|
const s = store.getState();
|
|
25626
25678
|
result.applied = applyDock({
|
package/lib/cli.mjs
CHANGED
|
@@ -85,7 +85,7 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
85
85
|
harbor try PROJECT [--drop KEYS] [--add "TITLE" [--count N]]
|
|
86
86
|
[--due KEY=DATE] [--give KEY=NAME] [--end DATE]
|
|
87
87
|
[--borrow NAME[=DAYS]] [--release NAME]
|
|
88
|
-
[--saved] [--apply|--save|--discard]
|
|
88
|
+
[--saved] [--apply [--confirm]|--save|--discard]
|
|
89
89
|
dry dock: the forecast with plan
|
|
90
90
|
changes tried on a copy; nothing
|
|
91
91
|
changes without --apply
|
|
@@ -142,6 +142,8 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
142
142
|
harbor issue "TITLE" --project P|--program P [--impact 1-5] [--owner NAME]
|
|
143
143
|
[--due DATE]
|
|
144
144
|
harbor decision "TITLE" --project P|--program P [--owner NAME] [--due DATE]
|
|
145
|
+
[--turned-down T] [--look-again DATE]
|
|
146
|
+
[--supersedes "OLD DECISION"]
|
|
145
147
|
harbor risks [words] [--kind risk,issue,decision] [--project P] [--closed]
|
|
146
148
|
the register, worst first
|
|
147
149
|
(--closed adds closed ones)
|
|
@@ -149,7 +151,7 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
149
151
|
[--probability 1-5] [--due DATE|none] [--update TEXT]
|
|
150
152
|
[--title T] [--description T] [--mitigation T]
|
|
151
153
|
[--contingency T] [--resolution T] [--rationale T]
|
|
152
|
-
[--notes T]
|
|
154
|
+
[--notes T] [--turned-down T] [--look-again DATE|none]
|
|
153
155
|
one risk, issue or decision in
|
|
154
156
|
full, or change it
|
|
155
157
|
harbor review [WORDS] [--mine|--owner NAME] [--portfolio P] [--done]
|
|
@@ -566,6 +568,8 @@ const DETAIL = {
|
|
|
566
568
|
" harbor try PROJECT [changes] reads the forecast with the changes",
|
|
567
569
|
" tried on a copy of the plan",
|
|
568
570
|
" harbor try PROJECT [changes] --apply writes: makes the changes for real",
|
|
571
|
+
" (--confirm too when it drops tasks:",
|
|
572
|
+
" they're removed from the harbor)",
|
|
569
573
|
" harbor try PROJECT [changes] --save writes: keeps them on the project to",
|
|
570
574
|
" show someone (Forecast tab, Open dry",
|
|
571
575
|
" dock)",
|
|
@@ -574,7 +578,8 @@ const DETAIL = {
|
|
|
574
578
|
" harbor try PROJECT --discard writes: throws the saved ones away",
|
|
575
579
|
"",
|
|
576
580
|
" Changes, each taking several with commas between:",
|
|
577
|
-
" --drop RL-4,RL-5 take tasks out of the plan (--apply
|
|
581
|
+
" --drop RL-4,RL-5 take tasks out of the plan (--apply --confirm",
|
|
582
|
+
" removes them)",
|
|
578
583
|
' --add "TITLE" --count N new work, N tasks called TITLE',
|
|
579
584
|
" --due RL-4=fri move a due date (none takes it off)",
|
|
580
585
|
" --give RL-4=Sam give a task to someone (none: nobody)",
|
|
@@ -588,6 +593,13 @@ const DETAIL = {
|
|
|
588
593
|
" date, the crew's pace, and the load of everyone the changes touch. A",
|
|
589
594
|
" borrowed person adds an average crew member's pace for each day a week",
|
|
590
595
|
" they give; a released one takes away their share of what was finished.",
|
|
596
|
+
" Notes under it say when the dates may not move: a short or lumpy record",
|
|
597
|
+
" of finished work, or a due date that sets the finish.",
|
|
598
|
+
"",
|
|
599
|
+
" --json gives each plan's endDate, open, likely, veryLikely, chanceByEnd",
|
|
600
|
+
" and tasksPerWeek (noForecast says why when there are no dates), the",
|
|
601
|
+
" shiftDays between them, crewPacePercent (100 is the crew as it is) and",
|
|
602
|
+
" the notes.",
|
|
591
603
|
],
|
|
592
604
|
report: [
|
|
593
605
|
" harbor report PROGRAM reads the program's status report",
|
|
@@ -733,7 +745,8 @@ const DETAIL = {
|
|
|
733
745
|
" Also --owner me|none|NAME, --impact 1-5, --probability 1-5 (risks),",
|
|
734
746
|
" --due DATE|none (review by, or decide by), --title, --description,",
|
|
735
747
|
" --mitigation and --contingency (risks), --resolution (issues and",
|
|
736
|
-
" decisions), --rationale
|
|
748
|
+
" decisions), --rationale, --turned-down and --look-again DATE|none",
|
|
749
|
+
" (decisions) and --notes. TITLE is its title or",
|
|
737
750
|
" enough of it to pick one out; harbor risks lists them. Changes several",
|
|
738
751
|
" at once, as one change.",
|
|
739
752
|
],
|
|
@@ -784,6 +797,12 @@ const DETAIL = {
|
|
|
784
797
|
' harbor decision "TITLE" --project P writes: logs a decision to make',
|
|
785
798
|
' harbor decision "TITLE" --program P writes: logs one on the program',
|
|
786
799
|
" [--owner NAME] [--due DATE] (the date to decide by)",
|
|
800
|
+
" [--turned-down T] (what was turned down, and why not)",
|
|
801
|
+
" [--look-again DATE] (when it comes back to the top of the brief)",
|
|
802
|
+
' harbor decision "TITLE" --supersedes "OLD"',
|
|
803
|
+
" writes: a decision that replaces an",
|
|
804
|
+
" earlier one, which reads as",
|
|
805
|
+
" superseded",
|
|
787
806
|
],
|
|
788
807
|
};
|
|
789
808
|
|
|
@@ -840,6 +859,9 @@ function distance(a, b) {
|
|
|
840
859
|
/** A mistyped command, with the one it was probably meant to be. */
|
|
841
860
|
export function noCommand(name) {
|
|
842
861
|
if (name.startsWith("-")) return `${name} goes after a command, like harbor mine ${name}.`;
|
|
862
|
+
// A plural kind reads the register, not the command that writes one.
|
|
863
|
+
if (["decisions", "issues"].includes(name))
|
|
864
|
+
return `No command "${name}". harbor risks --kind ${name.slice(0, -1)} lists them.`;
|
|
843
865
|
const near = COMMANDS.filter((c) => distance(name, c) <= (c.length > 4 ? 2 : 1));
|
|
844
866
|
const guess = near.length === 1 ? ` Did you mean harbor ${near[0]}?` : "";
|
|
845
867
|
return `No command "${name}".${guess} Run harbor help.`;
|
|
@@ -1169,7 +1191,7 @@ export async function run(argv, io = {}) {
|
|
|
1169
1191
|
|
|
1170
1192
|
if (command === "try") {
|
|
1171
1193
|
const { values, positionals } = parseArgs(rest, {
|
|
1172
|
-
flags: ["apply", "save", "discard", "saved", "json"],
|
|
1194
|
+
flags: ["apply", "confirm", "save", "discard", "saved", "json"],
|
|
1173
1195
|
options: ["harbor", "drop", "add", "count", "due", "give", "end", "borrow", "release"],
|
|
1174
1196
|
});
|
|
1175
1197
|
if (positionals.length !== 1)
|
|
@@ -1180,6 +1202,8 @@ export async function run(argv, io = {}) {
|
|
|
1180
1202
|
);
|
|
1181
1203
|
if (["apply", "save", "discard"].filter((k) => values[k]).length > 1)
|
|
1182
1204
|
throw new HarborError("Use one of --apply, --save or --discard.");
|
|
1205
|
+
if (values.confirm && !values.apply)
|
|
1206
|
+
throw new HarborError("--confirm goes with --apply, to remove the tasks it drops.");
|
|
1183
1207
|
const creds = await readCredentials(env);
|
|
1184
1208
|
if (!hasFullAccess(creds))
|
|
1185
1209
|
throw new HarborError(
|
|
@@ -1198,6 +1222,7 @@ export async function run(argv, io = {}) {
|
|
|
1198
1222
|
project: positionals[0],
|
|
1199
1223
|
flags: values,
|
|
1200
1224
|
apply: Boolean(values.apply),
|
|
1225
|
+
confirm: Boolean(values.confirm),
|
|
1201
1226
|
save: Boolean(values.save),
|
|
1202
1227
|
discard: Boolean(values.discard),
|
|
1203
1228
|
say,
|
|
@@ -1216,14 +1241,19 @@ export async function run(argv, io = {}) {
|
|
|
1216
1241
|
return 0;
|
|
1217
1242
|
}
|
|
1218
1243
|
if (values.json) {
|
|
1219
|
-
const side = (s) =>
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1244
|
+
const side = (s) => {
|
|
1245
|
+
const f = s.tide.kind === "forecast" ? s.tide : null;
|
|
1246
|
+
return {
|
|
1247
|
+
endDate: s.project.endDate,
|
|
1248
|
+
open: s.open,
|
|
1249
|
+
likely: f?.likely ?? null,
|
|
1250
|
+
veryLikely: f?.veryLikely ?? null,
|
|
1251
|
+
chanceByEnd: f?.chanceByEnd ?? null,
|
|
1252
|
+
tasksPerWeek: f?.perWeek ?? null,
|
|
1253
|
+
// Why there are no dates, when there aren't: no pace yet, paused, done.
|
|
1254
|
+
noForecast: f ? null : s.tide.kind,
|
|
1255
|
+
};
|
|
1256
|
+
};
|
|
1227
1257
|
const c = result.comparison;
|
|
1228
1258
|
out(
|
|
1229
1259
|
jsonText({
|
|
@@ -1232,10 +1262,10 @@ export async function run(argv, io = {}) {
|
|
|
1232
1262
|
real: side(c.real),
|
|
1233
1263
|
dryDock: side(c.docked),
|
|
1234
1264
|
shiftDays: c.shift,
|
|
1235
|
-
|
|
1265
|
+
crewPacePercent: Math.round(c.pace * 100),
|
|
1236
1266
|
...(result.applied !== undefined ? { applied: result.applied } : {}),
|
|
1237
1267
|
...(result.saved ? { saved: true } : {}),
|
|
1238
|
-
notes,
|
|
1268
|
+
notes: [...c.notes, ...notes],
|
|
1239
1269
|
}),
|
|
1240
1270
|
);
|
|
1241
1271
|
return 0;
|
|
@@ -1790,7 +1820,9 @@ export async function run(argv, io = {}) {
|
|
|
1790
1820
|
);
|
|
1791
1821
|
}
|
|
1792
1822
|
if (values.confirm || values["over-limit"]) {
|
|
1793
|
-
throw new HarborError(
|
|
1823
|
+
throw new HarborError(
|
|
1824
|
+
"--confirm and --over-limit are for moving a task: harbor submit TASK.",
|
|
1825
|
+
);
|
|
1794
1826
|
}
|
|
1795
1827
|
if (values.mine && values.owner) throw new HarborError("Say --mine or --owner, not both.");
|
|
1796
1828
|
return call(
|
|
@@ -1943,7 +1975,7 @@ export async function run(argv, io = {}) {
|
|
|
1943
1975
|
includeClosed: Boolean(values.closed),
|
|
1944
1976
|
limit: 50,
|
|
1945
1977
|
},
|
|
1946
|
-
(r) => raidList(r, positionals.join(" ")),
|
|
1978
|
+
(r) => raidList(r, positionals.join(" "), kind),
|
|
1947
1979
|
);
|
|
1948
1980
|
}
|
|
1949
1981
|
case "report": {
|
|
@@ -2272,6 +2304,8 @@ export async function run(argv, io = {}) {
|
|
|
2272
2304
|
"update",
|
|
2273
2305
|
"decided-by",
|
|
2274
2306
|
"decided-on",
|
|
2307
|
+
"turned-down",
|
|
2308
|
+
"look-again",
|
|
2275
2309
|
...fields,
|
|
2276
2310
|
],
|
|
2277
2311
|
),
|
|
@@ -2289,6 +2323,9 @@ export async function run(argv, io = {}) {
|
|
|
2289
2323
|
decidedBy: values["decided-by"],
|
|
2290
2324
|
decidedOn:
|
|
2291
2325
|
values["decided-on"] === undefined ? undefined : nullable(dueDate(values["decided-on"])),
|
|
2326
|
+
turnedDown: values["turned-down"],
|
|
2327
|
+
revisitOn:
|
|
2328
|
+
values["look-again"] === undefined ? undefined : nullable(dueDate(values["look-again"])),
|
|
2292
2329
|
update: values.update,
|
|
2293
2330
|
...Object.fromEntries(fields.map((f) => [f, values[f]])),
|
|
2294
2331
|
};
|
|
@@ -2350,11 +2387,15 @@ export async function run(argv, io = {}) {
|
|
|
2350
2387
|
spec(
|
|
2351
2388
|
[],
|
|
2352
2389
|
["project", "program", "owner", "due"].concat(
|
|
2353
|
-
command === "risk"
|
|
2390
|
+
command === "risk"
|
|
2391
|
+
? ["impact", "probability"]
|
|
2392
|
+
: command === "issue"
|
|
2393
|
+
? ["impact"]
|
|
2394
|
+
: ["supersedes", "turned-down", "look-again"],
|
|
2354
2395
|
),
|
|
2355
2396
|
),
|
|
2356
2397
|
);
|
|
2357
|
-
if (!values.project && !values.program) {
|
|
2398
|
+
if (!values.project && !values.program && !values.supersedes) {
|
|
2358
2399
|
throw new HarborError("Say where it belongs: --project NAME or --program NAME.");
|
|
2359
2400
|
}
|
|
2360
2401
|
return call(
|
|
@@ -2369,8 +2410,14 @@ export async function run(argv, io = {}) {
|
|
|
2369
2410
|
impact: level(values.impact, "impact"),
|
|
2370
2411
|
probability: level(values.probability, "probability"),
|
|
2371
2412
|
due: dueDate(values.due),
|
|
2413
|
+
supersedes: values.supersedes,
|
|
2414
|
+
turnedDown: values["turned-down"],
|
|
2415
|
+
revisitOn: values["look-again"] === undefined ? undefined : dueDate(values["look-again"]),
|
|
2372
2416
|
},
|
|
2373
|
-
(r) =>
|
|
2417
|
+
(r) =>
|
|
2418
|
+
r.replaces
|
|
2419
|
+
? `Logged the decision “${r.raised.title}”; it replaces “${r.replaces.title}”.`
|
|
2420
|
+
: `Logged the ${command} “${r.raised.title}”.`,
|
|
2374
2421
|
);
|
|
2375
2422
|
}
|
|
2376
2423
|
default:
|
package/lib/format.mjs
CHANGED
|
@@ -246,12 +246,19 @@ export function todayLocal(d = new Date()) {
|
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
/** The register, worst first: kind, title, then status, scores, owner and where it sits. */
|
|
249
|
-
export function raidList(r, words = "") {
|
|
249
|
+
export function raidList(r, words = "", kinds = []) {
|
|
250
250
|
const want = words.trim().toLowerCase();
|
|
251
251
|
const items = (r.items ?? []).filter(
|
|
252
252
|
(x) => !want || `${x.title} ${x.description ?? ""}`.toLowerCase().includes(want),
|
|
253
253
|
);
|
|
254
|
-
if (!items.length)
|
|
254
|
+
if (!items.length) {
|
|
255
|
+
if (want) return "Nothing in the register matches.";
|
|
256
|
+
const only =
|
|
257
|
+
kinds?.length === 1
|
|
258
|
+
? { risk: "risks", issue: "issues", decision: "decisions" }[kinds[0]]
|
|
259
|
+
: null;
|
|
260
|
+
return only ? `No ${only} in the register.` : "The register is clear.";
|
|
261
|
+
}
|
|
255
262
|
const lines = items.map((x) => {
|
|
256
263
|
const bits = [
|
|
257
264
|
x.status?.toLowerCase(),
|