create-pmos 0.133.0 → 0.135.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/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/bin/notes-replay.js +235 -0
- package/workspace/c3-briefing-test.js +130 -0
- package/workspace/c3-foundation-test.js +159 -0
- package/workspace/c3-ib-items-test.js +105 -0
- package/workspace/c3-ib-more-test.js +120 -0
- package/workspace/c3-ib-triage-backlog-test.js +119 -0
- package/workspace/c3-ini-harness.js +66 -0
- package/workspace/c3-initiatives-test.js +140 -0
- package/workspace/c3-one-home-test.js +14 -1
- package/workspace/c3-reports-test.js +57 -18
- package/workspace/c3-settings-test.js +139 -0
- package/workspace/c3-work-test.js +42 -10
- package/workspace/fixtures/one-home-baseline.json +1 -9
- package/workspace/guide/briefing-cards.md +15 -3
- package/workspace/guide/briefing.md +12 -8
- package/workspace/guide/calendar-popup.md +2 -2
- package/workspace/guide/console-calendar.md +39 -10
- package/workspace/guide/index.md +1 -0
- package/workspace/guide/initiative-boards.md +65 -0
- package/workspace/guide/jira.md +3 -1
- package/workspace/guide/layout.md +2 -0
- package/workspace/guide/my-work.md +5 -3
- package/workspace/guide/notes.md +35 -2
- package/workspace/guide/reports-dashboards.md +29 -5
- package/workspace/guide/settings.md +68 -0
- package/workspace/guide/welcome.md +10 -10
- package/workspace/lib/calendar-cli.js +3 -2
- package/workspace/lib/calendar-layers.js +21 -10
- package/workspace/lib/calendar-tidy.js +230 -0
- package/workspace/lib/engine/boards.js +542 -0
- package/workspace/lib/engine/breakdown-ops.js +180 -0
- package/workspace/lib/engine/calendar-import.js +84 -0
- package/workspace/lib/engine/calendar-model.js +185 -0
- package/workspace/lib/engine/card-state.js +92 -0
- package/workspace/lib/engine/derived/derive.js +92 -0
- package/workspace/lib/engine/derived/plan.js +3 -0
- package/workspace/lib/engine/derived/trust.js +3 -0
- package/workspace/lib/engine/doc-health.js +142 -0
- package/workspace/lib/engine/doc-sheet.js +75 -0
- package/workspace/lib/engine/intake.js +78 -0
- package/workspace/lib/engine/outbox.js +86 -0
- package/workspace/lib/engine/plan-forecast.js +342 -0
- package/workspace/lib/engine/promises.js +29 -0
- package/workspace/lib/engine/records/index.js +114 -0
- package/workspace/lib/engine/releases.js +96 -0
- package/workspace/lib/engine/replies.js +99 -0
- package/workspace/lib/engine/reply-contract/reply-contract.js +372 -0
- package/workspace/lib/engine/report-families.js +175 -0
- package/workspace/lib/engine/roadmap-ops.js +150 -0
- package/workspace/lib/engine/sessions-list.js +27 -0
- package/workspace/lib/engine/signal-rules.js +95 -0
- package/workspace/lib/engine/signals/cards.js +57 -0
- package/workspace/lib/engine/signals/detectors.js +370 -0
- package/workspace/lib/engine/signals/facts-cli.js +8 -0
- package/workspace/lib/engine/signals/facts.js +197 -0
- package/workspace/lib/engine/signals/index.js +33 -0
- package/workspace/lib/engine/signals/kinds.js +110 -0
- package/workspace/lib/engine/signals/layout-paths.js +44 -0
- package/workspace/lib/engine/signals/options.js +24 -0
- package/workspace/lib/engine/signals/score.js +72 -0
- package/workspace/lib/engine/signals/select.js +47 -0
- package/workspace/lib/engine/step-planner.js +65 -0
- package/workspace/lib/engine/trust/claim-rules.json +93 -0
- package/workspace/lib/engine/trust/import-calendar.js +21 -0
- package/workspace/lib/engine/trust/import-guard.js +36 -0
- package/workspace/lib/engine/trust/ledger.js +53 -0
- package/workspace/lib/engine/trust/outbox.js +45 -0
- package/workspace/lib/engine/trust/plan.js +5 -0
- package/workspace/lib/engine/trust/trust.js +21 -0
- package/workspace/lib/engine/truth-table.js +57 -0
- package/workspace/lib/engine/workspace-lib.js +24 -0
- package/workspace/lib/factory-join.js +1 -1
- package/workspace/lib/facts-async.js +4 -4
- package/workspace/lib/headless.js +95 -0
- package/workspace/lib/import-overlay.js +2 -1
- package/workspace/lib/jira-status.js +30 -3
- package/workspace/lib/notes-audio.js +28 -9
- package/workspace/lib/notes-capture.js +136 -0
- package/workspace/lib/notes-final.js +38 -13
- package/workspace/lib/notes-live-guard.js +117 -0
- package/workspace/lib/notes-replay.js +125 -0
- package/workspace/lib/notes-screen.js +180 -0
- package/workspace/lib/notes-whisper.js +139 -16
- package/workspace/lib/notes.js +4 -2
- package/workspace/lib/outbox-store.js +10 -6
- package/workspace/lib/pmos-verbs.js +3 -1
- package/workspace/lib/proactive-cli.js +7 -0
- package/workspace/lib/proactive.js +15 -2
- package/workspace/lib/repo-script.js +36 -0
- package/workspace/lib/report-dashboards.js +5 -2
- package/workspace/lib/report-frame.js +122 -0
- package/workspace/lib/report-schedules.js +113 -0
- package/workspace/lib/route-modules.js +9 -2
- package/workspace/lib/routes-boards.js +734 -0
- package/workspace/lib/routes-brief-page.js +13 -4
- package/workspace/lib/routes-briefing.js +22 -11
- package/workspace/lib/routes-c3-init.js +4 -3
- package/workspace/lib/routes-c3-life.js +2 -1
- package/workspace/lib/routes-c3-reports.js +96 -13
- package/workspace/lib/routes-c3-strategy.js +4 -3
- package/workspace/lib/routes-c3-timeline.js +3 -2
- package/workspace/lib/routes-calendar-import.js +3 -2
- package/workspace/lib/routes-calendar-views.js +9 -4
- package/workspace/lib/routes-doc-health.js +2 -1
- package/workspace/lib/routes-doc-sheet.js +2 -1
- package/workspace/lib/routes-forecast.js +2 -1
- package/workspace/lib/routes-hooks.js +3 -2
- package/workspace/lib/routes-initiatives.js +2 -1
- package/workspace/lib/routes-intake.js +2 -1
- package/workspace/lib/routes-notes.js +113 -15
- package/workspace/lib/routes-outbox.js +23 -3
- package/workspace/lib/routes-pm-items.js +19 -6
- package/workspace/lib/routes-records.js +3 -2
- package/workspace/lib/routes-releases.js +2 -1
- package/workspace/lib/routes-report-facts.js +4 -3
- package/workspace/lib/routes-report-families.js +2 -1
- package/workspace/lib/routes-roadmap-ops.js +5 -4
- package/workspace/lib/routes-runs.js +6 -5
- package/workspace/lib/routes-settings.js +51 -0
- package/workspace/lib/routes-signals.js +3 -2
- package/workspace/lib/routes-stage-tools.js +220 -0
- package/workspace/lib/run-answers.js +2 -1
- package/workspace/lib/settings-store.js +226 -0
- package/workspace/lib/stage-tools.js +208 -0
- package/workspace/lib/vendor/scripts/signals/layout-paths.js +2 -1
- package/workspace/lib/vendor/scripts/workspace-lib.js +24 -0
- package/workspace/lib/work-evidence.js +73 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/briefing-deck.js +63 -17
- package/workspace/public/pm/briefing-model.js +36 -7
- package/workspace/public/pm/c3-ambient.js +100 -0
- package/workspace/public/pm/c3-brief-needs.js +26 -6
- package/workspace/public/pm/c3-brief-pulse.js +12 -4
- package/workspace/public/pm/c3-brief-today.js +56 -21
- package/workspace/public/pm/c3-brief.js +2 -1
- package/workspace/public/pm/c3-calendar.js +161 -42
- package/workspace/public/pm/c3-calpop.js +5 -5
- package/workspace/public/pm/c3-demo-boards.js +649 -0
- package/workspace/public/pm/c3-demo-brief-page.js +1 -1
- package/workspace/public/pm/c3-demo-cal4.js +3 -2
- package/workspace/public/pm/c3-demo-cal5.js +52 -0
- package/workspace/public/pm/c3-demo-data.js +17 -27
- package/workspace/public/pm/c3-demo-rep3.js +46 -5
- package/workspace/public/pm/c3-demo.js +2 -1
- package/workspace/public/pm/c3-dock.js +1 -1
- package/workspace/public/pm/c3-ib-docs.js +80 -0
- package/workspace/public/pm/c3-ib-initiatives.js +134 -0
- package/workspace/public/pm/c3-ib-items.js +201 -0
- package/workspace/public/pm/c3-ib-releases.js +51 -0
- package/workspace/public/pm/c3-ib-sprints.js +55 -0
- package/workspace/public/pm/c3-ib-triage.js +178 -0
- package/workspace/public/pm/c3-ii-tabs.js +240 -0
- package/workspace/public/pm/c3-ini-records.js +197 -0
- package/workspace/public/pm/c3-ini-timeline.js +278 -0
- package/workspace/public/pm/c3-ini-ui.js +229 -0
- package/workspace/public/pm/c3-ini-work.js +139 -0
- package/workspace/public/pm/c3-initiatives.css +218 -0
- package/workspace/public/pm/c3-initiatives.js +741 -0
- package/workspace/public/pm/c3-plan.js +2 -2
- package/workspace/public/pm/c3-reports.css +47 -47
- package/workspace/public/pm/c3-reports.js +223 -185
- package/workspace/public/pm/c3-settings.css +74 -0
- package/workspace/public/pm/c3-settings.js +703 -0
- package/workspace/public/pm/c3-shell.js +102 -36
- package/workspace/public/pm/c3-terminal-icons.js +2 -2
- package/workspace/public/pm/c3-terminal.js +2 -2
- package/workspace/public/pm/calendar-views.js +26 -13
- package/workspace/public/pm/components.css +319 -213
- package/workspace/public/pm/console3.css +118 -23
- package/workspace/public/pm/dest-notes.js +26 -4
- package/workspace/public/pm/notes-screen-capture.js +165 -0
- package/workspace/public/pm/notes-screen-view.js +98 -0
- package/workspace/public/pm/panel-trees.js +5 -2
- package/workspace/public/pm/pm-work-view.js +233 -146
- package/workspace/public/pm/shell-bell.js +3 -2
- package/workspace/public/pm/shell-recording.js +148 -17
- package/workspace/public/pm/tokens.css +2 -2
- package/workspace/public/pm/view-notes.js +53 -11
- package/workspace/public/pm/welcome.js +197 -89
- package/workspace/public/welcome.html +6 -4
- package/workspace/server.js +105 -6
- package/workspace/skills/day-check.skill +1 -1
- package/workspace/skills/jira-sync.skill +6 -4
- package/workspace/skills/morning-brief.skill +1 -1
- package/workspace/test-files.json +14 -3
- package/workspace/test-lists/c3-ib-items.json +9 -0
- package/workspace/test-lists/c3-ib-more.json +9 -0
- package/workspace/test-lists/c3-ib-triage-backlog.json +9 -0
- package/workspace/test-lists/c3-initiative-boards.json +6 -0
- package/workspace/test-lists/calendar-tidy-up.json +6 -0
- package/workspace/test-lists/console3-briefing.json +9 -0
- package/workspace/test-lists/console3-foundation.json +9 -0
- package/workspace/test-lists/console3-reports.json +3 -3
- package/workspace/test-lists/meeting-notes.json +12 -4
- package/workspace/test-lists/workspace-headless-jobs.json +5 -0
- package/workspace/test-lists/workspace-package-script-closure.json +6 -0
- package/workspace/lib/vendor/workspace/lib/actor.js +0 -91
- package/workspace/lib/vendor/workspace/lib/board-cache.js +0 -156
- package/workspace/lib/vendor/workspace/lib/console-state.js +0 -295
- package/workspace/lib/vendor/workspace/lib/factory-join.js +0 -158
- package/workspace/lib/vendor/workspace/lib/git-info.js +0 -306
- package/workspace/lib/vendor/workspace/lib/plan-items.js +0 -922
- package/workspace/lib/vendor/workspace/lib/plan-tree.js +0 -419
- package/workspace/lib/vendor/workspace/lib/review-gate.js +0 -39
- package/workspace/lib/vendor/workspace/lib/sink.js +0 -321
- package/workspace/lib/vendor/workspace/lib/state-root.js +0 -587
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Detector registry. Each detector is a pure function over a normalized facts object (see facts.js) that
|
|
3
|
+
// returns signals in the shared schema:
|
|
4
|
+
// {id, page, key, entity, magnitude 0..1, deadlineDays, deadlineSource?, lens?, isNew?, irreversible?, positive?,
|
|
5
|
+
// policyCritical?, promiseMissed?, strategic?, child?, capTag?, resolves?, blockedBy?, shape?, title, facts[]}
|
|
6
|
+
// Ported from prototype/pmos-console-handoff/sample/detectors.js and present.js (branch feat/console3-merge),
|
|
7
|
+
// with the file reads replaced by reads of the facts object. Titles carry computed numbers, never typed ones.
|
|
8
|
+
// Families that the sample reads from fixtures files and that this repo has no reader for yet simply return
|
|
9
|
+
// no signals when the matching facts family is empty.
|
|
10
|
+
|
|
11
|
+
const { slipMag, sig } = require('./score');
|
|
12
|
+
const { DAY, dateMs, fmt } = require('./facts');
|
|
13
|
+
|
|
14
|
+
const REG = [];
|
|
15
|
+
const reg = (page, id, fn) => REG.push({ page, id, fn });
|
|
16
|
+
const s = (o) => sig(o);
|
|
17
|
+
const dd = (a, b) => Math.round((dateMs(a) - dateMs(b)) / DAY);
|
|
18
|
+
const short = (d) => String(d).slice(5, 10);
|
|
19
|
+
const sum = (a, k) => a.reduce((x, y) => x + (k ? y[k] : y), 0);
|
|
20
|
+
const cap = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
|
|
21
|
+
|
|
22
|
+
// ---------- BRIEFING (present.js signals, generalised from one hard-coded initiative to every initiative) ----------
|
|
23
|
+
reg('briefing', 'forecast-slip', (f) => f.inis.filter((i) => i.slip > 0 && i.promised).flatMap((i) => {
|
|
24
|
+
const key = 'replan:' + i.id;
|
|
25
|
+
const meet = i.meeting && f.daysTo(i.meeting.at);
|
|
26
|
+
const out = [s({
|
|
27
|
+
id: 'forecast-slip', page: 'briefing', key, entity: i.id, magnitude: slipMag(i.slip, i.tolerance),
|
|
28
|
+
promiseMissed: i.slip > i.tolerance && i.holders.length > 0,
|
|
29
|
+
deadlineDays: i.meeting ? meet : i.daysToPromise,
|
|
30
|
+
deadlineSource: i.meeting ? `the ${i.meeting.title} at ${String(i.meeting.at).slice(11, 16)}` : 'the promise date',
|
|
31
|
+
lens: 'Plan', strategic: true, isNew: true, shape: 'series',
|
|
32
|
+
title: `${i.title} lands ${i.slip} days late${i.slipFromEdits || i.slipFromAdds ? `: ${i.slipFromEdits} of ${i.slip} come from critical-path slips, ${i.slipFromAdds} from added scope` : ''}`,
|
|
33
|
+
facts: [`forecast ${short(i.forecast)} vs promise ${short(i.promised)}`, `${i.criticalPath.join(' > ') || 'no path'} on the critical path`,
|
|
34
|
+
i.meeting ? `decision needed before ${i.meeting.title}` : `promise in ${Math.round(i.daysToPromise)} days`],
|
|
35
|
+
options: i.options, initiative: i.id, tolerance: i.tolerance,
|
|
36
|
+
})];
|
|
37
|
+
if (i.slipFromEdits) out.push(s({ id: 'critical-path-edits', page: 'briefing', key, entity: i.id, magnitude: slipMag(i.slipFromEdits, i.tolerance), child: true }));
|
|
38
|
+
if (i.adds) out.push(s({ id: 'scope-creep', page: 'briefing', key, entity: i.id, magnitude: Math.min(1, i.adds / 5), child: true }));
|
|
39
|
+
return out;
|
|
40
|
+
}));
|
|
41
|
+
reg('briefing', 'stale-holder', (f) => f.holdersSent.filter((h) => h.truth && h.told).map((h) => {
|
|
42
|
+
const ini = f.inis.find((i) => i.id === h.initiative) || f.inis[0] || { tolerance: 1 };
|
|
43
|
+
const gap = dd(h.truth, h.told);
|
|
44
|
+
return s({
|
|
45
|
+
id: 'stale-holder', page: 'briefing', key: 'comms:' + h.to, entity: h.to, magnitude: slipMag(gap, ini.tolerance),
|
|
46
|
+
promiseMissed: gap > ini.tolerance, irreversible: true,
|
|
47
|
+
deadlineDays: h.dueAt ? f.daysTo(h.dueAt) : 1, deadlineSource: h.dueSource || 'the next report to leadership',
|
|
48
|
+
lens: 'Close', blockedBy: h.blockedBy, isNew: true, shape: 'compare',
|
|
49
|
+
title: `${gap} days apart: ${h.to} holds ${short(h.told)}; the true date is ${short(h.truth)}`,
|
|
50
|
+
facts: [`told ${short(h.told)}, true ${short(h.truth)}`, 'external: needs your update'],
|
|
51
|
+
});
|
|
52
|
+
}).filter((x) => x.magnitude > 0));
|
|
53
|
+
reg('briefing', 'report-blocked', (f) => (f.report && f.report.send && f.report.send !== 'allowed' ? [s({
|
|
54
|
+
id: 'report-blocked', page: 'briefing', key: 'comms:report', entity: 'report', magnitude: 0.6, promiseMissed: true, irreversible: true,
|
|
55
|
+
deadlineDays: f.daysTo(f.report.due), deadlineSource: 'the report due time', lens: 'Close', shape: 'rows',
|
|
56
|
+
title: `Report due ${String(f.report.due).replace('T', ' ')} cannot send: ${f.report.send}`, facts: [f.report.send],
|
|
57
|
+
})] : []));
|
|
58
|
+
reg('briefing', 'doc-conflict', (f) => {
|
|
59
|
+
const conflicts = f.edges.filter((e) => e.state === 'conflict');
|
|
60
|
+
return conflicts.map((e) => {
|
|
61
|
+
const down = new Set(f.edges.filter((x) => x.from === e.from).map((x) => x.to));
|
|
62
|
+
return s({
|
|
63
|
+
id: 'doc-conflict', page: 'briefing', key: 'conflict:' + e.from + '>' + e.to, entity: e.to, magnitude: Math.min(1, (down.size + 1) / 6),
|
|
64
|
+
deadlineDays: 30, lens: 'Unblock', isNew: true, shape: 'graph',
|
|
65
|
+
title: `${e.from} conflicts with ${e.to}${e.reason ? ' (' + e.reason + ')' : ''}; ${down.size} documents build on ${e.from}`, facts: [`${down.size} downstream`],
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
reg('briefing', 'anchor-order', (f) => f.inis.filter((i) => !i.done && (i.parent_kr === null || i.parent_kr === '')).map((i) => s({
|
|
70
|
+
id: 'anchor-order', page: 'briefing', key: 'anchor:' + i.id, entity: i.id, magnitude: 0.5, deadlineDays: 14, lens: 'Plan', policyCritical: true, shape: 'rows',
|
|
71
|
+
title: `${i.title} has no anchor`, facts: ['no KR or health budget', `lane: ${i.lane}`],
|
|
72
|
+
})));
|
|
73
|
+
reg('briefing', 'rubric-weak', (f) => {
|
|
74
|
+
const w = weakest(f.rubric);
|
|
75
|
+
return w && w.score < 0.6 ? [s({
|
|
76
|
+
id: 'rubric-weak', page: 'briefing', key: 'quality', entity: 'rubric', magnitude: 1 - w.score, deadlineDays: 7, lens: 'Close', shape: 'bars',
|
|
77
|
+
title: `Rubric passes at ${total(f.rubric).toFixed(2)} but ${w.dim} scores ${w.score}`, facts: [`threshold ${f.rubric.threshold}`, `${w.dim} ${w.score}`],
|
|
78
|
+
})] : [];
|
|
79
|
+
});
|
|
80
|
+
reg('briefing', 'waiting', (f) => f.waiting.map((w) => s({
|
|
81
|
+
id: 'waiting', page: 'briefing', key: 'waiting:' + w.who, entity: w.who, magnitude: 0.2, deadlineDays: 7, lens: 'Unblock', shape: 'rows',
|
|
82
|
+
title: `Waiting on ${w.who}`, facts: [`waiting since ${short(w.since)}`, w.blocks ? `blocks ${w.blocks}` : ''].filter(Boolean),
|
|
83
|
+
})));
|
|
84
|
+
reg('briefing', 'north-star-win', (f) => northStar(f, 'briefing', 'win'));
|
|
85
|
+
reg('briefing', 'audit-disagree', (f) => (f.audit && f.audit.disagreed > 0 ? [s({
|
|
86
|
+
id: 'audit-disagree', page: 'briefing', key: 'audit', entity: 'trust', magnitude: 0.5, deadlineDays: 14, lens: 'Reflect', shape: 'ring',
|
|
87
|
+
title: `You disagreed with ${f.audit.disagreed} of the last ${f.audit.samples} sampled agent actions`, facts: [`${f.audit.samples} samples`],
|
|
88
|
+
})] : []));
|
|
89
|
+
|
|
90
|
+
// helpers over rubric, runs, KR
|
|
91
|
+
function weakest(rubric) {
|
|
92
|
+
if (!rubric || !rubric.dims || !rubric.dims.length) return null;
|
|
93
|
+
return rubric.dims.reduce((a, b) => (b.score < a.score ? b : a));
|
|
94
|
+
}
|
|
95
|
+
const total = (rubric) => sum(rubric.dims.map((d) => d.w * d.score));
|
|
96
|
+
function northStar(f, page, key) {
|
|
97
|
+
const r = f.runs;
|
|
98
|
+
if (r.length < 2) return [];
|
|
99
|
+
const rt = (x) => x.corrections / Math.max(1, x.outputs);
|
|
100
|
+
const first = rt(r[0]);
|
|
101
|
+
if (!first) return [];
|
|
102
|
+
const imp = 1 - rt(r[r.length - 1]) / first;
|
|
103
|
+
// Only an achieved improvement is a win. A worsening rate is shown as a plain signal, not a win.
|
|
104
|
+
return [s({
|
|
105
|
+
id: 'north-star', page, key, entity: 'agent-runs', magnitude: Math.min(1, Math.abs(imp)), deadlineDays: 60, lens: 'Reflect',
|
|
106
|
+
positive: imp > 0, strategic: true, shape: 'series', series: r.map(rt),
|
|
107
|
+
title: `Human correction rate ${imp > 0 ? 'down' : 'up'} ${Math.round(Math.abs(imp) * 100)}% over ${r.length} runs`,
|
|
108
|
+
facts: [`${first.toFixed(2)} to ${rt(r[r.length - 1]).toFixed(2)} corrections per output`],
|
|
109
|
+
})];
|
|
110
|
+
}
|
|
111
|
+
function krProjection(f) {
|
|
112
|
+
const { kr, metric } = f;
|
|
113
|
+
if (!kr || metric.length < 2) return null;
|
|
114
|
+
const last = metric[metric.length - 1], first = metric[0];
|
|
115
|
+
const toBy = Math.max(1, dd(kr.by, last.at));
|
|
116
|
+
const slope = (last.value - first.value) / Math.max(1, dd(last.at, first.at));
|
|
117
|
+
const proj = +(last.value + slope * toBy).toFixed(2);
|
|
118
|
+
const shortBy = (i) => { const m = metric[i]; const sl = (m.value - first.value) / Math.max(1, dd(m.at, first.at)); return +(kr.target - (m.value + sl * Math.max(1, dd(kr.by, m.at)))).toFixed(2); };
|
|
119
|
+
return { last, slope, proj, short: +(kr.target - proj).toFixed(2), toBy, shortSeries: metric.slice(2).map((_, i) => shortBy(i + 2)) };
|
|
120
|
+
}
|
|
121
|
+
const workLoad = (f) => { const t = f.work.filter((w) => w.lens === 'today'); return { items: t, load: sum(t.map((w) => w.effort_min || 0)) }; };
|
|
122
|
+
|
|
123
|
+
// ---------- CALENDAR ----------
|
|
124
|
+
reg('calendar', 'cal.milestone-status', (f) => f.inis.filter((i) => i.promised && !i.done).flatMap((i) => {
|
|
125
|
+
if (i.status === 'no plan') return [s({ id: 'cal.no-plan:' + i.id, page: 'calendar', key: 'plan:' + i.id, entity: i.id, magnitude: 0.6, deadlineDays: i.daysToPromise, lens: 'Plan', shape: 'rows', title: `${i.title} is promised ${i.promised} but has no plan, so no forecast`, facts: ['promised ' + i.promised, '0 tickets'] })];
|
|
126
|
+
if (['delayed', 'at risk', 'overdue'].includes(i.status)) {
|
|
127
|
+
return [s({
|
|
128
|
+
id: 'cal.milestone:' + i.id, page: 'calendar', key: 'milestone:' + i.id, entity: i.id, magnitude: Math.max(0.2, slipMag(i.slip, i.tolerance)),
|
|
129
|
+
promiseMissed: i.slip > i.tolerance && i.holders.length > 0, deadlineDays: i.daysToPromise, deadlineSource: 'the promise date', shape: 'compare', isNew: true,
|
|
130
|
+
title: `${i.title} is ${i.status}: promised ${i.promised}, forecast ${i.forecast}`, facts: [i.status],
|
|
131
|
+
})];
|
|
132
|
+
}
|
|
133
|
+
return [];
|
|
134
|
+
}));
|
|
135
|
+
reg('calendar', 'cal.day-load', (f) => {
|
|
136
|
+
const { items, load } = workLoad(f);
|
|
137
|
+
return load > f.capFocusMin ? [s({
|
|
138
|
+
id: 'cal.day-load', page: 'calendar', key: 'load:today', entity: 'today', magnitude: Math.min(1, (load - f.capFocusMin) / f.capFocusMin), deadlineDays: 0, lens: 'Plan', shape: 'ring',
|
|
139
|
+
title: `Today holds ${Math.round(load / 6) / 10}h of work for ${Math.round(f.capFocusMin / 6) / 10}h of focus time`, facts: [items.length + ' items in Today'],
|
|
140
|
+
})] : [];
|
|
141
|
+
});
|
|
142
|
+
reg('calendar', 'cal.due-cluster', (f) => {
|
|
143
|
+
const by = {};
|
|
144
|
+
f.work.filter((w) => w.due && w.lens !== 'done').forEach((w) => (by[w.due] = by[w.due] || []).push(w));
|
|
145
|
+
return Object.entries(by).filter(([, v]) => v.length >= 3).map(([d, v]) => s({
|
|
146
|
+
id: 'cal.due-cluster:' + d, page: 'calendar', key: 'dues:' + d, entity: d, magnitude: Math.min(1, v.length / 6), deadlineDays: f.daysTo(d), capTag: 'Just in', shape: 'rows',
|
|
147
|
+
title: `${v.length} items fall due on ${d}`, facts: v.map((x) => x.title),
|
|
148
|
+
}));
|
|
149
|
+
});
|
|
150
|
+
reg('calendar', 'cal.kr-deviation', (f) => {
|
|
151
|
+
const p = krProjection(f);
|
|
152
|
+
return p && p.short > 0 ? [s({
|
|
153
|
+
id: 'cal.kr-deviation', page: 'calendar', key: 'kr', entity: f.kr.title, magnitude: Math.min(1, p.short / Math.max(0.0001, f.kr.target - f.kr.baseline)), deadlineDays: f.daysTo(f.kr.by),
|
|
154
|
+
strategic: true, lens: 'Plan', shape: 'series',
|
|
155
|
+
title: `${f.kr.metric} is ${p.last.value} against a ${f.kr.target} target; at this pace it ends the period at ${p.proj} (${p.short} short)`,
|
|
156
|
+
facts: ['slope ' + p.slope.toFixed(4) + '/day', 'needs ' + ((f.kr.target - p.last.value) / p.toBy).toFixed(4) + '/day'],
|
|
157
|
+
})] : [];
|
|
158
|
+
});
|
|
159
|
+
reg('calendar', 'cal.pivot-signal', (f) => {
|
|
160
|
+
const p = krProjection(f);
|
|
161
|
+
const ss = p ? p.shortSeries : [];
|
|
162
|
+
return ss.length >= 3 && ss.every((v) => v > 0) && ss[ss.length - 1] >= ss[0] ? [s({
|
|
163
|
+
id: 'cal.pivot-signal', page: 'calendar', key: 'kr', entity: f.kr.title, magnitude: 0.7, deadlineDays: 30, strategic: true, child: true,
|
|
164
|
+
title: `Shortfall has been positive for ${ss.length} checks: ${ss.join(', ')}`, facts: ['pivot watch'],
|
|
165
|
+
})] : [];
|
|
166
|
+
});
|
|
167
|
+
reg('calendar', 'cal.report-due', (f) => (f.report && f.report.send && f.report.send !== 'allowed' ? [s({
|
|
168
|
+
id: 'cal.report-due', page: 'calendar', key: 'report:' + (f.report.id || 'weekly'), entity: 'report', magnitude: 0.6, promiseMissed: true,
|
|
169
|
+
deadlineDays: Math.max(0, f.daysTo(f.report.due)), deadlineSource: 'the report due time', irreversible: true, lens: 'Close', shape: 'rows',
|
|
170
|
+
title: `Report is due ${String(f.report.due).replace('T', ' ')} and cannot send: ${f.report.send}`, facts: [f.report.send],
|
|
171
|
+
})] : []));
|
|
172
|
+
|
|
173
|
+
// ---------- WORK ----------
|
|
174
|
+
reg('work', 'work.overload', (f) => REG.find((d) => d.id === 'cal.day-load').fn(f).map((x) => ({ ...x, page: 'work' })));
|
|
175
|
+
reg('work', 'work.quick-win', (f) => f.work.filter((w) => w.lens === 'today' && w.effort_min <= 20 && w.resolves).map((w) => s({
|
|
176
|
+
id: 'work.quick-win:' + w.item, page: 'work', key: 'quick:' + w.item, entity: w.item, magnitude: 0.5, deadlineDays: f.daysTo(w.due || f.nowDate), capTag: 'Opportunity',
|
|
177
|
+
resolves: w.resolves, lens: 'Close', shape: 'number', title: `"${w.title}" takes ${w.effort_min} minutes and resolves the "${w.resolves}" card`, facts: ['resolves ' + w.resolves],
|
|
178
|
+
})));
|
|
179
|
+
reg('work', 'work.chase-waiting', (f) => f.work.filter((w) => w.lens === 'waiting' && w.due && f.daysTo(w.due) <= 3).map((w) => s({
|
|
180
|
+
id: 'work.chase:' + w.item, page: 'work', key: 'chase:' + w.item, entity: w.item, magnitude: 0.4, deadlineDays: f.daysTo(w.due), capTag: 'Just in', lens: 'Unblock', shape: 'rows',
|
|
181
|
+
title: `"${w.title}" is due ${w.due} and still waiting on ${w.on}`, facts: ['waiting on ' + w.on],
|
|
182
|
+
})));
|
|
183
|
+
reg('work', 'work.untriaged', (f) => {
|
|
184
|
+
const u = f.work.filter((w) => !w.lens);
|
|
185
|
+
if (!u.length) return [];
|
|
186
|
+
let load = workLoad(f).load;
|
|
187
|
+
const sug = (w) => {
|
|
188
|
+
const d = w.due ? f.daysTo(w.due) : null;
|
|
189
|
+
if (d !== null && d <= 0) { load += w.effort_min || 0; return ['Today', 'due today']; }
|
|
190
|
+
if (d !== null && d <= 1) { if (load + (w.effort_min || 0) <= f.capFocusMin) { load += w.effort_min || 0; return ['Today', 'due tomorrow and it fits']; } return ['Week', 'due tomorrow but Today is full']; }
|
|
191
|
+
return d !== null && d <= 7 ? ['Week', 'due within 7 days'] : ['Later', d === null ? 'no due date' : 'due in ' + d + ' days'];
|
|
192
|
+
};
|
|
193
|
+
const rows = u.map((w) => ({ w, s: sug(w) }));
|
|
194
|
+
return [s({
|
|
195
|
+
id: 'work.untriaged', page: 'work', key: 'triage', entity: 'inbox', magnitude: Math.min(1, u.length / 8), deadlineDays: 1, capTag: 'Just in', lens: 'Plan', isNew: true, shape: 'rows',
|
|
196
|
+
title: `${u.length} items are untriaged; suggested: ${rows.map((r) => r.w.title.split(' ').slice(0, 3).join(' ') + ' to ' + r.s[0] + ' (' + r.s[1] + ')').join('; ')}`, facts: rows.map((r) => r.w.item + ' ' + r.s[0]),
|
|
197
|
+
})];
|
|
198
|
+
});
|
|
199
|
+
reg('work', 'work.mis-lensed', (f) => f.work.filter((w) => w.lens === 'later' && w.due && f.daysTo(w.due) <= 1).map((w) => s({
|
|
200
|
+
id: 'work.mis-lensed:' + w.item, page: 'work', key: 'lens:' + w.item, entity: w.item, magnitude: 0.5, deadlineDays: f.daysTo(w.due), capTag: 'Just in',
|
|
201
|
+
title: `"${w.title}" is parked in Later but due ${w.due}`, facts: [],
|
|
202
|
+
})));
|
|
203
|
+
|
|
204
|
+
// ---------- INITIATIVES: lifecycle lenses ----------
|
|
205
|
+
reg('discover', 'discover.untraced-evidence', (f) => f.hyps.flatMap((h) => {
|
|
206
|
+
const miss = h.evidence.filter((e) => !f.docs[e.src]);
|
|
207
|
+
const tot = sum(h.evidence.map((e) => e.w)) || 1;
|
|
208
|
+
return miss.length ? [s({
|
|
209
|
+
id: 'discover.untraced:' + h.slug, page: 'discover', key: 'hyp:' + h.slug, entity: h.slug, magnitude: sum(miss.map((e) => e.w)) / tot, deadlineDays: 14, shape: 'rows',
|
|
210
|
+
title: `"${h.title}": ${miss.length} of ${h.evidence.length} pieces of evidence point to a missing source (${miss.map((e) => e.src).join(', ')})`,
|
|
211
|
+
facts: ['net ' + sum(h.evidence.map((e) => (e.dir === 'supports' ? e.w : -e.w)))],
|
|
212
|
+
})] : [];
|
|
213
|
+
}));
|
|
214
|
+
reg('discover', 'discover.stale-evidence', (f) => Object.entries(f.docs).filter(([, d]) => ['Research', 'Benchmark'].includes(d.type) && d.stale_after && d.stale_after < f.nowDate).map(([id, d]) => s({
|
|
215
|
+
id: 'discover.stale:' + id, page: 'discover', key: 'stale:' + id, entity: id, magnitude: 0.5, deadlineDays: 7, shape: 'number', title: `${d.title} went stale on ${d.stale_after}`, facts: ['rev ' + d.rev],
|
|
216
|
+
})));
|
|
217
|
+
reg('discover', 'discover.open-hypothesis', (f) => f.hyps.filter((h) => { const n = sum(h.evidence.map((e) => (e.dir === 'supports' ? e.w : -e.w))); return n < 3 && n > -2; }).map((h) => s({
|
|
218
|
+
id: 'discover.open:' + h.slug, page: 'discover', key: 'hyp:' + h.slug, entity: h.slug, magnitude: 0.4, deadlineDays: 21, child: true, title: `"${h.title}" is still open`, facts: [],
|
|
219
|
+
})));
|
|
220
|
+
reg('ideate', 'ideate.quick-win', (f) => f.ideas.filter((i) => i.effort <= 2 && i.score >= 1.5).map((i) => s({
|
|
221
|
+
id: 'ideate.quick:' + i.slug, page: 'ideate', key: 'idea:' + i.slug, entity: i.slug, magnitude: Math.min(1, i.score / 3), deadlineDays: 30, shape: 'rows',
|
|
222
|
+
title: `${i.title}: score ${i.score} for effort ${i.effort}, not on the roadmap`, facts: ['impact ' + i.impact, 'confidence ' + i.confidence],
|
|
223
|
+
})));
|
|
224
|
+
reg('ideate', 'ideate.unanchored', (f) => f.ideas.filter((i) => !i.parent_kr).map((i) => s({
|
|
225
|
+
id: 'ideate.unanchored:' + i.slug, page: 'ideate', key: 'idea:' + i.slug, entity: i.slug, magnitude: 0.5, deadlineDays: 14, title: `${i.title} has no KR anchor`, facts: [],
|
|
226
|
+
})));
|
|
227
|
+
reg('ideate', 'ideate.concentration', (f) => {
|
|
228
|
+
if (f.ideas.length < 4) return [];
|
|
229
|
+
const by = {}; f.ideas.forEach((i) => { by[i.parent_kr] = (by[i.parent_kr] || 0) + 1; });
|
|
230
|
+
const [k, n] = Object.entries(by).sort((a, b) => b[1] - a[1])[0];
|
|
231
|
+
return n === f.ideas.length ? [s({ id: 'ideate.concentration', page: 'ideate', key: 'concentration', entity: k, magnitude: 0.4, deadlineDays: 30, title: `All ${n} ideas aim at one KR (${k}); nothing explores the others`, facts: [] })] : [];
|
|
232
|
+
});
|
|
233
|
+
reg('strategize', 'strategize.kr-deviation', (f) => REG.find((d) => d.id === 'cal.kr-deviation').fn(f).map((x) => ({ ...x, page: 'strategize' })));
|
|
234
|
+
reg('strategize', 'strategize.unanchored', (f) => f.inis.filter((i) => !i.done && !i.parent_kr).map((i) => s({
|
|
235
|
+
id: 'strategize.unanchored:' + i.id, page: 'strategize', key: 'anchor:' + i.id, entity: i.id, magnitude: 0.5, deadlineDays: 14, policyCritical: true, shape: 'rows',
|
|
236
|
+
title: `${i.title} has no KR or health budget`, facts: ['horizon ' + i.horizon],
|
|
237
|
+
})));
|
|
238
|
+
reg('strategize', 'strategize.single-bet', (f) => {
|
|
239
|
+
const now = f.inis.filter((i) => i.horizon === 'now');
|
|
240
|
+
return now.length === 1 && f.inis.length >= 3 ? [s({ id: 'strategize.single-bet', page: 'strategize', key: 'bets', entity: 'portfolio', magnitude: 0.4, deadlineDays: 30, strategic: true, title: `Everything in flight rides on one bet (${now[0].title}); ${f.inis.length - 1} others are Next or Later`, facts: [] })] : [];
|
|
241
|
+
});
|
|
242
|
+
reg('prioritize', 'prioritize.capacity-free', (f) => {
|
|
243
|
+
const inflight = f.inis.filter((i) => i.horizon === 'now').length, CAP = 4;
|
|
244
|
+
return inflight < CAP && f.ideas.length ? [s({
|
|
245
|
+
id: 'prioritize.capacity', page: 'prioritize', key: 'capacity', entity: 'portfolio', magnitude: Math.min(1, (CAP - inflight) / CAP), deadlineDays: 14, shape: 'ring',
|
|
246
|
+
title: `${inflight} of ${CAP} bets in flight; the top idea is ${f.ideas[0].title} (score ${f.ideas[0].score})`, facts: f.ideas.slice(0, 3).map((i) => i.title + ' ' + i.score),
|
|
247
|
+
})] : [];
|
|
248
|
+
});
|
|
249
|
+
reg('prioritize', 'prioritize.score-vs-roadmap', (f) => {
|
|
250
|
+
const onRoad = new Set(f.inis.map((i) => String(i.id).split('/').pop()));
|
|
251
|
+
const top = f.ideas.filter((i) => !onRoad.has(i.slug));
|
|
252
|
+
return top.length && f.inis.some((i) => i.horizon === 'next' && !i.hasPlan) ? [s({
|
|
253
|
+
id: 'prioritize.rank', page: 'prioritize', key: 'capacity', entity: 'portfolio', magnitude: 0.4, deadlineDays: 14, child: true,
|
|
254
|
+
title: `${top[0].title} (score ${top[0].score}) outranks work already placed in Next that has no plan`, facts: [],
|
|
255
|
+
})] : [];
|
|
256
|
+
});
|
|
257
|
+
reg('plan', 'plan.critical-density', (f) => {
|
|
258
|
+
const crit = f.tickets.filter((t) => t.onCriticalPath);
|
|
259
|
+
const share = f.tickets.length ? crit.length / f.tickets.length : 0;
|
|
260
|
+
return share >= 0.5 ? [s({ id: 'plan.density', page: 'plan', key: 'fragile', entity: 'plan', magnitude: share, deadlineDays: 14, shape: 'bars', title: `${crit.length} of ${f.tickets.length} tickets are on the critical path: any slip moves the date`, facts: crit.map((t) => t.key) })] : [];
|
|
261
|
+
});
|
|
262
|
+
reg('plan', 'plan.long-ticket', (f) => {
|
|
263
|
+
const L = f.tickets.filter((t) => t.dur > 10 && t.status !== 'done');
|
|
264
|
+
return L.length ? [s({ id: 'plan.long', page: 'plan', key: 'split', entity: 'plan', magnitude: Math.min(1, Math.max(...L.map((t) => t.dur)) / 30), deadlineDays: 7, shape: 'bars', title: `${L.length} open tickets run over 10 days (${L.map((t) => t.key + ' ' + t.dur + 'd').join(', ')}); long tickets hide slips, so split them`, facts: L.map((t) => t.key + ' owner ' + t.owner) })] : [];
|
|
265
|
+
});
|
|
266
|
+
reg('plan', 'plan.unplanned', (f) => f.inis.filter((i) => !i.hasPlan && ['now', 'next'].includes(i.horizon)).map((i) => s({
|
|
267
|
+
id: 'plan.unplanned:' + i.id, page: 'plan', key: 'plan:' + i.id, entity: i.id, magnitude: 0.5, deadlineDays: i.promised ? i.daysToPromise : 60,
|
|
268
|
+
title: `${i.title} is in ${i.horizon} with no work breakdown`, facts: ['promised ' + (i.promised || 'none')],
|
|
269
|
+
})));
|
|
270
|
+
reg('monitor', 'monitor.slip', (f) => f.inis.filter((i) => i.slip > 0).map((i) => s({
|
|
271
|
+
id: 'monitor.slip:' + i.id, page: 'monitor', key: 'slip:' + i.id, entity: i.id, magnitude: slipMag(i.slip, i.tolerance), promiseMissed: i.slip > i.tolerance && i.holders.length > 0,
|
|
272
|
+
deadlineDays: i.daysToPromise, deadlineSource: 'the promise date', isNew: true, shape: 'series', title: `Forecast ${i.forecast} is ${i.slip} days past the promise`, facts: [],
|
|
273
|
+
})));
|
|
274
|
+
reg('monitor', 'monitor.scope', (f) => f.inis.filter((i) => i.tripScope && i.tripScope !== 'ok').map((i) => s({
|
|
275
|
+
id: 'monitor.scope:' + i.id, page: 'monitor', key: 'slip:' + i.id, entity: i.id, magnitude: Math.min(1, i.adds / 5), deadlineDays: 14, child: true, title: `${i.adds} scope additions since baseline (${i.tripScope})`, facts: [],
|
|
276
|
+
})));
|
|
277
|
+
reg('monitor', 'monitor.gate-order', (f) => f.inis.filter((i) => i.gateViolations.length).map((i) => s({
|
|
278
|
+
id: 'monitor.gate:' + i.id, page: 'monitor', key: 'gate:' + i.id, entity: i.id, magnitude: 0.5, deadlineDays: 14, policyCritical: true, shape: 'rows',
|
|
279
|
+
title: `${i.title}: ${i.gateViolations[0].gate} passed before an earlier gate`, facts: [],
|
|
280
|
+
})));
|
|
281
|
+
reg('monitor', 'monitor.doc-health', (f) => Object.entries(f.docs).filter(([, d]) => d.exists !== false && d.checks).flatMap(([id, d]) => {
|
|
282
|
+
const vals = Object.values(d.checks), n = vals.filter(Boolean).length;
|
|
283
|
+
return n <= vals.length / 2 ? [s({
|
|
284
|
+
id: 'monitor.health:' + id, page: 'monitor', key: 'doc:' + id, entity: id, magnitude: 1 - n / vals.length, deadlineDays: 14, isNew: true, shape: 'rows',
|
|
285
|
+
title: `${d.title}: ${n} of ${vals.length} checks pass (${Object.entries(d.checks).filter(([, v]) => !v).map(([k]) => k).join(', ')})`, facts: [],
|
|
286
|
+
})] : [];
|
|
287
|
+
}));
|
|
288
|
+
reg('evaluate', 'evaluate.rubric-weak', (f) => REG.find((d) => d.id === 'rubric-weak').fn(f).map((x) => ({ ...x, page: 'evaluate', key: 'rubric' })));
|
|
289
|
+
reg('evaluate', 'evaluate.calibration', (f) => (f.calibration && f.calibration.verdict !== 'calibrated' ? [s({
|
|
290
|
+
id: 'evaluate.calibration', page: 'evaluate', key: 'calibration', entity: 'confidence', magnitude: 0.5, deadlineDays: 30, shape: 'bars',
|
|
291
|
+
title: `Confidence figures are ${f.calibration.verdict}`, facts: ['brier ' + f.calibration.brier],
|
|
292
|
+
})] : []));
|
|
293
|
+
reg('evaluate', 'evaluate.north-star', (f) => northStar(f, 'evaluate', 'win'));
|
|
294
|
+
reg('evaluate', 'evaluate.retro-missing', (f) => f.inis.filter((i) => i.retroMissing).map((i) => s({
|
|
295
|
+
id: 'evaluate.retro:' + i.id, page: 'evaluate', key: 'retro:' + i.id, entity: i.id, magnitude: 0.4, deadlineDays: 14, title: `${i.id} was accepted with no retrospective`, facts: [],
|
|
296
|
+
})));
|
|
297
|
+
|
|
298
|
+
// ---------- INITIATIVES: views ----------
|
|
299
|
+
reg('roadmap', 'roadmap.overcommit', (f) => {
|
|
300
|
+
const n = f.inis.filter((i) => i.horizon === 'now').length;
|
|
301
|
+
return n > 4 ? [s({ id: 'roadmap.overcommit', page: 'roadmap', key: 'cap', entity: 'portfolio', magnitude: Math.min(1, (n - 4) / 4), deadlineDays: 7, title: `${n} bets in Now against a capacity of 4`, facts: [] })] : [];
|
|
302
|
+
});
|
|
303
|
+
reg('roadmap', 'roadmap.unanchored', (f) => REG.find((d) => d.id === 'strategize.unanchored').fn(f).map((x) => ({ ...x, page: 'roadmap' })));
|
|
304
|
+
reg('roadmap', 'roadmap.no-forecast', (f) => f.inis.filter((i) => ['now', 'next'].includes(i.horizon) && !i.hasPlan).map((i) => s({
|
|
305
|
+
id: 'roadmap.no-forecast:' + i.id, page: 'roadmap', key: 'plan:' + i.id, entity: i.id, magnitude: 0.4, deadlineDays: 30, title: `${i.title} sits in ${i.horizon} with no forecast date`, facts: [],
|
|
306
|
+
})));
|
|
307
|
+
const lateTickets = (f) => f.tickets.filter((t) => t.status !== 'done' && t.end && dateMs(t.end) < dateMs(f.nowDate));
|
|
308
|
+
reg('tickets', 'tickets.late', (f) => lateTickets(f).map((t) => s({
|
|
309
|
+
id: 'tickets.late:' + t.key, page: 'tickets', key: 'late:' + t.key, entity: t.key, magnitude: 0.5, deadlineDays: 0, title: `${t.key} should have finished ${t.end} and is ${t.status || 'not started'}`, facts: [],
|
|
310
|
+
})));
|
|
311
|
+
reg('tickets', 'tickets.blocked', (f) => {
|
|
312
|
+
const st = Object.fromEntries(f.tickets.map((t) => [t.key, t.status]));
|
|
313
|
+
return f.tickets.filter((t) => t.status === 'doing' && (t.deps || []).some((d) => st[d] !== 'done')).map((t) => s({
|
|
314
|
+
id: 'tickets.blocked:' + t.key, page: 'tickets', key: 'blocked:' + t.key, entity: t.key, magnitude: 0.6, deadlineDays: 3,
|
|
315
|
+
title: `${t.key} is in progress but waits on ${t.deps.filter((d) => st[d] !== 'done').join(', ')}`, facts: [],
|
|
316
|
+
}));
|
|
317
|
+
});
|
|
318
|
+
reg('tickets', 'tickets.unowned', (f) => f.tickets.filter((t) => !t.owner && t.status !== 'done').map((t) => s({
|
|
319
|
+
id: 'tickets.unowned:' + t.key, page: 'tickets', key: 'owner:' + t.key, entity: t.key, magnitude: 0.5, deadlineDays: 7, title: `${t.key} has no owner`, facts: [],
|
|
320
|
+
})));
|
|
321
|
+
reg('tickets', 'tickets.agent-proposed', (f) => {
|
|
322
|
+
const open = f.tickets.filter((t) => t.origin && String(t.origin).startsWith('claude-code/') && !t.edits && t.status !== 'done');
|
|
323
|
+
return open.length ? [s({ id: 'tickets.agent-proposed', page: 'tickets', key: 'proposals', entity: 'agent', magnitude: Math.min(1, open.length / 5), deadlineDays: 3, isNew: true, shape: 'rows', title: `${open.length} agent-added tickets are open: ${open.map((t) => t.key).join(', ')}`, facts: open.map((t) => t.key + ' ' + (t.pts || 0) + 'pts') })] : [];
|
|
324
|
+
});
|
|
325
|
+
reg('documents', 'documents.coverage', (f) => {
|
|
326
|
+
const miss = Object.entries(f.docs).filter(([, d]) => d.required && d.exists === false).map(([id, d]) => d.title || id);
|
|
327
|
+
const gone = f.edges.filter((e) => e.state === 'missing').map((e) => e.from);
|
|
328
|
+
const all = miss.concat(gone);
|
|
329
|
+
return all.length ? [s({ id: 'documents.coverage', page: 'documents', key: 'coverage', entity: 'documents', magnitude: 0.5, deadlineDays: 14, shape: 'rows', title: `${all.length} required document${all.length > 1 ? 's' : ''} missing: ${all.join(', ')}`, facts: [] })] : [];
|
|
330
|
+
});
|
|
331
|
+
reg('documents', 'documents.conflicts', (f) => f.edges.filter((e) => e.state === 'conflict').map((e) => s({
|
|
332
|
+
id: 'documents.conflict:' + e.from + '>' + e.to, page: 'documents', key: 'conflict:' + e.from + '>' + e.to, entity: e.to, magnitude: 0.67, deadlineDays: 30, isNew: true, shape: 'graph',
|
|
333
|
+
title: `${e.from} conflicts with ${e.to}${e.reason ? ' (' + e.reason + ')' : ''}`, facts: [],
|
|
334
|
+
})));
|
|
335
|
+
reg('documents', 'documents.stale', (f) => Object.entries(f.docs).filter(([, d]) => d.stale_after && d.stale_after < f.nowDate && d.exists !== false).map(([id, d]) => s({
|
|
336
|
+
id: 'documents.stale:' + id, page: 'documents', key: 'stale:' + id, entity: id, magnitude: 0.4, deadlineDays: 7, title: `${d.title} is past its review date ${d.stale_after}`, facts: [],
|
|
337
|
+
})));
|
|
338
|
+
|
|
339
|
+
// ---------- REPORTS ----------
|
|
340
|
+
reg('reports', 'reports.claims-blocked', (f) => {
|
|
341
|
+
const bad = (f.report && f.report.blockedClaims) || [];
|
|
342
|
+
return bad.length ? [s({
|
|
343
|
+
id: 'reports.claims', page: 'reports', key: 'report', entity: 'report', magnitude: Math.min(1, bad.length / Math.max(1, f.report.claimCount || bad.length) + 0.2), promiseMissed: true, deadlineDays: 1,
|
|
344
|
+
deadlineSource: 'the report due time', irreversible: true, shape: 'rows', title: `Report: ${bad.length} of ${f.report.claimCount || bad.length} claims block sending`, facts: bad.slice(0, 3).map((x) => String(x).slice(0, 60)),
|
|
345
|
+
})] : [];
|
|
346
|
+
});
|
|
347
|
+
reg('reports', 'reports.audience-leak', (f) => (f.report && f.report.ticketKeyLeaks > 0 && f.report.audience === 'leadership' ? [s({
|
|
348
|
+
id: 'reports.leak', page: 'reports', key: 'report', entity: 'report', magnitude: 0.3, deadlineDays: 1, child: true, title: `${f.report.ticketKeyLeaks} section(s) use ticket keys, which leadership does not read`, facts: [],
|
|
349
|
+
})] : []));
|
|
350
|
+
reg('reports', 'reports.cadence', (f) => (f.report && f.report.due ? [s({
|
|
351
|
+
id: 'reports.cadence', page: 'reports', key: 'report', entity: 'report', magnitude: 0.3, deadlineDays: Math.max(0, f.daysTo(f.report.due)), child: true, title: `Weekly digest is due ${String(f.report.due).replace('T', ' ')}`, facts: [],
|
|
352
|
+
})] : []));
|
|
353
|
+
reg('reports', 'reports.holders', (f) => f.holdersSent.filter((h) => h.truth && h.told && dd(h.truth, h.told) !== 0).map((h) => {
|
|
354
|
+
const ini = f.inis.find((i) => i.id === h.initiative) || f.inis[0] || { tolerance: 1 };
|
|
355
|
+
const g = dd(h.truth, h.told);
|
|
356
|
+
return s({
|
|
357
|
+
id: 'reports.holder:' + h.to, page: 'reports', key: 'holders', entity: h.to, magnitude: slipMag(g, ini.tolerance), promiseMissed: g > ini.tolerance, deadlineDays: 1,
|
|
358
|
+
deadlineSource: 'the next report to leadership', irreversible: true, isNew: true, shape: 'compare', title: `${h.to} holds ${h.told} for ${h.subject}; the true date is ${h.truth}`, facts: [g + ' days apart'],
|
|
359
|
+
});
|
|
360
|
+
}));
|
|
361
|
+
|
|
362
|
+
function run(facts, pages) {
|
|
363
|
+
const want = pages === 'all' || !pages ? null : Array.isArray(pages) ? pages : [pages];
|
|
364
|
+
const out = [];
|
|
365
|
+
for (const d of REG) if (!want || want.includes(d.page)) { const r = d.fn(facts) || []; out.push(...r); }
|
|
366
|
+
return out;
|
|
367
|
+
}
|
|
368
|
+
const pagesList = () => [...new Set(REG.map((d) => d.page))];
|
|
369
|
+
|
|
370
|
+
module.exports = { REG, run, pages: pagesList, count: () => REG.length, weakest, total, krProjection, fmt };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// facts-cli.js — readRepoFacts as a child process: `node facts-cli.js <root> [today]` prints the facts as JSON.
|
|
3
|
+
// readRepoFacts runs git once per initiative (minutes on ~300 records) and is synchronous, so a server that called it in its
|
|
4
|
+
// own process would stop answering for that long. workspace/lib/facts-async.js runs this file instead.
|
|
5
|
+
const { readRepoFacts } = require('./facts.js');
|
|
6
|
+
const root = process.argv[2];
|
|
7
|
+
const today = process.argv[3] || '';
|
|
8
|
+
process.stdout.write(JSON.stringify(readRepoFacts(root, today ? { today } : {})));
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// The facts boundary of the signal engine. Detectors read ONE plain facts object and nothing else.
|
|
3
|
+
//
|
|
4
|
+
// normalizeFacts(raw) fills every family with a safe default and derives slip, tolerance and days-to helpers.
|
|
5
|
+
// It is pure. readRepoFacts(root) is the only function that touches the repo, and the tests do not use it.
|
|
6
|
+
//
|
|
7
|
+
// WIRED TO REAL DATA (readRepoFacts): initiative records (id, title, lane, anchor `parent_kr`, stage, accepted,
|
|
8
|
+
// horizon, `promised`, `planned_days`, `slip_tolerance_days`) from planning/initiatives/*.md through
|
|
9
|
+
// scripts/workflow-state.js listInitiatives and computeWorkflow, which also supply the gate-order
|
|
10
|
+
// violations (`gate_failures`). Work-item status per item comes from the repo's work-item ledger and its plan items (see layout-paths.js).
|
|
11
|
+
//
|
|
12
|
+
// ALSO WIRED (when the files exist): ideas, hypotheses, decisions and the KR metric with its readings, from
|
|
13
|
+
// the repo's records folder through scripts/records (layout-paths.js: planning/records, or pmos/state/records in a tenant).
|
|
14
|
+
//
|
|
15
|
+
// NOT WIRED TO REAL DATA YET (detectors for these families return nothing until a reader fills them; the
|
|
16
|
+
// test fixtures fill them by hand):
|
|
17
|
+
// forecast and plan engine (forecast date, critical path, edits, scope additions, what-if options),
|
|
18
|
+
// promise ledger and who-was-told (holders, sent messages), calendar links (meetings), personal ledger
|
|
19
|
+
// (lenses, effort, due), document edges and document health,
|
|
20
|
+
// rubric dimension scores, calibration, agent-run correction rates, report claims, audit samples,
|
|
21
|
+
// waiting-on records.
|
|
22
|
+
//
|
|
23
|
+
// Raw facts shape (every key optional):
|
|
24
|
+
// now 'YYYY-MM-DDTHH:MM' | 'YYYY-MM-DD', stance 'hold'|'move', capFocusMin,
|
|
25
|
+
// inis[{id,title,promised,forecast,hasPlan,plannedDays,slip_tolerance_days,parent_kr,horizon,lane,done,
|
|
26
|
+
// holders[{who,told}], meeting{title,at}, slipFromEdits, slipFromAdds, adds, criticalPath[], tripScope,
|
|
27
|
+
// options[{id,label,slipAfter,touches[],promiseChange}], gateViolations[{gate}], retroMissing, accepted}],
|
|
28
|
+
// tickets[{key,status,dur,owner,deps[],end,pts,origin,edits}],
|
|
29
|
+
// docs{id:{title,type,rev,stale_after,exists,checks{name:bool}}}, edges[{from,to,state,reason}],
|
|
30
|
+
// work[{item,title,lens,due,on,effort_min,resolves}], kr{title,metric,target,baseline,by}, metric[{at,value}],
|
|
31
|
+
// events[{id,at,end,title,attendees,initiative,sensitivity,label:'reported'}] (imported calendar events),
|
|
32
|
+
// ideas[{slug,title,impact,confidence,effort,parent_kr}], hyps[{slug,title,evidence[{src,dir,w}]}],
|
|
33
|
+
// rubric{threshold,dims[{dim,w,score}]}, calibration{verdict,brier,rows[]}, runs[{outputs,corrections}],
|
|
34
|
+
// report{due,blockedClaims[],claimCount,ticketKeyLeaks,audience}, holdersSent[{to,subject,told,truth}],
|
|
35
|
+
// audit{disagreed,samples}, waiting[{who,since,blocks}].
|
|
36
|
+
|
|
37
|
+
const fs = require('fs');
|
|
38
|
+
const path = require('path');
|
|
39
|
+
const { defaultTolerance } = require('./score');
|
|
40
|
+
|
|
41
|
+
const DAY = 864e5;
|
|
42
|
+
const dateMs = (s) => Date.parse(String(s).slice(0, 10) + 'T00:00:00Z');
|
|
43
|
+
const ms = (s) => (String(s).includes('T') ? Date.parse(String(s).length === 16 ? s + ':00Z' : s) : dateMs(s));
|
|
44
|
+
const fmt = (m) => new Date(m).toISOString().slice(0, 10);
|
|
45
|
+
|
|
46
|
+
function temporalStatus({ promised, forecast, hasPlan, now, tol, done }) {
|
|
47
|
+
if (done) return 'done';
|
|
48
|
+
if (!hasPlan || !forecast) return 'no plan';
|
|
49
|
+
if (dateMs(promised) < dateMs(now)) return 'overdue';
|
|
50
|
+
const slip = Math.round((dateMs(forecast) - dateMs(promised)) / DAY);
|
|
51
|
+
if (slip > tol) return 'delayed';
|
|
52
|
+
if (slip > 0) return 'at risk';
|
|
53
|
+
return 'on track';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeFacts(raw, today) {
|
|
57
|
+
const r = raw || {};
|
|
58
|
+
const now = today || r.now || new Date().toISOString().slice(0, 16);
|
|
59
|
+
const nowDate = String(now).slice(0, 10);
|
|
60
|
+
const daysTo = (t) => (t == null ? null : (String(t).includes('T') ? (ms(t) - ms(now)) / DAY : (dateMs(t) - dateMs(nowDate)) / DAY));
|
|
61
|
+
const inis = (r.inis || []).map((i) => {
|
|
62
|
+
const hasPlan = i.hasPlan !== undefined ? i.hasPlan : !!i.forecast;
|
|
63
|
+
const tolerance = i.slip_tolerance_days != null ? Number(i.slip_tolerance_days) : defaultTolerance(i.plannedDays);
|
|
64
|
+
const slip = i.promised && i.forecast ? Math.round((dateMs(i.forecast) - dateMs(i.promised)) / DAY) : 0;
|
|
65
|
+
return {
|
|
66
|
+
slipFromEdits: 0, slipFromAdds: 0, adds: 0, criticalPath: [], options: [], gateViolations: [], holders: [], ...i,
|
|
67
|
+
hasPlan, tolerance, slip,
|
|
68
|
+
daysToPromise: i.promised ? daysTo(i.promised) : null,
|
|
69
|
+
status: temporalStatus({ promised: i.promised, forecast: i.forecast, hasPlan, now: nowDate, tol: tolerance, done: i.done }),
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
return {
|
|
73
|
+
now, nowDate, daysTo, stance: r.stance === 'move' ? 'move' : 'hold', capFocusMin: r.capFocusMin || 255,
|
|
74
|
+
inis, tickets: r.tickets || [], docs: r.docs || {}, edges: r.edges || [], work: r.work || [],
|
|
75
|
+
kr: r.kr || null, metric: r.metric || [],
|
|
76
|
+
ideas: (r.ideas || []).map((i) => ({ ...i, score: +((i.impact * i.confidence) / i.effort).toFixed(2) })).sort((a, b) => b.score - a.score),
|
|
77
|
+
decisions: r.decisions || [], hyps: r.hyps || [], rubric: r.rubric || null, calibration: r.calibration || null, runs: r.runs || [],
|
|
78
|
+
events: r.events || [],
|
|
79
|
+
report: r.report || null, holdersSent: r.holdersSent || [], audit: r.audit || null, waiting: r.waiting || [],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---- real-data reader (not exercised by the tests) ----
|
|
84
|
+
// workflow-state.js exports cleanly (its CLI runs only under require.main), but it is still required inside the
|
|
85
|
+
// function so that importing this module never reads the repo.
|
|
86
|
+
function readRepoFacts(root, opts) {
|
|
87
|
+
const base = path.resolve(root || process.cwd());
|
|
88
|
+
// A product repo (the kit) may not carry scripts/workflow-state.js; fall back to the copy that ships beside this module.
|
|
89
|
+
const own = path.join(base, 'scripts', 'workflow-state.js');
|
|
90
|
+
const ws = require(require('fs').existsSync(own) ? own : path.join(__dirname, '..', 'workflow-state.js'));
|
|
91
|
+
const range = sharedRange(ws, base);
|
|
92
|
+
const inis = [];
|
|
93
|
+
for (const id of ws.listInitiatives(base)) {
|
|
94
|
+
let w;
|
|
95
|
+
try { w = ws.computeWorkflow(id, range ? { root: base, range } : { root: base }); } catch { continue; }
|
|
96
|
+
if (!w || !w.ok) continue;
|
|
97
|
+
const fm = readFrontmatter(path.join(base, w.record || `planning/initiatives/${id}.md`));
|
|
98
|
+
inis.push({
|
|
99
|
+
id, title: w.title || id, lane: w.lane, parent_kr: w.anchor || fm.parent_kr || null, stage: w.stage,
|
|
100
|
+
done: !!w.accepted, horizon: fm.horizon || (w.accepted ? 'done' : 'now'), promised: fm.promised || null,
|
|
101
|
+
plannedDays: fm.planned_days != null ? Number(fm.planned_days) : null,
|
|
102
|
+
slip_tolerance_days: fm.slip_tolerance_days != null ? Number(fm.slip_tolerance_days) : undefined,
|
|
103
|
+
gateViolations: (w.gate_failures || []).map((g) => ({ gate: g.rule || g.gate || String(g) })),
|
|
104
|
+
// a stopped initiative (D70 lifecycle: on-hold, closed, cancelled); the Calendar tidy-up reads it
|
|
105
|
+
...(w.stopped && w.stopped.state ? { stopped: { state: String(w.stopped.state) } } : {}),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
// Ask where this repo keeps its files (the PMOS repo and a kit tenant differ), then read the factory work items and, for any
|
|
109
|
+
// layout, the PM's plan items: a tenant has plan items and no work-items ledger.
|
|
110
|
+
const LP = require('./layout-paths.js');
|
|
111
|
+
const P = LP.pathsFor(base);
|
|
112
|
+
const tickets = readWorkItems(P.workItemsFile);
|
|
113
|
+
try {
|
|
114
|
+
const have = new Set(tickets.map((t) => t.key));
|
|
115
|
+
for (const f of LP.planItemFacts(fs.readFileSync(P.planItemsFile, 'utf8'))) if (!have.has(f.key)) tickets.push(f);
|
|
116
|
+
} catch { /* no plan-items ledger in this repo */ }
|
|
117
|
+
const rec = readRecordFacts(P.recordsDir);
|
|
118
|
+
// Imported calendar events (scripts/calendar-import.js), when the caller names the runtime dir or passes events.
|
|
119
|
+
let events = (opts && opts.events) || [];
|
|
120
|
+
if (!events.length && opts && opts.runtimeDir) {
|
|
121
|
+
try { const CI = require('../calendar-import.js'); events = CI.foldEvents(CI.readLines(path.join(opts.runtimeDir, 'calendar-import', 'events.jsonl'))); } catch { events = []; }
|
|
122
|
+
}
|
|
123
|
+
return normalizeFacts({ inis, tickets, events, ...rec }, opts && opts.today);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ONE commit range for every initiative (workspace-data-freshness R1). computeWorkflow called without a range builds its own
|
|
127
|
+
// for each initiative, and on a range with no commits (HEAD level with main) firstAppearance then walks the history once
|
|
128
|
+
// per artifact path: 78 s for 324 records here, about 90% of it in those walks. The range is built once, and every path ever
|
|
129
|
+
// added is put in its `added` index with the answer the per-path walk gives: the position of its first add when that commit
|
|
130
|
+
// is in the range, else -1 ("already existed before the range"). The range's own adds keep their index. The walk runs with
|
|
131
|
+
// rename detection off, as a path-limited `git log -- <path>` effectively does. workflow-state.js is not changed.
|
|
132
|
+
// null when the range cannot be built; each initiative then builds its own, as before.
|
|
133
|
+
function sharedRange(ws, base) {
|
|
134
|
+
if (typeof ws.commitRange !== 'function') return null;
|
|
135
|
+
let range;
|
|
136
|
+
try { range = ws.commitRange(base); } catch { return null; }
|
|
137
|
+
if (!range || !range.ok || !(range.added instanceof Map)) return range || null;
|
|
138
|
+
let out;
|
|
139
|
+
try {
|
|
140
|
+
out = require('child_process').execFileSync('git', ['-C', base, 'log', '--reverse', '--no-renames', '--diff-filter=A', '--name-only', '--format=%x1e%H'],
|
|
141
|
+
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 512 * 1024 * 1024 });
|
|
142
|
+
} catch { return range; }
|
|
143
|
+
// oldest first, and the FIRST add of a path wins: the walk answers with the first commit that added it
|
|
144
|
+
const added = new Map();
|
|
145
|
+
for (const chunk of out.split('\x1e')) {
|
|
146
|
+
const lines = chunk.split('\n');
|
|
147
|
+
const sha = (lines.shift() || '').trim();
|
|
148
|
+
if (!sha) continue;
|
|
149
|
+
const at = range.index.has(sha) ? range.index.get(sha) : -1;
|
|
150
|
+
for (const l of lines) { const f = l.trim(); if (f && !added.has(f)) added.set(f, at); }
|
|
151
|
+
}
|
|
152
|
+
for (const [f, at] of range.added) added.set(f, at);
|
|
153
|
+
return { ...range, added };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Ideas, hypotheses, decisions and the KR metric from planning/records/*.jsonl (scripts/records). Empty lists when the
|
|
157
|
+
// files do not exist. The KR is the first metric that has a reading, else the first one defined.
|
|
158
|
+
function readRecordFacts(recordsDir) {
|
|
159
|
+
let all;
|
|
160
|
+
try { all = require('../records/index.js').readAll(recordsDir); } catch { return {}; }
|
|
161
|
+
const krs = all['kr-metrics'] || [];
|
|
162
|
+
const k = krs.find((m) => (m.readings || []).length) || krs[0] || null;
|
|
163
|
+
return {
|
|
164
|
+
ideas: (all.ideas || []).map((i) => ({ slug: i.rid, title: i.title, impact: i.impact, confidence: i.confidence, effort: i.effort, parent_kr: i.parent_kr || null, initiative: i.initiative || null })),
|
|
165
|
+
hyps: (all.hypotheses || []).map((h) => ({ slug: h.rid, title: h.title, evidence: Array.isArray(h.evidence) ? h.evidence : [], falsifier: h.falsifier, review_by: h.review_by })),
|
|
166
|
+
decisions: all.decisions || [],
|
|
167
|
+
kr: k ? { title: k.title || k.metric, metric: k.metric, target: k.target, baseline: k.baseline, by: k.by } : null,
|
|
168
|
+
metric: k ? k.readings.map((r) => ({ at: r.at, value: r.value })) : [],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function readFrontmatter(file) {
|
|
173
|
+
let t; try { t = fs.readFileSync(file, 'utf8'); } catch { return {}; }
|
|
174
|
+
const m = t.match(/^---\n([\s\S]*?)\n---/); const o = {};
|
|
175
|
+
if (!m) return o;
|
|
176
|
+
for (const l of m[1].split('\n')) {
|
|
177
|
+
const i = l.indexOf(':'); if (i < 1) continue;
|
|
178
|
+
o[l.slice(0, i).trim()] = l.slice(i + 1).trim().replace(/^["']|["']$/g, '');
|
|
179
|
+
}
|
|
180
|
+
return o;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function readWorkItems(file) {
|
|
184
|
+
let t; try { t = fs.readFileSync(file, 'utf8'); } catch { return []; }
|
|
185
|
+
const items = new Map();
|
|
186
|
+
for (const line of t.split('\n')) {
|
|
187
|
+
if (!line.trim()) continue;
|
|
188
|
+
let e; try { e = JSON.parse(line); } catch { continue; }
|
|
189
|
+
if (!e.item_id) continue;
|
|
190
|
+
const it = items.get(e.item_id) || { key: e.item_id, initiative: e.initiative_id, status: 'todo' };
|
|
191
|
+
if (e.event && e.event !== 'declared') it.status = /done|merged|accepted|passed/.test(e.event) ? 'done' : /start|doing|claim/.test(e.event) ? 'doing' : it.status;
|
|
192
|
+
items.set(e.item_id, it);
|
|
193
|
+
}
|
|
194
|
+
return [...items.values()];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = { DAY, dateMs, ms, fmt, temporalStatus, normalizeFacts, readRepoFacts };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// present(facts, { lens, zoom, today }): facts > detectors > signals > cluster > score > tag > select > anatomy.
|
|
3
|
+
// Returns { cards (the selected cards, in order), folded (count), extraCritical (keys), belowFloor (count), signals }.
|
|
4
|
+
// Options: lens (Plan|Unblock|Close|Reflect|null), zoom (day|week|month), today ('YYYY-MM-DD[THH:MM]'),
|
|
5
|
+
// pages (detector pages, default ['briefing']; 'all' runs every detector), weights, limit, critCap, pinMax,
|
|
6
|
+
// floor (relevance floor on the score, default FLOOR).
|
|
7
|
+
// Relevance before variety: a card shows only when its score is at or above the floor and it has a source the reader can
|
|
8
|
+
// open (a page) and a headline that says what happened. Critical cards always pass. Zero cards is a valid answer.
|
|
9
|
+
|
|
10
|
+
const { normalizeFacts } = require('./facts');
|
|
11
|
+
const detectors = require('./detectors');
|
|
12
|
+
const S = require('./score');
|
|
13
|
+
const sel = require('./select');
|
|
14
|
+
const { buildCard } = require('./cards');
|
|
15
|
+
const kinds = require('./kinds');
|
|
16
|
+
|
|
17
|
+
const FLOOR = 0.15;
|
|
18
|
+
const relevant = (c, floor) => !!c.page && !!String(c.title || '').trim() && (c.tag === 'Critical' || c.score >= floor);
|
|
19
|
+
|
|
20
|
+
function present(rawFacts, opts = {}) {
|
|
21
|
+
const f = normalizeFacts(rawFacts, opts.today);
|
|
22
|
+
const lens = opts.lens === undefined ? null : opts.lens;
|
|
23
|
+
const signals = detectors.run(f, opts.pages || ['briefing']);
|
|
24
|
+
const scored = S.scoreAll(S.cluster(signals), lens, opts.weights);
|
|
25
|
+
const floor = typeof opts.floor === 'number' ? opts.floor : FLOOR;
|
|
26
|
+
const kept = scored.filter((c) => relevant(c, floor));
|
|
27
|
+
const zoomed = sel.inZoom(kept, opts.zoom || 'week');
|
|
28
|
+
const r = sel.select(zoomed, { limit: opts.limit || 5, critCap: opts.critCap || 3, pinMax: opts.pinMax || 2 });
|
|
29
|
+
const ctx = { pinned: r.pinned, lens, stance: f.stance, rubric: f.rubric, holdersOf: (id) => (f.inis.find((i) => i.id === id) || { holders: [] }).holders.length };
|
|
30
|
+
return { cards: r.shown.map((c) => buildCard(c, ctx)), folded: r.folded.length, extraCritical: r.extraCrit.map((c) => c.key), belowFloor: scored.length - kept.length, signals };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { present, FLOOR, relevant, kinds, ...S, select: sel.select, normalizeFacts, detectors };
|