create-pmos 0.62.0 → 0.63.1
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/index.md +1 -0
- package/workspace/guide/layout.md +1 -1
- package/workspace/guide/notes.md +79 -0
- package/workspace/lib/jev-notes.js +162 -0
- package/workspace/lib/notes-repo.js +133 -0
- package/workspace/lib/notes-whisper.js +262 -0
- package/workspace/lib/notes.js +511 -0
- package/workspace/lib/route-modules.js +1 -0
- package/workspace/lib/routes-notes.js +240 -0
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/pm/components.css +272 -0
- package/workspace/public/pm/panel-cockpit.js +3 -0
- package/workspace/public/pm/view-notes.js +727 -0
- package/workspace/replaced-tests.json +14 -0
- package/workspace/server.js +3 -1
- package/workspace/test-lists/meeting-notes.json +15 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.63.1",
|
|
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-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-06T22:58:51Z"}
|
|
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-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-06T22:58:51Z"}
|
|
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-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-06T22:58:51Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
package/workspace/guide/index.md
CHANGED
|
@@ -48,6 +48,7 @@ On a desktop the workspace is one frame. [Layout](layout.md) explains each part.
|
|
|
48
48
|
- [Validate a repository](validate-with-typesafe.md): what each kind of document is compared with, how to read the verdict, and what leaves your machine.
|
|
49
49
|
- [Today and the proactive engine](proactive-engine.md): what Today finds, what you can do with an item, the settings, the commands and the limits.
|
|
50
50
|
- [Live notes](live-notes.md): take notes in a meeting, confirm each talking point, and what goes to Jev when you turn it on.
|
|
51
|
+
- [Notes](notes.md): listen to a meeting on this machine, see decisions, actions and questions caught and checked against the repository, and get a summary.
|
|
51
52
|
- [Find knowledge without reading whole files](knowledge-graph.md): what `pmos kg` finds, its commands, how to read its numbers, and its limits.
|
|
52
53
|
- [Initiatives: the Roadmap, the List and the initiative sheet](initiatives-screen.md): how to read and order the initiatives of a repository, open the sheet, make a new initiative, and review the cycle.
|
|
53
54
|
- [Initiatives and the Roadmap: Now, Next and Later](initiatives-roadmap.md): what day one shows, how an agent proposes a move, and `pmos initiative` and `pmos measure` from a terminal.
|
|
@@ -127,7 +127,7 @@ The address options `?layout=legacy` and `?fixture=cockpit` keep the older layou
|
|
|
127
127
|
|
|
128
128
|
The rail is 64 pixels wide and shows one tile for each repository. A dot on a tile means that repository has something that needs you. The selected repository has no dot, because the list beside it shows what needs you. The `+` tile adds a repository. The rail has no tools menu: the repository list and Validate are in Settings, and everything else is in ⌘K.
|
|
129
129
|
|
|
130
|
-
Today, Validate, live notes and Add repository open as a tab, not in place of the list. Choose the same one again and the existing tab comes to the front.
|
|
130
|
+
Today, Validate, live notes, notes and Add repository open as a tab, not in place of the list. Choose the same one again and the existing tab comes to the front.
|
|
131
131
|
|
|
132
132
|
Press ⌘K (Ctrl-K) to jump to anything: an initiative, a repository, an open tab, a session or a tool. Type to filter, use the arrow keys to choose, and press Enter. Esc closes it.
|
|
133
133
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Guide
|
|
3
|
+
title: "Guide: notes"
|
|
4
|
+
description: "How to take notes in a meeting: 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, and get a summary."
|
|
5
|
+
timestamp: 2026-10-06
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Guide: notes
|
|
9
|
+
|
|
10
|
+
The Notes view is one screen for one meeting. Your own notes are on the left, with the decisions, actions and questions that the workspace catches while people talk. The transcript is on the right. Notes are kept on this machine, outside every repository.
|
|
11
|
+
|
|
12
|
+
## Start a meeting
|
|
13
|
+
|
|
14
|
+
1. Press ⌘K, type **notes** and press Enter. The view opens as a tab. It is the same in every repository.
|
|
15
|
+
2. Press **New meeting**. Type a title. Type your own notes in the box below it; they steer the summary. The note saves itself when you stop typing.
|
|
16
|
+
3. Press **All notes** to open an earlier note. Press the settings button for local transcription, Jev, private and delete.
|
|
17
|
+
|
|
18
|
+
## Listen
|
|
19
|
+
|
|
20
|
+
1. Press **Listen**. Select what to listen to:
|
|
21
|
+
- **My microphone**: what you say.
|
|
22
|
+
- **The meeting**: the other people in an online meeting. Select the meeting's tab or window next, and tick **Share tab audio**.
|
|
23
|
+
2. Press **Start**. The header shows a red dot, the time, and the sources. Tell people that you are taking notes.
|
|
24
|
+
3. Each line shows in the transcript a few seconds after it is said, with who said it, the time and its label. Your lines are **You**. The meeting's lines are **Meeting**: click the name to change it. The transcript cannot tell the people in the meeting apart.
|
|
25
|
+
4. Press **Stop** to end. The summary shows at the top of your notes.
|
|
26
|
+
|
|
27
|
+
The meeting can only be heard when local transcription is set up. Until then, **Listen** hears your microphone only, through the browser's own recognition on this device.
|
|
28
|
+
|
|
29
|
+
## What is caught
|
|
30
|
+
|
|
31
|
+
Each line gets a label: decision, action, question, topic, statement or small talk. Decisions, actions and questions go into their sections as they are said. After **Stop**, statements with a number go under **Facts mentioned**.
|
|
32
|
+
|
|
33
|
+
- To remove an item that does not belong, press the **x** beside it.
|
|
34
|
+
- When a caught line is about something in the repository, the closest file shows beside it: **see** the file, a check mark when Jev finds that the file says the same, or **≠** the file when Jev finds that the line says something different. Point at the mark to see the file and the line.
|
|
35
|
+
|
|
36
|
+
## Set up local transcription
|
|
37
|
+
|
|
38
|
+
Local transcription uses whisper.cpp, a free program that runs on your machine. A Mac with Apple silicon (M1 or later) and 8 GB of memory is enough. The model is about 1.6 GB on disk and uses about 3 GB of memory while it runs.
|
|
39
|
+
|
|
40
|
+
1. Open the settings and press **Set up in a terminal**. A terminal opens with the setup commands typed in. They install whisper.cpp with Homebrew and download the model into the workspace's own folder.
|
|
41
|
+
2. Read the commands and press Enter in the terminal. The workspace does not run them for you.
|
|
42
|
+
3. Homebrew then says that whisper.cpp needs a model file. That is expected: the next command downloads it. Keep the terminal open until it says **Whisper is set up**.
|
|
43
|
+
4. Open the settings again. It says **Whisper is set up**.
|
|
44
|
+
|
|
45
|
+
If lines come slowly, choose **Faster (about 550 MB)** under **Model** in the settings, press **Set up in a terminal** and press Enter to download it. It is the same model in a smaller form: it starts and runs faster on an M1 and makes a few more mistakes. **Most accurate (1.6 GB)** is the default.
|
|
46
|
+
|
|
47
|
+
Whisper writes in the language you choose in the settings under **Language spoken in the meeting**: English unless you change it, or Indonesian. **Detect for each line** lets Whisper guess for each few seconds of speech; it can guess wrong and write a line in another language, so choose the language when you know it.
|
|
48
|
+
|
|
49
|
+
If the download stops, press **Set up in a terminal** again and press Enter. The download continues where it stopped, and a part-downloaded model is never used.
|
|
50
|
+
|
|
51
|
+
## Use Jev for labels and checks
|
|
52
|
+
|
|
53
|
+
Without Jev, the workspace labels each line with its own rules and finds the closest file by shared words. Jev labels better and can say whether a line agrees with the file.
|
|
54
|
+
|
|
55
|
+
1. Save a TypeSafe key and switch on validation for the repository (see [Validate with TypeSafe](validate-with-typesafe.md)).
|
|
56
|
+
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.
|
|
57
|
+
|
|
58
|
+
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.
|
|
59
|
+
|
|
60
|
+
## Dictate into your notes
|
|
61
|
+
|
|
62
|
+
1. Put the cursor where the text must go, then press **Dictate** and speak.
|
|
63
|
+
2. Say **new line** or **new paragraph** to start one. Say **scratch that** to remove the phrase you just said.
|
|
64
|
+
3. Press **Stop dictating** when you are done.
|
|
65
|
+
|
|
66
|
+
## Copy, private and delete
|
|
67
|
+
|
|
68
|
+
- **Copy as Markdown** puts the title, the summary, your notes, the caught items and the transcript on the clipboard.
|
|
69
|
+
- In the settings, **Mark private** stops listening at once. A private note is not recorded again, and nothing from it goes to Jev or to an agent. **Mark not private** undoes it.
|
|
70
|
+
- **Summary by agent** starts an agent session in the selected repository that writes a summary into the note. Lines that look like a secret are removed first.
|
|
71
|
+
- **Delete** removes the note, its transcript and its summaries from this machine after you confirm. You cannot undo it.
|
|
72
|
+
|
|
73
|
+
## What stays on your machine
|
|
74
|
+
|
|
75
|
+
- Audio never leaves this machine. The page sends it to the workspace on this machine, which gives it to whisper.cpp in a temporary file and deletes the file at once. The browser's own recognition is used only on the device, never in the cloud.
|
|
76
|
+
- Notes are files in the workspace's own folder on this machine (`notes/` in the state folder). They are never written into a repository.
|
|
77
|
+
- Only Jev, with the switches on, and **Summary by agent**, when you press it, receive any text from a note.
|
|
78
|
+
|
|
79
|
+
Live notes is a different tool: it maps each talking point to an initiative. See [Live notes](live-notes.md).
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* jev-notes.js — the one bridge from the Notes view to Jev (meeting-notes Run 2, D91).
|
|
3
|
+
*
|
|
4
|
+
* gatesFor(key, flags) · checkGatesFor(key, flags) · senderFor(key, flags, askOpts) · infoFor(present, flags)
|
|
5
|
+
* label({ ask, key, askOpts, text, pause }) → { cat, p } | { cat: null, reason }
|
|
6
|
+
* compare({ ask, key, askOpts, text, passage, pause }) → { verdict: 'agrees'|'differs'|'related', words } | { verdict: null, reason }
|
|
7
|
+
* getSwitch(runtimeDir, root) · setSwitch(runtimeDir, root, { jev, check })
|
|
8
|
+
*
|
|
9
|
+
* WHAT MAY LEAVE THE MACHINE. For a label: one cleaned transcript line and one Choice question. For a check: the line and
|
|
10
|
+
* ONE repository passage (lib/notes-repo.js closest, at most 1,200 characters) and two Noul questions. Only through
|
|
11
|
+
* lib/typesafe.js. Labels need three gates: a saved key, D79 consent for the repository and the switch "notes may use Jev".
|
|
12
|
+
* A check needs those and the switch "check notes against this repository". Both switches default off and live in the
|
|
13
|
+
* per-user runtime directory. Never sent: audio, a speaker's name, a line or passage the secret scan matches, a private
|
|
14
|
+
* note (the caller refuses it before this module), anything while paused.
|
|
15
|
+
*
|
|
16
|
+
* WHAT A RESULT IS. Advisory (D40), shown in words. A label under UNSURE_BELOW keeps the local label. A check says
|
|
17
|
+
* "agrees" or "differs" only above SURE; otherwise "related", which is what the local match alone also says.
|
|
18
|
+
*
|
|
19
|
+
* PAUSE. Two rejected keys or three failed asks in a row pause Jev for the repository until the server restarts or the
|
|
20
|
+
* PM turns a switch off and on again; the local labels carry on.
|
|
21
|
+
*/
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const typesafe = require('./typesafe');
|
|
25
|
+
const scanner = require('./secret-scan');
|
|
26
|
+
|
|
27
|
+
const UNSURE_BELOW = 0.55;
|
|
28
|
+
const SURE = 0.65;
|
|
29
|
+
const REJECTED_LIMIT = 2;
|
|
30
|
+
const UNREACHABLE_LIMIT = 3;
|
|
31
|
+
const NOTES_ASK = { timeoutMs: 10000, retries: 1, backoffMs: 400 };
|
|
32
|
+
|
|
33
|
+
const CATS = {
|
|
34
|
+
decision: 'A decision: something the group agreed, chose, approved or ruled out.',
|
|
35
|
+
action: 'An action item: a task someone will do, or is asked to do, often with an owner or a due day.',
|
|
36
|
+
question: 'An open question the meeting raises and has not answered.',
|
|
37
|
+
topic: 'A topic explanation: it introduces or explains what is being discussed, or moves to a new subject.',
|
|
38
|
+
statement: 'A statement of fact, data, a claim or an opinion about the work.',
|
|
39
|
+
'small talk': 'Small talk, a greeting, thanks, or talk about the meeting itself.',
|
|
40
|
+
};
|
|
41
|
+
const LABEL_INSTRUCTIONS = 'This is one line from a meeting transcript. Which kind of line is it?';
|
|
42
|
+
const SAME_Q = 'Does the PASSAGE from the repository already say what the MEETING LINE says?';
|
|
43
|
+
const DIFF_Q = 'Does the MEETING LINE say something different from, or in conflict with, the PASSAGE from the repository (for example another number, date, owner or choice)?';
|
|
44
|
+
|
|
45
|
+
/* ── gates and switches ─────────────────────────────────────────────────────────────────────────── */
|
|
46
|
+
function gates({ key, consent, jev }) {
|
|
47
|
+
const missing = [];
|
|
48
|
+
if (!key) missing.push('key');
|
|
49
|
+
if (!consent) missing.push('consent');
|
|
50
|
+
if (!jev) missing.push('jev');
|
|
51
|
+
return { open: missing.length === 0, missing };
|
|
52
|
+
}
|
|
53
|
+
function checkGates({ key, consent, jev, check }) {
|
|
54
|
+
const g = gates({ key, consent, jev });
|
|
55
|
+
if (!check) g.missing.push('check');
|
|
56
|
+
return { open: g.missing.length === 0, missing: g.missing };
|
|
57
|
+
}
|
|
58
|
+
const GATE_WORDS = {
|
|
59
|
+
key: 'No TypeSafe key is saved.',
|
|
60
|
+
consent: 'Validation is not switched on for this repository.',
|
|
61
|
+
jev: 'Notes may not use Jev in this repository.',
|
|
62
|
+
check: 'Checking notes against this repository is off.',
|
|
63
|
+
};
|
|
64
|
+
const gateSentence = (missing) => missing.map((m) => GATE_WORDS[m]).join(' ');
|
|
65
|
+
const gatesFor = (key, f) => gates({ key: typesafe.validKey(key), consent: !!(f && f.consent), jev: !!(f && f.jev) });
|
|
66
|
+
const checkGatesFor = (key, f) => checkGates({ key: typesafe.validKey(key), consent: !!(f && f.consent), jev: !!(f && f.jev), check: !!(f && f.check) });
|
|
67
|
+
|
|
68
|
+
/** { ask, check, askOpts }: `ask` sends labels and `check` is true when checks may also be sent. Both shut when a gate is. */
|
|
69
|
+
function senderFor(key, flags, askOpts = {}) {
|
|
70
|
+
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 } };
|
|
73
|
+
}
|
|
74
|
+
/** What the view needs to explain the two switches. Booleans and sentences only. */
|
|
75
|
+
function infoFor(present, flags) {
|
|
76
|
+
const f = { key: !!present, consent: !!(flags && flags.consent), jev: !!(flags && flags.jev), check: !!(flags && flags.check) };
|
|
77
|
+
const l = gates(f); const c = checkGates(f);
|
|
78
|
+
return { jev: f.jev, check: f.check, label_open: l.open, check_open: c.open, missing: c.missing,
|
|
79
|
+
sentence: l.open ? (c.open ? null : gateSentence(c.missing.filter((m) => m === 'check'))) : gateSentence(l.missing) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const readJson = (f, d) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return d; } };
|
|
83
|
+
const switchFile = (runtimeDir) => path.join(runtimeDir, 'notes', 'settings.json');
|
|
84
|
+
function getSwitch(runtimeDir, root) {
|
|
85
|
+
const s = readJson(switchFile(runtimeDir), {});
|
|
86
|
+
const r = s && s.repos && root ? s.repos[root] : null;
|
|
87
|
+
return { jev: !!(r && r.jev === true), check: !!(r && r.check === true) };
|
|
88
|
+
}
|
|
89
|
+
function setSwitch(runtimeDir, root, { jev, check } = {}) {
|
|
90
|
+
const s = readJson(switchFile(runtimeDir), {});
|
|
91
|
+
const prev = getSwitch(runtimeDir, root);
|
|
92
|
+
const next = { jev: typeof jev === 'boolean' ? jev : prev.jev, check: typeof check === 'boolean' ? check : prev.check };
|
|
93
|
+
const f = switchFile(runtimeDir);
|
|
94
|
+
fs.mkdirSync(path.dirname(f), { recursive: true });
|
|
95
|
+
const tmp = f + '.' + process.pid + '.tmp';
|
|
96
|
+
fs.writeFileSync(tmp, JSON.stringify({ repos: { ...((s && s.repos) || {}), [root]: next } }), { mode: 0o600 });
|
|
97
|
+
fs.renameSync(tmp, f);
|
|
98
|
+
pauses.delete(root);
|
|
99
|
+
return next;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* ── pause ─────────────────────────────────────────────────────────────────────────────────────── */
|
|
103
|
+
const pauses = new Map(); // root → { rejected, unreachable, paused }
|
|
104
|
+
const pauseOf = (root) => { if (!pauses.has(root)) pauses.set(root, { rejected: 0, unreachable: 0, paused: null }); return pauses.get(root); };
|
|
105
|
+
function note(p, res) {
|
|
106
|
+
if (res.ok) { p.rejected = 0; p.unreachable = 0; return; }
|
|
107
|
+
if (res.status === 401) { p.rejected++; p.unreachable = 0; } else { p.unreachable++; p.rejected = 0; }
|
|
108
|
+
if (p.rejected >= REJECTED_LIMIT) p.paused = 'The service rejected the key twice, so Jev is paused for notes.';
|
|
109
|
+
else if (p.unreachable >= UNREACHABLE_LIMIT) p.paused = 'The service could not be reached three times in a row, so Jev is paused for notes.';
|
|
110
|
+
}
|
|
111
|
+
const secretIn = (...texts) => texts.some((t) => scanner.scan(String(t || '')).length > 0);
|
|
112
|
+
|
|
113
|
+
/* ── asking ────────────────────────────────────────────────────────────────────────────────────── */
|
|
114
|
+
function choiceProbs(ans) {
|
|
115
|
+
const probs = {};
|
|
116
|
+
const put = (k, v) => { const n = Number(v); if (typeof k === 'string' && Number.isFinite(n)) probs[k.toLowerCase()] = n; };
|
|
117
|
+
if (!ans || typeof ans !== 'object') return probs;
|
|
118
|
+
const raw = ans.probabilities || ans.distribution;
|
|
119
|
+
if (Array.isArray(raw)) { for (const x of raw) if (x && typeof x === 'object') put(x.option || x.label || x.choice, x.probability != null ? x.probability : x.p); }
|
|
120
|
+
else if (raw && typeof raw === 'object') for (const [k, v] of Object.entries(raw)) put(k, v);
|
|
121
|
+
if (!Object.keys(probs).length && typeof ans.choice === 'string') put(ans.choice, ans.confidence != null ? ans.confidence : 1);
|
|
122
|
+
return probs;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** One line's label from Jev. The caller has checked the private flag; this checks the pause and the scan. */
|
|
126
|
+
async function label({ ask, key, askOpts = {}, text, root = '' }) {
|
|
127
|
+
const p = pauseOf(root);
|
|
128
|
+
if (!ask) return { cat: null, reason: 'off' };
|
|
129
|
+
if (p.paused) return { cat: null, reason: 'paused', sentence: p.paused };
|
|
130
|
+
if (secretIn(text)) return { cat: null, reason: 'withheld' };
|
|
131
|
+
const res = await ask({ key, state: String(text).slice(0, 2000), questions: { kind: { type: 'choice', instructions: LABEL_INSTRUCTIONS, criteria: CATS } }, ...askOpts });
|
|
132
|
+
note(p, res);
|
|
133
|
+
if (!res.ok) return { cat: null, reason: 'failed', sentence: res.reason || null };
|
|
134
|
+
const probs = choiceProbs(res.answers && res.answers.kind);
|
|
135
|
+
const top = Object.entries(probs).filter(([k]) => CATS[k]).sort((a, b) => b[1] - a[1])[0];
|
|
136
|
+
if (!top || top[1] < UNSURE_BELOW) return { cat: null, reason: 'unsure' };
|
|
137
|
+
return { cat: top[0], p: top[1], model: res.model || null };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const wordsFor = (v) => (v >= 0.85 ? 'very likely' : 'likely');
|
|
141
|
+
/** One captured line against one passage. */
|
|
142
|
+
async function compare({ ask, key, askOpts = {}, text, passage, root = '' }) {
|
|
143
|
+
const p = pauseOf(root);
|
|
144
|
+
if (!ask) return { verdict: null, reason: 'off' };
|
|
145
|
+
if (p.paused) return { verdict: null, reason: 'paused', sentence: p.paused };
|
|
146
|
+
if (!passage || !passage.text) return { verdict: null, reason: 'nothing' };
|
|
147
|
+
if (secretIn(text, passage.text)) return { verdict: null, reason: 'withheld' };
|
|
148
|
+
const state = 'MEETING LINE:\n' + String(text).slice(0, 2000) + '\n\nPASSAGE (' + passage.path + '):\n' + String(passage.text).slice(0, 1200);
|
|
149
|
+
const res = await ask({ key, state, questions: { same: { type: 'noul', instructions: SAME_Q }, differs: { type: 'noul', instructions: DIFF_Q } }, ...askOpts });
|
|
150
|
+
note(p, res);
|
|
151
|
+
if (!res.ok) return { verdict: null, reason: 'failed', sentence: res.reason || null };
|
|
152
|
+
const num = (a) => { const v = a && (a.type == null || a.type === 'noul') ? Number(a.noul) : NaN; return Number.isFinite(v) && v >= 0 && v <= 1 ? v : null; };
|
|
153
|
+
const same = num(res.answers && res.answers.same); const diff = num(res.answers && res.answers.differs);
|
|
154
|
+
if (same == null && diff == null) return { verdict: null, reason: 'unreadable' };
|
|
155
|
+
if (diff != null && diff >= SURE && (same == null || diff > same)) return { verdict: 'differs', words: wordsFor(diff), model: res.model || null };
|
|
156
|
+
if (same != null && same >= SURE) return { verdict: 'agrees', words: wordsFor(same), model: res.model || null };
|
|
157
|
+
return { verdict: 'related', words: null, model: res.model || null };
|
|
158
|
+
}
|
|
159
|
+
function pauseStatus(root) { const p = pauses.get(root); return p && p.paused ? p.paused : null; }
|
|
160
|
+
|
|
161
|
+
module.exports = { gates, checkGates, gatesFor, checkGatesFor, gateSentence, senderFor, infoFor, getSwitch, setSwitch, label, compare, choiceProbs, pauseStatus,
|
|
162
|
+
CATS, UNSURE_BELOW, SURE, REJECTED_LIMIT, UNREACHABLE_LIMIT };
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* notes-repo.js — find the repository passage a meeting line is about (meeting-notes Run 2, D91). Local and read-only.
|
|
3
|
+
*
|
|
4
|
+
* passages(root, { exclude }) → [{ path, line, text }] the repository's Markdown paragraphs, cached for a minute
|
|
5
|
+
* closest(root, text, { exclude }) → { path, line, text, shared } | null
|
|
6
|
+
*
|
|
7
|
+
* WHAT IT READS. Every tracked-looking Markdown file under the repository root, skipping dependency, build and hidden
|
|
8
|
+
* folders, at most MAX_FILES files and MAX_FILE_BYTES each. A paragraph is a run of non-blank lines outside frontmatter
|
|
9
|
+
* and code fences, cut to PASSAGE_MAX characters. An excluded path (D79 exclusions) is never read.
|
|
10
|
+
*
|
|
11
|
+
* HOW IT MATCHES. Content words and numbers shared with the line, each weighted by how rare it is across the paragraphs.
|
|
12
|
+
* A passage counts only when it shares at least MIN_SHARED words and scores at least MIN_SCORE, so a line about nothing
|
|
13
|
+
* in particular finds nothing. Numbers count as words: "a 14-day trial" and "21 days" are what a consistency check is for.
|
|
14
|
+
*
|
|
15
|
+
* WHAT IT NEVER DOES. It writes nothing and sends nothing. The Jev check (lib/jev-notes.js) decides what leaves.
|
|
16
|
+
*/
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const settings = require('./typesafe-settings');
|
|
20
|
+
|
|
21
|
+
const MAX_FILES = 3000;
|
|
22
|
+
const MAX_FILE_BYTES = 512 * 1024;
|
|
23
|
+
const PASSAGE_MAX = 1200;
|
|
24
|
+
const MIN_SHARED = 3;
|
|
25
|
+
const MIN_SCORE = 3;
|
|
26
|
+
const CACHE_MS = 60000;
|
|
27
|
+
const SKIP = new Set(['node_modules', '.git', 'vendor', 'dist', 'build', 'out', 'coverage', '.next', 'tmp', 'payload']);
|
|
28
|
+
|
|
29
|
+
const STOP = new Set(('the and for are but not you all any can had her was one our out has have this that with from they will would there their what '
|
|
30
|
+
+ 'about which when make like time just know take into year your some could them than then look only come its over think also back after use '
|
|
31
|
+
+ 'two how work first well way even want because these give most should does did done been being were yeah okay right really thing things '
|
|
32
|
+
+ 'going gonna said say says very much more less here where who why let lets get got need needs maybe sure good great thanks thank '
|
|
33
|
+
+ 'today week next last new old same other each every into onto upon per via yes no').split(/\s+/));
|
|
34
|
+
const NUM_WORDS = { one: '1', two: '2', three: '3', four: '4', five: '5', six: '6', seven: '7', eight: '8', nine: '9', ten: '10', eleven: '11', twelve: '12',
|
|
35
|
+
thirteen: '13', fourteen: '14', fifteen: '15', sixteen: '16', seventeen: '17', eighteen: '18', nineteen: '19', twenty: '20', thirty: '30', forty: '40', fifty: '50' };
|
|
36
|
+
const stem = (w) => {
|
|
37
|
+
const x = w.replace(/'s$/, '');
|
|
38
|
+
if (/(?:ing|ed|es)$/.test(x)) return x.replace(/(?:ing|ed|es)$/, (s) => (x.length - s.length >= 4 ? '' : s));
|
|
39
|
+
return /[^s]s$/.test(x) && x.length >= 4 ? x.slice(0, -1) : x;
|
|
40
|
+
};
|
|
41
|
+
/** Content words and numbers. "twenty-one" is 21; "14-day" is 14 and day. */
|
|
42
|
+
function termsOf(text) {
|
|
43
|
+
const t = String(text || '').toLowerCase().replace(/\b(twenty|thirty|forty|fifty)[- ](one|two|three|four|five|six|seven|eight|nine)\b/g,
|
|
44
|
+
(m, a, b) => String(Number(NUM_WORDS[a]) + Number(NUM_WORDS[b])));
|
|
45
|
+
const out = [];
|
|
46
|
+
for (const raw of t.match(/[\p{L}\p{N}']+/gu) || []) {
|
|
47
|
+
const w = NUM_WORDS[raw] || raw;
|
|
48
|
+
if (/^\d+$/.test(w)) { out.push(w); continue; }
|
|
49
|
+
if (w.length < 3 || STOP.has(w)) continue;
|
|
50
|
+
out.push(stem(w));
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function walk(root, exclude) {
|
|
56
|
+
const out = [];
|
|
57
|
+
const go = (dir) => {
|
|
58
|
+
if (out.length >= MAX_FILES) return;
|
|
59
|
+
let ents = [];
|
|
60
|
+
try { ents = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
61
|
+
for (const e of ents) {
|
|
62
|
+
if (out.length >= MAX_FILES) return;
|
|
63
|
+
if (e.name.startsWith('.') || SKIP.has(e.name)) continue;
|
|
64
|
+
const p = path.join(dir, e.name);
|
|
65
|
+
const rel = path.relative(root, p).split(path.sep).join('/');
|
|
66
|
+
if (settings.isExcluded(exclude, rel)) continue;
|
|
67
|
+
if (e.isDirectory()) go(p);
|
|
68
|
+
else if (e.isFile() && /\.md$/i.test(e.name)) out.push(rel);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
go(root);
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
function paragraphsOf(rel, text) {
|
|
75
|
+
const out = [];
|
|
76
|
+
const lines = String(text).split(/\r?\n/);
|
|
77
|
+
let i = 0;
|
|
78
|
+
if (lines[0] === '---') { i = 1; while (i < lines.length && lines[i] !== '---') i++; i++; }
|
|
79
|
+
let fence = false; let buf = []; let start = 0;
|
|
80
|
+
const end = () => { if (buf.length) { const t = buf.join(' ').replace(/\s+/g, ' ').trim(); if (t.length >= 30) out.push({ path: rel, line: start + 1, text: t.slice(0, PASSAGE_MAX) }); } buf = []; };
|
|
81
|
+
for (; i < lines.length; i++) {
|
|
82
|
+
const l = lines[i];
|
|
83
|
+
if (/^\s*(```|~~~)/.test(l)) { end(); fence = !fence; continue; }
|
|
84
|
+
if (fence) continue;
|
|
85
|
+
if (!l.trim() || /^#{1,6}\s/.test(l) || /^\s*\|?\s*-{3,}/.test(l)) { end(); continue; }
|
|
86
|
+
if (!buf.length) start = i;
|
|
87
|
+
buf.push(l.replace(/^\s*(?:[-*+]|\d+[.)])\s+/, ''));
|
|
88
|
+
}
|
|
89
|
+
end();
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const cache = new Map(); // root → { at, key, items, df }
|
|
94
|
+
function index(root, { exclude = [], now = Date.now() } = {}) {
|
|
95
|
+
const key = JSON.stringify(exclude);
|
|
96
|
+
const hit = cache.get(root);
|
|
97
|
+
if (hit && hit.key === key && now - hit.at < CACHE_MS) return hit;
|
|
98
|
+
const items = [];
|
|
99
|
+
for (const rel of walk(root, exclude)) {
|
|
100
|
+
let text = '';
|
|
101
|
+
try { const st = fs.statSync(path.join(root, rel)); if (st.size > MAX_FILE_BYTES) continue; text = fs.readFileSync(path.join(root, rel), 'utf8'); } catch { continue; }
|
|
102
|
+
for (const p of paragraphsOf(rel, text)) items.push({ ...p, terms: new Set(termsOf(p.text)) });
|
|
103
|
+
}
|
|
104
|
+
const df = new Map();
|
|
105
|
+
for (const it of items) for (const w of it.terms) df.set(w, (df.get(w) || 0) + 1);
|
|
106
|
+
const ix = { at: now, key, items, df };
|
|
107
|
+
cache.set(root, ix);
|
|
108
|
+
return ix;
|
|
109
|
+
}
|
|
110
|
+
function passages(root, opts = {}) { return index(root, opts).items.map(({ path: p, line, text }) => ({ path: p, line, text })); }
|
|
111
|
+
|
|
112
|
+
/** The passage that shares the most rare words with `text`, or null when none shares enough. */
|
|
113
|
+
function closest(root, text, { exclude = [], now = Date.now() } = {}) {
|
|
114
|
+
if (!root) return null;
|
|
115
|
+
const ix = index(root, { exclude, now });
|
|
116
|
+
const q = new Set(termsOf(text));
|
|
117
|
+
if (q.size < MIN_SHARED) return null;
|
|
118
|
+
const n = ix.items.length || 1;
|
|
119
|
+
let best = null;
|
|
120
|
+
for (const it of ix.items) {
|
|
121
|
+
let s = 0; const shared = [];
|
|
122
|
+
// a shared word counts 1, more when it is rare in the repository; a long passage is not favoured for its length
|
|
123
|
+
for (const w of q) if (it.terms.has(w)) { s += 1 + Math.log(n / (ix.df.get(w) || 1)); shared.push(w); }
|
|
124
|
+
if (shared.length < MIN_SHARED || s < MIN_SCORE) continue;
|
|
125
|
+
const rank = s / Math.sqrt(it.terms.size / 20 + 1);
|
|
126
|
+
if (!best || rank > best.rank) best = { path: it.path, line: it.line, text: it.text, shared, score: s, rank };
|
|
127
|
+
}
|
|
128
|
+
if (!best) return null;
|
|
129
|
+
return { path: best.path, line: best.line, text: best.text, shared: best.shared };
|
|
130
|
+
}
|
|
131
|
+
function clearCache() { cache.clear(); }
|
|
132
|
+
|
|
133
|
+
module.exports = { passages, closest, termsOf, paragraphsOf, clearCache, PASSAGE_MAX, MIN_SHARED, MIN_SCORE };
|