create-pmos 0.134.0 → 0.136.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/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/payload/state/log.md +1 -1
- package/workspace/bin/notes-replay.js +235 -0
- package/workspace/c3-briefing-test.js +130 -0
- package/workspace/c3-foundation-test.js +159 -0
- package/workspace/c3-ib-items-test.js +105 -0
- package/workspace/c3-ib-more-test.js +120 -0
- package/workspace/c3-ib-triage-backlog-test.js +119 -0
- package/workspace/c3-ini-harness.js +66 -0
- package/workspace/c3-initiatives-test.js +140 -0
- package/workspace/c3-one-home-test.js +14 -1
- package/workspace/c3-reports-test.js +57 -18
- package/workspace/c3-settings-test.js +139 -0
- package/workspace/c3-work-test.js +42 -10
- package/workspace/fixtures/one-home-baseline.json +1 -9
- package/workspace/guide/briefing-cards.md +15 -3
- package/workspace/guide/briefing.md +12 -8
- package/workspace/guide/calendar-popup.md +2 -2
- package/workspace/guide/console-calendar.md +39 -10
- package/workspace/guide/index.md +1 -0
- package/workspace/guide/initiative-boards.md +65 -0
- package/workspace/guide/jira.md +3 -1
- package/workspace/guide/layout.md +2 -0
- package/workspace/guide/my-work.md +5 -3
- package/workspace/guide/notes.md +16 -1
- package/workspace/guide/reports-dashboards.md +29 -5
- package/workspace/guide/settings.md +68 -0
- package/workspace/guide/welcome.md +10 -10
- package/workspace/lib/calendar-cli.js +3 -2
- package/workspace/lib/calendar-layers.js +21 -10
- package/workspace/lib/calendar-tidy.js +230 -0
- package/workspace/lib/engine/boards.js +542 -0
- package/workspace/lib/engine/breakdown-ops.js +180 -0
- package/workspace/lib/engine/calendar-import.js +84 -0
- package/workspace/lib/engine/calendar-model.js +185 -0
- package/workspace/lib/engine/card-state.js +92 -0
- package/workspace/lib/engine/derived/derive.js +92 -0
- package/workspace/lib/engine/derived/plan.js +3 -0
- package/workspace/lib/engine/derived/trust.js +3 -0
- package/workspace/lib/engine/doc-health.js +142 -0
- package/workspace/lib/engine/doc-sheet.js +75 -0
- package/workspace/lib/engine/intake.js +78 -0
- package/workspace/lib/engine/outbox.js +86 -0
- package/workspace/lib/engine/plan-forecast.js +342 -0
- package/workspace/lib/engine/promises.js +29 -0
- package/workspace/lib/engine/records/index.js +114 -0
- package/workspace/lib/engine/releases.js +96 -0
- package/workspace/lib/engine/replies.js +99 -0
- package/workspace/lib/engine/reply-contract/reply-contract.js +372 -0
- package/workspace/lib/engine/report-families.js +175 -0
- package/workspace/lib/engine/roadmap-ops.js +150 -0
- package/workspace/lib/engine/sessions-list.js +27 -0
- package/workspace/lib/engine/signal-rules.js +95 -0
- package/workspace/lib/engine/signals/cards.js +57 -0
- package/workspace/lib/engine/signals/detectors.js +370 -0
- package/workspace/lib/engine/signals/facts-cli.js +8 -0
- package/workspace/lib/engine/signals/facts.js +197 -0
- package/workspace/lib/engine/signals/index.js +33 -0
- package/workspace/lib/engine/signals/kinds.js +110 -0
- package/workspace/lib/engine/signals/layout-paths.js +44 -0
- package/workspace/lib/engine/signals/options.js +24 -0
- package/workspace/lib/engine/signals/score.js +72 -0
- package/workspace/lib/engine/signals/select.js +47 -0
- package/workspace/lib/engine/step-planner.js +65 -0
- package/workspace/lib/engine/trust/claim-rules.json +93 -0
- package/workspace/lib/engine/trust/import-calendar.js +21 -0
- package/workspace/lib/engine/trust/import-guard.js +36 -0
- package/workspace/lib/engine/trust/ledger.js +53 -0
- package/workspace/lib/engine/trust/outbox.js +45 -0
- package/workspace/lib/engine/trust/plan.js +5 -0
- package/workspace/lib/engine/trust/trust.js +21 -0
- package/workspace/lib/engine/truth-table.js +57 -0
- package/workspace/lib/engine/workspace-lib.js +24 -0
- package/workspace/lib/factory-join.js +1 -1
- package/workspace/lib/facts-async.js +4 -4
- package/workspace/lib/headless.js +95 -0
- package/workspace/lib/import-overlay.js +2 -1
- package/workspace/lib/jira-status.js +30 -3
- package/workspace/lib/notes-capture.js +9 -1
- package/workspace/lib/notes-final.js +38 -13
- package/workspace/lib/notes-replay.js +125 -0
- package/workspace/lib/notes-screen.js +180 -0
- package/workspace/lib/notes-whisper.js +10 -3
- package/workspace/lib/outbox-store.js +10 -6
- package/workspace/lib/pmos-verbs.js +3 -1
- package/workspace/lib/proactive-cli.js +7 -0
- package/workspace/lib/proactive.js +15 -2
- package/workspace/lib/records-dir.js +29 -0
- package/workspace/lib/repo-script.js +36 -0
- package/workspace/lib/report-dashboards.js +5 -2
- package/workspace/lib/report-frame.js +122 -0
- package/workspace/lib/report-schedules.js +113 -0
- package/workspace/lib/route-modules.js +9 -2
- package/workspace/lib/routes-boards.js +734 -0
- package/workspace/lib/routes-brief-page.js +13 -4
- package/workspace/lib/routes-briefing.js +22 -11
- package/workspace/lib/routes-c3-init.js +9 -8
- package/workspace/lib/routes-c3-life.js +2 -1
- package/workspace/lib/routes-c3-reports.js +99 -15
- package/workspace/lib/routes-c3-strategy.js +7 -5
- package/workspace/lib/routes-c3-timeline.js +3 -2
- package/workspace/lib/routes-calendar-import.js +3 -2
- package/workspace/lib/routes-calendar-views.js +9 -4
- package/workspace/lib/routes-doc-health.js +2 -1
- package/workspace/lib/routes-doc-sheet.js +2 -1
- package/workspace/lib/routes-forecast.js +2 -1
- package/workspace/lib/routes-hooks.js +3 -2
- package/workspace/lib/routes-initiatives.js +2 -1
- package/workspace/lib/routes-intake.js +2 -1
- package/workspace/lib/routes-notes.js +55 -9
- package/workspace/lib/routes-outbox.js +23 -3
- package/workspace/lib/routes-pm-items.js +19 -6
- package/workspace/lib/routes-records.js +10 -7
- package/workspace/lib/routes-releases.js +2 -1
- package/workspace/lib/routes-report-facts.js +4 -3
- package/workspace/lib/routes-report-families.js +2 -1
- package/workspace/lib/routes-roadmap-ops.js +17 -14
- package/workspace/lib/routes-runs.js +6 -5
- package/workspace/lib/routes-settings.js +51 -0
- package/workspace/lib/routes-signals.js +3 -2
- package/workspace/lib/routes-stage-tools.js +220 -0
- package/workspace/lib/run-answers.js +2 -1
- package/workspace/lib/settings-store.js +226 -0
- package/workspace/lib/stage-tools.js +208 -0
- package/workspace/lib/vendor/scripts/signals/layout-paths.js +2 -1
- package/workspace/lib/vendor/scripts/workspace-lib.js +24 -0
- package/workspace/lib/work-evidence.js +73 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/briefing-deck.js +63 -17
- package/workspace/public/pm/briefing-model.js +36 -7
- package/workspace/public/pm/c3-ambient.js +100 -0
- package/workspace/public/pm/c3-brief-needs.js +26 -6
- package/workspace/public/pm/c3-brief-pulse.js +12 -4
- package/workspace/public/pm/c3-brief-today.js +56 -21
- package/workspace/public/pm/c3-brief.js +2 -1
- package/workspace/public/pm/c3-calendar.js +161 -42
- package/workspace/public/pm/c3-calpop.js +5 -5
- package/workspace/public/pm/c3-demo-boards.js +649 -0
- package/workspace/public/pm/c3-demo-brief-page.js +1 -1
- package/workspace/public/pm/c3-demo-cal4.js +3 -2
- package/workspace/public/pm/c3-demo-cal5.js +52 -0
- package/workspace/public/pm/c3-demo-data.js +17 -27
- package/workspace/public/pm/c3-demo-rep3.js +46 -5
- package/workspace/public/pm/c3-demo.js +2 -1
- package/workspace/public/pm/c3-dock.js +1 -1
- package/workspace/public/pm/c3-ib-docs.js +80 -0
- package/workspace/public/pm/c3-ib-initiatives.js +134 -0
- package/workspace/public/pm/c3-ib-items.js +201 -0
- package/workspace/public/pm/c3-ib-releases.js +51 -0
- package/workspace/public/pm/c3-ib-sprints.js +55 -0
- package/workspace/public/pm/c3-ib-triage.js +178 -0
- package/workspace/public/pm/c3-ii-tabs.js +240 -0
- package/workspace/public/pm/c3-ini-records.js +197 -0
- package/workspace/public/pm/c3-ini-timeline.js +278 -0
- package/workspace/public/pm/c3-ini-ui.js +229 -0
- package/workspace/public/pm/c3-ini-work.js +139 -0
- package/workspace/public/pm/c3-initiatives.css +218 -0
- package/workspace/public/pm/c3-initiatives.js +741 -0
- package/workspace/public/pm/c3-plan.js +2 -2
- package/workspace/public/pm/c3-reports.css +47 -47
- package/workspace/public/pm/c3-reports.js +223 -185
- package/workspace/public/pm/c3-settings.css +74 -0
- package/workspace/public/pm/c3-settings.js +703 -0
- package/workspace/public/pm/c3-shell.js +102 -36
- package/workspace/public/pm/c3-terminal-icons.js +2 -2
- package/workspace/public/pm/c3-terminal.js +2 -2
- package/workspace/public/pm/calendar-views.js +1 -1
- package/workspace/public/pm/components.css +319 -213
- package/workspace/public/pm/console3.css +99 -23
- package/workspace/public/pm/dest-notes.js +26 -4
- package/workspace/public/pm/notes-screen-capture.js +165 -0
- package/workspace/public/pm/notes-screen-view.js +98 -0
- package/workspace/public/pm/panel-trees.js +5 -2
- package/workspace/public/pm/pm-work-view.js +233 -146
- package/workspace/public/pm/shell-bell.js +3 -2
- package/workspace/public/pm/shell-recording.js +29 -8
- package/workspace/public/pm/tokens.css +2 -2
- package/workspace/public/pm/view-notes.js +46 -9
- package/workspace/public/pm/welcome.js +197 -89
- package/workspace/public/welcome.html +6 -4
- package/workspace/server.js +105 -6
- package/workspace/skills/day-check.skill +1 -1
- package/workspace/skills/jira-sync.skill +6 -4
- package/workspace/skills/morning-brief.skill +1 -1
- package/workspace/test-files.json +12 -3
- package/workspace/test-lists/c3-ib-items.json +9 -0
- package/workspace/test-lists/c3-ib-more.json +9 -0
- package/workspace/test-lists/c3-ib-triage-backlog.json +9 -0
- package/workspace/test-lists/c3-initiative-boards.json +6 -0
- package/workspace/test-lists/calendar-tidy-up.json +6 -0
- package/workspace/test-lists/console3-briefing.json +9 -0
- package/workspace/test-lists/console3-foundation.json +9 -0
- package/workspace/test-lists/console3-reports.json +3 -3
- package/workspace/test-lists/meeting-notes.json +10 -4
- package/workspace/test-lists/workspace-headless-jobs.json +5 -0
- package/workspace/test-lists/workspace-package-script-closure.json +6 -0
- package/workspace/lib/vendor/workspace/lib/actor.js +0 -91
- package/workspace/lib/vendor/workspace/lib/board-cache.js +0 -156
- package/workspace/lib/vendor/workspace/lib/console-state.js +0 -295
- package/workspace/lib/vendor/workspace/lib/factory-join.js +0 -158
- package/workspace/lib/vendor/workspace/lib/git-info.js +0 -306
- package/workspace/lib/vendor/workspace/lib/plan-items.js +0 -922
- package/workspace/lib/vendor/workspace/lib/plan-tree.js +0 -419
- package/workspace/lib/vendor/workspace/lib/review-gate.js +0 -39
- package/workspace/lib/vendor/workspace/lib/sink.js +0 -321
- package/workspace/lib/vendor/workspace/lib/state-root.js +0 -587
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* doc-health.js — console3 wave B2: the six document health checks, a word diff, and a revision compare. Pure.
|
|
3
|
+
*
|
|
4
|
+
* healthOf(doc, context) is recomputed from the text on every call and is never stored.
|
|
5
|
+
* doc = { text, type?, changed? (ISO date of the last change) }
|
|
6
|
+
* context = { now?, staleAfterDays?, sources?: [{ path, changed, kind? }], decisionIds?: [id], decisionChanges?: { id: ISO },
|
|
7
|
+
* requiredHeadings?: { <type>: [heading] }, data?: { <name>: number }, readers?: [{ name, seen }], current? }
|
|
8
|
+
* A check whose input is not given returns { pass: null, reason: 'not enough input' }; it is neither passed nor failed,
|
|
9
|
+
* so `total` counts only the checks that could be answered.
|
|
10
|
+
*/
|
|
11
|
+
const { citedSummary, LABELS } = require('./releases.js');
|
|
12
|
+
const V = require('./derived/derive.js');
|
|
13
|
+
|
|
14
|
+
/* BACKED BY THE PM'S SAMPLE (derived/derive.js, from reader.js sections 3 and 4): the `complete` check below, and graphHealth()
|
|
15
|
+
* for a whole portfolio (document graph + six checks over frontmatter revisions). REPO-WRITTEN, because the sample derives
|
|
16
|
+
* health from frontmatter revs, not from text and git dates: the text/git-based fresh, decisions, data, traced and readers
|
|
17
|
+
* checks in healthOf, the six-check wording ('not enough input' reasons), wordDiff and historyCompare. */
|
|
18
|
+
|
|
19
|
+
const NO_INPUT = 'not enough input';
|
|
20
|
+
const na = (id) => ({ id, pass: null, reason: NO_INPUT });
|
|
21
|
+
const day = 86400000;
|
|
22
|
+
const ts = (v) => { const t = Date.parse(v); return Number.isNaN(t) ? null : t; };
|
|
23
|
+
|
|
24
|
+
function fresh(doc, c) {
|
|
25
|
+
const at = ts(doc.changed);
|
|
26
|
+
if (at == null) return na('fresh');
|
|
27
|
+
const newer = (c.sources || []).filter((s) => ts(s.changed) != null && ts(s.changed) > at).map((s) => s.path);
|
|
28
|
+
if (newer.length) return { id: 'fresh', pass: false, reason: 'a cited source changed after this document: ' + newer.join(', ') };
|
|
29
|
+
if (c.staleAfterDays != null) {
|
|
30
|
+
const now = ts(c.now) != null ? ts(c.now) : Date.now();
|
|
31
|
+
const age = Math.floor((now - at) / day);
|
|
32
|
+
return age > c.staleAfterDays
|
|
33
|
+
? { id: 'fresh', pass: false, reason: `last changed ${age} days ago; stale after ${c.staleAfterDays}` }
|
|
34
|
+
: { id: 'fresh', pass: true, reason: `changed ${age} days ago; stale after ${c.staleAfterDays}` };
|
|
35
|
+
}
|
|
36
|
+
if (!(c.sources || []).some((s) => ts(s.changed) != null)) return na('fresh');
|
|
37
|
+
return { id: 'fresh', pass: true, reason: 'no cited source changed after it' };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function matchesDecisions(doc, c) {
|
|
41
|
+
const at = ts(doc.changed);
|
|
42
|
+
if (at == null || !c.decisionChanges) return na('decisions');
|
|
43
|
+
const ids = c.decisionIds || Object.keys(c.decisionChanges);
|
|
44
|
+
const known = ids.filter((id) => ts(c.decisionChanges[id]) != null);
|
|
45
|
+
if (!known.length) return na('decisions');
|
|
46
|
+
const newer = known.filter((id) => ts(c.decisionChanges[id]) > at);
|
|
47
|
+
return newer.length
|
|
48
|
+
? { id: 'decisions', pass: false, reason: 'decision updated after this document: ' + newer.join(', ') }
|
|
49
|
+
: { id: 'decisions', pass: true, reason: known.length + ' referenced decision(s), none newer' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const numbersIn = (t) => (String(t).match(/\d[\d,]*(?:\.\d+)?/g) || []).map((n) => Number(n.replace(/,/g, '')));
|
|
53
|
+
|
|
54
|
+
function matchesData(doc, c) {
|
|
55
|
+
if (!c.data || !Object.keys(c.data).length) return na('data');
|
|
56
|
+
const miss = [];
|
|
57
|
+
for (const [name, v] of Object.entries(c.data)) {
|
|
58
|
+
const line = String(doc.text).split('\n').find((l) => l.toLowerCase().includes(name.toLowerCase()));
|
|
59
|
+
if (!line) continue; // the figure is not quoted: nothing to compare
|
|
60
|
+
if (!numbersIn(line).includes(Number(v))) miss.push(`${name}: data says ${v}, text says ${numbersIn(line).join(' / ') || 'no number'}`);
|
|
61
|
+
}
|
|
62
|
+
return miss.length ? { id: 'data', pass: false, reason: miss.join('; ') } : { id: 'data', pass: true, reason: 'every quoted figure matches its data file' };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function complete(doc, c) {
|
|
66
|
+
const req = c.requiredHeadings && (c.requiredHeadings[doc.type] || (Array.isArray(c.requiredHeadings) ? c.requiredHeadings : null));
|
|
67
|
+
if (!req || !req.length) return na('complete');
|
|
68
|
+
// the sample's rule (reader.js section 4): each required section is a level-2 heading, "## <name>", case-sensitive
|
|
69
|
+
const ok = V.docHealth('x', { x: { type: 'x', rev: 1, _body: String(doc.text) } }, [], [], { x: req }, '').complete;
|
|
70
|
+
const missing = req.filter((h) => !String(doc.text).includes('## ' + h));
|
|
71
|
+
return ok ? { id: 'complete', pass: true, reason: 'all ' + req.length + ' required headings are present' }
|
|
72
|
+
: { id: 'complete', pass: false, reason: 'missing headings: ' + missing.join(', ') };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** graphHealth(docs, findings, readers, need, now) → { edges, health: { id: { fresh, decisions, data, complete, traced, readers } } }.
|
|
76
|
+
* The sample's whole-portfolio derivation: docs { id: { type, rev, sources:[{id,rev}], stale_after, tickets, _body } }. */
|
|
77
|
+
function graphHealth(docs, findings, readers, need, now) {
|
|
78
|
+
const edges = V.docGraph(docs, findings);
|
|
79
|
+
const health = {};
|
|
80
|
+
for (const id of Object.keys(docs)) health[id] = V.docHealth(id, docs, edges, readers, need, now);
|
|
81
|
+
return { edges, health };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// A claim line is a body line with a figure in it. It is traced when it names a label or a ticket key.
|
|
85
|
+
function sourceOf(line) {
|
|
86
|
+
const t = /\b[A-Z][A-Z0-9]+-\d+\b/.exec(line);
|
|
87
|
+
if (t) return t[0];
|
|
88
|
+
const l = new RegExp('[\\[(](' + LABELS.join('|') + ')[\\])]', 'i').exec(line);
|
|
89
|
+
return l ? l[1].toLowerCase() : null;
|
|
90
|
+
}
|
|
91
|
+
function traced(doc) {
|
|
92
|
+
const claims = String(doc.text).split('\n').filter((l) => l.trim() && !/^#/.test(l) && /\d/.test(l));
|
|
93
|
+
if (!claims.length) return { id: 'traced', pass: true, reason: 'no claim lines (a line with a figure)' };
|
|
94
|
+
const r = citedSummary(claims.map((l) => ({ text: l, source: sourceOf(l) })));
|
|
95
|
+
return r.ok ? { id: 'traced', pass: true, reason: claims.length + ' claim line(s), all cited' }
|
|
96
|
+
: { id: 'traced', pass: false, reason: r.refused.length + ' of ' + claims.length + ' claim line(s) carry no label or ticket: ' + r.refused.slice(0, 3).map((i) => `"${claims[i].trim().slice(0, 40)}"`).join(', ') };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function readersCurrent(doc, c) {
|
|
100
|
+
if (!Array.isArray(c.readers) || c.current == null) return na('readers');
|
|
101
|
+
if (!c.readers.length) return { id: 'readers', pass: true, reason: 'no readers listed' };
|
|
102
|
+
const behind = c.readers.filter((r) => r.seen !== c.current).map((r) => r.name);
|
|
103
|
+
return behind.length ? { id: 'readers', pass: false, reason: 'not on the current version: ' + behind.join(', ') }
|
|
104
|
+
: { id: 'readers', pass: true, reason: 'every reader has the current version' };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function healthOf(doc, context = {}) {
|
|
108
|
+
const d = doc || {};
|
|
109
|
+
const checks = [fresh(d, context), matchesDecisions(d, context), matchesData(d, context), complete(d, context), traced(d), readersCurrent(d, context)];
|
|
110
|
+
const answered = checks.filter((k) => k.pass !== null);
|
|
111
|
+
return { passed: answered.filter((k) => k.pass).length, total: answered.length, checks };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const words = (s) => String(s || '').split(/\s+/).filter(Boolean);
|
|
115
|
+
|
|
116
|
+
function wordDiff(before, after) {
|
|
117
|
+
const a = words(before), b = words(after);
|
|
118
|
+
const n = a.length, m = b.length;
|
|
119
|
+
const L = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
120
|
+
for (let i = n - 1; i >= 0; i--) for (let j = m - 1; j >= 0; j--) L[i][j] = a[i] === b[j] ? L[i + 1][j + 1] + 1 : Math.max(L[i + 1][j], L[i][j + 1]);
|
|
121
|
+
const out = [];
|
|
122
|
+
const push = (op, text) => { const last = out[out.length - 1]; if (last && last.op === op) last.text += ' ' + text; else out.push({ op, text }); };
|
|
123
|
+
let i = 0, j = 0;
|
|
124
|
+
while (i < n && j < m) {
|
|
125
|
+
if (a[i] === b[j]) { push('same', a[i]); i++; j++; }
|
|
126
|
+
else if (L[i + 1][j] >= L[i][j + 1]) push('del', a[i++]);
|
|
127
|
+
else push('add', b[j++]);
|
|
128
|
+
}
|
|
129
|
+
while (i < n) push('del', a[i++]);
|
|
130
|
+
while (j < m) push('add', b[j++]);
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// revisions: [{ id, text }]. a and b are revision ids.
|
|
135
|
+
function historyCompare(revisions, a, b) {
|
|
136
|
+
const find = (id) => (revisions || []).find((r) => r.id === id);
|
|
137
|
+
const ra = find(a), rb = find(b);
|
|
138
|
+
if (!ra || !rb) return { ok: false, error: 'revision not found: ' + (!ra ? a : b) };
|
|
139
|
+
return { ok: true, from: a, to: b, diff: wordDiff(ra.text, rb.text) };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = { graphHealth, healthOf, wordDiff, historyCompare, NO_INPUT };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* doc-sheet.js — the document sheet's pure core (console3 wave E1c). No file, no clock beyond `at`, no network.
|
|
3
|
+
*
|
|
4
|
+
* editWithWhy(before, after, why, by, opts?) → { rev, before_hash, after_hash, why, by, at, text } refuses an empty why
|
|
5
|
+
* revisions(events, path) → the ordered revision list for one path (text where stored)
|
|
6
|
+
* foldAt(events, rev, path?) → the text at a revision (the fold of the revisions), or null
|
|
7
|
+
* askAgentProposal({ before, after, prompt }) → { state:'proposed', prompt, diff, hash } only a proposal; nothing is written
|
|
8
|
+
* acceptProposal(proposal, { hash, by }) → { ok:true, event } | { ok:false, reason } PM only, hash-bound
|
|
9
|
+
* changeNote(doc, downstreamDocs) → a DRAFT note for owners of documents whose source rev is behind; never sent
|
|
10
|
+
*
|
|
11
|
+
* The slide-to-accept is the same hash-bound confirm as the outbox: the hash covers the proposed text, so an edited
|
|
12
|
+
* proposal has a new hash and the PM's press on the old one is void. An agent can create a proposal and never accept it.
|
|
13
|
+
*/
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
const { wordDiff } = require('./doc-health.js');
|
|
16
|
+
|
|
17
|
+
const sha = (v) => crypto.createHash('sha256').update(typeof v === 'string' ? v : JSON.stringify(v)).digest('hex');
|
|
18
|
+
const hashText = (t) => sha(String(t == null ? '' : t));
|
|
19
|
+
|
|
20
|
+
function editWithWhy(before, after, why, by, opts = {}) {
|
|
21
|
+
if (typeof why !== 'string' || !why.trim()) throw new Error('a why is required: say why the document changes');
|
|
22
|
+
if (!by || typeof by !== 'string') throw new Error('by is required');
|
|
23
|
+
const ev = { rev: opts.rev == null ? 1 : opts.rev, before_hash: hashText(before), after_hash: hashText(after),
|
|
24
|
+
why: why.trim(), by, at: opts.at || new Date().toISOString(), text: String(after == null ? '' : after) };
|
|
25
|
+
if (opts.path) ev.path = opts.path;
|
|
26
|
+
return ev;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function revisions(events, path) {
|
|
30
|
+
return (events || []).filter((e) => e && typeof e.rev === 'number' && (path == null || e.path === path))
|
|
31
|
+
.slice().sort((a, b) => a.rev - b.rev);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function foldAt(events, rev, path) {
|
|
35
|
+
let text = null;
|
|
36
|
+
for (const e of revisions(events, path)) {
|
|
37
|
+
if (e.rev > rev) break;
|
|
38
|
+
if (typeof e.text === 'string') text = e.text;
|
|
39
|
+
}
|
|
40
|
+
return text;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function proposalHash(before_hash, after) { return sha({ before_hash, after: String(after == null ? '' : after) }); }
|
|
44
|
+
|
|
45
|
+
function askAgentProposal({ before, after, prompt } = {}) {
|
|
46
|
+
if (typeof prompt !== 'string' || !prompt.trim()) throw new Error('a prompt is required');
|
|
47
|
+
const before_hash = hashText(before);
|
|
48
|
+
return { state: 'proposed', prompt: prompt.trim(), before_hash, after: String(after == null ? '' : after),
|
|
49
|
+
diff: wordDiff(before, after), hash: proposalHash(before_hash, after) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function acceptProposal(proposal, { hash, by, rev, at, path } = {}) {
|
|
53
|
+
if (!proposal) return { ok: false, reason: 'no such proposal' };
|
|
54
|
+
if (by !== 'pm') return { ok: false, reason: 'only the PM can accept a proposal, from the page' };
|
|
55
|
+
if (proposal.state && proposal.state !== 'proposed') return { ok: false, reason: 'the proposal is already ' + proposal.state };
|
|
56
|
+
const current = proposalHash(proposal.before_hash, proposal.after);
|
|
57
|
+
if (typeof hash !== 'string' || hash !== current) return { ok: false, reason: 'hash does not match the proposal as it stands' };
|
|
58
|
+
return { ok: true, hash: current, event: { ...editWithWhy(proposal.before_text == null ? '' : proposal.before_text, proposal.after, proposal.prompt, 'pm', { rev, at, path }), before_hash: proposal.before_hash } };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// doc: { path, rev, title? }. downstreamDocs: [{ path, owner?, sources: [{ path, rev }] }]; sources[].rev is the upstream rev last used.
|
|
62
|
+
function changeNote(doc, downstreamDocs) {
|
|
63
|
+
const behind = [];
|
|
64
|
+
for (const d of downstreamDocs || []) {
|
|
65
|
+
const s = (d.sources || []).find((x) => x.path === doc.path);
|
|
66
|
+
if (s && Number(s.rev) < Number(doc.rev)) behind.push({ path: d.path, owner: d.owner || null, used_rev: s.rev, current_rev: doc.rev });
|
|
67
|
+
}
|
|
68
|
+
const lines = behind.map((b) => `- ${b.path}${b.owner ? ' (' + b.owner + ')' : ''}: built on rev ${b.used_rev}, now rev ${b.current_rev}`);
|
|
69
|
+
const body = behind.length
|
|
70
|
+
? `${doc.path} changed to rev ${doc.rev}. These documents were built on an earlier rev:\n${lines.join('\n')}\nPlease check whether each needs an update.`
|
|
71
|
+
: `${doc.path} is at rev ${doc.rev}. No downstream document is behind.`;
|
|
72
|
+
return { draft: true, sent: false, path: doc.path, rev: doc.rev, behind, body };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { editWithWhy, revisions, foldAt, askAgentProposal, acceptProposal, changeNote, hashText, proposalHash };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* intake.js — the one-line intake run (planning-flows run 4). Pure: reads planning/out-of-scope/, writes nothing.
|
|
3
|
+
* Honest by design: confidence counts matched lane signals, the anchor is never invented, a prior rejection refuses. */
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { laneNames } = require('./lane-defs');
|
|
7
|
+
|
|
8
|
+
const SIGNALS = {
|
|
9
|
+
incident: ['outage', 'incident', 'postmortem', 'broke', 'down', 'sev1', 'sev2', 'regression in prod'],
|
|
10
|
+
discovery: ['investigate', 'explore', 'research', 'spike', 'unknown', 'should we', 'whether', 'feasibility', 'discover'],
|
|
11
|
+
chore: ['chore', 'bump', 'upgrade', 'rename', 'cleanup', 'clean up', 'refactor', 'dependency', 'dependencies', 'lint'],
|
|
12
|
+
content: ['blog', 'copy', 'docs', 'documentation', 'announcement', 'article', 'landing page copy', 'newsletter'],
|
|
13
|
+
prototype: ['prototype', 'mockup', 'sketch', 'specimen', 'try out', 'what if'],
|
|
14
|
+
feature: ['add', 'build', 'new', 'support', 'allow', 'let users', 'enable', 'feature', 'create', 'implement'],
|
|
15
|
+
};
|
|
16
|
+
const STOP = new Set('a an the and or of to in on for with is are be as at by it this that from into we i our my your should can will new add use using make get'.split(' '));
|
|
17
|
+
|
|
18
|
+
const words = (s) => String(s || '').toLowerCase().match(/[a-z0-9][a-z0-9-]*/g) || [];
|
|
19
|
+
const sig = (s) => new Set(words(s).filter((w) => w.length > 2 && !STOP.has(w)));
|
|
20
|
+
|
|
21
|
+
function parseIntakeLine(text) {
|
|
22
|
+
const title = String(text || '').trim().replace(/\s+/g, ' ');
|
|
23
|
+
const low = ` ${title.toLowerCase()} `;
|
|
24
|
+
const valid = new Set(laneNames());
|
|
25
|
+
const hits = {};
|
|
26
|
+
for (const [lane, keys] of Object.entries(SIGNALS)) {
|
|
27
|
+
if (!valid.has(lane)) continue;
|
|
28
|
+
const n = keys.filter((k) => new RegExp(`(^|[^a-z0-9])${k.replace(/ /g, '\\s+')}([^a-z0-9]|$)`).test(low)).length;
|
|
29
|
+
if (n) hits[lane] = n;
|
|
30
|
+
}
|
|
31
|
+
const ranked = Object.entries(hits).sort((a, b) => b[1] - a[1]);
|
|
32
|
+
if (!ranked.length) return { title, lane_guess: null, confidence: 0.2 };
|
|
33
|
+
const total = ranked.reduce((s, [, n]) => s + n, 0);
|
|
34
|
+
const [lane, n] = ranked[0];
|
|
35
|
+
// Honest: more matches and less competition raise it; the ceiling is 0.85 because a keyword match is not a judgment.
|
|
36
|
+
const confidence = Math.min(0.85, Math.round((0.3 + 0.15 * n) * (n / total) * 100) / 100 || 0.2);
|
|
37
|
+
return { title, lane_guess: lane, confidence };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function priorRejections(root, text) {
|
|
41
|
+
const dir = path.join(root, 'planning', 'out-of-scope');
|
|
42
|
+
let files = [];
|
|
43
|
+
try { files = fs.readdirSync(dir).filter((f) => f.endsWith('.md') && f !== 'README.md'); } catch (_) { return []; }
|
|
44
|
+
const want = sig(text);
|
|
45
|
+
const out = [];
|
|
46
|
+
for (const f of files) {
|
|
47
|
+
let body = '';
|
|
48
|
+
try { body = fs.readFileSync(path.join(dir, f), 'utf8'); } catch (_) { continue; }
|
|
49
|
+
const t = (body.match(/^title:\s*"?(.*?)"?\s*$/m) || [])[1] || f.replace(/\.md$/, '');
|
|
50
|
+
const topic = (body.match(/^topic:\s*"?(.*?)"?\s*$/m) || [])[1] || '';
|
|
51
|
+
const have = sig(`${t} ${topic} ${f.replace(/\.md$/, '').replace(/-/g, ' ')}`);
|
|
52
|
+
const shared = [...want].filter((w) => have.has(w));
|
|
53
|
+
if (shared.length >= 2) out.push({ file: `planning/out-of-scope/${f}`, title: t, shared });
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// anchor: only an explicit `health:<id>` or `KR<n>` typed in the line; the caller must still confirm it.
|
|
59
|
+
function suggestAnchor(line) {
|
|
60
|
+
const m = String(line || '').match(/\bhealth:[a-z0-9-]+\b/i) || String(line || '').match(/\bKR\d+\b/);
|
|
61
|
+
return m ? m[0] : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function intakeDraft({ line, root, acknowledged = false }) {
|
|
65
|
+
const p = parseIntakeLine(line);
|
|
66
|
+
const prior = priorRejections(root || process.cwd(), line);
|
|
67
|
+
const refused = prior.length > 0 && !acknowledged;
|
|
68
|
+
return {
|
|
69
|
+
title: p.title, lane: p.lane_guess, confidence: p.confidence,
|
|
70
|
+
anchor_suggestion: suggestAnchor(line),
|
|
71
|
+
prior_rejections: prior,
|
|
72
|
+
refused,
|
|
73
|
+
refused_reason: refused ? `Prior rejection on record: ${prior.map((r) => r.title).join('; ')}. Acknowledge it before you create.` : null,
|
|
74
|
+
stage: 'discover',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { parseIntakeLine, priorRejections, intakeDraft, suggestAnchor };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// The outbox queue: pure functions over an entry, rebased on the PM's trust controls (scripts/trust/outbox.js, trust.js).
|
|
3
|
+
// The content hash, the claim rules and the PM-only confirm come from scripts/trust. This file only adapts them to the
|
|
4
|
+
// entry shape the workspace stores (channel, recipient, subject, body). Sends NOTHING: a channel is only a name until a
|
|
5
|
+
// sender is registered.
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const T = require('./trust/outbox.js');
|
|
9
|
+
const { checkReport } = require('./trust/trust.js');
|
|
10
|
+
const RULES = require('./trust/claim-rules.json');
|
|
11
|
+
|
|
12
|
+
const CHANNELS = ['jira-write', 'calendar-hold', 'customer-note'];
|
|
13
|
+
const ORIGINS = ['agent', 'pm'];
|
|
14
|
+
const SENDERS = {}; // channel -> function. Empty on purpose.
|
|
15
|
+
const BASES = Object.keys(RULES.basis);
|
|
16
|
+
const KEY = /^[A-Z][A-Z0-9]+-\d+$/;
|
|
17
|
+
// The trust rules, for free text: the destination keeps the label rules (every claim labelled, allowed, an import only as
|
|
18
|
+
// reported, a fact or cite names a source that exists) and drops the field-completeness rules, which prose cannot carry.
|
|
19
|
+
const DEST = { ...RULES.destinations.leadership, incomplete: 'ok', stale: 'ok', overdue_assumption: 'ok' };
|
|
20
|
+
|
|
21
|
+
const RULES_OUT = { ...RULES, destinations: { ...RULES.destinations, outbox: DEST } };
|
|
22
|
+
|
|
23
|
+
// Sample hash inputs: [channel, to, subject, body]. `recipient` is the entry's name for `to`.
|
|
24
|
+
const hashOf = ({ channel, recipient, to, subject, body }) =>
|
|
25
|
+
T.hashOf({ channel, to: recipient !== undefined ? recipient : to, subject: subject || '', body });
|
|
26
|
+
|
|
27
|
+
const entryHash = (e) => hashOf({ channel: e.channel, recipient: e.recipient || null, subject: e.subject || '', body: e.body });
|
|
28
|
+
|
|
29
|
+
// Claim lines are body lines starting with "- " or "* ". Each ends with [fact|cited|reported|estimate|assumption|judgement] or [KEY-123].
|
|
30
|
+
// A [KEY-123] is a citation of that ticket. rows[i].line is the line text.
|
|
31
|
+
function claimCheck(entry) {
|
|
32
|
+
const lines = String(entry.body).split('\n').filter((l) => /^\s*[-*]\s+/.test(l));
|
|
33
|
+
const docs = {};
|
|
34
|
+
const sections = lines.map((l, i) => {
|
|
35
|
+
const m = l.match(/\[([^\]]+)\]\s*$/);
|
|
36
|
+
const label = m ? m[1] : null;
|
|
37
|
+
const c = { id: i, claim: l.trim() };
|
|
38
|
+
if (label && KEY.test(label)) { c.basis = 'cited'; c.source = label; docs[label] = { rev: 0 }; }
|
|
39
|
+
else if (label && BASES.includes(label)) c.basis = label;
|
|
40
|
+
return c;
|
|
41
|
+
});
|
|
42
|
+
const verdict = checkReport({ sections }, RULES_OUT, { snaps: [], last: '', docs, now: new Date().toISOString().slice(0, 10), tripped: [] }, 'outbox');
|
|
43
|
+
const refused = verdict.rows.filter((r) => r.verdict === 'block').map((r) => lines[r.id].trim());
|
|
44
|
+
return { ok: refused.length === 0, refused, rows: verdict.rows, send: verdict.send };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function draftOutbox({ channel, recipient, subject, body, origin } = {}) {
|
|
48
|
+
if (!CHANNELS.includes(channel)) throw new Error('unknown channel: ' + channel);
|
|
49
|
+
if (!ORIGINS.includes(origin)) throw new Error("origin must be 'agent' or 'pm'");
|
|
50
|
+
if (typeof body !== 'string' || !body.trim()) throw new Error('body required');
|
|
51
|
+
const e = { channel, recipient: recipient || null, subject: subject || '', body, origin };
|
|
52
|
+
return { ...e, state: claimCheck(e).ok ? 'queued' : 'blocked', hash: entryHash(e) };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// New entry, new hash. The old hash is voided: confirmOutbox compares against the entry it is given.
|
|
56
|
+
function editOutbox(entry, { body } = {}) {
|
|
57
|
+
if (typeof body !== 'string' || !body.trim()) throw new Error('body required');
|
|
58
|
+
const e = { ...entry, body };
|
|
59
|
+
return { ...e, state: claimCheck(e).ok ? 'queued' : 'blocked', hash: entryHash(e) };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The PM's press. A blocked entry (a claim fails the trust rules) needs a written reason, which is the override.
|
|
63
|
+
function confirmOutbox(entry, { hash, by, reason } = {}, senders = SENDERS) {
|
|
64
|
+
if (by !== 'pm') return { ok: false, reason: 'agent cannot confirm' };
|
|
65
|
+
if (hash !== entry.hash || entryHash(entry) !== entry.hash) return { ok: false, reason: 'hash does not match the content' };
|
|
66
|
+
const override = !claimCheck(entry).ok;
|
|
67
|
+
if (override && !String(reason || '').trim()) return { ok: false, reason: 'blocked by claims check: a written reason is required to override' };
|
|
68
|
+
if (typeof senders[entry.channel] !== 'function') return { ok: false, reason: 'no channel connected' };
|
|
69
|
+
return { ok: true, hash: entry.hash, override, reason: reason || null };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Kept for older callers: true when every claim line passes the trust rules.
|
|
73
|
+
const claimLabelsOK = (entry) => claimCheck(entry).ok;
|
|
74
|
+
|
|
75
|
+
// The send step is a stub. No network, no connector. A real send goes through scripts/trust/outbox.js execute (see workspace/lib/outbox-store.js).
|
|
76
|
+
function sendOutbox() {
|
|
77
|
+
return { sent: false, reason: 'no channel connected' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Generic append-only line writer shared by brakes.js, card-state and the run routes.
|
|
81
|
+
function appendOutbox(file, event, now = new Date()) {
|
|
82
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
83
|
+
fs.appendFileSync(file, JSON.stringify({ ...event, at: now.toISOString() }) + '\n');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = { CHANNELS, hashOf, entryHash, claimCheck, draftOutbox, editOutbox, confirmOutbox, claimLabelsOK, sendOutbox, appendOutbox };
|