Skip to main content
Glama
diff.ts3.75 kB
import stringify from "fast-json-stable-stringify"; import { resolveFrmrDocument } from "./indexer.js"; import type { DiffChange, DiffResult, FrmrDocumentRecord } from "./types.js"; import { createError } from "./util.js"; interface DiffOptions { idKey?: string; } function extractItems( doc: FrmrDocumentRecord, ): Array<Record<string, unknown>> { const raw = doc.raw as Record<string, unknown>; const candidates = [ raw.items, raw.controls, raw.entries, raw.records, ]; for (const candidate of candidates) { if (Array.isArray(candidate)) { return candidate.filter( (item): item is Record<string, unknown> => Boolean(item && typeof item === "object"), ); } } return []; } function getIdKey( options: DiffOptions, left: FrmrDocumentRecord, right: FrmrDocumentRecord, ): string { if (options.idKey) { return options.idKey; } if (left.idKey && right.idKey && left.idKey === right.idKey) { return left.idKey; } return left.idKey ?? right.idKey ?? "id"; } function toMap( items: Array<Record<string, unknown>>, idKey: string, ): Map<string, Record<string, unknown>> { const map = new Map<string, Record<string, unknown>>(); for (const item of items) { const id = item[idKey]; if (typeof id === "string") { map.set(id, item); } } return map; } function detectChangedFields( left: Record<string, unknown>, right: Record<string, unknown>, idKey: string, ): string[] { const keys = new Set([ ...Object.keys(left), ...Object.keys(right), ]); keys.delete(idKey); const changed: string[] = []; for (const key of keys) { const leftValue = left[key]; const rightValue = right[key]; if (stringify(leftValue) !== stringify(rightValue)) { changed.push(key); } } return changed; } export function diffFrmrDocuments( leftPath: string, rightPath: string, options: DiffOptions = {}, ): DiffResult { const leftDoc = resolveFrmrDocument(leftPath); const rightDoc = resolveFrmrDocument(rightPath); if (!leftDoc || !rightDoc) { throw createError({ code: "NOT_FOUND", message: "One or both FRMR documents could not be found in the index.", }); } const idKey = getIdKey(options, leftDoc, rightDoc); const leftItems = extractItems(leftDoc); const rightItems = extractItems(rightDoc); const leftMap = toMap(leftItems, idKey); const rightMap = toMap(rightItems, idKey); const changes: DiffChange[] = []; for (const [id, rightValue] of rightMap.entries()) { if (!leftMap.has(id)) { changes.push({ type: "added", id, title: typeof rightValue.title === "string" ? rightValue.title : undefined, }); } } for (const [id, leftValue] of leftMap.entries()) { if (!rightMap.has(id)) { changes.push({ type: "removed", id, title: typeof leftValue.title === "string" ? leftValue.title : undefined, }); continue; } const rightValue = rightMap.get(id)!; const fields = detectChangedFields(leftValue, rightValue, idKey); if (fields.length) { changes.push({ type: "modified", id, title: typeof rightValue.title === "string" ? rightValue.title : typeof leftValue.title === "string" ? leftValue.title : undefined, fields, }); } } const summary = { added: changes.filter((change) => change.type === "added").length, removed: changes.filter((change) => change.type === "removed").length, modified: changes.filter((change) => change.type === "modified").length, }; return { summary, changes, }; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ethanolivertroy/fedramp-docs-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server