/** Pure capture guards; fixtures must never contain real cookies or notes. */ export function normalizeBubbleRef(value) { if (typeof value !== 'string') return value; if (!value.startsWith('LOOKUP__')) return value; return value.match(/[0-9]+x[0-9]+/)?.[0] || value; } export function assertSessionFresh(state, now = Date.now() / 1000) { if ((state.cookies || []).some(c => c.name.endsWith('.sig') && c.expires > 0 && c.expires <= now)) { throw new Error('bubble_session_expired'); } } export function mergeCapturedRecord(existing, source, id) { return { ...(existing || {}), ...source, _id: normalizeBubbleRef(id) }; } export function selectTargetArticles(articles, rootId) { const root = articles[normalizeBubbleRef(rootId)]; const spaceId = normalizeBubbleRef(root?.space_custom_space); if (!root || !spaceId) throw new Error('target_space_identity_missing'); const selected = Object.values(articles).filter(a => normalizeBubbleRef(a.space_custom_space) === spaceId); if (selected.length <= 1) throw new Error('target_protocols_not_captured'); if (selected.some(a => !Object.hasOwn(a, 'content_text'))) { throw new Error('target_protocol_bodies_incomplete'); } return [spaceId, selected]; }