create-pmos 0.30.1 → 0.31.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/README.md +333 -985
- package/package.json +1 -1
- package/payload/AGENTS.md +52 -149
- package/payload/KIT-VERSION +1 -1
- package/payload/okf/core/concepts/control-plane.md +10 -4
- package/payload/scripts/md-newlines.js +204 -0
- package/payload/scripts/md-normalize.js +223 -0
- package/payload/scripts/workflow-state.js +61 -0
- package/workspace/lib/engine/workflow-state.js +61 -0
- package/workspace/lib/md-normalize.js +223 -0
- package/workspace/package.json +1 -1
- package/workspace/public/index.html +4 -0
- package/workspace/public/pm/tab-doc.js +34 -3
- package/workspace/server.js +27 -3
|
@@ -316,6 +316,7 @@ function diffBody(tab, raw, text, dirty) {
|
|
|
316
316
|
/* ── per-tab state that is DOM, not data ───────────────────────────────────────────────────── */
|
|
317
317
|
|
|
318
318
|
const boxes = new Map(); // tab id -> the persistent textarea for that tab
|
|
319
|
+
const normNote = new Map(); // tab id -> what the last `normalize` click did, in words
|
|
319
320
|
|
|
320
321
|
function editBox(tab) {
|
|
321
322
|
let ta = boxes.get(tab.id);
|
|
@@ -336,6 +337,7 @@ function editBox(tab) {
|
|
|
336
337
|
/** Release what a closed doc tab held. Called by panel-tabs.js when the tab goes away. */
|
|
337
338
|
export function disposeDoc(id) {
|
|
338
339
|
boxes.delete(id);
|
|
340
|
+
normNote.delete(id);
|
|
339
341
|
frames.delete(id);
|
|
340
342
|
scrollers.delete(id);
|
|
341
343
|
}
|
|
@@ -373,15 +375,44 @@ export function mountDoc(host, tab) {
|
|
|
373
375
|
hover: 'border-color:var(--ring)',
|
|
374
376
|
}, 'revert') : null;
|
|
375
377
|
|
|
378
|
+
/* ── normalize ────────────────────────────────────────────────────────────────────────────────
|
|
379
|
+
* The one-click half of markdown-newlines. It writes the normalized text into the BUFFER, never
|
|
380
|
+
* to disk: the pane goes dirty, `diff` shows exactly which lines were joined, and `save` is the
|
|
381
|
+
* existing write path with its existing refusal on a changed base. So a repair the PM did not
|
|
382
|
+
* want is one `revert` away, and no new way to write a file was added to get it.
|
|
383
|
+
*
|
|
384
|
+
* A document that is already normalized reports that and leaves the buffer alone — marking a
|
|
385
|
+
* clean file dirty for nothing is how a tool teaches you to ignore its dirty flag. */
|
|
386
|
+
const norm = typeof window !== 'undefined' ? window.MdNormalize : null;
|
|
387
|
+
const canNormalize = !!(norm && typeof norm.normalize === 'function')
|
|
388
|
+
&& !isHtmlDoc(tab.path) && !isReference(tab.path) && !unread.has(tab.path);
|
|
389
|
+
const normBtn = canNormalize ? el('button', {
|
|
390
|
+
onClick: () => {
|
|
391
|
+
const r = norm.normalize(text);
|
|
392
|
+
normNote.set(tab.id, r.changed
|
|
393
|
+
? r.joins + ' line(s) joined' + (r.blanks ? ', ' + r.blanks + ' blank inserted' : '')
|
|
394
|
+
: 'already normalized — every paragraph is one line');
|
|
395
|
+
if (!r.changed) { setState({}); return; }
|
|
396
|
+
setState((s) => ({ docEdits: { ...s.docEdits, [tab.path]: r.text } }));
|
|
397
|
+
},
|
|
398
|
+
style: 'margin-left:8px;font-family:var(--mono);font-size:10.5px;padding:4px 10px;min-height:26px;background:var(--secondary);border:1px solid var(--border);color:var(--muted-foreground)',
|
|
399
|
+
hover: 'border-color:var(--ring)',
|
|
400
|
+
title: 'Join hard-wrapped paragraphs into one line each — what a Jira issue, a Google Doc or any'
|
|
401
|
+
+ ' plain-text destination needs. Writes the buffer, not the file: read it in diff, then save.',
|
|
402
|
+
}, 'normalize') : null;
|
|
403
|
+
|
|
376
404
|
// The one place a real-mode refusal is reported: the same line that carries the dirty flag, so
|
|
377
|
-
// a save that did not happen cannot read as one that did.
|
|
378
|
-
|
|
405
|
+
// a save that did not happen cannot read as one that did. The normalize note shares the line for
|
|
406
|
+
// the same reason — a repair that changed nothing must say so where the PM is already looking.
|
|
407
|
+
const note = normNote.get(tab.id);
|
|
408
|
+
const dirtyLabel = err ? 'not saved — ' + err
|
|
409
|
+
: (dirty ? '● modified — unsaved' : (note || 'saved on disk'));
|
|
379
410
|
const dirtyColor = err ? 'var(--destructive)' : (dirty ? 'var(--warning)' : 'var(--muted-foreground)');
|
|
380
411
|
|
|
381
412
|
const modeBar = el('div', {
|
|
382
413
|
style: 'flex:none;display:flex;gap:2px;align-items:center;padding:6px 10px;border-bottom:1px solid var(--border);background:var(--background)',
|
|
383
414
|
},
|
|
384
|
-
modeBtns, saveBtn, revertBtn,
|
|
415
|
+
modeBtns, normBtn, saveBtn, revertBtn,
|
|
385
416
|
el('span', { style: 'margin-left:auto;font-family:var(--mono);font-size:10px;color:' + dirtyColor }, iv(dirtyLabel)),
|
|
386
417
|
);
|
|
387
418
|
|
package/workspace/server.js
CHANGED
|
@@ -3760,6 +3760,10 @@ async function handleRequest(req, res) {
|
|
|
3760
3760
|
// /pm/ module graph below, and one front end is the point. A stray <script src="/app.js"> now
|
|
3761
3761
|
// 404s loudly rather than silently resurrecting a second, diverging surface.
|
|
3762
3762
|
if (p === '/lib/preview-kind.js') return serveFile(res, path.join(__dirname, 'lib', 'preview-kind.js'));
|
|
3763
|
+
// The newline normalizer the doc tab's `normalize` action runs, and the same file
|
|
3764
|
+
// scripts/md-newlines.js requires — one implementation, so the button and the gate can never
|
|
3765
|
+
// disagree about what a normalized document is (markdown-newlines).
|
|
3766
|
+
if (p === '/lib/md-normalize.js') return serveFile(res, path.join(__dirname, 'lib', 'md-normalize.js'));
|
|
3763
3767
|
if (p === '/lib/layout.js') return serveFile(res, path.join(__dirname, 'lib', 'layout.js'));
|
|
3764
3768
|
if (p === '/lib/mobile-nav.js') return serveFile(res, path.join(__dirname, 'lib', 'mobile-nav.js'));
|
|
3765
3769
|
if (p === '/lib/session-state.js') return serveFile(res, path.join(__dirname, 'lib', 'session-state.js'));
|
|
@@ -4644,10 +4648,23 @@ async function handleRequest(req, res) {
|
|
|
4644
4648
|
action: body.action, run_id: body.run_id, initiative_id: body.initiative_id,
|
|
4645
4649
|
// `accepted` is the kit's field (D54) — what metrics.py counts for the north star and
|
|
4646
4650
|
// throughput. Only the explicit Acceptance action sets it; `approve` is a Review-gate
|
|
4647
|
-
// disposition and leaves it unset. `pm_label` is
|
|
4648
|
-
//
|
|
4651
|
+
// disposition and leaves it unset. `pm_label` is still never DERIVED here: κ labels are the
|
|
4652
|
+
// PM's own word (D40 — never manufacture calibration data from a UI click).
|
|
4653
|
+
//
|
|
4654
|
+
// But refusing to derive it is not the same as refusing to CARRY it, and the two were
|
|
4655
|
+
// conflated. This endpoint is the PM's real click path under D62/D64, so every acceptance it
|
|
4656
|
+
// wrote was unlabeled — 38 of 42 distinct runs in the live ledger — and κ stayed degenerate
|
|
4657
|
+
// because tn and fp had nowhere to come from. `record-acceptance.js` requires the label and
|
|
4658
|
+
// nothing routes the PM to it. So the label is accepted from the client when the PM states
|
|
4659
|
+
// it, validated against the same closed set, and simply absent when they do not.
|
|
4649
4660
|
accepted: body.action === 'accept' ? true : body.action === 'return' ? false : undefined,
|
|
4661
|
+
...(body.pm_label === 'pass' || body.pm_label === 'fail' ? { pm_label: body.pm_label } : {}),
|
|
4650
4662
|
note: body.note || '', workspace: w ? w.name : null, root: w ? w.root : null, pm: true };
|
|
4663
|
+
// A malformed label is refused rather than dropped. Silently discarding it would put the row in
|
|
4664
|
+
// the unlabeled pile while the PM believed they had labelled it — the same shape as inferring.
|
|
4665
|
+
if (body.pm_label !== undefined && body.pm_label !== 'pass' && body.pm_label !== 'fail') {
|
|
4666
|
+
return json(res, { ok: false, error: "pm_label must be 'pass' or 'fail' — it answers whether the EVALUATOR was right (D40), separately from whether the work goes in (D12)" }, 400);
|
|
4667
|
+
}
|
|
4651
4668
|
// Record into the workspace's OWN state, creating the kit layout for a PMOS-shaped repo.
|
|
4652
4669
|
// If it can't be resolved, still record — but in the server's state/, and say so in the
|
|
4653
4670
|
// response so the client can be honest about where the decision landed.
|
|
@@ -4677,7 +4694,14 @@ async function handleRequest(req, res) {
|
|
|
4677
4694
|
fs.writeFileSync(nf, JSON.stringify(needs.filter((n) => n.run_id !== body.run_id)));
|
|
4678
4695
|
}
|
|
4679
4696
|
} catch (e) { warnWriteFail('needs.json (clearing the accepted need)', e); }
|
|
4680
|
-
|
|
4697
|
+
/* `pm_label_recorded` is reported so the surface can show the gap rather than the PM having to
|
|
4698
|
+
* know it exists. An acceptance with no label is legal and always will be — the PM's attention
|
|
4699
|
+
* is the scarce thing — but it is the reason κ is degenerate, and a gap nobody is shown is a gap
|
|
4700
|
+
* nobody closes. Reported, never enforced. */
|
|
4701
|
+
return json(res, {
|
|
4702
|
+
ok: true, recorded: rec, sink, fallback,
|
|
4703
|
+
pm_label_recorded: rec.pm_label === 'pass' || rec.pm_label === 'fail',
|
|
4704
|
+
});
|
|
4681
4705
|
}
|
|
4682
4706
|
if (p === '/api/doc/save' && req.method === 'POST') {
|
|
4683
4707
|
// The editor's save (workspace-doc-editor R3/R4). The ONLY write path on this surface, and it is
|