@harborppm/cli 0.42.0 → 0.43.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/dist/app.mjs CHANGED
@@ -3009,10 +3009,18 @@ function tideForecast(project, tasks, now) {
3009
3009
  remaining
3010
3010
  };
3011
3011
  const today = todayISO(now);
3012
- const runs = simulate(remaining, weekly, hash(`${project.id}|${remaining}|${today}`));
3013
3012
  const base = noon(today);
3013
+ const paced = simulate(remaining, weekly, hash(`${project.id}|${remaining}|${today}`));
3014
+ const at = (share, list = runs) => list[Math.min(list.length - 1, Math.ceil(share * list.length) - 1)];
3015
+ let lastDue = null;
3016
+ for (const t of tasks) {
3017
+ if (t.status === "done" || !t.dueDate) continue;
3018
+ if (!lastDue || t.dueDate > lastDue.dueDate) lastDue = t;
3019
+ }
3020
+ const floor = lastDue ? differenceInCalendarDays(noon(lastDue.dueDate), base) : 0;
3021
+ const runs = floor > 0 ? paced.map((d) => Math.max(d, floor)) : paced;
3022
+ const heldBy = lastDue && floor > at(.5, paced) ? lastDue : null;
3014
3023
  const dayOf = (days) => Number.isFinite(days) ? todayISO(addDays(base, days)) : null;
3015
- const at = (share) => runs[Math.min(runs.length - 1, Math.ceil(share * runs.length) - 1)];
3016
3024
  const toEnd = differenceInCalendarDays(noon(project.endDate), base);
3017
3025
  const byEnd = runs.filter((d) => d <= toEnd).length / runs.length;
3018
3026
  const last = Math.max(Number.isFinite(at(.99)) ? at(.99) : 728, toEnd + 14, 7);
@@ -3037,7 +3045,8 @@ function tideForecast(project, tasks, now) {
3037
3045
  almostCertain: dayOf(at(.95)),
3038
3046
  chanceByEnd: Math.round(byEnd * 100) / 100,
3039
3047
  curve,
3040
- rough: finished < ROUGH_BELOW
3048
+ rough: finished < ROUGH_BELOW,
3049
+ heldBy
3041
3050
  };
3042
3051
  }
3043
3052
  /** "15%", with "under 5%" and "over 95%" at the ends, where a thousand runs can't be that sure. */
@@ -3047,18 +3056,11 @@ function chanceText(chance) {
3047
3056
  return `${Math.round(chance * 20) * 5}%`;
3048
3057
  }
3049
3058
  /**
3050
- * The open task due last, when it's due after the likely finish. The pace
3051
- * knows nothing of due dates, so a task due later means the plan runs
3052
- * longer than the pace says.
3059
+ * The open task whose due date sets the finish, when the crew's pace alone
3060
+ * would be done sooner; null otherwise.
3053
3061
  */
3054
- function dueAfterFinish(tide, tasks) {
3055
- if (tide.kind !== "forecast" || !tide.likely) return null;
3056
- let last = null;
3057
- for (const t of tasks) {
3058
- if (t.status === "done" || !t.dueDate) continue;
3059
- if (!last || t.dueDate > last.dueDate) last = t;
3060
- }
3061
- return last && last.dueDate.slice(0, 10) > tide.likely ? last : null;
3062
+ function dueAfterFinish(tide) {
3063
+ return tide.kind === "forecast" ? tide.heldBy : null;
3062
3064
  }
3063
3065
 
3064
3066
  //#endregion
@@ -3970,8 +3972,8 @@ function filterTasks(tasks, f) {
3970
3972
  }
3971
3973
  /**
3972
3974
  * When the project will likely finish at its crew's recent pace, in one
3973
- * line under its name, whether that misses the end date, and an open task
3974
- * due after it, which the pace can't see. Empty when
3975
+ * line under its name, whether that misses the end date, and the open task
3976
+ * whose due date holds it later than the pace would. Empty when
3975
3977
  * there's no forecast (all done, paused, or nothing finished lately).
3976
3978
  */
3977
3979
  function tideLine(data, project, today) {
@@ -3988,8 +3990,8 @@ function tideLine(data, project, today) {
3988
3990
  };
3989
3991
  const very = tide.veryLikely ? `, very likely by ${shortDay(tide.veryLikely, today)}` : "";
3990
3992
  const end = project.endDate >= today ? ` · ${chanceText(tide.chanceByEnd)} by its end, ${shortDay(project.endDate, today)}` : "";
3991
- const later = dueAfterFinish(tide, tasks);
3992
- const due = later ? ` · ${taskKey(later, project) ?? `“${later.title}”`} is due later, ${shortDay(later.dueDate.slice(0, 10), today)}` : "";
3993
+ const later = dueAfterFinish(tide);
3994
+ const due = later ? ` · ${taskKey(later, project) ?? `“${later.title}”`} is due ${shortDay(later.dueDate.slice(0, 10), today)}` : "";
3993
3995
  return {
3994
3996
  text: `Probably done ${shortDay(tide.likely, today)}${very}${end}${due}${tide.rough ? " · rough" : ""}`,
3995
3997
  late: tide.chanceByEnd < .5
@@ -15657,9 +15659,12 @@ async function initLinked(opts) {
15657
15659
  consumed: /* @__PURE__ */ new Set(),
15658
15660
  skipped: /* @__PURE__ */ new Set()
15659
15661
  }, report, true);
15662
+ const mine = data.tasks.filter((t) => t.projectId === project.id);
15663
+ const open = mine.filter((t) => t.status !== "done").length;
15660
15664
  return {
15661
15665
  config,
15662
- tasks: data.tasks.filter((t) => t.projectId === project.id).length,
15666
+ tasks: mine.length,
15667
+ open,
15663
15668
  moved,
15664
15669
  report
15665
15670
  };
@@ -15956,12 +15961,13 @@ async function initHere(opts) {
15956
15961
  dryRun: opts.dryRun
15957
15962
  });
15958
15963
  const { project, harbor } = made.config;
15964
+ const counted = made.open === made.tasks ? plural(made.tasks, "task") : `${made.open} open and ${made.tasks - made.open} done ${made.tasks === 1 ? "task" : "tasks"}`;
15959
15965
  if (opts.dryRun) {
15960
- opts.say(made.moved ? `Would move this folder's ${plural(made.moved, "task")} into ${project.name} (${project.key}) in ${harbor.name}.` : `Would make ${shownDir(opts.cwd, dir)} for ${project.name} (${project.key}) in ${harbor.name}, with ${plural(made.tasks, "task")}.`);
15966
+ opts.say(made.moved ? `Would move this folder's ${plural(made.moved, "task")} into ${project.name} (${project.key}) in ${harbor.name}.` : `Would make ${shownDir(opts.cwd, dir)} for ${project.name} (${project.key}) in ${harbor.name}, with a file for each of its ${counted}.`);
15961
15967
  opts.say("Dry run: nothing was written or sent. Run it again without --dry-run to do it.");
15962
15968
  return made.config;
15963
15969
  }
15964
- opts.say(made.moved ? `Moved this folder's ${plural(made.moved, "task")} into ${project.name} in ${harbor.name}.` : `Made ${shownDir(opts.cwd, dir)} for ${project.name} (${project.key}) in ${harbor.name}, with ${plural(made.tasks, "task")}. Commit it like any code.`);
15970
+ opts.say(made.moved ? `Moved this folder's ${plural(made.moved, "task")} into ${project.name} in ${harbor.name}.` : `Made ${shownDir(opts.cwd, dir)} for ${project.name} (${project.key}) in ${harbor.name}, with a file for each of its ${counted}. Commit it like any code.`);
15965
15971
  opts.say("Edit a file, or add one, then run harbor sync. Changes on the website come back the same way.");
15966
15972
  if (opts.hook) opts.say(await addHook(home));
15967
15973
  return made.config;
package/lib/cli.mjs CHANGED
@@ -506,12 +506,13 @@ const DETAIL = {
506
506
  " harbor forecast PROJECT reads one project's forecast and what",
507
507
  " it rests on",
508
508
  "",
509
- " Worked out from the crew's pace alone: how many tasks they finished each",
510
- " week over the last six (or the project's life, if shorter), replayed a",
509
+ " Worked out from the crew's pace: how many tasks they finished each week",
510
+ " over the last six (or the project's life, if shorter), replayed a",
511
511
  " thousand times until its open tasks run out. Probably is half of those",
512
512
  " runs, very likely 85%, almost certainly 95%. No estimates or points.",
513
- " It warns when under two weeks of pace is all there is, and when an open",
514
- " task is due after the likely finish, since the pace can't see due dates.",
513
+ " No run finishes before the last open task is due, so a task due later",
514
+ " than the pace would finish sets the dates, and the forecast names it. It",
515
+ " warns when under two weeks of pace is all there is.",
515
516
  ],
516
517
  report: [
517
518
  " harbor report PROGRAM reads the program's status report",
package/lib/format.mjs CHANGED
@@ -346,7 +346,7 @@ export function forecastOne(p) {
346
346
  if (f.lastDue) {
347
347
  const which = f.lastDue.key ? `${f.lastDue.key} “${f.lastDue.title}”` : `“${f.lastDue.title}”`;
348
348
  lines.push(
349
- `${which} is due ${f.lastDue.due}, after the likely finish, so the plan runs longer than the pace says.`,
349
+ `${which} is due ${f.lastDue.due}, later than the crew's pace alone would finish, so the dates wait for it.`,
350
350
  );
351
351
  }
352
352
  return lines.join("\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harborppm/cli",
3
- "version": "0.42.0",
3
+ "version": "0.43.0",
4
4
  "description": "Harbor from the terminal: a full-screen app with everything, plus commands for scripts and agents.",
5
5
  "keywords": [
6
6
  "harbor",