create-pmos 0.22.0 → 0.24.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.
@@ -465,6 +465,29 @@ export function branchOfWorkspace(id) {
465
465
  return (id && wsBranch.get(id)) || null;
466
466
  }
467
467
 
468
+ /**
469
+ * The same map as a plain object, for `setState`.
470
+ *
471
+ * `branchOfWorkspace` answers "what branch is THIS workspace on?", which is all the + session
472
+ * chooser ever needed. The initiative board asks the other direction — "which initiatives does this
473
+ * machine have a checkout for, and which of my live sessions is on one?" — and a lookup by id
474
+ * cannot be enumerated.
475
+ *
476
+ * It is PUBLISHED rather than exported as a getter for the reason `productWs` is: a panel that has
477
+ * to import the data layer to render a row cannot render that row until the import resolves, and
478
+ * panel 2 is the board. Published, the fact arrives in the same patch as everything else the panel
479
+ * reads, and the panel needs no adapter at all.
480
+ *
481
+ * `wsBranch` stays the single source — this is a projection of it taken at publish time, not a
482
+ * second copy anything writes to. Nulls are kept out: a folder with no git has no branch, and a map
483
+ * carrying holes would make every reader re-check the same thing.
484
+ */
485
+ function branchMap() {
486
+ const out = {};
487
+ for (const [id, b] of wsBranch) if (typeof b === 'string' && b) out[id] = b;
488
+ return out;
489
+ }
490
+
468
491
  /** Group registered working trees into products: several worktrees of one repo share a `repo_key`. */
469
492
  function foldWorkspaces(rows) {
470
493
  const byRepo = new Map();
@@ -595,7 +618,11 @@ function publishBoards() {
595
618
  board[name] = p.board;
596
619
  records = { ...records, ...p.records };
597
620
  }
598
- setState({ board, productWs, records: { ...records, ...(hydrated) } });
621
+ // `wsBranches` rides the same patch as `productWs` and for the same reason: it is a fact about
622
+ // every registered working tree, read once from /api/workspaces, that a panel needs in order to
623
+ // draw a row. panel 2 joins it against `state.sessions` to say which initiative each live agent is
624
+ // on — the same `initiative/<id>` join lib/four-panel.js makes per initiative on the server.
625
+ setState({ board, productWs, wsBranches: branchMap(), records: { ...records, ...(hydrated) } });
599
626
  }
600
627
 
601
628
  /* Hydrated (fully derived) records live apart from the light ones so that a board reload cannot
@@ -25,6 +25,33 @@
25
25
  * overrides the data layer may publish into state — `state.board` ({repoId: group[]}) and
26
26
  * `state.records` ({slug: record}). Nothing here caches anything derivable (D33): groups, counts,
27
27
  * search hits and the selected row are computed at read time, every render.
28
+ *
29
+ * ── What the board says about YOUR work (workspace-active-visibility) ────────────────────────
30
+ * On 178 records the filing order is the right filing order and the wrong ATTENTION order, and the
31
+ * PM could not tell which initiatives they were working on or which had a live agent. Two additions,
32
+ * both inside the elements that were already here:
33
+ *
34
+ * 1. THE ROW'S LEADING SLOT. It held a constant `#` — the Slack channel glyph, ported for its
35
+ * look, carrying nothing on every row. On an initiative with a live session it now holds that
36
+ * session's glyph, in that session's colour, with every session named in the tooltip. Same
37
+ * span, same node count, same geometry. The TRAILING dot is untouched: it is the ladder's, and
38
+ * `lightRecord` cannot derive 178 of those without ~30s of `git log`.
39
+ *
40
+ * 2. A DERIVED GROUP ABOVE THE OBJECTIVES. Built from `groupNode`, the same element, with the same
41
+ * collapse key — derived groups are not new here (`unanchored` and `no record` are both
42
+ * constructed in data.js). It lists what a live session, a checked-out working tree, or an open
43
+ * tab says the PM is on, ordered so the initiative WAITING on them is the first row.
44
+ *
45
+ * Three rules hold it honest. Rows are DUPLICATED, never moved — an initiative keeps its place under
46
+ * its own anchor, because the anchor is the truth (D59) and a shortcut that re-homes work is a second
47
+ * filing system. The group is ABSENT, not empty, when nothing is live. And the join is
48
+ * lib/working-set.js's, which is the server's own `initiative/<id>` two-hopper — so a session the
49
+ * join cannot reach contributes nothing rather than being bucketed somewhere plausible.
50
+ *
51
+ * Fixture mode is structurally unaffected, not conditionally suppressed: `pollSessions` returns early
52
+ * on FIXTURE and `foldWorkspaces` never runs, so `state.sessions` is empty and there are no branches.
53
+ * There is no server in fixture mode, so there are no live sessions to claim — the same rule line 45
54
+ * states in the other direction.
28
55
  */
29
56
 
30
57
  import { el, setChildren } from './dom.js';
@@ -103,6 +130,75 @@ function sessionsOf(slug, DATA) {
103
130
  return [...(i.sessions || []), ...((state.extraSess && state.extraSess[slug]) || [])];
104
131
  }
105
132
 
133
+ /* ── the working set: which initiatives are yours, and which of them are live ─────────────────── */
134
+
135
+ /** The shared modules, off the globals the way panel-tabs.js reads window.PMOSTreeShare. */
136
+ const WS = () => (typeof window !== 'undefined' ? window.PMOSWorkingSet : null);
137
+ const SS = () => (typeof window !== 'undefined' ? window.PMOSSessionState : null);
138
+
139
+ /** workspace id → branch, published by the data layer alongside `board` and `productWs`. */
140
+ const branches = () => (state.wsBranches && typeof state.wsBranches === 'object' ? state.wsBranches : {});
141
+
142
+ /**
143
+ * Every LIVE session, keyed by the initiative its working tree is on: `{slug: [session…]}`.
144
+ *
145
+ * Fixture mode returns nothing and SAYS so rather than relying on the inputs happening to be empty.
146
+ * `pollSessions` does return early on FIXTURE — but a marker drawn from seed data is "the beautiful
147
+ * shell" in the PRD's own failure-mode list, and that is not a property to leave resting on a
148
+ * `return` two modules away.
149
+ */
150
+ function liveSessions() {
151
+ if (FIXTURE) return {};
152
+ const ws = WS();
153
+ if (!ws) return {};
154
+ const map = branches();
155
+ return ws.sessionsByInitiative(state.sessions, (id) => map[id] || null);
156
+ }
157
+
158
+ /**
159
+ * The rows the derived group shows, in attention order.
160
+ *
161
+ * `onBoard` is what stops a tab or a working tree belonging to ANOTHER repository putting a row on
162
+ * this repository's board — `slugProduct()` is the same map panel 4 scopes its tabs by.
163
+ */
164
+ function workingSet(live, product) {
165
+ if (FIXTURE) return [];
166
+ const ws = WS();
167
+ if (!ws) return [];
168
+ const owner = slugProduct();
169
+ return ws.workingSet({
170
+ live,
171
+ trees: ws.initiativesWithTree(Object.values(branches())),
172
+ tabs: ws.initiativesWithTab(state.tabs),
173
+ onBoard: (slug) => owner[slug] === product,
174
+ });
175
+ }
176
+
177
+ /**
178
+ * The row's leading slot, for one initiative: `['#', colour, title]` when nothing is live, and the
179
+ * session's own glyph/colour/tooltip when something is.
180
+ *
181
+ * The vocabulary is `window.PMOSSessionState`'s — the module index.html loads for exactly this
182
+ * reason ("ONE table, TWO consumers"). Not `FX.SDOT`, which is the design prototype's seed data,
183
+ * parity-locked to prototype-original.html, and which gives `working` and `needs` the same ● and
184
+ * separates them by hue alone. On a board the PM scans, a signal that dies in greyscale is not a
185
+ * signal.
186
+ */
187
+ const HASH = ['#', 'var(--muted-foreground)', ''];
188
+
189
+ function leadOf(slug, live) {
190
+ const rows = live[slug];
191
+ if (!rows || !rows.length) return HASH;
192
+ const ss = SS();
193
+ const ws = WS();
194
+ if (!ss || !ws) return HASH;
195
+ const st = ws.stateOf(rows);
196
+ // A state the table does not know is NOT drawn. Falling back to a generic dot here would put a
197
+ // mark on the board that no vocabulary can explain, which is the one thing worse than no mark.
198
+ if (!st || !ss.GLYPH[st]) return HASH;
199
+ return [ss.GLYPH[st], ss.COLOR[st] || 'var(--muted-foreground)', ws.sessionTitle(slug, rows)];
200
+ }
201
+
106
202
  /** Which repository owns each slug (lines 1963–1965) — the tab-scoping key. */
107
203
  function slugProduct() {
108
204
  const map = {};
@@ -221,8 +317,14 @@ function groupNode(g, product) {
221
317
  onClick: () => setState({ sel: it.slug, detail: { kind: 'init' }, col2: true }),
222
318
  style: 'display:flex;align-items:center;gap:7px;width:100%;padding:5px 12px 5px 27px;min-height:32px;border-left:2px solid ' + it.bl + ';background:' + it.bg,
223
319
  hover: 'background:var(--accent)',
320
+ // Stated where the PM is standing, not only in a tooltip on the glyph: on a working-set row
321
+ // this names WHICH of the three signals put it there. The glyph summarises; this does not.
322
+ title: it.why || null,
224
323
  },
225
- el('span', { style: 'flex:none;font-size:12px;color:var(--muted-foreground)' }, '#'),
324
+ // ① The slot that used to be a constant. `#` when nothing is live — byte-identical to what
325
+ // shipped, which is what keeps the comparator's text axis clean — and the session's own
326
+ // glyph when something is. One span either way: no node added, no geometry moved.
327
+ el('span', { style: 'flex:none;font-size:12px;color:' + it.leadC, title: it.leadT || null }, it.lead),
226
328
  el('span', { style: 'font-family:var(--mono);font-size:12.5px;color:var(--foreground);overflow:hidden;text-overflow:ellipsis;white-space:nowrap' }, iv(it.slug)),
227
329
  el('span', { style: 'margin-left:auto;flex:none;font-size:9px;color:' + it.l2c, title: it.l2b }, '●'),
228
330
  ));
@@ -238,22 +340,44 @@ function groupNode(g, product) {
238
340
 
239
341
  /* ── "matches elsewhere" — artifacts and sessions that the group list cannot show ──────────── */
240
342
 
241
- function searchHits(q, DATA) {
343
+ /**
344
+ * `searchHits` used to read sessions off `record.sessions` ONLY — and board records are LIGHT, with
345
+ * `sessions: []`, until the PM opens one. So a search for a session's name returned nothing for 177
346
+ * of 178 initiatives, silently, with no indication that the search had not looked. The hit was
347
+ * scoped to hydrated records and nothing said so.
348
+ *
349
+ * `live` closes it: the same join the row markers use, over every session the server has published,
350
+ * regardless of what has been hydrated. The old path is KEPT rather than replaced — it also carries
351
+ * `state.extraSess`, the sessions this window started, which have no server row until the next poll.
352
+ * `seen` is what stops one session arriving twice when both paths find it.
353
+ */
354
+ function searchHits(q, DATA, live) {
242
355
  const hits = [];
243
356
  if (!q) return hits;
357
+ const seen = new Set();
358
+ const push = (slug, s) => {
359
+ const key = slug + '|' + (s.id || s.name);
360
+ if (seen.has(key)) return;
361
+ seen.add(key);
362
+ hits.push({
363
+ kind: 'session · ' + slug,
364
+ label: s.name + ' · ' + (s.label || s.shown || s.st || ''),
365
+ on: () => { setState({ sel: slug, detail: { kind: 'init' } }); openTerm(slug, s); },
366
+ });
367
+ };
244
368
  Object.keys(DATA).forEach((slug) => {
245
369
  (DATA[slug].arts || []).forEach((a) => {
246
370
  if (a.path.toLowerCase().includes(q)) {
247
371
  hits.push({ kind: 'artifact · ' + a.role, label: a.path, on: () => openDoc(slug, a.path, a.role) });
248
372
  }
249
373
  });
250
- sessionsOf(slug, DATA).forEach((s) => {
251
- if (s.name.toLowerCase().includes(q)) {
252
- hits.push({
253
- kind: 'session · ' + slug, label: s.name + ' · ' + s.label,
254
- on: () => { setState({ sel: slug, detail: { kind: 'init' } }); openTerm(slug, s); },
255
- });
256
- }
374
+ sessionsOf(slug, DATA).forEach((s) => { if (s.name.toLowerCase().includes(q)) push(slug, s); });
375
+ });
376
+ Object.keys(live || {}).forEach((slug) => {
377
+ (live[slug] || []).forEach((s) => {
378
+ // The server's row calls the presented state `shown`; a record's row calls it `st`. `openTerm`
379
+ // reads `st`, so the shape is normalised HERE rather than teaching the opener a second one.
380
+ if (s.name && s.name.toLowerCase().includes(q)) push(slug, { ...s, st: s.shown });
257
381
  });
258
382
  });
259
383
  return hits.slice(0, 8);
@@ -398,23 +522,54 @@ export function renderInitiatives(host) {
398
522
  const DATA = records();
399
523
  const selSlug = selectedSlug();
400
524
  const detKind = (state.detail && state.detail.kind) || '';
525
+ // ONE join per render, shared by the row markers, the working-set group and the search. Computing
526
+ // it three times would be three chances to disagree about one fact.
527
+ const live = liveSessions();
528
+
529
+ /** One row's view model. `why` is null everywhere except in the working-set group. */
530
+ const rowOf = (slug, why) => {
531
+ const i = DATA[slug];
532
+ // The prototype reads `i.l2` unguarded; a real repository can hand back a slug with no
533
+ // record at all, and a board that throws is worse than a row that reads "no record".
534
+ const l2 = (i && i.l2) || ['', '', FX.C.mut];
535
+ const [lead, leadC, leadT] = leadOf(slug, live);
536
+ return {
537
+ slug, ...selRow(slug === selSlug && detKind === 'init'),
538
+ l2a: l2[0], l2b: l2[1], l2c: l2[2], lead, leadC, leadT, why: why || null,
539
+ };
540
+ };
401
541
 
402
542
  let total = 0;
403
543
  const groups = groupsFor(product).map((g) => {
404
- const inits = g.slugs.filter((s) => !q || s.includes(q)).map((slug) => {
405
- const i = DATA[slug];
406
- // The prototype reads `i.l2` unguarded; a real repository can hand back a slug with no
407
- // record at all, and a board that throws is worse than a row that reads "no record".
408
- const l2 = (i && i.l2) || ['', '', FX.C.mut];
409
- return { slug, ...selRow(slug === selSlug && detKind === 'init'), l2a: l2[0], l2b: l2[1], l2c: l2[2] };
410
- });
544
+ const inits = g.slugs.filter((s) => !q || s.includes(q)).map((slug) => rowOf(slug, null));
411
545
  total += inits.length;
412
546
  return { ...g, inits, isEmpty: inits.length === 0 };
413
547
  }).filter((g) => g.inits.length > 0 || (g.dyn && !q));
414
548
 
415
- const hits = searchHits(q, DATA);
549
+ const hits = searchHits(q, DATA, live);
416
550
 
417
551
  const kids = [header, searchBox];
552
+ /* THE WORKING SET, pinned above the Objectives.
553
+ *
554
+ * It is `groupNode` — the same element, the same collapse key, the same forced-open-on-search
555
+ * rule — and it is NOT in `groups`, deliberately: it must not be counted in `total`, because
556
+ * `total` decides the "nothing matches" line and a duplicate row is not a second match.
557
+ *
558
+ * `hasKr` is reused to state the derivation. That slot renders an id and a sentence under a group
559
+ * header, which is exactly the shape "what is this group?" needs, and reusing it is what the PM
560
+ * asked for ("use the existing one"). It carries no KR here and claims none.
561
+ *
562
+ * Absent when empty. Not a "0", not a placeholder: a heading over nothing is a heading the PM
563
+ * learns to skip. */
564
+ const now = workingSet(live, product).filter((e) => !q || e.slug.includes(q));
565
+ if (now.length) {
566
+ kids.push(groupNode({
567
+ gid: 'NOW', dyn: true, idc: FX.C.brand, gname: 'working on', gct: String(now.length),
568
+ hasKr: true, krid: '⑂', krname: 'live sessions · checked-out trees · open tabs',
569
+ inits: now.map((e) => rowOf(e.slug, WS() ? WS().reasonTitle(e) : null)),
570
+ isEmpty: false,
571
+ }, product));
572
+ }
418
573
  for (const g of groups) kids.push(groupNode(g, product));
419
574
  kids.push(fileNewRow);
420
575
  if (hits.length) {
@@ -922,71 +922,58 @@ function library(root) {
922
922
  // ("— not tracked"), never invented (D14/D40 honesty).
923
923
  const PHASE_OF = { planning: 'plan', building: 'deliver', blocked: 'deliver', delivered: 'report', accepted: 'report', unknown: 'plan' };
924
924
 
925
- // ── Delivery evidence (board-delivery-truth R-2/R-3). ──────────────────────────────────────────
925
+ // ── Delivery evidence (board-delivery-truth R-2/R-3; shared reader, workflow-status-sweep R-1). ──
926
926
  // "Has this shipped?" answered from the trunk's own history rather than from anything anyone typed.
927
927
  //
928
- // ONE `git log` per trunk head, memoised on that sha — ~0.3s and ~800 KB on this repo's 1,214
929
- // commits, paid once and reused by every surface until the trunk moves. Local only: git reads the
930
- // object store on disk, so the suite's "no network" guarantee (K. Surfaces) is untouched.
928
+ // THIS DERIVATION NO LONGER LIVES HERE. It was module-private to this file, which meant the GUI's
929
+ // initiative board answered the question with attribution while the engine's ladder
930
+ // (`scripts/workflow-state.js`) answered it without any — `buildCommits` counted every build commit
931
+ // in a range with no idea which initiative it was answering about, and over the trunk reported the
932
+ // same 488 commits as evidence for all 177 initiatives. Two readers, one repository, different
933
+ // answers to "did this ship". `scripts/delivery-index.js` is now the single reader, in the same
934
+ // shape and for the same reason as `scripts/anchor-resolve.js`; its header carries the two-signal
935
+ // rationale that used to be written here.
931
936
  //
932
- // TWO signals, because one is not enough. The branch convention (`initiative/<id>`) is precise but
933
- // incomplete: 31% of this repo's merged PRs shipped on `claude/*` branches, which match no id. So a
934
- // commit that MENTIONS the id and TOUCHES a build path counts too. The build-path requirement is
935
- // what keeps the mention signal honest — `feat(planning): add <id> PRD` mentions the id while
936
- // delivering nothing, and without the path test every specified initiative would read as shipped.
937
- const BUILD_EXCLUDE = /^(planning|okf|prototype)\//;
938
- function isDeliveryPath(f) {
939
- if (!f) return false;
940
- if (BUILD_EXCLUDE.test(f)) return false;
941
- return !/^[^/]+\.md$/.test(f); // root governance prose (DECISIONS.md, CLAUDE.md…) is not a build
942
- }
943
- const deliveryCache = new Map(); // root -> { head, index }
944
- function trunkRef(root) {
945
- for (const c of ['origin/main', 'main', 'origin/master', 'master']) {
946
- const r = gitCap(root, ['rev-parse', '--verify', '--quiet', c]);
947
- if (r.ok && r.out.trim()) return { ref: c, head: r.out.trim() };
937
+ // Resolution follows the ANCHOR_LIB_PATHS pattern below: a repo checkout first (so a dev surface and
938
+ // CI cannot disagree), then the copy `scripts/vendor-workspace-engine.js` vendors into the bundle.
939
+ // The engine requires this module, so it is already inside the vendored closure — an adopter's
940
+ // install carries it without a second packaging step to remember.
941
+ const DELIVERY_LIB_PATHS = [
942
+ path.join(__dirname, '..', 'scripts', 'delivery-index.js'),
943
+ path.join(__dirname, 'lib', 'engine', 'delivery-index.js'),
944
+ ];
945
+ function deliveryLib() {
946
+ for (const p of DELIVERY_LIB_PATHS) {
947
+ if (!fs.existsSync(p)) continue;
948
+ try { return require(p); } catch { return null; }
948
949
  }
949
950
  return null;
950
951
  }
951
- function gitCap(root, args) {
952
- try {
953
- const r = spawnSync('git', args, { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
954
- if (r.error || r.status !== 0) return { ok: false, out: '', err: (r.stderr || (r.error && r.error.message) || '').trim() };
955
- return { ok: true, out: r.stdout || '' };
956
- } catch (e) { return { ok: false, out: '', err: String((e && e.message) || e) }; }
952
+ function isDeliveryPath(f) {
953
+ const D = deliveryLib();
954
+ // Absent library → no path can be CONFIRMED as a delivery path. Returning `true` here would let
955
+ // every commit count as delivery evidence in exactly the install where the evidence cannot be
956
+ // read, which is the false-green direction. `deliveryIndex` below reports the absence out loud.
957
+ return D ? D.isDeliveryPath(f) : false;
957
958
  }
958
959
  /**
959
960
  * → { ok, reason?, byBranch:Set<id>, byMention:Set<id> }
960
961
  * `ok:false` is a REAL answer and never degrades into "nothing was delivered" — the caller renders
961
962
  * it red and marks every initiative's delivery `unknown` (R-3). A board that cannot read the
962
963
  * history must say so; silently reporting `planning` for 110 shipped initiatives is the failure
963
- * this whole initiative exists to remove.
964
+ * this whole initiative exists to remove. A missing library is the same class of answer as an
965
+ * unreadable history and takes the same `ok:false` path.
964
966
  */
965
967
  function deliveryIndex(root, ids) {
966
- const t = trunkRef(root);
967
- if (!t) return { ok: false, reason: 'no trunk branch (main/master) is reachable from this repository — delivery cannot be derived', byBranch: new Set(), byMention: new Set() };
968
- const hit = deliveryCache.get(root);
969
- if (hit && hit.head === t.head) return hit.index;
970
-
971
- // %x1e separates commits, %x1f separates the message from --name-only's file list.
972
- const r = gitCap(root, ['log', '--format=%x1e%s %b%x1f', '--name-only', t.head]);
973
- if (!r.ok) {
974
- return { ok: false, reason: `git could not read the history at ${t.ref}${r.err ? ` — ${r.err}` : ''}`, byBranch: new Set(), byMention: new Set() };
975
- }
976
- const byBranch = new Set(), byMention = new Set();
977
- const known = Array.isArray(ids) ? ids : [...ids];
978
- for (const chunk of r.out.split('\x1e')) {
979
- if (!chunk.trim()) continue;
980
- const cut = chunk.indexOf('\x1f');
981
- const msg = cut < 0 ? chunk : chunk.slice(0, cut);
982
- const files = cut < 0 ? [] : chunk.slice(cut + 1).split('\n').map((s) => s.trim()).filter(Boolean);
983
- for (const m of msg.matchAll(/initiative\/([A-Za-z0-9][\w.-]*)/g)) byBranch.add(m[1]);
984
- if (!files.some(isDeliveryPath)) continue;
985
- for (const id of known) if (id.length > 3 && msg.includes(id)) byMention.add(id);
986
- }
987
- const index = { ok: true, byBranch, byMention };
988
- deliveryCache.set(root, { head: t.head, index });
989
- return index;
968
+ const D = deliveryLib();
969
+ if (!D) {
970
+ return {
971
+ ok: false,
972
+ reason: 'the delivery reader (scripts/delivery-index.js) is not present in this install — delivery cannot be derived here',
973
+ byBranch: new Set(), byMention: new Set(),
974
+ };
975
+ }
976
+ return D.deliveryIndex(root, ids);
990
977
  }
991
978
  /**
992
979
  * The five states, in precedence order. Every one of them names the artifact it was read from, so
@@ -3256,6 +3243,10 @@ async function handleRequest(req, res) {
3256
3243
  if (p === '/lib/mobile-nav.js') return serveFile(res, path.join(__dirname, 'lib', 'mobile-nav.js'));
3257
3244
  if (p === '/lib/session-state.js') return serveFile(res, path.join(__dirname, 'lib', 'session-state.js'));
3258
3245
  if (p === '/lib/tree-share.js') return serveFile(res, path.join(__dirname, 'lib', 'tree-share.js'));
3246
+ // Which initiatives the PM is working on, and which of those are live. The session→initiative join
3247
+ // is the same two-hopper lib/four-panel.js makes per initiative on the server; the board makes it
3248
+ // once over every session, from this module, so 178 rows cost no requests.
3249
+ if (p === '/lib/working-set.js') return serveFile(res, path.join(__dirname, 'lib', 'working-set.js'));
3259
3250
  // The editor computes its live diff with the SAME module the server saves through, so what the PM
3260
3251
  // sees while typing and what gets written cannot disagree. NB: /lib/ is an explicit allowlist, not
3261
3252
  // a directory route — a <script src> for a file missing from this list 404s silently, and the