create-pmos 0.47.0 → 0.48.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/AGENTS-HUMAN.md +1 -1
- package/payload/AGENTS.md +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/mods/pmos-cockpit/.claude-plugin/plugin.json +1 -1
- package/payload/mods/pmos-cockpit/README.md +4 -2
- package/payload/mods/pmos-cockpit/hooks/lib/commands.mjs +34 -6
- package/payload/mods/pmos-cockpit/hooks/lib/grants.mjs +22 -3
- package/payload/mods/pmos-cockpit/hooks/lib/guard.mjs +16 -3
- package/payload/mods/pmos-cockpit/hooks/register.tsx +37 -18
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/scripts/pmos-log.sh +18 -8
- package/payload/scripts/pmos-model.js +233 -2
- package/payload/scripts/status-facts.js +204 -0
- package/payload/scripts/workflow-state.js +52 -0
- package/payload/state/discoveries.md +1 -1
- package/workspace/README.md +2 -2
- package/workspace/bin/live-notes.js +133 -0
- package/workspace/design-parity.js +328 -21
- package/workspace/doc-category-bench.js +165 -0
- package/workspace/fixtures/doc-category-labelled.json +167 -0
- package/workspace/guide/initiatives-and-next-steps.md +15 -2
- package/workspace/guide/knowledge-graph.md +83 -1
- package/workspace/guide/layout.md +29 -2
- package/workspace/guide/proactive-engine.md +6 -2
- package/workspace/guide/validate-with-typesafe.md +16 -1
- package/workspace/lib/claims.js +51 -2
- package/workspace/lib/doc-category-corrections.js +113 -0
- package/workspace/lib/doc-category-writeback.js +110 -0
- package/workspace/lib/doc-category.js +225 -0
- package/workspace/lib/engine/pmos-model.js +233 -2
- package/workspace/lib/engine/status-facts.js +204 -0
- package/workspace/lib/engine/workflow-state.js +52 -0
- package/workspace/lib/jev-categorize.js +399 -0
- package/workspace/lib/jev-claims.js +59 -15
- package/workspace/lib/jev-route.js +129 -0
- package/workspace/lib/kg-cli.js +3 -1
- package/workspace/lib/kg.js +6 -1
- package/workspace/lib/learn.js +5 -3
- package/workspace/lib/live-notes.js +890 -0
- package/workspace/lib/typesafe-settings.js +10 -2
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +5 -4
- package/workspace/public/pm/app.js +148 -79
- package/workspace/public/pm/data.js +58 -0
- package/workspace/public/pm/fixture.js +40 -2
- package/workspace/public/pm/modes-live.js +216 -18
- package/workspace/public/pm/panel-cockpit.js +636 -0
- package/workspace/public/pm/panel-detail.js +210 -8
- package/workspace/public/pm/panel-initiatives.js +9 -9
- package/workspace/public/pm/panel-newsess.js +145 -86
- package/workspace/public/pm/panel-repos.js +34 -17
- package/workspace/public/pm/panel-tabs.js +145 -27
- package/workspace/public/pm/state.js +45 -4
- package/workspace/public/pm/tab-doc.js +244 -16
- package/workspace/public/pm/tab-term.js +11 -1
- package/workspace/public/pm/tokens.css +4 -3
- package/workspace/public/pm/view-addrepo.js +207 -0
- package/workspace/public/pm/view-files.js +197 -0
- package/workspace/public/pm/view-guide.js +141 -0
- package/workspace/public/pm/view-knowledge.js +225 -0
- package/workspace/public/pm/view-memory.js +122 -0
- package/workspace/public/pm/view-portfolio.js +153 -0
- package/workspace/public/pm/view-reports.js +190 -0
- package/workspace/public/pm/view-scheduled.js +161 -0
- package/workspace/public/pm/view-skills.js +216 -0
- package/workspace/public/pm/view-today.js +168 -0
- package/workspace/public/pm/view-validate.js +171 -0
- package/workspace/server.js +290 -7
- package/workspace/skills/live-notes.skill +58 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/* pm/view-validate.js — the Validate view, drawn from planning/design/cockpit-mockup/ViewValidate.dc.html.
|
|
2
|
+
*
|
|
3
|
+
* PRD workspace-cockpit-nav, Run 3 (R4). The board is a header with two actions on the right, a row of five stat cards, a
|
|
4
|
+
* table of findings and a closing note. The content is the view's existing list (modes-live.js `validateList`): this module
|
|
5
|
+
* draws it into a host and arranges its parts into the board. No node is removed and no text is changed, so every `data-r19`,
|
|
6
|
+
* `data-r22` and `data-r24` hook and every live behaviour stays as it was. Only the frame is new.
|
|
7
|
+
*
|
|
8
|
+
* Where each part comes from:
|
|
9
|
+
* - The two header actions are the list's own buttons, moved: "check now" (`data-r22="check"`, the local consistency check)
|
|
10
|
+
* and "show what would be sent" (`data-r19="plan"`, the first step of a TypeSafe run). Their labels are kept because the
|
|
11
|
+
* verdict line and the guide page name them.
|
|
12
|
+
* - The stat cards count the findings of the last TypeSafe run by rule (`broken-link`, `stale-after-passed`,
|
|
13
|
+
* `workflow-sweep`, `withheld-by-secret-scan`), read off the rows the list drew, so a card and the table always agree.
|
|
14
|
+
* The consent card reads `data-r19="consent-state"`. When no run exists, or the read has not answered, a card says so.
|
|
15
|
+
* - The table is the run's finding rows (`data-r19="finding"`), restyled as a grid. The board's "Status" column has no
|
|
16
|
+
* backing (a finding has no open or closed state), so the table shows the finding's severity instead.
|
|
17
|
+
* - The local consistency check and the TypeSafe settings have no place on the board. They are kept below the table as
|
|
18
|
+
* two cards, unchanged inside.
|
|
19
|
+
* In the fixture the list is empty, and the board says that validation is not read there.
|
|
20
|
+
*/
|
|
21
|
+
import { el } from './dom.js';
|
|
22
|
+
|
|
23
|
+
const BTN = 'font:500 12.5px var(--sans);background:transparent;color:var(--foreground);border:1px solid var(--input);border-radius:8px;padding:0 12px;height:30px;min-height:30px;display:inline-grid;place-items:center;cursor:pointer;flex:none';
|
|
24
|
+
const BTNP = 'font:500 12.5px var(--sans);background:var(--foreground);color:var(--background);border:0;border-radius:8px;padding:0 12px;height:30px;min-height:30px;display:inline-grid;place-items:center;cursor:pointer;flex:none';
|
|
25
|
+
const BADGE = 'font:500 11px var(--sans);border:1px solid var(--border);border-radius:999px;padding:0 8px;line-height:18px;white-space:nowrap;display:inline-block';
|
|
26
|
+
const TONE = {
|
|
27
|
+
s: ';border-color:transparent;background:color-mix(in oklab, var(--success) 16%, transparent);color:var(--success)',
|
|
28
|
+
w: ';border-color:transparent;background:color-mix(in oklab, var(--warning) 16%, transparent);color:var(--warning)',
|
|
29
|
+
d: ';border-color:transparent;background:color-mix(in oklab, var(--destructive) 18%, transparent);color:var(--destructive)',
|
|
30
|
+
};
|
|
31
|
+
const STAT = 'flex:1 1 150px;min-width:0;border:1px solid var(--border);background:var(--background);border-radius:10px;padding:12px 14px';
|
|
32
|
+
const TH = 'font:500 10px var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--muted-foreground);text-align:left';
|
|
33
|
+
const COLS = 'display:grid;grid-template-columns:minmax(0,2.4fr) minmax(0,1fr) 92px minmax(0,1.3fr);gap:0 14px;align-items:center';
|
|
34
|
+
const CARD = 'border:1px solid var(--border);border-radius:10px;background:var(--background);padding:4px 0 8px;overflow:hidden';
|
|
35
|
+
const CARD_HEAD = 'padding:12px 14px 2px;font:600 15px var(--sans);color:var(--foreground)';
|
|
36
|
+
const LBL = 'font:500 10px var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--muted-foreground)';
|
|
37
|
+
const NOTE = 'color:var(--muted-foreground);font-size:13px;line-height:1.5';
|
|
38
|
+
|
|
39
|
+
const badge = (text, tone) => el('span', { style: BADGE + (tone ? TONE[tone] : '') }, String(text));
|
|
40
|
+
|
|
41
|
+
/** The board's stat cards. `kind` is what the card counts; the counts come from the rows the list drew. */
|
|
42
|
+
const STATS = [
|
|
43
|
+
{ key: 'broken', label: 'Broken links', rule: 'broken-link', tone: 'var(--destructive)' },
|
|
44
|
+
{ key: 'stale', label: 'Stale documents', rule: 'stale-after-passed', tone: 'var(--warning)' },
|
|
45
|
+
{ key: 'workflow', label: 'Workflow defects', rule: 'workflow-sweep', tone: 'var(--warning)' },
|
|
46
|
+
{ key: 'withheld', label: 'Withheld (credential shape)', rule: 'withheld-by-secret-scan', tone: null },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
/** What the list says about the last run: 'run' (the coverage line is drawn), 'none' (no run), or 'unread'. */
|
|
50
|
+
function runState(wrap) {
|
|
51
|
+
if (!wrap) return 'unread';
|
|
52
|
+
if (wrap.querySelector('[data-r19="coverage"]')) return 'run';
|
|
53
|
+
if (wrap.querySelector('[data-r19="no-run"]')) return 'none';
|
|
54
|
+
return 'unread';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function stats(wrap) {
|
|
58
|
+
const run = runState(wrap);
|
|
59
|
+
const rows = wrap ? [...wrap.querySelectorAll('[data-r19="finding"]')] : [];
|
|
60
|
+
const cards = STATS.map((s) => {
|
|
61
|
+
const n = rows.filter((r) => r.getAttribute('data-rule') === s.rule).length;
|
|
62
|
+
const value = run === 'run'
|
|
63
|
+
? el('div', { 'data-board-value': String(n), style: 'font:600 24px var(--sans);line-height:1.3;color:' + (n > 0 && s.tone ? s.tone : 'var(--foreground)') }, String(n))
|
|
64
|
+
: el('div', { 'data-board-value': run, style: 'margin-top:6px' }, badge(run === 'none' ? 'no run yet' : 'not read'));
|
|
65
|
+
return el('div', { 'data-board-stat': s.key, style: STAT }, el('div', { style: 'color:var(--muted-foreground)' }, s.label), value);
|
|
66
|
+
});
|
|
67
|
+
const cs = wrap ? wrap.querySelector('[data-r19="consent-state"]') : null;
|
|
68
|
+
const txt = cs ? cs.textContent || '' : '';
|
|
69
|
+
const consent = !cs ? badge('not read') : /\bON\b/.test(txt) ? badge('given', 's') : badge('not given', 'w');
|
|
70
|
+
cards.push(el('div', { 'data-board-stat': 'consent', 'data-board-value': !cs ? 'unread' : /\bON\b/.test(txt) ? 'given' : 'not-given', style: STAT },
|
|
71
|
+
el('div', { style: 'color:var(--muted-foreground)' }, 'Jev consent'), el('div', { style: 'margin-top:6px' }, consent)));
|
|
72
|
+
return el('div', { 'data-board-part': 'stats', style: 'display:flex;gap:14px;flex-wrap:wrap' }, cards);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Split the list's children at its section labels. The labels are fixed text in modes-live.js. */
|
|
76
|
+
function sections(wrap) {
|
|
77
|
+
const kids = [...wrap.children];
|
|
78
|
+
const at = (re) => kids.findIndex((k) => re.test((k.textContent || '').trim()));
|
|
79
|
+
const iT = at(/^typesafe \(jev\)/i);
|
|
80
|
+
const iF = at(/^4 · findings/);
|
|
81
|
+
const end = (i, fallback) => (i < 0 ? fallback : i);
|
|
82
|
+
return {
|
|
83
|
+
consistency: kids.slice(0, end(iT, end(iF, kids.length))),
|
|
84
|
+
typesafe: iT < 0 ? [] : kids.slice(iT, end(iF, kids.length)),
|
|
85
|
+
findings: iF < 0 ? [] : kids.slice(iF),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Restyle one finding row as a table row. Only styles change: the row keeps its nodes, text, hooks and click. */
|
|
90
|
+
function asTableRow(row) {
|
|
91
|
+
row.setAttribute('style', COLS + ';width:100%;box-sizing:border-box;padding:10px 14px;text-align:left;border-bottom:1px solid var(--border);font-size:13px;color:var(--foreground);cursor:pointer;min-height:40px');
|
|
92
|
+
const [head, files] = row.children;
|
|
93
|
+
if (files) files.setAttribute('style', 'order:0;font:12px var(--mono);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--foreground)');
|
|
94
|
+
if (head) {
|
|
95
|
+
head.setAttribute('style', 'display:contents');
|
|
96
|
+
const [sev, rule, meta] = head.children;
|
|
97
|
+
if (rule) rule.setAttribute('style', 'order:1;font-size:13px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap');
|
|
98
|
+
if (sev) {
|
|
99
|
+
const v = (sev.textContent || '').trim();
|
|
100
|
+
const tone = v === 'high' ? TONE.d : v === 'medium' ? TONE.w : '';
|
|
101
|
+
sev.setAttribute('style', 'order:2;justify-self:start;' + BADGE + tone);
|
|
102
|
+
}
|
|
103
|
+
if (meta) meta.setAttribute('style', 'order:3;font:11px var(--mono);color:var(--muted-foreground);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap');
|
|
104
|
+
}
|
|
105
|
+
row.setAttribute('data-board-row', 'finding');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function findingsSection(parts) {
|
|
109
|
+
const [label, ...rest] = parts;
|
|
110
|
+
if (label) label.setAttribute('style', LBL + ';padding:0 2px');
|
|
111
|
+
const rows = rest.filter((n) => n.getAttribute && n.getAttribute('data-r19') === 'finding');
|
|
112
|
+
const notes = rest.filter((n) => !rows.includes(n));
|
|
113
|
+
for (const n of notes) {
|
|
114
|
+
const tone = /color:var\(--destructive\)/.test(n.getAttribute('style') || '') ? ';color:var(--destructive)' : '';
|
|
115
|
+
n.setAttribute('style', NOTE + tone + ';padding:0 2px');
|
|
116
|
+
}
|
|
117
|
+
rows.forEach(asTableRow);
|
|
118
|
+
const table = rows.length
|
|
119
|
+
? el('div', { 'data-board-part': 'table', style: 'border:1px solid var(--border);border-radius:10px;overflow:hidden;background:var(--background)' },
|
|
120
|
+
el('div', { 'data-board-part': 'table-head', style: COLS + ';padding:10px 14px;border-bottom:1px solid var(--border)' },
|
|
121
|
+
el('span', { style: TH }, 'File and finding'), el('span', { style: TH }, 'Rule'), el('span', { style: TH }, 'Severity'), el('span', { style: TH }, 'Checked by')),
|
|
122
|
+
rows)
|
|
123
|
+
: null;
|
|
124
|
+
return el('div', { 'data-board-section': 'findings', style: 'display:flex;flex-direction:column;gap:8px' }, label || null, notes, table);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function card(key, title, parts) {
|
|
128
|
+
if (!parts.length) return null;
|
|
129
|
+
return el('div', { 'data-board-section': key, style: CARD }, el('div', { style: CARD_HEAD }, title), parts);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function renderBoard(inner, ctx) {
|
|
133
|
+
const { FIXTURE, state } = ctx;
|
|
134
|
+
const repo = state.product || null;
|
|
135
|
+
|
|
136
|
+
const toolbar = el('div', { 'data-board-part': 'toolbar', style: 'display:flex;gap:8px;flex-wrap:wrap;align-items:center' });
|
|
137
|
+
const header = el('div', { 'data-board-part': 'header', style: 'display:flex;justify-content:space-between;align-items:flex-end;gap:12px;flex-wrap:wrap' },
|
|
138
|
+
el('div', null,
|
|
139
|
+
el('div', { 'data-board-title': '', style: 'font:600 22px var(--sans);line-height:1.45' }, 'Validate documents'),
|
|
140
|
+
el('div', { 'data-board-subtitle': '', style: 'color:var(--muted-foreground)' },
|
|
141
|
+
'Local checks first. Nothing is sent anywhere until you consent for ' + (repo ? repo : 'this repository') + '.')),
|
|
142
|
+
toolbar);
|
|
143
|
+
|
|
144
|
+
const host = el('div', { 'data-board-section': 'list', style: 'display:flex;flex-direction:column;gap:14px' });
|
|
145
|
+
ctx.renderMode(host);
|
|
146
|
+
const wrap = host.querySelector('[data-r19="validate"]');
|
|
147
|
+
|
|
148
|
+
let statRow = null;
|
|
149
|
+
if (FIXTURE) {
|
|
150
|
+
statRow = el('div', { 'data-board-part': 'stats', style: NOTE }, 'Validation is not read in the fixture.');
|
|
151
|
+
} else if (wrap) {
|
|
152
|
+
statRow = stats(wrap);
|
|
153
|
+
// the two header actions are the list's own buttons, moved, so their hooks and presses stay as they were
|
|
154
|
+
const check = wrap.querySelector('[data-r22="check"]');
|
|
155
|
+
const plan = wrap.querySelector('[data-r19="plan"]');
|
|
156
|
+
if (check) { check.setAttribute('style', BTN); toolbar.appendChild(check); }
|
|
157
|
+
if (plan) { plan.setAttribute('style', BTNP); toolbar.appendChild(plan); }
|
|
158
|
+
const s = sections(wrap);
|
|
159
|
+
wrap.setAttribute('style', 'display:flex;flex-direction:column;gap:14px;padding:0');
|
|
160
|
+
const findings = s.findings.length ? findingsSection(s.findings) : null;
|
|
161
|
+
const foot = el('div', { 'data-board-part': 'note', style: NOTE }, 'Findings never edit a file. You decide what to fix.');
|
|
162
|
+
const cons = card('consistency', 'Consistency of this repository', s.consistency);
|
|
163
|
+
const ts = card('typesafe', 'TypeSafe (Jev)', s.typesafe);
|
|
164
|
+
// the consistency card keeps its own label, which the card head now says
|
|
165
|
+
if (s.consistency[0] && /^consistency of this repository/.test((s.consistency[0].textContent || '').trim())) s.consistency[0].style.display = 'none';
|
|
166
|
+
for (const n of [findings, foot, cons, ts]) if (n) wrap.appendChild(n);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
inner.appendChild(el('div', { 'data-board': 'validate', style: 'display:flex;flex-direction:column;gap:14px;padding:4px 6px;font-size:13px;line-height:1.45;color:var(--foreground)' },
|
|
170
|
+
header, statRow, host));
|
|
171
|
+
}
|
package/workspace/server.js
CHANGED
|
@@ -20,6 +20,12 @@
|
|
|
20
20
|
* under state/ (the reconcile seam). Request headers are never logged (D08).
|
|
21
21
|
*/
|
|
22
22
|
'use strict';
|
|
23
|
+
// The workspace reads git in the background (repo-watch, git-info, claims), and a plain `git status` takes the index lock to
|
|
24
|
+
// refresh the index. A press that writes, such as Sync's `git add`, then fails on `index.lock: File exists` when the two
|
|
25
|
+
// meet. With optional locks off, the server's reads never take that lock. Set only when the PM has not set it, and taken back
|
|
26
|
+
// out of every terminal session's environment (sessionEnv), so the PM's own shells and agents are unchanged.
|
|
27
|
+
const SET_GIT_OPTIONAL_LOCKS = process.env.GIT_OPTIONAL_LOCKS === undefined;
|
|
28
|
+
if (SET_GIT_OPTIONAL_LOCKS) process.env.GIT_OPTIONAL_LOCKS = '0';
|
|
23
29
|
const http = require('http');
|
|
24
30
|
const fs = require('fs');
|
|
25
31
|
const os = require('os');
|
|
@@ -65,9 +71,15 @@ const { createSinkGuard } = require('./lib/sink.js');
|
|
|
65
71
|
const tsSettings = require('./lib/typesafe-settings.js');
|
|
66
72
|
const sessionFlag = require('./lib/session-flag'); // PMOS_WORKSPACE=1 in each agent terminal (cockpit mod, M12)
|
|
67
73
|
const tsClient = require('./lib/typesafe.js');
|
|
74
|
+
const jevRoute = require('./lib/jev-route.js'); // prompt-model-routing — the bridge between prompt routing and the one TypeSafe module
|
|
68
75
|
const jevClaims = require('./lib/jev-claims.js'); // D80 — the bridge between the claims ledger and the one TypeSafe module
|
|
76
|
+
const jevCategorize = require('./lib/jev-categorize.js'); // D82 — the bridge between the document categorizer and the one TypeSafe module
|
|
69
77
|
const validateLib = require('./lib/validate.js');
|
|
78
|
+
const liveNotes = require('./lib/live-notes.js'); // jev-live-notes — reads and appends a repository's live-notes ledger; no network
|
|
70
79
|
const claimsLib = require('./lib/claims.js');
|
|
80
|
+
const docCategory = require('./lib/doc-category.js'); // jev-doc-categorizer Run 1 — the PM-facing kind of a document, derived, local, no model
|
|
81
|
+
const docCatCorrections = require('./lib/doc-category-corrections.js'); // jev-doc-categorizer Run 3 — the PM's own category, per user, outside the repository
|
|
82
|
+
const docCatWriteback = require('./lib/doc-category-writeback.js'); // …and the separate press that writes it into the file's frontmatter
|
|
71
83
|
const repoWatch = require('./lib/repo-watch.js');
|
|
72
84
|
tsSettings.adoptEnv(process.env); // D79 — take the env key into memory and delete it, before anything is spawned
|
|
73
85
|
const scheduleLogic = require('./lib/schedule-logic');
|
|
@@ -278,6 +290,7 @@ function sessionEnv() {
|
|
|
278
290
|
// D79 — the TypeSafe key is the workspace server's own credential. A terminal runs an agent that can
|
|
279
291
|
// print its environment, so the key is never handed to one, whether it came from settings or the shell.
|
|
280
292
|
delete env[tsSettings.ENV_NAME];
|
|
293
|
+
if (SET_GIT_OPTIONAL_LOCKS) delete env.GIT_OPTIONAL_LOCKS;
|
|
281
294
|
return env;
|
|
282
295
|
}
|
|
283
296
|
|
|
@@ -1536,6 +1549,85 @@ function stampEdit(text, at) {
|
|
|
1536
1549
|
const next = fm.replace(re, `generated: {by: human:wawan, at: "${at}"}`);
|
|
1537
1550
|
return next === fm ? text : text.replace(fm, () => next);
|
|
1538
1551
|
}
|
|
1552
|
+
// ── Document category (jev-doc-categorizer Run 1) ───────────────────────────────────────────────────
|
|
1553
|
+
// A derived label beside the document's own `type`: local rules only, no model, nothing written. The library lists it with the
|
|
1554
|
+
// basis that decided it, so the PM can see why. A file is read once per modification; only its first 16 KB is needed, because
|
|
1555
|
+
// the evidence is the frontmatter, the name and the headings.
|
|
1556
|
+
const catCache = new Map();
|
|
1557
|
+
function categoryInput(full) {
|
|
1558
|
+
let st; try { st = fs.statSync(full); } catch { return null; }
|
|
1559
|
+
const hit = catCache.get(full);
|
|
1560
|
+
if (hit && hit.m === st.mtimeMs) return hit;
|
|
1561
|
+
let text = '';
|
|
1562
|
+
try { const fd = fs.openSync(full, 'r'); const buf = Buffer.alloc(16384); const n = fs.readSync(fd, buf, 0, 16384, 0); fs.closeSync(fd); text = buf.slice(0, n).toString('utf8'); } catch { return null; }
|
|
1563
|
+
let fm = {}; try { fm = relatedDocs.frontmatter(text) || {}; } catch { /* no frontmatter */ }
|
|
1564
|
+
const ent = { m: st.mtimeMs, fm, text };
|
|
1565
|
+
catCache.set(full, ent);
|
|
1566
|
+
return ent;
|
|
1567
|
+
}
|
|
1568
|
+
function namedRecords(root) {
|
|
1569
|
+
const records = [];
|
|
1570
|
+
for (const rel of ['planning/initiatives', 'pmos/state/initiatives', 'state/initiatives']) {
|
|
1571
|
+
const d = path.join(root, ...rel.split('/'));
|
|
1572
|
+
let names = []; try { names = fs.readdirSync(d).filter((f) => f.endsWith('.md')); } catch { continue; }
|
|
1573
|
+
for (const f of names) { const c = categoryInput(path.join(d, f)); if (c) records.push({ rel: rel + '/' + f, fm: c.fm }); }
|
|
1574
|
+
}
|
|
1575
|
+
return docCategory.namedFromRecords(records);
|
|
1576
|
+
}
|
|
1577
|
+
// Run 3 (C7) — the PM's corrections for one repository live in the per-user runtime directory, keyed like the claims cache
|
|
1578
|
+
// (sha1 of the root), never in the repository.
|
|
1579
|
+
function doccatDir(root) { return docCatCorrections.repoDir(STATE, root); }
|
|
1580
|
+
// Adds `category`, `category_basis` and `category_why` to every markdown document anywhere in a library answer, and
|
|
1581
|
+
// `category_kind` for a ticket. A stored correction is passed in, so it wins with basis `pm correction`; the bases that
|
|
1582
|
+
// disagree with the winner come as `category_disagrees`.
|
|
1583
|
+
function annotateCategories(root, node, named, corr = {}, seen = new Set()) {
|
|
1584
|
+
if (!node || typeof node !== 'object' || seen.has(node)) return node;
|
|
1585
|
+
seen.add(node);
|
|
1586
|
+
if (Array.isArray(node)) { for (const x of node) annotateCategories(root, x, named, corr, seen); return node; }
|
|
1587
|
+
if (typeof node.path === 'string' && /\.md$/i.test(node.path)) {
|
|
1588
|
+
const c = categoryInput(path.join(root, node.path));
|
|
1589
|
+
if (c) {
|
|
1590
|
+
const rel = node.path.split(path.sep).join('/');
|
|
1591
|
+
const r = docCategory.categorize({ rel, fm: c.fm, text: c.text, named, correction: Object.prototype.hasOwnProperty.call(corr, rel) ? corr[rel] : null });
|
|
1592
|
+
node.category = r.category; node.category_basis = r.basis; node.category_why = r.via;
|
|
1593
|
+
if (r.ticket_kind) node.category_kind = r.ticket_kind;
|
|
1594
|
+
if (r.disagrees.length) node.category_disagrees = r.disagrees;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
for (const k of Object.keys(node)) if (node[k] && typeof node[k] === 'object') annotateCategories(root, node[k], named, corr, seen);
|
|
1598
|
+
return node;
|
|
1599
|
+
}
|
|
1600
|
+
// ── D82 — Jev's category for a file the local rules leave undetermined ─────────────────────────────────
|
|
1601
|
+
// Its state lives beside the corrections, in doccatDir(root): per user, outside the repository.
|
|
1602
|
+
// Reads Jev's CACHED answers only; sends nothing. A decided answer becomes the category with the basis `jev`; an unsure one
|
|
1603
|
+
// leaves the file `undetermined` and says which two options led. Advisory (D40): nothing downstream reads it as fact.
|
|
1604
|
+
function annotateJev(root, node, cache, seen = new Set()) {
|
|
1605
|
+
if (!node || typeof node !== 'object' || seen.has(node)) return node;
|
|
1606
|
+
seen.add(node);
|
|
1607
|
+
if (Array.isArray(node)) { for (const x of node) annotateJev(root, x, cache, seen); return node; }
|
|
1608
|
+
if (node.category === docCategory.UNDETERMINED && typeof node.path === 'string' && /\.md$/i.test(node.path)) {
|
|
1609
|
+
const c = categoryInput(path.join(root, node.path));
|
|
1610
|
+
const l = c ? jevCategorize.lookup(cache, { fm: c.fm, text: c.text }) : null;
|
|
1611
|
+
if (l && l.decided) { node.category = l.category; node.category_basis = 'jev'; node.category_why = l.sentence; if (l.ticket_kind) node.ticket_kind = l.ticket_kind; }
|
|
1612
|
+
else if (l) node.category_why = l.sentence;
|
|
1613
|
+
}
|
|
1614
|
+
for (const k of Object.keys(node)) if (node[k] && typeof node[k] === 'object') annotateJev(root, node[k], cache, seen);
|
|
1615
|
+
return node;
|
|
1616
|
+
}
|
|
1617
|
+
// Every Markdown document the library lists, with what the categorizer needs. Read through the same per-file cache.
|
|
1618
|
+
function doccatDocs(root, lib) {
|
|
1619
|
+
const rels = new Set();
|
|
1620
|
+
(function walk(n, seen) {
|
|
1621
|
+
if (!n || typeof n !== 'object' || seen.has(n)) return; seen.add(n);
|
|
1622
|
+
if (Array.isArray(n)) { for (const x of n) walk(x, seen); return; }
|
|
1623
|
+
if (typeof n.path === 'string' && /\.md$/i.test(n.path)) rels.add(n.path.split(path.sep).join('/'));
|
|
1624
|
+
for (const k of Object.keys(n)) if (n[k] && typeof n[k] === 'object') walk(n[k], seen);
|
|
1625
|
+
})(lib, new Set());
|
|
1626
|
+
const docs = [];
|
|
1627
|
+
const corrections = docCatCorrections.get(doccatDir(root)); // C7 — a corrected file is decided, so it is never sent
|
|
1628
|
+
for (const rel of rels) { const c = categoryInput(path.join(root, ...rel.split('/'))); if (c) docs.push({ rel, fm: c.fm, text: c.text, correction: corrections[rel] || null }); }
|
|
1629
|
+
return docs;
|
|
1630
|
+
}
|
|
1539
1631
|
function library(root) {
|
|
1540
1632
|
const shape = wsShape(root);
|
|
1541
1633
|
const roots = libraryTree(root);
|
|
@@ -1553,7 +1645,8 @@ function library(root) {
|
|
|
1553
1645
|
const notes = NOTE_DIRS.map((d) => path.join(root, ...d.split('/'))).filter((d) => isDir(d))
|
|
1554
1646
|
.map((d) => tree(root, d, {}, 0, { n: TREE_MAX_NODES }))
|
|
1555
1647
|
.filter((t) => t.dirs.length || t.docs.length);
|
|
1556
|
-
|
|
1648
|
+
const out = { shape, roots, initiatives, ungrouped, knowledge, notes, governance: looseDocs(root) };
|
|
1649
|
+
return annotateJev(root, annotateCategories(root, out, namedRecords(root), docCatCorrections.get(doccatDir(root))), jevCategorize.readCache(doccatDir(root)));
|
|
1557
1650
|
}
|
|
1558
1651
|
|
|
1559
1652
|
// ── On-demand surfaces (D60 / the workspace-ondemand-surfaces PRD). ────────────────────────────
|
|
@@ -4594,19 +4687,39 @@ async function runClaims(w, { sweep = false } = {}) {
|
|
|
4594
4687
|
// D80 — after the local check, and only when all four gates hold (the bridge decides), ask Jev about changed claim pairs.
|
|
4595
4688
|
if (out.ok) {
|
|
4596
4689
|
const f = tsSettings.getRepo(STATE, w.root);
|
|
4597
|
-
const jev = await jevClaims.run({ root: w.root, cacheDir, ledgerFile, key: tsSettings.getKey(STATE), flags: f, exclude: f.exclude, changed: out.run ? out.run.changed : null });
|
|
4690
|
+
const jev = await jevClaims.run({ root: w.root, cacheDir, ledgerFile, key: tsSettings.getKey(STATE), flags: f, exclude: f.exclude, changed: out.run ? out.run.changed : null, askOpts: jevModelOpts(w.root) });
|
|
4598
4691
|
const fresh = claimsLib.readView({ cacheDir, ledgerFile });
|
|
4599
4692
|
return { ...out, ...fresh, jev };
|
|
4600
4693
|
}
|
|
4601
4694
|
return out;
|
|
4602
4695
|
} finally { claimsRunning.delete(w.root); }
|
|
4603
4696
|
}
|
|
4697
|
+
// prompt-model-routing R11 — the Jev version a PM names in the model policy reaches the claim checks and keys their cache. None named: the module's own default.
|
|
4698
|
+
function jevModelOpts(root) { const m = jevRoute.modelFromPolicy(root); return m ? { model: m } : {}; }
|
|
4604
4699
|
// D80 — what the page needs to explain the Jev switch: which gates hold, and what the last pass did.
|
|
4605
4700
|
function jevGateInfo(w) {
|
|
4606
4701
|
const f = tsSettings.getRepo(STATE, w.root);
|
|
4607
4702
|
const g = jevClaims.gates({ key: tsSettings.status(STATE).present, consent: f.consent, watch: f.watch, jev: f.jev });
|
|
4608
4703
|
const { cacheDir } = claimsPaths(w);
|
|
4609
|
-
return { switch: f.jev, open: g.open, missing: g.missing, sentence: g.open ? null : jevClaims.gateSentence(g.missing), ...jevClaims.status(cacheDir) };
|
|
4704
|
+
return { switch: f.jev, status_read: f.status_read, open: g.open, missing: g.missing, sentence: g.open ? null : jevClaims.gateSentence(g.missing), ...jevClaims.status(cacheDir) };
|
|
4705
|
+
}
|
|
4706
|
+
// D82 — what the page needs to explain the categorize switch: which gates hold, and what the last pass did.
|
|
4707
|
+
const doccatRunning = new Set();
|
|
4708
|
+
const doccatAutoAt = new Map(); // root → time of the last automatic pass; at most one a minute
|
|
4709
|
+
function doccatInfo(w) {
|
|
4710
|
+
const f = tsSettings.getRepo(STATE, w.root);
|
|
4711
|
+
const g = jevCategorize.gates({ key: tsSettings.status(STATE).present, consent: f.consent, categorize: f.categorize });
|
|
4712
|
+
return { switch: f.categorize, open: g.open, missing: g.missing, sentence: g.open ? null : jevCategorize.gateSentence(g.missing), running: doccatRunning.has(w.root), ...jevCategorize.status(doccatDir(w.root)) };
|
|
4713
|
+
}
|
|
4714
|
+
// The automatic pass, after a library listing. The bridge checks the three gates and the confirmed first run; this only
|
|
4715
|
+
// keeps it to one pass at a time and one a minute, and never makes the listing wait for it.
|
|
4716
|
+
function doccatAuto(w, lib) {
|
|
4717
|
+
const f = tsSettings.getRepo(STATE, w.root);
|
|
4718
|
+
if (!f.categorize || doccatRunning.has(w.root) || Date.now() - (doccatAutoAt.get(w.root) || 0) < 60000) return;
|
|
4719
|
+
doccatAutoAt.set(w.root, Date.now());
|
|
4720
|
+
doccatRunning.add(w.root);
|
|
4721
|
+
jevCategorize.runAuto({ docs: doccatDocs(w.root, lib), named: namedRecords(w.root), cacheDir: doccatDir(w.root), key: tsSettings.getKey(STATE), flags: f, exclude: f.exclude })
|
|
4722
|
+
.catch(() => null).finally(() => doccatRunning.delete(w.root));
|
|
4610
4723
|
}
|
|
4611
4724
|
const claimsLast = new Map(); // root → { at, run, error }
|
|
4612
4725
|
const claimsWatcher = repoWatch.createWatcher({
|
|
@@ -5072,7 +5185,9 @@ async function handleRequest(req, res) {
|
|
|
5072
5185
|
if (p === '/api/library') {
|
|
5073
5186
|
const w = getWs(url.searchParams.get('ws'));
|
|
5074
5187
|
if (!w) return json(res, { ws: null, shape: 'plain', roots: [], initiatives: [], ungrouped: [], knowledge: [], governance: [] });
|
|
5075
|
-
|
|
5188
|
+
const lib = library(w.root);
|
|
5189
|
+
doccatAuto(w, lib); // D82 — in the background, and only when the three gates hold and the first run was confirmed
|
|
5190
|
+
return json(res, { ws: w.id, ...lib, doccat: doccatInfo(w) });
|
|
5076
5191
|
}
|
|
5077
5192
|
if (p === '/api/surface') {
|
|
5078
5193
|
// On-demand P/D/M/R surface, derived through the local state adapter only (no network).
|
|
@@ -5427,6 +5542,36 @@ async function handleRequest(req, res) {
|
|
|
5427
5542
|
let md; try { md = fs.readFileSync(path.join(DELIV_DIR, id + '.md'), 'utf8'); } catch { res.writeHead(404); return res.end('not found'); }
|
|
5428
5543
|
res.writeHead(200, { 'Content-Type': 'text/markdown; charset=utf-8' }); return res.end(md);
|
|
5429
5544
|
}
|
|
5545
|
+
// ── prompt-model-routing — route ANY prompt to a tier. A SUGGESTION: nothing here starts, switches or types into a session. ──
|
|
5546
|
+
if (p === '/api/model/route/switch' && req.method === 'POST') {
|
|
5547
|
+
let body = {}; try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
5548
|
+
const w = getWs(body.ws); if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
|
|
5549
|
+
if (typeof body.route !== 'boolean') return json(res, { ok: false, error: 'route must be true or false' }, 400);
|
|
5550
|
+
const out = tsSettings.setRepo(STATE, w.root, { route: body.route });
|
|
5551
|
+
if (!out.ok) return json(res, { ok: false, error: out.error }, 500);
|
|
5552
|
+
const f = tsSettings.getRepo(STATE, w.root);
|
|
5553
|
+
const g = jevRoute.gates({ key: tsSettings.status(STATE).present, consent: f.consent, route: f.route });
|
|
5554
|
+
return json(res, { ok: true, ws: w.id, route: f.route, open: g.open, missing: g.missing, sentence: g.open ? null : jevRoute.gateSentence(g.missing) });
|
|
5555
|
+
}
|
|
5556
|
+
if (p === '/api/model/route' && req.method === 'POST') {
|
|
5557
|
+
let body = {}; try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
5558
|
+
const w = getWs(body.ws); if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
|
|
5559
|
+
if (typeof body.prompt !== 'string' || !body.prompt.trim() || body.prompt.length > 60000) return json(res, { ok: false, error: 'prompt must be text of up to 60000 characters' }, 400);
|
|
5560
|
+
const f = tsSettings.getRepo(STATE, w.root);
|
|
5561
|
+
const out = await jevRoute.routePrompt({ root: w.root, prompt: body.prompt, ctx: { skill: typeof body.skill === 'string' ? body.skill : undefined, source: body.source, running: true },
|
|
5562
|
+
key: tsSettings.getKey(STATE), flags: { consent: f.consent, route: f.route, exclude: f.exclude } });
|
|
5563
|
+
if (!out.ok) return json(res, { ok: false, error: out.error }, 400);
|
|
5564
|
+
if (out.shadow) return json(res, { ok: true, shadow: true, id: out.id }); // shadow: logged, nothing shown, nothing applied
|
|
5565
|
+
return json(res, { ok: true, ws: w.id, id: out.id, tier: out.tier, model: out.model, effort: out.effort, stage: out.stage, confidence: out.confidence, why: out.why,
|
|
5566
|
+
applies: out.applies, sent: out.sent, withheld: out.withheld ? out.withheld.why : null, gate: out.gate });
|
|
5567
|
+
}
|
|
5568
|
+
if (p === '/api/model/route/outcome' && req.method === 'POST') {
|
|
5569
|
+
let body = {}; try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
5570
|
+
const w = getWs(body.ws); if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
|
|
5571
|
+
const mc = modelLib.core(); if (!mc.ok) return json(res, { ok: false, error: mc.error }, 500);
|
|
5572
|
+
if (!mc.core.attachOutcome(w.root, body.id, { outcome: body.outcome, applied_tier: body.tier })) return json(res, { ok: false, error: 'an id from a routed prompt and an outcome of ' + mc.core.OUTCOMES.join(', ') + ' are needed (and the decisions ledger opted in)' }, 400);
|
|
5573
|
+
return json(res, { ok: true });
|
|
5574
|
+
}
|
|
5430
5575
|
if (p === '/api/session/model' && req.method === 'POST') {
|
|
5431
5576
|
// D75 — switch the model of a RUNNING Claude session, on the PM's press only. It types the harness's own
|
|
5432
5577
|
// `/model <model>` command into the pane; effort is not changed by it. Never automatic, never for a binary
|
|
@@ -5740,7 +5885,12 @@ async function handleRequest(req, res) {
|
|
|
5740
5885
|
try { nextStepActions.annotate(nextSteps, (it) => nextStepPathOf(root, it)); attachRuns(nextSteps, id); } catch (e) { void e; }
|
|
5741
5886
|
let lcHistory = [];
|
|
5742
5887
|
try { lcHistory = lifecycleHistory(w, id); } catch { lcHistory = []; }
|
|
5743
|
-
|
|
5888
|
+
// D83 — what Jev suggests about the documents tied to this initiative, read from the claims ledger. Advisory: the
|
|
5889
|
+
// panel shows it beside the ladder and nothing derives a step, a stage or a count from it.
|
|
5890
|
+
let suggestions = null;
|
|
5891
|
+
try { if (w) suggestions = jevClaims.suggestionsFor({ ledgerFile: claimsPaths(w).ledgerFile, initiativeId: id }); }
|
|
5892
|
+
catch { suggestions = { ok: false, reason: 'the suggestions could not be read', items: [] }; }
|
|
5893
|
+
return json(res, { ws: w ? w.id : null, ...out, lanes, conversations, files, next_steps: nextSteps, lifecycle_history: lcHistory, suggestions });
|
|
5744
5894
|
}
|
|
5745
5895
|
if (p === '/api/audit' && req.method === 'GET') {
|
|
5746
5896
|
/* The workflow audit (D77): the sweep run on one repository, read-only. It reports state that contradicts
|
|
@@ -6231,6 +6381,62 @@ async function handleRequest(req, res) {
|
|
|
6231
6381
|
if (!out.ok) return json(res, { ok: false, error: out.error }, 500);
|
|
6232
6382
|
return json(res, { ok: true, ws: w.id, watching: tsSettings.getRepo(STATE, w.root).watch === true });
|
|
6233
6383
|
}
|
|
6384
|
+
// ── jev-doc-categorizer Run 3 (C7) — the PM corrects a category, and may write it into the file ─────────────────────
|
|
6385
|
+
// A correction is per-user state OUTSIDE the repository and never edits a file. The write-back is a separate press with
|
|
6386
|
+
// two steps — the diff first, then a confirmed write that names the sha1 of the text the PM saw — and it is the ONLY
|
|
6387
|
+
// write into the repository on this surface.
|
|
6388
|
+
if (p === '/api/doccat/corrections' && req.method === 'GET') {
|
|
6389
|
+
const w = getWs(url.searchParams.get('ws'));
|
|
6390
|
+
if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
|
|
6391
|
+
return json(res, { ok: true, ws: w.id, corrections: docCatCorrections.get(doccatDir(w.root)) });
|
|
6392
|
+
}
|
|
6393
|
+
if ((p === '/api/doccat/correct' || p === '/api/doccat/writeback') && req.method === 'POST') {
|
|
6394
|
+
let body = {};
|
|
6395
|
+
try { body = JSON.parse((await readBody(req)) || '{}'); }
|
|
6396
|
+
catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
6397
|
+
const w = getWs(body.ws);
|
|
6398
|
+
if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
|
|
6399
|
+
// Path guard: the store's own check (relative, no `..`, Markdown only), then the same inside-the-workspace rule every
|
|
6400
|
+
// read and write on this surface uses, then the real path, so a symlink cannot lead out of the repository.
|
|
6401
|
+
const chk = docCatCorrections.checkRel(body.path);
|
|
6402
|
+
if (chk.error) return json(res, { ok: false, error: chk.error }, 400);
|
|
6403
|
+
const rel = chk.rel;
|
|
6404
|
+
const full = path.resolve(w.root, ...rel.split('/'));
|
|
6405
|
+
if (!insideRoot(w.root, full)) return json(res, { ok: false, error: 'outside workspace' }, 404);
|
|
6406
|
+
let lst = null; try { lst = fs.lstatSync(full); } catch { /* reported below */ }
|
|
6407
|
+
if (!lst) return json(res, { ok: false, error: 'There is no such document in this repository.' }, 404);
|
|
6408
|
+
let real = null; try { real = fs.realpathSync(full); } catch { /* reported below */ }
|
|
6409
|
+
let realRoot = w.root; try { realRoot = fs.realpathSync(w.root); } catch { /* keep the root as given */ }
|
|
6410
|
+
if (!real || !insideRoot(realRoot, real)) return json(res, { ok: false, error: 'outside workspace' }, 404);
|
|
6411
|
+
const dir = doccatDir(w.root);
|
|
6412
|
+
if (p === '/api/doccat/correct') {
|
|
6413
|
+
const out = body.clear === true
|
|
6414
|
+
? docCatCorrections.clear(dir, rel)
|
|
6415
|
+
: docCatCorrections.set(dir, rel, { category: body.category, ticket_kind: body.ticket_kind === undefined ? null : body.ticket_kind });
|
|
6416
|
+
if (!out.ok) return json(res, { ok: false, error: out.error }, /could not be saved|symlink/.test(out.error) ? 500 : 400);
|
|
6417
|
+
return json(res, { ok: true, ws: w.id, path: rel, cleared: body.clear === true ? out.cleared : undefined, correction: out.correction || null, corrections: docCatCorrections.get(dir) });
|
|
6418
|
+
}
|
|
6419
|
+
// the write-back
|
|
6420
|
+
if (lst.isSymbolicLink() || !lst.isFile()) return json(res, { ok: false, error: 'This document is a link or not a plain file, so the category was not written into it.' }, 400);
|
|
6421
|
+
let current;
|
|
6422
|
+
try { current = fs.readFileSync(full, 'utf8'); }
|
|
6423
|
+
catch { return json(res, { ok: false, error: 'could not read the document' }, 404); }
|
|
6424
|
+
const corr = { category: body.category, ticket_kind: body.ticket_kind === undefined ? null : body.ticket_kind };
|
|
6425
|
+
if (body.confirm !== true) {
|
|
6426
|
+
const plan = docCatWriteback.plan(current, corr);
|
|
6427
|
+
if (!plan.ok) return json(res, { ok: false, error: plan.error, sha: plan.sha }, 400);
|
|
6428
|
+
return json(res, { ok: true, ws: w.id, path: rel, written: false, diff: plan.diff, changes: plan.changes, sha: plan.sha, next_sha: plan.next_sha, unchanged: plan.unchanged });
|
|
6429
|
+
}
|
|
6430
|
+
const done = docCatWriteback.apply(current, corr, body.expect_sha, body.expect_next_sha);
|
|
6431
|
+
if (!done.ok) return json(res, { ok: false, error: done.error, stale: !!done.stale, sha: done.sha }, done.stale ? 409 : 400);
|
|
6432
|
+
if (done.unchanged) return json(res, { ok: true, ws: w.id, path: rel, written: false, unchanged: true, diff: '' });
|
|
6433
|
+
try { fs.writeFileSync(full, done.next); }
|
|
6434
|
+
catch (e) {
|
|
6435
|
+
warnWriteFail('category write-back (' + rel + ')', e);
|
|
6436
|
+
return json(res, { ok: false, error: 'could not write the document — nothing was changed' }, 500);
|
|
6437
|
+
}
|
|
6438
|
+
return json(res, { ok: true, ws: w.id, path: rel, written: true, diff: done.diff, changes: done.changes, sha: docCatWriteback.sha1(done.next) });
|
|
6439
|
+
}
|
|
6234
6440
|
// ── D80 — Jev on changed claim pairs: the switch, a resume after a pause, and the backlog (plan, then confirmed run) ──
|
|
6235
6441
|
if (p === '/api/claims/jev' && req.method === 'POST') {
|
|
6236
6442
|
let body = {};
|
|
@@ -6243,6 +6449,18 @@ async function handleRequest(req, res) {
|
|
|
6243
6449
|
if (!out.ok) return json(res, { ok: false, error: out.error }, 500);
|
|
6244
6450
|
return json(res, { ok: true, ws: w.id, jev: jevGateInfo(w) });
|
|
6245
6451
|
}
|
|
6452
|
+
// D83 — the second switch: also read the documents tied to each initiative for the two status questions. Default off.
|
|
6453
|
+
if (p === '/api/claims/jev/status' && req.method === 'POST') {
|
|
6454
|
+
let body = {};
|
|
6455
|
+
try { body = JSON.parse((await readBody(req)) || '{}'); }
|
|
6456
|
+
catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
6457
|
+
const w = getWs(body.ws);
|
|
6458
|
+
if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
|
|
6459
|
+
if (typeof body.status_read !== 'boolean') return json(res, { ok: false, error: 'status_read must be true or false' }, 400);
|
|
6460
|
+
const out = tsSettings.setRepo(STATE, w.root, { status_read: body.status_read });
|
|
6461
|
+
if (!out.ok) return json(res, { ok: false, error: out.error }, 500);
|
|
6462
|
+
return json(res, { ok: true, ws: w.id, jev: jevGateInfo(w) });
|
|
6463
|
+
}
|
|
6246
6464
|
if (p === '/api/claims/jev/resume' && req.method === 'POST') {
|
|
6247
6465
|
let body = {};
|
|
6248
6466
|
try { body = JSON.parse((await readBody(req)) || '{}'); }
|
|
@@ -6262,14 +6480,61 @@ async function handleRequest(req, res) {
|
|
|
6262
6480
|
const g = jevClaims.gates({ key: tsSettings.status(STATE).present, consent: f.consent, watch: true, jev: f.jev });
|
|
6263
6481
|
if (!g.open) return json(res, { ok: false, error: jevClaims.gateSentence(g.missing) }, 409);
|
|
6264
6482
|
const { cacheDir, ledgerFile } = claimsPaths(w);
|
|
6265
|
-
if (body.confirm !== true) return json(res, { ok: true, ws: w.id, plan: jevClaims.backlogPlan({ root: w.root, cacheDir, exclude: f.exclude }) });
|
|
6483
|
+
if (body.confirm !== true) return json(res, { ok: true, ws: w.id, plan: jevClaims.backlogPlan({ root: w.root, cacheDir, exclude: f.exclude, model: jevRoute.modelFromPolicy(w.root) || undefined, flags: f }) });
|
|
6266
6484
|
if (claimsRunning.has(w.root)) return json(res, { ok: false, busy: true, error: 'a check is already running for this repository' }, 409);
|
|
6267
6485
|
claimsRunning.add(w.root);
|
|
6268
6486
|
try {
|
|
6269
|
-
const out = await jevClaims.runBacklog({ root: w.root, cacheDir, ledgerFile, key: tsSettings.getKey(STATE), flags: f, exclude: f.exclude, expectRequests: body.expect_requests });
|
|
6487
|
+
const out = await jevClaims.runBacklog({ root: w.root, cacheDir, ledgerFile, key: tsSettings.getKey(STATE), flags: f, exclude: f.exclude, expectRequests: body.expect_requests, askOpts: jevModelOpts(w.root) });
|
|
6270
6488
|
return json(res, { ws: w.id, ...out, jev: jevGateInfo(w), ...claimsLib.readView({ cacheDir, ledgerFile }) }, out.ok ? 200 : (out.conflict ? 409 : 500));
|
|
6271
6489
|
} finally { claimsRunning.delete(w.root); }
|
|
6272
6490
|
}
|
|
6491
|
+
// ── D82 — Jev on documents the local rules leave undetermined: the switch, a resume, and the run (plan, then confirmed) ──
|
|
6492
|
+
if (p === '/api/doccat/jev' && req.method === 'GET') {
|
|
6493
|
+
const w = getWs(url.searchParams.get('ws'));
|
|
6494
|
+
if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
|
|
6495
|
+
return json(res, { ok: true, ws: w.id, doccat: doccatInfo(w) });
|
|
6496
|
+
}
|
|
6497
|
+
if (p === '/api/doccat/jev' && req.method === 'POST') {
|
|
6498
|
+
let body = {};
|
|
6499
|
+
try { body = JSON.parse((await readBody(req)) || '{}'); }
|
|
6500
|
+
catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
6501
|
+
const w = getWs(body.ws);
|
|
6502
|
+
if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
|
|
6503
|
+
if (typeof body.categorize !== 'boolean') return json(res, { ok: false, error: 'categorize must be true or false' }, 400);
|
|
6504
|
+
const out = tsSettings.setRepo(STATE, w.root, { categorize: body.categorize });
|
|
6505
|
+
if (!out.ok) return json(res, { ok: false, error: out.error }, 500);
|
|
6506
|
+
return json(res, { ok: true, ws: w.id, doccat: doccatInfo(w) });
|
|
6507
|
+
}
|
|
6508
|
+
if (p === '/api/doccat/jev/resume' && req.method === 'POST') {
|
|
6509
|
+
let body = {};
|
|
6510
|
+
try { body = JSON.parse((await readBody(req)) || '{}'); }
|
|
6511
|
+
catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
6512
|
+
const w = getWs(body.ws);
|
|
6513
|
+
if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
|
|
6514
|
+
jevCategorize.resume(doccatDir(w.root));
|
|
6515
|
+
return json(res, { ok: true, ws: w.id, doccat: doccatInfo(w) });
|
|
6516
|
+
}
|
|
6517
|
+
if (p === '/api/doccat/jev/run' && req.method === 'POST') {
|
|
6518
|
+
let body = {};
|
|
6519
|
+
try { body = JSON.parse((await readBody(req)) || '{}'); }
|
|
6520
|
+
catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
6521
|
+
const w = getWs(body.ws);
|
|
6522
|
+
if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
|
|
6523
|
+
const f = tsSettings.getRepo(STATE, w.root);
|
|
6524
|
+
const g = jevCategorize.gates({ key: tsSettings.status(STATE).present, consent: f.consent, categorize: f.categorize });
|
|
6525
|
+
if (!g.open) return json(res, { ok: false, error: jevCategorize.gateSentence(g.missing), doccat: doccatInfo(w) }, 409);
|
|
6526
|
+
const cacheDir = doccatDir(w.root);
|
|
6527
|
+
const docs = doccatDocs(w.root, library(w.root));
|
|
6528
|
+
const named = namedRecords(w.root);
|
|
6529
|
+
if (body.confirm !== true) return json(res, { ok: true, ws: w.id, plan: jevCategorize.plan({ docs, named, cacheDir, exclude: f.exclude }) });
|
|
6530
|
+
if (doccatRunning.has(w.root)) return json(res, { ok: false, busy: true, error: 'Jev is already categorizing this repository' }, 409);
|
|
6531
|
+
doccatRunning.add(w.root);
|
|
6532
|
+
try {
|
|
6533
|
+
const out = await jevCategorize.runConfirmed({ docs, named, cacheDir, key: tsSettings.getKey(STATE), flags: f, exclude: f.exclude, expectRequests: body.expect_requests,
|
|
6534
|
+
expectFingerprint: typeof body.expect_fingerprint === 'string' ? body.expect_fingerprint : null });
|
|
6535
|
+
return json(res, { ws: w.id, ...out, doccat: doccatInfo(w) }, out.ok ? 200 : (out.conflict || out.busy ? 409 : 500));
|
|
6536
|
+
} finally { doccatRunning.delete(w.root); }
|
|
6537
|
+
}
|
|
6273
6538
|
// ── D79 — repository validation through TypeSafe ──────────────────────────────────────────────────
|
|
6274
6539
|
// The ONLY routes that lead to an outbound request, and only through lib/typesafe.js. The key is never
|
|
6275
6540
|
// in a response: `status` says present or absent. Nothing here edits a repository file.
|
|
@@ -6300,6 +6565,24 @@ async function handleRequest(req, res) {
|
|
|
6300
6565
|
}
|
|
6301
6566
|
return json(res, { ok: true, ws: w.id, name: w.name, ...tsSettings.getRepo(STATE, w.root), key: tsSettings.status(STATE) });
|
|
6302
6567
|
}
|
|
6568
|
+
/* ── live notes (jev-live-notes, Run 2): the ranked proposals of a conversation, and the PM's accept, change or reject.
|
|
6569
|
+
* Reads the repository's ledger; the one write is a decision row. No request leaves this process, and no page is built yet. ── */
|
|
6570
|
+
if (p === '/api/live-notes' && req.method === 'GET') {
|
|
6571
|
+
const wsq = url.searchParams.get('ws');
|
|
6572
|
+
const w = wsq ? workspaces.get(wsq) : homeWorkspace(); // a named workspace the server does not know is refused, never replaced by the home one
|
|
6573
|
+
if (!w) return json(res, { ok: false, error: 'no such workspace' }, 400);
|
|
6574
|
+
const r = liveNotes.apiList({ root: w.root, session: url.searchParams.get('session') || null });
|
|
6575
|
+
return json(res, r, r.ok ? 200 : (r.status || 400));
|
|
6576
|
+
}
|
|
6577
|
+
if (p === '/api/live-notes/decide' && req.method === 'POST') {
|
|
6578
|
+
let body = {};
|
|
6579
|
+
try { body = JSON.parse((await readBody(req)) || '{}'); }
|
|
6580
|
+
catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
6581
|
+
const w = body.ws ? workspaces.get(String(body.ws)) : homeWorkspace();
|
|
6582
|
+
if (!w) return json(res, { ok: false, error: 'no such workspace' }, 400);
|
|
6583
|
+
const r = liveNotes.apiDecide({ root: w.root, runtimeDir: STATE, session: body.session, segment: body.segment, action: body.action, initiative: body.initiative || null, relation: body.relation || null, exclude: tsSettings.getRepo(STATE, w.root).exclude });
|
|
6584
|
+
return json(res, r, r.ok ? 200 : (r.status || 400));
|
|
6585
|
+
}
|
|
6303
6586
|
if (p === '/api/validate/plan' && req.method === 'POST') {
|
|
6304
6587
|
let body = {};
|
|
6305
6588
|
try { body = JSON.parse((await readBody(req)) || '{}'); }
|