@nonbot/mcp 0.2.4 → 0.3.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.
@@ -0,0 +1,81 @@
1
+ import { MAP_WIDTH, levelOf, isItem } from './layout.js';
2
+ import { satellitePositions, SATELLITE_FIELDS } from './satellites.js';
3
+ const HEIGHT = { epic: 120, feature: 100, story: 96, enabler: 96, milestone: 44, objective: 120, 'key-result': 100 };
4
+ const esc = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
5
+ const strip = (s) => s.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
6
+ 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; };
7
+ export function renderPreviewSvg(nodes, opts = {}) {
8
+ const view = opts.view ?? 'map';
9
+ const pos = (n) => (view === 'map' ? (n.freePosition ?? n.position) : n.position) ?? { x: 0, y: 0 };
10
+ const size = (n) => {
11
+ const m = opts.sizes?.[n.id];
12
+ if (m)
13
+ return m;
14
+ if (n.type === 'rad-module') {
15
+ const ns = n.data?.moduleData?.__noteSize;
16
+ if (n.data?.moduleData?.__chromeCollapsed && ns?.width)
17
+ return ns;
18
+ return { width: Number(n.data?.width) || 400, height: Number(n.data?.height) || 300 };
19
+ }
20
+ const cs = n.data?.canvasSize;
21
+ if (view === 'map' && cs?.width)
22
+ return cs;
23
+ const lv = levelOf(n);
24
+ return { width: MAP_WIDTH[lv], height: HEIGHT[lv] };
25
+ };
26
+ const drawn = nodes.filter(n => isItem(n) || n.type === 'rad-module');
27
+ if (!drawn.length)
28
+ return { svg: '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"/>', cards: 0, satellites: 0, extent: { width: 0, height: 0 } };
29
+ const bands = drawn.filter(isItem).flatMap(n => { const p = pos(n); const s = size(n); return satellitePositions(n.data, { x: p.x, y: p.y, width: s.width, height: s.height }); });
30
+ let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
31
+ const grow = (x, y, w, h) => { x0 = Math.min(x0, x); y0 = Math.min(y0, y); x1 = Math.max(x1, x + w); y1 = Math.max(y1, y + h); };
32
+ for (const n of drawn) {
33
+ const p = pos(n);
34
+ const s = size(n);
35
+ grow(p.x, p.y, s.width, s.height);
36
+ }
37
+ for (const b of bands)
38
+ grow(b.x, b.y, b.width, b.height);
39
+ const pad = 80;
40
+ const W = x1 - x0 + 2 * pad;
41
+ const H = y1 - y0 + 2 * pad;
42
+ const outW = opts.width ?? 1600;
43
+ const parts = [];
44
+ for (const b of bands) {
45
+ const x = b.x - x0 + pad, y = b.y - y0 + pad;
46
+ parts.push(`<rect x="${x}" y="${y}" width="${b.width}" height="${b.height}" rx="8" fill="#17142A" stroke="#4A4566" stroke-width="1.5" stroke-dasharray="6 4"/>`);
47
+ parts.push(`<text x="${x + 10}" y="${y + 20}" font-size="12" fill="#8F87B5">${esc(fit(SATELLITE_FIELDS[b.field].label, b.width - 20, 12))}</text>`);
48
+ }
49
+ for (const n of drawn) {
50
+ const p = pos(n);
51
+ const s = size(n);
52
+ const x = p.x - x0 + pad, y = p.y - y0 + pad;
53
+ if (n.type === 'rad-module') {
54
+ const header = n.data?.moduleData?.__layoutHeader;
55
+ const title = String(n.data?.moduleData?.title ?? n.data?.subtype ?? 'module').trim();
56
+ const body = strip(String(n.data?.moduleData?.body ?? '')).replace(title, '').trim();
57
+ if (header) {
58
+ parts.push(`<text x="${x}" y="${y + 64}" font-size="64" font-weight="700" fill="#E6E1F5">${esc(fit(title, s.width, 64))}</text>`);
59
+ if (body)
60
+ parts.push(`<text x="${x}" y="${y + 130}" font-size="36" fill="#A79FC4">${esc(fit(body, s.width, 36))}</text>`);
61
+ }
62
+ else {
63
+ parts.push(`<rect x="${x}" y="${y}" width="${s.width}" height="${s.height}" rx="10" fill="#2A2740" stroke="#4A4566" stroke-width="2"/>`);
64
+ parts.push(`<text x="${x + 14}" y="${y + 32}" font-size="22" fill="#C9C2E6">${esc(fit(title, s.width - 28, 22))}</text>`);
65
+ }
66
+ continue;
67
+ }
68
+ const lv = levelOf(n);
69
+ const done = n.data?.state === 'done';
70
+ const big = lv === 'epic' || lv === 'objective';
71
+ const fill = done ? '#CFEBD9' : big ? '#2B2152' : '#1F1B33';
72
+ const ink = done ? '#24563A' : '#ECE8FA';
73
+ const stroke = big ? '#7367F0' : done ? '#9BCFAF' : '#3E3860';
74
+ const fs = big ? 26 : lv === 'feature' || lv === 'key-result' ? 21 : 18;
75
+ parts.push(`<rect x="${x}" y="${y}" width="${s.width}" height="${s.height}" rx="10" fill="${fill}" stroke="${stroke}" stroke-width="${big ? 4 : 2}"/>`);
76
+ 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>`);
77
+ 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>`);
78
+ }
79
+ 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>`;
80
+ return { svg, cards: drawn.length, satellites: bands.length, extent: { width: Math.round(W), height: Math.round(H) } };
81
+ }
@@ -0,0 +1,159 @@
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;
128
+ export interface LaneBox {
129
+ x: number;
130
+ y: number;
131
+ width: number;
132
+ height: number;
133
+ }
134
+ export interface LaneRelease {
135
+ id: string;
136
+ x: number;
137
+ y: number;
138
+ width: number;
139
+ data: Record<string, unknown>;
140
+ }
141
+ export interface LanePlace {
142
+ id: string;
143
+ x: number;
144
+ y: number;
145
+ }
146
+ export declare const LANE_GAP_X = 48;
147
+ export declare const LANE_GAP_Y = 120;
148
+ export declare function laneOrder<R extends {
149
+ id: string;
150
+ data: Record<string, unknown>;
151
+ }>(releases: readonly R[]): R[];
152
+ export declare function releaseLane(releases: readonly LaneRelease[], boxes: readonly LaneBox[]): LanePlace[];
153
+ export declare function layoutReleaseLane(nodes: ReadonlyArray<GraphNode>, boxesOf: (n: GraphNode) => LaneBox[]): {
154
+ positions: Map<string, {
155
+ x: number;
156
+ y: number;
157
+ }>;
158
+ y: number | null;
159
+ };
@@ -0,0 +1,313 @@
1
+ export const RELEASE_SUBTYPE = 'release';
2
+ export const RELEASE_KEY = 'releaseId';
3
+ export const DONE_AT_KEY = 'doneAt';
4
+ export const RELEASE_SIZE = { width: 300, height: 184 };
5
+ const dataOf = (n) => (n?.data ?? {});
6
+ export const releaseIdOf = (n) => {
7
+ const v = dataOf(n)[RELEASE_KEY];
8
+ return typeof v === 'string' && v ? v : null;
9
+ };
10
+ export function releasesById(modules) {
11
+ const m = new Map();
12
+ for (const r of modules)
13
+ if (r.subtype === RELEASE_SUBTYPE)
14
+ m.set(r.id, r);
15
+ return m;
16
+ }
17
+ export const releaseIsOpen = (r) => r.data?.open === true;
18
+ export function isHiddenByRelease(n, releases) {
19
+ const rid = releaseIdOf(n);
20
+ if (!rid)
21
+ return false;
22
+ const r = releases.get(rid);
23
+ return !!r && !releaseIsOpen(r);
24
+ }
25
+ export function isFiled(n, releases) {
26
+ const rid = releaseIdOf(n);
27
+ return !!rid && releases.has(rid);
28
+ }
29
+ export function membersOf(releaseId, nodes) {
30
+ return nodes.filter(n => releaseIdOf(n) === releaseId);
31
+ }
32
+ export function subtreeOf(rootIds, nodes, edges = []) {
33
+ const byParent = new Map();
34
+ const byId = new Map();
35
+ for (const n of nodes) {
36
+ byId.set(n.id, n);
37
+ const p = dataOf(n).parentItemId;
38
+ if (typeof p === 'string' && p)
39
+ (byParent.get(p) ?? byParent.set(p, []).get(p)).push(n.id);
40
+ }
41
+ const okr = new Map();
42
+ for (const e of edges)
43
+ if ((e.dependencyType ?? e.type) === 'okr-link')
44
+ (okr.get(e.source) ?? okr.set(e.source, []).get(e.source)).push(e.target);
45
+ const out = new Set();
46
+ const stack = [...rootIds];
47
+ while (stack.length) {
48
+ const id = stack.pop();
49
+ if (out.has(id) || !byId.has(id))
50
+ continue;
51
+ out.add(id);
52
+ for (const c of byParent.get(id) ?? [])
53
+ stack.push(c);
54
+ const linked = dataOf(byId.get(id)).linkedEpicIds;
55
+ if (Array.isArray(linked))
56
+ for (const x of linked)
57
+ if (typeof x === 'string')
58
+ stack.push(x);
59
+ for (const t of okr.get(id) ?? [])
60
+ stack.push(t);
61
+ }
62
+ return out;
63
+ }
64
+ export const isDone = (n) => dataOf(n).state === 'done';
65
+ export function unfinishedIn(ids, nodes) {
66
+ const byId = new Map(nodes.map(n => [n.id, n]));
67
+ const out = [];
68
+ for (const id of ids) {
69
+ const n = byId.get(id);
70
+ if (n && !isDone(n))
71
+ out.push(id);
72
+ }
73
+ return out;
74
+ }
75
+ export function doneAtOf(n, now = Date.now()) {
76
+ const d = dataOf(n);
77
+ for (const v of [d.doneAt, d.stateChangedAt, d.updatedAt])
78
+ if (typeof v === 'number' && Number.isFinite(v) && v > 0)
79
+ return v;
80
+ return now;
81
+ }
82
+ export function fileSet(ids, releaseId, nodes, opts = {}) {
83
+ const byId = new Map(nodes.map(n => [n.id, n]));
84
+ const out = [];
85
+ for (const id of ids) {
86
+ const n = byId.get(id);
87
+ if (!n)
88
+ continue;
89
+ if (opts.onlyDone !== false && !isDone(n))
90
+ continue;
91
+ if (releaseIdOf(n) === releaseId)
92
+ continue;
93
+ out.push({ id, patch: { releaseId, doneAt: doneAtOf(n, opts.now) } });
94
+ }
95
+ return out;
96
+ }
97
+ export function unfileSet(ids, nodes) {
98
+ const byId = new Map(nodes.map(n => [n.id, n]));
99
+ const out = [];
100
+ for (const id of ids)
101
+ if (releaseIdOf(byId.get(id) ?? { id, data: {} }))
102
+ out.push({ id, patch: { releaseId: null } });
103
+ return out;
104
+ }
105
+ export function memberCounts(releaseId, nodes) {
106
+ const out = {};
107
+ for (const n of membersOf(releaseId, nodes)) {
108
+ const lvl = String(dataOf(n).hierarchyLevel ?? 'item');
109
+ out[lvl] = (out[lvl] ?? 0) + 1;
110
+ }
111
+ return out;
112
+ }
113
+ export function previousRelease(from, releases, exceptId) {
114
+ let best = null;
115
+ for (const r of releases) {
116
+ if (r.subtype !== RELEASE_SUBTYPE || r.id === exceptId)
117
+ continue;
118
+ const to = String(r.data?.to ?? '');
119
+ if (!to || to >= from)
120
+ continue;
121
+ if (!best || to > String(best.data.to ?? ''))
122
+ best = r;
123
+ }
124
+ return best;
125
+ }
126
+ const iso = (x) => `${x.getFullYear()}-${String(x.getMonth() + 1).padStart(2, '0')}-${String(x.getDate()).padStart(2, '0')}`;
127
+ export function weekWindow(d = new Date()) {
128
+ const day = (d.getDay() + 6) % 7;
129
+ const mon = new Date(d);
130
+ mon.setDate(d.getDate() - day);
131
+ const sun = new Date(mon);
132
+ sun.setDate(mon.getDate() + 6);
133
+ return { from: iso(mon), to: iso(sun) };
134
+ }
135
+ export function releaseOf(n, releases) {
136
+ const rid = releaseIdOf(n);
137
+ return (rid && releases.get(rid)) || null;
138
+ }
139
+ export function filedItemIds(nodes, releases) {
140
+ const out = new Set();
141
+ if (releases.size === 0)
142
+ return out;
143
+ for (const n of nodes)
144
+ if (isFiled(n, releases))
145
+ out.add(n.id);
146
+ return out;
147
+ }
148
+ export function releaseModulesFromGraphNodes(nodes) {
149
+ const out = new Map();
150
+ for (const raw of nodes) {
151
+ const n = raw;
152
+ if (!n || n.type !== 'rad-module' || typeof n.id !== 'string' || n.data?.subtype !== RELEASE_SUBTYPE)
153
+ continue;
154
+ const md = n.data.moduleData;
155
+ out.set(n.id, { id: n.id, subtype: RELEASE_SUBTYPE, data: md && typeof md === 'object' ? md : {} });
156
+ }
157
+ return out;
158
+ }
159
+ export function outTargetsOf(inst) {
160
+ const list = Array.isArray(inst.outConnectedNodeIds) ? inst.outConnectedNodeIds.filter(Boolean) : [];
161
+ const single = inst.outConnectedNodeId;
162
+ const ids = single && !list.includes(single) ? [single, ...list] : [...list];
163
+ return Array.from(new Set(ids));
164
+ }
165
+ const str = (v) => (typeof v === 'string' ? v : '');
166
+ export function chainWrites(id, all) {
167
+ const releases = all.filter(r => r.subtype === RELEASE_SUBTYPE);
168
+ const self = releases.find(r => r.id === id);
169
+ if (!self)
170
+ return [];
171
+ const isRelease = (x) => !!x && releases.some(r => r.id === x);
172
+ const out = new Map();
173
+ for (const r of releases) {
174
+ const t = outTargetsOf(r)[0];
175
+ out.set(r.id, isRelease(t) ? t : null);
176
+ }
177
+ const original = new Map(out);
178
+ const mine = out.get(id) ?? null;
179
+ for (const r of releases)
180
+ if (r.id !== id && out.get(r.id) === id)
181
+ out.set(r.id, mine === r.id ? null : mine);
182
+ out.set(id, null);
183
+ const from = str(self.data.from);
184
+ const prev = from ? previousRelease(from, releases, id) : null;
185
+ if (prev) {
186
+ const next = out.get(prev.id) ?? null;
187
+ out.set(prev.id, id);
188
+ out.set(id, next && next !== id ? next : null);
189
+ }
190
+ else {
191
+ const to = str(self.data.to) || from;
192
+ let next = null;
193
+ for (const r of releases) {
194
+ if (r.id === id)
195
+ continue;
196
+ const f = str(r.data.from);
197
+ if (!f || !to || f <= to)
198
+ continue;
199
+ if (!next || f < str(next.data.from))
200
+ next = r;
201
+ }
202
+ const fed = next && [...out.entries()].some(([k, v]) => k !== id && v === next.id);
203
+ if (next && !fed)
204
+ out.set(id, next.id);
205
+ }
206
+ const writes = [];
207
+ for (const [k, v] of out)
208
+ if ((original.get(k) ?? null) !== v)
209
+ writes.push({ id: k, out: v });
210
+ return writes;
211
+ }
212
+ export function chainReleasesFromGraphNodes(nodes) {
213
+ const out = [];
214
+ for (const n of nodes) {
215
+ if (n.type !== 'rad-module' || n.data?.subtype !== RELEASE_SUBTYPE)
216
+ continue;
217
+ const md = n.data.moduleData;
218
+ out.push({
219
+ id: n.id, subtype: RELEASE_SUBTYPE, data: md && typeof md === 'object' ? md : {},
220
+ ...(typeof n.data.outConnectedNodeId === 'string' ? { outConnectedNodeId: n.data.outConnectedNodeId } : {}),
221
+ ...(Array.isArray(n.data.outConnectedNodeIds) ? { outConnectedNodeIds: n.data.outConnectedNodeIds } : {}),
222
+ });
223
+ }
224
+ return out;
225
+ }
226
+ export function applyChainWrites(nodes, writes) {
227
+ if (!writes.length)
228
+ return [...nodes];
229
+ const w = new Map(writes.map(x => [x.id, x.out]));
230
+ return nodes.map(n => {
231
+ if (!w.has(n.id))
232
+ return n;
233
+ const { outConnectedNodeId: _o, outConnectedNodeIds: _l, ...rest } = n.data ?? {};
234
+ const out = w.get(n.id);
235
+ return { ...n, data: { ...rest, ...(out ? { outConnectedNodeId: out } : {}) } };
236
+ });
237
+ }
238
+ export function applyItemPatches(nodes, patches) {
239
+ if (!patches.length)
240
+ return [...nodes];
241
+ const p = new Map(patches.map(x => [x.id, x.patch]));
242
+ return nodes.map(n => (p.has(n.id) ? { ...n, data: { ...(n.data ?? {}), ...p.get(n.id) } } : n));
243
+ }
244
+ export function patchReleaseData(nodes, releaseId, patch) {
245
+ return nodes.map(n => (n.id === releaseId && n.type === 'rad-module' ? { ...n, data: { ...(n.data ?? {}), moduleData: { ...(n.data?.moduleData ?? {}), ...patch } } } : n));
246
+ }
247
+ export function newReleaseId(now = Date.now()) {
248
+ return `radmod-${RELEASE_SUBTYPE}-${now}-${Math.random().toString(36).slice(2, 8)}`;
249
+ }
250
+ const overlaps = (a, b) => Math.abs(a.x - b.x) < RELEASE_SIZE.width + 20 && Math.abs(a.y - b.y) < RELEASE_SIZE.height + 20;
251
+ export function placeRelease(nodes, from, prevId, xOfDate) {
252
+ const cards = nodes.filter(n => n.type === 'portfolio-item');
253
+ const rels = nodes.filter(n => n.type === 'rad-module' && n.data?.subtype === RELEASE_SUBTYPE);
254
+ const GAP = 80;
255
+ const topY = cards.length ? Math.min(...cards.map(n => n.position?.y ?? 0)) : 0;
256
+ const pos = { x: xOfDate(from), y: topY - RELEASE_SIZE.height - GAP };
257
+ while (rels.some(r => r.position && overlaps(r.position, pos)))
258
+ pos.y -= RELEASE_SIZE.height + 24;
259
+ const freeOf = (n) => n.freePosition ?? n.position ?? { x: 0, y: 0 };
260
+ const prev = prevId ? rels.find(r => r.id === prevId) : undefined;
261
+ let free;
262
+ if (prev)
263
+ free = { x: freeOf(prev).x + RELEASE_SIZE.width + 60, y: freeOf(prev).y };
264
+ else {
265
+ const minX = cards.length ? Math.min(...cards.map(n => freeOf(n).x)) : 0;
266
+ const minY = cards.length ? Math.min(...cards.map(n => freeOf(n).y)) : 0;
267
+ free = { x: minX, y: minY - RELEASE_SIZE.height - 120 };
268
+ }
269
+ while (rels.some(r => overlaps(freeOf(r), free)))
270
+ free.x += RELEASE_SIZE.width + 60;
271
+ return { position: pos, freePosition: free };
272
+ }
273
+ export function releaseNode(id, md, place) {
274
+ return {
275
+ id, type: 'rad-module',
276
+ position: { ...place.position },
277
+ freePosition: { ...place.freePosition },
278
+ data: { subtype: RELEASE_SUBTYPE, moduleData: { ...md }, width: RELEASE_SIZE.width, height: RELEASE_SIZE.height },
279
+ };
280
+ }
281
+ export const LANE_GAP_X = 48;
282
+ export const LANE_GAP_Y = 120;
283
+ export function laneOrder(releases) {
284
+ return [...releases].sort((a, b) => str(a.data.from).localeCompare(str(b.data.from))
285
+ || str(a.data.to).localeCompare(str(b.data.to))
286
+ || a.id.localeCompare(b.id));
287
+ }
288
+ export function releaseLane(releases, boxes) {
289
+ if (!releases.length)
290
+ return [];
291
+ const live = boxes.filter(b => [b.x, b.y, b.width, b.height].every(Number.isFinite));
292
+ const left = live.length ? Math.min(...live.map(b => b.x)) : Math.min(...releases.map(r => r.x));
293
+ const top = live.length ? Math.max(...live.map(b => b.y + b.height)) + LANE_GAP_Y : Math.min(...releases.map(r => r.y));
294
+ const out = [];
295
+ let x = Math.round(left);
296
+ const y = Math.round(top);
297
+ for (const r of laneOrder(releases)) {
298
+ out.push({ id: r.id, x, y });
299
+ x += Math.round(Math.max(r.width, 1)) + LANE_GAP_X;
300
+ }
301
+ return out;
302
+ }
303
+ export function layoutReleaseLane(nodes, boxesOf) {
304
+ const isRel = (n) => n.type === 'rad-module' && n.data?.subtype === RELEASE_SUBTYPE;
305
+ const at = (n) => n.freePosition ?? n.position ?? { x: 0, y: 0 };
306
+ const releases = nodes.filter(isRel).map(n => ({
307
+ id: n.id, ...at(n), width: Number(n.data?.width) || RELEASE_SIZE.width,
308
+ data: n.data?.moduleData && typeof n.data.moduleData === 'object' ? n.data.moduleData : {},
309
+ }));
310
+ const boxes = nodes.filter(n => !isRel(n) && (n.type === 'portfolio-item' || n.type === 'rad-module')).flatMap(boxesOf);
311
+ const places = releaseLane(releases, boxes);
312
+ return { positions: new Map(places.map(p => [p.id, { x: p.x, y: p.y }])), y: places[0]?.y ?? null };
313
+ }
@@ -0,0 +1,54 @@
1
+ export declare const INNER_LOOP_GAP = 16;
2
+ export declare const INNER_LOOP_Y_OFFSET = 40;
3
+ export declare const INNER_LOOP_COLLAPSED_HEIGHT = 44;
4
+ export declare const INNER_LOOP_FIELDS: readonly ["epicColor", "nodeDisplay", "artifact", "whyGraph", "description", "hypothesisStatement", "acceptanceCriteria", "bluf", "lbcProblem", "lbcBenefitHypothesis", "lbcLeadingIndicators", "lbcNfrs", "mvpDefinition", "outcomeMetrics", "owner", "tags", "confidenceVote", "approvalRating", "mockupSvg", "character", "location"];
5
+ export type InnerLoopField = (typeof INNER_LOOP_FIELDS)[number];
6
+ type FieldConfig = {
7
+ label: string;
8
+ width: number;
9
+ height: number;
10
+ levels?: readonly string[];
11
+ retired?: true;
12
+ };
13
+ export declare const SATELLITE_FIELDS: Record<InnerLoopField, FieldConfig>;
14
+ export declare const HOST_DEFAULT_HEIGHT: Record<string, number>;
15
+ export interface SatelliteHost {
16
+ hierarchyLevel?: string;
17
+ childCardType?: string;
18
+ innerLoopExpanded?: boolean;
19
+ innerLoopExtraFields?: string[];
20
+ innerLoopRemovedFields?: string[];
21
+ innerLoopSizes?: Record<string, {
22
+ width?: number;
23
+ height?: number;
24
+ }>;
25
+ subnodeCollapsed?: Record<string, boolean>;
26
+ [k: string]: unknown;
27
+ }
28
+ export declare const canHostInnerLoop: (d: SatelliteHost | undefined) => boolean;
29
+ export declare function activeSatelliteFields(d: SatelliteHost | undefined): InnerLoopField[];
30
+ export declare function satelliteItems(d: SatelliteHost | undefined): Array<{
31
+ field: InnerLoopField;
32
+ width: number;
33
+ height: number;
34
+ }>;
35
+ export declare const openSatellites: (d: SatelliteHost | undefined) => {
36
+ field: InnerLoopField;
37
+ width: number;
38
+ height: number;
39
+ }[];
40
+ export declare function satelliteRowWidth(d: SatelliteHost | undefined): number;
41
+ export declare function satelliteBandHeight(d: SatelliteHost | undefined, hostHeight?: number): number;
42
+ export declare function satellitePositions(d: SatelliteHost | undefined, host: {
43
+ x: number;
44
+ y: number;
45
+ width: number;
46
+ height: number;
47
+ }): Array<{
48
+ field: InnerLoopField;
49
+ x: number;
50
+ y: number;
51
+ width: number;
52
+ height: number;
53
+ }>;
54
+ export {};