Files
brain/tools/oq-ledger.mjs
T
Дмитрий 7c282242c2 feat(oq): журнал охотника — формат/разбор/replay/активная-сессия
Узел «журнал» скила surfacing-open-questions (спека v6, раздел zhurnal):
formatEventLine/parseEventLine, replay с отбросом битой хвостовой строки,
activeSessionOf (порог протухания 30 мин — одна активная сессия на тему).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 08:18:31 +03:00

40 lines
2.1 KiB
JavaScript

/**
* oq-ledger — журнал «охотника за открытыми вопросами» (surfacing-open-questions, раздел спеки zhurnal).
* Append-only человекочитаемый .md-журнал: одна строка на событие, с провенансом источника.
* Чистое ядро (формат / разбор / replay / активная-сессия); запись на диск делает вызывающий
* скил через окно карантина docs/observer/questions/open-questions-<slug>.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;
}