create-pmos 0.52.0 → 0.53.1
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/README.md +2 -0
- package/workspace/REMOTE.md +24 -63
- package/workspace/bin/pmos.js +19 -3
- 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 +28 -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/launcher.md +51 -0
- package/workspace/guide/layout.md +102 -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/proactive-engine.md +21 -5
- 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 +75 -0
- package/workspace/lib/brief.js +20 -2
- 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/goals.js +165 -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 +210 -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/launcher-cli.js +207 -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 +380 -0
- package/workspace/lib/pm-guard.js +174 -0
- package/workspace/lib/pm-lines-needs.js +331 -0
- package/workspace/lib/pmos-verbs.js +69 -0
- package/workspace/lib/proactive-cli.js +58 -2
- package/workspace/lib/proactive.js +9 -6
- package/workspace/lib/qr.js +293 -0
- package/workspace/lib/remote-access.js +93 -1
- package/workspace/lib/report-cli.js +152 -0
- package/workspace/lib/report-draft.js +578 -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/icons/apple-touch-icon.png +0 -0
- package/workspace/public/icons/icon-192.png +0 -0
- package/workspace/public/icons/icon-512.png +0 -0
- package/workspace/public/index.html +40 -19
- package/workspace/public/pm/app.js +330 -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 +3135 -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 +182 -0
- package/workspace/public/pm/dialogs.js +88 -0
- package/workspace/public/pm/display-word.js +56 -0
- package/workspace/public/pm/dock.js +175 -0
- package/workspace/public/pm/fixture.js +8 -227
- package/workspace/public/pm/frame-copy.js +99 -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 +200 -111
- 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 +126 -0
- package/workspace/public/pm/today-goals.js +172 -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 +2877 -0
- package/workspace/run-tests.js +56 -5
- package/workspace/server.js +446 -40
- package/workspace/sheet-answer.js +61 -0
- package/workspace/skills/measure.skill +41 -0
- package/workspace/skills/morning-brief.skill +15 -9
- package/workspace/skills/report-biweekly.skill +23 -25
- package/workspace/skills/tracker-refresh.skill +50 -0
- package/workspace/test-files.json +21 -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-lists/workspace-launcher.json +5 -0
- package/workspace/test-lists/workspace-phone-app.json +5 -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,189 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* measurements.js — THE ONE WRITER and THE ONE FOLD of measurements.jsonl: a measured value of a KR or a health
|
|
3
|
+
* budget, with its target, its date and its source (pm-os-absorb, PRD R17, R23, R24; D86).
|
|
4
|
+
*
|
|
5
|
+
* readMeasurements(root) → {
|
|
6
|
+
* ok: true,
|
|
7
|
+
* byAnchor: { [anchor]: [{ value, target, unit, as_of, source, by, at }] }, oldest first; `source` is "path@sha"
|
|
8
|
+
* flags: [{ kind, eid, why }],
|
|
9
|
+
* }
|
|
10
|
+
* latest(result, anchor) → the newest measurement of `anchor` by as_of, or null
|
|
11
|
+
* appendFromUi(root, events, { actor, now }) → { ok, written, lines } | { ok: false, status, error, index }
|
|
12
|
+
* appendFromCli(root, events, { actor, now }) → the same
|
|
13
|
+
* checkEvent(ev) → null | error text the caller-side checks, shared with `pmos measure`
|
|
14
|
+
*
|
|
15
|
+
* `anchor` is the initiative's parent_kr as written (a KR id, or `health:<id>`, D59). A measurement needs a source; an
|
|
16
|
+
* agent may write one (it is evidence, not a PM act), so the actor comes from actor.js, never from the caller.
|
|
17
|
+
* The workspace never computes a value (PRD Non-goals): a group shows a value only from a line here.
|
|
18
|
+
*
|
|
19
|
+
* THE `measured` LINE (the shared contract):
|
|
20
|
+
* { v: 1, eid, ev: 'measured', anchor, value: <number>, target: <number> | null, unit?: '%'|…, as_of: 'YYYY-MM-DD',
|
|
21
|
+
* source: '<path>@<sha>', by, via, at }
|
|
22
|
+
* pmOnly(rec) → false: a measurement is evidence, not a PM act.
|
|
23
|
+
*
|
|
24
|
+
* `source` is a repository-relative path, `@`, and a commit sha of 7 to 40 hex characters: the file the number was
|
|
25
|
+
* read from, at the commit it was read at. The writer does not open it (no read outside the call's own checks), so a
|
|
26
|
+
* wrong path is the writer's caller's error; scripts/check-measurements.js reports a path that is not in the tree.
|
|
27
|
+
*
|
|
28
|
+
* The fold is order-free, like lib/plan-items.js: byte-identical duplicates count once, one eid with two contents keeps
|
|
29
|
+
* the first. The ledger is one per repository: lib/plan-ledgers.js ledgerPath(root, 'measurements'). Nothing is cached.
|
|
30
|
+
*/
|
|
31
|
+
const fs = require('fs');
|
|
32
|
+
const path = require('path');
|
|
33
|
+
const crypto = require('crypto');
|
|
34
|
+
const ledgers = require('./plan-ledgers.js');
|
|
35
|
+
const actorLib = require('./actor.js');
|
|
36
|
+
const { createSinkGuard } = require('./sink.js');
|
|
37
|
+
|
|
38
|
+
const MAX_LINES = 50;
|
|
39
|
+
const EID_RE = /^[0-9a-z]{12}$/;
|
|
40
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
41
|
+
const AT_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
|
|
42
|
+
/** A KR id as the OKR files write it, or a health budget or a domain (D59, D67). One line, no control characters. */
|
|
43
|
+
const ANCHOR_RE = /^(?:health:[a-z0-9][a-z0-9-]{0,59}|domain:[a-z0-9][a-z0-9-]{0,59}|[A-Za-z0-9][A-Za-z0-9 .:_-]{0,79})$/;
|
|
44
|
+
const SOURCE_RE = /^([A-Za-z0-9_][A-Za-z0-9_./-]{0,239})@([0-9a-f]{7,40})$/;
|
|
45
|
+
const UNIT_RE = /^[^\s\x00-\x1f]{1,12}$/;
|
|
46
|
+
const WRITER_SET = Object.freeze(['v', 'eid', 'by', 'via', 'at', 'override', 'pm', 'actor', 'nonce']);
|
|
47
|
+
const CALLER_KEYS = Object.freeze(['anchor', 'value', 'target', 'unit', 'as_of', 'source']);
|
|
48
|
+
const STORED_KEYS = Object.freeze(['v', 'eid', 'ev', 'by', 'via', 'at', ...CALLER_KEYS]);
|
|
49
|
+
const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';
|
|
50
|
+
|
|
51
|
+
const isStr = (v) => typeof v === 'string';
|
|
52
|
+
const has = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
|
|
53
|
+
const dateOk = (v) => isStr(v) && DATE_RE.test(v) && !Number.isNaN(Date.parse(v + 'T00:00:00Z'))
|
|
54
|
+
&& new Date(v + 'T00:00:00Z').toISOString().slice(0, 10) === v;
|
|
55
|
+
|
|
56
|
+
function newEventId() {
|
|
57
|
+
let out = '';
|
|
58
|
+
while (out.length < 12) {
|
|
59
|
+
for (const b of crypto.randomBytes(24)) {
|
|
60
|
+
if (b >= 252) continue;
|
|
61
|
+
out += ALPHABET[b % 36];
|
|
62
|
+
if (out.length === 12) break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The source as { path, sha }, or null when it is not "<repository-relative path>@<sha>". */
|
|
69
|
+
function parseSource(s) {
|
|
70
|
+
const m = isStr(s) ? SOURCE_RE.exec(s) : null;
|
|
71
|
+
if (!m) return null;
|
|
72
|
+
if (m[1].split('/').some((seg) => seg === '..' || seg === '.' || seg === '')) return null;
|
|
73
|
+
return { path: m[1], sha: m[2] };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The checks on what a caller sends (the writer runs them; `pmos measure` runs them before it writes). */
|
|
77
|
+
function checkEvent(ev) {
|
|
78
|
+
if (!ev || typeof ev !== 'object' || Array.isArray(ev)) return 'each line must be a JSON object';
|
|
79
|
+
const sent = WRITER_SET.find((k) => has(ev, k));
|
|
80
|
+
if (sent) return `the writer sets "${sent}"; do not send it`;
|
|
81
|
+
if (ev.ev !== 'measured') return 'ev must be measured';
|
|
82
|
+
const extra = Object.keys(ev).filter((k) => k !== 'ev' && !CALLER_KEYS.includes(k));
|
|
83
|
+
if (extra.length) return `a measured line has no field ${extra.join(', ')}`;
|
|
84
|
+
if (!isStr(ev.anchor) || !ANCHOR_RE.test(ev.anchor.trim()) || ev.anchor !== ev.anchor.trim()) return 'anchor must be a KR id, health:<id> or domain:<id>';
|
|
85
|
+
if (typeof ev.value !== 'number' || !Number.isFinite(ev.value)) return 'value must be a number';
|
|
86
|
+
if (has(ev, 'target') && ev.target !== null && (typeof ev.target !== 'number' || !Number.isFinite(ev.target))) return 'target must be a number, or null';
|
|
87
|
+
if (has(ev, 'unit') && !(isStr(ev.unit) && UNIT_RE.test(ev.unit))) return 'unit must be a short word with no spaces, like %';
|
|
88
|
+
if (!dateOk(ev.as_of)) return 'as_of must be YYYY-MM-DD';
|
|
89
|
+
if (!has(ev, 'source') || ev.source === '' || ev.source == null) return 'a measurement needs a source: <path>@<commit sha>';
|
|
90
|
+
if (!parseSource(ev.source)) return 'source must be <repository-relative path>@<commit sha of 7 to 40 hex characters>';
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Why a stored line is not a valid line of this ledger, or null. */
|
|
95
|
+
function lineErr(r) {
|
|
96
|
+
if (r.v !== 1) return 'v must be 1';
|
|
97
|
+
if (!isStr(r.eid) || !EID_RE.test(r.eid)) return 'eid must be 12 characters of [0-9a-z]';
|
|
98
|
+
if (!isStr(r.by) || !actorLib.BY_RE.test(r.by)) return 'by must be pm:<slug>, agent:unknown or agent:<session>';
|
|
99
|
+
if (!actorLib.VIA.includes(r.via)) return 'via must be ui or cli';
|
|
100
|
+
if (!isStr(r.at) || !AT_RE.test(r.at)) return 'at must be an ISO UTC time';
|
|
101
|
+
const extra = Object.keys(r).filter((k) => !STORED_KEYS.includes(k));
|
|
102
|
+
if (extra.length) return `a measured line has no field ${extra.join(', ')}`;
|
|
103
|
+
const body = {};
|
|
104
|
+
for (const k of ['ev', ...CALLER_KEYS]) if (has(r, k)) body[k] = r[k];
|
|
105
|
+
return checkEvent(body);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readMeasurements(root) {
|
|
109
|
+
const out = { ok: true, byAnchor: {}, flags: [] };
|
|
110
|
+
const { lines, bad } = ledgers.readLines(root, 'measurements');
|
|
111
|
+
for (const b of bad) out.flags.push({ kind: 'unreadable', eid: null, why: `line ${b.n}: ${b.why}` });
|
|
112
|
+
const kept = [];
|
|
113
|
+
for (const { n, rec } of lines) {
|
|
114
|
+
if (rec.ev !== 'measured') { out.flags.push({ kind: 'invalid', eid: isStr(rec.eid) ? rec.eid : null, why: `line ${n}: ev must be measured` }); continue; }
|
|
115
|
+
const why = lineErr(rec);
|
|
116
|
+
if (why) { out.flags.push({ kind: 'invalid', eid: isStr(rec.eid) ? rec.eid : null, why: `line ${n}: ${why}` }); continue; }
|
|
117
|
+
kept.push({ n, rec });
|
|
118
|
+
}
|
|
119
|
+
kept.sort((a, b) => (Date.parse(a.rec.at) - Date.parse(b.rec.at)) || (a.rec.eid < b.rec.eid ? -1 : a.rec.eid > b.rec.eid ? 1 : 0) || (a.n - b.n));
|
|
120
|
+
const seen = new Map();
|
|
121
|
+
for (const { n, rec } of kept) {
|
|
122
|
+
const text = JSON.stringify(rec);
|
|
123
|
+
if (seen.has(rec.eid)) {
|
|
124
|
+
if (seen.get(rec.eid) !== text) out.flags.push({ kind: 'duplicate-event', eid: rec.eid, why: `line ${n}: the eid ${rec.eid} is used by two different lines; the first one counts` });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
seen.set(rec.eid, text);
|
|
128
|
+
(out.byAnchor[rec.anchor] = out.byAnchor[rec.anchor] || []).push({
|
|
129
|
+
value: rec.value, target: Number.isFinite(rec.target) ? rec.target : null, unit: rec.unit || null,
|
|
130
|
+
as_of: rec.as_of, source: rec.source, by: rec.by, at: rec.at,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
for (const rows of Object.values(out.byAnchor)) rows.sort((a, b) => String(a.as_of).localeCompare(String(b.as_of)) || String(a.at).localeCompare(String(b.at)));
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const pmOnly = () => false;
|
|
138
|
+
|
|
139
|
+
function latest(result, anchor) {
|
|
140
|
+
const rows = result && result.byAnchor && Object.prototype.hasOwnProperty.call(result.byAnchor, anchor) ? result.byAnchor[anchor] : null;
|
|
141
|
+
if (!Array.isArray(rows) || !rows.length) return null;
|
|
142
|
+
return rows.reduce((a, b) => (String(b.as_of) >= String(a.as_of) ? b : a));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const refuse = (status, error, index) => ({ ok: false, status, error, index: index == null ? null : index });
|
|
146
|
+
|
|
147
|
+
function append(root, events, actor, { now } = {}) {
|
|
148
|
+
if (!actorLib.valid(actor)) return refuse(500, 'measurements: the caller must pass the actor from pm-guard.js or actor.js', null);
|
|
149
|
+
if (!Array.isArray(events) || !events.length) return refuse(400, 'send at least one line', null);
|
|
150
|
+
if (events.length > MAX_LINES) return refuse(413, `send at most ${MAX_LINES} lines at once`, null);
|
|
151
|
+
for (let i = 0; i < events.length; i++) {
|
|
152
|
+
const why = checkEvent(events[i]);
|
|
153
|
+
if (why) return refuse(400, why, i);
|
|
154
|
+
}
|
|
155
|
+
const read = ledgers.readLines(root, 'measurements');
|
|
156
|
+
if (read.bad.some((b) => b.n === 0)) return refuse(500, 'could not read the measurements ledger — nothing was saved: ' + read.bad[0].why.slice(0, 160), null);
|
|
157
|
+
const clock = now == null ? Date.now() : now;
|
|
158
|
+
let last = 0;
|
|
159
|
+
for (const l of read.lines) {
|
|
160
|
+
const t = isStr(l.rec.at) ? Date.parse(l.rec.at) : NaN;
|
|
161
|
+
if (Number.isFinite(t) && t <= clock + 1000 && t > last) last = t;
|
|
162
|
+
}
|
|
163
|
+
const recs = events.map((ev) => {
|
|
164
|
+
last = Math.max(clock, last + 1);
|
|
165
|
+
const rec = { v: 1, eid: newEventId(), ev: 'measured', anchor: ev.anchor, value: ev.value, target: has(ev, 'target') ? ev.target : null };
|
|
166
|
+
if (has(ev, 'unit')) rec.unit = ev.unit;
|
|
167
|
+
Object.assign(rec, { as_of: ev.as_of, source: ev.source, by: actor.by, via: actor.via, at: new Date(last).toISOString() });
|
|
168
|
+
return rec;
|
|
169
|
+
});
|
|
170
|
+
let file;
|
|
171
|
+
try { file = ledgers.ledgerPath(root, 'measurements'); fs.mkdirSync(path.dirname(file), { recursive: true }); }
|
|
172
|
+
catch { return refuse(500, 'could not record — nothing was saved', null); }
|
|
173
|
+
const guard = createSinkGuard({ sink: file, what: 'measurements' });
|
|
174
|
+
let wrote = 0;
|
|
175
|
+
for (const rec of recs) {
|
|
176
|
+
try { guard.appendLine(file, rec); wrote++; }
|
|
177
|
+
catch {
|
|
178
|
+
return refuse(500, wrote ? `could not record — ${wrote} of ${recs.length} lines were saved before the write failed` : 'could not record — nothing was saved', wrote);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return { ok: true, written: recs.length, lines: recs.map((r) => ({ eid: r.eid, anchor: r.anchor })) };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** A page press (requirePagePress's actor). */
|
|
185
|
+
function appendFromUi(root, events, { actor, now } = {}) { return append(root, events, actor, { now }); }
|
|
186
|
+
/** `pmos measure` (actor.fromCli's actor; an agent may write a measurement). */
|
|
187
|
+
function appendFromCli(root, events, { actor, now } = {}) { return append(root, events, actor, { now }); }
|
|
188
|
+
|
|
189
|
+
module.exports = { MAX_LINES, readMeasurements, latest, pmOnly, checkEvent, parseSource, lineErr, appendFromUi, appendFromCli };
|
|
@@ -17,13 +17,14 @@
|
|
|
17
17
|
*
|
|
18
18
|
* The screen model:
|
|
19
19
|
* { dest, kind, mode, id, dk, title, column }
|
|
20
|
-
* dest one of DESTS — which
|
|
21
|
-
* kind '
|
|
20
|
+
* dest one of DESTS — which tab this screen belongs to
|
|
21
|
+
* kind 'dest' | 'agent' (the frame) · 'list' | 'more' (the old shell) · 'detail' | 'terminal' | 'doc'
|
|
22
22
|
* mode a MODES key, for kind 'list'
|
|
23
|
-
* id the detail id / session id / viewer id
|
|
23
|
+
* id the frame destination (kind 'dest') / detail id / session id / viewer id
|
|
24
24
|
* dk the DETAIL kind, for kind 'detail'
|
|
25
25
|
* title only for 'terminal' and 'doc', where the name is not derivable
|
|
26
|
-
* column 'side' | 'term' | 'rail' — determined by `kind` and by nothing else
|
|
26
|
+
* column 'side' | 'term' | 'rail' — determined by `kind` and by nothing else (panelOf is the same for the four
|
|
27
|
+
* panel grid)
|
|
27
28
|
*
|
|
28
29
|
* `column` is the whole of the exclusivity guarantee: the stylesheet shows exactly one of the three
|
|
29
30
|
* column nodes per value, so P1 is a property of the CSS rather than something this reducer has to
|
|
@@ -65,87 +66,90 @@ const DETAIL_TITLE = {
|
|
|
65
66
|
addprod: 'add repository', new: 'file new',
|
|
66
67
|
};
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
/* ── the frame's five phone tabs (pm-os-absorb Run 3, slice 3A; PRD R15) ─────────────────────────────────────────
|
|
70
|
+
* Today · Initiatives · Board · Reports · Agent. The first four are the frame's destinations (a `dest` screen, drawn
|
|
71
|
+
* in the frame's place, panel index 0); Agent is the dock (an `agent` screen, panel 4, the live terminals). Settings
|
|
72
|
+
* is not a tab: it is pushed on top of the current tab as a `dest` screen with id `settings`, so Back returns.
|
|
73
|
+
* The old cockpit's list modes still map to a tab (MOBILE_DEST), for the old shell (fixture mode, ?layout=legacy). */
|
|
74
|
+
const MOBILE_DESTS = ['today', 'initiatives', 'board', 'reports', 'agent'];
|
|
75
|
+
// What each tab and Settings is called. The same words as the desktop bar (public/pm/frame-copy.js COPY.nav), which
|
|
76
|
+
// MOBNAV-P7 holds equal.
|
|
77
|
+
const DEST_TITLE = { today: 'Today', initiatives: 'Initiatives', board: 'Board', reports: 'Reports', agent: 'Agent', settings: 'Settings' };
|
|
78
|
+
// Every old MODES key has a tab. The suite ITERATES this map, so a mode without a phone path fails.
|
|
79
|
+
const MOBILE_DEST = { product: 'initiatives', scheduled: 'reports', agent: 'agent', memory: 'initiatives',
|
|
80
|
+
workflow: 'initiatives', skills: 'initiatives', knowledge: 'initiatives' };
|
|
74
81
|
const MOBILE_ROOT = {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
82
|
+
today: { kind: 'dest', id: 'today' },
|
|
83
|
+
initiatives: { kind: 'dest', id: 'initiatives' },
|
|
84
|
+
board: { kind: 'dest', id: 'board' },
|
|
85
|
+
reports: { kind: 'dest', id: 'reports' },
|
|
86
|
+
agent: { kind: 'agent', id: null },
|
|
80
87
|
};
|
|
88
|
+
// The tab every stored value of the retired five-tab nav (board · scheduled · agents · memory · more) and of the
|
|
89
|
+
// three-way switcher before it lands on. Read once from the old key; a new value is never passed through it.
|
|
90
|
+
const LEGACY_DEST = { board: 'initiatives', scheduled: 'reports', agents: 'agent', memory: 'today', more: 'today',
|
|
91
|
+
sessions: 'agent', terminal: 'agent', library: 'today' };
|
|
81
92
|
// kind → column. There is no second input, and no screen may carry a column its kind does not give.
|
|
82
|
-
const MOBILE_COLUMN = { list: 'side', more: 'side', detail: 'rail', terminal: 'term', doc: 'term' };
|
|
93
|
+
const MOBILE_COLUMN = { list: 'side', more: 'side', dest: 'side', detail: 'rail', terminal: 'term', doc: 'term', agent: 'term' };
|
|
83
94
|
function mobColumnOf(kind) { return MOBILE_COLUMN[kind] || 'side'; }
|
|
84
95
|
|
|
85
|
-
/* kind → PANEL INDEX
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
|
|
90
|
-
* and two of the old `side` screens belong to DIFFERENT panels there: a `list` screen is panel 2
|
|
91
|
-
* (the mode list) while a `more` screen is panel 1, which is the structure/settings panel and is
|
|
92
|
-
* therefore the phone's "more" destination in the four-panel design.
|
|
93
|
-
*
|
|
94
|
-
* So this is an ADDITIVE widening, not an edit: `columnOf` is untouched and still describes the
|
|
95
|
-
* three-column layout truthfully for the surface that owns it, and `panelOf` describes the
|
|
96
|
-
* four-panel one. Both are pure functions of `kind` alone, so the exclusivity guarantee — one
|
|
97
|
-
* visible surface per screen — is the same property stated against a different layout. Putting it
|
|
98
|
-
* HERE rather than in pm/mobile.js keeps a single node-walkable source of truth for "which surface
|
|
99
|
-
* does this screen show", which is the whole reason the reducer was pulled out of app.js.
|
|
100
|
-
*/
|
|
101
|
-
const MOBILE_PANEL = { more: 0, list: 1, detail: 2, terminal: 3, doc: 3 };
|
|
96
|
+
/* kind → PANEL INDEX: which child of the body grid a phone shows (pm/mobile.css `data-mpanel`). In the frame the body
|
|
97
|
+
* grid holds the place, the dock's rail (never shown on a phone), the detail and the dock, in that order; in the old
|
|
98
|
+
* shell it holds its four panels. `dest` is the frame's place, `agent` the dock. A pure function of `kind` alone, so
|
|
99
|
+
* one visible surface per screen is a property of the CSS. */
|
|
100
|
+
const MOBILE_PANEL = { dest: 0, more: 0, list: 1, detail: 2, terminal: 3, doc: 3, agent: 3 };
|
|
102
101
|
function mobPanelOf(kind) {
|
|
103
|
-
return Object.prototype.hasOwnProperty.call(MOBILE_PANEL, kind) ? MOBILE_PANEL[kind] :
|
|
102
|
+
return Object.prototype.hasOwnProperty.call(MOBILE_PANEL, kind) ? MOBILE_PANEL[kind] : 0;
|
|
104
103
|
}
|
|
105
104
|
function mobDestForMode(k) { return MOBILE_DEST[k] || null; }
|
|
106
105
|
// Complete a partial screen. The column is derived, never passed in, so a caller cannot put a
|
|
107
106
|
// detail on the terminal column by writing it down.
|
|
108
107
|
function mobScreen(s) {
|
|
109
|
-
const kind = (s && s.kind) || '
|
|
110
|
-
return { dest: (s && s.dest) || '
|
|
108
|
+
const kind = (s && s.kind) || 'dest';
|
|
109
|
+
return { dest: (s && s.dest) || 'today', kind, mode: (s && s.mode) || null,
|
|
111
110
|
id: (s && s.id) || null, dk: (s && s.dk) || null, title: (s && s.title) || null,
|
|
112
111
|
column: mobColumnOf(kind) };
|
|
113
112
|
}
|
|
114
113
|
function mobRootOf(dest) {
|
|
115
|
-
const
|
|
116
|
-
|
|
114
|
+
const d = MOBILE_ROOT[dest] ? dest : 'today';
|
|
115
|
+
const r = MOBILE_ROOT[d];
|
|
116
|
+
return mobScreen({ dest: d, kind: r.kind, id: r.id });
|
|
117
117
|
}
|
|
118
|
-
/* The title map. `
|
|
119
|
-
*
|
|
120
|
-
* a title because a session's name and a document's name are not derivable from a screen. */
|
|
118
|
+
/* The title map. `dest` and `agent` read DEST_TITLE; `list` and `more` derive (the old shell); `detail` reads
|
|
119
|
+
* DETAIL_TITLE; `terminal` and `doc` carry a title because a session's name and a document's name are not derivable. */
|
|
121
120
|
function mobTitleOf(s) {
|
|
122
121
|
if (!s) return '';
|
|
122
|
+
if (s.kind === 'dest') return DEST_TITLE[s.id] || DEST_TITLE[s.dest] || '';
|
|
123
|
+
if (s.kind === 'agent') return DEST_TITLE.agent;
|
|
123
124
|
if (s.kind === 'list') return MODE_TITLE[s.mode] || String(s.mode || '');
|
|
124
125
|
if (s.kind === 'more') return 'more';
|
|
125
126
|
if (s.kind === 'detail') return DETAIL_TITLE[s.dk] || 'detail';
|
|
126
127
|
return s.title || String(s.id || '');
|
|
127
128
|
}
|
|
128
|
-
function mobNavInit(dest) { return { dest: MOBILE_ROOT[dest] ? dest : '
|
|
129
|
+
function mobNavInit(dest) { return { dest: MOBILE_ROOT[dest] ? dest : 'today', stack: [] }; }
|
|
129
130
|
function mobPush(nav, screen) {
|
|
130
|
-
const n = nav && MOBILE_ROOT[nav.dest] ? nav : mobNavInit('
|
|
131
|
+
const n = nav && MOBILE_ROOT[nav.dest] ? nav : mobNavInit('today');
|
|
131
132
|
return { dest: n.dest, stack: n.stack.concat([mobScreen(Object.assign({ dest: n.dest }, screen))]) };
|
|
132
133
|
}
|
|
133
|
-
// Removes EXACTLY one. Back from a
|
|
134
|
-
// unwinds into another
|
|
134
|
+
// Removes EXACTLY one. Back from a tab's root is a no-op — it never exits the app and never
|
|
135
|
+
// unwinds into another tab.
|
|
135
136
|
function mobPop(nav) {
|
|
136
|
-
const n = nav && MOBILE_ROOT[nav.dest] ? nav : mobNavInit('
|
|
137
|
+
const n = nav && MOBILE_ROOT[nav.dest] ? nav : mobNavInit('today');
|
|
137
138
|
return n.stack.length ? { dest: n.dest, stack: n.stack.slice(0, -1) } : { dest: n.dest, stack: [] };
|
|
138
139
|
}
|
|
139
|
-
// Choosing a
|
|
140
|
+
// Choosing a tab RESETS it to its root with an empty stack.
|
|
140
141
|
function mobSwitchDest(nav, dest) { return mobNavInit(dest); }
|
|
141
142
|
function mobCurrentOf(nav) {
|
|
142
|
-
const n = nav && MOBILE_ROOT[nav.dest] ? nav : mobNavInit('
|
|
143
|
+
const n = nav && MOBILE_ROOT[nav.dest] ? nav : mobNavInit('today');
|
|
143
144
|
return n.stack.length ? n.stack[n.stack.length - 1] : mobRootOf(n.dest);
|
|
144
145
|
}
|
|
145
|
-
/* R11 —
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
function mobMigrateView(v) {
|
|
146
|
+
/* R11 — a value stored by an older nav is read ONCE, for migration. Every value the retired navs could hold, plus
|
|
147
|
+
* anything unrecognised and nothing at all, lands on a tab's root: the only screen that is safe to draw without
|
|
148
|
+
* re-reading anything. */
|
|
149
|
+
function mobMigrateView(v) {
|
|
150
|
+
if (typeof v === 'string' && Object.prototype.hasOwnProperty.call(LEGACY_DEST, v)) return LEGACY_DEST[v];
|
|
151
|
+
return MOBILE_ROOT[v] ? v : 'today';
|
|
152
|
+
}
|
|
149
153
|
|
|
150
154
|
return { push: mobPush, pop: mobPop, switchDest: mobSwitchDest, init: mobNavInit,
|
|
151
155
|
current: mobCurrentOf, titleOf: mobTitleOf, columnOf: mobColumnOf, panelOf: mobPanelOf,
|
|
@@ -153,5 +157,5 @@ function mobMigrateView(v) { return MOBILE_ROOT[v] ? v : 'board'; }
|
|
|
153
157
|
MODE_TITLE,
|
|
154
158
|
migrateView: mobMigrateView, screen: mobScreen, root: mobRootOf,
|
|
155
159
|
DESTS: MOBILE_DESTS, DEST_FOR_MODE: MOBILE_DEST, ROOTS: MOBILE_ROOT,
|
|
156
|
-
PANEL_OF_KIND: MOBILE_PANEL, DETAIL_TITLE };
|
|
160
|
+
PANEL_OF_KIND: MOBILE_PANEL, DETAIL_TITLE, DEST_TITLE, LEGACY_DEST };
|
|
157
161
|
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* needs-providers.js — the planning layer's needs-you rows, one file per run, folded into server.js deriveNeeds
|
|
3
|
+
* (pm-os-absorb Runs 2 to 5, PRD R12).
|
|
4
|
+
*
|
|
5
|
+
* rowsFor(w, { lead }) → rows every provider's rows for workspace `w`, each tagged { derived: true, ws, provider }
|
|
6
|
+
* sigFor(w) → string one signature over every provider (server.js needsSig), so a count is never stale
|
|
7
|
+
* isProviderRow(n) → boolean server.js keeps these rows out of derived state, next steps and the portfolio,
|
|
8
|
+
* as it keeps Run 1's plan rows out (notPlanNeed)
|
|
9
|
+
* PROVIDERS · RESERVED_KINDS · status()
|
|
10
|
+
*
|
|
11
|
+
* Four runs add needs rows (kind 8 "needs repair", kind 7 cycle rows, kind 4 work to check, kind 1 report to issue).
|
|
12
|
+
* Each adds ONE file, named in PROVIDERS before it exists, instead of a block in deriveNeeds. A PROVIDER exports:
|
|
13
|
+
* module.exports = {
|
|
14
|
+
* kinds: ['pm-line'], // the row kinds it makes; unique across providers
|
|
15
|
+
* rowsFor(w, { lead }) → [{ run_id, kind, initiative_id, question, doc?, today? }],
|
|
16
|
+
* sig(w) → string, // optional: changes whenever its rows may change
|
|
17
|
+
* };
|
|
18
|
+
* `w` is a registered workspace ({ id, root, … }). `lead` is true for the one registered tree that speaks for a
|
|
19
|
+
* repository's ledgers (server.js isPlanNeedsTree): a provider of repository-wide rows returns [] when it is false, or
|
|
20
|
+
* a repository with a linked worktree registered counts every row twice. `run_id` is unique and stable per row and
|
|
21
|
+
* starts with the provider's name: the needs queue de-duplicates by it.
|
|
22
|
+
* `today` (optional) is what a Today card needs, passed through /api/needs unchanged:
|
|
23
|
+
* { kind: 1..8, source: '<provider>', cause, evidence, act: { label, cli? } }
|
|
24
|
+
* A row the page receives never carries a session token: name an agent by its page name (`agent:sess-<n>`).
|
|
25
|
+
*
|
|
26
|
+
* A provider file that is not on disk yet is skipped. One that fails to load, claims a kind another uses, or throws
|
|
27
|
+
* gives ONE row of kind 'provider-error' that names it: a broken source is never an empty list (R10).
|
|
28
|
+
*/
|
|
29
|
+
const path = require('path');
|
|
30
|
+
|
|
31
|
+
const PROVIDERS = Object.freeze([
|
|
32
|
+
'pm-lines-needs', // Run 2 (2B): a PM-only line that is not attested (kind 8)
|
|
33
|
+
'initiative-needs', // Run 2 (2C): day one, agent proposals, passed check-by (kind 7); a record with no anchor (kind 8)
|
|
34
|
+
'work-check-needs', // Run 4 (4D): a person's item in Review, a batched Release (kind 4)
|
|
35
|
+
'report-needs', // Run 5 (5C): a report to issue (kind 1), cycle rows (kind 7), a schedule left no output (kind 8)
|
|
36
|
+
]);
|
|
37
|
+
/** Kinds that already mean something in the needs queue. A provider may not reuse one. */
|
|
38
|
+
const RESERVED_KINDS = Object.freeze(['question', 'accept', 'outcome', 'keep', 'evidence', 'plan-flag', 'provider-error']);
|
|
39
|
+
|
|
40
|
+
let table = null;
|
|
41
|
+
function load() {
|
|
42
|
+
if (table) return table;
|
|
43
|
+
const mods = [];
|
|
44
|
+
const broken = [];
|
|
45
|
+
const owner = new Map(RESERVED_KINDS.map((k) => [k, '(reserved)']));
|
|
46
|
+
for (const name of PROVIDERS) {
|
|
47
|
+
const full = path.join(__dirname, name + '.js');
|
|
48
|
+
let mod;
|
|
49
|
+
try { mod = require(full); }
|
|
50
|
+
catch (e) {
|
|
51
|
+
if (e && e.code === 'MODULE_NOT_FOUND' && String(e.message).includes(full)) continue; // not built yet
|
|
52
|
+
broken.push({ name, error: `failed to load: ${String((e && e.message) || e).slice(0, 160)}` });
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const kinds = mod && Array.isArray(mod.kinds) ? mod.kinds : null;
|
|
56
|
+
if (!kinds || !kinds.length || typeof mod.rowsFor !== 'function') { broken.push({ name, error: 'must export kinds and rowsFor' }); continue; }
|
|
57
|
+
const clash = kinds.find((k) => typeof k !== 'string' || owner.has(k));
|
|
58
|
+
if (clash !== undefined) { broken.push({ name, error: `kind ${JSON.stringify(clash)} is already used by ${owner.get(clash) || 'nobody (not a string)'}` }); continue; }
|
|
59
|
+
for (const k of kinds) owner.set(k, name);
|
|
60
|
+
mods.push({ name, mod, kinds });
|
|
61
|
+
}
|
|
62
|
+
table = { mods, broken };
|
|
63
|
+
return table;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const errorRow = (w, name, error) => ({
|
|
67
|
+
run_id: `${name}:error`, kind: 'provider-error', initiative_id: null, derived: true, doc: null, ws: w.id, provider: name,
|
|
68
|
+
question: `The needs source ${name} could not be read: ${error}`,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
function rowsFor(w, { lead = true } = {}) {
|
|
72
|
+
if (!w) return [];
|
|
73
|
+
const { mods, broken } = load();
|
|
74
|
+
const out = broken.map((b) => errorRow(w, b.name, b.error));
|
|
75
|
+
for (const { name, mod, kinds } of mods) {
|
|
76
|
+
let rows;
|
|
77
|
+
try { rows = mod.rowsFor(w, { lead: !!lead }); }
|
|
78
|
+
catch (e) { out.push(errorRow(w, name, String((e && e.message) || e).slice(0, 160))); continue; }
|
|
79
|
+
for (const r of Array.isArray(rows) ? rows : []) {
|
|
80
|
+
if (!r || typeof r.run_id !== 'string' || !kinds.includes(r.kind)) { out.push(errorRow(w, name, 'a row has no run_id or a kind it did not declare')); break; }
|
|
81
|
+
out.push({ ...r, derived: true, ws: w.id, provider: name });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Each provider's state-health lines (optional `banner(w, { lead })` → [{ source, error }]): a source that is still being
|
|
88
|
+
* read, not a row. Today shows them in its banner. */
|
|
89
|
+
function bannersFor(w, { lead = true } = {}) {
|
|
90
|
+
if (!w) return [];
|
|
91
|
+
const out = [];
|
|
92
|
+
for (const { name, mod } of load().mods) {
|
|
93
|
+
if (typeof mod.banner !== 'function') continue;
|
|
94
|
+
try { for (const b of mod.banner(w, { lead: !!lead }) || []) if (b && b.error) out.push({ source: String(b.source || name), error: String(b.error) }); }
|
|
95
|
+
catch { /* a banner that fails says nothing; its rows still report errors */ }
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function sigFor(w) {
|
|
101
|
+
if (!w) return '';
|
|
102
|
+
const { mods, broken } = load();
|
|
103
|
+
const parts = [`broken:${broken.length}`];
|
|
104
|
+
for (const { name, mod } of mods) {
|
|
105
|
+
if (typeof mod.sig !== 'function') { parts.push(`${name}:-`); continue; }
|
|
106
|
+
let s;
|
|
107
|
+
try { s = String(mod.sig(w)); } catch { s = 'unreadable'; }
|
|
108
|
+
parts.push(`${name}:${s}`);
|
|
109
|
+
}
|
|
110
|
+
return parts.join(',');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const isProviderRow = (n) => !!n && n.derived === true && typeof n.provider === 'string';
|
|
114
|
+
|
|
115
|
+
function status() {
|
|
116
|
+
const t = load();
|
|
117
|
+
return { loaded: t.mods.map((m) => ({ name: m.name, kinds: m.kinds.slice() })), broken: t.broken.map((b) => ({ ...b })) };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = { PROVIDERS, RESERVED_KINDS, rowsFor, bannersFor, sigFor, isProviderRow, status };
|