@nonbot/mcp 0.2.4 → 0.3.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 +18 -6
- package/dist/core/completedWork.d.ts +127 -0
- package/dist/core/completedWork.js +287 -0
- package/dist/core/config.d.ts +8 -0
- package/dist/core/config.js +11 -0
- package/dist/core/layout.d.ts +51 -0
- package/dist/core/layout.js +325 -6
- package/dist/core/mapHeaders.d.ts +13 -0
- package/dist/core/mapHeaders.js +64 -0
- package/dist/core/preview.d.ts +17 -0
- package/dist/core/preview.js +74 -0
- package/dist/core/releases.d.ts +127 -0
- package/dist/core/releases.js +280 -0
- package/dist/core/server.d.ts +1 -1
- package/dist/core/server.js +12 -2
- package/dist/core/style.d.ts +31 -0
- package/dist/core/style.js +64 -0
- package/dist/core/toolsets/build.d.ts +16 -3
- package/dist/core/toolsets/build.js +117 -11
- package/dist/core/toolsets/radbugs.d.ts +27 -0
- package/dist/core/toolsets/radbugs.js +55 -0
- package/dist/core/toolsets/release.d.ts +3 -0
- package/dist/core/toolsets/release.js +329 -0
- package/dist/gitLog.d.ts +3 -0
- package/dist/gitLog.js +46 -0
- package/dist/stdio.js +2 -0
- package/package.json +1 -1
package/dist/core/layout.js
CHANGED
|
@@ -130,9 +130,6 @@ export function layoutTimeline(nodes, opts = {}) {
|
|
|
130
130
|
const slot = (id, from) => ({ id, ...geo.get(id), ...(from ? { from } : {}) });
|
|
131
131
|
const byX = (a, b) => geo.get(a.id).x - geo.get(b.id).x || a.id.localeCompare(b.id);
|
|
132
132
|
let y = 0;
|
|
133
|
-
for (const o of items.filter(n => levelOf(n) === 'objective').sort(byX))
|
|
134
|
-
y = placeBand([slot(o.id)], y);
|
|
135
|
-
y = placeBand(items.filter(n => levelOf(n) === 'key-result').sort(byX).map(n => slot(n.id)), y);
|
|
136
133
|
const ipNodes = nodes.filter(isIpNode);
|
|
137
134
|
const ipFirstSource = new Map();
|
|
138
135
|
const ipUnplaced = [];
|
|
@@ -143,14 +140,13 @@ export function layoutTimeline(nodes, opts = {}) {
|
|
|
143
140
|
else
|
|
144
141
|
ipUnplaced.push(n.id);
|
|
145
142
|
}
|
|
146
|
-
const epics = items.filter(n => levelOf(n) === 'epic').sort(byX);
|
|
147
143
|
const rest = items.filter(n => !['objective', 'key-result', 'epic'].includes(levelOf(n)));
|
|
148
|
-
|
|
144
|
+
const placeEpicBlock = (epic) => {
|
|
149
145
|
const inBlock = rest.filter(n => epicOf(n) === epic);
|
|
150
146
|
const blockIds = new Set([...(epic ? [epic] : []), ...inBlock.map(n => n.id)]);
|
|
151
147
|
const ips = ipNodes.filter(n => blockIds.has(ipFirstSource.get(n.id) ?? ''));
|
|
152
148
|
if (!epic && !inBlock.length && !ips.length)
|
|
153
|
-
|
|
149
|
+
return;
|
|
154
150
|
if (y > 0)
|
|
155
151
|
y += BLOCK_GAP - ROW_GAP;
|
|
156
152
|
if (epic)
|
|
@@ -169,6 +165,59 @@ export function layoutTimeline(nodes, opts = {}) {
|
|
|
169
165
|
pool.push(...leaves.filter(n => !featureIds.has(n.data?.parentItemId)).map(n => slot(n.id)));
|
|
170
166
|
pool.push(...inBlock.filter(n => levelOf(n) === 'milestone').sort(byX).map(n => slot(n.id)));
|
|
171
167
|
y = placeBand(pool, y);
|
|
168
|
+
};
|
|
169
|
+
const epics = items.filter(n => levelOf(n) === 'epic').sort(byX);
|
|
170
|
+
const objectives = items.filter(n => levelOf(n) === 'objective').sort(byX);
|
|
171
|
+
const krs = items.filter(n => levelOf(n) === 'key-result').sort(byX);
|
|
172
|
+
if (opts.group === 'objective') {
|
|
173
|
+
const epicIds = new Set(epics.map(e => e.id));
|
|
174
|
+
const linked = new Map();
|
|
175
|
+
const link = (kr, ep) => { if (epicIds.has(ep) && !(linked.get(kr) ?? []).includes(ep))
|
|
176
|
+
linked.set(kr, [...(linked.get(kr) ?? []), ep]); };
|
|
177
|
+
for (const e of opts.edges ?? [])
|
|
178
|
+
if (e.dependencyType === 'okr-link')
|
|
179
|
+
link(e.source, e.target);
|
|
180
|
+
for (const kr of krs)
|
|
181
|
+
for (const ep of (Array.isArray(kr.data?.linkedEpicIds) ? kr.data.linkedEpicIds : []))
|
|
182
|
+
link(kr.id, ep);
|
|
183
|
+
const rank = new Map((opts.objectiveOrder ?? []).map((id, i) => [id, i]));
|
|
184
|
+
const ordered = [...objectives].sort((a, b) => (rank.get(a.id) ?? Infinity) - (rank.get(b.id) ?? Infinity) || byX(a, b));
|
|
185
|
+
const placedEpics = new Set();
|
|
186
|
+
const placeGroup = (krList) => {
|
|
187
|
+
if (krList.length)
|
|
188
|
+
y = placeBand(krList.map(n => slot(n.id)), y);
|
|
189
|
+
for (const kr of krList) {
|
|
190
|
+
const own = (linked.get(kr.id) ?? []).filter(id => !placedEpics.has(id)).map(id => byId.get(id)).sort(byX);
|
|
191
|
+
for (const ep of own) {
|
|
192
|
+
placedEpics.add(ep.id);
|
|
193
|
+
placeEpicBlock(ep.id);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
for (const o of ordered) {
|
|
198
|
+
if (y > 0)
|
|
199
|
+
y += BLOCK_GAP - ROW_GAP;
|
|
200
|
+
y = placeBand([slot(o.id)], y);
|
|
201
|
+
placeGroup(krs.filter(k => k.data?.parentItemId === o.id));
|
|
202
|
+
}
|
|
203
|
+
const objectiveIds = new Set(objectives.map(o => o.id));
|
|
204
|
+
const looseKrs = krs.filter(k => !objectiveIds.has(k.data?.parentItemId));
|
|
205
|
+
if (looseKrs.length) {
|
|
206
|
+
if (y > 0)
|
|
207
|
+
y += BLOCK_GAP - ROW_GAP;
|
|
208
|
+
placeGroup(looseKrs);
|
|
209
|
+
}
|
|
210
|
+
for (const ep of epics)
|
|
211
|
+
if (!placedEpics.has(ep.id))
|
|
212
|
+
placeEpicBlock(ep.id);
|
|
213
|
+
placeEpicBlock(null);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
for (const o of objectives)
|
|
217
|
+
y = placeBand([slot(o.id)], y);
|
|
218
|
+
y = placeBand(krs.map(n => slot(n.id)), y);
|
|
219
|
+
for (const epic of [...epics.map(e => e.id), null])
|
|
220
|
+
placeEpicBlock(epic);
|
|
172
221
|
}
|
|
173
222
|
ipUnplaced.forEach((id, k) => positions.set(id, { x: k * (IP_NODE_SIZE.width + LANE_GAP_X), y: y + BLOCK_GAP }));
|
|
174
223
|
return { positions, widthOverride };
|
|
@@ -191,3 +240,273 @@ export function applyLayout(nodes, res) {
|
|
|
191
240
|
return out;
|
|
192
241
|
});
|
|
193
242
|
}
|
|
243
|
+
export const MAP_WIDTH = { epic: 280, feature: 260, story: 240, enabler: 240, milestone: 120, objective: 300, 'key-result': 260 };
|
|
244
|
+
const MAP_HERO_EXTRA = 260;
|
|
245
|
+
const MAP_GAP = 24;
|
|
246
|
+
const MAP_INDENT = 32;
|
|
247
|
+
export const MAP_GRID_COLS = 3;
|
|
248
|
+
export const MAP_GRID_MAX_COLS = 6;
|
|
249
|
+
const MAP_GRID_ROWS = 10;
|
|
250
|
+
const MAP_COLUMN_GAP = 96;
|
|
251
|
+
const MAP_BAND_GAP = 240;
|
|
252
|
+
export function layoutMap(nodes, opts = {}) {
|
|
253
|
+
return layoutMapDetailed(nodes, opts).positions;
|
|
254
|
+
}
|
|
255
|
+
export function layoutMapDetailed(nodes, opts = {}) {
|
|
256
|
+
const heights = opts.heights ?? {};
|
|
257
|
+
const sizes = opts.sizes ?? {};
|
|
258
|
+
const items = nodes.filter(isItem);
|
|
259
|
+
const byId = new Map(items.map(n => [n.id, n]));
|
|
260
|
+
const out = new Map();
|
|
261
|
+
const measured = (n) => { const z = sizes[n.id]; return z && z.width > 0 && z.height > 0 ? z : null; };
|
|
262
|
+
const w = (n) => {
|
|
263
|
+
const m = measured(n);
|
|
264
|
+
if (m)
|
|
265
|
+
return m.width;
|
|
266
|
+
const cs = n.data?.canvasSize;
|
|
267
|
+
return cs && Number.isFinite(cs.width) ? cs.width : MAP_WIDTH[levelOf(n)];
|
|
268
|
+
};
|
|
269
|
+
const h = (n) => {
|
|
270
|
+
const m = measured(n);
|
|
271
|
+
if (m)
|
|
272
|
+
return m.height;
|
|
273
|
+
if (heights[n.id])
|
|
274
|
+
return heights[n.id];
|
|
275
|
+
const cs = n.data?.canvasSize;
|
|
276
|
+
if (cs && Number.isFinite(cs.height))
|
|
277
|
+
return cs.height;
|
|
278
|
+
const hero = Array.isArray(n.data?.pastedImages) && n.data.pastedImages.length > 0;
|
|
279
|
+
return HEIGHT[levelOf(n)] + (hero ? MAP_HERO_EXTRA : 0);
|
|
280
|
+
};
|
|
281
|
+
const tx = (n) => n.position?.x ?? 0;
|
|
282
|
+
const byTime = (a, b) => tx(a) - tx(b) || a.id.localeCompare(b.id);
|
|
283
|
+
const kids = new Map();
|
|
284
|
+
for (const n of items) {
|
|
285
|
+
const p = n.data?.parentItemId;
|
|
286
|
+
if (p && byId.has(p))
|
|
287
|
+
kids.set(p, [...(kids.get(p) ?? []), n]);
|
|
288
|
+
}
|
|
289
|
+
const childrenOf = (id) => (kids.get(id) ?? []).slice().sort(byTime);
|
|
290
|
+
const isDone = (n) => n.data?.state === 'done';
|
|
291
|
+
const grid = (leaves, x, y) => {
|
|
292
|
+
const cols = Math.min(MAP_GRID_MAX_COLS, Math.max(MAP_GRID_COLS, Math.ceil(leaves.length / MAP_GRID_ROWS)));
|
|
293
|
+
const colW = [];
|
|
294
|
+
leaves.forEach((n, i) => { const c = i % cols; colW[c] = Math.max(colW[c] ?? 0, w(n)); });
|
|
295
|
+
const colX = [];
|
|
296
|
+
colW.forEach((cw, c) => { colX[c] = c === 0 ? x : colX[c - 1] + colW[c - 1] + MAP_GAP; });
|
|
297
|
+
let bottom = y;
|
|
298
|
+
for (let i = 0; i < leaves.length; i += cols) {
|
|
299
|
+
const row = leaves.slice(i, i + cols);
|
|
300
|
+
let rowH = 0;
|
|
301
|
+
row.forEach((n, c) => { out.set(n.id, { x: colX[c], y: bottom }); rowH = Math.max(rowH, h(n)); });
|
|
302
|
+
bottom += rowH + MAP_GAP;
|
|
303
|
+
}
|
|
304
|
+
const right = colW.length ? colX[colW.length - 1] + colW[colW.length - 1] : x;
|
|
305
|
+
return { bottom, right };
|
|
306
|
+
};
|
|
307
|
+
const placed = new Set();
|
|
308
|
+
const colCap = opts.columnHeight ?? Infinity;
|
|
309
|
+
const column = (epic, x, top, loose = []) => {
|
|
310
|
+
let y = top;
|
|
311
|
+
let right = x;
|
|
312
|
+
let colX = x;
|
|
313
|
+
let flowTop = top;
|
|
314
|
+
const maybeWrap = () => { if (y - top > colCap && y > flowTop) {
|
|
315
|
+
colX = right + MAP_COLUMN_GAP / 2;
|
|
316
|
+
y = flowTop;
|
|
317
|
+
} };
|
|
318
|
+
const take = (n, nx, ny) => { out.set(n.id, { x: nx, y: ny }); placed.add(n.id); right = Math.max(right, nx + w(n)); return ny + h(n) + MAP_GAP; };
|
|
319
|
+
const leavesGrid = (list, gx) => {
|
|
320
|
+
if (!list.length)
|
|
321
|
+
return;
|
|
322
|
+
for (const n of list)
|
|
323
|
+
placed.add(n.id);
|
|
324
|
+
const g = grid(list, gx, y);
|
|
325
|
+
y = g.bottom;
|
|
326
|
+
right = Math.max(right, g.right);
|
|
327
|
+
};
|
|
328
|
+
const descend = (id, gx, depth = 1) => {
|
|
329
|
+
const at = () => (depth === 1 ? colX + MAP_INDENT : gx);
|
|
330
|
+
const all = childrenOf(id).filter(n => !placed.has(n.id)).sort((a, b) => Number(isDone(a)) - Number(isDone(b)));
|
|
331
|
+
const containers = all.filter(n => (kids.get(n.id) ?? []).length > 0 || levelOf(n) === 'feature');
|
|
332
|
+
const leaves = all.filter(n => !containers.includes(n) && levelOf(n) !== 'milestone');
|
|
333
|
+
const ms = all.filter(n => !containers.includes(n) && levelOf(n) === 'milestone');
|
|
334
|
+
for (const c of containers) {
|
|
335
|
+
if (depth === 1)
|
|
336
|
+
maybeWrap();
|
|
337
|
+
const cx = at();
|
|
338
|
+
y = take(c, cx, y);
|
|
339
|
+
descend(c.id, cx + MAP_INDENT, depth + 1);
|
|
340
|
+
}
|
|
341
|
+
if (leaves.length && depth === 1)
|
|
342
|
+
maybeWrap();
|
|
343
|
+
leavesGrid(leaves, at());
|
|
344
|
+
for (const m of ms)
|
|
345
|
+
y = take(m, at(), y);
|
|
346
|
+
};
|
|
347
|
+
if (epic) {
|
|
348
|
+
y = take(epic, x, y);
|
|
349
|
+
flowTop = y;
|
|
350
|
+
descend(epic.id, x + MAP_INDENT);
|
|
351
|
+
}
|
|
352
|
+
for (const n of loose)
|
|
353
|
+
if (!placed.has(n.id)) {
|
|
354
|
+
y = take(n, x, y);
|
|
355
|
+
descend(n.id, x + MAP_INDENT);
|
|
356
|
+
}
|
|
357
|
+
return { right, bottom: y };
|
|
358
|
+
};
|
|
359
|
+
const objectives = items.filter(n => levelOf(n) === 'objective');
|
|
360
|
+
const krs = items.filter(n => levelOf(n) === 'key-result').sort(byTime);
|
|
361
|
+
const epics = items.filter(n => levelOf(n) === 'epic' && !byId.has(n.data?.parentItemId)).sort(byTime);
|
|
362
|
+
const epicIds = new Set(epics.map(e => e.id));
|
|
363
|
+
const linked = new Map();
|
|
364
|
+
const link = (kr, ep) => { if (epicIds.has(ep) && !(linked.get(kr) ?? []).includes(ep))
|
|
365
|
+
linked.set(kr, [...(linked.get(kr) ?? []), ep]); };
|
|
366
|
+
for (const e of opts.edges ?? [])
|
|
367
|
+
if (e.dependencyType === 'okr-link')
|
|
368
|
+
link(e.source, e.target);
|
|
369
|
+
for (const kr of krs)
|
|
370
|
+
for (const ep of (Array.isArray(kr.data?.linkedEpicIds) ? kr.data.linkedEpicIds : []))
|
|
371
|
+
link(kr.id, ep);
|
|
372
|
+
const rank = new Map((opts.objectiveOrder ?? []).map((id, i) => [id, i]));
|
|
373
|
+
const ordered = objectives.slice().sort((a, b) => (rank.get(a.id) ?? Infinity) - (rank.get(b.id) ?? Infinity) || byTime(a, b));
|
|
374
|
+
const headerH = opts.headerHeight ?? 0;
|
|
375
|
+
const allById = new Map(nodes.map(n => [n.id, n]));
|
|
376
|
+
const modW = (m) => sizes[m.id]?.width || Number(m.data?.width) || (isIpNode(m) ? IP_NODE_SIZE.width : 400);
|
|
377
|
+
const modH = (m) => sizes[m.id]?.height || Number(m.data?.height) || (isIpNode(m) ? IP_NODE_SIZE.height : 300);
|
|
378
|
+
const anyW = (n) => (n.type === 'rad-module' ? modW(n) : w(n));
|
|
379
|
+
const anyH = (n) => (n.type === 'rad-module' ? modH(n) : h(n));
|
|
380
|
+
const bands = [];
|
|
381
|
+
const capture = (kind, objective, header, fn) => {
|
|
382
|
+
const before = new Set(out.keys());
|
|
383
|
+
fn(header ? headerH : 0);
|
|
384
|
+
const ids = [...out.keys()].filter(k => !before.has(k));
|
|
385
|
+
if (!ids.length)
|
|
386
|
+
return;
|
|
387
|
+
let width = 0;
|
|
388
|
+
let height = header ? headerH : 0;
|
|
389
|
+
for (const id of ids) {
|
|
390
|
+
const p = out.get(id);
|
|
391
|
+
const n = allById.get(id);
|
|
392
|
+
width = Math.max(width, p.x + anyW(n));
|
|
393
|
+
height = Math.max(height, p.y + anyH(n));
|
|
394
|
+
}
|
|
395
|
+
const its = ids.map(id => allById.get(id)).filter(isItem);
|
|
396
|
+
bands.push({ kind, objectiveId: objective?.id ?? null, x: 0, y: 0, width, height, header, items: its.length, done: its.filter(isDone).length, epics: its.filter(n => levelOf(n) === 'epic').length, ids });
|
|
397
|
+
};
|
|
398
|
+
const ipNodes = nodes.filter(isIpNode);
|
|
399
|
+
const firstSource = (m) => (Array.isArray(m.data?.attachedItemIds) ? m.data.attachedItemIds : []).find((id) => byId.has(id));
|
|
400
|
+
const band = (top, lead, bandEpics) => {
|
|
401
|
+
const before = new Set(out.keys());
|
|
402
|
+
let x = 0;
|
|
403
|
+
if (lead.length) {
|
|
404
|
+
let y = top;
|
|
405
|
+
let right = 0;
|
|
406
|
+
for (const n of lead) {
|
|
407
|
+
out.set(n.id, { x: 0, y });
|
|
408
|
+
placed.add(n.id);
|
|
409
|
+
right = Math.max(right, w(n));
|
|
410
|
+
y += h(n) + MAP_GAP;
|
|
411
|
+
}
|
|
412
|
+
x = right + MAP_COLUMN_GAP;
|
|
413
|
+
}
|
|
414
|
+
for (const ep of bandEpics)
|
|
415
|
+
x = column(ep, x, top).right + MAP_COLUMN_GAP;
|
|
416
|
+
let y = top;
|
|
417
|
+
for (const m of ipNodes) {
|
|
418
|
+
const src = firstSource(m);
|
|
419
|
+
if (out.has(m.id) || !src || !out.has(src) || before.has(src))
|
|
420
|
+
continue;
|
|
421
|
+
out.set(m.id, { x, y });
|
|
422
|
+
y += modH(m) + MAP_GAP;
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
const placedEpics = new Set();
|
|
426
|
+
const epicsOf = (krList) => {
|
|
427
|
+
const list = [];
|
|
428
|
+
for (const kr of krList)
|
|
429
|
+
for (const id of linked.get(kr.id) ?? [])
|
|
430
|
+
if (!placedEpics.has(id)) {
|
|
431
|
+
placedEpics.add(id);
|
|
432
|
+
list.push(byId.get(id));
|
|
433
|
+
}
|
|
434
|
+
return list;
|
|
435
|
+
};
|
|
436
|
+
for (const o of ordered) {
|
|
437
|
+
const own = krs.filter(k => k.data?.parentItemId === o.id);
|
|
438
|
+
capture('objective', o, true, top => band(top, [o, ...own], epicsOf(own)));
|
|
439
|
+
}
|
|
440
|
+
const objectiveIds = new Set(objectives.map(o => o.id));
|
|
441
|
+
const looseKrs = krs.filter(k => !objectiveIds.has(k.data?.parentItemId));
|
|
442
|
+
const looseEpics = epicsOf(looseKrs);
|
|
443
|
+
const unlinked = epics.filter(e => !placedEpics.has(e.id));
|
|
444
|
+
for (const e of unlinked)
|
|
445
|
+
placedEpics.add(e.id);
|
|
446
|
+
capture('loose', null, true, top => band(top, looseKrs, [...looseEpics, ...unlinked]));
|
|
447
|
+
capture('rest', null, false, () => {
|
|
448
|
+
const rest = items.filter(n => !placed.has(n.id) && !byId.has(n.data?.parentItemId)).sort(byTime);
|
|
449
|
+
let y = rest.length ? column(null, 0, 0, rest).bottom : 0;
|
|
450
|
+
const stray = items.filter(n => !placed.has(n.id));
|
|
451
|
+
if (stray.length) {
|
|
452
|
+
for (const n of stray)
|
|
453
|
+
placed.add(n.id);
|
|
454
|
+
grid(stray, 0, y);
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
capture('modules', null, false, () => {
|
|
458
|
+
let mx = 0;
|
|
459
|
+
let my = 0;
|
|
460
|
+
let rowH = 0;
|
|
461
|
+
for (const m of nodes.filter(n => n.type === 'rad-module' && !out.has(n.id))) {
|
|
462
|
+
const mw = modW(m);
|
|
463
|
+
if (mx > 0 && mx + mw > 4 * 520) {
|
|
464
|
+
my += rowH + MAP_GAP;
|
|
465
|
+
mx = 0;
|
|
466
|
+
rowH = 0;
|
|
467
|
+
}
|
|
468
|
+
out.set(m.id, { x: mx, y: my });
|
|
469
|
+
mx += mw + MAP_GAP;
|
|
470
|
+
rowH = Math.max(rowH, modH(m));
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
const place = (b, dx, dy) => {
|
|
474
|
+
for (const id of b.ids) {
|
|
475
|
+
const p = out.get(id);
|
|
476
|
+
out.set(id, { x: p.x + dx, y: p.y + dy });
|
|
477
|
+
}
|
|
478
|
+
b.x = dx;
|
|
479
|
+
b.y = dy;
|
|
480
|
+
};
|
|
481
|
+
if (opts.pack === 'grid' && bands.length) {
|
|
482
|
+
const area = bands.reduce((sum, b) => sum + (b.width + MAP_BAND_GAP) * (b.height + MAP_BAND_GAP), 0);
|
|
483
|
+
const rowW = Math.max(...bands.map(b => b.width), Math.sqrt(area * (opts.aspect ?? 16 / 9)));
|
|
484
|
+
let x = 0;
|
|
485
|
+
let y = 0;
|
|
486
|
+
let rowH = 0;
|
|
487
|
+
for (const b of bands) {
|
|
488
|
+
if (x > 0 && x + b.width > rowW) {
|
|
489
|
+
y += rowH + MAP_BAND_GAP;
|
|
490
|
+
x = 0;
|
|
491
|
+
rowH = 0;
|
|
492
|
+
}
|
|
493
|
+
place(b, x, y);
|
|
494
|
+
x += b.width + MAP_BAND_GAP;
|
|
495
|
+
rowH = Math.max(rowH, b.height);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
let y = 0;
|
|
500
|
+
for (const b of bands) {
|
|
501
|
+
place(b, 0, y);
|
|
502
|
+
y += b.height + MAP_BAND_GAP;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return { positions: out, bands: bands.map(({ ids: _ids, ...b }) => b) };
|
|
506
|
+
}
|
|
507
|
+
export function applyMapLayout(nodes, positions) {
|
|
508
|
+
return nodes.map(n => {
|
|
509
|
+
const p = positions.get(n.id);
|
|
510
|
+
return p ? { ...n, freePosition: { x: Math.round(p.x), y: Math.round(p.y) } } : n;
|
|
511
|
+
});
|
|
512
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { LayoutNode, MapBand } from './layout.js';
|
|
2
|
+
export declare const HEADER_MARKER = "__layoutHeader";
|
|
3
|
+
export declare const HEADER_TIMELINE_X = -6000;
|
|
4
|
+
export declare function headerText(b: MapBand, title: string): {
|
|
5
|
+
title: string;
|
|
6
|
+
body: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function withMapHeaders<T extends LayoutNode>(nodes: T[], bands: MapBand[], headerHeight: number, now?: number): {
|
|
9
|
+
nodes: T[];
|
|
10
|
+
added: number;
|
|
11
|
+
updated: number;
|
|
12
|
+
removed: number;
|
|
13
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export const HEADER_MARKER = '__layoutHeader';
|
|
2
|
+
export const HEADER_TIMELINE_X = -6000;
|
|
3
|
+
const MAX_NOTE_WIDTH = 2400;
|
|
4
|
+
const esc = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
5
|
+
const keyOf = (b) => b.objectiveId ?? b.kind;
|
|
6
|
+
export function headerText(b, title) {
|
|
7
|
+
const pct = b.items ? Math.round((b.done / b.items) * 100) : 0;
|
|
8
|
+
const epics = `${b.epics} epic${b.epics === 1 ? '' : 's'}`;
|
|
9
|
+
const line = `${epics} · ${b.done} of ${b.items} cards done (${pct}%)`;
|
|
10
|
+
return { title, body: `<h2>${esc(title)}</h2><p>${esc(line)}</p>` };
|
|
11
|
+
}
|
|
12
|
+
export function withMapHeaders(nodes, bands, headerHeight, now = Date.now()) {
|
|
13
|
+
const titleOf = new Map(nodes.map(n => [n.id, String(n.data?.title ?? '')]));
|
|
14
|
+
const wanted = new Map(bands.filter(b => b.header).map(b => [keyOf(b), b]));
|
|
15
|
+
let added = 0;
|
|
16
|
+
let updated = 0;
|
|
17
|
+
let removed = 0;
|
|
18
|
+
const timelineY = (b) => {
|
|
19
|
+
const o = b.objectiveId ? nodes.find(n => n.id === b.objectiveId) : undefined;
|
|
20
|
+
return o?.position?.y ?? 0;
|
|
21
|
+
};
|
|
22
|
+
const build = (b, id, prev) => {
|
|
23
|
+
const t = headerText(b, b.objectiveId ? titleOf.get(b.objectiveId) || 'Objective' : 'Not linked to a goal yet');
|
|
24
|
+
const size = { width: Math.min(MAX_NOTE_WIDTH, Math.max(600, Math.round(b.width))), height: Math.max(80, headerHeight - 40) };
|
|
25
|
+
const prevModule = (prev?.data?.moduleData ?? {});
|
|
26
|
+
return {
|
|
27
|
+
...(prev ?? {}),
|
|
28
|
+
id,
|
|
29
|
+
type: 'rad-module',
|
|
30
|
+
position: { x: HEADER_TIMELINE_X, y: timelineY(b) },
|
|
31
|
+
freePosition: { x: Math.round(b.x), y: Math.round(b.y) },
|
|
32
|
+
data: {
|
|
33
|
+
...(prev?.data ?? {}),
|
|
34
|
+
subtype: 'document',
|
|
35
|
+
width: size.width,
|
|
36
|
+
height: size.height,
|
|
37
|
+
moduleData: { files: [], images: [], ...prevModule, title: t.title, body: t.body, __chromeCollapsed: true, __noteSize: size, [HEADER_MARKER]: keyOf(b) },
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
const out = [];
|
|
42
|
+
for (const n of nodes) {
|
|
43
|
+
const k = n.type === 'rad-module' && n.data?.subtype === 'document' ? n.data?.moduleData?.[HEADER_MARKER] : undefined;
|
|
44
|
+
if (typeof k !== 'string') {
|
|
45
|
+
out.push(n);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const b = wanted.get(k);
|
|
49
|
+
if (!b) {
|
|
50
|
+
removed++;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
out.push(build(b, n.id, n));
|
|
54
|
+
updated++;
|
|
55
|
+
wanted.delete(k);
|
|
56
|
+
}
|
|
57
|
+
let i = 0;
|
|
58
|
+
for (const [k, b] of wanted) {
|
|
59
|
+
const slug = k.replace(/[^a-zA-Z0-9]/g, '').slice(-12) || String(i);
|
|
60
|
+
out.push(build(b, `radmod-document-${now}-hdr${slug}${i++}`));
|
|
61
|
+
added++;
|
|
62
|
+
}
|
|
63
|
+
return { nodes: out, added, updated, removed };
|
|
64
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type LayoutNode } from './layout.js';
|
|
2
|
+
export interface PreviewOptions {
|
|
3
|
+
view?: 'map' | 'timeline';
|
|
4
|
+
sizes?: Record<string, {
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
}>;
|
|
8
|
+
width?: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function renderPreviewSvg(nodes: LayoutNode[], opts?: PreviewOptions): {
|
|
11
|
+
svg: string;
|
|
12
|
+
cards: number;
|
|
13
|
+
extent: {
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { MAP_WIDTH, levelOf, isItem } from './layout.js';
|
|
2
|
+
const HEIGHT = { epic: 120, feature: 100, story: 96, enabler: 96, milestone: 44, objective: 120, 'key-result': 100 };
|
|
3
|
+
const esc = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
4
|
+
const strip = (s) => s.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
5
|
+
const fit = (s, px, size) => { const max = Math.max(3, Math.floor(px / (size * 0.56))); return s.length > max ? s.slice(0, max - 1) + '…' : s; };
|
|
6
|
+
export function renderPreviewSvg(nodes, opts = {}) {
|
|
7
|
+
const view = opts.view ?? 'map';
|
|
8
|
+
const pos = (n) => (view === 'map' ? (n.freePosition ?? n.position) : n.position) ?? { x: 0, y: 0 };
|
|
9
|
+
const size = (n) => {
|
|
10
|
+
const m = opts.sizes?.[n.id];
|
|
11
|
+
if (m)
|
|
12
|
+
return m;
|
|
13
|
+
if (n.type === 'rad-module') {
|
|
14
|
+
const ns = n.data?.moduleData?.__noteSize;
|
|
15
|
+
if (n.data?.moduleData?.__chromeCollapsed && ns?.width)
|
|
16
|
+
return ns;
|
|
17
|
+
return { width: Number(n.data?.width) || 400, height: Number(n.data?.height) || 300 };
|
|
18
|
+
}
|
|
19
|
+
const cs = n.data?.canvasSize;
|
|
20
|
+
if (view === 'map' && cs?.width)
|
|
21
|
+
return cs;
|
|
22
|
+
const lv = levelOf(n);
|
|
23
|
+
return { width: MAP_WIDTH[lv], height: HEIGHT[lv] };
|
|
24
|
+
};
|
|
25
|
+
const drawn = nodes.filter(n => isItem(n) || n.type === 'rad-module');
|
|
26
|
+
if (!drawn.length)
|
|
27
|
+
return { svg: '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"/>', cards: 0, extent: { width: 0, height: 0 } };
|
|
28
|
+
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
|
|
29
|
+
for (const n of drawn) {
|
|
30
|
+
const p = pos(n);
|
|
31
|
+
const s = size(n);
|
|
32
|
+
x0 = Math.min(x0, p.x);
|
|
33
|
+
y0 = Math.min(y0, p.y);
|
|
34
|
+
x1 = Math.max(x1, p.x + s.width);
|
|
35
|
+
y1 = Math.max(y1, p.y + s.height);
|
|
36
|
+
}
|
|
37
|
+
const pad = 80;
|
|
38
|
+
const W = x1 - x0 + 2 * pad;
|
|
39
|
+
const H = y1 - y0 + 2 * pad;
|
|
40
|
+
const outW = opts.width ?? 1600;
|
|
41
|
+
const parts = [];
|
|
42
|
+
for (const n of drawn) {
|
|
43
|
+
const p = pos(n);
|
|
44
|
+
const s = size(n);
|
|
45
|
+
const x = p.x - x0 + pad, y = p.y - y0 + pad;
|
|
46
|
+
if (n.type === 'rad-module') {
|
|
47
|
+
const header = n.data?.moduleData?.__layoutHeader;
|
|
48
|
+
const title = String(n.data?.moduleData?.title ?? n.data?.subtype ?? 'module').trim();
|
|
49
|
+
const body = strip(String(n.data?.moduleData?.body ?? '')).replace(title, '').trim();
|
|
50
|
+
if (header) {
|
|
51
|
+
parts.push(`<text x="${x}" y="${y + 64}" font-size="64" font-weight="700" fill="#E6E1F5">${esc(fit(title, s.width, 64))}</text>`);
|
|
52
|
+
if (body)
|
|
53
|
+
parts.push(`<text x="${x}" y="${y + 130}" font-size="36" fill="#A79FC4">${esc(fit(body, s.width, 36))}</text>`);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
parts.push(`<rect x="${x}" y="${y}" width="${s.width}" height="${s.height}" rx="10" fill="#2A2740" stroke="#4A4566" stroke-width="2"/>`);
|
|
57
|
+
parts.push(`<text x="${x + 14}" y="${y + 32}" font-size="22" fill="#C9C2E6">${esc(fit(title, s.width - 28, 22))}</text>`);
|
|
58
|
+
}
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const lv = levelOf(n);
|
|
62
|
+
const done = n.data?.state === 'done';
|
|
63
|
+
const big = lv === 'epic' || lv === 'objective';
|
|
64
|
+
const fill = done ? '#CFEBD9' : big ? '#2B2152' : '#1F1B33';
|
|
65
|
+
const ink = done ? '#24563A' : '#ECE8FA';
|
|
66
|
+
const stroke = big ? '#7367F0' : done ? '#9BCFAF' : '#3E3860';
|
|
67
|
+
const fs = big ? 26 : lv === 'feature' || lv === 'key-result' ? 21 : 18;
|
|
68
|
+
parts.push(`<rect x="${x}" y="${y}" width="${s.width}" height="${s.height}" rx="10" fill="${fill}" stroke="${stroke}" stroke-width="${big ? 4 : 2}"/>`);
|
|
69
|
+
parts.push(`<text x="${x + 14}" y="${y + 24}" font-size="13" fill="${done ? '#4E8A66' : '#8F87B5'}" letter-spacing="1">${esc(lv.toUpperCase())}${done ? ' · DONE' : n.data?.state ? ' · ' + esc(String(n.data.state).toUpperCase()) : ''}</text>`);
|
|
70
|
+
parts.push(`<text x="${x + 14}" y="${y + 28 + fs * 1.3}" font-size="${fs}" font-weight="${big ? 700 : 500}" fill="${ink}">${esc(fit(String(n.data?.title ?? '').trim(), s.width - 28, fs))}</text>`);
|
|
71
|
+
}
|
|
72
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${outW}" height="${Math.round((outW * H) / W)}" viewBox="0 0 ${Math.round(W)} ${Math.round(H)}" font-family="Public Sans, Helvetica, Arial, sans-serif"><rect width="100%" height="100%" fill="#120F1E"/>${parts.join('')}</svg>`;
|
|
73
|
+
return { svg, cards: drawn.length, extent: { width: Math.round(W), height: Math.round(H) } };
|
|
74
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
export declare const RELEASE_SUBTYPE = "release";
|
|
2
|
+
export declare const RELEASE_KEY: "releaseId";
|
|
3
|
+
export declare const DONE_AT_KEY: "doneAt";
|
|
4
|
+
export declare const RELEASE_SIZE: {
|
|
5
|
+
readonly width: 300;
|
|
6
|
+
readonly height: 184;
|
|
7
|
+
};
|
|
8
|
+
export interface ReleaseData {
|
|
9
|
+
name: string;
|
|
10
|
+
from: string;
|
|
11
|
+
to: string;
|
|
12
|
+
summary?: string;
|
|
13
|
+
highlights?: string[];
|
|
14
|
+
draft?: boolean;
|
|
15
|
+
approvedAt?: number;
|
|
16
|
+
open?: boolean;
|
|
17
|
+
proposed?: string[];
|
|
18
|
+
}
|
|
19
|
+
export type Item = {
|
|
20
|
+
id: string;
|
|
21
|
+
data?: Record<string, unknown>;
|
|
22
|
+
};
|
|
23
|
+
export type Edge = {
|
|
24
|
+
source: string;
|
|
25
|
+
target: string;
|
|
26
|
+
dependencyType?: string;
|
|
27
|
+
type?: string;
|
|
28
|
+
};
|
|
29
|
+
export type ReleaseModule = {
|
|
30
|
+
id: string;
|
|
31
|
+
subtype: string;
|
|
32
|
+
data: Record<string, unknown>;
|
|
33
|
+
};
|
|
34
|
+
export type ReleaseMap = ReadonlyMap<string, ReleaseModule>;
|
|
35
|
+
export declare const releaseIdOf: (n: Item) => string | null;
|
|
36
|
+
export declare function releasesById(modules: ReadonlyArray<ReleaseModule>): Map<string, ReleaseModule>;
|
|
37
|
+
export declare const releaseIsOpen: (r: ReleaseModule) => boolean;
|
|
38
|
+
export declare function isHiddenByRelease(n: Item, releases: ReleaseMap): boolean;
|
|
39
|
+
export declare function isFiled(n: Item, releases: ReleaseMap): boolean;
|
|
40
|
+
export declare function membersOf<T extends Item>(releaseId: string, nodes: ReadonlyArray<T>): T[];
|
|
41
|
+
export declare function subtreeOf(rootIds: ReadonlyArray<string>, nodes: ReadonlyArray<Item>, edges?: ReadonlyArray<Edge>): Set<string>;
|
|
42
|
+
export declare const isDone: (n: Item) => boolean;
|
|
43
|
+
export declare function unfinishedIn(ids: Iterable<string>, nodes: ReadonlyArray<Item>): string[];
|
|
44
|
+
export declare function doneAtOf(n: Item, now?: number): number;
|
|
45
|
+
export interface FilePatch {
|
|
46
|
+
id: string;
|
|
47
|
+
patch: {
|
|
48
|
+
releaseId: string;
|
|
49
|
+
doneAt: number;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export declare function fileSet(ids: Iterable<string>, releaseId: string, nodes: ReadonlyArray<Item>, opts?: {
|
|
53
|
+
onlyDone?: boolean;
|
|
54
|
+
now?: number;
|
|
55
|
+
}): FilePatch[];
|
|
56
|
+
export declare function unfileSet(ids: Iterable<string>, nodes: ReadonlyArray<Item>): Array<{
|
|
57
|
+
id: string;
|
|
58
|
+
patch: {
|
|
59
|
+
releaseId: null;
|
|
60
|
+
};
|
|
61
|
+
}>;
|
|
62
|
+
export declare function memberCounts(releaseId: string, nodes: ReadonlyArray<Item>): Record<string, number>;
|
|
63
|
+
export declare function previousRelease<R extends ReleaseModule>(from: string, releases: ReadonlyArray<R>, exceptId?: string): R | null;
|
|
64
|
+
export declare function weekWindow(d?: Date): {
|
|
65
|
+
from: string;
|
|
66
|
+
to: string;
|
|
67
|
+
};
|
|
68
|
+
export declare function releaseOf(n: Item, releases: ReleaseMap): ReleaseModule | null;
|
|
69
|
+
export declare function filedItemIds(nodes: ReadonlyArray<Item>, releases: ReleaseMap): Set<string>;
|
|
70
|
+
export declare function releaseModulesFromGraphNodes(nodes: ReadonlyArray<unknown>): Map<string, ReleaseModule>;
|
|
71
|
+
export interface ChainRelease {
|
|
72
|
+
id: string;
|
|
73
|
+
subtype: string;
|
|
74
|
+
data: Record<string, unknown>;
|
|
75
|
+
outConnectedNodeId?: string;
|
|
76
|
+
outConnectedNodeIds?: string[];
|
|
77
|
+
}
|
|
78
|
+
export interface ChainWrite {
|
|
79
|
+
id: string;
|
|
80
|
+
out: string | null;
|
|
81
|
+
}
|
|
82
|
+
export declare function outTargetsOf(inst: {
|
|
83
|
+
outConnectedNodeId?: string;
|
|
84
|
+
outConnectedNodeIds?: string[];
|
|
85
|
+
}): string[];
|
|
86
|
+
export declare function chainWrites(id: string, all: ReadonlyArray<ChainRelease>): ChainWrite[];
|
|
87
|
+
export interface GraphNode {
|
|
88
|
+
id: string;
|
|
89
|
+
type?: string;
|
|
90
|
+
position?: {
|
|
91
|
+
x: number;
|
|
92
|
+
y: number;
|
|
93
|
+
};
|
|
94
|
+
freePosition?: {
|
|
95
|
+
x: number;
|
|
96
|
+
y: number;
|
|
97
|
+
};
|
|
98
|
+
data?: Record<string, any>;
|
|
99
|
+
}
|
|
100
|
+
export declare function chainReleasesFromGraphNodes(nodes: ReadonlyArray<GraphNode>): ChainRelease[];
|
|
101
|
+
export declare function applyChainWrites<T extends GraphNode>(nodes: ReadonlyArray<T>, writes: ReadonlyArray<ChainWrite>): T[];
|
|
102
|
+
export declare function applyItemPatches<T extends GraphNode>(nodes: ReadonlyArray<T>, patches: ReadonlyArray<{
|
|
103
|
+
id: string;
|
|
104
|
+
patch: Record<string, unknown>;
|
|
105
|
+
}>): T[];
|
|
106
|
+
export declare function patchReleaseData<T extends GraphNode>(nodes: ReadonlyArray<T>, releaseId: string, patch: Record<string, unknown>): T[];
|
|
107
|
+
export declare function newReleaseId(now?: number): string;
|
|
108
|
+
export declare function placeRelease(nodes: ReadonlyArray<GraphNode>, from: string, prevId: string | null, xOfDate: (iso: string) => number): {
|
|
109
|
+
position: {
|
|
110
|
+
x: number;
|
|
111
|
+
y: number;
|
|
112
|
+
};
|
|
113
|
+
freePosition: {
|
|
114
|
+
x: number;
|
|
115
|
+
y: number;
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
export declare function releaseNode(id: string, md: ReleaseData, place: {
|
|
119
|
+
position: {
|
|
120
|
+
x: number;
|
|
121
|
+
y: number;
|
|
122
|
+
};
|
|
123
|
+
freePosition: {
|
|
124
|
+
x: number;
|
|
125
|
+
y: number;
|
|
126
|
+
};
|
|
127
|
+
}): GraphNode;
|