@harborppm/cli 0.46.0 → 0.47.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 +271 -32
- package/package.json +1 -1
package/dist/app.mjs
CHANGED
|
@@ -2068,9 +2068,43 @@ function normalizeRaidItem(r) {
|
|
|
2068
2068
|
updatedAt: r.updatedAt ?? r.createdAt ?? now,
|
|
2069
2069
|
updates: Array.isArray(r.updates) ? r.updates : [],
|
|
2070
2070
|
scoreHistory: Array.isArray(r.scoreHistory) && r.scoreHistory.length ? r.scoreHistory : void 0,
|
|
2071
|
-
...kind === "risk" && typeof r.happened === "boolean" ? { happened: r.happened } : {}
|
|
2071
|
+
...kind === "risk" && typeof r.happened === "boolean" ? { happened: r.happened } : {},
|
|
2072
|
+
...kind === "decision" ? decisionFields(r) : {}
|
|
2072
2073
|
};
|
|
2073
2074
|
}
|
|
2075
|
+
/** A revisit condition that still says something, or null. */
|
|
2076
|
+
function cleanRevisit(v) {
|
|
2077
|
+
if (!v || typeof v !== "object") return null;
|
|
2078
|
+
const r = v;
|
|
2079
|
+
const str = (x) => typeof x === "string" && x ? x : null;
|
|
2080
|
+
if (r.kind === "date" && str(r.on)) return {
|
|
2081
|
+
kind: "date",
|
|
2082
|
+
on: str(r.on).slice(0, 10)
|
|
2083
|
+
};
|
|
2084
|
+
if (r.kind === "risk" && str(r.riskId)) return {
|
|
2085
|
+
kind: "risk",
|
|
2086
|
+
riskId: str(r.riskId)
|
|
2087
|
+
};
|
|
2088
|
+
if (r.kind === "milestone" && str(r.milestoneId) && str(r.due)) return {
|
|
2089
|
+
kind: "milestone",
|
|
2090
|
+
milestoneId: str(r.milestoneId),
|
|
2091
|
+
due: str(r.due).slice(0, 10)
|
|
2092
|
+
};
|
|
2093
|
+
return null;
|
|
2094
|
+
}
|
|
2095
|
+
/** A decision's own fields, kept only where they say something so older rows stay as they were. */
|
|
2096
|
+
function decisionFields(r) {
|
|
2097
|
+
const out = {};
|
|
2098
|
+
if (r.turnedDown?.trim()) out.turnedDown = r.turnedDown;
|
|
2099
|
+
const tasks = ids$1(r.taskIds);
|
|
2100
|
+
if (tasks.length) out.taskIds = tasks;
|
|
2101
|
+
const risks = ids$1(r.riskIds);
|
|
2102
|
+
if (risks.length) out.riskIds = risks;
|
|
2103
|
+
const revisit = cleanRevisit(r.revisitIf);
|
|
2104
|
+
if (revisit) out.revisitIf = revisit;
|
|
2105
|
+
if (r.supersedesId) out.supersedesId = r.supersedesId;
|
|
2106
|
+
return out;
|
|
2107
|
+
}
|
|
2074
2108
|
/** Active first, then by score, then most recently touched. */
|
|
2075
2109
|
function sortRaid(items) {
|
|
2076
2110
|
return [...items].sort((a, b) => {
|
|
@@ -2112,7 +2146,7 @@ function heatGrid(items) {
|
|
|
2112
2146
|
function inHeatCell(item, probability, impact) {
|
|
2113
2147
|
return item.kind === "risk" && item.probability === probability && item.impact === impact;
|
|
2114
2148
|
}
|
|
2115
|
-
var KIND_LABEL, STATUS_LABEL$1, STATUSES$1, PROBABILITY_LABEL, IMPACT_LABEL, FIELD_COPY;
|
|
2149
|
+
var KIND_LABEL, STATUS_LABEL$1, STATUSES$1, PROBABILITY_LABEL, IMPACT_LABEL, FIELD_COPY, ids$1;
|
|
2116
2150
|
var init_raid = __esmMin((() => {
|
|
2117
2151
|
KIND_LABEL = {
|
|
2118
2152
|
risk: "Risk",
|
|
@@ -2182,6 +2216,7 @@ var init_raid = __esmMin((() => {
|
|
|
2182
2216
|
dueDate: "Decide by"
|
|
2183
2217
|
}
|
|
2184
2218
|
};
|
|
2219
|
+
ids$1 = (v) => Array.isArray(v) ? [...new Set(v.filter((x) => typeof x === "string" && !!x))] : [];
|
|
2185
2220
|
}));
|
|
2186
2221
|
|
|
2187
2222
|
//#endregion
|
|
@@ -5435,6 +5470,85 @@ function freshlyOpened(data, projects, now) {
|
|
|
5435
5470
|
return out.sort((a, b) => (b.state.openedOn ?? "").localeCompare(a.state.openedOn ?? ""));
|
|
5436
5471
|
}
|
|
5437
5472
|
|
|
5473
|
+
//#endregion
|
|
5474
|
+
//#region src/lib/decisions.ts
|
|
5475
|
+
/** A risk counts as come true when someone said so, or when it was turned into an issue. */
|
|
5476
|
+
function cameTrue(risk) {
|
|
5477
|
+
return risk.happened === true || risk.kind === "issue";
|
|
5478
|
+
}
|
|
5479
|
+
/** "the risk “Vendor misses window” comes true", for "Revisit if …". */
|
|
5480
|
+
function revisitText(revisit, data) {
|
|
5481
|
+
if (revisit.kind === "date") return `it's ${md$1(revisit.on)}`;
|
|
5482
|
+
if (revisit.kind === "risk") {
|
|
5483
|
+
const risk = byId(data.raidItems).get(revisit.riskId);
|
|
5484
|
+
return risk ? `“${risk.title}” comes true` : "a risk that's since been removed comes true";
|
|
5485
|
+
}
|
|
5486
|
+
const m = byId(data.milestones).get(revisit.milestoneId);
|
|
5487
|
+
return m ? `“${m.name}” slips past ${md$1(revisit.due)}` : "a milestone that's since been removed slips";
|
|
5488
|
+
}
|
|
5489
|
+
/** Why a decided decision is due another look today, in a sentence, or null while it isn't. */
|
|
5490
|
+
function revisitReason(item, data, today) {
|
|
5491
|
+
const revisit = item.revisitIf;
|
|
5492
|
+
if (item.kind !== "decision" || item.status !== "decided" || !revisit) return null;
|
|
5493
|
+
if (revisit.kind === "date") return today >= revisit.on ? `${md$1(revisit.on)} came, the day set to look again.` : null;
|
|
5494
|
+
if (revisit.kind === "risk") {
|
|
5495
|
+
const risk = byId(data.raidItems).get(revisit.riskId);
|
|
5496
|
+
return risk && cameTrue(risk) ? `The risk “${risk.title}” came true.` : null;
|
|
5497
|
+
}
|
|
5498
|
+
const m = byId(data.milestones).get(revisit.milestoneId);
|
|
5499
|
+
if (!m || m.done) return null;
|
|
5500
|
+
const due = m.dueDate.slice(0, 10);
|
|
5501
|
+
if (due > revisit.due) return `“${m.name}” moved from ${md$1(revisit.due)} to ${md$1(due)}.`;
|
|
5502
|
+
if (due < today) return `“${m.name}” is past its date, ${md$1(due)}.`;
|
|
5503
|
+
return null;
|
|
5504
|
+
}
|
|
5505
|
+
/** The decision that replaced this one, if any. */
|
|
5506
|
+
function supersededBy(data, id) {
|
|
5507
|
+
return data.raidItems.find((r) => r.kind === "decision" && r.supersedesId === id) ?? null;
|
|
5508
|
+
}
|
|
5509
|
+
/** Decided decisions in scope whose "revisit if" has happened, oldest decision first. */
|
|
5510
|
+
function decisionsToRevisit(data, inScope, today) {
|
|
5511
|
+
const out = [];
|
|
5512
|
+
for (const item of data.raidItems) {
|
|
5513
|
+
if (item.kind !== "decision" || !item.revisitIf || !inScope(item)) continue;
|
|
5514
|
+
const reason = revisitReason(item, data, today);
|
|
5515
|
+
if (!reason) continue;
|
|
5516
|
+
const where = item.projectId && byId(data.projects).get(item.projectId)?.name || item.programId && byId(data.programs).get(item.programId)?.name || item.portfolioId && byId(data.portfolios).get(item.portfolioId)?.name || null;
|
|
5517
|
+
out.push({
|
|
5518
|
+
item,
|
|
5519
|
+
reason,
|
|
5520
|
+
where
|
|
5521
|
+
});
|
|
5522
|
+
}
|
|
5523
|
+
return out.sort((a, b) => (a.item.decidedAt ?? a.item.createdAt).localeCompare(b.item.decidedAt ?? b.item.createdAt));
|
|
5524
|
+
}
|
|
5525
|
+
/** The comment a "still stands" leaves, so the thread says it was looked at. */
|
|
5526
|
+
function stillStandsNote(reason) {
|
|
5527
|
+
return reason ? `Looked at again (${reason.replace(/\.$/, "")}): it still stands.` : "Looked at again: it still stands.";
|
|
5528
|
+
}
|
|
5529
|
+
/** Fields a new decision copies from the one it replaces: where it sits, what it touches. */
|
|
5530
|
+
function successorOf(old) {
|
|
5531
|
+
return {
|
|
5532
|
+
kind: "decision",
|
|
5533
|
+
title: old.title,
|
|
5534
|
+
description: old.description,
|
|
5535
|
+
portfolioId: old.portfolioId ?? null,
|
|
5536
|
+
programId: old.programId ?? null,
|
|
5537
|
+
projectId: old.projectId ?? null,
|
|
5538
|
+
ownerId: old.ownerId,
|
|
5539
|
+
taskIds: old.taskIds,
|
|
5540
|
+
riskIds: old.riskIds,
|
|
5541
|
+
supersedesId: old.id,
|
|
5542
|
+
status: "proposed"
|
|
5543
|
+
};
|
|
5544
|
+
}
|
|
5545
|
+
var md$1;
|
|
5546
|
+
var init_decisions = __esmMin((() => {
|
|
5547
|
+
init_indexes();
|
|
5548
|
+
init_date_text();
|
|
5549
|
+
md$1 = (iso) => format(parseIso(iso.slice(0, 10)), "MMM d");
|
|
5550
|
+
}));
|
|
5551
|
+
|
|
5438
5552
|
//#endregion
|
|
5439
5553
|
//#region src/lib/brief.ts
|
|
5440
5554
|
init_date_fns();
|
|
@@ -5445,6 +5559,7 @@ init_work();
|
|
|
5445
5559
|
init_indexes();
|
|
5446
5560
|
init_blockers();
|
|
5447
5561
|
init_clock();
|
|
5562
|
+
init_decisions();
|
|
5448
5563
|
const BRIEF_QUIET = "Nothing late or at risk. Say what shipped — or enjoy the quiet week.";
|
|
5449
5564
|
/** Late tasks worst first, each with what it holds up, so the letter says what to chase. */
|
|
5450
5565
|
function lateLinesOf(data, tasks, now) {
|
|
@@ -5503,14 +5618,18 @@ function isHighRisk(r) {
|
|
|
5503
5618
|
function isMajorIssue(r) {
|
|
5504
5619
|
return r.kind === "issue" && (r.impact != null ? r.impact >= 4 : r.severity === "high");
|
|
5505
5620
|
}
|
|
5506
|
-
/**
|
|
5507
|
-
function
|
|
5508
|
-
|
|
5621
|
+
/** Whether a register item sits inside the brief's portfolio (or anywhere, for all of them). */
|
|
5622
|
+
function inBriefScope(scope) {
|
|
5623
|
+
return (r) => {
|
|
5509
5624
|
if (r.projectId) return scope.projectIds.has(r.projectId);
|
|
5510
5625
|
if (r.programId) return scope.programIds.has(r.programId);
|
|
5511
5626
|
if (r.portfolioId) return scope.portfolioId == null || r.portfolioId === scope.portfolioId;
|
|
5512
5627
|
return scope.portfolioId == null;
|
|
5513
5628
|
};
|
|
5629
|
+
}
|
|
5630
|
+
/** The risks and issues worth a line in the brief: live, and either a high-scored risk or a major issue. */
|
|
5631
|
+
function watchListOf(data, scope, limit = 5) {
|
|
5632
|
+
const inScope = inBriefScope(scope);
|
|
5514
5633
|
const live = data.raidItems.filter((r) => r.kind !== "decision" && isActive(r) && inScope(r) && (r.kind === "risk" && isHighRisk(r) || isMajorIssue(r)));
|
|
5515
5634
|
return sortRaid(live).slice(0, limit).map((item) => ({
|
|
5516
5635
|
item,
|
|
@@ -5546,6 +5665,11 @@ function composeBriefBody(opts) {
|
|
|
5546
5665
|
}
|
|
5547
5666
|
lines.push("");
|
|
5548
5667
|
}
|
|
5668
|
+
if (opts.revisit?.length) {
|
|
5669
|
+
lines.push("Decisions to look at again");
|
|
5670
|
+
for (const d of opts.revisit) lines.push(`• ${d.title}${d.where ? ` (${d.where})` : ""}: ${d.reason}`);
|
|
5671
|
+
lines.push("");
|
|
5672
|
+
}
|
|
5549
5673
|
const done = () => {
|
|
5550
5674
|
if (!opts.shipped?.length) return;
|
|
5551
5675
|
lines.push("Done this week");
|
|
@@ -5790,6 +5914,11 @@ function briefOf(data, opts = {}) {
|
|
|
5790
5914
|
said: saidThisWeek(data, now, portfolioId ? new Set(tasks.flatMap((t) => t.assigneeId ? [t.assigneeId] : [])) : null),
|
|
5791
5915
|
late: lateLinesOf(data, tasks, now),
|
|
5792
5916
|
opened: freshlyOpened(data, projects, now),
|
|
5917
|
+
revisit: decisionsToRevisit(data, inBriefScope({
|
|
5918
|
+
programIds,
|
|
5919
|
+
projectIds,
|
|
5920
|
+
portfolioId
|
|
5921
|
+
}), todayISO(now)),
|
|
5793
5922
|
talk,
|
|
5794
5923
|
milestonesSoon,
|
|
5795
5924
|
milestonesLate,
|
|
@@ -5841,6 +5970,11 @@ function composeBriefBodyFrom(brief, data, workspace, all = false) {
|
|
|
5841
5970
|
project: b.project.name,
|
|
5842
5971
|
who: data.people.find((p) => p.id === b.note.authorId)?.name ?? null,
|
|
5843
5972
|
body: b.note.body
|
|
5973
|
+
})),
|
|
5974
|
+
revisit: brief.revisit.map((d) => ({
|
|
5975
|
+
title: d.item.title,
|
|
5976
|
+
reason: d.reason,
|
|
5977
|
+
where: d.where
|
|
5844
5978
|
}))
|
|
5845
5979
|
});
|
|
5846
5980
|
}
|
|
@@ -7685,6 +7819,7 @@ init_baseline();
|
|
|
7685
7819
|
init_derived_health();
|
|
7686
7820
|
init_date_text();
|
|
7687
7821
|
init_format();
|
|
7822
|
+
init_decisions();
|
|
7688
7823
|
init_swatches();
|
|
7689
7824
|
init_raid();
|
|
7690
7825
|
/** The people of this harbor to pick from, with "Nobody" first when that's allowed. */
|
|
@@ -8511,30 +8646,54 @@ function RaidPanel({ id, width, height, go, active, onClose }) {
|
|
|
8511
8646
|
label: "Score",
|
|
8512
8647
|
value: score != null ? String(score) : ""
|
|
8513
8648
|
}] : [],
|
|
8514
|
-
...item.kind === "decision" ? [
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
8527
|
-
|
|
8528
|
-
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
|
|
8532
|
-
|
|
8533
|
-
|
|
8534
|
-
|
|
8535
|
-
|
|
8649
|
+
...item.kind === "decision" ? [
|
|
8650
|
+
{
|
|
8651
|
+
label: "Decided by",
|
|
8652
|
+
value: person(item.decidedById) ?? "Not yet",
|
|
8653
|
+
edit: {
|
|
8654
|
+
kind: "pick",
|
|
8655
|
+
choices: [{
|
|
8656
|
+
label: "Not yet",
|
|
8657
|
+
value: null
|
|
8658
|
+
}, ...people.map((p) => ({
|
|
8659
|
+
label: p.name,
|
|
8660
|
+
value: p.id
|
|
8661
|
+
}))],
|
|
8662
|
+
initial: Math.max(0, people.findIndex((p) => p.id === item.decidedById) + 1),
|
|
8663
|
+
onPick: (v) => up({ decidedById: v })
|
|
8664
|
+
}
|
|
8665
|
+
},
|
|
8666
|
+
{
|
|
8667
|
+
label: "Decided on",
|
|
8668
|
+
value: item.decidedAt ? shortDay(item.decidedAt, today()) : "Not yet",
|
|
8669
|
+
edit: {
|
|
8670
|
+
kind: "date",
|
|
8671
|
+
initial: item.decidedAt ?? null,
|
|
8672
|
+
onSave: (v) => up({ decidedAt: v })
|
|
8673
|
+
}
|
|
8674
|
+
},
|
|
8675
|
+
{
|
|
8676
|
+
label: "Turned down",
|
|
8677
|
+
value: (item.turnedDown ?? "").replace(/\s+/g, " "),
|
|
8678
|
+
edit: {
|
|
8679
|
+
kind: "text",
|
|
8680
|
+
initial: item.turnedDown ?? "",
|
|
8681
|
+
onSave: (v) => up({ turnedDown: v })
|
|
8682
|
+
}
|
|
8683
|
+
},
|
|
8684
|
+
{
|
|
8685
|
+
label: "Look again",
|
|
8686
|
+
value: item.revisitIf ? revisitText(item.revisitIf, store.getState()) : "",
|
|
8687
|
+
edit: {
|
|
8688
|
+
kind: "date",
|
|
8689
|
+
initial: item.revisitIf?.kind === "date" ? item.revisitIf.on : null,
|
|
8690
|
+
onSave: (v) => up({ revisitIf: v ? {
|
|
8691
|
+
kind: "date",
|
|
8692
|
+
on: v
|
|
8693
|
+
} : null })
|
|
8694
|
+
}
|
|
8536
8695
|
}
|
|
8537
|
-
|
|
8696
|
+
] : [],
|
|
8538
8697
|
{
|
|
8539
8698
|
label: copy.dueDate,
|
|
8540
8699
|
value: shortDay(item.dueDate, today()),
|
|
@@ -14721,6 +14880,42 @@ function milestonesRaidSlice({ set, get, write }) {
|
|
|
14721
14880
|
updatedAt: stamp()
|
|
14722
14881
|
} : r) });
|
|
14723
14882
|
},
|
|
14883
|
+
supersedeDecision: (id) => {
|
|
14884
|
+
const old = get().raidItems.find((r) => r.id === id && r.kind === "decision");
|
|
14885
|
+
if (!old) return null;
|
|
14886
|
+
const now = stamp();
|
|
14887
|
+
const row = normalizeRaidItem({
|
|
14888
|
+
...successorOf(old),
|
|
14889
|
+
id: uid("raid"),
|
|
14890
|
+
createdById: get().currentUserId,
|
|
14891
|
+
createdAt: now,
|
|
14892
|
+
updatedAt: now
|
|
14893
|
+
});
|
|
14894
|
+
write({ raidItems: [...get().raidItems.map((r) => r.id === id ? {
|
|
14895
|
+
...r,
|
|
14896
|
+
status: "superseded",
|
|
14897
|
+
updatedAt: now
|
|
14898
|
+
} : r), row] });
|
|
14899
|
+
get().log(`changed a decision — ${old.title}`);
|
|
14900
|
+
return row.id;
|
|
14901
|
+
},
|
|
14902
|
+
decisionStillStands: (id, reason) => {
|
|
14903
|
+
const item = get().raidItems.find((r) => r.id === id);
|
|
14904
|
+
if (!item || item.kind !== "decision") return;
|
|
14905
|
+
const now = stamp();
|
|
14906
|
+
const update = {
|
|
14907
|
+
id: uid("ru"),
|
|
14908
|
+
authorId: get().currentUserId,
|
|
14909
|
+
body: stillStandsNote(reason ?? null),
|
|
14910
|
+
createdAt: now
|
|
14911
|
+
};
|
|
14912
|
+
write({ raidItems: get().raidItems.map((r) => r.id === id ? normalizeRaidItem({
|
|
14913
|
+
...r,
|
|
14914
|
+
revisitIf: null,
|
|
14915
|
+
updates: [...r.updates, update],
|
|
14916
|
+
updatedAt: now
|
|
14917
|
+
}) : r) });
|
|
14918
|
+
},
|
|
14724
14919
|
startPremortem: (projectId) => {
|
|
14725
14920
|
const project = get().projects.find((p) => p.id === projectId);
|
|
14726
14921
|
if (!project || project.premortem) return;
|
|
@@ -14864,6 +15059,7 @@ var init_milestones_raid = __esmMin((() => {
|
|
|
14864
15059
|
init_clock();
|
|
14865
15060
|
init_premortem();
|
|
14866
15061
|
init_raid();
|
|
15062
|
+
init_decisions();
|
|
14867
15063
|
init_raid_history();
|
|
14868
15064
|
init_utils();
|
|
14869
15065
|
init_shared();
|
|
@@ -23121,6 +23317,7 @@ function voyageLog(data, projectId) {
|
|
|
23121
23317
|
guessed: true
|
|
23122
23318
|
};
|
|
23123
23319
|
});
|
|
23320
|
+
const decisions = raid.filter((r) => r.kind === "decision" && r.status !== "proposed").sort((a, b) => (a.decidedAt ?? a.createdAt).localeCompare(b.decidedAt ?? b.createdAt));
|
|
23124
23321
|
const counts = /* @__PURE__ */ new Map();
|
|
23125
23322
|
for (const t of tasks) if (t.status === "done" && t.assigneeId) counts.set(t.assigneeId, (counts.get(t.assigneeId) ?? 0) + 1);
|
|
23126
23323
|
const people = new Map(data.people.map((p) => [p.id, p]));
|
|
@@ -23142,6 +23339,7 @@ function voyageLog(data, projectId) {
|
|
|
23142
23339
|
slips,
|
|
23143
23340
|
risks,
|
|
23144
23341
|
issues: issues.length,
|
|
23342
|
+
decisions,
|
|
23145
23343
|
carriers,
|
|
23146
23344
|
budget: project.budget,
|
|
23147
23345
|
spent: project.spent,
|
|
@@ -23189,6 +23387,10 @@ function voyageStory(log) {
|
|
|
23189
23387
|
out.push(s.waitedOn ? `${late}; it waited on “${s.waitedOn.title}”, done ${md(s.waitedOn.completedAt)}.` : `${late}.`);
|
|
23190
23388
|
}
|
|
23191
23389
|
for (const r of log.risks.filter((x) => x.happened)) out.push(`The risk “${r.risk.title}” came true${r.guessed ? " (an issue names it)" : ""}.`);
|
|
23390
|
+
if (log.decisions.length) {
|
|
23391
|
+
const changed = log.decisions.filter((d) => d.status === "superseded").length;
|
|
23392
|
+
out.push(`${plural(log.decisions.length, "decision was", "decisions were")} written down${changed ? `; ${changed === 1 ? "one was" : `${changed} were`} changed later` : ""}.`);
|
|
23393
|
+
}
|
|
23192
23394
|
const top = log.carriers[0];
|
|
23193
23395
|
if (top && log.carriers.length > 1) out.push(`${top.person.name} closed ${plural(top.closed, "task")}, the most on the crew.`);
|
|
23194
23396
|
else if (top) out.push(`${top.person.name} closed ${plural(top.closed, "task")}.`);
|
|
@@ -23233,6 +23435,7 @@ init_hill();
|
|
|
23233
23435
|
init_raid();
|
|
23234
23436
|
init_priority();
|
|
23235
23437
|
init_premortem();
|
|
23438
|
+
init_decisions();
|
|
23236
23439
|
init_search();
|
|
23237
23440
|
init_derived_health();
|
|
23238
23441
|
init_format();
|
|
@@ -23440,10 +23643,7 @@ function raidOut(data, r) {
|
|
|
23440
23643
|
...text("contingency"),
|
|
23441
23644
|
...text("resolution"),
|
|
23442
23645
|
...text("rationale"),
|
|
23443
|
-
...r.kind === "decision" ? {
|
|
23444
|
-
decidedBy: nameOf(people, r.decidedById),
|
|
23445
|
-
decidedOn: dayOf(r.decidedAt)
|
|
23446
|
-
} : {},
|
|
23646
|
+
...r.kind === "decision" ? decisionOut(data, r, people) : {},
|
|
23447
23647
|
...text("notes"),
|
|
23448
23648
|
updates: r.updates.map((u) => ({
|
|
23449
23649
|
by: nameOf(people, u.authorId) ?? "Someone",
|
|
@@ -23452,6 +23652,39 @@ function raidOut(data, r) {
|
|
|
23452
23652
|
}))
|
|
23453
23653
|
};
|
|
23454
23654
|
}
|
|
23655
|
+
/** A decision's own fields: who and when, what was turned down, what it touches, when to look again. */
|
|
23656
|
+
function decisionOut(data, r, people) {
|
|
23657
|
+
const tasks = byId(data.tasks);
|
|
23658
|
+
const items = byId(data.raidItems);
|
|
23659
|
+
const touches = [...(r.taskIds ?? []).flatMap((id) => {
|
|
23660
|
+
const t = tasks.get(id);
|
|
23661
|
+
if (!t) return [];
|
|
23662
|
+
const key = taskKey(t, data.projects.find((p) => p.id === t.projectId));
|
|
23663
|
+
return [key ? `${key} ${t.title}` : t.title];
|
|
23664
|
+
}), ...(r.riskIds ?? []).flatMap((id) => {
|
|
23665
|
+
const x = items.get(id);
|
|
23666
|
+
return x ? [`${KIND_LABEL[x.kind]}: ${x.title}`] : [];
|
|
23667
|
+
})];
|
|
23668
|
+
const older = r.supersedesId ? items.get(r.supersedesId) : null;
|
|
23669
|
+
const newer = supersededBy(data, r.id);
|
|
23670
|
+
const due = revisitReason(r, data, todayISO());
|
|
23671
|
+
return {
|
|
23672
|
+
decidedBy: nameOf(people, r.decidedById),
|
|
23673
|
+
decidedOn: dayOf(r.decidedAt),
|
|
23674
|
+
...r.turnedDown?.trim() ? { turnedDown: r.turnedDown.trim() } : {},
|
|
23675
|
+
...touches.length ? { touches } : {},
|
|
23676
|
+
...r.revisitIf ? { revisitIf: revisitText(r.revisitIf, data) } : {},
|
|
23677
|
+
...due ? { lookAgainNow: due } : {},
|
|
23678
|
+
...older ? { replaces: {
|
|
23679
|
+
id: older.id,
|
|
23680
|
+
title: older.title
|
|
23681
|
+
} } : {},
|
|
23682
|
+
...newer ? { replacedBy: {
|
|
23683
|
+
id: newer.id,
|
|
23684
|
+
title: newer.title
|
|
23685
|
+
} } : {}
|
|
23686
|
+
};
|
|
23687
|
+
}
|
|
23455
23688
|
const tool = (t) => t;
|
|
23456
23689
|
/** A project's notes for later, as get_project gives them: sealed ones without their text unless they're the caller's. */
|
|
23457
23690
|
function notesOf(ctx, p) {
|
|
@@ -24089,6 +24322,12 @@ const HARBOR_TOOLS = [
|
|
|
24089
24322
|
cameTrue: r.happened,
|
|
24090
24323
|
...r.guessed ? { guessed: true } : {}
|
|
24091
24324
|
})),
|
|
24325
|
+
decisions: log.decisions.map((d) => ({
|
|
24326
|
+
title: d.title,
|
|
24327
|
+
on: dayOf(d.decidedAt),
|
|
24328
|
+
...d.rationale?.trim() ? { why: d.rationale.trim() } : {},
|
|
24329
|
+
...d.status === "superseded" ? { changedLater: true } : {}
|
|
24330
|
+
})),
|
|
24092
24331
|
closedBy: log.carriers.map((c) => ({
|
|
24093
24332
|
who: c.person.name,
|
|
24094
24333
|
tasks: c.closed
|