UNPKG

paqad-ai

Version:

Spec-driven development framework — AI agents that think before they type

242 lines (239 loc) • 7.16 kB
import { createRequire as __paqadCreateRequire } from 'module'; const require = __paqadCreateRequire(import.meta.url); import { PATHS } from "./chunk-IHMBVS6H.js"; import { init_esm_shims } from "./chunk-IDFRWBMN.js"; // src/session-ledger/ledger.ts init_esm_shims(); import { createHash } from "crypto"; import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, writeFileSync } from "fs"; import { dirname, join } from "path"; function sessionLedgerDir(docType, sessionId) { return join(PATHS.EVIDENCE_LEDGER_DIR, docType, sessionId); } function sessionLedgerPath(docType, sessionId, ordinal) { return join(sessionLedgerDir(docType, sessionId), `${ordinal}.jsonl`); } function sessionOpenPointerPath(docType, sessionId) { return join(sessionLedgerDir(docType, sessionId), ".open"); } function absDir(projectRoot, docType, sessionId) { return join(projectRoot, sessionLedgerDir(docType, sessionId)); } function highestOrdinal(dir) { let max = 0; let entries; try { entries = readdirSync(dir); } catch { return 0; } for (const entry of entries) { const match = /^(\d+)\.jsonl$/.exec(entry); if (match) { const n = Number(match[1]); if (n > max) { max = n; } } } return max; } function allocateOrdinal(projectRoot, docType, sessionId) { const dir = absDir(projectRoot, docType, sessionId); mkdirSync(dir, { recursive: true }); let ordinal = highestOrdinal(dir) + 1; for (; ; ) { const path = join(dir, `${ordinal}.jsonl`); try { closeSync(openSync(path, "wx")); writeFileSync(join(dir, ".open"), String(ordinal), "utf8"); return ordinal; } catch (error) { if (error.code === "EEXIST") { ordinal++; continue; } throw error; } } } function currentOrdinal(projectRoot, docType, sessionId) { try { const raw = readFileSync(join(projectRoot, sessionOpenPointerPath(docType, sessionId)), "utf8"); const n = Number(raw.trim()); return Number.isInteger(n) && n > 0 ? n : 0; } catch { return 0; } } var HASH_EXCLUDED_KEYS = /* @__PURE__ */ new Set(["ts", "recorded_at", "content_hash", "note"]); function computeSessionRowHash(row) { const identity = {}; for (const key of Object.keys(row).sort()) { if (!HASH_EXCLUDED_KEYS.has(key)) { identity[key] = row[key]; } } return createHash("sha256").update(JSON.stringify(identity)).digest("hex"); } function stampSessionRow(docType, sessionId, row, options = {}) { const now = options.now ?? (() => /* @__PURE__ */ new Date()); const base = { schema_version: options.schemaVersion ?? 1, doc_type: docType, session_id: sessionId, ...row }; const content_hash = computeSessionRowHash(base); const stamped = { ...base, ts: now().toISOString(), content_hash }; if (options.validate) { const errors = options.validate(stamped); if (errors.length > 0) { throw new Error(`Invalid ${docType} row: ${errors.join("; ")}`); } } return stamped; } function appendStampedRowToUnit(projectRoot, unitRelPath, stamped) { const abs = join(projectRoot, unitRelPath); mkdirSync(dirname(abs), { recursive: true }); appendFileSync(abs, `${JSON.stringify(stamped)} `, "utf8"); } function readUnitFile(projectRoot, unitRelPath) { return readJsonl(join(projectRoot, unitRelPath)); } function readUnitFileHealth(projectRoot, unitRelPath) { const abs = join(projectRoot, unitRelPath); const rows = readJsonl(abs); if (!existsSync(abs)) return { rows, torn_tail: false, torn_lines: 0 }; const lines = readFileSync(abs, "utf8").split("\n").filter((line) => line.trim() !== ""); const torn = lines.map((line) => { try { JSON.parse(line); return false; } catch { return true; } }); return { rows, torn_tail: torn.at(-1) === true, torn_lines: torn.filter(Boolean).length }; } function appendSessionEvent(projectRoot, docType, sessionId, ordinal, row, options = {}) { const stamped = stampSessionRow(docType, sessionId, row, options); appendStampedRowToUnit(projectRoot, sessionLedgerPath(docType, sessionId, ordinal), stamped); return stamped; } function openSessionDoc(projectRoot, docType, sessionId, openRow = {}, options = {}) { const ordinal = allocateOrdinal(projectRoot, docType, sessionId); appendSessionEvent( projectRoot, docType, sessionId, ordinal, { kind: "open", conversation_ordinal: ordinal, ...openRow }, options ); return { ordinal, path: sessionLedgerPath(docType, sessionId, ordinal) }; } function readSessionUnit(projectRoot, docType, sessionId, ordinal) { return readJsonl(join(projectRoot, sessionLedgerPath(docType, sessionId, ordinal))); } function readSessionDoc(projectRoot, docType, sessionId) { const dir = absDir(projectRoot, docType, sessionId); let entries; try { entries = readdirSync(dir); } catch { return []; } const ordinals = entries.map((entry) => /^(\d+)\.jsonl$/.exec(entry)).filter((match) => match !== null).map((match) => Number(match[1])).sort((a, b) => a - b); return ordinals.flatMap((ordinal) => readSessionUnit(projectRoot, docType, sessionId, ordinal)); } function listSessionIds(projectRoot, docType) { const dir = join(projectRoot, PATHS.EVIDENCE_LEDGER_DIR, docType); try { return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort(); } catch { return []; } } function readAllSessionRows(projectRoot, docType) { return listSessionIds(projectRoot, docType).flatMap( (sessionId) => readSessionDoc(projectRoot, docType, sessionId) ); } function foldByOrdinal(rows) { const byOrdinal = /* @__PURE__ */ new Map(); for (const row of rows) { const ordinal = typeof row.conversation_ordinal === "number" ? row.conversation_ordinal : 0; const bucket = byOrdinal.get(ordinal); if (bucket) { bucket.push(row); } else { byOrdinal.set(ordinal, [row]); } } return new Map([...byOrdinal.entries()].sort((a, b) => a[0] - b[0])); } function readJsonl(absPath) { if (!existsSync(absPath)) { return []; } const out = []; for (const line of readFileSync(absPath, "utf8").split("\n")) { const trimmed = line.trim(); if (trimmed.length === 0) { continue; } try { const parsed = JSON.parse(trimmed); if (isSessionLedgerRow(parsed)) { out.push(parsed); } } catch { } } return out; } function isSessionLedgerRow(value) { if (typeof value !== "object" || value === null) { return false; } const row = value; return typeof row.doc_type === "string" && typeof row.session_id === "string" && (typeof row.ts === "string" || typeof row.recorded_at === "string") && typeof row.content_hash === "string"; } export { sessionLedgerDir, allocateOrdinal, currentOrdinal, computeSessionRowHash, stampSessionRow, appendStampedRowToUnit, readUnitFile, readUnitFileHealth, appendSessionEvent, openSessionDoc, readSessionDoc, readAllSessionRows, foldByOrdinal };