import * as fs from "node:fs"; import { EMBEDDINGS_FILE, LEDGER_FILES, POSITIONS_FILE } from "../../config.ts"; import type { GraphSnapshot } from "../core/contracts.ts"; import type { GraphCostSummary } from "../core/cost-summary.ts"; import { graphCostSummary } from "../core/cost-summary.ts"; import { graphSnapshot } from "../core/graph.ts"; import { stores } from "./stores.ts"; /** * Server-side graph entry for the home view (GET /api/graph). A PURE READ: * the extraction pipeline (the sole graph writer) is triggered by quiz * answers in sessions (turn-service), never from here — the home view * reflects whatever the pipeline has merged so far. */ export function graphView(): GraphSnapshot { return graphSnapshot(stores); } /** The graph view's cost surface (GET /api/graph/costs — the 2026-09 cost * pill): fold every usage channel into stage totals + recent runs. A pure * read over the chat records (chat-front-door runs), the notes sources * (notes-front-door runs), and the naming ledger (graph-global runs); * dollars are inferred by the pure core fold (core/cost-summary.ts + * prices.ts) — nothing here stores or caches money. */ export function graphCosts(): GraphCostSummary { const chats = stores.chatStore .listChats() .flatMap((summary) => { const record = stores.chatStore.getChat(summary.id); return record?.extractions?.length ? [{ title: record.title ?? summary.id, extractions: record.extractions }] : []; }); const sources = stores.graphStore .readSources() .filter((s) => s.extractions?.length) .map((s) => ({ filePath: s.filePath, extractions: s.extractions! })); return graphCostSummary({ chats, sources, naming: stores.graphStore.readNamingRuns() }); } /** The graph's version stamp (session-deletion wave 6): the max mtime * across every graph data file — ledgers, positions, embeddings. Moves * on ANY graph write (merge append, settle, embed, or a deletion's * atomic rewrite), never moves on reads, and needs no bookkeeping of * its own: the fs IS the counter. Consumers poll it (the activity feed * carries it; the DELETE response returns it) to know clusters/layout * must refetch — the "graph changed underneath me" signal. */ export function graphRevision(): number { let revision = 0; for (const file of [ LEDGER_FILES.nodes, LEDGER_FILES.edges, LEDGER_FILES.events, LEDGER_FILES.mappings, LEDGER_FILES.sessions, LEDGER_FILES.coverage, LEDGER_FILES.clusterLabels, LEDGER_FILES.namingRuns, POSITIONS_FILE, EMBEDDINGS_FILE, ]) { try { revision = Math.max(revision, fs.statSync(file).mtimeMs); } catch { /* absent — nothing has ever written it */ } } return revision; }