create-pmos 0.52.0 → 0.53.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 +2 -2
- package/payload/mods/pmos-cockpit/README.md +13 -0
- package/payload/mods/pmos-cockpit/hooks/lib/commands.mjs +2 -1
- package/payload/mods/pmos-cockpit/hooks/lib/grants.mjs +41 -11
- package/payload/mods/pmos-cockpit/hooks/lib/guard.mjs +228 -8
- package/payload/mods/pmos-cockpit/hooks/register.tsx +11 -3
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/scripts/plan-items-stage.js +271 -0
- package/payload/scripts/pmos-log.sh +18 -7
- package/payload/scripts/workflow-state.js +28 -2
- package/payload/state/discoveries.md +1 -1
- package/payload/state/log.md +1 -1
- package/workspace/bin/pmos.js +15 -1
- package/workspace/cut-stub.js +40 -0
- package/workspace/first-paint-audit.js +145 -0
- package/workspace/fixtures/capture/requirements/capture-2026-10-04-1.md +10 -0
- package/workspace/fixtures/initiatives-api/list.json +176 -0
- package/workspace/fixtures/initiatives-api/post-403.json +5 -0
- package/workspace/fixtures/initiatives-api/post-ok.json +10 -0
- package/workspace/fixtures/initiatives-api/roadmap.json +208 -0
- package/workspace/fixtures/initiatives-api/sheet.json +112 -0
- package/workspace/fixtures/report-api/draft.json +177 -0
- package/workspace/fixtures/reports-page/draft-biweekly-2026-10-16.md +97 -0
- package/workspace/fixtures/today-api/provider-rows.json +204 -0
- package/workspace/fixtures/today-api/seam-probe-kinds.json +5 -0
- package/workspace/guide/board.md +81 -0
- package/workspace/guide/breakdown.md +154 -0
- package/workspace/guide/command-palette.md +75 -0
- package/workspace/guide/index.md +27 -17
- package/workspace/guide/initiatives-roadmap.md +130 -0
- package/workspace/guide/initiatives-screen.md +89 -0
- package/workspace/guide/initiatives-timeline.md +57 -0
- package/workspace/guide/integrated-work.md +94 -0
- package/workspace/guide/knowledge-graph.md +3 -68
- package/workspace/guide/layout.md +98 -32
- package/workspace/guide/live-notes.md +3 -3
- package/workspace/guide/pm-only-acts.md +89 -0
- package/workspace/guide/pm-only-lines.md +61 -0
- package/workspace/guide/reports-page.md +80 -0
- package/workspace/guide/reports.md +73 -0
- package/workspace/guide/timeline.md +70 -0
- package/workspace/guide/today.md +85 -0
- package/workspace/guide/validate-with-typesafe.md +1 -1
- package/workspace/lib/actor.js +91 -0
- package/workspace/lib/board-cache.js +32 -2
- package/workspace/lib/board-cli.js +108 -0
- package/workspace/lib/board-fold.js +232 -0
- package/workspace/lib/breakdown-export.js +80 -0
- package/workspace/lib/capture.js +98 -0
- package/workspace/lib/engine/plan-items-stage.js +271 -0
- package/workspace/lib/engine/workflow-state.js +28 -2
- package/workspace/lib/factory-join.js +158 -0
- package/workspace/lib/grant-mint.js +113 -0
- package/workspace/lib/health-rules-factory.js +92 -0
- package/workspace/lib/health-rules-plan.js +164 -0
- package/workspace/lib/health.js +79 -0
- package/workspace/lib/initiative-cli.js +222 -0
- package/workspace/lib/initiative-display.js +183 -0
- package/workspace/lib/initiative-needs.js +127 -0
- package/workspace/lib/initiative-rows.js +206 -0
- package/workspace/lib/initiative-writer.js +141 -0
- package/workspace/lib/issues.js +206 -0
- package/workspace/lib/item-cli.js +195 -0
- package/workspace/lib/item-spans.js +137 -0
- package/workspace/lib/measure-cli.js +100 -0
- package/workspace/lib/measurements.js +189 -0
- package/workspace/lib/mobile-nav.js +57 -53
- package/workspace/lib/needs-providers.js +120 -0
- package/workspace/lib/placements.js +440 -0
- package/workspace/lib/plan-items.js +922 -0
- package/workspace/lib/plan-ledgers.js +85 -0
- package/workspace/lib/plan-rules.js +72 -0
- package/workspace/lib/plan-tree.js +367 -0
- package/workspace/lib/pm-guard.js +174 -0
- package/workspace/lib/pm-lines-needs.js +331 -0
- package/workspace/lib/pmos-verbs.js +66 -0
- package/workspace/lib/proactive.js +7 -4
- package/workspace/lib/report-cli.js +152 -0
- package/workspace/lib/report-draft.js +579 -0
- package/workspace/lib/report-needs.js +213 -0
- package/workspace/lib/route-modules.js +128 -0
- package/workspace/lib/routes-board.js +91 -0
- package/workspace/lib/routes-capture.js +52 -0
- package/workspace/lib/routes-initiatives.js +371 -0
- package/workspace/lib/routes-reports.js +163 -0
- package/workspace/lib/routes-timeline.js +197 -0
- package/workspace/lib/routes-today.js +32 -0
- package/workspace/lib/today-cli.js +99 -0
- package/workspace/lib/today.js +333 -0
- package/workspace/lib/triage.js +163 -0
- package/workspace/lib/verify-work-cli.js +81 -0
- package/workspace/lib/work-check-needs.js +99 -0
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +4 -5
- package/workspace/public/index.html +29 -19
- package/workspace/public/pm/app.js +299 -30
- package/workspace/public/pm/bd-board.js +113 -0
- package/workspace/public/pm/bd-drag.js +59 -0
- package/workspace/public/pm/bd-model.js +152 -0
- package/workspace/public/pm/bd-timeline.js +233 -0
- package/workspace/public/pm/bd-tree.js +244 -0
- package/workspace/public/pm/board-copy.js +57 -0
- package/workspace/public/pm/breakdown-api.js +58 -0
- package/workspace/public/pm/breakdown-copy.js +138 -0
- package/workspace/public/pm/components.css +3054 -0
- package/workspace/public/pm/cycle-review.js +125 -0
- package/workspace/public/pm/data.js +40 -187
- package/workspace/public/pm/dest-board.js +438 -0
- package/workspace/public/pm/dest-initiatives.js +134 -0
- package/workspace/public/pm/dest-reports.js +270 -0
- package/workspace/public/pm/dest-settings.js +217 -0
- package/workspace/public/pm/dest-today.js +176 -0
- package/workspace/public/pm/dialogs.js +88 -0
- package/workspace/public/pm/display-word.js +56 -0
- package/workspace/public/pm/dock.js +105 -0
- package/workspace/public/pm/fixture.js +8 -227
- package/workspace/public/pm/frame-copy.js +94 -0
- package/workspace/public/pm/handoff-box.js +82 -0
- package/workspace/public/pm/http.js +88 -0
- package/workspace/public/pm/icons.js +53 -0
- package/workspace/public/pm/initiative-list.js +218 -0
- package/workspace/public/pm/initiative-sheet.js +261 -0
- package/workspace/public/pm/initiatives-api.js +59 -0
- package/workspace/public/pm/initiatives-copy.js +215 -0
- package/workspace/public/pm/initiatives-timeline-copy.js +59 -0
- package/workspace/public/pm/initiatives-timeline.js +410 -0
- package/workspace/public/pm/initiatives-ui.js +123 -0
- package/workspace/public/pm/item-sheet.js +251 -0
- package/workspace/public/pm/keep-card.js +204 -0
- package/workspace/public/pm/lifecycle.js +7 -1
- package/workspace/public/pm/mobile.css +49 -20
- package/workspace/public/pm/mobile.js +157 -29
- package/workspace/public/pm/modes-live.js +7 -883
- package/workspace/public/pm/modes.js +32 -1024
- package/workspace/public/pm/new-initiative-sheet.js +74 -0
- package/workspace/public/pm/next-step-ui.js +3 -2
- package/workspace/public/pm/palette-copy.js +51 -0
- package/workspace/public/pm/palette.js +312 -0
- package/workspace/public/pm/panel-breakdown.js +702 -0
- package/workspace/public/pm/panel-cockpit.js +63 -295
- package/workspace/public/pm/panel-detail.js +25 -172
- package/workspace/public/pm/panel-initiatives.js +1 -19
- package/workspace/public/pm/panel-repos.js +31 -81
- package/workspace/public/pm/panel-tabs.js +197 -110
- package/workspace/public/pm/panel-trees.js +5 -2
- package/workspace/public/pm/portfolio-report.js +61 -0
- package/workspace/public/pm/primitives.js +656 -0
- package/workspace/public/pm/print.css +33 -0
- package/workspace/public/pm/provider-cards.js +136 -0
- package/workspace/public/pm/registry.js +194 -0
- package/workspace/public/pm/release-card.js +110 -0
- package/workspace/public/pm/report-view.js +309 -0
- package/workspace/public/pm/reports-copy.js +114 -0
- package/workspace/public/pm/roadmap.js +271 -0
- package/workspace/public/pm/sig-watch.js +41 -0
- package/workspace/public/pm/state.js +8 -4
- package/workspace/public/pm/status-banner.js +13 -5
- package/workspace/public/pm/tab-doc.js +2 -4
- package/workspace/public/pm/tab-term.js +12 -2
- package/workspace/public/pm/tab-web.js +3 -2
- package/workspace/public/pm/timeline-copy.js +48 -0
- package/workspace/public/pm/today-cards.js +244 -0
- package/workspace/public/pm/today-copy.js +97 -0
- package/workspace/public/pm/tokens.css +16 -18
- package/workspace/public/pm/topbar.js +102 -0
- package/workspace/public/pm/triage-card.js +60 -0
- package/workspace/public/pm/undo-hold.js +103 -0
- package/workspace/public/pm/view-addrepo.js +2 -1
- package/workspace/public/pm/view-livenotes.js +3 -1
- package/workspace/public/pm/view-today.js +7 -162
- package/workspace/public/vendor/Sortable.min.js +2 -0
- package/workspace/public/vendor/lucide-sprite.js +79 -0
- package/workspace/replaced-tests.json +2864 -0
- package/workspace/run-tests.js +56 -5
- package/workspace/server.js +312 -36
- package/workspace/sheet-answer.js +61 -0
- package/workspace/skills/measure.skill +41 -0
- package/workspace/skills/report-biweekly.skill +23 -25
- package/workspace/skills/tracker-refresh.skill +50 -0
- package/workspace/test-files.json +19 -3
- package/workspace/test-lists/base.json +6 -0
- package/workspace/test-lists/run2-a.json +6 -0
- package/workspace/test-lists/run2-b.json +13 -0
- package/workspace/test-lists/run2-c.json +19 -0
- package/workspace/test-lists/run2-d.json +22 -0
- package/workspace/test-lists/run3-0.json +5 -0
- package/workspace/test-lists/run3-a.json +5 -0
- package/workspace/test-lists/run3-b.json +6 -0
- package/workspace/test-lists/run3-c.json +15 -0
- package/workspace/test-lists/run3-d.json +6 -0
- package/workspace/test-lists/run4-a.json +8 -0
- package/workspace/test-lists/run4-b.json +6 -0
- package/workspace/test-lists/run4-c.json +13 -0
- package/workspace/test-lists/run4-d.json +9 -0
- package/workspace/test-lists/run5-a.json +6 -0
- package/workspace/test-lists/run5-b.json +6 -0
- package/workspace/test-lists/run5-c.json +15 -0
- package/workspace/test-run.js +27 -1
- package/workspace/third-party/README.md +44 -0
- package/workspace/third-party/lucide-static-1.52.0.LICENSE +43 -0
- package/workspace/third-party/sortablejs-1.15.7.LICENSE +21 -0
- package/workspace/design-parity.js +0 -673
- package/workspace/public/pm/tab-task.js +0 -86
- package/workspace/public/pm/view-guide.js +0 -141
- package/workspace/public/pm/view-knowledge.js +0 -225
- package/workspace/public/pm/view-memory.js +0 -122
- package/workspace/public/pm/view-portfolio.js +0 -153
- package/workspace/public/pm/view-reports.js +0 -190
- package/workspace/public/pm/view-scheduled.js +0 -161
- package/workspace/public/pm/view-skills.js +0 -216
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/* pm/print.css — a report on paper (pm-os-absorb Run 5, slice 5C). Linked with media="print" by report-view.js.
|
|
2
|
+
*
|
|
3
|
+
* printReport() draws the report into `body > .rp-print`, in the light theme. On paper every other child of the body goes,
|
|
4
|
+
* and the report takes the page. The report's own rules (.rp-doc …) are in components.css. */
|
|
5
|
+
@page {
|
|
6
|
+
margin: 16mm;
|
|
7
|
+
}
|
|
8
|
+
@media print {
|
|
9
|
+
body > *:not(.rp-print) {
|
|
10
|
+
display: none !important;
|
|
11
|
+
}
|
|
12
|
+
html,
|
|
13
|
+
body {
|
|
14
|
+
height: auto !important;
|
|
15
|
+
overflow: visible !important;
|
|
16
|
+
background: Canvas !important;
|
|
17
|
+
}
|
|
18
|
+
body > .rp-print {
|
|
19
|
+
display: block !important;
|
|
20
|
+
padding: 0;
|
|
21
|
+
background: Canvas;
|
|
22
|
+
color: CanvasText;
|
|
23
|
+
font-family: var(--sans);
|
|
24
|
+
}
|
|
25
|
+
.rp-print .rp-doc__h2,
|
|
26
|
+
.rp-print .rp-doc__h3 {
|
|
27
|
+
break-after: avoid;
|
|
28
|
+
}
|
|
29
|
+
.rp-print .rp-doc__table,
|
|
30
|
+
.rp-print .rp-doc__note {
|
|
31
|
+
break-inside: avoid;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/* pm/provider-cards.js — every needs provider's rows as Today cards (pm-os-absorb seams, Runs 2 to 5).
|
|
2
|
+
*
|
|
3
|
+
* The server folds each run's needs-you rows into /api/needs (lib/needs-providers.js): a day-one Cycle review, an
|
|
4
|
+
* unattested PM line, an item in Review, a report to issue. This module is the ONE Today source for all of them, so the
|
|
5
|
+
* nav's Today count and Today's header count each row once, whichever run made it:
|
|
6
|
+
* - it reads /api/needs of each repository's workspace, once the first sessions poll has answered (as keep-card.js
|
|
7
|
+
* does, so the page's startup makes no extra request), and again when that workspace's ledgers change;
|
|
8
|
+
* - it keeps the provider rows only (derived, with a `provider`), never Run 1's own rows, which keep-card.js draws;
|
|
9
|
+
* - it draws a row with the renderer its run registered for the row's kind (registry.js registerTodayCard), or else
|
|
10
|
+
* a plain card from the row's `today` payload: the cause as the title, the evidence as the meta line.
|
|
11
|
+
*
|
|
12
|
+
* Loaded by registry.js loadExtensions at boot. Run 3 (slice 3B) owns this file after the base: every card carries its R12
|
|
13
|
+
* kind and its id (data-today-kind, data-today-id) so Today ranks them, the plain card has its one button and the ⋯ menu,
|
|
14
|
+
* and a provider's failure is a notice on Today, never a card. Fixture mode never loads it.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { ensureComponentsCss, toast, SEL } from './primitives.js';
|
|
18
|
+
import { pmGet } from './http.js';
|
|
19
|
+
import { wsOf } from './data.js';
|
|
20
|
+
import { state, setState } from './state.js';
|
|
21
|
+
import { registerTodaySource, todayCardFor, openInitiative } from './registry.js';
|
|
22
|
+
import { todayCard, ensureFeed, onFeed } from './today-cards.js';
|
|
23
|
+
import { COPY } from './today-copy.js';
|
|
24
|
+
|
|
25
|
+
const cache = new Map(); // ws -> { rows, loading, stale }
|
|
26
|
+
let hooked = false;
|
|
27
|
+
|
|
28
|
+
/** A row that is a card: derived, from a provider, and not that provider's own failure (a failed source is a notice, not a card). */
|
|
29
|
+
const isCardRow = (r) => !!r && r.derived === true && typeof r.provider === 'string' && r.kind !== 'provider-error' && r.run_id !== 'plan-items:unreadable';
|
|
30
|
+
|
|
31
|
+
function load(ws) {
|
|
32
|
+
const c = cache.get(ws) || { rows: [], loading: false, stale: true };
|
|
33
|
+
cache.set(ws, c);
|
|
34
|
+
if (c.loading) { c.again = true; return; }
|
|
35
|
+
c.loading = true; c.again = false;
|
|
36
|
+
pmGet('/api/needs?ws=' + encodeURIComponent(ws)).then((out) => {
|
|
37
|
+
c.loading = false; c.stale = false;
|
|
38
|
+
const all = out && Array.isArray(out.needs) ? out.needs : [];
|
|
39
|
+
// an item Run 1's `evidence` row already names (keep-card.js draws it) is not drawn again from 4D's item-no-evidence row
|
|
40
|
+
const evidenceOf = new Set(all.filter((r) => r && r.kind === 'evidence').map((r) => String(r.run_id).replace(/^evidence:/, '')));
|
|
41
|
+
const dup = (r) => r.kind === 'item-no-evidence' && evidenceOf.has(String(r.run_id || '').replace(/^work-check-needs:no-evidence:/, ''));
|
|
42
|
+
c.rows = all.filter((r) => isCardRow(r) && !dup(r));
|
|
43
|
+
if (c.again) load(ws);
|
|
44
|
+
setState({});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function hook() {
|
|
49
|
+
if (hooked || typeof window === 'undefined') return;
|
|
50
|
+
hooked = true;
|
|
51
|
+
// A D86 row changes with a placement, a measurement, an issue: files the sessions poll does not watch. The shared feed
|
|
52
|
+
// (today-cards.js) watches the repository's own /api/today sig, and tells this source when it moved.
|
|
53
|
+
onFeed((ws) => { const c = cache.get(ws); if (c) { c.stale = true; load(ws); } });
|
|
54
|
+
window.addEventListener('pmos:ledgers', (e) => {
|
|
55
|
+
const changed = (e && e.detail && e.detail.changed) || [];
|
|
56
|
+
for (const ws of changed) { const c = cache.get(ws); if (c) { c.stale = true; load(ws); } }
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The provider rows of the given repositories, each with its repository: [{ repo, ws, row }]. Starts a read if needed. */
|
|
61
|
+
function rowsOf(repos) {
|
|
62
|
+
hook();
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const repo of repos || []) {
|
|
65
|
+
const ws = wsOf(repo);
|
|
66
|
+
if (!ws) continue;
|
|
67
|
+
ensureFeed(ws);
|
|
68
|
+
const c = cache.get(ws);
|
|
69
|
+
if ((!c || (c.stale && !c.loading)) && Array.isArray(state.sessions)) load(ws);
|
|
70
|
+
for (const row of (c && c.rows) || []) out.push({ repo, ws, row });
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const rowId = (ws, row) => ws + '|' + row.run_id;
|
|
76
|
+
|
|
77
|
+
/** What the one button of a plain provider card does: open the initiative, else the destination that holds the record. */
|
|
78
|
+
function pressOf(repo, row) {
|
|
79
|
+
return () => {
|
|
80
|
+
const t = row.today && typeof row.today === 'object' ? row.today : {};
|
|
81
|
+
if (row.initiative_id) { openInitiative(repo, row.initiative_id); return; }
|
|
82
|
+
const open = typeof window !== 'undefined' && window.pmApp && window.pmApp.open;
|
|
83
|
+
const dest = row.provider === 'report-needs' ? 'reports' : row.provider === 'initiative-needs' ? 'initiatives' : null;
|
|
84
|
+
if (dest && open) { open(dest); return; }
|
|
85
|
+
if (t.act && t.act.cli) toast({ text: COPY.commandShown(t.act.cli), timeout: 12000 }); else toast({ text: COPY.noInitiative });
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function plainCard({ repo, ws, row }) {
|
|
90
|
+
const t = row.today && typeof row.today === 'object' ? row.today : {};
|
|
91
|
+
const slug = row.initiative_id || null;
|
|
92
|
+
const kind = Number.isInteger(t.kind) && t.kind >= 1 && t.kind <= 8 ? t.kind : 8;
|
|
93
|
+
return todayCard({
|
|
94
|
+
kind, id: rowId(ws, row), repo, ws, initiative: slug, cause: t.cause || row.question || row.kind, evidence: t.evidence || slug || '',
|
|
95
|
+
act: t.act || { label: 'Open' }, onAct: pressOf(repo, row),
|
|
96
|
+
attrs: { 'data-today-row': row.kind, 'data-provider': row.provider },
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
registerTodaySource({
|
|
101
|
+
id: 'provider-rows',
|
|
102
|
+
kind: null, // every R12 kind: each row carries its own in row.today.kind
|
|
103
|
+
rank: 50,
|
|
104
|
+
count: (ctx) => rowsOf(ctx.repos).length,
|
|
105
|
+
cards: (ctx) => {
|
|
106
|
+
const out = rowsOf(ctx.repos).map((x) => {
|
|
107
|
+
const render = todayCardFor(x.row.kind);
|
|
108
|
+
if (render) {
|
|
109
|
+
try {
|
|
110
|
+
const node = render(x.row, { ...ctx, repo: x.repo, ws: x.ws });
|
|
111
|
+
if (node) {
|
|
112
|
+
// A run's renderer draws its own face (4D's Release card, 2D's triage and Cycle cards, 5C's report cards). That face
|
|
113
|
+
// is the Today card's body: its title is the one cause and its action the one button, and Today adds only the ⋯
|
|
114
|
+
// menu (R12), so every card ranks, counts and reads alike without a second cause or a second button.
|
|
115
|
+
const t = x.row.today && typeof x.row.today === 'object' ? x.row.today : {};
|
|
116
|
+
const kind = Number.isInteger(t.kind) && t.kind >= 1 && t.kind <= 8 ? t.kind : 8;
|
|
117
|
+
// the renderer's action: its last button that is not the card's own open button, else the open button
|
|
118
|
+
const btns = [...node.querySelectorAll('button')];
|
|
119
|
+
const actBtn = btns.filter((b) => !b.matches(SEL.cardOpen)).pop() || btns[0];
|
|
120
|
+
if (actBtn) actBtn.setAttribute('data-today-act', '');
|
|
121
|
+
const causeEl = node.querySelector('[data-today-cause]') || node.querySelector(SEL.cardTitle);
|
|
122
|
+
if (causeEl) causeEl.setAttribute('data-today-cause', '');
|
|
123
|
+
return todayCard({
|
|
124
|
+
kind, id: rowId(x.ws, x.row), repo: x.repo, ws: x.ws, initiative: x.row.initiative_id || null,
|
|
125
|
+
cause: t.cause || x.row.question || x.row.kind, act: t.act || { label: 'Open' },
|
|
126
|
+
attrs: { 'data-provider': x.row.provider, 'data-today-host': x.row.kind }, face: node,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
} catch (e) { console.error('[pm] the Today card for ' + x.row.kind + ' failed:', e); }
|
|
130
|
+
}
|
|
131
|
+
return plainCard(x);
|
|
132
|
+
});
|
|
133
|
+
if (out.length) ensureComponentsCss();
|
|
134
|
+
return out;
|
|
135
|
+
},
|
|
136
|
+
});
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/* pm/registry.js — where the planning layer's page modules plug into the shell (pm-os-absorb Runs 2 to 5).
|
|
2
|
+
*
|
|
3
|
+
* Four runs add screens at once. If each one edited app.js, panel-cockpit.js, view-today.js and panel-tabs.js, every
|
|
4
|
+
* merge would collide on the same lines. So the shell reads this registry once, in a few hook lines, and each run adds
|
|
5
|
+
* its own module files.
|
|
6
|
+
*
|
|
7
|
+
* ── DESTINATIONS: the four places of PRD R9, plus ⚙ ─────────────────────────────────────────────────────────────────
|
|
8
|
+
* Listed below before their modules exist. Run 3 (slice 3A) builds the frame that shows them as top-level places. Until
|
|
9
|
+
* that frame is the default, each one opens as a tab in panel 4 (kind 'dest', its id in the tab's `view` field), from
|
|
10
|
+
* ⌘K or from `window.pmApp.open(id)`. Wherever it is drawn, its host carries data-dest="<id>" (mountDestTab sets it; the
|
|
11
|
+
* frame mounts through mountDestTab too). A DESTINATION MODULE exports:
|
|
12
|
+
* mountDestination(host, ctx) called on EVERY render with the same host element. Idempotent: keep one persistent
|
|
13
|
+
* root per host, rebuild only when its own data changed, never while a drag is live.
|
|
14
|
+
* disposeDestination(host) optional; the tab closed or the frame let go of the host.
|
|
15
|
+
* ctx = { state, setState, FIXTURE, tab, openInitiative }
|
|
16
|
+
*
|
|
17
|
+
* ── EXTENSIONS: modules that only register ──────────────────────────────────────────────────────────────────────────
|
|
18
|
+
* Loaded at boot with the destinations, so a view or a Today source is in place before its host renders.
|
|
19
|
+
*
|
|
20
|
+
* ── THE REGISTRIES (call these from your own module, at import time) ─────────────────────────────────────────────────
|
|
21
|
+
* registerTodaySource({ id, kind, rank, cards(ctx) → Element[], count(ctx) → number })
|
|
22
|
+
* kind is the R12 card kind (1 to 8); rank orders sources inside Today (lower first). ctx = { all, repos,
|
|
23
|
+
* setState, open(repo, slug) }. count must equal cards().length for the same ctx: the nav badge and Today's
|
|
24
|
+
* header add the counts, and they must agree (Run 1 finding F8).
|
|
25
|
+
* registerTodayCard(rowKind, render(row, ctx) → Element)
|
|
26
|
+
* how ONE needs-row kind of a server provider (lib/needs-providers.js) is drawn on Today. The rows themselves come
|
|
27
|
+
* from /api/needs through provider-cards.js, the one Today source for every provider row, so the count is the
|
|
28
|
+
* server's row count and a run never counts its rows twice. A kind with no renderer is drawn from its `today`
|
|
29
|
+
* payload (cause, evidence, act). Use registerTodaySource only for a card the page computes itself.
|
|
30
|
+
* registerCommand({ id, label, hint, cli, run(), when() }) a ⌘K row; `cli` is the same act's CLI form (R11)
|
|
31
|
+
* registerBreakdownView(id, { label, mount(host, ctx), dispose(host) }) a Breakdown view next to Tree and Board
|
|
32
|
+
* registerInitiativesView(id, { label, mount(host, ctx), dispose(host) }) a view in Initiatives (Roadmap, List, …)
|
|
33
|
+
* ctx is ALWAYS initiativesViewCtx(…): { repo, ws, rows, sig, openInitiative, setState } — rows = the roadmap.json
|
|
34
|
+
* horizons rows (fixtures/initiatives-api/roadmap.json), sig = that GET's sig. 2D builds it; 4C reads it.
|
|
35
|
+
* setInitiativeOpener(fn) Run 2 points openInitiative at the initiative sheet; nobody else calls it
|
|
36
|
+
* openInitiative(repo, slug) the one way to open an initiative from any module
|
|
37
|
+
* An id registered twice keeps the first and logs an error: two runs must not claim one id.
|
|
38
|
+
*
|
|
39
|
+
* Fixture mode (the parity boards) loads nothing and registers nothing, so the prototype renders are unchanged.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import { state, setState, FIXTURE } from './state.js';
|
|
43
|
+
import { pmGet } from './http.js';
|
|
44
|
+
import { empty, ensureComponentsCss } from './primitives.js';
|
|
45
|
+
|
|
46
|
+
export const DESTINATIONS = Object.freeze([
|
|
47
|
+
Object.freeze({ id: 'today', label: 'Today', key: 'g t', module: 'dest-today.js', run: 3 }),
|
|
48
|
+
Object.freeze({ id: 'initiatives', label: 'Initiatives', key: 'g i', module: 'dest-initiatives.js', run: 2 }),
|
|
49
|
+
Object.freeze({ id: 'board', label: 'Board', key: 'g b', module: 'dest-board.js', run: 4 }),
|
|
50
|
+
Object.freeze({ id: 'reports', label: 'Reports', key: 'g r', module: 'dest-reports.js', run: 5 }),
|
|
51
|
+
Object.freeze({ id: 'settings', label: 'Settings', key: null, module: 'dest-settings.js', run: 3 }),
|
|
52
|
+
]);
|
|
53
|
+
export const EXTENSIONS = Object.freeze([
|
|
54
|
+
'provider-cards.js', // the base: every needs provider's rows as Today cards (Run 3, 3B, owns it after)
|
|
55
|
+
'bd-timeline.js', // Run 4 (4B): Breakdown › Timeline
|
|
56
|
+
'initiatives-timeline.js', // Run 4 (4C): Initiatives › Timeline
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
const loaded = {}; // module file name -> the module
|
|
60
|
+
const loadErrors = {}; // module file name -> why it did not load
|
|
61
|
+
const todaySources = [];
|
|
62
|
+
const commandList = [];
|
|
63
|
+
const bdViews = [];
|
|
64
|
+
const initViews = [];
|
|
65
|
+
const rowCards = new Map(); // needs-row kind -> render(row, ctx)
|
|
66
|
+
let opener = null;
|
|
67
|
+
|
|
68
|
+
const dup = (list, id, what) => {
|
|
69
|
+
if (!list.some((x) => x.id === id)) return false;
|
|
70
|
+
console.error('[pm] ' + what + ' ' + id + ' is registered twice; the first one is kept');
|
|
71
|
+
return true;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export function registerTodaySource(src) {
|
|
75
|
+
if (FIXTURE || !src || typeof src.id !== 'string' || typeof src.cards !== 'function' || dup(todaySources, src.id, 'Today source')) return;
|
|
76
|
+
todaySources.push({ rank: 100, count: (ctx) => src.cards(ctx).length, ...src });
|
|
77
|
+
todaySources.sort((a, b) => a.rank - b.rank || (a.id < b.id ? -1 : 1));
|
|
78
|
+
}
|
|
79
|
+
export function registerTodayCard(rowKind, render) {
|
|
80
|
+
if (FIXTURE || typeof rowKind !== 'string' || typeof render !== 'function') return;
|
|
81
|
+
if (rowCards.has(rowKind)) { console.error('[pm] Today card ' + rowKind + ' is registered twice; the first one is kept'); return; }
|
|
82
|
+
rowCards.set(rowKind, render);
|
|
83
|
+
}
|
|
84
|
+
export function registerCommand(cmd) {
|
|
85
|
+
if (FIXTURE || !cmd || typeof cmd.id !== 'string' || typeof cmd.run !== 'function' || dup(commandList, cmd.id, 'command')) return;
|
|
86
|
+
commandList.push(cmd);
|
|
87
|
+
}
|
|
88
|
+
export function registerBreakdownView(id, view) {
|
|
89
|
+
if (FIXTURE || typeof id !== 'string' || !view || typeof view.mount !== 'function' || dup(bdViews, id, 'Breakdown view')) return;
|
|
90
|
+
bdViews.push({ id, ...view });
|
|
91
|
+
}
|
|
92
|
+
export function registerInitiativesView(id, view) {
|
|
93
|
+
if (FIXTURE || typeof id !== 'string' || !view || typeof view.mount !== 'function' || dup(initViews, id, 'Initiatives view')) return;
|
|
94
|
+
initViews.push({ id, ...view });
|
|
95
|
+
}
|
|
96
|
+
/** The frozen keys of an Initiatives view's mount ctx, and the one way to build it (a missing key is null). */
|
|
97
|
+
export const INITIATIVES_VIEW_CTX_KEYS = Object.freeze(['repo', 'ws', 'rows', 'sig', 'openInitiative', 'setState']);
|
|
98
|
+
export function initiativesViewCtx(src) {
|
|
99
|
+
const o = {};
|
|
100
|
+
for (const k of INITIATIVES_VIEW_CTX_KEYS) o[k] = src && src[k] !== undefined ? src[k] : null;
|
|
101
|
+
return Object.freeze(o);
|
|
102
|
+
}
|
|
103
|
+
export function setInitiativeOpener(fn) { if (typeof fn === 'function') opener = fn; }
|
|
104
|
+
|
|
105
|
+
export function openInitiative(repo, slug) {
|
|
106
|
+
if (opener) return opener(repo, slug);
|
|
107
|
+
const repos = typeof window !== 'undefined' && window.pmApp && window.pmApp.mods ? window.pmApp.mods.repos : null;
|
|
108
|
+
if (repos && typeof repos.selectRepo === 'function' && repo) repos.selectRepo(repo);
|
|
109
|
+
setState({ sel: slug, detail: { kind: 'init' }, col2: true });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* ── readers, for the shell's hook lines ───────────────────────────────────────────────────────────────────────────── */
|
|
113
|
+
|
|
114
|
+
const safe = (what, fn, fallback) => { try { return fn(); } catch (e) { console.error('[pm] ' + what + ' failed:', e); return fallback; } };
|
|
115
|
+
export const todayCards = (ctx) => (FIXTURE ? [] : todaySources.flatMap((s) => safe('Today source ' + s.id, () => s.cards(ctx) || [], [])));
|
|
116
|
+
export const todayCount = (ctx) => (FIXTURE ? 0 : todaySources.reduce((n, s) => n + safe('Today source ' + s.id, () => Number(s.count(ctx)) || 0, 0), 0));
|
|
117
|
+
export const commands = () => commandList.filter((c) => !c.when || safe('command ' + c.id, () => !!c.when(), false));
|
|
118
|
+
/** The renderer registered for a needs-row kind, or null (provider-cards.js then draws the row's `today` payload). */
|
|
119
|
+
export const todayCardFor = (rowKind) => rowCards.get(rowKind) || null;
|
|
120
|
+
export const breakdownViews = () => bdViews.slice();
|
|
121
|
+
export const initiativesViews = () => initViews.slice();
|
|
122
|
+
/** The destinations whose module is loaded, in order. */
|
|
123
|
+
export const destinations = () => DESTINATIONS.filter((d) => loaded[d.module]);
|
|
124
|
+
export const extensionErrors = () => ({ ...loadErrors });
|
|
125
|
+
|
|
126
|
+
/** Load every listed module that is on disk (the server says which: GET /api/pm-modules), once, at boot. */
|
|
127
|
+
let loading = null;
|
|
128
|
+
export function loadExtensions() {
|
|
129
|
+
if (FIXTURE) return Promise.resolve();
|
|
130
|
+
if (loading) return loading;
|
|
131
|
+
const want = [...DESTINATIONS.map((d) => d.module), ...EXTENSIONS];
|
|
132
|
+
loading = (async () => {
|
|
133
|
+
const j = await pmGet('/api/pm-modules?want=' + encodeURIComponent(want.join(',')));
|
|
134
|
+
const present = Array.isArray(j && j.present) ? j.present.filter((n) => want.includes(n)) : [];
|
|
135
|
+
await Promise.all(present.map(async (name) => {
|
|
136
|
+
try { loaded[name] = await import('./' + name); }
|
|
137
|
+
catch (e) { loadErrors[name] = String((e && e.message) || e); console.error('[pm] ' + name + ' failed to load:', e); }
|
|
138
|
+
}));
|
|
139
|
+
})();
|
|
140
|
+
return loading;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* ── the home of a destination: the frame, or (outside it) a tab in panel 4 ────────────────────────────────────────── */
|
|
144
|
+
|
|
145
|
+
const tabId = (id) => 'dest:' + id;
|
|
146
|
+
const hosts = new Map(); // tab id -> { host, mod }
|
|
147
|
+
let frameHome = null; // set by app.js when the frame is on (Run 3, slice 3A)
|
|
148
|
+
|
|
149
|
+
/** app.js (the frame): from now on a destination opens as a top-level place of the frame, not as a panel-4 tab. */
|
|
150
|
+
export function setFrameHome(fn) { frameHome = typeof fn === 'function' ? fn : null; }
|
|
151
|
+
|
|
152
|
+
/** Open destination `id`: in the frame, as the place on screen; outside it (fixture mode does not load this module, so
|
|
153
|
+
* this is ?layout=legacy), as a panel-4 tab, opened or focused. `params` may carry { repo, slug } for a destination that
|
|
154
|
+
* opens on one. Returns false for an id that names no destination. */
|
|
155
|
+
export function openDestination(id, params) {
|
|
156
|
+
const d = DESTINATIONS.find((x) => x.id === id);
|
|
157
|
+
if (!d) return false;
|
|
158
|
+
if (frameHome) return frameHome(id, params || {}) !== false;
|
|
159
|
+
const p = params || {};
|
|
160
|
+
const tid = tabId(id);
|
|
161
|
+
setState((s) => {
|
|
162
|
+
const tabs = s.tabs || [];
|
|
163
|
+
const tab = { id: tid, kind: 'dest', view: id, title: d.label, role: d.label.toLowerCase(), repo: p.repo || null, slug: p.slug || null };
|
|
164
|
+
return { tabs: tabs.some((t) => t.id === tid) ? tabs.map((t) => (t.id === tid ? { ...t, repo: tab.repo, slug: tab.slug } : t)) : [...tabs, tab], active: tid };
|
|
165
|
+
});
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Draw destination `tab.view` into its persistent host: the frame's host for that place (tab id 'frame:<id>'), or a
|
|
170
|
+
* 'dest' tab's pane (panel-tabs.js paneFor). The frame's tab also carries `scope`: 'repo', or 'all' when Repository ▾ is
|
|
171
|
+
* on All repositories (then `repo` is null). */
|
|
172
|
+
export function mountDestTab(host, tab) {
|
|
173
|
+
const d = DESTINATIONS.find((x) => x.id === tab.view);
|
|
174
|
+
host.setAttribute('data-dest', tab.view || ''); // the one hook a test finds a destination by, in a tab or the frame
|
|
175
|
+
const mod = d ? loaded[d.module] : null;
|
|
176
|
+
if (!mod || typeof mod.mountDestination !== 'function') {
|
|
177
|
+
const text = !d ? 'This tab names no destination.'
|
|
178
|
+
: loadErrors[d.module] ? d.label + ' could not load: ' + loadErrors[d.module]
|
|
179
|
+
: d.label + ' arrives in Run ' + d.run + '.';
|
|
180
|
+
ensureComponentsCss();
|
|
181
|
+
host.replaceChildren(empty({ text, attrs: { 'data-dest-missing': tab.view || '' } }));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
hosts.set(tab.id, { host, mod });
|
|
185
|
+
safe('destination ' + d.id, () => mod.mountDestination(host, { state, setState, FIXTURE, tab, openInitiative }), null);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** panel-tabs.js reapClosedPanes: the tab closed. */
|
|
189
|
+
export function disposeDestTab(id) {
|
|
190
|
+
const h = hosts.get(id);
|
|
191
|
+
if (!h) return;
|
|
192
|
+
hosts.delete(id);
|
|
193
|
+
if (typeof h.mod.disposeDestination === 'function') safe('dispose ' + id, () => h.mod.disposeDestination(h.host), null);
|
|
194
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/* pm/release-card.js — the Release card, on the Board and on Today (pm-os-absorb Run 4, slice 4D; PRD R12 kind 4, D86 point 2).
|
|
2
|
+
*
|
|
3
|
+
* releaseButton({ ws, initiative, batch, items, onDone }) the one Release button; a press posts /api/board/release
|
|
4
|
+
* releaseCard(release, { ws, onDone }) one ready batch as a card, for the Board
|
|
5
|
+
* grantScopes(onReady) the scopes a press can grant now, or null until they are read
|
|
6
|
+
*
|
|
7
|
+
* Importing this file registers the Today renderers for the needs-row kinds of lib/work-check-needs.js (registry.js
|
|
8
|
+
* registerTodayCard): `release`, `item-review` and `item-no-evidence`, so dest-board.js imports it and the cards exist at
|
|
9
|
+
* boot. An item row shows the item's title (the row's question) and opens the initiative that holds it. A Release is the PM's press: the page sends the press with
|
|
10
|
+
* pmPost, which carries the page's Origin (and, from Run 2, the launch nonce). The server mints ONE grant for the whole
|
|
11
|
+
* batch. Nothing here writes a record or starts a factory run, and the button never acts on its own.
|
|
12
|
+
*
|
|
13
|
+
* Until the grant writer is merged, /api/pm-modules lists no `release` scope. The button then shows disabled, with
|
|
14
|
+
* "Release arrives in Run 2." beside it, so a press is never made to fail on purpose.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { el } from './dom.js';
|
|
18
|
+
import { button, card, toast, ensureComponentsCss } from './primitives.js';
|
|
19
|
+
import { pmGet, pmPost } from './http.js';
|
|
20
|
+
import { FIXTURE, setState } from './state.js';
|
|
21
|
+
import { registerTodayCard } from './registry.js';
|
|
22
|
+
import { COPY } from './board-copy.js';
|
|
23
|
+
|
|
24
|
+
let scopes = null;
|
|
25
|
+
let reading = false;
|
|
26
|
+
const waiting = [];
|
|
27
|
+
|
|
28
|
+
/** The grant scopes a press can mint now (GET /api/pm-modules), or null until the first read ends. */
|
|
29
|
+
export function grantScopes(onReady) {
|
|
30
|
+
if (scopes) return scopes;
|
|
31
|
+
if (typeof onReady === 'function') waiting.push(onReady);
|
|
32
|
+
if (!reading && !FIXTURE) {
|
|
33
|
+
reading = true;
|
|
34
|
+
pmGet('/api/pm-modules?want=').then((j) => {
|
|
35
|
+
scopes = Array.isArray(j && j.grantScopes) ? j.grantScopes : [];
|
|
36
|
+
reading = false;
|
|
37
|
+
for (const fn of waiting.splice(0)) { try { fn(scopes); } catch (e) { console.error('[release-card]', e); } }
|
|
38
|
+
setState({});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return scopes;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** True when the page knows the release scope cannot be granted yet. Unknown (not read) counts as grantable. */
|
|
45
|
+
const notYet = () => Array.isArray(scopes) && !scopes.includes('release');
|
|
46
|
+
|
|
47
|
+
/** One Release button. `onDone(out)` runs after a press that was granted. */
|
|
48
|
+
export function releaseButton({ ws, initiative, batch, items, onDone } = {}) {
|
|
49
|
+
const b = button({
|
|
50
|
+
label: COPY.release.press, variant: 'outline', size: 'sm',
|
|
51
|
+
attrs: { 'data-bo-release-press': `${initiative}:${batch}`, 'aria-label': COPY.release.pressLabel(initiative, batch) },
|
|
52
|
+
disabled: notYet(),
|
|
53
|
+
onClick: async () => {
|
|
54
|
+
if (b.disabled) return;
|
|
55
|
+
b.disabled = true;
|
|
56
|
+
const out = await pmPost('/api/board/release', { ws, initiative, batch });
|
|
57
|
+
if (!out || out.ok === false) {
|
|
58
|
+
toast({ text: COPY.release.failed((out && out.error) || 'the server did not answer'), tone: 'error' });
|
|
59
|
+
b.disabled = notYet();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
toast({ text: COPY.release.pressed(out.items || items || 0) });
|
|
63
|
+
if (onDone) onDone(out);
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
return b;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** One ready batch as a card: the initiative, the batch, how many work items, and the Release button. */
|
|
70
|
+
export function releaseCard(r, { ws, onDone } = {}) {
|
|
71
|
+
ensureComponentsCss();
|
|
72
|
+
const waitingForRun = notYet();
|
|
73
|
+
return el('div', { class: 'bo-rel', 'data-bo-release': `${r.initiative}:${r.batch}` },
|
|
74
|
+
el('div', { class: 'bo-rel__main' },
|
|
75
|
+
el('div', { class: 'bo-rel__title' }, COPY.release.title(r.initiativeTitle || r.initiative)),
|
|
76
|
+
el('div', { class: 'bo-rel__meta' }, [COPY.release.batch(r.batch), COPY.release.items((r.items || []).length)].join(' · ')),
|
|
77
|
+
waitingForRun ? el('div', { class: 'bo-rel__wait', 'data-bo-release-wait': '' }, COPY.release.waiting) : null),
|
|
78
|
+
releaseButton({ ws, initiative: r.initiative, batch: r.batch, items: (r.items || []).length, onDone }));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* Today: a needs row of kind `release` (lib/work-check-needs.js). Its run_id is work-check-needs:release:<initiative>:<batch>. */
|
|
82
|
+
function todayRelease(row, ctx) {
|
|
83
|
+
const m = /^work-check-needs:release:([^:]+):(.+)$/.exec(String(row.run_id || ''));
|
|
84
|
+
if (!m) return null;
|
|
85
|
+
const t = row.today && typeof row.today === 'object' ? row.today : {};
|
|
86
|
+
grantScopes();
|
|
87
|
+
return el('div', { class: 'bo-rel bo-rel--today', 'data-today-row': 'release', 'data-provider': row.provider || '', 'data-initiative': m[1] },
|
|
88
|
+
el('div', { class: 'bo-rel__main' },
|
|
89
|
+
el('div', { class: 'bo-rel__title', 'data-today-cause': '' }, row.question || t.cause || COPY.release.head),
|
|
90
|
+
el('div', { class: 'bo-rel__meta' }, [m[1], COPY.release.batch(m[2])].join(' · ')),
|
|
91
|
+
notYet() ? el('div', { class: 'bo-rel__wait', 'data-bo-release-wait': '' }, COPY.release.waiting) : null),
|
|
92
|
+
releaseButton({ ws: (ctx && ctx.ws) || row.ws, initiative: m[1], batch: m[2], onDone: () => setState({}) }));
|
|
93
|
+
}
|
|
94
|
+
registerTodayCard('release', todayRelease);
|
|
95
|
+
|
|
96
|
+
/* Today: an item row (item-review, item-no-evidence). The row's question names the item; the card opens its initiative. */
|
|
97
|
+
function todayItem(row, ctx) {
|
|
98
|
+
const t = row.today && typeof row.today === 'object' ? row.today : {};
|
|
99
|
+
const slug = row.initiative_id || null;
|
|
100
|
+
ensureComponentsCss();
|
|
101
|
+
return card({
|
|
102
|
+
title: row.question || t.cause || row.kind,
|
|
103
|
+
meta: [t.cause, slug, t.evidence].filter(Boolean),
|
|
104
|
+
onOpen: slug && ctx && ctx.open ? () => ctx.open(ctx.repo, slug) : undefined,
|
|
105
|
+
openLabel: slug ? COPY.aria.open(row.question || slug) : undefined,
|
|
106
|
+
attrs: { 'data-today-row': row.kind, 'data-provider': row.provider || '', 'data-initiative': slug || '' },
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
registerTodayCard('item-review', todayItem);
|
|
110
|
+
registerTodayCard('item-no-evidence', todayItem);
|