function createTodayWork({ storage, getLogin, limit = 5 }) { const prefix = 'stackchain.today-work.v1.'; const planningPrefix = 'stackchain.today-planning.v1.'; function identity(item) { if (!item) return ''; const kind = item.is_review ? 'review' : (item.kind || 'work'); const number = Number.isInteger(item.number) ? item.number : ''; const notification = Number.isInteger(item.notification_id) ? item.notification_id : ''; return [kind, item.repository || '', number, notification].join(':'); } function ownerKey(keyPrefix) { const login = String(getLogin?.() || '').trim().toLowerCase(); return login ? keyPrefix + encodeURIComponent(login) : ''; } function storageKey() { return ownerKey(prefix); } function planningKey() { return ownerKey(planningPrefix); } function read() { const key = storageKey(); if (!key || !storage) return []; try { const value = JSON.parse(storage.getItem(key) || '[]'); return Array.isArray(value) ? value.filter(id => typeof id === 'string' && id) : []; } catch (_error) { return []; } } function write(ids) { const key = storageKey(); if (!key || !storage) return false; try { if (ids.length) storage.setItem(key, JSON.stringify(ids)); else storage.removeItem(key); return true; } catch (_error) { return false; } } function planning() { const key = planningKey(); if (!key || !storage) return { capacity_minutes: null, estimates: {} }; try { const value = JSON.parse(storage.getItem(key) || 'null'); const capacity = Number.isInteger(value?.capacity_minutes) && value.capacity_minutes > 0 ? value.capacity_minutes : null; const ids = new Set(read()); const estimates = Object.fromEntries(Object.entries(value?.estimates || {}).filter(([id, minutes]) => ids.has(id) && Number.isInteger(minutes) && minutes > 0 )); return { capacity_minutes: capacity, estimates }; } catch (_error) { return { capacity_minutes: null, estimates: {} }; } } function replacePlanning(value) { const key = planningKey(); if (!key || !storage) return false; const ids = new Set(read()); const capacity = Number.isInteger(value?.capacity_minutes) && value.capacity_minutes > 0 ? value.capacity_minutes : null; const estimates = Object.fromEntries(Object.entries(value?.estimates || {}).filter(([id, minutes]) => ids.has(id) && Number.isInteger(minutes) && minutes > 0 )); try { storage.setItem(key, JSON.stringify({ capacity_minutes: capacity, estimates })); return true; } catch (_error) { return false; } } function add(item) { if (!storageKey()) return 'unavailable'; const id = identity(item); if (!id) return 'unavailable'; const ids = read(); if (ids.includes(id)) return 'exists'; if (ids.length >= limit) return 'full'; ids.push(id); return write(ids) ? 'added' : 'unavailable'; } function addMany(items) { if (!storageKey()) return { status: 'unavailable', ids: [] }; const current = read(); const additions = []; for (const item of items || []) { const id = identity(item); if (!id) return { status: 'unavailable', ids: [] }; if (!current.includes(id) && !additions.includes(id)) additions.push(id); } if (!additions.length) return { status: 'exists', ids: [] }; if (current.length + additions.length > limit) return { status: 'full', ids: [] }; return write(current.concat(additions)) ? { status: 'added', ids: additions } : { status: 'unavailable', ids: [] }; } function replace(ids) { const unique = []; for (const id of ids || []) { if (typeof id === 'string' && id && !unique.includes(id) && unique.length < limit) { unique.push(id); } } const saved = write(unique); if (saved && planningKey()) replacePlanning(planning()); return saved; } function remove(item) { const id = identity(item); const ids = read(); const next = ids.filter(candidate => candidate !== id); const saved = next.length !== ids.length && write(next); if (saved) replacePlanning(planning()); return saved; } function capture(item) { const id = identity(item); const ids = read(); const index = ids.indexOf(id); if (index < 0) return null; return { id, index, ids: ids.slice(), plan: planning() }; } function restore(snapshot) { if (!snapshot || typeof snapshot.id !== 'string' || !Array.isArray(snapshot.ids)) return 'unavailable'; const current = read(); const expected = snapshot.ids.filter(id => id !== snapshot.id); if (current.includes(snapshot.id)) return 'changed'; if (current.length >= limit) return 'full'; if (current.length !== expected.length || current.some((id, index) => id !== expected[index])) return 'changed'; if (!write(snapshot.ids)) return 'unavailable'; if (!replacePlanning(snapshot.plan)) { write(current); return 'unavailable'; } return 'restored'; } function move(item, direction) { const ids = read(); const index = ids.indexOf(identity(item)); const target = direction === 'up' ? index - 1 : direction === 'down' ? index + 1 : -1; if (index < 0 || target < 0 || target >= ids.length) return false; [ids[index], ids[target]] = [ids[target], ids[index]]; return write(ids); } function reconcile(items, { pruneMissing = false, onPrune } = {}) { const available = new Map((items || []).map(item => [identity(item), item])); const ids = read(); const retained = pruneMissing ? ids.filter(id => available.has(id)) : ids; const retired = ids.filter(id => !retained.includes(id)); if (retired.length) { const reported = onPrune?.(retired); if (reported !== false) { write(retained); replacePlanning(planning()); } } return retained.flatMap(id => available.has(id) ? [available.get(id)] : []); } function contains(item) { return read().includes(identity(item)); } function position(item) { const ids = read(); const index = ids.indexOf(identity(item)); return { can_up: index > 0, can_down: index >= 0 && index < ids.length - 1, }; } function runway(items, currentIndex = 0) { const plan = planning(); const minutes = (items || []).slice(Math.max(0, currentIndex)).map(item => plan.estimates[identity(item)] || null); const total = values => values.some(value => value === null) ? null : values.reduce((sum, value) => sum + value, 0); return { current_minutes: minutes[0] || null, remaining_minutes: total(minutes), future_minutes: total(minutes.slice(1)), capacity_minutes: plan.capacity_minutes, }; } return { identity, read, replace, planning, replacePlanning, runway, add, addMany, remove, capture, restore, move, reconcile, contains, position, limit }; } if (typeof module !== 'undefined' && module.exports) module.exports = createTodayWork;