/** Portable, opt-in presentation of an already authorized record read. */ import { boundRecordEvidence } from "./record-evidence"; export const COMPACT_CRM_ENTRY_BYTES = 14_000; const encoder = new TextEncoder(); type Row = Record; const row = (value: unknown): Row | undefined => value !== null && typeof value === "object" && !Array.isArray(value) ? (value as Row) : undefined; const pick = (value: Row | undefined, keys: string[]): Row => Object.fromEntries( keys.flatMap((key) => value?.[key] === undefined ? [] : [[key, value[key]]], ), ); const bytes = (value: string) => encoder.encode(value).byteLength; const serialized = (value: unknown) => JSON.stringify(value); const fits = (value: unknown, reserve = 0) => bytes(serialized(value)) + 1 + reserve <= COMPACT_CRM_ENTRY_BYTES; function clip(text: string, limit: number): string { if (bytes(text) <= limit) return text; let result = ""; let size = 3; // the ellipsis for (const point of text) { size += bytes(point); if (size > limit) break; result += point; } return `${result}…`; } /** Only recognized editor documents are decoded; ordinary JSON stays literal. */ function richText(value: unknown, omitted: () => void): unknown { let document = row(value); if (typeof value === "string" && value.trimStart().startsWith("{")) { try { document = row(JSON.parse(value)); } catch { return value; } } if (document?.type !== "doc" || !Array.isArray(document.content)) return value; const parts: string[] = []; let textBytes = 0; let visited = 0; let stopped = false; const append = (text: string) => { const room = 16_000 - textBytes; if (bytes(text) > room) { omitted(); stopped = true; text = room >= 3 ? clip(text, room) : ""; } parts.push(text); textBytes += bytes(text); }; // Iterator frames avoid spread argument limits and do not copy arbitrary // child arrays. Both malformed and valid editor documents have bounded work. const pending = [ { nodes: document.content as unknown[], index: 0, after: "" }, ]; while (pending.length && !stopped) { const frame = pending[pending.length - 1]; if (frame.index >= frame.nodes.length) { pending.pop(); append(frame.after); continue; } if (++visited > 10_000) { omitted(); break; } const next = frame.nodes[frame.index++]; const node = row(next); if (!node) { omitted(); continue; } if (typeof node.text === "string") { const link = Array.isArray(node.marks) ? node.marks.map(row).find((mark) => mark?.type === "link") : undefined; const href = row(link?.attrs)?.href; append(typeof href === "string" ? `[${node.text}](${href})` : node.text); } else if (node.type === "hardBreak") append("\n"); else if (node.type === "horizontalRule") append("\n---\n"); else if ( node.type === "image" && typeof row(node.attrs)?.src === "string" ) { const attrs = row(node.attrs); append( `[${typeof attrs?.alt === "string" ? attrs.alt : "Image"}](${attrs?.src})`, ); } else if ( node.type === "mention" && typeof row(node.attrs)?.label === "string" ) { append(String(row(node.attrs)?.label)); } else if ( node.type === "fileAttachment" && typeof row(node.attrs)?.path === "string" ) { const attrs = row(node.attrs); append( `[${typeof attrs?.name === "string" ? attrs.name : "Attachment"}](${attrs?.path})`, ); } else if (!Array.isArray(node.content)) { omitted(); append("[Unsupported editor content]"); } if (Array.isArray(node.content)) { pending.push({ nodes: node.content, index: 0, after: ["paragraph", "heading", "listItem", "tableRow"].includes( String(node.type), ) ? "\n" : "", }); } } return parts .join("") .replace(/\n{3,}/g, "\n\n") .trim(); } const shellWord = (value: string) => /^[a-zA-Z0-9_./:@-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`; /** Input must already contain its final URLs/reference envelope. The returned * string is the exact stdout payload; callers append only console.log's newline. */ export function serializeCompactCrmEntry( value: unknown, options: { objectName: string; entryId: string; withContext: boolean }, ): string { const entry = row(value); if (!entry) return serialized( value == null ? null : { ok: false, error: "compact_invalid_record", message: "Expected a CRM entry object.", }, ); const objectName = typeof entry.objectName === "string" ? entry.objectName : options.objectName; const entryId = typeof entry.id === "string" ? entry.id : options.entryId; const rawReadCommand = `dench crm entries get ${shellWord(objectName)} ${shellWord(entryId)}${options.withContext ? " --with-context" : ""} --json`; const metadata = { version: 1, truncated: false, clippedPaths: [] as string[], additionalClippedPaths: 0, omittedCounts: { fields: 0, activityItems: 0, notes: 0, values: 0, duplicateNotes: 0, }, rawReadCommand, }; const paths = new Set(); const changed = (path: string) => { metadata.truncated = true; if (paths.has(path)) return; paths.add(path); if (metadata.clippedPaths.length < 16) metadata.clippedPaths.push(clip(path, 96)); else metadata.additionalClippedPaths += 1; }; const pointer = (key: string) => key.replaceAll("~", "~0").replaceAll("/", "~1"); const bounded = ( input: unknown, path: string, limit = 800, depth = 0, ): unknown => { const text = richText(input, () => changed(path)); if (typeof text === "string") { const result = clip(text, limit); if (result !== text) changed(path); return result; } if (depth >= 3 && (Array.isArray(text) || row(text))) { changed(path); metadata.omittedCounts.values += 1; return null; } if (Array.isArray(text)) { if (text.length > 8) { changed(path); metadata.omittedCounts.values += text.length - 8; } return text .slice(0, 8) .map((item, index) => bounded(item, `${path}/${index}`, limit, depth + 1), ); } const object = row(text); if (!object) return text; const entries = Object.entries(object); if (entries.length > 8) { changed(path); metadata.omittedCounts.values += entries.length - 8; } return Object.fromEntries( entries .slice(0, 8) .map(([key, item]) => [ key, bounded(item, `${path}/${pointer(key)}`, limit, depth + 1), ]), ); }; const displayText = ( input: Row | undefined, path: string, keys: string[], ) => { if (!input) return input; const result = { ...input }; for (const key of keys) { if (typeof input[key] === "string") result[key] = bounded(input[key], `${path}/${key}`, 512); } return result; }; const actor = (input: unknown, path: string) => displayText( pick(row(input), ["kind", "displayName", "label", "userId", "isDench"]), path, ["displayName", "label"], ); const fields: Row = Object.create(null); const output: Row = { ...pick(entry, [ "id", "objectId", "objectName", "href", "url", "createdAt", "updatedAt", "calculationsTruncated", "references", "referenceCoverage", ]), fields, compact: metadata, }; const context = row(entry.context); const brief = row(context?.brief); const shared = row(brief?.shared); const briefOutput: Row = { ...pick(brief, [ "coverage", "hasMaterial", "enabled", "needsRefresh", "revision", "audience", ]), shared: shared ? pick(shared, ["status", "stale", "generatedAt", "checkedAt"]) : null, }; const activity = row(context?.recentActivity); const items: Row[] = []; const notes: Row[] = []; if (context) output.context = { brief: briefOutput, recentActivity: { ...pick(activity, ["hasMore", "contextLimited", "nextBoundary"]), items, }, notes, }; const tooLarge = () => { const error = { ok: false, error: "compact_metadata_too_large", message: "Identity, references or coverage exceed the compact read budget. Use the raw read.", rawReadCommand, }; // Do not echo arbitrarily large identifiers in the exceptional error either. return serialized( fits(error) ? error : { ok: false, error: error.error, message: error.message }, ); }; if (!fits(output)) return tooLarge(); const prepared = row(brief?.context); const evidence = row(prepared?.data); if (evidence) { // Reserve useful source evidence before optional fields/activity consume the // envelope. It contains facts (e.g. scheduled meetings) absent from the ledger. const room = Math.min( 6_000, COMPACT_CRM_ENTRY_BYTES - bytes(serialized(output)) - 1_800, ); if (room < 512) return tooLarge(); const bounded = boundRecordEvidence(evidence, room); briefOutput.context = { ...pick(prepared, ["assembledAt", "stale"]), data: bounded.data, }; if (bounded.truncated) changed("/context/brief/context/data"); } else if (brief?.context === null) briefOutput.context = null; if (!fits(output)) return tooLarge(); type Group = "fields" | "activityItems" | "notes" | "values"; const accepted: Array<{ remove: () => void; group: Group; path: string }> = []; const add = ( apply: () => void, remove: () => void, group: Group, path: string, ) => { apply(); if (fits(output, 1800)) { accepted.push({ remove, group, path }); return true; } remove(); metadata.omittedCounts[group] += 1; changed(path); return false; }; const fieldEntries = Object.entries(row(entry.fields) ?? {}); const short = ([, field]: [string, unknown]) => bytes(serialized(field)) <= 256; const addField = ([name, field]: [string, unknown]) => { const path = `/fields/${pointer(name)}`; const projected = bounded(field, path, 1200); add( () => { fields[name] = projected; }, () => { delete fields[name]; }, "fields", path, ); }; fieldEntries.filter(short).forEach(addField); const includedNotes = new Set(); const sourceItems = Array.isArray(activity?.items) ? activity.items : []; sourceItems.forEach((raw, index) => { const item = row(raw); if (!item) return; const path = `/context/recentActivity/items/${index}`; const projected = pick(item, ["type", "key", "at"]); const thread = row(item.thread); const comment = row(item.comment); const history = row(item.row); if (thread) { const digest = row(thread.digest); const open = row(thread.open); projected.thread = { ...displayText( pick(thread, [ "threadKey", "kind", "title", "latestAt", "firstAt", "messageCount", "messageCountCapped", "direction", "role", "latestFrom", ]), `${path}/thread`, ["title", "latestFrom"], ), ...(thread.context ? { context: displayText( row(thread.context), `${path}/thread/context`, ["label"], ), } : {}), ...(Array.isArray(thread.participants) ? { participants: thread.participants.map((participant, index) => displayText( row(participant), `${path}/thread/participants/${index}`, ["label"], ), ), } : {}), ...(open ? { open: { ...open, ...(open.emailThread ? { emailThread: displayText( row(open.emailThread), `${path}/thread/open/emailThread`, ["subject"], ), } : {}), }, } : {}), ...("summary" in thread ? { summary: bounded(thread.summary, `${path}/thread/summary`) } : {}), ...(digest ? { digest: bounded( pick(digest, ["requests", "commitments"]), `${path}/thread/digest`, 480, ), } : {}), }; } else if (comment) projected.comment = { ...pick(comment, [ "_id", "kind", "title", "createdAt", "editedAt", "deletedAt", "replyCount", ]), actor: actor(comment.actor, `${path}/comment/actor`), body: bounded(comment.body, `${path}/comment/body`), }; else if (history) projected.row = { ...displayText( pick(history, ["_id", "changeKind", "at", "isSystem", "entryLabel"]), `${path}/row`, ["entryLabel"], ), actor: actor(history.actor, `${path}/row/actor`), changes: bounded(history.changes, `${path}/row/changes`, 480), }; if ( add( () => { items.push(projected); }, () => { items.pop(); }, "activityItems", path, ) && typeof comment?._id === "string" ) includedNotes.add(comment._id); }); // A stale brief must never regain its withheld prose through compact mode. if (shared && shared.stale !== true) { for (const key of ["headline", "body", "facets"]) { if (shared[key] === undefined) continue; const target = briefOutput.shared as Row; const path = `/context/brief/shared/${key}`; const projected = bounded(shared[key], path, key === "body" ? 1800 : 800); add( () => { target[key] = projected; }, () => { delete target[key]; }, "values", path, ); } } fieldEntries.filter((field) => !short(field)).forEach(addField); const sourceNotes = Array.isArray(context?.notes) ? context.notes : []; sourceNotes.forEach((raw, index) => { const note = row(raw); if (!note) return; if (typeof note._id === "string" && includedNotes.has(note._id)) { metadata.omittedCounts.duplicateNotes += 1; return; } const path = `/context/notes/${index}`; const projected = { ...pick(note, [ "_id", "kind", "title", "createdAt", "editedAt", "deletedAt", "replyCount", ]), actor: actor(note.actor, `${path}/actor`), body: bounded(note.body, `${path}/body`), }; add( () => { notes.push(projected); }, () => { notes.pop(); }, "notes", path, ); }); // Coverage grows when optional values are dropped; measure the complete final // envelope again rather than assuming the per-value allowance was sufficient. while (!fits(output) && accepted.length) { const last = accepted.pop(); if (!last) break; last.remove(); metadata.omittedCounts[last.group] += 1; changed(last.path); } return fits(output) ? serialized(output) : tooLarge(); }