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.
- package/README.md +28 -0
- package/package.json +5 -3
- package/payload/AGENTS.md +7 -0
- package/payload/KIT-VERSION +1 -1
- package/payload/README.md +16 -1
- package/payload/scripts/anchor-resolve.js +277 -0
- package/payload/scripts/delivery-index.js +278 -0
- package/payload/scripts/workflow-state.js +1139 -0
- package/payload/scripts/workflow-sweep.js +764 -0
- package/workspace/README.md +10 -0
- package/workspace/lib/console-state.js +39 -18
- package/workspace/lib/engine/delivery-index.js +278 -0
- package/workspace/lib/engine/workflow-state.js +165 -21
- package/workspace/lib/session-state.js +56 -2
- package/workspace/lib/working-set.js +224 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +3 -3
- package/workspace/public/index.html +7 -0
- package/workspace/public/pm/data.js +28 -1
- package/workspace/public/pm/panel-initiatives.js +172 -17
- package/workspace/server.js +42 -51
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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) {
|
package/workspace/server.js
CHANGED
|
@@ -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
|
-
//
|
|
929
|
-
//
|
|
930
|
-
//
|
|
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
|
-
//
|
|
933
|
-
//
|
|
934
|
-
//
|
|
935
|
-
//
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
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
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
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
|
|
967
|
-
if (!
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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
|