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.
@@ -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
- const dirtyLabel = err ? 'not saved — ' + err : (dirty ? '● modified — unsaved' : 'saved on disk');
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
 
@@ -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 deliberately NOT derived here: κ labels
4648
- // are the PM's own word (D40 — never manufacture calibration data from a UI click).
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
- return json(res, { ok: true, recorded: rec, sink, fallback });
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