create-pmos 0.528.0 → 0.530.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 +39 -35
- 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/briefing-premium-calendar-3-test.js +11 -9
- package/workspace/briefing-premium-calendar-test.js +1 -1
- package/workspace/changelog.json +38 -0
- package/workspace/fixtures/premium-viewer-fixtures.js +4 -2
- package/workspace/guide/idle-lab.md +1 -1
- package/workspace/guide/notes.md +20 -8
- package/workspace/guide/reports-set-up.md +3 -1
- package/workspace/lib/bg-queue.js +5 -1
- package/workspace/lib/jev-notes.js +58 -3
- package/workspace/lib/jev-screen.js +110 -0
- package/workspace/lib/meeting-replay.js +14 -2
- package/workspace/lib/meeting-suggest.js +105 -0
- package/workspace/lib/meeting-terms.js +57 -17
- package/workspace/lib/notes-screen.js +15 -1
- package/workspace/lib/report-chat.js +3 -3
- package/workspace/lib/routes-lab.js +44 -2
- package/workspace/lib/routes-meeting-points.js +27 -3
- package/workspace/lib/routes-meeting-replay.js +13 -2
- package/workspace/lib/routes-notes.js +25 -4
- package/workspace/lib/routes-report-flow.js +35 -5
- package/workspace/lib/typesafe.js +71 -5
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/premium-vw-meet-test.js +17 -11
- package/workspace/public/pm/cozy-lab-ins.js +13 -4
- package/workspace/public/pm/cozy-vw-mtg.css +122 -24
- package/workspace/public/pm/notes-screen-capture.js +5 -4
- package/workspace/public/pm/premium-meeting.js +2 -1
- package/workspace/public/pm/premium-mtg-replay.js +149 -73
- package/workspace/public/pm/premium-report-tab.js +33 -4
- package/workspace/public/pm/premium-vw-mtg.js +191 -53
- package/workspace/public/pm/report-chat.js +1 -0
- package/workspace/public/pm/report-flow.js +7 -3
- package/workspace/public/pm/view-notes.js +2 -2
- package/workspace/test-files.json +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.530.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": {
|
|
@@ -32,6 +32,44 @@
|
|
|
32
32
|
"audit"
|
|
33
33
|
],
|
|
34
34
|
"pmosChanges": [
|
|
35
|
+
{
|
|
36
|
+
"v": "0.530.0",
|
|
37
|
+
"at": "2026-11-04T14:38:26.528Z",
|
|
38
|
+
"f": 4,
|
|
39
|
+
"prs": [
|
|
40
|
+
{
|
|
41
|
+
"n": 1246,
|
|
42
|
+
"t": "Send meeting captures to Jev and keep only the slide (MpJ)",
|
|
43
|
+
"s": "Send meeting captures to Jev and keep only the slide (MpJ)",
|
|
44
|
+
"k": "new"
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"v": "0.529.0",
|
|
50
|
+
"at": "2026-11-04T13:14:44.790Z",
|
|
51
|
+
"f": 4,
|
|
52
|
+
"prs": [
|
|
53
|
+
{
|
|
54
|
+
"n": 1243,
|
|
55
|
+
"t": "A look change from a report's chat becomes its next look, kept with ✓ and redrawn into the open draft; the drafting session's answer shows",
|
|
56
|
+
"s": "A look change from a report's chat becomes its next look, kept with ✓ and redrawn into the open draft; the drafting session's answer shows",
|
|
57
|
+
"k": "fix"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"n": 1245,
|
|
61
|
+
"t": "The Lab's background record says in plain words what Claude did, what came out and when",
|
|
62
|
+
"s": "The Lab's background record says in plain words what Claude did, what came out and when",
|
|
63
|
+
"k": "fix"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"n": 1244,
|
|
67
|
+
"t": "Meeting player, Jev crop display and word fix A",
|
|
68
|
+
"s": "Meeting player, Jev crop display and word fix A",
|
|
69
|
+
"k": "new"
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
},
|
|
35
73
|
{
|
|
36
74
|
"v": "0.528.0",
|
|
37
75
|
"at": "2026-11-04T12:00:44.809Z",
|
|
@@ -395,40 +433,6 @@
|
|
|
395
433
|
"d": "Updates listed pull request titles and numbers, grouped by kind, which was hard to skim. Updates opens with a short summary of the week, then the newest version, then each change as a plain story, with a dot for New and Fixed."
|
|
396
434
|
}
|
|
397
435
|
]
|
|
398
|
-
},
|
|
399
|
-
{
|
|
400
|
-
"v": "0.510.0",
|
|
401
|
-
"at": "2026-11-03T14:33:47.373Z",
|
|
402
|
-
"f": 4,
|
|
403
|
-
"prs": [
|
|
404
|
-
{
|
|
405
|
-
"n": 1197,
|
|
406
|
-
"t": "Clean Updates lines: no, one PR number, no repeated title, right group",
|
|
407
|
-
"s": "Clean Updates lines: no, one PR number, no repeated title, right group",
|
|
408
|
-
"k": "fix",
|
|
409
|
-
"h": "Clearer update notes",
|
|
410
|
-
"d": "Update notes could repeat a line, show odd tags, or put every change under Improved. Now each change shows once, in the right group."
|
|
411
|
-
},
|
|
412
|
-
{
|
|
413
|
-
"n": 1199,
|
|
414
|
-
"t": "A Board product head's name opens its page in the Viewer",
|
|
415
|
-
"s": "A Board product head's name opens its page in the Viewer",
|
|
416
|
-
"k": "new"
|
|
417
|
-
}
|
|
418
|
-
]
|
|
419
|
-
},
|
|
420
|
-
{
|
|
421
|
-
"v": "0.509.0",
|
|
422
|
-
"at": "2026-11-03T14:23:32.696Z",
|
|
423
|
-
"f": 4,
|
|
424
|
-
"prs": [
|
|
425
|
-
{
|
|
426
|
-
"n": 1198,
|
|
427
|
-
"t": "No initiative without an expected outcome",
|
|
428
|
-
"s": "No initiative without an expected outcome",
|
|
429
|
-
"k": "new"
|
|
430
|
-
}
|
|
431
|
-
]
|
|
432
436
|
}
|
|
433
437
|
]
|
|
434
438
|
}
|
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-11-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-11-04T14:38:23Z"}
|
|
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-11-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-11-04T14:38:23Z"}
|
|
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-11-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-11-04T14:38:23Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -145,21 +145,23 @@ const json = (r, b, status = 200) => r.fulfill({ status, contentType: 'applicati
|
|
|
145
145
|
const r = C.page;
|
|
146
146
|
// batch 25 item 11 (MvC): no tabs; the kept points sit left, the timeline is docked, the lines are the Transcript part of the one strip
|
|
147
147
|
const unfold = async (id) => { if (await r.evaluate((k) => document.querySelector(`#czViewer .vzr-fold [data-a="strip"][data-k="${k}"]`).getAttribute('aria-expanded') !== 'true', id)) await tap(r, `.vzr-fold [data-a="strip"][data-k="${id}"]`); };
|
|
148
|
-
ok('R7 a kept recording has the replay card under the summary', await until(() => r.evaluate(() => !!document.querySelector('#czViewer .vzp-card .
|
|
149
|
-
const playing = () => r.evaluate(() => /^Pause/.test((document.querySelector('#czViewer .
|
|
150
|
-
ok('R7 Replay shows the player, the strip and the kept points (MtReplay)', await until(() => r.evaluate(() => !!document.querySelector('#czViewer .
|
|
151
|
-
await tap(r, '.
|
|
148
|
+
ok('R7 a kept recording has the replay card under the summary', await until(() => r.evaluate(() => !!document.querySelector('#czViewer .vzp-card .vzr-np') && !document.querySelector('#czViewer [role="tablist"]')), 15000));
|
|
149
|
+
const playing = () => r.evaluate(() => /^Pause/.test((document.querySelector('#czViewer .vzr-np:not(.vzr-ph1) [data-a="play"]') || { getAttribute: () => '' }).getAttribute('aria-label') || ''));
|
|
150
|
+
ok('R7 Replay shows the player, the strip and the kept points (MtReplay)', await until(() => r.evaluate(() => !!document.querySelector('#czViewer .vzr-np:not(.vzr-ph1) .vzp-strip') && document.querySelectorAll('#czViewer .vzp-kr').length > 0)));
|
|
151
|
+
await tap(r, '.vzr-np:not(.vzr-ph1) [data-a="play"]');
|
|
152
152
|
ok('R7 Play plays', await until(playing));
|
|
153
|
-
const pos = () => r.evaluate(() => (document.querySelector('#czViewer .
|
|
153
|
+
const pos = () => r.evaluate(() => (document.querySelector('#czViewer .vzr-np:not(.vzr-ph1) [data-clk="pos"]') || {}).textContent || '');
|
|
154
154
|
const p0 = await pos();
|
|
155
155
|
ok('R7 the playhead keeps time on its own clock', await until(async () => (await pos()) !== p0 && /^00:0[1-9]$/.test(await pos()), 5000), await pos());
|
|
156
|
-
await tap(r, '.
|
|
156
|
+
await tap(r, '.vzr-np:not(.vzr-ph1) [data-a="play"]');
|
|
157
157
|
const p1 = await pos();
|
|
158
158
|
await r.waitForTimeout(600); // longer than the playhead's 250 ms step: a paused playhead does not move
|
|
159
159
|
ok('R7 Pause stops the playhead', (await pos()) === p1 && !(await playing()), p1 + ' / ' + await pos());
|
|
160
|
-
await r
|
|
161
|
-
|
|
162
|
-
await
|
|
160
|
+
await tap(r, '.vzr-np:not(.vzr-ph1) [data-a="caps"]'); // batch 27 item 12 (MpD4): the captures row is hidden until its icon is pressed
|
|
161
|
+
await until(() => r.evaluate(() => !!document.querySelector('#czViewer .vzr-np:not(.vzr-ph1) .vzp-th[data-a="seekto"]')));
|
|
162
|
+
await r.$eval('#czViewer .vzr-np:not(.vzr-ph1) .vzp-th[data-a="seekto"]', (b) => b.click());
|
|
163
|
+
ok('R7 a capture plays from its time, with the chapter of that time named above the bar (MpD1)', await until(async () => /^03:1\d$/.test(await pos()) && await r.evaluate(() => /Viewer beside the chat/.test((document.querySelector('#czViewer .vzr-np:not(.vzr-ph1) .vzr-nt b') || {}).textContent || ''))), await pos());
|
|
164
|
+
await tap(r, '.vzr-np:not(.vzr-ph1) [data-a="play"]');
|
|
163
165
|
/* R12 every line: the Transcript fold seeks the replay */
|
|
164
166
|
await unfold('tr');
|
|
165
167
|
ok('R12 the Transcript lists every line, each with its time to play from', await until(async () => /Saved views move in their own work-item\./.test(await text(r)) && !!(await r.$('#czViewer [data-a="trseek"][data-t="760000"]'))), (await text(r)).slice(0, 300));
|
|
@@ -102,7 +102,7 @@ const json = (r, b, status = 200) => r.fulfill({ status, contentType: 'applicati
|
|
|
102
102
|
|
|
103
103
|
/* C8 a kept recording offers Replay */
|
|
104
104
|
const R = await open('/premium-viewer.html?open=meeting¬e=n1');
|
|
105
|
-
ok('C8 a kept recording offers Replay, and the card reads its replay', await until(() => R.page.evaluate(() => !!document.querySelector('#czViewer .vzp-card .
|
|
105
|
+
ok('C8 a kept recording offers Replay, and the card reads its replay', await until(() => R.page.evaluate(() => !!document.querySelector('#czViewer .vzp-card .vzr-np')), 15000) && R.S.gets.some((g) => /meeting-replay\?note=n1/.test(g)));
|
|
106
106
|
await R.ctx.close();
|
|
107
107
|
|
|
108
108
|
/* C3 honest states */
|
package/workspace/changelog.json
CHANGED
|
@@ -1,4 +1,42 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "0.530.0",
|
|
4
|
+
"at": "2026-11-04T14:38:26.528Z",
|
|
5
|
+
"f": 4,
|
|
6
|
+
"prs": [
|
|
7
|
+
{
|
|
8
|
+
"n": 1246,
|
|
9
|
+
"t": "Send meeting captures to Jev and keep only the slide (MpJ)",
|
|
10
|
+
"s": "Send meeting captures to Jev and keep only the slide (MpJ)",
|
|
11
|
+
"k": "new"
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"v": "0.529.0",
|
|
17
|
+
"at": "2026-11-04T13:14:44.790Z",
|
|
18
|
+
"f": 4,
|
|
19
|
+
"prs": [
|
|
20
|
+
{
|
|
21
|
+
"n": 1243,
|
|
22
|
+
"t": "A look change from a report's chat becomes its next look, kept with ✓ and redrawn into the open draft; the drafting session's answer shows",
|
|
23
|
+
"s": "A look change from a report's chat becomes its next look, kept with ✓ and redrawn into the open draft; the drafting session's answer shows",
|
|
24
|
+
"k": "fix"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"n": 1245,
|
|
28
|
+
"t": "The Lab's background record says in plain words what Claude did, what came out and when",
|
|
29
|
+
"s": "The Lab's background record says in plain words what Claude did, what came out and when",
|
|
30
|
+
"k": "fix"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"n": 1244,
|
|
34
|
+
"t": "Meeting player, Jev crop display and word fix A",
|
|
35
|
+
"s": "Meeting player, Jev crop display and word fix A",
|
|
36
|
+
"k": "new"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
},
|
|
2
40
|
{
|
|
3
41
|
"v": "0.528.0",
|
|
4
42
|
"at": "2026-11-04T12:00:44.809Z",
|
|
@@ -84,8 +84,10 @@ function install(page, o = {}) {
|
|
|
84
84
|
page.route('**/api/notes/series?*', (r) => json(r, { ok: true, series: { key: 'viewer-sync', meetings: [{ id: 'n0', at: '2026-10-15T10:00:00Z', decided: 1, steps: 3 }, { id: 'n1', at: W.NOTE.meeting.start, decided: 2, steps: 3 }] } })),
|
|
85
85
|
page.route('**/api/notes/search*', (r) => json(r, { ok: true, ids: ['n4'], hits: [{ id: 'n4', snip: '…which teams use saved views for teams every week…', open: true }] })),
|
|
86
86
|
page.route('**/api/meeting-points/*', (r) => { // a correction and its Undo (the answers lib/meeting-learn.js gives)
|
|
87
|
-
|
|
88
|
-
if (/
|
|
87
|
+
if (/suggest$/.test(new URL(r.request().url()).pathname)) return json(r, { ok: true, suggest: S.suggest || null }); // batch 27 item 12: the ✦ spelling (none unless a test sets one)
|
|
88
|
+
const b = post(r); if (/keep$/.test(new URL(r.request().url()).pathname)) { S.terms = (S.terms || []).map((x) => (x.id === b.lesson ? { ...x, keep: b.keep ? 'yes' : 'no' } : x)); return json(r, { ok: true, term: S.terms.find((x) => x.id === b.lesson), terms: S.terms }); }
|
|
89
|
+
if (/forget$/.test(new URL(r.request().url()).pathname)) { S.terms = (S.terms || []).filter((x) => x.id !== b.lesson); return json(r, { ok: true }); }
|
|
90
|
+
if (/term$/.test(new URL(r.request().url()).pathname)) { const t = { id: 't0123456789', note: b.note, heard: b.heard, means: b.means, keep: 'ask' }; S.terms = (S.terms || []).concat([t]); return json(r, { ok: true, term: t, terms: S.terms }); }
|
|
89
91
|
const phrase = require('../lib/meeting-learn.js').phraseOf(b.orig && b.orig.text, b.text);
|
|
90
92
|
S.edits = { ...(S.edits || {}), [b.id]: { kind: b.kind, text: b.text, orig: b.orig } };
|
|
91
93
|
return json(r, { ok: true, edits: S.edits, lesson: b.learn === false ? null : { id: 'l0123456789', phrase, kind: b.kind, meaning: b.text, orig: b.orig }, check: [] });
|
|
@@ -39,7 +39,7 @@ The Lab shows only once there is something to show. Top to bottom:
|
|
|
39
39
|
|
|
40
40
|
- One ✦ line for the week: the idle time Claude used, how many results you kept, and how many findings fit no initiative.
|
|
41
41
|
- The findings that fit no initiative, each with ✓ and ✕.
|
|
42
|
-
- **What Claude did in the background**: every background job this week, not only the idle lab.
|
|
42
|
+
- **What Claude did in the background**: every background job this week, not only the idle lab. Each row says in plain words what Claude did, why it ran and what came out, and when. For example: "Checked Inline login for eShop for risks · monthly check · 3 findings waiting · Sat 00:32", or "Redrew TSC Monthly Product Update in its saved look · 19:37". **See what it found** shows only when a result waits on the initiative. It opens the initiative, where the result is. It is not a second copy.
|
|
43
43
|
- **You kept**: how many prototypes and scans you kept.
|
|
44
44
|
- **Next time you step away**: what Claude plans to do next. Claude picks these by itself. To skip one, say so in chat.
|
|
45
45
|
|
package/workspace/guide/notes.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Guide: notes"
|
|
4
4
|
description: "How to take notes in a meeting: one note top to bottom, typed notes as timed lines, key points written each minute and labelled by Jev, delete a note; final notes written after Stop from the whole recording, with speakers and a quote for each item, decisions with their why and who, next steps with an open question, a discussion paragraph, and Wispr Flow's notes merged in and fetched by hand when the fetch fails; listen to your microphone and the meeting on this machine, see decisions, actions and questions caught as they are said, check them against the repository for conflicts, capture highlights during the meeting, create tasks from next steps, correct and tag lines, see what changed in your notes, replay the meeting, see screenshots of the shared screen beside the transcript, link a note to its meeting and its initiative, and get a summary."
|
|
5
|
-
timestamp: 2026-11-
|
|
5
|
+
timestamp: 2026-11-04
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Guide: notes
|
|
@@ -190,7 +190,7 @@ Without Jev, the workspace labels each line with its own rules and finds the clo
|
|
|
190
190
|
1. Save a TypeSafe key and switch on validation for the repository (see [Validate with TypeSafe](validate-with-typesafe.md)).
|
|
191
191
|
2. Open the settings and switch on **Label lines with Jev**. Switch on **Check notes against this repository with Jev** for the checks too. Both are off until you switch them on, for each repository.
|
|
192
192
|
|
|
193
|
-
With the switches on, each line goes to TypeSafe for its label. For a caught line, that line and one passage of the closest file go too. Audio, speaker names and private notes never go. A line that looks like a secret is not sent.
|
|
193
|
+
With the switches on, each line goes to TypeSafe for its label. For a caught line, that line and one passage of the closest file go too. Each screen capture goes too, so Jev can find the slide or document in it (see the meeting player below). Audio, speaker names and private notes never go. A line that looks like a secret is not sent, and a capture set aside as another app is not sent.
|
|
194
194
|
|
|
195
195
|
## Correct and tag a line
|
|
196
196
|
|
|
@@ -245,19 +245,31 @@ A meeting with a recording opens wide, in two columns. Press **Back to the side*
|
|
|
245
245
|
|
|
246
246
|
- **Left:** the summary (two lines; press **Read it all** for the rest), then the kept points. Each point is one line. The icon shows its kind: a green dot is a decision, a box is a next step, a question mark is an open question. The header counts each kind. A next step said within two minutes after a decision, in the same topic, shows under that decision, with **Make a task**.
|
|
247
247
|
- **Confirm:** press **✓** on a row to confirm it (press it again to undo). **Confirm N** in the header confirms every point that is left.
|
|
248
|
-
- **Right:**
|
|
248
|
+
- **Right:** one player. The screen capture at the playing time is at the top. Under it is the name of the chapter that plays, with **chapter N of M**. The bar under the name shows each chapter as one part, with a gap between parts, and an icon for each kept point in its chapter. Move the pointer over the bar to see the capture and the chapter at that place. Press the bar to play from there.
|
|
249
|
+
- **Controls:** back 15 seconds, play or pause, and forward 15 seconds, with **‹** and **›** for the previous and next chapter. The speed (1×, 1.5×, 2×) is at the left. At the right, the captures icon shows or hides the row of screen captures, and the chapters icon opens the list of chapters. Press a chapter in the list to play from its start. Escape closes the list.
|
|
250
|
+
- **Caption:** under the controls, the line said at the playing time shows larger, with the speaker. The line before it and the line after it show lighter.
|
|
251
|
+
- **A kept point:** press a row to play from that point. The selected row shows the pen (it opens the edit card) and **Not right** (it moves the point to **Left out**).
|
|
249
252
|
- **Under the points:** one row of parts. Press one to open it: **Left out**, **Next steps**, **Since the last one**, **Facts mentioned**, **Names and terms**, **Prep and material** and **Transcript**.
|
|
250
|
-
- **Bottom:** the timeline stays in view, with the full name of each chapter and a dot for each kept point.
|
|
251
253
|
|
|
252
|
-
The captures row
|
|
254
|
+
The captures row is hidden until you press the captures icon. It shows the five captures near the playing time, and **+N more** shows the rest.
|
|
255
|
+
|
|
256
|
+
When Jev is on for the repository (Settings › **Label lines with Jev**) and Jev gave a box for a capture, the player and the captures row show only the slide or document in that box. The meeting window and the faces are cut off, and a capture that shows only faces is not in the captures row. The icon at the top right of the picture shows the whole frame. When Jev is off, or gave no box, you see the whole frame. Captures kept before you switched Jev on are sent the next time you open the meeting. Jev reads pictures up to 1568 px a side. On a Mac a larger capture from an earlier meeting is made smaller first; on another computer it shows the whole frame.
|
|
253
257
|
|
|
254
258
|
### Fix a word that was heard wrong
|
|
255
259
|
|
|
256
|
-
|
|
260
|
+
Select a word or a few words anywhere in the meeting's notes: a kept point, the summary, **Next steps**, **Facts mentioned**, **Left out**, the caption or the transcript. You can also double-click one word. A small box opens under the words:
|
|
261
|
+
|
|
262
|
+
- **Heard as “…”** says what was heard. The field holds the fix. Type the right spelling and press **✓** or Enter. Escape closes the box and changes nothing.
|
|
263
|
+
- **✦ Claude suggests …** shows only when a real match was found: a fix you made before, a name in the meeting's invite (its title, agenda, material or people), or a word that the workspace's repository writes. The line names where the spelling came from. Press a spelling to put it in the field. When nothing matches, no suggestion shows.
|
|
264
|
+
- **N places in this meeting** counts where the words show, by kind: kept points, next steps, facts, **Left out** and transcript lines.
|
|
265
|
+
|
|
266
|
+
On a phone, the box opens at the bottom of the screen.
|
|
267
|
+
|
|
268
|
+
After the fix, the words show fixed in every place this meeting shows them, and the message says **Fixed in N places**, with **Undo**. The fix shows in **Names and terms**: the word, what was heard, the places, and an Undo icon.
|
|
257
269
|
|
|
258
|
-
|
|
270
|
+
**Names and terms** also asks **Remember “…” for your next meetings?** Press **✓** to remember it: later notes spell the word your way (the fix goes to your agent with the notes it writes), and in other meetings the same heard words are underlined, with your fix ready. Press **✕** to keep the fix in this meeting only. Until you press **✓**, the fix is for this meeting only. A fix made before this change stays remembered.
|
|
259
271
|
|
|
260
|
-
The note's own files do not change. The fix shows where PMOS draws the note.
|
|
272
|
+
The note's own files do not change. The fix shows where PMOS draws the note. A one-time hint under the kept points says how to select a word. It goes away after your first fix.
|
|
261
273
|
|
|
262
274
|
### Fix or delete a key point
|
|
263
275
|
|
|
@@ -59,7 +59,9 @@ Each cycle is a document: Draft or Issued, with its output, its copy state and w
|
|
|
59
59
|
- A download happens only when you press a download item.
|
|
60
60
|
- You issue a report. An issued report is frozen.
|
|
61
61
|
- While a draft is written, the report waits until the file is still, then shows it once. The slides do not flicker, and the scroll position stays.
|
|
62
|
-
- When a report has an open draft, keep talking in the chat. Each change
|
|
62
|
+
- When a report has an open draft, keep talking in the chat. Each change to its words, such as a date, a status or a wording, goes to that draft by itself. You do not press anything. The draft updates beside the chat, and Claude says in one line what it passed. An issued report does not change. Change its template for the next cycle.
|
|
63
|
+
- The report's session writes one line when it is done with your change: what it changed, or why it did not. The line shows in the report beside the chat. The report says "drafting it" only while a draft is written.
|
|
64
|
+
- A change to the look, such as the colours, the tables or the Gantt, or a deck or screenshot to follow, is not a change to the words. Attach the file in the chat if there is one. Claude starts the next look of the report from your words and the file. It shows in the report's template with ✓ and ✕. Press ✓ to keep it: the open draft is drawn again in the new look, and its numbers stay the same. Nothing is issued.
|
|
63
65
|
|
|
64
66
|
## Send a dashboard as a report
|
|
65
67
|
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* THE RECORD, <stateDir>/bg-queue.jsonl:
|
|
40
40
|
* turn { at, kind: 'turn', job, step, go, why } a job's turn changed (it was held and now goes, or the reason changed). A steady
|
|
41
41
|
* minute writes nothing.
|
|
42
|
-
* done { at, kind: 'done', job, title, ws, ini?, session, meta? } a background session ended (ended(), from the server's one
|
|
42
|
+
* done { at, kind: 'done', job, title, ws, ini?, session, meta?, ref? } a background session ended (ended(), from the server's one
|
|
43
43
|
* session-end hook). A session the PM started, or a chat, is not written.
|
|
44
44
|
* status() is the last plan, for the Lab ("what runs next"). record({ ws, since }) is the done lines, newest first, for the Lab's
|
|
45
45
|
* "What Claude did in the background" (lib/routes-lab.js): [{ job, title, meta?, at, ini?, board? }].
|
|
@@ -182,6 +182,8 @@ function create(host = {}) {
|
|
|
182
182
|
const job = jobOfOrigin(s.origin);
|
|
183
183
|
if (!job) return;
|
|
184
184
|
const line = { at: new Date(now).toISOString(), kind: 'done', job, title: String(s.name || job).slice(0, 160), ws: s.ws || null, session: s.id || null };
|
|
185
|
+
const ref = String(s.origin).split(':').slice(1).join(':').slice(0, 120); // what the job was for (a report's id for report-design), for the Lab's words
|
|
186
|
+
if (ref) line.ref = ref;
|
|
185
187
|
const ini = s.initiative || initiativeOfOrigin(s.origin);
|
|
186
188
|
if (ini) line.ini = String(ini);
|
|
187
189
|
if (s.outcome && typeof s.outcome === 'string') line.meta = s.outcome.slice(0, 160);
|
|
@@ -202,6 +204,8 @@ function create(host = {}) {
|
|
|
202
204
|
if (l.meta) row.meta = l.meta;
|
|
203
205
|
if (l.ini) row.ini = l.ini;
|
|
204
206
|
if (l.board) row.board = l.board;
|
|
207
|
+
if (l.ref) row.ref = l.ref;
|
|
208
|
+
if (l.session) row.session = l.session;
|
|
205
209
|
out.push(row);
|
|
206
210
|
}
|
|
207
211
|
return out.reverse();
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* gatesFor(key, flags) · checkGatesFor(key, flags) · senderFor(key, flags, askOpts) · infoFor(present, flags)
|
|
5
5
|
* label({ ask, key, askOpts, text, pause }) → { cat, p } | { cat: null, reason }
|
|
6
6
|
* compare({ ask, key, askOpts, text, passage, pause }) → { verdict: 'agrees'|'differs'|'related', words } | { verdict: null, reason }
|
|
7
|
+
* screen({ askImage, key, askOpts, image, root }) → { kind: 'slide'|'document'|'faces'|'unsure', box?, p } | { kind: null, reason }
|
|
7
8
|
* getSwitch(runtimeDir, root) · setSwitch(runtimeDir, root, { jev, check })
|
|
8
9
|
*
|
|
9
10
|
* WHAT MAY LEAVE THE MACHINE. For a label: one cleaned transcript line and one Choice question. For a check: the line and
|
|
@@ -13,6 +14,9 @@
|
|
|
13
14
|
* per-user runtime directory. Never sent: audio, a speaker's name, a line or passage the secret scan matches, a private
|
|
14
15
|
* note (the caller refuses it before this module), anything while paused.
|
|
15
16
|
*
|
|
17
|
+
* For a screen capture (MpJ): the capture's picture and nothing else, with one Choice and one Box question, under the same three
|
|
18
|
+
* gates. The caller sends only shots the note lists: never a shot set aside as another app, never one from a private note.
|
|
19
|
+
*
|
|
16
20
|
* WHAT A RESULT IS. Advisory (D40), shown in words. A label under UNSURE_BELOW keeps the local label. A check says
|
|
17
21
|
* "agrees" or "differs" only above SURE; otherwise "related", which is what the local match alone also says.
|
|
18
22
|
*
|
|
@@ -68,8 +72,8 @@ const checkGatesFor = (key, f) => checkGates({ key: typesafe.validKey(key), cons
|
|
|
68
72
|
/** { ask, check, askOpts }: `ask` sends labels and `check` is true when checks may also be sent. Both shut when a gate is. */
|
|
69
73
|
function senderFor(key, flags, askOpts = {}) {
|
|
70
74
|
const g = gatesFor(key, flags);
|
|
71
|
-
if (!g.open) return { ask: null, check: false, gates: g, askOpts: {} };
|
|
72
|
-
return { ask: (a) => typesafe.ask(a), check: checkGatesFor(key, flags).open, gates: g, askOpts: { ...NOTES_ASK, ...askOpts } };
|
|
75
|
+
if (!g.open) return { ask: null, askImage: null, check: false, gates: g, askOpts: {} };
|
|
76
|
+
return { ask: (a) => typesafe.ask(a), askImage: (a) => typesafe.askImage(a), check: checkGatesFor(key, flags).open, gates: g, askOpts: { ...NOTES_ASK, ...askOpts } };
|
|
73
77
|
}
|
|
74
78
|
/** What the view needs to explain the two switches. Booleans and sentences only. */
|
|
75
79
|
function infoFor(present, flags) {
|
|
@@ -86,6 +90,11 @@ function getSwitch(runtimeDir, root) {
|
|
|
86
90
|
const r = s && s.repos && root ? s.repos[root] : null;
|
|
87
91
|
return { jev: !!(r && r.jev === true), check: !!(r && r.check === true) };
|
|
88
92
|
}
|
|
93
|
+
/** The repositories whose switch "notes may use Jev" is on, sorted: a note bound to no repository looks at its shots under the first of these that also has consent (routes-notes.js lookAtShots). */
|
|
94
|
+
function onRoots(runtimeDir) {
|
|
95
|
+
const s = readJson(switchFile(runtimeDir), {});
|
|
96
|
+
return Object.entries((s && s.repos) || {}).filter(([, v]) => v && v.jev === true).map(([r]) => r).sort();
|
|
97
|
+
}
|
|
89
98
|
function setSwitch(runtimeDir, root, { jev, check } = {}) {
|
|
90
99
|
const s = readJson(switchFile(runtimeDir), {});
|
|
91
100
|
const prev = getSwitch(runtimeDir, root);
|
|
@@ -156,7 +165,53 @@ async function compare({ ask, key, askOpts = {}, text, passage, root = '' }) {
|
|
|
156
165
|
if (same != null && same >= SURE) return { verdict: 'agrees', words: wordsFor(same), model: res.model || null };
|
|
157
166
|
return { verdict: 'related', words: null, model: res.model || null };
|
|
158
167
|
}
|
|
168
|
+
/* ── screen captures (MpJ) ──────────────────────────────────────────────────────────────────────── */
|
|
169
|
+
const SHOT_KINDS = {
|
|
170
|
+
slide: 'A shared slide or presentation is on the screen.',
|
|
171
|
+
document: 'A shared document, web page, spreadsheet, board or app screen is on the screen.',
|
|
172
|
+
faces: 'Only the people in the meeting: their video tiles, pictures or names. Nothing is shared.',
|
|
173
|
+
other: 'Something else, such as an empty, loading or desktop screen.',
|
|
174
|
+
};
|
|
175
|
+
const SHOT_KIND_Q = 'The `capture` is a screenshot taken during a video meeting. What does it mainly show?';
|
|
176
|
+
const SHOT_BOX_Q = 'the shared slide or document in `capture`, without the meeting app\'s toolbars and without the video tiles of the people';
|
|
177
|
+
const SHOT_SHOWN = ['slide', 'document'];
|
|
178
|
+
const SHOT_ASK = { timeoutMs: 20000, retries: 1, backoffMs: 600 };
|
|
179
|
+
const r4 = (n) => Math.round(n * 10000) / 10000;
|
|
180
|
+
/** Jev's { top, bottom, left, right } (fractions of the picture) as [x, y, w, h], or null when it is not a useful crop. */
|
|
181
|
+
function boxOf(ans) {
|
|
182
|
+
const b = ans && ans.box;
|
|
183
|
+
if (!b || typeof b !== 'object') return null;
|
|
184
|
+
const [t, bo, l, ri] = [b.top, b.bottom, b.left, b.right].map(Number);
|
|
185
|
+
if (![t, bo, l, ri].every((v) => Number.isFinite(v) && v >= -0.01 && v <= 1.01)) return null;
|
|
186
|
+
const x = Math.max(0, l), y = Math.max(0, t), w = Math.min(1, ri) - x, h = Math.min(1, bo) - y;
|
|
187
|
+
if (!(w > 0.1) || !(h > 0.1)) return null; // a sliver is not a slide
|
|
188
|
+
if (w * h > 0.95) return null; // the whole frame already: nothing to crop
|
|
189
|
+
return [r4(x), r4(y), r4(w), r4(h)];
|
|
190
|
+
}
|
|
191
|
+
/** What one capture shows, from Jev. Faces are named only when Jev is sure; a crop only for a slide or a document. */
|
|
192
|
+
async function screen({ askImage, key, askOpts = {}, image, root = '' }) {
|
|
193
|
+
const p = pauseOf(root);
|
|
194
|
+
if (!askImage) return { kind: null, reason: 'off' };
|
|
195
|
+
if (p.paused) return { kind: null, reason: 'paused', sentence: p.paused };
|
|
196
|
+
const questions = { kind: { type: 'choice', instructions: SHOT_KIND_Q, criteria: SHOT_KINDS }, area: { type: 'box', instructions: SHOT_BOX_Q } };
|
|
197
|
+
const res = await askImage({ key, image, name: 'capture', questions, ...SHOT_ASK, ...askOpts });
|
|
198
|
+
if (!res.requested) return { kind: null, reason: 'refused', sentence: res.reason || null };
|
|
199
|
+
note(p, res);
|
|
200
|
+
if (!res.ok) return { kind: null, reason: 'failed', sentence: res.reason || null };
|
|
201
|
+
const probs = choiceProbs(res.answers && res.answers.kind);
|
|
202
|
+
const top = Object.entries(probs).filter(([k]) => SHOT_KINDS[k]).sort((a, b) => b[1] - a[1])[0];
|
|
203
|
+
const model = res.model || null;
|
|
204
|
+
if (!top) return { kind: null, reason: 'unreadable' };
|
|
205
|
+
if (top[0] === 'faces' && top[1] >= SURE) return { kind: 'faces', p: r4(top[1]), model };
|
|
206
|
+
if (SHOT_SHOWN.includes(top[0]) && top[1] >= UNSURE_BELOW) {
|
|
207
|
+
const box = boxOf(res.answers && res.answers.area);
|
|
208
|
+
return box ? { kind: top[0], box, p: r4(top[1]), model } : { kind: top[0], p: r4(top[1]), model };
|
|
209
|
+
}
|
|
210
|
+
return { kind: 'unsure', p: r4(top[1]), model };
|
|
211
|
+
}
|
|
212
|
+
|
|
159
213
|
function pauseStatus(root) { const p = pauses.get(root); return p && p.paused ? p.paused : null; }
|
|
160
214
|
|
|
161
|
-
module.exports = { gates, checkGates, gatesFor, checkGatesFor, gateSentence, senderFor, infoFor, getSwitch, setSwitch, label, compare,
|
|
215
|
+
module.exports = { gates, checkGates, gatesFor, checkGatesFor, gateSentence, senderFor, infoFor, getSwitch, setSwitch, onRoots, label, compare, screen, boxOf,
|
|
216
|
+
pictures: { imageSize: typesafe.imageSize, imageRefusal: typesafe.imageRefusal, IMAGE_SIDE: typesafe.IMAGE_SIDE }, choiceProbs, pauseStatus, SHOT_KINDS,
|
|
162
217
|
CATS, UNSURE_BELOW, SURE, REJECTED_LIMIT, UNREACHABLE_LIMIT };
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* jev-screen.js — Jev looks at a note's screen captures and keeps only the useful area (batch 27 item 12, board MpJ).
|
|
3
|
+
*
|
|
4
|
+
* run({ runtimeDir, id, g, screen?, jev?, fit? }) → Promise<{ asked, kept, skipped, stopped? }>
|
|
5
|
+
* busy(id) → boolean a run for the note is in progress
|
|
6
|
+
* fitFor(buf, { platform?, sips? }) → Promise<{ buf } | { reason }> a picture in Jev's size range, or why there is none
|
|
7
|
+
*
|
|
8
|
+
* WHAT IT DOES. For each shot the note lists that has no `jev` yet, in meeting-time order, it sends the picture to Jev
|
|
9
|
+
* (lib/jev-notes.js screen, through lib/typesafe.js askImage) and keeps the answer on the shot's row in the index:
|
|
10
|
+
* { kind: 'slide'|'document', box: [x, y, w, h] } the page shows only the box, with the whole-frame icon (premium-mtg-replay.js)
|
|
11
|
+
* { kind: 'faces' } the page leaves the shot out of the captures row
|
|
12
|
+
* { kind: 'slide'|'document'|'unsure' } (no box) the page shows the whole frame, as without Jev
|
|
13
|
+
* { kind: 'skip', why } not sent (out of Jev's size range and no way to resize here); whole frame
|
|
14
|
+
* x, y, w, h are fractions of the picture, so they hold for the kept file at any size.
|
|
15
|
+
*
|
|
16
|
+
* GATES. `g` is routes-notes.js gatesFor: no `askImage` (no key, no consent, the switch "notes may use Jev" off) means no
|
|
17
|
+
* request. Private notes and shots set aside as another app are never sent: the caller passes only an open note, and
|
|
18
|
+
* notes-screen.js list leaves set-aside shots out. It waits for the text reads first, because a text read is what sets a
|
|
19
|
+
* shot aside. A failed ask leaves the row as it was, so the next run asks again; a pause (jev-notes.js) stops the run.
|
|
20
|
+
*
|
|
21
|
+
* SIZE. Jev does not resize: each side 280 to 1568 px. The page captures at most 1568 px a side (notes-screen-capture.js);
|
|
22
|
+
* older shots were 1600 px wide. On a Mac, `sips` (part of macOS) makes a smaller copy in the temp folder, which is sent and
|
|
23
|
+
* deleted. Elsewhere such a shot is marked skip and shows whole.
|
|
24
|
+
*/
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const os = require('os');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const crypto = require('crypto');
|
|
29
|
+
const { execFile } = require('child_process');
|
|
30
|
+
const jevNotes = require('./jev-notes');
|
|
31
|
+
// the size rules come through the notes bridge: only the bridges require lib/typesafe.js (no-network.test.js)
|
|
32
|
+
const typesafe = jevNotes.pictures;
|
|
33
|
+
const notesScreen = require('./notes-screen');
|
|
34
|
+
|
|
35
|
+
const SIPS = '/usr/bin/sips';
|
|
36
|
+
const MAX_PER_RUN = 400;
|
|
37
|
+
const running = new Map(); // note id → the run in progress, so two triggers never ask twice for one shot
|
|
38
|
+
|
|
39
|
+
/** A copy of the picture in Jev's size range, made by sips on a Mac. */
|
|
40
|
+
function fitFor(buf, { platform = process.platform, sips = SIPS } = {}) {
|
|
41
|
+
const size = typesafe.imageSize(buf);
|
|
42
|
+
if (!size) return Promise.resolve({ reason: 'not a PNG or a JPEG' });
|
|
43
|
+
if (!typesafe.imageRefusal(size, buf.length)) return Promise.resolve({ buf });
|
|
44
|
+
const { min, max } = typesafe.IMAGE_SIDE;
|
|
45
|
+
if (size.w < min || size.h < min) return Promise.resolve({ reason: `${size.w} × ${size.h} px is smaller than Jev reads` });
|
|
46
|
+
let can = platform === 'darwin';
|
|
47
|
+
try { fs.accessSync(sips, fs.constants.X_OK); } catch { can = false; }
|
|
48
|
+
if (!can) return Promise.resolve({ reason: `${size.w} × ${size.h} px is larger than Jev reads, and this machine cannot make it smaller` });
|
|
49
|
+
const tmp = path.join(os.tmpdir(), 'pmos-jev-' + crypto.randomBytes(6).toString('hex'));
|
|
50
|
+
const src = tmp + '.' + size.format, out = tmp + '-fit.' + size.format;
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
try { fs.writeFileSync(src, buf, { mode: 0o600 }); } catch { resolve({ reason: 'the temp folder cannot be written' }); return; }
|
|
53
|
+
execFile(sips, ['-Z', String(max), src, '--out', out], { timeout: 20000, windowsHide: true }, (err) => {
|
|
54
|
+
let fit = null;
|
|
55
|
+
try { if (!err) fit = fs.readFileSync(out); } catch { fit = null; }
|
|
56
|
+
for (const f of [src, out]) { try { fs.unlinkSync(f); } catch { /* gone */ } }
|
|
57
|
+
const s2 = fit ? typesafe.imageSize(fit) : null;
|
|
58
|
+
resolve(fit && s2 && !typesafe.imageRefusal(s2, fit.length) ? { buf: fit } : { reason: 'the smaller copy could not be made' });
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function runNow({ runtimeDir, id, g, screen = notesScreen, jev = jevNotes, fit = fitFor }) {
|
|
64
|
+
const out = { asked: 0, kept: 0, skipped: 0 };
|
|
65
|
+
if (!g || !g.askImage) return { ...out, stopped: 'off' };
|
|
66
|
+
await screen.ocrIdle();
|
|
67
|
+
const todo = screen.list(runtimeDir, id).filter((f) => !f.jev).slice(0, MAX_PER_RUN);
|
|
68
|
+
for (const f of todo) {
|
|
69
|
+
const buf = screen.readShot(runtimeDir, id, f);
|
|
70
|
+
if (!buf) continue;
|
|
71
|
+
const ok = await fit(buf);
|
|
72
|
+
if (!ok.buf) { screen.setJev(runtimeDir, id, f, { kind: 'skip', why: ok.reason }); out.skipped++; continue; }
|
|
73
|
+
out.asked++;
|
|
74
|
+
const r = await jev.screen({ askImage: g.askImage, key: g.key, askOpts: g.askOpts, image: ok.buf, root: g.root });
|
|
75
|
+
if (r.reason === 'paused') return { ...out, stopped: 'paused' };
|
|
76
|
+
if (r.reason === 'refused') { screen.setJev(runtimeDir, id, f, { kind: 'skip', why: r.sentence || 'refused' }); out.skipped++; continue; }
|
|
77
|
+
if (!r.kind) continue; // failed or unreadable: asked again next run
|
|
78
|
+
const row = { kind: r.kind, at: Date.now() };
|
|
79
|
+
if (r.box) row.box = r.box;
|
|
80
|
+
if (r.p != null) row.p = r.p;
|
|
81
|
+
if (r.model) row.model = r.model;
|
|
82
|
+
if (screen.setJev(runtimeDir, id, f, row)) out.kept++;
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Ask Jev about the note's shots that have no answer yet. One run per note at a time: a call during a run makes the run
|
|
88
|
+
* go once more at its end, so a shot kept meanwhile is asked about too. */
|
|
89
|
+
function run(a) {
|
|
90
|
+
const cur = running.get(a.id);
|
|
91
|
+
if (cur) { cur.again = a; return cur.p; }
|
|
92
|
+
const st = { again: null, p: null };
|
|
93
|
+
st.p = (async () => {
|
|
94
|
+
let last;
|
|
95
|
+
let next = a;
|
|
96
|
+
while (next) {
|
|
97
|
+
st.again = null;
|
|
98
|
+
last = await runNow(next).catch(() => ({ asked: 0, kept: 0, skipped: 0, stopped: 'error' }));
|
|
99
|
+
next = last.stopped ? null : st.again;
|
|
100
|
+
}
|
|
101
|
+
return last;
|
|
102
|
+
})().finally(() => running.delete(a.id));
|
|
103
|
+
running.set(a.id, st);
|
|
104
|
+
return st.p;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Whether Jev is looking at this note's shots right now (the replay tells the page to read again when it ends). */
|
|
108
|
+
const busy = (id) => running.has(id);
|
|
109
|
+
|
|
110
|
+
module.exports = { run, busy, fitFor, MAX_PER_RUN };
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* topics: [{ id, text, src, state, minutes, n, first_t, stretches, parked, by }] visited ones first, in the order they came up
|
|
24
24
|
* runs: [{ topic, n, from_ms, to_ms }]
|
|
25
25
|
* notes: [{ id, t, cat, body, heard, why, src }] PMOS's notes with the time they were made
|
|
26
|
-
* shots: [{ t, w, h, why?, text? }]
|
|
26
|
+
* shots: [{ t, w, h, why?, text?, jev? }] kept shots only; jev = { kind, box? } when Jev for images read the shot
|
|
27
27
|
* recordings: how many times the recording was started (1 for most notes)
|
|
28
28
|
* gaps: [{ from_ms, to_ms }] where the recording was off between two starts (empty when there was one)
|
|
29
29
|
* aside_count }
|
|
@@ -105,13 +105,25 @@ function timeOfKey(item, lines) {
|
|
|
105
105
|
return null;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/** What Jev for images said of a shot (batch 27 item 12, MpJ), when a shot's index row holds it: the kind ('content', a slide or a
|
|
109
|
+
* document; 'faces', only people) and the box around the useful area, as fractions of the frame [x, y, w, h]. Anything else is dropped. */
|
|
110
|
+
/* lib/jev-screen.js keeps Jev's kind as slide or document; both are content here */
|
|
111
|
+
const CONTENT = ['content', 'slide', 'document'];
|
|
112
|
+
function jevOf(j) {
|
|
113
|
+
if (!j || typeof j !== 'object' || !(CONTENT.includes(j.kind) || j.kind === 'faces')) return null;
|
|
114
|
+
const b = Array.isArray(j.box) && j.box.length === 4 && j.box.every((x) => Number.isFinite(x) && x >= 0 && x <= 1) ? j.box.map((x) => Math.round(x * 1000) / 1000) : null;
|
|
115
|
+
const box = b && b[2] >= 0.05 && b[3] >= 0.05 && b[0] + b[2] <= 1.001 && b[1] + b[3] <= 1.001 ? b : null;
|
|
116
|
+
const kind = j.kind === 'faces' ? 'faces' : 'content';
|
|
117
|
+
return { kind, ...(box && kind === 'content' ? { box } : {}) };
|
|
118
|
+
}
|
|
119
|
+
|
|
108
120
|
/** Join the three records. */
|
|
109
121
|
function build({ note, event = null, stateLines = [], frames = [], audio = null, topics = [], now = Date.now() } = {}) {
|
|
110
122
|
const n = note || {};
|
|
111
123
|
const lines = linesOf(n);
|
|
112
124
|
const start = startOf(n, event);
|
|
113
125
|
const shots = (frames || []).filter((f) => f && finite(f.t) && !f.aside).sort((a, b) => a.t - b.t)
|
|
114
|
-
.map((f) => ({ t: f.t, w: f.w || 0, h: f.h || 0, ...(f.why ? { why: f.why } : {}), ...(f.text ? { text: str(f.text, SHOT_TEXT_MAX) } : {}) }));
|
|
126
|
+
.map((f) => ({ t: f.t, w: f.w || 0, h: f.h || 0, ...(f.why ? { why: f.why } : {}), ...(f.text ? { text: str(f.text, SHOT_TEXT_MAX) } : {}), ...(jevOf(f.jev) ? { jev: jevOf(f.jev) } : {}) }));
|
|
115
127
|
const asideCount = (frames || []).filter((f) => f && f.aside).length;
|
|
116
128
|
|
|
117
129
|
// Focus's state: topics, stretches, PMOS's notes. Stretches are measured from meeting time 0.
|