create-pmos 0.51.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 +5 -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 +117 -6
- 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 +20 -4
- 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,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* plan-ledgers.js — the four ledgers of the planning layer, where each one lives, and its one-stat signature
|
|
3
|
+
* (pm-os-absorb, D86, PRD R24).
|
|
4
|
+
*
|
|
5
|
+
* LEDGERS → { [name]: { file, writer, run } } plan-items, placements, measurements, issues
|
|
6
|
+
* ledgerPath(root, name) → absolute path of that ledger for the repository of `root`
|
|
7
|
+
* ledgerSig(root, name) → sha1 of size:mtime:ino, or 'none' when the file does not exist
|
|
8
|
+
* readLines(root, name) → { lines: [{ n, rec }], bad: [{ n, why }] } every line in file order, n counts from 1;
|
|
9
|
+
* no workflow engine → bad: [{ n: 0, why: 'engine missing: …' }]
|
|
10
|
+
* notBuilt(what, run) → the refusal a writer stub returns before its run has built it
|
|
11
|
+
*
|
|
12
|
+
* THE LINE ENVELOPE, the same in all four (lib/plan-items.js set it): { v: 1, eid, ev, …the event's fields…, by, via,
|
|
13
|
+
* at }. `eid` is 12 × [0-9a-z]; `by` is pm:<slug> | agent:unknown | agent:<session>; `via` is ui | cli; `at` is an ISO
|
|
14
|
+
* UTC time. The writer sets v, eid, by, via and at; a caller never sends them.
|
|
15
|
+
*
|
|
16
|
+
* Each ledger has ONE writer module (the `writer` below), and attribution comes from the call path inside that module,
|
|
17
|
+
* never from a field (lib/plan-items.js is the pattern). This file holds only what the four share, so a new writer, the
|
|
18
|
+
* checker (scripts/check-plan-ledgers.js), the board cache and the seam test read one list.
|
|
19
|
+
*
|
|
20
|
+
* ONE LEDGER PER REPOSITORY. The path is the runs directory (`planning/runs/` for PMOS-self, `pmos/state/` in a kit
|
|
21
|
+
* repository) of the MAIN working tree of `root`, whichever worktree writes — the same rule as plan-items.js
|
|
22
|
+
* ledgerRoot. A writer exercised from a linked worktree therefore writes into the main tree: tests use the synthetic
|
|
23
|
+
* repositories of plan-test-repos.js, never this checkout.
|
|
24
|
+
*
|
|
25
|
+
* Nothing here reads a ledger's lines or writes one. Nothing is cached (D33).
|
|
26
|
+
*/
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
const crypto = require('crypto');
|
|
30
|
+
const planTree = require('./plan-tree.js');
|
|
31
|
+
const consoleState = require('./console-state');
|
|
32
|
+
|
|
33
|
+
const LEDGERS = Object.freeze({
|
|
34
|
+
'plan-items': Object.freeze({ file: 'plan-items.jsonl', writer: 'workspace/lib/plan-items.js', run: 1 }),
|
|
35
|
+
placements: Object.freeze({ file: 'placements.jsonl', writer: 'workspace/lib/placements.js', run: 2 }),
|
|
36
|
+
measurements: Object.freeze({ file: 'measurements.jsonl', writer: 'workspace/lib/measurements.js', run: 2 }),
|
|
37
|
+
issues: Object.freeze({ file: 'issues.jsonl', writer: 'workspace/lib/issues.js', run: 5 }),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function spec(name) {
|
|
41
|
+
const s = Object.prototype.hasOwnProperty.call(LEDGERS, name) ? LEDGERS[name] : null;
|
|
42
|
+
if (!s) throw new Error(`plan-ledgers: no ledger named ${JSON.stringify(name)}`);
|
|
43
|
+
return s;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The runs directory of the main working tree of `root`. Throws when the workflow engine is not installed. */
|
|
47
|
+
function runsDir(root) {
|
|
48
|
+
const loaded = consoleState.loadEngine();
|
|
49
|
+
if (!loaded.ok) throw new Error(loaded.error);
|
|
50
|
+
return loaded.engine.layout(planTree.mainTree(path.resolve(String(root)))).runsDir;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function ledgerPath(root, name) { return path.join(runsDir(root), spec(name).file); }
|
|
54
|
+
|
|
55
|
+
function ledgerSig(root, name) {
|
|
56
|
+
let st;
|
|
57
|
+
try { st = fs.statSync(ledgerPath(root, name)); } catch { return 'none'; }
|
|
58
|
+
return crypto.createHash('sha1').update(`${st.size}:${st.mtimeMs}:${st.ino}`).digest('hex');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Every line of a ledger, parsed, in file order; a line that is not a JSON object is named in `bad`, never thrown. */
|
|
62
|
+
function readLines(root, name) {
|
|
63
|
+
let file;
|
|
64
|
+
// A missing workflow engine is NOT an empty ledger: it comes back as a bad line (n 0), so every reader's flags show it
|
|
65
|
+
// and a fold test cannot pass vacuously in a worktree without the vendored engine.
|
|
66
|
+
try { file = ledgerPath(root, name); } catch (e) { return { lines: [], bad: [{ n: 0, why: 'engine missing: ' + ((e && e.message) || String(e)) }] }; }
|
|
67
|
+
let text = '';
|
|
68
|
+
try { text = fs.readFileSync(file, 'utf8'); } catch { return { lines: [], bad: [] }; }
|
|
69
|
+
const lines = []; const bad = [];
|
|
70
|
+
text.split('\n').forEach((raw, i) => {
|
|
71
|
+
if (!raw.trim()) return;
|
|
72
|
+
let rec = null;
|
|
73
|
+
try { rec = JSON.parse(raw); } catch { bad.push({ n: i + 1, why: 'not JSON' }); return; }
|
|
74
|
+
if (!rec || typeof rec !== 'object' || Array.isArray(rec)) bad.push({ n: i + 1, why: 'not a JSON object' });
|
|
75
|
+
else lines.push({ n: i + 1, rec });
|
|
76
|
+
});
|
|
77
|
+
return { lines, bad };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The refusal of a writer whose run has not built it yet: the same shape as every writer refusal. */
|
|
81
|
+
function notBuilt(what, run) {
|
|
82
|
+
return { ok: false, status: 501, error: `${what} arrives in Run ${run} of pm-os-absorb`, index: null };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { LEDGERS, runsDir, ledgerPath, ledgerSig, readLines, notBuilt };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* plan-rules.js — the numbers the planning layer reads (pm-os-absorb, PRD R24): planning/plan-rules.json.
|
|
3
|
+
*
|
|
4
|
+
* readRules(root) → { stall_days, dormant_days, report_cycle_days, now_cap, via_required_from, …other keys }
|
|
5
|
+
* readRulesDetail(root) → { rules, path, errors, unknown } the same read, with what was wrong with the file
|
|
6
|
+
*
|
|
7
|
+
* One reader, so the Today ranking, the health rules, day one and the report window cannot each keep their own copy
|
|
8
|
+
* of "5 days". The file is committed with the repository and read from the tree that is shown (`root`), not from the
|
|
9
|
+
* main tree: a branch that changes a rule sees its own value. PMOS-self keeps it at planning/plan-rules.json; a kit
|
|
10
|
+
* repository at pmos/state/plan-rules.json. No file is not an error: every key reads its default (the PRD's values).
|
|
11
|
+
*
|
|
12
|
+
* A value that is not valid reads its default, and the reason is in `errors`; readRules never throws, because a
|
|
13
|
+
* typo in a rules file must not take the Today count or a report down with it. A key this reader does not know is
|
|
14
|
+
* passed through unchanged and named in `unknown` (a run that adds a key adds it to KEYS with its check). Keys that
|
|
15
|
+
* start with `_` are comments. Nothing is cached (D33): the file is small.
|
|
16
|
+
*/
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
// via_required_from (Run 2, R25(4), map-spec X10): the ISO UTC time from which a PM-only line must carry `via`.
|
|
21
|
+
// scripts/check-pm-lines.js fails a newer line without one and reports each older line once, never failing it.
|
|
22
|
+
// null means no time is set yet: every PM-only line is older, so none fails for a missing via.
|
|
23
|
+
const DEFAULTS = Object.freeze({ stall_days: 5, dormant_days: 45, report_cycle_days: 14, now_cap: null, via_required_from: null });
|
|
24
|
+
const posInt = (v) => Number.isInteger(v) && v > 0;
|
|
25
|
+
const isoUtc = (v) => typeof v === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z$/.test(v) && !Number.isNaN(Date.parse(v));
|
|
26
|
+
/** Each known key and the check its value must pass. */
|
|
27
|
+
const KEYS = Object.freeze({
|
|
28
|
+
stall_days: { ok: posInt, want: 'a whole number of days, 1 or more' },
|
|
29
|
+
dormant_days: { ok: posInt, want: 'a whole number of days, 1 or more' },
|
|
30
|
+
report_cycle_days: { ok: posInt, want: 'a whole number of days, 1 or more' },
|
|
31
|
+
now_cap: { ok: (v) => v === null || posInt(v), want: 'null (no cap) or a whole number, 1 or more' },
|
|
32
|
+
via_required_from: { ok: (v) => v === null || isoUtc(v), want: 'null (not set yet) or an ISO UTC time such as 2026-10-20T09:00:00Z' },
|
|
33
|
+
});
|
|
34
|
+
const CANDIDATES = [['planning', 'plan-rules.json'], ['pmos', 'state', 'plan-rules.json']];
|
|
35
|
+
|
|
36
|
+
/** The rules file of `root`, or null when it has none. */
|
|
37
|
+
function rulesPath(root) {
|
|
38
|
+
for (const parts of CANDIDATES) {
|
|
39
|
+
const f = path.join(path.resolve(String(root || '.')), ...parts);
|
|
40
|
+
try { if (fs.statSync(f).isFile()) return f; } catch { /* not this layout */ }
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Check a parsed rules object. Returns the rules with every bad or missing known key at its default. */
|
|
46
|
+
function validate(obj) {
|
|
47
|
+
const rules = { ...DEFAULTS };
|
|
48
|
+
const errors = [];
|
|
49
|
+
const unknown = [];
|
|
50
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return { rules, errors: ['the rules file is not a JSON object'], unknown };
|
|
51
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
52
|
+
if (k.startsWith('_')) continue;
|
|
53
|
+
const spec = KEYS[k];
|
|
54
|
+
if (!spec) { unknown.push(k); rules[k] = v; continue; }
|
|
55
|
+
if (spec.ok(v)) rules[k] = v;
|
|
56
|
+
else errors.push(`${k} is ${JSON.stringify(v)}; it must be ${spec.want}. The default ${JSON.stringify(DEFAULTS[k])} is used.`);
|
|
57
|
+
}
|
|
58
|
+
return { rules, errors, unknown };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readRulesDetail(root) {
|
|
62
|
+
const f = rulesPath(root);
|
|
63
|
+
if (!f) return { rules: { ...DEFAULTS }, path: null, errors: [], unknown: [] };
|
|
64
|
+
let obj;
|
|
65
|
+
try { obj = JSON.parse(fs.readFileSync(f, 'utf8')); }
|
|
66
|
+
catch (e) { return { rules: { ...DEFAULTS }, path: f, errors: [`${f} could not be read as JSON: ${String((e && e.message) || e).slice(0, 160)}`], unknown: [] }; }
|
|
67
|
+
return { ...validate(obj), path: f };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readRules(root) { return readRulesDetail(root).rules; }
|
|
71
|
+
|
|
72
|
+
module.exports = { DEFAULTS, KEYS, rulesPath, validate, readRules, readRulesDetail };
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* plan-tree.js — the stages an initiative's breakdown hangs under, and the layout the Breakdown pane draws.
|
|
3
|
+
*
|
|
4
|
+
* pm-os-absorb Run 1 (PRD R4, R5, R6, R18, R19). Pure apart from reading the initiative record and the
|
|
5
|
+
* repository's profile: the caller passes the folded ledger (lib/plan-items.js) and the ladder
|
|
6
|
+
* (lib/console-state.js `ladder()`), and this module groups them. Nothing here writes, and nothing is cached
|
|
7
|
+
* (D33): every answer is derived from what the caller read on this request.
|
|
8
|
+
*
|
|
9
|
+
* WHY THE STAGES ARE NOT A LIST OF THEIR OWN. A stage header groups the steps `scripts/workflow-state.js`
|
|
10
|
+
* derives for the initiative's lane and profile (R4). The step keys come from the engine (lane-defs
|
|
11
|
+
* RUNG_RULES), and the suite asserts that STAGE_GROUPS partitions them exactly, so a rung added to the
|
|
12
|
+
* engine without a stage here fails a test instead of disappearing from the tree.
|
|
13
|
+
*
|
|
14
|
+
* The owed row of a stage is the first step of its group that is still open — not done, n/a or unknown —
|
|
15
|
+
* and it is DERIVED: it ticks itself when the ladder sees its evidence, and nobody can edit it.
|
|
16
|
+
*/
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const consoleState = require('./console-state');
|
|
20
|
+
const factoryJoin = require('./factory-join.js');
|
|
21
|
+
|
|
22
|
+
const STAGE_GROUPS = Object.freeze({
|
|
23
|
+
'product-code': [
|
|
24
|
+
{ id: 'intake', label: 'Intake', steps: ['route', 'anchor', 'prototype', 'understand'] },
|
|
25
|
+
{ id: 'spec', label: 'Spec', steps: ['spec', 'priors'] },
|
|
26
|
+
{ id: 'contract', label: 'Contract', steps: ['contract'] },
|
|
27
|
+
{ id: 'build', label: 'Build', steps: ['build', 'quality'] },
|
|
28
|
+
{ id: 'evaluate', label: 'Evaluate', steps: ['review', 'explain', 'accept', 'outcome'] },
|
|
29
|
+
],
|
|
30
|
+
'pm-artifacts': [
|
|
31
|
+
{ id: 'intake', label: 'Intake', steps: ['route', 'anchor'] },
|
|
32
|
+
{ id: 'brief', label: null, steps: ['brief'] }, // the lane's brief_label, capitalised: "Problem brief"
|
|
33
|
+
{ id: 'draft', label: null, steps: ['draft'] }, // the lane's draft_label: "PRD", "Change specification"
|
|
34
|
+
{ id: 'evaluate', label: 'Evaluate', steps: ['review', 'approved'] },
|
|
35
|
+
{ id: 'handoff', label: 'Handoff', steps: ['handoff'] }, // dropped for a lane whose rungs have no handoff
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
// The noun an owed row names. `brief` and `draft` read the lane's own label at run time.
|
|
39
|
+
const STEP_NOUN = Object.freeze({
|
|
40
|
+
route: 'routing', anchor: 'anchor', prototype: 'prototype', understand: 'explainer', spec: 'spec', priors: 'priors',
|
|
41
|
+
contract: 'contract', build: 'build', quality: 'checks', review: 'review', explain: 'run explainer', accept: 'acceptance',
|
|
42
|
+
outcome: 'outcome check', brief: '<brief_label>', draft: '<draft_label>', approved: 'approval', handoff: 'handoff',
|
|
43
|
+
});
|
|
44
|
+
// Who owes the step. Everything not named here is the agent's.
|
|
45
|
+
const OWED_ACTOR = Object.freeze({ accept: 'you', approved: 'you', outcome: 'you', handoff: 'you' });
|
|
46
|
+
const OPEN = (s) => !['done', 'na', 'unknown'].includes(s);
|
|
47
|
+
const DONE_WINDOW_MS = 14 * 86400000;
|
|
48
|
+
// An initiative id is a record file name. Shared with lib/plan-items.js, which re-exports it.
|
|
49
|
+
const INIT_RE = /^[A-Za-z0-9][\w.-]{0,119}$/;
|
|
50
|
+
const STATE_LABEL = { todo: 'Todo', doing: 'Doing', review: 'Review', done: 'Done' };
|
|
51
|
+
|
|
52
|
+
function engine() {
|
|
53
|
+
const loaded = consoleState.loadEngine();
|
|
54
|
+
if (!loaded.ok) throw new Error(loaded.error);
|
|
55
|
+
return loaded.engine;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The main working tree of the repository `root` sits in. A linked worktree (an agent started by Hand to
|
|
60
|
+
* agent on a new branch runs in one) has a `.git` FILE naming its gitdir, whose `commondir` leads back to the
|
|
61
|
+
* main repository's `.git`. Filesystem reads only — no git process, because this runs on every poll.
|
|
62
|
+
*
|
|
63
|
+
* A `.git` FILE is followed ONLY for a real linked worktree of a non-bare repository whose main tree exists.
|
|
64
|
+
* Everything else keeps `root`: a submodule and a `--separate-git-dir` repository have no `commondir`; a worktree
|
|
65
|
+
* of a bare repository has a common directory that is not named `.git`. Following those would put the ledger
|
|
66
|
+
* outside the repository (or inside a git directory), where git, CI and every other clone never see it.
|
|
67
|
+
*/
|
|
68
|
+
function mainTree(root) {
|
|
69
|
+
const dotgit = path.join(root, '.git');
|
|
70
|
+
let st;
|
|
71
|
+
try { st = fs.statSync(dotgit); } catch { return root; }
|
|
72
|
+
if (st.isDirectory()) return root;
|
|
73
|
+
let m = null;
|
|
74
|
+
try { m = /^gitdir:\s*(.+)$/m.exec(fs.readFileSync(dotgit, 'utf8')); } catch { return root; }
|
|
75
|
+
if (!m) return root;
|
|
76
|
+
const gitdir = path.resolve(root, m[1].trim());
|
|
77
|
+
let common;
|
|
78
|
+
try { common = path.resolve(gitdir, fs.readFileSync(path.join(gitdir, 'commondir'), 'utf8').trim()); }
|
|
79
|
+
catch { return root; } // a submodule or a separate git directory
|
|
80
|
+
if (path.basename(common) !== '.git') return root; // a worktree of a bare repository
|
|
81
|
+
const main = path.dirname(common);
|
|
82
|
+
try { if (!fs.statSync(path.join(main, '.git')).isDirectory()) return root; } catch { return root; }
|
|
83
|
+
return main;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The initiative record, looked up in this tree first and then in the main tree. null when neither has it. */
|
|
87
|
+
function recordPath(root, initiative) {
|
|
88
|
+
if (typeof initiative !== 'string' || !INIT_RE.test(initiative)) return null;
|
|
89
|
+
// The folder's README.md and index.md are not records: the same rule as the engine's listInitiatives().
|
|
90
|
+
if (initiative === 'README' || initiative === 'index') return null;
|
|
91
|
+
const eng = engine();
|
|
92
|
+
const roots = [root];
|
|
93
|
+
const main = mainTree(root);
|
|
94
|
+
if (main !== root) roots.push(main);
|
|
95
|
+
for (const r of roots) {
|
|
96
|
+
const file = path.join(eng.layout(r).initiatives, `${initiative}.md`);
|
|
97
|
+
try { if (fs.statSync(file).isFile()) return file; } catch { /* try the next tree */ }
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The repository's profile, the initiative's lane (as workflow-state.js resolves it) and the lane definitions. */
|
|
103
|
+
function profileAndLane(root, initiative) {
|
|
104
|
+
const eng = engine();
|
|
105
|
+
const { profile, defs } = eng.lanesFor(root);
|
|
106
|
+
const file = recordPath(root, initiative);
|
|
107
|
+
let fm = {};
|
|
108
|
+
if (file) { try { fm = eng.parseLeadingFrontmatter(fs.readFileSync(file, 'utf8')) || {}; } catch { fm = {}; } }
|
|
109
|
+
const declared = String(fm.lane || '').trim();
|
|
110
|
+
// An unknown or missing lane is the profile's strictest, exactly as workflow-state.js computes `effLane`.
|
|
111
|
+
const fallback = profile === 'pm-artifacts' ? 'prd' : 'feature';
|
|
112
|
+
const lane = declared && defs[declared] ? declared : fallback;
|
|
113
|
+
return { profile, lane, defs, record: file };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const cap = (s) => (s && /^[a-z]/.test(s) ? s[0].toUpperCase() + s.slice(1) : s);
|
|
117
|
+
|
|
118
|
+
/** The stages of this initiative, in order: [{ id, label, steps }]. */
|
|
119
|
+
function stagesFor(root, initiative) {
|
|
120
|
+
const { profile, lane, defs } = profileAndLane(root, initiative);
|
|
121
|
+
if (profile !== 'pm-artifacts') return STAGE_GROUPS['product-code'].map((g) => ({ ...g, steps: g.steps.slice() }));
|
|
122
|
+
const def = defs[lane] || defs.prd;
|
|
123
|
+
const out = [];
|
|
124
|
+
for (const g of STAGE_GROUPS['pm-artifacts']) {
|
|
125
|
+
if (g.id === 'handoff' && !(def.rungs || []).includes('handoff')) continue;
|
|
126
|
+
const label = g.id === 'brief' ? cap(def.brief_label) : g.id === 'draft' ? cap(def.draft_label) : g.label;
|
|
127
|
+
out.push({ id: g.id, label, steps: g.steps.slice() });
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
function stageIdsFor(root, initiative) { return stagesFor(root, initiative).map((s) => s.id); }
|
|
132
|
+
|
|
133
|
+
/** A stage id from an id or a label, case- and space-insensitive. null when it names no stage of this initiative. */
|
|
134
|
+
function resolveStage(root, initiative, idOrLabel) {
|
|
135
|
+
const norm = (s) => String(s == null ? '' : s).toLowerCase().replace(/\s+/g, ' ').trim();
|
|
136
|
+
const want = norm(idOrLabel);
|
|
137
|
+
if (!want) return null;
|
|
138
|
+
for (const s of stagesFor(root, initiative)) if (norm(s.id) === want || norm(s.label) === want) return s.id;
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The noun an owed row uses for a step, with the lane's own brief and draft labels filled in. */
|
|
143
|
+
function nounFor(key, def) {
|
|
144
|
+
if (key === 'brief') return (def && def.brief_label) || 'brief';
|
|
145
|
+
if (key === 'draft') return (def && def.draft_label) || 'draft';
|
|
146
|
+
return STEP_NOUN[key] || key;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The owed row of a stage: the first step of its group that is still open. null when every step is done,
|
|
151
|
+
* n/a or unknown — those neither block nor count as owed (R3, R4).
|
|
152
|
+
*/
|
|
153
|
+
function owedFor(group, steps, def) {
|
|
154
|
+
const byKey = new Map((steps || []).map((s) => [s.key, s]));
|
|
155
|
+
for (const key of group.steps) {
|
|
156
|
+
const s = byKey.get(key);
|
|
157
|
+
if (!s || !OPEN(s.status)) continue;
|
|
158
|
+
const noun = nounFor(key, def);
|
|
159
|
+
const actor = OWED_ACTOR[key] || 'agent';
|
|
160
|
+
return { key, noun, actor, status: s.status, next: s.next || null, label: `Owed: ${noun} · ${actor}` };
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The progress label (R6), shared by the layout, the fold's progressOf and the CLI. Proposed and withdrawn items never
|
|
167
|
+
* count; when nothing is kept yet but an agent proposed items, it says so instead of a bare "no breakdown".
|
|
168
|
+
*/
|
|
169
|
+
function progressLabel(done, total, proposed) {
|
|
170
|
+
if (total) return `${done} of ${total}`;
|
|
171
|
+
return proposed ? `no breakdown · ${proposed} proposed` : 'no breakdown';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const byLabel = (by) => {
|
|
175
|
+
const s = String(by || '');
|
|
176
|
+
if (/^pm:/.test(s)) return 'you';
|
|
177
|
+
const m = /^agent:(sess-\d+)\./.exec(s);
|
|
178
|
+
if (m) return `agent · ${m[1]}`;
|
|
179
|
+
if (s === 'agent:unknown') return 'agent (unknown)';
|
|
180
|
+
return s || 'unknown';
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/** One history entry as a sentence. `stageLabelOf(id)` names a stage. */
|
|
184
|
+
function historyText(h, stageLabelOf) {
|
|
185
|
+
const lab = (id) => (stageLabelOf && stageLabelOf(id)) || id;
|
|
186
|
+
const st = (s) => STATE_LABEL[s] || s;
|
|
187
|
+
if (h.ev === 'planned') return `added in ${lab(h.stage)}`;
|
|
188
|
+
if (h.ev === 'moved') {
|
|
189
|
+
const ev = h.evidence ? ` with evidence ${h.evidence.kind}:${h.evidence.ref}` : '';
|
|
190
|
+
return `moved to ${st(h.to)}${ev}`;
|
|
191
|
+
}
|
|
192
|
+
if (h.ev === 'flagged') return h.blocked ? `blocked: ${h.reason}` : 'unblocked';
|
|
193
|
+
if (h.ev === 'withdrawn') return 'withdrawn';
|
|
194
|
+
if (h.ev === 'kept') return 'kept';
|
|
195
|
+
if (h.ev === 'set') {
|
|
196
|
+
const f = h.fields || {};
|
|
197
|
+
const parts = [];
|
|
198
|
+
for (const k of Object.keys(f)) {
|
|
199
|
+
const v = f[k];
|
|
200
|
+
if (k === 'stage') parts.push(`moved to stage ${lab(v)}`);
|
|
201
|
+
else if (k === 'order') parts.push('reordered');
|
|
202
|
+
else if (k === 'title') parts.push(`renamed to "${v}"`);
|
|
203
|
+
else if (k === 'due' || k === 'start') parts.push(v ? `${k} set to ${v}` : `${k} cleared`);
|
|
204
|
+
else if (k === 'owner') parts.push(`owner set to ${v}`);
|
|
205
|
+
else if (k === 'after') parts.push(Array.isArray(v) && v.length ? `after ${v.join(', ')}` : 'predecessors cleared');
|
|
206
|
+
else if (k === 'milestone') parts.push(v ? 'marked a milestone' : 'no longer a milestone');
|
|
207
|
+
else if (k === 'note') parts.push(v ? 'note changed' : 'note cleared');
|
|
208
|
+
else parts.push(`${k} changed`);
|
|
209
|
+
}
|
|
210
|
+
return parts.join('; ') || 'changed';
|
|
211
|
+
}
|
|
212
|
+
return h.ev;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const treeOrder = (a, b) => (a.order - b.order) || String(a.createdAt).localeCompare(String(b.createdAt)) || a.id.localeCompare(b.id);
|
|
216
|
+
|
|
217
|
+
/** What a plan item reads when it was dragged to Done and the factory has not integrated its work item (R7). */
|
|
218
|
+
const NOT_MERGED_LABEL = 'Done · your call · not merged';
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The factory join applied to the items of one initiative (PRD R7, D86 point 1; pm-os-absorb Run 4).
|
|
222
|
+
*
|
|
223
|
+
* A plan item that a work item materializes (its `declared` line names it as `plan_item`) carries
|
|
224
|
+
* `factory: { item, state, integrated, pr, notMerged, label }`. Once that work item is integrated (`pmos verify-work`
|
|
225
|
+
* saw its trailer on the default branch), the plan item reads Done with no hand edit: state `done`, and its Done time
|
|
226
|
+
* is the integration's unless it was already Done. A plan item moved to Done whose work item is NOT integrated stays
|
|
227
|
+
* Done, the PM's call, and says so (`notMerged`, `label`). The plan-items ledger is not written: the factory's own
|
|
228
|
+
* `integrated` line is the evidence (D86 point 1), so this is applied on every read. A withdrawn work item is not a
|
|
229
|
+
* join. An item no work item names is returned unchanged and gains no key, so the layout's shape holds for it.
|
|
230
|
+
*/
|
|
231
|
+
function withFactory(root, initiative, fold, items) {
|
|
232
|
+
let entry = null;
|
|
233
|
+
try { entry = factoryJoin.factoryOf(factoryJoin.joinFactory(root, { initiative, fold }), initiative); } catch { entry = null; }
|
|
234
|
+
if (!entry || !entry.items.length) return items;
|
|
235
|
+
const byPlan = new Map();
|
|
236
|
+
for (const w of entry.items) if (w.plan_item && w.state !== 'withdrawn' && !byPlan.has(w.plan_item)) byPlan.set(w.plan_item, w);
|
|
237
|
+
return items.map((it) => {
|
|
238
|
+
const w = byPlan.get(it.id);
|
|
239
|
+
if (!w) return it;
|
|
240
|
+
const notMerged = !w.integrated && it.state === 'done';
|
|
241
|
+
const factory = { item: w.id, state: w.state, integrated: !!w.integrated, pr: w.pr || null, notMerged, label: notMerged ? NOT_MERGED_LABEL : null };
|
|
242
|
+
if (!w.integrated) return { ...it, factory };
|
|
243
|
+
const doneAt = it.state === 'done' ? it.doneAt : (w.integrated_at || it.doneAt || null);
|
|
244
|
+
return { ...it, state: 'done', doneAt, noEvidence: false, late: false, factory };
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The Breakdown layout of one initiative (the shape the pane draws; see the Run 1 plan §2.2).
|
|
250
|
+
*
|
|
251
|
+
* @param {object} o
|
|
252
|
+
* @param {string} o.root the workspace root
|
|
253
|
+
* @param {string} o.initiative the initiative id
|
|
254
|
+
* @param {object} o.fold planItems.fold() result for the repository's ledger
|
|
255
|
+
* @param {object|null} o.ladder consoleState.ladder() result, or null for the fast path (no owed rows)
|
|
256
|
+
* @param {number} [o.now]
|
|
257
|
+
*/
|
|
258
|
+
function layout({ root, initiative, fold, ladder, now }) {
|
|
259
|
+
const t = Number.isFinite(now) ? now : Date.now();
|
|
260
|
+
const { profile, lane, defs } = profileAndLane(root, initiative);
|
|
261
|
+
const def = profile === 'pm-artifacts' ? (defs[lane] || defs.prd) : null;
|
|
262
|
+
const stages = stagesFor(root, initiative);
|
|
263
|
+
const stageIds = stages.map((s) => s.id);
|
|
264
|
+
const labelOf = (id) => { const s = stages.find((x) => x.id === id); return s ? s.label : id; };
|
|
265
|
+
const ladderIncluded = ladder !== null && ladder !== undefined;
|
|
266
|
+
let steps = null;
|
|
267
|
+
let ladderState = null;
|
|
268
|
+
if (ladderIncluded) {
|
|
269
|
+
if (ladder && ladder.ok && ladder.row && Array.isArray(ladder.row.steps)) {
|
|
270
|
+
steps = ladder.row.steps;
|
|
271
|
+
ladderState = { ok: true, error: null };
|
|
272
|
+
} else {
|
|
273
|
+
ladderState = { ok: false, error: (ladder && (ladder.error || (ladder.empty ? `no ladder for ${initiative}` : null))) || 'the ladder could not be derived' };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const mine = withFactory(root, initiative, fold,
|
|
278
|
+
(fold.order || []).map((id) => fold.items[id]).filter((it) => it && it.initiative === initiative));
|
|
279
|
+
const gone = new Set(fold.withdrawnIds || []);
|
|
280
|
+
const items = {};
|
|
281
|
+
const flags = [];
|
|
282
|
+
for (const it of mine) {
|
|
283
|
+
const stageKnown = stageIds.includes(it.stage);
|
|
284
|
+
if (!stageKnown) flags.push({ kind: 'unknown-stage', line: null, eid: null, item: it.id, message: `${it.id} names stage "${it.stage}", which this initiative's lane does not have — shown under Other` });
|
|
285
|
+
const { movedVia, ...shown } = it; // movedVia stays in the fold (needsFor reads it); the pane does not
|
|
286
|
+
items[it.id] = {
|
|
287
|
+
...shown, stageKnown, byLabel: byLabel(it.updatedBy),
|
|
288
|
+
// A withdrawn predecessor is not shown: the sheet then sends the list without it, and the writer lets it go.
|
|
289
|
+
after: it.after.filter((x) => !gone.has(x)),
|
|
290
|
+
// A planned entry carries its stage in the fold so its sentence can say where it was added; the pane gets
|
|
291
|
+
// the §1.4 history keys and the sentence, not the stage.
|
|
292
|
+
history: it.history.map(({ stage, ...h }) => ({ ...h, byLabel: byLabel(h.by), text: historyText({ ...h, stage }, labelOf) })),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
const visibleItems = mine;
|
|
296
|
+
const counted = (list) => list.filter((it) => !it.proposed);
|
|
297
|
+
|
|
298
|
+
let firstOpenSeen = false;
|
|
299
|
+
const stageRows = stages.map((s) => {
|
|
300
|
+
const inStage = visibleItems.filter((it) => it.stage === s.id).sort(treeOrder);
|
|
301
|
+
const real = counted(inStage);
|
|
302
|
+
const total = real.length;
|
|
303
|
+
const done = real.filter((it) => it.state === 'done').length;
|
|
304
|
+
const proposed = inStage.length - real.length;
|
|
305
|
+
const row = { id: s.id, label: s.label };
|
|
306
|
+
let owed = null;
|
|
307
|
+
if (steps) owed = owedFor(s, steps, def);
|
|
308
|
+
row.visible = total > 0 || proposed > 0 || owed !== null;
|
|
309
|
+
// A stage before the first open step is `done`; every stage after it is `later`, even one whose own steps
|
|
310
|
+
// are all done, because the work reaches it in ladder order. Without the ladder (`ladder=0`) `current`,
|
|
311
|
+
// `state` and `owed` are not derived, so they are left out: the client keeps them from its last full
|
|
312
|
+
// response. A ladder that was asked for but failed gives `current:false`, `state:null`, `owed:null`.
|
|
313
|
+
if (steps) {
|
|
314
|
+
const current = owed !== null && !firstOpenSeen;
|
|
315
|
+
row.current = current;
|
|
316
|
+
row.state = current ? 'current' : (firstOpenSeen ? 'later' : 'done');
|
|
317
|
+
if (owed !== null) firstOpenSeen = true;
|
|
318
|
+
} else if (ladderIncluded) {
|
|
319
|
+
row.current = false;
|
|
320
|
+
row.state = null;
|
|
321
|
+
}
|
|
322
|
+
Object.assign(row, { done, total, proposed, count: total > 0 ? `${done} of ${total}` : null });
|
|
323
|
+
if (ladderIncluded) row.owed = owed;
|
|
324
|
+
row.items = inStage.map((it) => it.id);
|
|
325
|
+
return row;
|
|
326
|
+
});
|
|
327
|
+
const otherItems = visibleItems.filter((it) => !stageIds.includes(it.stage)).sort(treeOrder);
|
|
328
|
+
|
|
329
|
+
const real = counted(visibleItems);
|
|
330
|
+
const pDone = real.filter((it) => it.state === 'done').length;
|
|
331
|
+
const progress = { done: pDone, total: real.length, proposed: visibleItems.length - real.length,
|
|
332
|
+
label: progressLabel(pDone, real.length, visibleItems.length - real.length) };
|
|
333
|
+
|
|
334
|
+
const stageIdx = (id) => { const i = stageIds.indexOf(id); return i === -1 ? stageIds.length : i; };
|
|
335
|
+
const boardSort = (a, b) => (stageIdx(a.stage) - stageIdx(b.stage)) || treeOrder(a, b);
|
|
336
|
+
const board = { todo: [], doing: [], review: [], done: [], doneOlder: 0 };
|
|
337
|
+
for (const st of ['todo', 'doing', 'review', 'done']) {
|
|
338
|
+
let list = visibleItems.filter((it) => it.state === st);
|
|
339
|
+
if (st === 'done') {
|
|
340
|
+
const recent = list.filter((it) => it.doneAt && Date.parse(it.doneAt) >= t - DONE_WINDOW_MS);
|
|
341
|
+
board.doneOlder = list.length - recent.length;
|
|
342
|
+
list = recent;
|
|
343
|
+
}
|
|
344
|
+
board[st] = list.sort(boardSort).map((it) => it.id);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const batches = (fold.batches || []).filter((b) => b.initiative === initiative)
|
|
348
|
+
.map((b) => ({ key: b.by, by: b.by, byLabel: b.byLabel, count: b.count, items: b.items.slice(), firstAt: b.firstAt, lastAt: b.lastAt }));
|
|
349
|
+
for (const f of fold.flags || []) {
|
|
350
|
+
if (f.initiative && f.initiative !== initiative) continue;
|
|
351
|
+
flags.push({ kind: f.kind, line: f.line, eid: f.eid, item: f.item, message: f.message });
|
|
352
|
+
}
|
|
353
|
+
const withdrawnCount = (fold.withdrawnIds || []).filter((id) => id.startsWith(initiative + '.')
|
|
354
|
+
&& /^[0-9a-z]{10}$/.test(id.slice(initiative.length + 1))).length;
|
|
355
|
+
|
|
356
|
+
const out = { profile, lane, ladderIncluded };
|
|
357
|
+
if (ladderIncluded) out.ladder = ladderState;
|
|
358
|
+
Object.assign(out, {
|
|
359
|
+
progress, stages: stageRows, other: { items: otherItems.map((it) => it.id) }, items, board, batches, flags, withdrawnCount,
|
|
360
|
+
});
|
|
361
|
+
return out;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
module.exports = {
|
|
365
|
+
STAGE_GROUPS, STEP_NOUN, OWED_ACTOR, OPEN, DONE_WINDOW_MS, INIT_RE, NOT_MERGED_LABEL, withFactory,
|
|
366
|
+
mainTree, recordPath, profileAndLane, stagesFor, stageIdsFor, resolveStage, owedFor, nounFor, byLabel, progressLabel, layout, historyText,
|
|
367
|
+
};
|