/** * oq-ledger — журнал «охотника за открытыми вопросами» (surfacing-open-questions, раздел спеки zhurnal). * Append-only человекочитаемый .md-журнал: одна строка на событие, с провенансом источника. * Чистое ядро (формат / разбор / replay / активная-сессия); запись на диск делает вызывающий * скил через окно карантина docs/observer/questions/open-questions-.md. */ const SEP = ' · '; /** Событие → одна .md-строка с провенансом и сессией. */ export function formatEventLine({ at, kind, qid, text, provenance, session }) { return `- [${at}] ${kind} ${qid}: ${text}${SEP}источник: ${provenance}${SEP}сессия:${session}`; } /** Обратная операция formatEventLine; не-наша строка → null. */ export function parseEventLine(line) { const m = /^- \[([^\]]+)\] (\S+) (\S+): (.*?) · источник: (.*?) · сессия:(.*)$/.exec(String(line)); if (!m) return null; return { at: m[1], kind: m[2], qid: m[3], text: m[4], provenance: m[5], session: m[6] }; } /** Сырьё журнала → события; непарсящаяся (битый хвост/обрыв) строка отбрасывается (терпимый replay). */ export function replay(raw) { const lines = String(raw).split('\n').filter((l) => l.trim() !== ''); const out = []; for (const line of lines) { const ev = parseEventLine(line); if (ev) out.push(ev); } return out; } /** Активная сессия темы или null: последняя запись свежее порога → её session; иначе свободно. */ export function activeSessionOf(raw, { nowMs, staleMinutes = 30 } = {}) { const events = replay(raw); if (events.length === 0) return null; const last = events[events.length - 1]; const ageMin = (nowMs - Date.parse(last.at)) / 60000; return ageMin <= staleMinutes ? last.session : null; }