/** * The shared contract pin formatter. Every surface that shows a human the * contract identity — MCP metadata, agent context files, hook output, LSP * diagnostics, CI check text, PR comments — interpolates the string this * function returns; no surface reformats it. One formatter is what makes * "compare the hashes yourself" a falsifiable claim instead of three surfaces * rendering the pin three ways. * * Display convention (spine "Identifiers"): never a bare hash without the * version, never a version without the hash. */ import { CONTRACT_DOMAINS, type ContractDomain } from "./preimage.js"; /** The committed agent context file every gate reads for local contract stamps. */ export const AGENT_CONTEXT_RELATIVE_PATH = ".fragments/agent-context.md"; /** Length of the abbreviated fcid in the display form. */ const SHORT_FCID_LENGTH = 7; export interface ContractStampPin { /** Monotonic, human-facing version from the ledger. */ contractVersion: number; /** The 64-hex FCID; abbreviated for display. */ fcid: string; } export interface ContractStampRecord extends ContractStampPin { /** Full 64-hex domain sub-hashes, when known. */ domains?: Record; } /** * The bare `v12 · a3f9e21` form, for surfaces where "contract" is already said * by the surrounding copy (e.g. the hook staleness line's `local v11 · 9c01b22`). * {@link formatStampPin} delegates here so the two forms can never drift. */ export function formatStampVersionHash(pin: ContractStampPin): string { return `v${pin.contractVersion} · ${pin.fcid.slice(0, SHORT_FCID_LENGTH)}`; } /** * Render the canonical pin: `contract v12 · a3f9e21`. */ export function formatStampPin(pin: ContractStampPin): string { return `contract ${formatStampVersionHash(pin)}`; } /** The context-file header form: `fragments contract v12 · a3f9e21`. */ export function formatStampHeader(pin: ContractStampPin): string { return `fragments ${formatStampPin(pin)}`; } /** * Serialize pin + full fcid (+ domain hashes when known) as plain header lines — * no markdown syntax, so the block is safe inside YAML-ish frontmatter and * inside HTML-comment blocks. The first line carries the literal * {@link formatStampHeader} output; surfaces grep for that substring, so the * header form and the serialized form may never diverge. */ export function serializeContractStamp(record: ContractStampRecord): string { const lines = [`contract: ${formatStampHeader(record)}`, `contract-fcid: ${record.fcid}`]; if (record.domains) { const domains = record.domains; lines.push( `contract-domains: ${CONTRACT_DOMAINS.map((domain) => `${domain}=${domains[domain]}`).join(" ")}` ); } return lines.join("\n"); } // Line-anchored so the stamp is found anywhere in a file — frontmatter, an // HTML-comment block, or a marker-merged region — without parsing the host // format. const STAMP_HEADER_LINE = /^contract:\s*fragments contract v(\d+) · ([0-9a-f]+)\s*$/mu; const STAMP_FCID_LINE = /^contract-fcid:\s*([0-9a-f]{64})\s*$/mu; const STAMP_DOMAINS_LINE = /^contract-domains:\s*(\S.*?)\s*$/mu; /** * Parse a stamp record back out of arbitrary file text. Requires the * `contract:` + `contract-fcid:` lines and an abbreviated fcid that prefixes * the full one; `contract-domains` is optional. Returns null when absent or * malformed — never throws. */ export function parseContractStamp(text: string): ContractStampRecord | null { const header = STAMP_HEADER_LINE.exec(text); const fcidLine = STAMP_FCID_LINE.exec(text); if (!header || !fcidLine) return null; const contractVersion = Number(header[1]); if (!Number.isSafeInteger(contractVersion)) return null; const fcid = fcidLine[1]; if (!fcid.startsWith(header[2])) return null; const domainsLine = STAMP_DOMAINS_LINE.exec(text); if (!domainsLine) return { contractVersion, fcid }; const domains = {} as Record; for (const pair of domainsLine[1].split(/\s+/u)) { const match = /^([a-zA-Z]+)=([0-9a-f]{64})$/u.exec(pair); if (!match || !(CONTRACT_DOMAINS as readonly string[]).includes(match[1])) return null; domains[match[1] as ContractDomain] = match[2]; } if (CONTRACT_DOMAINS.some((domain) => !domains[domain])) return null; return { contractVersion, fcid, domains }; }