function newIssueOperationId() { if (globalThis.crypto?.randomUUID) return globalThis.crypto.randomUUID(); if (globalThis.crypto?.getRandomValues) { const bytes = new Uint8Array(16); globalThis.crypto.getRandomValues(bytes); return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join(''); } return String(Date.now()) + '-' + Math.random().toString(16).slice(2); } function normalizeSharedContent(value = {}) { const clean = input => String(input || '').replace(/\s+/g, ' ').trim(); const text = String(value.text || '').trim().slice(0, 9500); const textSummary = clean(text); const sentence = (textSummary.match(/^.{1,80}?[.!?](?:\s|$)/)?.[0] || textSummary.slice(0, 80)).trim(); const title = (clean(value.title) || sentence).slice(0, 240); const url = clean(value.url).slice(0, 2000); const body = text && url && text.includes(url) ? text : [text, url].filter(Boolean).join('\n\n'); return { title, body }; } function createIssueCapture({ fetchJson, storage, createOperationId = newIssueOperationId }) { const storageKey = 'stackchain.issue-capture.v1'; const sharedStorageKey = 'stackchain.issue-share.v1'; let pending = null; const safeLabelIds = value => Array.from(new Set( (Array.isArray(value) ? value : []).filter(id => Number.isInteger(id) && id > 0) )).slice(0, 20); const emptyDraft = () => ({ repository: '', title: '', body: '', labelIds: [] }); const safeMilestoneId = value => Number.isInteger(Number(value)) && Number(value) > 0 ? Number(value) : null; const safeDueDate = value => /^\d{4}-\d{2}-\d{2}$/.test(String(value || '')) ? String(value) : ''; function loadStored() { try { const parsed = JSON.parse(storage.getItem(storageKey) || 'null'); if (!parsed || typeof parsed !== 'object') return {...emptyDraft(), operationId: ''}; const draft = { repository: String(parsed.repository || ''), title: String(parsed.title || ''), body: String(parsed.body || ''), labelIds: safeLabelIds(parsed.labelIds), operationId: String(parsed.operationId || '').slice(0, 128), }; const milestoneId = safeMilestoneId(parsed.milestoneId); const dueDate = safeDueDate(parsed.dueDate); if (milestoneId !== null) draft.milestoneId = milestoneId; if (dueDate) draft.dueDate = dueDate; return draft; } catch (_error) { return {...emptyDraft(), operationId: ''}; } } function writeStored(record) { try { storage.setItem(storageKey, JSON.stringify(record)); } catch (_error) { /* Keep the form as the in-memory fallback. */ } } function saveDraft(draft) { const previous = loadStored(); const safe = { repository: String(draft?.repository || ''), title: String(draft?.title || ''), body: String(draft?.body || ''), labelIds: safeLabelIds(draft?.labelIds), }; const milestoneId = safeMilestoneId(draft?.milestoneId); const dueDate = safeDueDate(draft?.dueDate); if (milestoneId !== null) safe.milestoneId = milestoneId; if (dueDate) safe.dueDate = dueDate; const unchanged = ['repository', 'title', 'body', 'milestoneId', 'dueDate'] .every(key => (previous[key] || '') === (safe[key] || '')) && JSON.stringify(previous.labelIds) === JSON.stringify(safe.labelIds); writeStored({...safe, operationId: unchanged ? previous.operationId : ''}); return safe; } function loadDraft() { const {operationId: _operationId, ...draft} = loadStored(); return draft; } function clearDraft() { try { storage.removeItem(storageKey); } catch (_error) { /* Confirmed creation remains authoritative. */ } } function pendingSharedContent() { try { const parsed = JSON.parse(storage.getItem(sharedStorageKey) || 'null'); if (!parsed || typeof parsed !== 'object') return null; const shared = normalizeSharedContent({title: parsed.title, text: parsed.body}); return shared.title || shared.body ? shared : null; } catch (_error) { return null; } } function acceptSharedContent() { const shared = pendingSharedContent(); if (!shared) return loadDraft(); const accepted = saveDraft({...loadDraft(), ...shared}); try { storage.removeItem(sharedStorageKey); } catch (_error) { /* Accepted content is already persisted as the issue draft. */ } return accepted; } function discardSharedContent() { try { storage.removeItem(sharedStorageKey); } catch (_error) { /* The existing issue draft remains authoritative. */ } } function stageSharedContent(value) { const shared = normalizeSharedContent(value); if (!shared.title && !shared.body) return {status: 'empty'}; try { storage.setItem(sharedStorageKey, JSON.stringify(shared)); } catch (_error) { /* The caller can still use the in-page share payload. */ } const existing = loadDraft(); if (existing.title || existing.body) return {status: 'conflict'}; acceptSharedContent(); return {status: 'ready'}; } function loadLabels(repository) { const encoded = String(repository || '').split('/').map(encodeURIComponent).join('/'); const priorities = new Set(['p0', 'priority-high', 'critical']); return fetchJson('api/v1/repos/' + encoded + '/labels').then(labels => (Array.isArray(labels) ? labels : []).slice().sort((left, right) => { const leftPriority = priorities.has(String(left?.name || '').toLowerCase()); const rightPriority = priorities.has(String(right?.name || '').toLowerCase()); return Number(rightPriority) - Number(leftPriority); }) ); } function loadMilestones(repository) { const encoded = String(repository || '').split('/').map(encodeURIComponent).join('/'); return fetchJson('api/v1/repos/' + encoded + '/milestones').then(milestones => Array.isArray(milestones) ? milestones : [] ); } function submit(draft) { if (pending) return pending; const saved = saveDraft(draft); const stored = loadStored(); const operationId = stored.operationId || String(createOperationId()).slice(0, 128); writeStored({...saved, operationId}); const repository = saved.repository.split('/').map(encodeURIComponent).join('/'); pending = fetchJson('api/v1/repos/' + repository + '/issues', { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'Idempotency-Key': operationId, }, body: JSON.stringify({ title: saved.title, body: saved.body, label_ids: saved.labelIds, ...(saved.milestoneId ? {milestone_id: saved.milestoneId} : {}), ...(saved.dueDate ? {due_date: saved.dueDate + 'T23:59:59Z'} : {}), }), }).then(issue => { clearDraft(); return issue; }).finally(() => { pending = null; }); return pending; } return { saveDraft, loadDraft, clearDraft, loadLabels, loadMilestones, submit, stageSharedContent, pendingSharedContent, acceptSharedContent, discardSharedContent, }; } createIssueCapture.normalizeSharedContent = normalizeSharedContent; if (typeof module !== 'undefined' && module.exports) module.exports = createIssueCapture;