create-pmos 0.151.0 → 0.152.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/guide/ask-the-agent.md +5 -5
- package/workspace/guide/document-sheet.md +58 -0
- package/workspace/guide/index.md +1 -0
- package/workspace/lib/engine/doc-health.js +4 -2
- package/workspace/lib/engine/doc-sheet.js +49 -1
- package/workspace/lib/routes-doc-health.js +15 -4
- package/workspace/lib/routes-doc-sheet.js +154 -13
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-demo-boards.js +6 -1
- package/workspace/public/pm/c3-demo-init.js +44 -3
- package/workspace/public/pm/c3-demo-rep2.js +27 -6
- package/workspace/public/pm/c3-doc-sheet.js +586 -184
- package/workspace/public/pm/c3-md.js +395 -0
- package/workspace/public/pm/console3.css +165 -0
- package/workspace/public/pm/pm-work-view.js +1 -1
- package/workspace/public/pm/tokens.css +2 -2
- package/workspace/test-lists/c3-doc-sheet-rich.json +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.152.0",
|
|
4
4
|
"description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Eval calibration log"
|
|
4
4
|
description: "Append-only record of every PM override of an evaluator verdict."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-12T03:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-12T03:39:15Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Health budgets"
|
|
4
4
|
description: "Closed registry of health-budget anchors (D59), edited only by the PM."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-12T03:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-12T03:39:15Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Discoveries and friction log"
|
|
4
4
|
description: "Append-only record of what runs discovered and where the process rubbed."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-12T03:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-12T03:39:15Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -12,20 +12,20 @@ Five presses in the console (`/c3.html`) start an agent from the page. You do no
|
|
|
12
12
|
## Ask the agent on a document
|
|
13
13
|
|
|
14
14
|
1. Open a document in Initiatives › Documents, or open a report as a document from Reports.
|
|
15
|
-
2. Select **
|
|
15
|
+
2. Select **Proposals**, write what to change, and select **Ask the agent**.
|
|
16
16
|
3. The sheet says that the agent is drafting. You can close the sheet and come back.
|
|
17
|
-
4. When the draft is ready,
|
|
18
|
-
5.
|
|
17
|
+
4. When the draft is ready, it shows in **Proposals** with the difference, section by section.
|
|
18
|
+
5. Clear the tick of any section that you do not want, and slide to accept. Or select **Change the request** or **Reject**.
|
|
19
19
|
|
|
20
20
|
The agent runs in the background and can only read: it has the Read, Glob and Grep tools and nothing else. It answers with the revised document. The workspace records that answer as a proposal. The file does not change until you slide to accept it, and the slide is bound to the proposal's hash. If the document changed while the agent worked, the sheet tells you to ask again.
|
|
21
21
|
|
|
22
22
|
**Copy the request** stays beside the button. Use it to give the request to a session that you already have open.
|
|
23
23
|
|
|
24
|
-
On a document's **Health** segment, **Fix with the agent** writes the fix as the request and opens
|
|
24
|
+
On a document's **Health** segment, **Fix with the agent** writes the fix as the request and opens Proposals.
|
|
25
25
|
|
|
26
26
|
## Fix with the agent on a report
|
|
27
27
|
|
|
28
|
-
In Reports, open a section that is not traced and select **Fix with the agent**. The report opens as a document on
|
|
28
|
+
In Reports, open a section that is not traced and select **Fix with the agent**. The report opens as a document on Proposals, with the fix written in. A report that is not issued yet has no stored document, so the sheet tells you to issue it first.
|
|
29
29
|
|
|
30
30
|
## Lifecycle: a draft, a spike and an early gate
|
|
31
31
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Guide
|
|
3
|
+
title: "The document sheet"
|
|
4
|
+
description: "How to read a document as rich text, edit it, review the agent's proposals section by section, read the edit log, and clear a stale document."
|
|
5
|
+
timestamp: 2026-10-12
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# The document sheet
|
|
9
|
+
|
|
10
|
+
Open a document in Initiatives › Documents, in an initiative's Docs, or from Reports. The document opens in a sheet on the right. On a phone, it opens from the bottom. The segment under the title has six modes.
|
|
11
|
+
|
|
12
|
+
## Read
|
|
13
|
+
|
|
14
|
+
Read shows the document as formatted text: headings, bold, links, tables, checklists and callouts. The front matter shows as properties at the top.
|
|
15
|
+
|
|
16
|
+
- Each section shows who changed it last: a person, or the agent and the person who accepted it, with the revision and the day. Select the name to open that revision in the edit log.
|
|
17
|
+
- **Show who wrote what** colours each section: blue for a person, purple for the agent.
|
|
18
|
+
- **Markdown source** shows the file as text. **Copy the markdown** copies it.
|
|
19
|
+
- **Contents** goes to a section.
|
|
20
|
+
- When an open proposal changes a section, a note shows above that section. Select **Show change** to see the difference in place.
|
|
21
|
+
|
|
22
|
+
## Edit
|
|
23
|
+
|
|
24
|
+
Edit opens the whole document as rich text. Use the toolbar for the paragraph style, bold, italic, code, links, lists, checklists, tables and dividers. Use **Markdown** to edit the same draft as text, and **Rich** to go back. The front matter shows as fields.
|
|
25
|
+
|
|
26
|
+
1. Change the text. A changed section shows **Edited** next to its heading.
|
|
27
|
+
2. Write why you change it. The sheet does not save a change without a reason.
|
|
28
|
+
3. Select **Save**.
|
|
29
|
+
|
|
30
|
+
A section that you did not change is saved exactly as it was. The reason goes to the edit log with your name.
|
|
31
|
+
|
|
32
|
+
## Proposals
|
|
33
|
+
|
|
34
|
+
Proposals lists what the agent drafted and you did not accept or reject yet. To ask for a change, see [Ask the agent from the page](ask-the-agent.md).
|
|
35
|
+
|
|
36
|
+
1. Read the difference of each section. Added words are green. Removed words are struck through.
|
|
37
|
+
2. Clear the tick of each section that you do not want.
|
|
38
|
+
3. Slide to accept. Only the ticked sections are written, as one revision.
|
|
39
|
+
|
|
40
|
+
If a section changed after the agent drafted it, you cannot tick it. Ask the agent again. To reject a proposal, select **Reject** and write why.
|
|
41
|
+
|
|
42
|
+
## Edit log
|
|
43
|
+
|
|
44
|
+
The edit log lists every change, newest first: who wrote the text, how it came in, the reason, the sections and the words added and removed. It also lists rejected proposals, requests to the agent, and documents marked as still current. Use **All**, **People**, **Agent** and **Outside PMOS** to filter it.
|
|
45
|
+
|
|
46
|
+
- A change made outside the sheet (in an editor, or by a commit) shows **outside PMOS**, with its commit when git knows it. When it has no reason, select **Add a reason**. The reason is added to the log; the revision itself does not change.
|
|
47
|
+
- Select **Show the change** to see the difference.
|
|
48
|
+
|
|
49
|
+
## Stale documents
|
|
50
|
+
|
|
51
|
+
A document is stale when a document that it links to changed after it. The sheet shows a banner that lists those sources and the sections that cite them.
|
|
52
|
+
|
|
53
|
+
- Edit the sections, or ask the agent for an update.
|
|
54
|
+
- If the document is still right, select **Mark as still current** and write why. The mark counts as a change, so the banner clears.
|
|
55
|
+
|
|
56
|
+
## Health and Relations
|
|
57
|
+
|
|
58
|
+
Health shows the checks of the document and how many pass. Relations shows the documents around this one.
|
package/workspace/guide/index.md
CHANGED
|
@@ -69,6 +69,7 @@ On a desktop the workspace is one frame. [Layout](layout.md) explains each part.
|
|
|
69
69
|
- [Integrated work](integrated-work.md): how `pmos verify-work` turns a merged commit into Done, and what "Done · your call · not merged" means.
|
|
70
70
|
- [Report slides](report-slides.md): how the Reports page shows a report as the PM-OS biweekly slides, Present and Print / PDF, and how your notes become the words on the slides.
|
|
71
71
|
- [Reports: dashboards of reports](reports-dashboards.md): the console Reports page, its dashboards at portfolio, board and initiative scope, how to edit and share one as a draft, and the Drafts tab.
|
|
72
|
+
- [The document sheet](document-sheet.md): read a document as rich text, edit it, accept the agent's proposals section by section, read the edit log, and clear a stale document.
|
|
72
73
|
- [Ask the agent from the page](ask-the-agent.md): the five presses that start an agent from the console, what the agent may do, and what stays yours.
|
|
73
74
|
- [Sessions in console3](console3-sessions.md): the Sessions panel, how to open the terminal of a session on the same page, and how to end a session.
|
|
74
75
|
- [Settings in console3](settings.md): how to open Settings, what each section holds, the scope tags, the environment notes, and where the settings are kept.
|
|
@@ -24,8 +24,10 @@ const ts = (v) => { const t = Date.parse(v); return Number.isNaN(t) ? null : t;
|
|
|
24
24
|
function fresh(doc, c) {
|
|
25
25
|
const at = ts(doc.changed);
|
|
26
26
|
if (at == null) return na('fresh');
|
|
27
|
-
const
|
|
28
|
-
|
|
27
|
+
const newerSrc = (c.sources || []).filter((s) => ts(s.changed) != null && ts(s.changed) > at);
|
|
28
|
+
const newer = newerSrc.map((s) => s.path);
|
|
29
|
+
// `sources` names each newer source and when it changed, so a page can list them (the document sheet's stale banner)
|
|
30
|
+
if (newer.length) return { id: 'fresh', pass: false, reason: 'a cited source changed after this document: ' + newer.join(', '), sources: newerSrc.map((s) => ({ path: s.path, changed: s.changed })) };
|
|
29
31
|
if (c.staleAfterDays != null) {
|
|
30
32
|
const now = ts(c.now) != null ? ts(c.now) : Date.now();
|
|
31
33
|
const age = Math.floor((now - at) / day);
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
* askAgentProposal({ before, after, prompt }) → { state:'proposed', prompt, diff, hash } only a proposal; nothing is written
|
|
8
8
|
* acceptProposal(proposal, { hash, by }) → { ok:true, event } | { ok:false, reason } PM only, hash-bound
|
|
9
9
|
* changeNote(doc, downstreamDocs) → a DRAFT note for owners of documents whose source rev is behind; never sent
|
|
10
|
+
* splitSections(text) → the text cut at its headings: [{ i, prefix, name, lead, text, tail }], joined back exactly
|
|
11
|
+
* mergeSections(current, before, after, names) → { ok, text } with only the named sections of `after` applied to `current`;
|
|
12
|
+
* { ok:false, stale:[name] } when a named section changed since `before`
|
|
10
13
|
*
|
|
11
14
|
* The slide-to-accept is the same hash-bound confirm as the outbox: the hash covers the proposed text, so an edited
|
|
12
15
|
* 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.
|
|
@@ -72,4 +75,49 @@ function changeNote(doc, downstreamDocs) {
|
|
|
72
75
|
return { draft: true, sent: false, path: doc.path, rev: doc.rev, behind, body };
|
|
73
76
|
}
|
|
74
77
|
|
|
75
|
-
|
|
78
|
+
/* Sections: the same cut as the page's splitDoc (public/pm/c3-doc-sheet.js), so a name means the same section on both sides. */
|
|
79
|
+
function splitSections(text) {
|
|
80
|
+
const t = String(text == null ? '' : text);
|
|
81
|
+
const heads = []; let off = 0; let fence = false;
|
|
82
|
+
for (const line of t.split('\n')) {
|
|
83
|
+
if (/^\s*(```|~~~)/.test(line)) fence = !fence;
|
|
84
|
+
if (!fence && /^#{1,6}\s+\S/.test(line)) heads.push({ at: off, line, end: off + line.length + 1 });
|
|
85
|
+
off += line.length + 1;
|
|
86
|
+
}
|
|
87
|
+
const raw = [];
|
|
88
|
+
const first = heads.length ? heads[0].at : t.length;
|
|
89
|
+
if (first > 0 || !heads.length) raw.push({ prefix: '', name: /^---\n/.test(t) ? 'Front matter' : 'Overview', body: t.slice(0, first) });
|
|
90
|
+
heads.forEach((h, i) => raw.push({ prefix: t.slice(h.at, Math.min(h.end, t.length)), name: h.line.replace(/^#+\s+/, '').replace(/\s+#+\s*$/, '').trim(), body: t.slice(Math.min(h.end, t.length), i + 1 < heads.length ? heads[i + 1].at : t.length) }));
|
|
91
|
+
return raw.map((x, i) => {
|
|
92
|
+
const lead = x.body.match(/^\s*/)[0]; const rest = x.body.slice(lead.length); const tail = rest.match(/\s*$/)[0];
|
|
93
|
+
return { i, prefix: x.prefix, name: x.name, lead, tail, text: rest.slice(0, rest.length - tail.length) };
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
const joinSections = (secs) => secs.map((x) => x.prefix + x.lead + x.text + x.tail).join('');
|
|
97
|
+
|
|
98
|
+
// Apply only the named sections of a proposal. A named section must read in `current` as it read in `before` (the text the
|
|
99
|
+
// proposal was drafted against); otherwise it is stale and nothing is applied. A section new in `after` goes after the
|
|
100
|
+
// section that precedes it there, or last; a section `after` dropped is removed.
|
|
101
|
+
function mergeSections(current, before, after, names) {
|
|
102
|
+
const C = splitSections(current), B = splitSections(before), P = splitSections(after);
|
|
103
|
+
const want = [...new Set((names || []).map(String))];
|
|
104
|
+
const stale = want.filter((n) => { const b = B.find((x) => x.name === n), c = C.find((x) => x.name === n); return b ? (!c || c.text !== b.text) : !!c; });
|
|
105
|
+
const unknown = want.filter((n) => !B.some((x) => x.name === n) && !P.some((x) => x.name === n));
|
|
106
|
+
if (unknown.length) return { ok: false, unknown };
|
|
107
|
+
if (stale.length) return { ok: false, stale };
|
|
108
|
+
let out = C.map((x) => ({ ...x }));
|
|
109
|
+
for (const n of want) {
|
|
110
|
+
const p = P.find((x) => x.name === n); const k = out.findIndex((x) => x.name === n);
|
|
111
|
+
if (!p) { if (k >= 0) out.splice(k, 1); continue; }
|
|
112
|
+
if (k >= 0) { out[k] = { ...out[k], text: p.text }; continue; }
|
|
113
|
+
const prev = P[P.indexOf(p) - 1]; const at = prev ? out.findIndex((x) => x.name === prev.name) : -1;
|
|
114
|
+
const add = { ...p, tail: p.tail || '\n' };
|
|
115
|
+
if (at >= 0) { if (!/\n\s*\n$/.test(out[at].tail)) out[at] = { ...out[at], tail: '\n\n' }; out.splice(at + 1, 0, add); }
|
|
116
|
+
else if (prev === undefined && out.length && out[0].prefix === '' && out[0].name === 'Front matter') out.splice(1, 0, add);
|
|
117
|
+
else if (prev === undefined) out.unshift(add);
|
|
118
|
+
else { const l = out.length - 1; if (l >= 0 && !/\n\s*\n$/.test(out[l].tail)) out[l] = { ...out[l], tail: '\n\n' }; out.push(add); }
|
|
119
|
+
}
|
|
120
|
+
return { ok: true, text: joinSections(out) };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = { splitSections, joinSections, mergeSections, editWithWhy, revisions, foldAt, askAgentProposal, acceptProposal, changeNote, hashText, proposalHash };
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* A read: nothing is written and no page press is needed. The file is read from the repository (or the named workspace's
|
|
7
7
|
* root) and refused when it resolves outside that root. From git it derives: the last change of the file, the last change
|
|
8
|
-
* of every relative Markdown link it cites, and the last change of each ADR a `D<n>` id in its text names. Everything
|
|
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
11
|
*/
|
|
@@ -34,7 +34,18 @@ function adrFor(root, id) {
|
|
|
34
34
|
try { return fs.readdirSync(dir).find((f) => new RegExp('^d0*' + n + '-', 'i').test(f)) || null; } catch { return null; }
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
// The document's last change: its last commit, or a later save or "still current" mark in the document sheet, which
|
|
38
|
+
// writes the file without committing it (routes-doc-sheet.js lastSheetChange).
|
|
39
|
+
function lastChange(root, relOut, runtimeDir) {
|
|
40
|
+
const git = gitChanged(root, relOut);
|
|
41
|
+
let sheet = null;
|
|
42
|
+
if (runtimeDir) { try { sheet = require('./routes-doc-sheet.js').lastSheetChange(runtimeDir, relOut); } catch { sheet = null; } }
|
|
43
|
+
if (!git) return sheet;
|
|
44
|
+
if (!sheet) return git;
|
|
45
|
+
return Date.parse(sheet) > Date.parse(git) ? sheet : git;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function check(root, body, runtimeDir) {
|
|
38
49
|
const rel = typeof body.path === 'string' ? body.path : '';
|
|
39
50
|
if (!rel) return bad('path is required');
|
|
40
51
|
const full = path.resolve(root, rel);
|
|
@@ -42,7 +53,7 @@ function check(root, body) {
|
|
|
42
53
|
let text;
|
|
43
54
|
try { if (!inside(root, full)) return bad('outside workspace', 404); text = fs.readFileSync(full, 'utf8'); } catch { return bad('file not found', 404); }
|
|
44
55
|
const relOut = path.relative(root, full);
|
|
45
|
-
const changed =
|
|
56
|
+
const changed = lastChange(root, relOut, runtimeDir);
|
|
46
57
|
const sources = [];
|
|
47
58
|
for (const m of text.matchAll(/\]\(([^)#\s]+\.md)(?:#[^)]*)?\)/g)) {
|
|
48
59
|
if (/^[a-z]+:/i.test(m[1])) continue;
|
|
@@ -73,7 +84,7 @@ module.exports = {
|
|
|
73
84
|
const body = got.body || {};
|
|
74
85
|
const w = body.ws && ctx.getWs ? ctx.getWs(body.ws) : null;
|
|
75
86
|
if (body.ws && !w) { ctx.json(res, { ok: false, error: 'no workspace' }, 404); return true; }
|
|
76
|
-
const r = check(w ? w.root : (ctx.root || ROOT), body);
|
|
87
|
+
const r = check(w ? w.root : (ctx.root || ROOT), body, ctx.runtimeDir);
|
|
77
88
|
ctx.json(res, r.body, r.status);
|
|
78
89
|
return true;
|
|
79
90
|
},
|
|
@@ -11,6 +11,17 @@
|
|
|
11
11
|
* (lib/headless.js 'ask'); its answer becomes a proposal, never a write
|
|
12
12
|
* GET /api/doc-sheet/ask?id= → { ok, id, state: working | proposed | failed, error?, proposal? }
|
|
13
13
|
* POST /api/doc-sheet/change-note { path, downstream? } a draft body only; nothing is sent
|
|
14
|
+
* GET /api/doc-sheet/proposals?path= → { ok, proposals: [{ id, state, prompt, at, hash, after, sections: [{ name, kind, stale }] }] }
|
|
15
|
+
* GET /api/doc-sheet/log?path= → { ok, revisions, events } the edit log: revisions (with a reason added later) and
|
|
16
|
+
* the other entries (proposed, rejected, asked, marked current)
|
|
17
|
+
* POST /api/doc-sheet/reject { path, id, why } page press; a why is required; the proposal is closed, the file untouched
|
|
18
|
+
* POST /api/doc-sheet/why { path, rev, why } page press; a reason for a revision that has none (an outside change);
|
|
19
|
+
* appended to annotations.jsonl, the revision itself is not rewritten
|
|
20
|
+
* POST /api/doc-sheet/mark-current { path, why } page press; "still current" with a reason; it counts as the last change
|
|
21
|
+
*
|
|
22
|
+
* Every revision says who wrote the text and how it came in: via 'sheet' (an edit here), 'proposal' (accepted; text_by 'agent',
|
|
23
|
+
* by 'pm', the proposal id and the sections taken) or 'outside' (the file changed outside the sheet; git's last commit of
|
|
24
|
+
* the file names the author when there is one).
|
|
14
25
|
*
|
|
15
26
|
* Storage (append-only JSONL): <runtime>/doc-sheet/revisions.jsonl, proposals.jsonl and asks.jsonl.
|
|
16
27
|
* The write is the same one /api/doc/save makes: the path must resolve inside the repository root, only Markdown is
|
|
@@ -56,12 +67,29 @@ function resolveDoc(root, rel) {
|
|
|
56
67
|
|
|
57
68
|
const revsOf = (runtimeDir, rel) => S.revisions(readLines(path.join(dirOf(runtimeDir), 'revisions.jsonl')), rel);
|
|
58
69
|
|
|
70
|
+
// git's last commit of a file: who and why, when the file is committed as it reads now. Best effort; null otherwise.
|
|
71
|
+
function lastCommit(root, rel, text) {
|
|
72
|
+
try {
|
|
73
|
+
const { spawnSync } = require('child_process');
|
|
74
|
+
const r = spawnSync('git', ['log', '-1', '--format=%h%x09%an%x09%cI%x09%s', '--', rel], { cwd: root, encoding: 'utf8', timeout: 3000 });
|
|
75
|
+
if (r.status !== 0 || !r.stdout.trim()) return null;
|
|
76
|
+
const [commit, author, at, subject] = r.stdout.trim().split('\t');
|
|
77
|
+
const head = spawnSync('git', ['show', commit + ':' + rel.split(path.sep).join('/')], { cwd: root, encoding: 'utf8', timeout: 3000, maxBuffer: 8 << 20 });
|
|
78
|
+
if (head.status !== 0 || head.stdout !== text) return { committed: false };
|
|
79
|
+
return { committed: true, commit, author, at, subject: (subject || '').slice(0, 200), agent: /\b(claude|agent|bot)\b/i.test(author || '') };
|
|
80
|
+
} catch { return null; }
|
|
81
|
+
}
|
|
82
|
+
|
|
59
83
|
// Record the file's current text first when it moved outside this sheet (or was never seen), so the fold is complete.
|
|
60
|
-
function syncBaseline(runtimeDir, rel, text, now) {
|
|
84
|
+
function syncBaseline(runtimeDir, rel, text, now, root) {
|
|
61
85
|
const revs = revsOf(runtimeDir, rel), last = revs[revs.length - 1];
|
|
62
86
|
if (last && last.after_hash === S.hashText(text)) return last.rev;
|
|
63
87
|
const rev = last ? last.rev + 1 : 1;
|
|
64
|
-
const
|
|
88
|
+
const git = root ? lastCommit(root, rel, text) : null;
|
|
89
|
+
const why = git && git.committed && git.subject ? git.subject : (last ? 'changed outside the sheet' : 'first seen by the sheet');
|
|
90
|
+
const ev = { ...S.editWithWhy(last ? last.text : text, text, why, 'baseline', { rev, path: rel, at: now.toISOString() }), via: last ? 'outside' : 'first',
|
|
91
|
+
why_known: !!(git && git.committed && git.subject) };
|
|
92
|
+
if (git && git.committed) { ev.git = { commit: git.commit, author: git.author, at: git.at }; ev.text_by = git.agent ? 'agent' : 'person'; }
|
|
65
93
|
append(runtimeDir, 'revisions.jsonl', ev);
|
|
66
94
|
return rev;
|
|
67
95
|
}
|
|
@@ -75,21 +103,30 @@ function logChange(ctx, w, rel, before, after, why, by, now) {
|
|
|
75
103
|
try { fs.mkdirSync(dir, { recursive: true }); fs.appendFileSync(path.join(dir, 'doc-changes.jsonl'), JSON.stringify(rec) + '\n'); return true; } catch { return null; }
|
|
76
104
|
}
|
|
77
105
|
|
|
78
|
-
function writeDoc(root, rel, before, after, why, runtimeDir, ctx, w, now) {
|
|
106
|
+
function writeDoc(root, rel, before, after, why, runtimeDir, ctx, w, now, extra) {
|
|
79
107
|
const r = resolveDoc(root, rel); if (r.error) return r.error;
|
|
80
108
|
if (r.text !== before) return fail(409, 'stale: the document changed since it was read');
|
|
81
|
-
syncBaseline(runtimeDir, r.rel, r.text, now);
|
|
109
|
+
syncBaseline(runtimeDir, r.rel, r.text, now, root);
|
|
82
110
|
const rev = revsOf(runtimeDir, r.rel).length + 1;
|
|
83
|
-
const ev = S.editWithWhy(before, after, why, 'pm', { rev, path: r.rel, at: now.toISOString() });
|
|
111
|
+
const ev = { ...S.editWithWhy(before, after, why, 'pm', { rev, path: r.rel, at: now.toISOString() }), via: 'sheet', text_by: 'person', ...(extra || {}) };
|
|
84
112
|
try { fs.writeFileSync(r.full, after); } catch { return fail(500, 'could not write the document — nothing was changed'); }
|
|
85
113
|
append(runtimeDir, 'revisions.jsonl', ev);
|
|
86
114
|
const logged = logChange(ctx, w, r.rel, before, after, why, 'pm', now);
|
|
87
115
|
return { status: 200, body: { ok: true, path: r.rel, rev, revision: { ...ev, text: undefined }, logged: !!logged } };
|
|
88
116
|
}
|
|
89
117
|
|
|
118
|
+
const annotationsOf = (runtimeDir, rel) => readLines(path.join(dirOf(runtimeDir), 'annotations.jsonl')).filter((a) => a && a.path === rel);
|
|
119
|
+
function revsOut(runtimeDir, rel) {
|
|
120
|
+
const notes = annotationsOf(runtimeDir, rel);
|
|
121
|
+
return revsOf(runtimeDir, rel).map(({ text, ...rest }) => {
|
|
122
|
+
const n = notes.filter((a) => a.rev === rest.rev).pop();
|
|
123
|
+
return n ? { ...rest, why_added: n.why, why_added_at: n.at } : rest;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
90
126
|
function revisionsRoute(root, q, runtimeDir) {
|
|
91
127
|
const r = resolveDoc(root, q.path); if (r.error) return r.error;
|
|
92
|
-
|
|
128
|
+
syncBaseline(runtimeDir, r.rel, r.text, new Date(), root);
|
|
129
|
+
return { status: 200, body: { ok: true, path: r.rel, revisions: revsOut(runtimeDir, r.rel) } };
|
|
93
130
|
}
|
|
94
131
|
function atRoute(root, q, runtimeDir) {
|
|
95
132
|
const r = resolveDoc(root, q.path); if (r.error) return r.error;
|
|
@@ -100,6 +137,14 @@ function atRoute(root, q, runtimeDir) {
|
|
|
100
137
|
return { status: 200, body: { ok: true, path: r.rel, rev, text } };
|
|
101
138
|
}
|
|
102
139
|
|
|
140
|
+
/** The last time the document changed through this sheet or was marked current: the Fresh check counts it as a change. */
|
|
141
|
+
function lastSheetChange(runtimeDir, rel) {
|
|
142
|
+
let t = null;
|
|
143
|
+
for (const e of revsOf(runtimeDir, rel)) if (e.by !== 'baseline' && e.at && (!t || e.at > t)) t = e.at;
|
|
144
|
+
for (const m of readLines(path.join(dirOf(runtimeDir), 'marks.jsonl'))) if (m && m.path === rel && m.at && (!t || m.at > t)) t = m.at;
|
|
145
|
+
return t;
|
|
146
|
+
}
|
|
147
|
+
|
|
103
148
|
function edit(req, body, ctx, now = new Date()) {
|
|
104
149
|
const g = guard.requirePagePress(req, body); if (!g.ok) return fail(g.status, g.error);
|
|
105
150
|
if (typeof body.why !== 'string' || !body.why.trim()) return fail(400, 'a why is required: say why the document changes');
|
|
@@ -120,11 +165,16 @@ function propose(req, body, ctx, now = new Date()) {
|
|
|
120
165
|
if (!body || typeof body !== 'object' || Array.isArray(body)) return fail(400, 'body must be an object');
|
|
121
166
|
if (typeof body.after !== 'string') return fail(400, 'after is required');
|
|
122
167
|
const r = resolveDoc(ctx.root, body.path); if (r.error) return r.error;
|
|
168
|
+
// an agent answering a request from the sheet names the request; its prompt is the PM's words, kept on the request
|
|
169
|
+
const asked = typeof body.ask === 'string' ? readLines(path.join(dirOf(ctx.runtimeDir), 'asks.jsonl')).find((a) => a.id === body.ask && a.path === r.rel) : null;
|
|
170
|
+
if (asked && (typeof body.prompt !== 'string' || !body.prompt.trim())) body = { ...body, prompt: asked.prompt };
|
|
123
171
|
let p; try { p = S.askAgentProposal({ before: r.text, after: body.after, prompt: body.prompt }); } catch (e) { return fail(400, e.message); }
|
|
124
172
|
const id = typeof body.id === 'string' && proposalsOf(ctx.runtimeDir).has(body.id) ? body.id : 'prop-' + require('crypto').randomBytes(6).toString('hex');
|
|
125
173
|
const prior = proposalsOf(ctx.runtimeDir).get(id);
|
|
126
174
|
if (prior && prior.path !== r.rel) return fail(400, 'that proposal belongs to another document');
|
|
127
|
-
const row = { event: prior ? 'revised' : 'proposed', id, path: r.rel, at: now.toISOString(), state: 'proposed', prompt: p.prompt, before_hash: p.before_hash, before_text: r.text, after: p.after, hash: p.hash
|
|
175
|
+
const row = { event: prior ? 'revised' : 'proposed', id, path: r.rel, at: now.toISOString(), state: 'proposed', prompt: p.prompt, before_hash: p.before_hash, before_text: r.text, after: p.after, hash: p.hash,
|
|
176
|
+
by: body.by === 'pm' ? 'pm' : 'agent' };
|
|
177
|
+
if (asked) { row.ask = asked.id; if (asked.session) row.session = asked.session; }
|
|
128
178
|
append(ctx.runtimeDir, 'proposals.jsonl', row);
|
|
129
179
|
return { status: 200, body: { ok: true, id, state: 'proposed', hash: p.hash, diff: p.diff, voided_hash: prior ? prior.hash : null } };
|
|
130
180
|
}
|
|
@@ -137,12 +187,100 @@ function accept(req, body, ctx, now = new Date()) {
|
|
|
137
187
|
if (!p || p.path !== r.rel) return fail(404, 'no such proposal');
|
|
138
188
|
const res = S.acceptProposal(p, { hash: body.hash, by: 'pm' }); // by is set here, never read from the body
|
|
139
189
|
if (!res.ok) return fail(409, res.reason, { reason: res.reason });
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
if (
|
|
190
|
+
const all = changedNames(p.before_text, p.after);
|
|
191
|
+
const names = Array.isArray(body.sections) ? body.sections.map(String).filter((n) => all.includes(n)) : null;
|
|
192
|
+
if (names && !names.length) return fail(400, 'pick at least one section of the proposal');
|
|
193
|
+
let text;
|
|
194
|
+
if (names) {
|
|
195
|
+
const m = S.mergeSections(r.text, p.before_text, p.after, names);
|
|
196
|
+
if (!m.ok) return fail(409, m.stale ? 'stale: ' + m.stale.join(', ') + ' changed since this proposal was made' : 'not in the proposal: ' + m.unknown.join(', '), { reason: 'stale', stale: m.stale || [] });
|
|
197
|
+
text = m.text;
|
|
198
|
+
} else {
|
|
199
|
+
if (r.text !== p.before_text) return fail(409, 'stale: the document changed since this proposal was made', { reason: 'stale' });
|
|
200
|
+
text = p.after;
|
|
201
|
+
}
|
|
202
|
+
const taken = names || all;
|
|
203
|
+
const out = writeDoc(root, body.path, r.text, text, p.prompt, ctx.runtimeDir, ctx, ctx.w || null, now,
|
|
204
|
+
{ via: 'proposal', proposal: p.id, text_by: p.by === 'pm' ? 'person' : 'agent', sections: taken, of: all.length });
|
|
205
|
+
if (out.status === 200) append(ctx.runtimeDir, 'proposals.jsonl', { event: 'accepted', id: p.id, path: r.rel, at: now.toISOString(), state: 'accepted', hash: p.hash, sections: taken, of: all.length, rev: out.body.rev });
|
|
143
206
|
return out;
|
|
144
207
|
}
|
|
145
208
|
|
|
209
|
+
const changedNames = (a, b) => {
|
|
210
|
+
const A = S.splitSections(a), B = S.splitSections(b); const out = [];
|
|
211
|
+
B.forEach((x) => { const y = A.find((q) => q.name === x.name); if (!y || y.text !== x.text) out.push(x.name); });
|
|
212
|
+
A.forEach((y) => { if (!B.some((x) => x.name === y.name)) out.push(y.name); });
|
|
213
|
+
return out;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
function proposalsRoute(root, q, runtimeDir) {
|
|
217
|
+
const r = resolveDoc(root, q.path); if (r.error) return r.error;
|
|
218
|
+
const cur = S.splitSections(r.text);
|
|
219
|
+
const list = [...proposalsOf(runtimeDir).values()].filter((p) => p.path === r.rel).map((p) => {
|
|
220
|
+
const A = S.splitSections(p.before_text || ''), B = S.splitSections(p.after || '');
|
|
221
|
+
const sections = changedNames(p.before_text || '', p.after || '').map((name) => {
|
|
222
|
+
const a = A.find((x) => x.name === name), b = B.find((x) => x.name === name), c = cur.find((x) => x.name === name);
|
|
223
|
+
const kind = !a ? 'added' : !b ? 'removed' : 'changed';
|
|
224
|
+
const stale = a ? (!c || c.text !== a.text) : !!c;
|
|
225
|
+
return { name, kind, before: a ? a.text : '', after: b ? b.text : '', current: c ? c.text : null, stale };
|
|
226
|
+
});
|
|
227
|
+
return { id: p.id, state: p.state, prompt: p.prompt, at: p.at, hash: p.hash, by: p.by || 'agent', session: p.session || null, ask: p.ask || null,
|
|
228
|
+
closed_at: p.state === 'proposed' ? null : p.at, why: p.why || null, taken: p.sections || null, of: p.of || null, rev: p.rev || null, sections };
|
|
229
|
+
}).sort((a, b) => String(b.at).localeCompare(String(a.at)));
|
|
230
|
+
return { status: 200, body: { ok: true, path: r.rel, proposals: list } };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function logRoute(root, q, runtimeDir) {
|
|
234
|
+
const r = resolveDoc(root, q.path); if (r.error) return r.error;
|
|
235
|
+
syncBaseline(runtimeDir, r.rel, r.text, new Date(), root);
|
|
236
|
+
const events = [];
|
|
237
|
+
const firstOf = new Map();
|
|
238
|
+
for (const e of readLines(path.join(dirOf(runtimeDir), 'proposals.jsonl'))) {
|
|
239
|
+
if (!e || e.path !== r.rel) continue;
|
|
240
|
+
if (e.event === 'proposed' || e.event === 'revised') { if (!firstOf.has(e.id)) firstOf.set(e.id, e.prompt); events.push({ kind: e.event, at: e.at, id: e.id, prompt: e.prompt || firstOf.get(e.id), by: e.by || 'agent', session: e.session || null }); }
|
|
241
|
+
else if (e.event === 'rejected') events.push({ kind: 'rejected', at: e.at, id: e.id, prompt: firstOf.get(e.id) || null, why: e.why, by: 'pm' });
|
|
242
|
+
}
|
|
243
|
+
for (const a of readLines(path.join(dirOf(runtimeDir), 'asks.jsonl'))) {
|
|
244
|
+
if (!a || a.path !== r.rel) continue;
|
|
245
|
+
if (a.event === 'asked') events.push({ kind: 'asked', at: a.at, id: a.id, prompt: a.prompt, session: a.session || null, by: 'pm' });
|
|
246
|
+
else if (a.event === 'failed') events.push({ kind: 'ask-failed', at: a.at, id: a.id, why: a.error, by: 'agent' });
|
|
247
|
+
}
|
|
248
|
+
for (const m of readLines(path.join(dirOf(runtimeDir), 'marks.jsonl'))) if (m && m.path === r.rel) events.push({ kind: 'marked', at: m.at, why: m.why, by: 'pm' });
|
|
249
|
+
return { status: 200, body: { ok: true, path: r.rel, revisions: revsOut(runtimeDir, r.rel), events } };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const needWhy = (body) => (typeof body.why !== 'string' || !body.why.trim() ? fail(400, 'a why is required') : null);
|
|
253
|
+
|
|
254
|
+
function reject(req, body, ctx, now = new Date()) {
|
|
255
|
+
const g = guard.requirePagePress(req, body); if (!g.ok) return fail(g.status, g.error);
|
|
256
|
+
const nw = needWhy(body); if (nw) return nw;
|
|
257
|
+
const r = resolveDoc(ctx.root, body.path); if (r.error) return r.error;
|
|
258
|
+
const p = proposalsOf(ctx.runtimeDir).get(body.id);
|
|
259
|
+
if (!p || p.path !== r.rel) return fail(404, 'no such proposal');
|
|
260
|
+
if (p.state !== 'proposed') return fail(409, 'the proposal is already ' + p.state);
|
|
261
|
+
append(ctx.runtimeDir, 'proposals.jsonl', { event: 'rejected', id: p.id, path: r.rel, at: now.toISOString(), state: 'rejected', why: body.why.trim().slice(0, 400) });
|
|
262
|
+
return { status: 200, body: { ok: true, id: p.id, state: 'rejected' } };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function addWhy(req, body, ctx, now = new Date()) {
|
|
266
|
+
const g = guard.requirePagePress(req, body); if (!g.ok) return fail(g.status, g.error);
|
|
267
|
+
const nw = needWhy(body); if (nw) return nw;
|
|
268
|
+
const r = resolveDoc(ctx.root, body.path); if (r.error) return r.error;
|
|
269
|
+
const rev = Number(body.rev);
|
|
270
|
+
if (!revsOf(ctx.runtimeDir, r.rel).some((e) => e.rev === rev)) return fail(404, 'no such revision');
|
|
271
|
+
append(ctx.runtimeDir, 'annotations.jsonl', { path: r.rel, rev, why: body.why.trim().slice(0, 400), at: now.toISOString(), by: 'pm' });
|
|
272
|
+
return { status: 200, body: { ok: true, rev } };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function markCurrent(req, body, ctx, now = new Date()) {
|
|
276
|
+
const g = guard.requirePagePress(req, body); if (!g.ok) return fail(g.status, g.error);
|
|
277
|
+
const nw = needWhy(body); if (nw) return nw;
|
|
278
|
+
const r = resolveDoc(ctx.root, body.path); if (r.error) return r.error;
|
|
279
|
+
append(ctx.runtimeDir, 'marks.jsonl', { path: r.rel, why: body.why.trim().slice(0, 400), at: now.toISOString(), by: 'pm' });
|
|
280
|
+
return { status: 200, body: { ok: true, at: now.toISOString() } };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
|
|
146
284
|
/* ── Ask the agent from the page (page-starts-agent) ──
|
|
147
285
|
* The run reads the document and answers with the whole revised text between two marker lines. It has Read, Glob and Grep
|
|
148
286
|
* only, so it cannot write; its answer is recorded through propose() and changes nothing until the PM's slide accepts it. */
|
|
@@ -187,7 +325,7 @@ function settleAsk(ctx, a, { result, timedOut, stopped, note } = {}, now = new D
|
|
|
187
325
|
if (r.text !== a.before_text) return end({ event: 'failed', state: 'failed', error: 'The document changed while the agent worked. Ask again.' });
|
|
188
326
|
if (/\n$/.test(r.text) && !/\n$/.test(after)) after += '\n';
|
|
189
327
|
if (after === r.text) return end({ event: 'failed', state: 'failed', error: 'The agent proposed no change.' });
|
|
190
|
-
const p = propose(null, { path: a.path, prompt: a.prompt, after }, ctx, now);
|
|
328
|
+
const p = propose(null, { path: a.path, prompt: a.prompt, after, by: 'agent', ask: a.id }, ctx, now);
|
|
191
329
|
if (p.status !== 200) return end({ event: 'failed', state: 'failed', error: p.body.error });
|
|
192
330
|
return end({ event: 'proposed', state: 'proposed', proposal: p.body.id, hash: p.body.hash });
|
|
193
331
|
}
|
|
@@ -231,7 +369,7 @@ const respond = (ctx, res, r) => { ctx.json(res, r.body, r.status); return true;
|
|
|
231
369
|
|
|
232
370
|
module.exports = {
|
|
233
371
|
prefixes: ['/api/doc-sheet'],
|
|
234
|
-
resolveDoc, revisionsRoute, atRoute, edit, propose, accept, changeNote, ask, askState, askPrompt, parseAsk, settleAsk,
|
|
372
|
+
resolveDoc, revisionsRoute, atRoute, edit, propose, accept, changeNote, proposalsRoute, logRoute, reject, addWhy, markCurrent, lastSheetChange, ask, askState, askPrompt, parseAsk, settleAsk,
|
|
235
373
|
handle: async (req, res, url, ctx) => {
|
|
236
374
|
const p = url.pathname;
|
|
237
375
|
const w = (url.searchParams.get('ws') && ctx.getWs) ? ctx.getWs(url.searchParams.get('ws')) : null;
|
|
@@ -239,7 +377,10 @@ module.exports = {
|
|
|
239
377
|
if (req.method === 'GET' && p === '/api/doc-sheet/revisions') return respond(ctx, res, revisionsRoute(base.root, Object.fromEntries(url.searchParams), base.runtimeDir));
|
|
240
378
|
if (req.method === 'GET' && p === '/api/doc-sheet/ask') return respond(ctx, res, askState(Object.fromEntries(url.searchParams), base));
|
|
241
379
|
if (req.method === 'GET' && p === '/api/doc-sheet/at') return respond(ctx, res, atRoute(base.root, Object.fromEntries(url.searchParams), base.runtimeDir));
|
|
242
|
-
|
|
380
|
+
if (req.method === 'GET' && p === '/api/doc-sheet/proposals') return respond(ctx, res, proposalsRoute(base.root, Object.fromEntries(url.searchParams), base.runtimeDir));
|
|
381
|
+
if (req.method === 'GET' && p === '/api/doc-sheet/log') return respond(ctx, res, logRoute(base.root, Object.fromEntries(url.searchParams), base.runtimeDir));
|
|
382
|
+
const fn = req.method === 'POST' && { '/api/doc-sheet/edit': edit, '/api/doc-sheet/propose': propose, '/api/doc-sheet/accept': accept,
|
|
383
|
+
'/api/doc-sheet/reject': reject, '/api/doc-sheet/why': addWhy, '/api/doc-sheet/mark-current': markCurrent, '/api/doc-sheet/ask': ask,
|
|
243
384
|
'/api/doc-sheet/change-note': (rq, b, c) => changeNote(c.root, b, c.runtimeDir) }[p];
|
|
244
385
|
if (!fn) return false;
|
|
245
386
|
const got = await ctx.readJson(req);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.152.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.152.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -562,7 +562,7 @@ export function handle(S, method, path, p, body) {
|
|
|
562
562
|
|
|
563
563
|
/* ── views of other routes, for the Releases, Docs and initiative tabs (c3-ib-releases.js, c3-ib-docs.js, c3-ii-tabs.js) ──
|
|
564
564
|
* These answer only a request that carries `board=` (the tabs add it; the old page never does), so the old demo fixtures keep answering
|
|
565
|
-
* everything else: GET /api/releases, /api/docs/library, /api/my-work, /api/c3-init/breakdown, /api/doc and /api/doc-sheet/revisions.
|
|
565
|
+
* everything else: GET /api/releases, /api/docs/library, /api/my-work, /api/c3-init/breakdown, /api/doc and /api/doc-sheet/revisions, /proposals and /log.
|
|
566
566
|
* The rows are made from the boards' featured initiatives. IDPF-07 gets no documents on purpose, so the empty state can be seen. */
|
|
567
567
|
function handleMore(S, method, path, p) {
|
|
568
568
|
if (method !== 'GET') return undefined;
|
|
@@ -574,6 +574,11 @@ function handleMore(S, method, path, p) {
|
|
|
574
574
|
if (path === '/api/doc') return { status: 200, text };
|
|
575
575
|
return okb({ path: p.path, revisions: [{ rev: 1, by: 'human:wawan', at: at(-3, 9) + '.000Z', why: 'First version', before_hash: '', after_hash: '' }] });
|
|
576
576
|
}
|
|
577
|
+
/* the document sheet's Proposals and Edit log tabs: none yet, one first version */
|
|
578
|
+
if ((path === '/api/doc-sheet/proposals' || path === '/api/doc-sheet/log') && /^planning\/ib\//.test(p.path || '')) {
|
|
579
|
+
if (path === '/api/doc-sheet/proposals') return okb({ path: p.path, proposals: [] });
|
|
580
|
+
return okb({ path: p.path, revisions: [{ rev: 1, by: 'human:wawan', at: at(-3, 9) + '.000Z', why: 'First version', via: 'sheet', text_by: 'person', before_hash: '', after_hash: '' }], events: [] });
|
|
581
|
+
}
|
|
577
582
|
if (!p.board) return undefined;
|
|
578
583
|
if (path === '/api/releases') {
|
|
579
584
|
return okb({ board: { name: 'Demo board' }, releases: S.releases.filter((x) => x.board === p.board).map((x) => ({ id: x.id, name: x.name, type: 'both', initiative_ids: x.scope.slice(), deploy_at: x.date + 'T11:00:00', release_at: x.date + 'T12:00:00', accepted_at: x.status === 'Released' ? x.date + 'T12:30:00' : null, gapFlags: [], tags: { detected: [], reason: null }, summary: [], status: x.status, board: x.board })) });
|