function createHumanGates(options = {}) { const storage = options.storage || window.localStorage; const getLogin = options.getLogin || (() => ''); const getAccountKey = options.getAccountKey || getLogin; const isOnline = options.isOnline || (() => navigator.onLine); const location = options.location || window.location; const fetchJson = options.fetchJson; const nodes = options.nodes || {}; let queue = { pending_count: 0, items: [] }; let reviewSnapshot = []; let reviewIndex = -1; let loadedAccountKey = ''; let loadEpoch = 0; const decisionKeys = new Map(); let decisionFlight = null; let openFlight = null; let onChange = options.onChange; let historyItems = []; let historyNextCursor = null; let historyLoadedMore = false; let historyLoadError = ''; const escape = value => String(value ?? '').replace(/[&<>"']/g, character => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', })[character]); const cacheKey = () => 'stackchain.human-gates.v1:' + String(getAccountKey() || '').trim().toLowerCase(); const progressKey = item => 'stackchain.human-gate-review.v1:' + String(getAccountKey() || '').trim().toLowerCase() + ':' + item.id + ':' + item.revision; const setText = (node, value) => { if (node) node.textContent = value; }; const setHtml = (node, value) => { if (node) node.innerHTML = value; }; const publish = state => onChange?.(JSON.parse(JSON.stringify(queue)), state); function validSnapshot(value) { return value && Number.isInteger(value.pending_count) && Array.isArray(value.items) ? value : null; } function restore() { if (!getLogin()) return null; try { return validSnapshot(JSON.parse(storage.getItem(cacheKey()) || 'null')); } catch (_) { return null; } } function save(value) { if (!getLogin()) return; try { storage.setItem(cacheKey(), JSON.stringify(value)); } catch (_) {} } function restoreProgress(item) { if (!getLogin() || !item) return null; try { const value = JSON.parse(storage.getItem(progressKey(item)) || 'null'); if (!value || value.gate_id !== item.id || value.revision !== item.revision) return null; return value; } catch (_) { return null; } } function saveProgress(values = {}) { const item = current(); if (!getLogin() || !item) return false; const checklist = values.checklist || {}; const progress = { gate_id:item.id, revision:item.revision, checklist:Object.fromEntries(['exact_hash', 'artifacts_reviewed', 'provenance_reviewed'].map(key => [key, checklist[key] === true])), reason:String(values.reason || ''), override_reason:String(values.override_reason || ''), }; try { storage.setItem(progressKey(item), JSON.stringify(progress)); return true; } catch (_) { return false; } } function valuesFromDetail() { const checklist = Object.fromEntries(Array.from(nodes.detail?.querySelectorAll?.('[data-gate-checklist]') || []).map(input => [input.dataset.gateChecklist, input.checked])); return { checklist, reason:nodes.detail?.querySelector?.('[data-gate-reason]')?.value || '', override_reason:nodes.detail?.querySelector?.('[data-gate-override]')?.value || '', }; } function updateReadiness(values = valuesFromDetail()) { const completed = ['exact_hash', 'artifacts_reviewed', 'provenance_reviewed'] .filter(key => values.checklist?.[key] === true).length; setText(nodes.detail?.querySelector?.('[data-gate-readiness]'), completed + ' of 3 confirmations complete'); } function captureProgress() { if (!nodes.detail?.querySelectorAll) return false; const values = valuesFromDetail(); updateReadiness(values); return saveProgress(values); } nodes.detail?.addEventListener?.('input', captureProgress); nodes.detail?.addEventListener?.('change', captureProgress); function render() { setText(nodes.count, String(queue.pending_count)); if (!queue.pending_count) { setHtml(nodes.list, '
Inbox zeroNo release candidates need your decision.
'); setText(nodes.status, 'Human Gates inbox zero.'); return; } setText(nodes.status, queue.pending_count + (queue.pending_count === 1 ? ' gate pending.' : ' gates pending.')); setHtml(nodes.list, queue.items.map(item => '' ).join('')); } function setView(view) { nodes.pendingTab?.setAttribute?.('aria-pressed', view === 'pending' ? 'true' : 'false'); nodes.historyTab?.setAttribute?.('aria-pressed', view === 'history' ? 'true' : 'false'); } function renderHistory(preserveDetail = false) { setView('history'); if (historyNextCursor) { setText(nodes.status, historyItems.length + ' Human Gate decisions loaded. Older decisions are available.'); } else if (historyLoadedMore) { setText(nodes.status, 'All ' + historyItems.length + ' Human Gate decisions loaded.'); } else { setText(nodes.status, historyItems.length + (historyItems.length === 1 ? ' past Human Gate decision.' : ' past Human Gate decisions.')); } if (!historyItems.length) { setHtml(nodes.list, '
No decision historyReleased and held candidates will appear here.
'); setHtml(nodes.detail, ''); return; } setHtml(nodes.list, historyItems.map(item => '' ).join('') + (historyNextCursor ? '' : '')); Array.from(nodes.list?.querySelectorAll?.('[data-human-gate-history-id]') || []).forEach(card => { card.addEventListener('click', () => selectHistory(card.dataset.humanGateHistoryId).catch(error => { setText(nodes.status, error.message || 'Human Gate history is unavailable.'); })); }); nodes.list?.querySelector?.('[data-human-gate-history-more]')?.addEventListener?.('click', () => { loadMoreHistory().catch(error => setText(nodes.status, error.message || 'Older Human Gate decisions are unavailable.')); }); if (!preserveDetail) setHtml(nodes.detail, '
Decision historyOpen a candidate to review its durable receipt.
'); } async function showHistory() { if (!isOnline()) throw new Error('Human Gate history requires an online connection.'); if (!String(getLogin() || '').trim()) throw new Error('Authenticated account identity is required.'); const result = validSnapshot(await fetchJson('api/v1/human-gates?state=history&limit=20')); if (!result) throw new Error('Human Gate history response is invalid.'); historyItems = result.items; historyNextCursor = result.next_cursor || null; historyLoadedMore = false; historyLoadError = ''; renderHistory(); return JSON.parse(JSON.stringify(historyItems)); } async function loadMoreHistory() { if (!historyNextCursor) return JSON.parse(JSON.stringify(historyItems)); if (!isOnline()) throw new Error('Human Gate history requires an online connection.'); const cursor = historyNextCursor; let result; try { result = validSnapshot(await fetchJson( 'api/v1/human-gates?state=history&limit=20&cursor=' + encodeURIComponent(cursor) )); if (!result) throw new Error('Human Gate history response is invalid.'); } catch (error) { historyLoadError = error?.message || 'Older Human Gate decisions are unavailable.'; renderHistory(true); setText(nodes.status, historyLoadError + ' Loaded decisions are still available.'); throw error; } const known = new Set(historyItems.map(item => item.id)); historyItems.push(...result.items.filter(item => !known.has(item.id))); historyNextCursor = result.next_cursor || null; historyLoadedMore = true; historyLoadError = ''; renderHistory(true); return JSON.parse(JSON.stringify(historyItems)); } async function selectHistory(gateId) { const summary = historyItems.find(item => item.id === gateId); if (!summary) throw new Error('Gate is not in the current history.'); const detail = await fetchJson('api/v1/human-gates/' + encodeURIComponent(gateId)); if (!detail || detail.id !== gateId) throw new Error('Gate history detail is invalid.'); let receipt = null; if (detail.receipt_id) receipt = await fetchJson('api/v1/human-gate-receipts/' + encodeURIComponent(detail.receipt_id)); const checklist = receipt?.checklist || {}; const confirmations = ['exact_hash', 'artifacts_reviewed', 'provenance_reviewed'] .filter(key => checklist[key] === true).map(key => '
  • ' + escape(key.replaceAll('_', ' ')) + '
  • ').join(''); setHtml(nodes.detail, '

    ' + escape(detail.state.toUpperCase()) + '

    ' + '

    ' + escape(detail.title) + '

    Project ' + escape(detail.project) + '

    ' + '

    Exact candidate ' + escape(detail.candidate_hash) + '

    ' + (receipt ? '

    Decision receipt ' + escape(receipt.receipt_id) + '

    ' + '

    Decided ' + escape(new Date(Number(receipt.decided_at) * 1000).toLocaleString()) + '

    ' + (receipt.reason ? '

    Reason

    ' + escape(receipt.reason) + '

    ' : '') + (receipt.override_reason ? '

    Override

    ' + escape(receipt.override_reason) + '

    ' : '') + (confirmations ? '

    Confirmed

    ' : '') : '

    No decision receipt exists because this candidate was superseded.

    ') + '
    '); return {detail, receipt}; } function showPending() { setView('pending'); render(); renderDetail(current()); return JSON.parse(JSON.stringify(queue)); } function renderDetail(item) { if (!item) { setHtml(nodes.detail, '
    Inbox zeroFixed review snapshot complete.
    '); return; } const progress = restoreProgress(item) || {checklist:{}, reason:'', override_reason:''}; const checked = key => progress.checklist?.[key] === true ? ' checked' : ''; const checks = (item.checks || []).map(check => '
  • ' + escape(check.name) + ' · ' + escape(check.state) + (check.required ? ' · required' : '') + '
  • ' ).join(''); const artifacts = (item.artifacts || []).map(artifact => '
  • ' + escape(artifact.name) + '
  • ').join(''); const links = (item.links || []).map(link => '
  • ' + escape(link.label) + '
  • ').join(''); const provenance = Object.entries(item.provenance || {}).map(([key, value]) => '
  • ' + escape(key) + ' · ' + escape(value) + '
  • ').join(''); const history = (item.history || []).map(event => '
  • ' + escape(event.action) + ' · ' + escape(event.at) + '
  • ').join(''); setHtml(nodes.detail, '

    ' + escape(item.title) + '

    ' + '

    Project ' + escape(item.project) + '

    ' + '

    Exact candidate ' + escape(item.candidate_hash) + '

    ' + '

    Score ' + escape(item.score?.value ?? 'not supplied') + ' · ' + escape(item.score?.provenance || '') + '

    ' + '

    Artifacts

    Links

    ' + '

    Checks

    Provenance

    ' + '

    History

    ' + '' + '' + '' + '' + '' + '
    ' + '0 of 3 confirmations complete' + '
    ' + '
    ' + '
    ' ); updateReadiness(); } async function load() { const epoch = ++loadEpoch; const accountKey = String(getAccountKey() || '').trim().toLowerCase(); if (accountKey !== loadedAccountKey) { loadedAccountKey = accountKey; queue = { pending_count: 0, items: [] }; reviewSnapshot = []; reviewIndex = -1; render(); } const cached = restore(); if (cached) { queue = cached; render(); publish({available:true, authoritative:false, cached:true}); } try { const live = validSnapshot(await fetchJson('api/v1/human-gates')); if (epoch !== loadEpoch || accountKey !== loadedAccountKey) return queue; if (!live) throw new Error('Human Gates response is invalid.'); queue = { pending_count: live.pending_count, items: live.items.slice() }; save(queue); render(); publish({available:true, authoritative:true}); return queue; } catch (error) { if (epoch !== loadEpoch || accountKey !== loadedAccountKey) return queue; if (!cached) { publish({available:false, authoritative:false}); throw error; } setText(nodes.status, 'Offline cached gate list · reconnect before deciding.'); return queue; } } function reviewNext() { if (reviewIndex < 0) { reviewSnapshot = queue.items.slice(); reviewIndex = 0; } const item = reviewSnapshot[reviewIndex] || null; renderDetail(item); if (item) { const index = reviewIndex; fetchJson('api/v1/human-gates/' + encodeURIComponent(item.id)).then(detail => { if (!detail || detail.id !== item.id) throw new Error('Gate detail is invalid.'); if (reviewIndex !== index || reviewSnapshot[index]?.id !== item.id) return; reviewSnapshot[index] = detail; renderDetail(detail); }).catch(() => { setText(nodes.status, 'Gate detail is unavailable. Retry while online.'); }); } return item; } function select(gateId) { if (reviewIndex < 0) reviewSnapshot = queue.items.slice(); const index = reviewSnapshot.findIndex(item => item.id === gateId); if (index < 0) throw new Error('Gate is not in the current review snapshot.'); reviewIndex = index; return reviewNext(); } function current() { return reviewIndex < 0 ? null : (reviewSnapshot[reviewIndex] || null); } function idempotencyKey(item, decision, payload) { const operation = item.id + ':' + item.revision + ':' + decision + ':' + JSON.stringify(payload); if (decisionKeys.has(operation)) return { operation, key: decisionKeys.get(operation) }; const nonce = globalThis.crypto?.randomUUID?.() || (Date.now().toString(36) + '-' + Math.random().toString(36).slice(2)); const key = 'human-gate:' + item.id + ':' + item.revision + ':' + decision + ':' + nonce; decisionKeys.set(operation, key); return { operation, key }; } function decisionError(message, selector) { const error = new Error(message); error.targetSelector = selector; return error; } async function decideAndNext(decision, values = {}) { const item = current(); if (!item) throw new Error('No gate is selected.'); if (!isOnline()) throw new Error('Human Gate decisions require an online connection.'); if (!String(getLogin() || '').trim()) throw new Error('Authenticated account identity is required.'); const checklist = values.checklist || {}; const complete = ['exact_hash', 'artifacts_reviewed', 'provenance_reviewed'].every(key => checklist[key] === true); if (decision === 'release' && !complete) { const firstMissing = ['exact_hash', 'artifacts_reviewed', 'provenance_reviewed'].find(key => checklist[key] !== true); throw decisionError('Complete the release checklist before deciding.', '[data-gate-checklist="' + firstMissing + '"]'); } const unmet = (item.checks || []).filter(check => check.required && check.state !== 'success'); if (decision === 'release' && unmet.length && !String(values.override_reason || '').trim()) { const names = unmet.map(check => String(check.name || 'Unnamed check')).join(', '); throw decisionError('An explicit override reason is required for unmet required checks: ' + names + '.', '[data-gate-override]'); } if (decision === 'hold' && !String(values.reason || '').trim()) { throw decisionError('A hold reason is required.', '[data-gate-reason]'); } const payload = { expected_revision: item.revision, decision, reason: String(values.reason || '').trim(), override_reason: String(values.override_reason || '').trim(), checklist, }; if (decisionFlight) return decisionFlight; const operation = (async () => { const decisionKey = idempotencyKey(item, decision, payload); const receipt = await fetchJson('api/v1/human-gates/' + encodeURIComponent(item.id) + '/decision', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Idempotency-Key': decisionKey.key }, body: JSON.stringify(payload), }); decisionKeys.delete(decisionKey.operation); try { storage.removeItem?.(progressKey(item)); } catch (_) {} queue.items = queue.items.filter(candidate => candidate.id !== item.id); queue.pending_count = Math.max(0, queue.pending_count - 1); save(queue); render(); publish({available:true, authoritative:true, decision:true}); reviewIndex += 1; const next = current(); if (next) reviewNext(); else renderDetail(null); setText(nodes.status, next ? (decision === 'release' ? 'Released. Reviewing next gate.' : 'Held. Reviewing next gate.') : 'Decision saved. Human Gates review snapshot complete.'); return { receipt, next }; })(); decisionFlight = operation; try { return await operation; } finally { if (decisionFlight === operation) decisionFlight = null; } } async function submitDecision(decision) { const values = valuesFromDetail(); updateReadiness(values); const errorNode = nodes.detail?.querySelector?.('[data-gate-error]'); if (errorNode) { errorNode.textContent = ''; errorNode.hidden = true; } const buttons = Array.from(nodes.detail?.querySelectorAll?.('[data-gate-decision]') || []); buttons.forEach(button => { button.disabled = true; }); try { return await decideAndNext(decision, values); } catch (error) { if (errorNode) { errorNode.textContent = error?.message || 'The decision could not be saved.'; errorNode.hidden = false; } let target = error?.targetSelector && nodes.detail?.querySelector?.(error.targetSelector); if (!target && error?.targetSelector?.startsWith('[data-gate-checklist=')) { const key = error.targetSelector.match(/"([^"]+)"/)?.[1]; target = Array.from(nodes.detail?.querySelectorAll?.('[data-gate-checklist]') || []) .find(input => input.dataset.gateChecklist === key); } target?.scrollIntoView?.({block:'center', behavior:'smooth'}); target?.focus?.({preventScroll:true}); throw error; } finally { buttons.forEach(button => { button.disabled = false; }); } } function open() { location.hash = '#/my-work/human-gates'; if (nodes.panel) nodes.panel.hidden = false; if (openFlight) return openFlight; const operation = (async () => { setView('pending'); await load(); reviewSnapshot = queue.items.slice(); reviewIndex = 0; return reviewNext(); })(); openFlight = operation; const clearFlight = () => { if (openFlight === operation) openFlight = null; }; operation.then(clearFlight, clearFlight); return operation; } return { load, open, reviewNext, select, showHistory, loadMoreHistory, selectHistory, showPending, decideAndNext, submitDecision, current, saveProgress, setOnChange(callback) { onChange = callback; }, restoreCached: restore, snapshot: () => JSON.parse(JSON.stringify(queue)), route: () => location.hash, }; } if (typeof module !== 'undefined') module.exports = createHumanGates; if (typeof window !== 'undefined') window.createHumanGates = createHumanGates;