create-pmos 0.179.0 → 0.181.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/c3-driver-test.js +6 -1
- package/workspace/c3-report-setup-test.js +190 -0
- package/workspace/c3-reports-test.js +30 -97
- package/workspace/guide/document-sheet.md +14 -0
- package/workspace/guide/index.md +3 -2
- package/workspace/guide/initiative-driver.md +2 -2
- package/workspace/guide/reports-dashboards.md +17 -28
- package/workspace/guide/reports-page.md +10 -5
- package/workspace/guide/reports-set-up.md +56 -0
- package/workspace/guide/reports.md +13 -1
- package/workspace/guide/settings.md +11 -2
- package/workspace/lib/doc-sync.js +28 -6
- package/workspace/lib/docs-cli.js +7 -11
- package/workspace/lib/four-panel.js +1 -1
- package/workspace/lib/headless.js +21 -2
- package/workspace/lib/report-checks.js +103 -0
- package/workspace/lib/report-cli.js +89 -4
- package/workspace/lib/report-export.js +347 -0
- package/workspace/lib/report-finder.js +156 -0
- package/workspace/lib/report-needs.js +1 -1
- package/workspace/lib/report-setups.js +743 -0
- package/workspace/lib/route-modules.js +1 -0
- package/workspace/lib/routes-c3-reports.js +35 -17
- package/workspace/lib/routes-doc-health.js +15 -1
- package/workspace/lib/routes-report-setups.js +368 -0
- package/workspace/lib/schedule-logic.js +5 -2
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-demo-rep2.js +15 -0
- package/workspace/public/pm/c3-demo-rs.js +281 -0
- package/workspace/public/pm/c3-demo.js +1 -1
- package/workspace/public/pm/c3-doc-formats.js +152 -0
- package/workspace/public/pm/c3-doc-sheet.js +115 -25
- package/workspace/public/pm/c3-ini-ui.js +15 -1
- package/workspace/public/pm/c3-initiatives.js +3 -6
- package/workspace/public/pm/c3-quiet.js +2 -2
- package/workspace/public/pm/c3-report-print.css +20 -0
- package/workspace/public/pm/c3-report-setup.css +62 -0
- package/workspace/public/pm/c3-report-setup.js +260 -0
- package/workspace/public/pm/c3-report-store.js +208 -0
- package/workspace/public/pm/c3-reports.css +4 -0
- package/workspace/public/pm/c3-reports.js +47 -181
- package/workspace/public/pm/c3-settings.js +4 -3
- package/workspace/public/pm/c3-shell.js +15 -31
- package/workspace/server.js +66 -5
- package/workspace/skills/doc-sync.skill +17 -8
- package/workspace/skills/report-find.skill +75 -0
- package/workspace/skills/report-setup.skill +67 -0
- package/workspace/test-files.json +10 -0
- package/workspace/test-lists/report-workspace-ui.json +5 -0
|
@@ -70,6 +70,7 @@ const MODULES = Object.freeze([
|
|
|
70
70
|
'routes-stage-tools', // stage-tools-records: the records the PM edits per stage, what PMOS holds per stage, the sprint burndown
|
|
71
71
|
'routes-driver', // initiative-driver: the Agent strip's data, Keep, Send back, Drop, Pause and the board mode (driver.jsonl)
|
|
72
72
|
'routes-scan', // horizon-scan: a board's watch list, cadence and last scan, the day's scan for the Briefing, the PM's watch and cadence presses (scan.jsonl)
|
|
73
|
+
'routes-report-setups', // report-workspace: recurring reports, the Ask box, the finder, the report documents and their export (report-setups.jsonl)
|
|
73
74
|
]);
|
|
74
75
|
const PM_DIR = path.join(__dirname, '..', 'public', 'pm');
|
|
75
76
|
const MODULE_NAME = /^[a-z0-9][a-z0-9-]*\.js$/;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* POST /api/report-dashboards/schedule { key, plan, audience } → { ok, key, schedule, schedules } plan: none|weekly|monthly; none removes it.
|
|
10
10
|
* 400 on a bad key, plan or audience; 403 without a page press. (lib/report-schedules.js)
|
|
11
11
|
* runDue(ctx, now) the server tick: each due schedule appends ONE draft to the outbox. Nothing is sent.
|
|
12
|
+
* A key that is a recurring report now (report-workspace) is skipped: lib/routes-report-setups.js drafts it.
|
|
12
13
|
*
|
|
13
14
|
* Initiatives come from the workspace's facts (lib/facts-async.js), like routes-calendar-views.js. Key results, bets and hypotheses
|
|
14
15
|
* come from the same records as routes-c3-strategy.js. The handlers are pure over their inputs so a test needs no server.
|
|
@@ -91,10 +92,10 @@ function titleOf(key, c) {
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
95
|
+
* The frame of a dashboard (its data, its current layout or the scope's default, the frame the Share sheet makes): { ok, F } | { ok:false, error }.
|
|
96
|
+
* sched { key, audience }; deps { facts, slug, build(today), hypotheses, runtimeDir, today }; `title` replaces the dashboard's own title.
|
|
96
97
|
*/
|
|
97
|
-
function
|
|
98
|
+
function frameFor(sched, deps, title) {
|
|
98
99
|
const key = sched.key;
|
|
99
100
|
const scope = key === 'portfolio' ? 'portfolio' : key.slice(0, key.indexOf(':'));
|
|
100
101
|
const id = scope === 'portfolio' ? null : key.slice(key.indexOf(':') + 1);
|
|
@@ -104,11 +105,35 @@ function runOne(sched, deps, now = new Date()) {
|
|
|
104
105
|
const ids = Object.prototype.hasOwnProperty.call(layouts, key) ? layouts[key] : rd.DEFAULTS[scope];
|
|
105
106
|
const reports = ids.map((x) => rd.LIBRARY.find((l) => l.id === x)).filter(Boolean);
|
|
106
107
|
const c = catalogue(deps.facts, deps.slug, deps.index);
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
return { ok: true, F: RF.frameOf({ title: title || titleOf(key, c), reports, raw: r.body, audience: sched.audience, today: r.body.today }) };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Run one schedule: the frame, then ONE outbox draft. deps as frameFor. Returns { ok, entry } | { ok:false, error }. */
|
|
112
|
+
function runOne(sched, deps, now = new Date()) {
|
|
113
|
+
const f = frameFor(sched, deps);
|
|
114
|
+
if (!f.ok) return f;
|
|
115
|
+
try { return { ok: true, entry: rs.makeDraft(deps.runtimeDir, sched, f.F, RF.draftBody(f.F), now) }; }
|
|
109
116
|
catch (e) { return { ok: false, error: 'the draft could not be saved: ' + String((e && e.message) || e) }; }
|
|
110
117
|
}
|
|
111
118
|
|
|
119
|
+
/** The deps frameFor needs for one workspace, read now. ctx { getWs, runtimeDir, today, readFacts, build }. The tick here and lib/routes-report-setups.js use it. */
|
|
120
|
+
async function depsFor(ctx, wsId) {
|
|
121
|
+
const w = ctx.getWs(wsId || null);
|
|
122
|
+
if (!w) throw new Error('no workspace is registered');
|
|
123
|
+
const slug = w.id || (w.root ? path.basename(path.resolve(w.root)) : null);
|
|
124
|
+
const facts = await (ctx.readFacts || readFacts)(w);
|
|
125
|
+
const root = rootOf(ctx, wsId || null);
|
|
126
|
+
let hypotheses = [];
|
|
127
|
+
try { hypotheses = require(repoScript('records/index.js')).list(root, 'hypotheses'); } catch { hypotheses = []; }
|
|
128
|
+
return { facts, slug, hypotheses, runtimeDir: ctx.runtimeDir, today: ctx.today,
|
|
129
|
+
build: ctx.build || ((today) => strategy.build(root, path.join(ctx.runtimeDir, 'signals', 'signals.jsonl'), today)) };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** True when the key is a recurring report now (lib/report-setups.js dashboardKeys). A repository that cannot be read holds nothing. */
|
|
133
|
+
function handled(ctx, s) {
|
|
134
|
+
try { const w = ctx.getWs(s.ws || null); return !!w && require('./report-setups.js').dashboardKeys(w.root).has(s.key); } catch { return false; }
|
|
135
|
+
}
|
|
136
|
+
|
|
112
137
|
/** The tick. lastRun moves first, so a slow read never runs the same slot twice; the outcome is written after. */
|
|
113
138
|
let running = false;
|
|
114
139
|
async function runDue(ctx, now = new Date()) {
|
|
@@ -117,19 +142,12 @@ async function runDue(ctx, now = new Date()) {
|
|
|
117
142
|
const done = [];
|
|
118
143
|
try {
|
|
119
144
|
for (const s of rs.dueOf(rs.readSchedules(ctx.runtimeDir), now)) {
|
|
145
|
+
// A key that became a recurring report (lib/report-setups.js) is drafted there, as a document. This tick leaves it alone.
|
|
146
|
+
if (handled(ctx, s)) continue;
|
|
120
147
|
rs.markRun(ctx.runtimeDir, s.key, { lastRun: now.toISOString() });
|
|
121
148
|
let out;
|
|
122
|
-
try {
|
|
123
|
-
|
|
124
|
-
if (!w) throw new Error('no workspace is registered');
|
|
125
|
-
const slug = w.id || (w.root ? path.basename(path.resolve(w.root)) : null);
|
|
126
|
-
const facts = await (ctx.readFacts || readFacts)(w);
|
|
127
|
-
const root = rootOf(ctx, s.ws || null);
|
|
128
|
-
let hypotheses = [];
|
|
129
|
-
try { hypotheses = require(repoScript('records/index.js')).list(root, 'hypotheses'); } catch { hypotheses = []; }
|
|
130
|
-
out = runOne(s, { facts, slug, hypotheses, runtimeDir: ctx.runtimeDir, today: ctx.today,
|
|
131
|
-
build: ctx.build || ((today) => strategy.build(root, path.join(ctx.runtimeDir, 'signals', 'signals.jsonl'), today)) }, now);
|
|
132
|
-
} catch (e) { out = { ok: false, error: String((e && e.message) || e) }; }
|
|
149
|
+
try { out = runOne(s, await depsFor(ctx, s.ws || null), now); }
|
|
150
|
+
catch (e) { out = { ok: false, error: String((e && e.message) || e) }; }
|
|
133
151
|
rs.markRun(ctx.runtimeDir, s.key, out.ok ? { last_status: 'ok', last_error: null, last_draft: out.entry.id } : { last_status: 'failed', last_error: out.error });
|
|
134
152
|
done.push({ key: s.key, ...out });
|
|
135
153
|
}
|
|
@@ -146,7 +164,7 @@ async function readFacts(w) {
|
|
|
146
164
|
|
|
147
165
|
module.exports = {
|
|
148
166
|
prefixes: ['/api/report-dashboards'],
|
|
149
|
-
catalogue, list, data, layout, schedule, runOne, runDue,
|
|
167
|
+
catalogue, list, data, layout, schedule, frameFor, depsFor, runOne, runDue,
|
|
150
168
|
handle: async (req, res, url, ctx) => {
|
|
151
169
|
const p = url.pathname;
|
|
152
170
|
const send = (r) => { ctx.json(res, r.body, r.status); return true; };
|
|
@@ -8,12 +8,14 @@
|
|
|
8
8
|
* of every relative Markdown link it cites (a save or a "still current" mark in the document sheet counts as a change), and the last change of each ADR a `D<n>` id in its text names. Everything
|
|
9
9
|
* else (requiredHeadings, data, readers, current, staleAfterDays) comes only from `context` in the body; a check without
|
|
10
10
|
* its input answers "not enough input". No write routes in this wave.
|
|
11
|
+
* A document under reports/<cadence>/ also gets the three report checks (lib/report-checks.js) appended to `health.checks`; passed and total count them.
|
|
11
12
|
*/
|
|
12
13
|
const fs = require('fs');
|
|
13
14
|
const path = require('path');
|
|
14
15
|
const { spawnSync } = require('child_process');
|
|
15
16
|
const { repoScript } = require('./repo-script.js');
|
|
16
17
|
const H = require(repoScript('doc-health.js'));
|
|
18
|
+
const RC = require('./report-checks.js');
|
|
17
19
|
|
|
18
20
|
const ROOT = path.resolve(__dirname, '..', '..');
|
|
19
21
|
const bad = (error, status = 400) => ({ status, body: { ok: false, error } });
|
|
@@ -71,7 +73,19 @@ function check(root, body, runtimeDir) {
|
|
|
71
73
|
const context = { ...given, sources: sources.length ? sources : given.sources,
|
|
72
74
|
decisionChanges: Object.keys(decisionChanges).length ? decisionChanges : given.decisionChanges };
|
|
73
75
|
const doc = { text, type: given.type || body.type, changed };
|
|
74
|
-
|
|
76
|
+
const health = H.healthOf(doc, context);
|
|
77
|
+
// A report document (<state>/reports/<cadence>/<slot>.md) is also held to three report checks (lib/report-checks.js): numbers have a source,
|
|
78
|
+
// dates are written in full, and Decisions and Risk watch are present when the report's definition lists them.
|
|
79
|
+
if (RC.isReportPath(relOut)) {
|
|
80
|
+
let listed = [];
|
|
81
|
+
try { listed = RC.listedOf(fs.readFileSync(path.join(path.dirname(path.dirname(full)), 'definitions', RC.cadenceOfPath(relOut) + '.md'), 'utf8')); } catch { listed = []; }
|
|
82
|
+
const extra = RC.reportChecks(text, { listed });
|
|
83
|
+
health.checks = health.checks.concat(extra);
|
|
84
|
+
const answered = health.checks.filter((k) => k.pass !== null);
|
|
85
|
+
health.passed = answered.filter((k) => k.pass).length;
|
|
86
|
+
health.total = answered.length;
|
|
87
|
+
}
|
|
88
|
+
return { status: 200, body: { ok: true, path: relOut, health } };
|
|
75
89
|
}
|
|
76
90
|
|
|
77
91
|
module.exports = {
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* routes-report-setups.js — the Reports workspace's routes (initiative report-workspace, PRD sections 2 to 6).
|
|
3
|
+
*
|
|
4
|
+
* GET /api/report-setups?ws → { setups: [{ id, name, purpose, audience, source, output, schedule, label, next_draft, last_issued, status, cycles,
|
|
5
|
+
* copy: { kind, state: none|in-step|pmos-ahead|copy-changed|not-checked, url } }],
|
|
6
|
+
* findings: [{ id, name, why, evidence: [{ kind, text }], confidence: likely|not-sure, proposal }],
|
|
7
|
+
* asks: [{ id, text, state: running|ready|failed, changed, proposal, error }],
|
|
8
|
+
* finder: { cadence, sources: { calendar, slack, mail, drive }, last_run, next_run } }
|
|
9
|
+
* POST /api/report-setups/ask { ws, text } → { ok, id } starts the report-setup run (headless)
|
|
10
|
+
* POST /api/report-setups/change { ws, id, text } → { ok } a correction to a proposal; a second run revises it
|
|
11
|
+
* POST /api/report-setups/cancel { ws, id } → { ok }
|
|
12
|
+
* POST /api/report-setups/setup { ws, ask? | finding?, proposal? } → { ok, setup, first_draft } THE press that sets a report up
|
|
13
|
+
* POST /api/report-setups/dismiss { ws, finding } → { ok } "Not a report"
|
|
14
|
+
* POST /api/report-setups/pause | resume { ws, id } → { ok, setup }
|
|
15
|
+
* POST /api/report-setups/from-dashboard { ws, key, name, output, schedule, audience } → { ok, setup }
|
|
16
|
+
* POST /api/report-setups/finder { ws, cadence?, sources? } → { ok, finder }
|
|
17
|
+
* GET /api/report-documents?ws → { docs: [{ path, id, setup, name, slot, state: draft|issued, source, output, made_by: schedule|you|agent, v, copy }] }
|
|
18
|
+
* GET /api/report-export?ws&path&fmt=pptx|xlsx|docx|csv → the file, Content-Disposition: attachment. PDF is printed by the browser: there is no route.
|
|
19
|
+
*
|
|
20
|
+
* WHO SETS A REPORT UP. Only a PM press does. Every POST passes pm-guard requirePagePress, and a request that carries an agent session header is
|
|
21
|
+
* refused first (403), as routes-scan.js does. A proposal from the Ask box or from the finder is DATA in report-setups.jsonl: the report-setup
|
|
22
|
+
* and report-find runs (lib/headless.js) file it with `pmos report propose` and have no tool that writes anything else. Nothing here, and nothing
|
|
23
|
+
* those runs do, sends anything outward (D65: the factory is propose-only).
|
|
24
|
+
*
|
|
25
|
+
* A DOCUMENT is any <state>/reports/<cadence>/<slot>.md, one-offs included, joined to issues.jsonl. Its `id` is "<cadence>/<slot>"; `setup` is the
|
|
26
|
+
* recurring report that made it (the cadence folder is the setup id), or null for a one-off. `made_by` reads the file's own `made_by` line; with
|
|
27
|
+
* none, a setup's folder says "schedule" and a one-off says "you". `v` is 1 plus the saves the document sheet kept for the file. `copy` is the linked
|
|
28
|
+
* copy's state from lib/doc-sync-store.js (kind: the output's copy kind; state: none | in-step | pmos-ahead | copy-changed | not-checked).
|
|
29
|
+
*
|
|
30
|
+
* TICK. tick(ctx, now) moves the old dashboard schedules into recurring reports (once per repository), then drafts each due dashboard-sourced setup
|
|
31
|
+
* as a document under <state>/reports/<id>/<slot>.md. The server calls it before the old dashboard tick, which then skips the migrated keys.
|
|
32
|
+
* ctx adds to the usual route ctx: { scheduleStore: { read, write }, skillPrompt(id, w), stopSession(id), workspaceList() }.
|
|
33
|
+
*/
|
|
34
|
+
const fs = require('fs');
|
|
35
|
+
const path = require('path');
|
|
36
|
+
const guard = require('./pm-guard.js');
|
|
37
|
+
const RS = require('./report-setups.js');
|
|
38
|
+
const RN = require('./report-needs.js');
|
|
39
|
+
const RC = require('./report-checks.js');
|
|
40
|
+
const RX = require('./report-export.js');
|
|
41
|
+
const RF = require('./report-finder.js');
|
|
42
|
+
const logic = require('./schedule-logic.js');
|
|
43
|
+
const docSync = require('./doc-sync.js');
|
|
44
|
+
const headless = require('./headless.js');
|
|
45
|
+
|
|
46
|
+
const text = (v) => (typeof v === 'string' ? v : '');
|
|
47
|
+
const bad = (status, error) => ({ status, body: { ok: false, error } });
|
|
48
|
+
const POSTS = ['ask', 'change', 'cancel', 'setup', 'dismiss', 'pause', 'resume', 'from-dashboard', 'finder'];
|
|
49
|
+
const MADE_BY = ['schedule', 'you', 'agent'];
|
|
50
|
+
|
|
51
|
+
const runs = new Map(); // ask id → session id, for the report-setup runs this server started
|
|
52
|
+
|
|
53
|
+
function wsOf(ctx, id) {
|
|
54
|
+
const w = ctx.getWs(id || null);
|
|
55
|
+
return w && (!id || w.id === id) ? w : null;
|
|
56
|
+
}
|
|
57
|
+
const isHome = (ctx, w) => { const h = ctx.getWs(null); return !!h && h.id === w.id; };
|
|
58
|
+
const dayEnd = (ctx) => { try { const s = ctx.proSettings ? ctx.proSettings() : null; return (s && s.dayCheck && s.dayCheck.end) || '18:00'; } catch { return '18:00'; } };
|
|
59
|
+
const finderClock = (env) => { const t = (env || process.env).PMOS_REPORT_FIND_NOW ? Date.parse((env || process.env).PMOS_REPORT_FIND_NOW) : NaN; return Number.isFinite(t) ? t : null; };
|
|
60
|
+
|
|
61
|
+
// ── the old dashboard schedules become recurring reports, once ───────────────────────────────────────────
|
|
62
|
+
/** Move this workspace's dashboard schedules into setups. Nothing to move: nothing is written (no state folder is made). Idempotent: a marker line. */
|
|
63
|
+
function ensureMigrated(ctx, w, now = new Date()) {
|
|
64
|
+
if (!ctx.runtimeDir) return null;
|
|
65
|
+
try {
|
|
66
|
+
const st = RS.stateOf(w.root);
|
|
67
|
+
if (st.migrated) return null;
|
|
68
|
+
const rs = require('./report-schedules.js');
|
|
69
|
+
const mine = Object.values(rs.readSchedules(ctx.runtimeDir)).filter((s) => (s.ws ? s.ws === w.id : isHome(ctx, w)));
|
|
70
|
+
if (!mine.length) return null;
|
|
71
|
+
let outbox = [];
|
|
72
|
+
try { outbox = require('./outbox-store.js').readOutbox(ctx.runtimeDir); } catch { outbox = []; }
|
|
73
|
+
return RS.migrate(w.root, { runtimeDir: ctx.runtimeDir, wsId: w.id, isHome: isHome(ctx, w), outbox, now });
|
|
74
|
+
} catch { return null; }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── what the page reads ──────────────────────────────────────────────────────────────────────────────────
|
|
78
|
+
function copyOf(link, kind) {
|
|
79
|
+
if (!link) return { kind: kind || null, state: 'none', url: null };
|
|
80
|
+
const state = link.signal === 'same' ? 'in-step' : link.signal === 'ahead' ? 'pmos-ahead' : (link.signal === 'behind' || link.signal === 'diverged') ? 'copy-changed' : 'not-checked';
|
|
81
|
+
return { kind: link.kind, state, url: link.target || null };
|
|
82
|
+
}
|
|
83
|
+
function copyIndex(root) {
|
|
84
|
+
const idx = new Map();
|
|
85
|
+
try { const v = require('./doc-sync-store.js').view(root); if (v.ok) for (const d of v.docs) idx.set(d.path, d.links); } catch { /* no link store */ }
|
|
86
|
+
return idx;
|
|
87
|
+
}
|
|
88
|
+
function linkFor(idx, rel, output) {
|
|
89
|
+
const links = idx.get(rel) || [];
|
|
90
|
+
const want = docSync.copyKindOf(output);
|
|
91
|
+
return links.find((l) => l.kind === want) || links[0] || null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function documentsOf(ctx, w) {
|
|
95
|
+
const root = w.root;
|
|
96
|
+
const st = RS.stateOf(root);
|
|
97
|
+
const r = RN.reportsOf(root);
|
|
98
|
+
const idx = copyIndex(root);
|
|
99
|
+
let revs = [];
|
|
100
|
+
try { revs = fs.readFileSync(path.join(ctx.runtimeDir, 'doc-sheet', 'revisions.jsonl'), 'utf8').split('\n').filter(Boolean).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean); } catch { revs = []; }
|
|
101
|
+
const saves = new Map();
|
|
102
|
+
for (const e of revs) if (e && typeof e.rev === 'number' && e.path) saves.set(e.path, (saves.get(e.path) || 0) + 1);
|
|
103
|
+
const abs = path.resolve(root);
|
|
104
|
+
const docs = [];
|
|
105
|
+
for (const x of r.reports) {
|
|
106
|
+
const fm = RN.frontmatter(path.join(abs, x.path));
|
|
107
|
+
const setup = st.setups.get(x.cadence) || null;
|
|
108
|
+
const output = fm.output || (setup && setup.output) || null;
|
|
109
|
+
docs.push({
|
|
110
|
+
path: x.path, id: `${x.cadence}/${x.slot}`, setup: setup ? setup.id : null,
|
|
111
|
+
name: setup ? setup.name : (fm.title || x.cadence), slot: x.slot, state: x.issued ? 'issued' : 'draft',
|
|
112
|
+
source: fm.source || (setup ? setup.source : 'work'), output,
|
|
113
|
+
made_by: MADE_BY.includes(fm.made_by) ? fm.made_by : (setup ? 'schedule' : 'you'),
|
|
114
|
+
v: 1 + (saves.get(x.path) || 0), copy: copyOf(linkFor(idx, x.path, output), docSync.copyKindOf(output)),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return { docs, reports: r.reports };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function setupOut(s, now, docsOf, idx) {
|
|
121
|
+
const mine = (docsOf.reports || []).filter((x) => x.cadence === s.id);
|
|
122
|
+
const issued = mine.filter((x) => x.issued).map((x) => x.slot).sort().pop() || null;
|
|
123
|
+
const latest = mine.slice().sort((a, b) => (a.slot < b.slot ? 1 : -1))[0] || null;
|
|
124
|
+
let next = null;
|
|
125
|
+
if (s.status === 'active') { try { const n = RS.nextDraftAt(s.schedule, s.last_run, now); next = n ? n.toISOString() : null; } catch { next = null; } }
|
|
126
|
+
return {
|
|
127
|
+
id: s.id, name: s.name, purpose: s.purpose, audience: s.audience, source: s.source, output: s.output, schedule: s.schedule, label: RS.scheduleLabel(s.schedule),
|
|
128
|
+
next_draft: next, last_issued: issued, status: s.status, cycles: mine.length,
|
|
129
|
+
copy: copyOf(latest ? linkFor(idx, latest.path, s.output) : null, docSync.copyKindOf(s.output)),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function finderOut(ctx, st, now) {
|
|
134
|
+
const last = st.finds.length ? st.finds[st.finds.length - 1] : null;
|
|
135
|
+
const clock = finderClock() != null ? finderClock() : now;
|
|
136
|
+
const next = RF.off(process.env) ? null : RF.nextRun(st, clock, { dayEnd: dayEnd(ctx) });
|
|
137
|
+
return { cadence: st.finder.cadence, sources: { ...st.finder.sources }, last_run: last ? last.start : null, next_run: next == null ? null : new Date(next).toISOString() };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** A running ask whose run this server no longer holds (the server restarted) is over. */
|
|
141
|
+
function settleStale(file, st, now) {
|
|
142
|
+
let changed = false;
|
|
143
|
+
for (const a of st.asks.values()) {
|
|
144
|
+
if (a.state !== 'running' || runs.has(a.id) || now - Date.parse(a.started) < 90000) continue;
|
|
145
|
+
RS.askEnd(file, { id: a.id, ok: false, error: 'The run stopped when the workspace restarted. Ask again.' });
|
|
146
|
+
changed = true;
|
|
147
|
+
}
|
|
148
|
+
return changed;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function listAnswer(ctx, w, now) {
|
|
152
|
+
const file = RS.ledgerFile(w.root);
|
|
153
|
+
let st = RS.fold(RS.readLines(file));
|
|
154
|
+
if (settleStale(file, st, now)) st = RS.fold(RS.readLines(file));
|
|
155
|
+
const docsOf = documentsOf(ctx, w);
|
|
156
|
+
const idx = copyIndex(w.root);
|
|
157
|
+
return {
|
|
158
|
+
setups: [...st.setups.values()].map((s) => setupOut(s, new Date(now), docsOf, idx)),
|
|
159
|
+
findings: RS.openFindings(st).map((f) => ({ id: f.id, name: f.name, why: f.why, evidence: f.evidence, confidence: f.confidence, proposal: f.proposal })),
|
|
160
|
+
asks: [...st.asks.values()].map((a) => ({ id: a.id, text: a.text, state: a.state, changed: !!a.changed, proposal: a.proposal, error: a.error })),
|
|
161
|
+
finder: finderOut(ctx, st, now),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── the report-setup run ─────────────────────────────────────────────────────────────────────────────────
|
|
166
|
+
function askPrompt(skill, ask, extra) {
|
|
167
|
+
const lines = [`- Ask: ${ask.id}`, `- Today: ${new Date().toISOString().slice(0, 10)}`, `- Request: ${ask.text}`];
|
|
168
|
+
if (extra && extra.correction) lines.push(`- Correction: ${extra.correction}`, `- Current proposal: ${JSON.stringify(extra.proposal || {})}`);
|
|
169
|
+
return `${skill}\n\nThis request:\n${lines.join('\n')}\n\nFile ONE proposal with \`pmos report propose --json -\`. Send nothing.\r`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Start (or restart, for a correction) the run of an ask. → { ok } | { ok: false, status, error } */
|
|
173
|
+
function startRun(ctx, w, file, ask, extra) {
|
|
174
|
+
if (typeof ctx.spawnSession !== 'function') return { ok: false, status: 409, error: 'this workspace cannot start sessions' };
|
|
175
|
+
const origin = `report-setup:${ask.id}`;
|
|
176
|
+
if (headless.kindFor({ agent: 'claude', origin }) !== 'report-setup') return { ok: false, status: 409, error: 'Background runs are switched off here (PMOS_HEADLESS=off), so this page cannot start the request.' };
|
|
177
|
+
const skill = typeof ctx.skillPrompt === 'function' ? ctx.skillPrompt('report-setup', w) : null;
|
|
178
|
+
if (!skill) return { ok: false, status: 500, error: 'the report-setup skill is not installed' };
|
|
179
|
+
const s = ctx.spawnSession({ name: 'Report setup', agent: 'claude', ws: w.id, origin, initialInput: askPrompt(skill, ask, extra),
|
|
180
|
+
extraEnv: { PMOS_REPORT_FILE: file, PMOS_REPORT_RUN: `setup:${ask.id}` },
|
|
181
|
+
onDone: (r) => {
|
|
182
|
+
runs.delete(ask.id);
|
|
183
|
+
const res = (r && r.result) || {};
|
|
184
|
+
const ok = !!(r && !r.timedOut && !r.stopped && r.code === 0 && res.ok && !res.isError);
|
|
185
|
+
try { RS.askEnd(file, { id: ask.id, ok, error: ok ? null : (r && r.note) || 'the run failed' }); } catch { /* the next read settles it */ }
|
|
186
|
+
} });
|
|
187
|
+
if (!s || s.state === 'exited' || s.headless !== 'report-setup') {
|
|
188
|
+
if (s && s.headless !== 'report-setup' && s.state !== 'exited' && typeof ctx.stopSession === 'function') { try { ctx.stopSession(s.id); } catch { /* best effort */ } }
|
|
189
|
+
return { ok: false, status: 409, error: 'The request could not start: ' + ((s && s.note) || 'no background run') };
|
|
190
|
+
}
|
|
191
|
+
runs.set(ask.id, s.id);
|
|
192
|
+
return { ok: true };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── the tick ─────────────────────────────────────────────────────────────────────────────────────────────
|
|
196
|
+
let ticking = false;
|
|
197
|
+
/** Migrate, then draft each due dashboard-sourced setup as a document. ctx { getWs, workspaceList, runtimeDir, … }. → [{ id, slot, ok, error? }] */
|
|
198
|
+
async function tick(ctx, now = new Date()) {
|
|
199
|
+
if (ticking || !ctx || !ctx.runtimeDir) return [];
|
|
200
|
+
ticking = true;
|
|
201
|
+
const done = [];
|
|
202
|
+
try {
|
|
203
|
+
const list = typeof ctx.workspaceList === 'function' ? ctx.workspaceList() : [ctx.getWs(null)].filter(Boolean);
|
|
204
|
+
for (const w of list) {
|
|
205
|
+
if (!w || !w.root) continue;
|
|
206
|
+
ensureMigrated(ctx, w, now);
|
|
207
|
+
let st;
|
|
208
|
+
try { st = RS.stateOf(w.root); } catch { continue; }
|
|
209
|
+
for (const s of st.setups.values()) {
|
|
210
|
+
if (s.status !== 'active' || !String(s.source).startsWith('dashboard:')) continue;
|
|
211
|
+
const sched = RS.scheduleOf(s);
|
|
212
|
+
if (!logic.isDue(sched, now)) continue;
|
|
213
|
+
const slotAt = logic.lastSlot(sched, now);
|
|
214
|
+
if (!slotAt) continue;
|
|
215
|
+
const slot = RS.localDay(slotAt);
|
|
216
|
+
if (s.drafted.some((d) => d.slot === slot)) continue;
|
|
217
|
+
let out;
|
|
218
|
+
try {
|
|
219
|
+
const c3 = require('./routes-c3-reports.js');
|
|
220
|
+
const deps = await c3.depsFor({ ...ctx, getWs: (id) => (id === w.id || id == null ? w : ctx.getWs(id)) }, w.id);
|
|
221
|
+
const f = c3.frameFor({ key: RS.keyOfSource(s.source), audience: s.audience }, deps, s.name);
|
|
222
|
+
if (!f.ok) out = f;
|
|
223
|
+
else {
|
|
224
|
+
const w2 = RS.writeDocument(w.root, s, slot, RS.frameMarkdown(s, f.F, slot, now), { now });
|
|
225
|
+
out = w2.ok ? { ok: true } : { ok: false, error: w2.error, existed: w2.status === 409 };
|
|
226
|
+
}
|
|
227
|
+
} catch (e) { out = { ok: false, error: String((e && e.message) || e) }; }
|
|
228
|
+
done.push({ id: s.id, slot, ...out });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
} finally { ticking = false; }
|
|
232
|
+
return done;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── the handler ──────────────────────────────────────────────────────────────────────────────────────────
|
|
236
|
+
function exportAnswer(ctx, res, w, url) {
|
|
237
|
+
const rel = text(url.searchParams.get('path'));
|
|
238
|
+
const fmt = text(url.searchParams.get('fmt'));
|
|
239
|
+
if (fmt === 'pdf') return bad(400, 'PDF is printed by the browser. Use Print in the document.');
|
|
240
|
+
if (!Object.prototype.hasOwnProperty.call(RX.FORMATS, fmt)) return bad(400, `fmt must be one of ${Object.keys(RX.FORMATS).join(', ')}`);
|
|
241
|
+
if (!RC.isReportPath(rel)) return bad(400, 'path must be a report document: <state>/reports/<cadence>/<slot>.md');
|
|
242
|
+
const abs = path.resolve(w.root);
|
|
243
|
+
const full = path.resolve(abs, rel);
|
|
244
|
+
if (!full.startsWith(abs + path.sep)) return bad(404, 'outside workspace');
|
|
245
|
+
let md;
|
|
246
|
+
try {
|
|
247
|
+
const real = fs.realpathSync(full);
|
|
248
|
+
if (!real.startsWith(fs.realpathSync(abs) + path.sep)) return bad(404, 'outside workspace');
|
|
249
|
+
md = fs.readFileSync(real, 'utf8');
|
|
250
|
+
} catch { return bad(404, 'no such document'); }
|
|
251
|
+
const out = RX.exportOf(md, fmt);
|
|
252
|
+
if (!out.ok) return bad(500, out.error);
|
|
253
|
+
return { status: 200, file: out };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function handle(req, res, url, ctx) {
|
|
257
|
+
const p = url.pathname;
|
|
258
|
+
const send = (r) => { ctx.json(res, r.body, r.status); return true; };
|
|
259
|
+
const isDocs = p === '/api/report-documents' || p.startsWith('/api/report-documents/');
|
|
260
|
+
const isExport = p === '/api/report-export' || p.startsWith('/api/report-export/');
|
|
261
|
+
const sub = isDocs ? 'documents' : isExport ? 'export' : p.replace(/^\/api\/report-setups\/?/, '');
|
|
262
|
+
|
|
263
|
+
if (isDocs || isExport || (sub === '' && req.method === 'GET')) {
|
|
264
|
+
if (req.method !== 'GET') return send(bad(405, 'GET only'));
|
|
265
|
+
if ((isDocs && p !== '/api/report-documents') || (isExport && p !== '/api/report-export')) return send(bad(404, 'no such report route'));
|
|
266
|
+
const w = wsOf(ctx, url.searchParams.get('ws'));
|
|
267
|
+
if (!w) return send(bad(404, 'no such workspace'));
|
|
268
|
+
const now = Date.now();
|
|
269
|
+
ensureMigrated(ctx, w, new Date(now));
|
|
270
|
+
if (isExport) {
|
|
271
|
+
const r = exportAnswer(ctx, res, w, url);
|
|
272
|
+
if (!r.file) return send(r);
|
|
273
|
+
const f = r.file;
|
|
274
|
+
res.writeHead(200, { 'Content-Type': f.mime, 'Content-Length': f.buffer.length, 'Content-Disposition': `attachment; filename="${f.name}"`, 'Cache-Control': 'no-store' });
|
|
275
|
+
res.end(f.buffer);
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
if (isDocs) return send({ status: 200, body: { docs: documentsOf(ctx, w).docs } });
|
|
279
|
+
return send({ status: 200, body: listAnswer(ctx, w, now) });
|
|
280
|
+
}
|
|
281
|
+
if (!POSTS.includes(sub)) return send(bad(404, 'no such report route'));
|
|
282
|
+
if (req.method !== 'POST') return send(bad(405, 'POST only'));
|
|
283
|
+
|
|
284
|
+
const read = await ctx.readJson(req);
|
|
285
|
+
if (!read.ok) return send(bad(400, 'the body is not JSON'));
|
|
286
|
+
const body = read.body || {};
|
|
287
|
+
if (req.headers && req.headers['x-pmos-session']) return send(bad(403, 'an agent session cannot set up, change or dismiss a report. Only a press on the page does'));
|
|
288
|
+
const g = guard.requirePagePress(req, body);
|
|
289
|
+
if (!g.ok) return send(bad(g.status, g.error));
|
|
290
|
+
const w = wsOf(ctx, text(body.ws));
|
|
291
|
+
if (!w) return send(bad(404, 'no such workspace'));
|
|
292
|
+
const root = w.root;
|
|
293
|
+
const file = RS.ledgerFile(root);
|
|
294
|
+
const opts = { by: g.actor, wsId: w.id, scheduleStore: ctx.scheduleStore };
|
|
295
|
+
const fail = (r) => send(bad(r.status || 400, r.error));
|
|
296
|
+
|
|
297
|
+
if (sub === 'ask') {
|
|
298
|
+
if (typeof ctx.spawnSession !== 'function' || headless.kindFor({ agent: 'claude', origin: 'report-setup:probe' }) !== 'report-setup') {
|
|
299
|
+
return send(bad(409, 'Background runs are switched off here (PMOS_HEADLESS=off), so this page cannot start the request.'));
|
|
300
|
+
}
|
|
301
|
+
const a = RS.askStart(file, { text: body.text, ws: w.id, by: g.actor });
|
|
302
|
+
if (!a.ok) return fail(a);
|
|
303
|
+
const ask = RS.fold(RS.readLines(file)).asks.get(a.id);
|
|
304
|
+
const s = startRun(ctx, w, file, ask);
|
|
305
|
+
if (!s.ok) { RS.askCancel(file, { id: a.id, by: g.actor }); return fail(s); }
|
|
306
|
+
return send({ status: 200, body: { ok: true, id: a.id } });
|
|
307
|
+
}
|
|
308
|
+
if (sub === 'change') {
|
|
309
|
+
const c = RS.askChange(file, { id: text(body.id), text: body.text, by: g.actor });
|
|
310
|
+
if (!c.ok) return fail(c);
|
|
311
|
+
const ask = RS.fold(RS.readLines(file)).asks.get(text(body.id));
|
|
312
|
+
const s = startRun(ctx, w, file, ask, { correction: String(body.text).replace(/\s+/g, ' ').trim(), proposal: c.ask.proposal });
|
|
313
|
+
if (!s.ok) { RS.askEnd(file, { id: ask.id, ok: false, error: s.error }); return fail(s); }
|
|
314
|
+
return send({ status: 200, body: { ok: true } });
|
|
315
|
+
}
|
|
316
|
+
if (sub === 'cancel') {
|
|
317
|
+
const id = text(body.id);
|
|
318
|
+
const r = RS.askCancel(file, { id, by: g.actor });
|
|
319
|
+
if (!r.ok) return fail(r);
|
|
320
|
+
const sid = runs.get(id);
|
|
321
|
+
runs.delete(id);
|
|
322
|
+
if (sid && typeof ctx.stopSession === 'function') { try { ctx.stopSession(sid); } catch { /* the run ends by itself */ } }
|
|
323
|
+
return send({ status: 200, body: { ok: true } });
|
|
324
|
+
}
|
|
325
|
+
if (sub === 'setup') {
|
|
326
|
+
const st = RS.fold(RS.readLines(file));
|
|
327
|
+
let from = null; let base = null;
|
|
328
|
+
if (body.ask) { const a = st.asks.get(text(body.ask)); if (!a) return send(bad(404, 'no such request')); from = { ask: a.id }; base = a.proposal; }
|
|
329
|
+
else if (body.finding) { const f = st.findings.get(text(body.finding)); if (!f || f.state !== 'open') return send(bad(404, 'no such finding')); from = { finding: f.id }; base = f.proposal; }
|
|
330
|
+
const proposal = body.proposal && typeof body.proposal === 'object' && !Array.isArray(body.proposal) ? body.proposal : base;
|
|
331
|
+
if (!proposal) return send(bad(400, 'say which request or finding to set up, or give the proposal'));
|
|
332
|
+
const r = RS.setUp(root, { ...proposal, ...(from ? { from } : {}) }, opts);
|
|
333
|
+
if (!r.ok) return fail(r);
|
|
334
|
+
const docsOf = { reports: [] };
|
|
335
|
+
return send({ status: 200, body: { ok: true, setup: setupOut(r.setup, new Date(), docsOf, new Map()), first_draft: r.first_draft } });
|
|
336
|
+
}
|
|
337
|
+
if (sub === 'dismiss') {
|
|
338
|
+
const r = RS.dismissFinding(file, { id: text(body.finding), by: g.actor });
|
|
339
|
+
if (!r.ok) return fail(r);
|
|
340
|
+
return send({ status: 200, body: { ok: true } });
|
|
341
|
+
}
|
|
342
|
+
if (sub === 'pause' || sub === 'resume') {
|
|
343
|
+
const r = RS[sub](root, text(body.id), opts);
|
|
344
|
+
if (!r.ok) return fail(r);
|
|
345
|
+
return send({ status: 200, body: { ok: true, setup: setupOut(r.setup, new Date(), documentsOf(ctx, w), copyIndex(root)) } });
|
|
346
|
+
}
|
|
347
|
+
if (sub === 'from-dashboard') {
|
|
348
|
+
const key = text(body.key);
|
|
349
|
+
if (!require('./report-dashboards.js').validKey(key)) return send(bad(400, 'key must be portfolio, board:<id> or initiative:<id>'));
|
|
350
|
+
const source = `dashboard:${key}`;
|
|
351
|
+
if ([...RS.fold(RS.readLines(file)).setups.values()].some((s) => s.source === source)) return send(bad(409, 'that dashboard is a recurring report already'));
|
|
352
|
+
const name = text(body.name).trim() || RS.dashTitle(key);
|
|
353
|
+
const audience = text(body.audience).trim();
|
|
354
|
+
const proposal = { name, purpose: `The ${name} dashboard as a report for ${audience || 'its readers'}.`, audience, source, output: text(body.output) || 'pdf',
|
|
355
|
+
schedule: body.schedule, sources: [`The ${name} dashboard`], sections: RS.FRAME.slice(), learned_from: 'Set up from the dashboard.', fixes: [] };
|
|
356
|
+
const r = RS.setUp(root, proposal, opts);
|
|
357
|
+
if (!r.ok) return fail(r);
|
|
358
|
+
return send({ status: 200, body: { ok: true, setup: setupOut(r.setup, new Date(), { reports: [] }, new Map()) } });
|
|
359
|
+
}
|
|
360
|
+
if (sub === 'finder') {
|
|
361
|
+
const r = RS.setFinder(file, { cadence: body.cadence, sources: body.sources, by: g.actor });
|
|
362
|
+
if (!r.ok) return fail(r);
|
|
363
|
+
return send({ status: 200, body: { ok: true, finder: finderOut(ctx, RS.fold(RS.readLines(file)), Date.now()) } });
|
|
364
|
+
}
|
|
365
|
+
return send(bad(404, 'no such report route'));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
module.exports = { prefixes: ['/api/report-setups', '/api/report-documents', '/api/report-export'], handle, tick, ensureMigrated, documentsOf, listAnswer, copyOf };
|
|
@@ -101,7 +101,7 @@ function finishReason(schedule, now = new Date()) {
|
|
|
101
101
|
return null;
|
|
102
102
|
}
|
|
103
103
|
// Is the schedule due to fire at `now`? Enabled + a slot in (lastRun, now]. For bi-weekly, also
|
|
104
|
-
// require ≥
|
|
104
|
+
// require the slot to be ≥ 12.5 days after lastRun (weekly slots, fired every other week).
|
|
105
105
|
function isDue(schedule, now = new Date()) {
|
|
106
106
|
if (!schedule || !schedule.enabled) return false;
|
|
107
107
|
if (finishReason(schedule, now)) return false;
|
|
@@ -111,8 +111,11 @@ function isDue(schedule, now = new Date()) {
|
|
|
111
111
|
if (last && last >= slot) return false;
|
|
112
112
|
// A one-off fires inside its grace window, once; after that it is missed (see isMissed), never caught up.
|
|
113
113
|
if (schedule.cadence === 'once') return !last && (now - slot) <= ONCE_GRACE_MS;
|
|
114
|
+
// The SLOT must be about two weeks after the last run, not the clock: counting from `now` fired a day early on the second cycle
|
|
115
|
+
// (13 days after a 09:00:30 fire is a Tuesday 09:00:30, and the newest slot then is the Wednesday before) and drifted a day each cycle.
|
|
116
|
+
// Half a day of slack lets a slot fire after a run a few minutes late. A slot a week after the last run is far outside it.
|
|
114
117
|
if (schedule.cadence === 'bi-weekly' && last) {
|
|
115
|
-
if ((
|
|
118
|
+
if ((slot - last) / 86400000 < 12.5) return false;
|
|
116
119
|
}
|
|
117
120
|
return true;
|
|
118
121
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.181.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pmos-workspace",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.181.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -43,8 +43,23 @@ function propView(id, p, cur) {
|
|
|
43
43
|
return { name, kind: !a ? 'added' : !b ? 'removed' : 'changed', before: a ? a.body : '', after: b ? b.body : '', current: c ? c.body : null, stale: a ? !c || c.body !== a.body : !!c }; }) };
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/* Report documents (initiative report-workspace): c3-demo-rs.js seeds the markdown of each cycle, its revisions, its open proposals and its checks here, so
|
|
47
|
+
* the document sheet reads them through the routes below. A path seeded here is answered as written; nothing else about the routes changes. */
|
|
48
|
+
const SEEDED = {}; // path -> the health answer { passed, total, checks }
|
|
49
|
+
export function seedDoc(path, revs, healthAnswer) {
|
|
50
|
+
DOCS[path] = { revs: revs.map((r, i) => ({ rev: i + 1, v: 'v' + (i + 1), by: r.by || 'claude-code/session', at: r.at || '2026-10-08T08:12:00.000Z', why: r.why || 'Drafted by PMOS', text: r.text, ...(r.via ? { via: r.via, text_by: r.text_by || 'agent' } : {}) })), events: [] };
|
|
51
|
+
if (healthAnswer) SEEDED[path] = healthAnswer;
|
|
52
|
+
}
|
|
53
|
+
export function seedProposal(id, path, o) {
|
|
54
|
+
const d = DOCS[path]; const before = d.revs[d.revs.length - 1].text;
|
|
55
|
+
PROPS[id] = { path, prompt: o.prompt, after: o.after, hash: hashOf(o.after), before, state: o.state || 'proposed', why: o.why || null };
|
|
56
|
+
}
|
|
57
|
+
export function reviseDoc(path, by, why, text) { if (DOCS[path]) push(path, by, why, text); }
|
|
58
|
+
export const docText = (path) => (DOCS[path] ? DOCS[path].revs[DOCS[path].revs.length - 1].text : '');
|
|
59
|
+
|
|
46
60
|
/* the mockup's docHealth for a report that is still a draft: Fresh passes, Complete does not (a draft), Traced passes, and the claims check. */
|
|
47
61
|
function health(path) {
|
|
62
|
+
if (SEEDED[path]) return SEEDED[path];
|
|
48
63
|
const R = window.__c3repix; const [fam, rep, aud] = path.slice(PREFIX.length, -3).split('-');
|
|
49
64
|
const stale = fam === 'communications' || !R ? 0 : R.sections(fam, rep, aud).filter((s) => s.st === 'stale').length;
|
|
50
65
|
const C = [{ id: 'fresh', pass: true, reason: 'Updated today' },
|