interface TruncatedText { text: string; truncated: boolean; } export interface ChangeSummarySection { title: string; nameStatus: string; statSummary: string; } export interface RawFileDiff { source: string; path: string; diff: string; } export interface PromptChangeContent { summary: string; diff: string; truncated: boolean; } const MAX_SUMMARY_CHARS = 12_000; const SUMMARY_BUDGET_RATIO = 0.2; const MIN_SUMMARY_CHARS = 500; const BASE64_SEQUENCE = /[A-Za-z0-9+/]{256,}={0,2}/g; const LOCKFILE_NAMES = new Set([ "bun.lock", "bun.lockb", "cargo.lock", "composer.lock", "flake.lock", "gemfile.lock", "go.sum", "package-lock.json", "packages.lock.json", "pnpm-lock.yaml", "poetry.lock", "pubspec.lock", "uv.lock", "yarn.lock", ]); const COMPRESSED_OR_BINARY_EXTENSIONS = /\.(?:7z|avi|bmp|br|bz2|class|dll|dmg|eot|exe|flac|gif|gz|ico|jar|jpe?g|mov|mp3|mp4|o|otf|pdf|png|so|tar|tgz|ttf|wav|webm|webp|woff2?|xz|zip)$/i; const MINIFIED_OR_MAP_FILE = /(?:\.min\.(?:css|js)|\.map)$/i; /** 使用 JSON 字符串形式安全展示不可信文件路径。 */ function formatPath(path: string): string { return JSON.stringify(path); } /** 在严格字符预算内保留文本首尾,并明确标记中间省略。 */ export function truncateTextToBudget( text: string, maxChars: number, notice = "\n[Content truncated in the middle by pi-ai-git-commit]\n", ): TruncatedText { if (text.length <= maxChars) { return { text, truncated: false }; } if (maxChars <= 0) { return { text: "", truncated: true }; } if (maxChars <= notice.length + 2) { return { text: text.slice(0, maxChars), truncated: true }; } const available = maxChars - notice.length; const headLength = Math.ceil(available / 2); const tailLength = Math.floor(available / 2); return { text: `${text.slice(0, headLength)}${notice}${text.slice(-tailLength)}`, truncated: true, }; } /** 判断文件是否为应压缩展示的依赖锁文件。 */ function isLockfile(path: string): boolean { const normalized = path.replaceAll("\\", "/").toLowerCase(); const name = normalized.slice(normalized.lastIndexOf("/") + 1); return LOCKFILE_NAMES.has(name); } /** 判断文件路径是否通常只包含高噪声压缩或映射内容。 */ function isCompressedOrBinaryPath(path: string): boolean { return MINIFIED_OR_MAP_FILE.test(path) || COMPRESSED_OR_BINARY_EXTENSIONS.test(path); } /** 判断 Git diff 是否表示二进制补丁。 */ function isBinaryDiff(diff: string): boolean { return /(?:^|\n)(?:Binary files .* differ|GIT binary patch)(?:\n|$)/.test(diff); } /** 过滤单行中的长 Base64 片段,并限制异常超长行。 */ function sanitizeDiffLine(line: string): { text: string; filtered: boolean } { let filtered = false; const withoutBase64 = line.replace(BASE64_SEQUENCE, () => { filtered = true; return "[BASE64_CONTENT_OMITTED]"; }); if (withoutBase64.length <= 4_000) { return { text: withoutBase64, filtered }; } return { text: `${withoutBase64.slice(0, 2_000)}...[remaining oversized line omitted]`, filtered: true, }; } /** 按文件类型和内容过滤不利于模型理解的高噪声 diff。 */ function sanitizeFileDiff(entry: RawFileDiff): TruncatedText { if (isLockfile(entry.path)) { return { text: "[Detailed lockfile diff omitted; file status and size remain in the structured summary. Determine dependency intent only from visible manifest evidence.]", truncated: true, }; } if (isCompressedOrBinaryPath(entry.path) || isBinaryDiff(entry.diff)) { return { text: "[Detailed binary, compressed, or source map content omitted; file status and size remain in the structured summary.]", truncated: true, }; } let filtered = false; const lines = entry.diff.split("\n").map((line) => { const sanitized = sanitizeDiffLine(line); filtered ||= sanitized.filtered; return sanitized.text; }); return { text: lines.join("\n").trim() || "(No displayable text diff)", truncated: filtered, }; } /** 解码 Git 在 diff 头中使用的双引号 C 风格路径。 */ function decodeGitQuotedPath(value: string): string { const withoutSeparator = value.endsWith("\t") ? value.slice(0, -1) : value; if (!(withoutSeparator.startsWith('"') && withoutSeparator.endsWith('"'))) { return withoutSeparator; } const input = withoutSeparator.slice(1, -1); const bytes: number[] = []; const escapes: Record = { a: 7, b: 8, t: 9, n: 10, v: 11, f: 12, r: 13, '"': 34, "\\": 92, }; for (let index = 0; index < input.length;) { const character = input[index] as string; if (character !== "\\") { const codePoint = input.codePointAt(index) as number; const literal = String.fromCodePoint(codePoint); bytes.push(...Buffer.from(literal)); index += literal.length; continue; } const escaped = input[index + 1]; if (escaped === undefined) { bytes.push(92); break; } if (/[0-7]/.test(escaped)) { let octal = escaped; let cursor = index + 2; while (cursor < input.length && octal.length < 3 && /[0-7]/.test(input[cursor] as string)) { octal += input[cursor] as string; cursor += 1; } bytes.push(Number.parseInt(octal, 8)); index = cursor; continue; } bytes.push(escapes[escaped] ?? escaped.charCodeAt(0)); index += 2; } return Buffer.from(bytes).toString("utf8"); } /** 规范化 diff 元数据路径,并移除 a/ 或 b/ 前缀。 */ function normalizeDiffPath(value: string): string | undefined { const decoded = decodeGitQuotedPath(value); if (decoded === "/dev/null") { return undefined; } return decoded.startsWith("a/") || decoded.startsWith("b/") ? decoded.slice(2) : decoded; } /** 从带引号的 diff --git 头中读取目标路径。 */ function parseQuotedHeaderDestination(payload: string): string | undefined { const tokens: string[] = []; let index = 0; while (index < payload.length && tokens.length < 2) { while (payload[index] === " ") { index += 1; } if (payload[index] !== '"') { return undefined; } const start = index; index += 1; let escaped = false; while (index < payload.length) { const character = payload[index] as string; if (!escaped && character === '"') { index += 1; break; } escaped = !escaped && character === "\\"; if (character !== "\\") { escaped = false; } index += 1; } tokens.push(decodeGitQuotedPath(payload.slice(start, index))); } return tokens[1] ? normalizeDiffPath(tokens[1]) : undefined; } /** 从单个 diff chunk 的元数据中解析目标路径。 */ function parseDiffChunkPath(chunk: string, files: string[]): string | undefined { const lines = chunk.split("\n"); for (const prefix of ["rename to ", "copy to "]) { const line = lines.find((candidate) => candidate.startsWith(prefix)); if (line) { return normalizeDiffPath(line.slice(prefix.length)); } } const destination = lines.find((line) => line.startsWith("+++ ")); if (destination) { const parsed = normalizeDiffPath(destination.slice(4)); if (parsed) { return parsed; } } const source = lines.find((line) => line.startsWith("--- ")); if (source) { const parsed = normalizeDiffPath(source.slice(4)); if (parsed) { return parsed; } } const header = lines[0] ?? ""; const prefix = "diff --git "; if (!header.startsWith(prefix)) { return undefined; } const payload = header.slice(prefix.length); if (payload.startsWith('"')) { return parseQuotedHeaderDestination(payload); } const knownDestination = [...files] .sort((left, right) => right.length - left.length) .find((path) => header.endsWith(` b/${path}`)); if (knownDestination) { return knownDestination; } for (let index = payload.indexOf(" b/"); index >= 0; index = payload.indexOf(" b/", index + 1)) { const left = payload.slice(2, index); const right = payload.slice(index + 3); if (left === right) { return right; } } return undefined; } /** 将组合 Git diff 按文件边界拆分,并优先从每个 chunk 自身解析路径。 */ export function splitTrackedDiff(source: string, files: string[], diff: string): RawFileDiff[] { const starts: number[] = []; const boundary = /^diff --git /gm; for (let match = boundary.exec(diff); match; match = boundary.exec(diff)) { starts.push(match.index); } const chunks = starts.map((start, index) => diff.slice(start, starts[index + 1] ?? diff.length).trim(), ); const unusedFiles = new Set(files); const entries = chunks.map((chunk, index) => { const parsedPath = parseDiffChunkPath(chunk, files); const fallbackPath = unusedFiles.values().next().value as string | undefined; const path = parsedPath ?? fallbackPath ?? `unidentified-file-${index + 1}`; unusedFiles.delete(path); return { source, path, diff: chunk }; }); for (const path of files) { if (!unusedFiles.has(path)) { continue; } entries.push({ source, path, diff: "(No text diff is available; the change may contain only rename, file mode, or other metadata.)", }); } return entries; } /** 将结构化 Git 摘要区块格式化为模型可读文本。 */ function formatSummarySection(section: ChangeSummarySection): string { return [ `===== ${section.title} =====`, "File status:", section.nameStatus.trim() || "(none)", "Change size and special operations:", section.statSummary.trim() || "(no additional statistics)", ].join("\n"); } /** 以水位填充方式公平分配预算,小文件未用完的额度会让给大文件。 */ function allocateFairBudgets(lengths: number[], totalBudget: number): number[] { const budgets = lengths.map(() => 0); let remaining = Math.max(0, totalBudget); let active = lengths.map((_, index) => index); while (active.length > 0 && remaining > 0) { const share = Math.floor(remaining / active.length); if (share <= 0) { for (const index of active.slice(0, remaining)) { budgets[index] = (budgets[index] ?? 0) + 1; } break; } const satisfied = active.filter( (index) => (lengths[index] ?? 0) - (budgets[index] ?? 0) <= share, ); if (satisfied.length === 0) { for (const index of active) { budgets[index] = (budgets[index] ?? 0) + share; remaining -= share; } for (const index of active.slice(0, remaining)) { budgets[index] = (budgets[index] ?? 0) + 1; } break; } const satisfiedSet = new Set(satisfied); for (const index of satisfied) { const needed = (lengths[index] ?? 0) - (budgets[index] ?? 0); budgets[index] = (budgets[index] ?? 0) + needed; remaining -= needed; } active = active.filter((index) => !satisfiedSet.has(index)); } return budgets; } /** 在总预算内为每个文件保留公平的详情份额,避免尾部文件整体消失。 */ function buildBudgetedFileDiffs(entries: RawFileDiff[], maxChars: number): TruncatedText { if (entries.length === 0 || maxChars <= 0) { return { text: entries.length === 0 ? "(No file details)" : "(File detail budget is unavailable; use the structured summary.)", truncated: entries.length > 0, }; } let filtered = false; const sections = entries.map((entry) => { const sanitized = sanitizeFileDiff(entry); filtered ||= sanitized.truncated; return [`===== ${entry.source} · ${formatPath(entry.path)} =====`, sanitized.text].join("\n"); }); const separatorLength = Math.max(0, sections.length - 1) * 2; if (separatorLength >= maxChars) { return { text: "(Too many files for the detail budget; every file remains listed in the structured summary.)".slice( 0, maxChars, ), truncated: true, }; } const contentBudget = Math.max(0, maxChars - separatorLength); const budgets = allocateFairBudgets( sections.map((section) => section.length), contentBudget, ); const rendered = sections.map((section, index) => { const limited = truncateTextToBudget( section, budgets[index] ?? 0, "\n[Middle of this file's diff omitted]\n", ); filtered ||= limited.truncated; return limited.text; }); return { text: rendered.join("\n\n"), truncated: filtered, }; } /** 构建“结构化摘要 + 文件级公平详情”两层 Prompt 内容。 */ export function buildPromptChangeContent( summarySections: ChangeSummarySection[], fileDiffs: RawFileDiff[], maxChars: number, ): PromptChangeContent { const rawSummary = summarySections.map(formatSummarySection).join("\n\n"); const targetSummaryBudget = Math.min( MAX_SUMMARY_CHARS, Math.max(MIN_SUMMARY_CHARS, Math.floor(maxChars * SUMMARY_BUDGET_RATIO)), maxChars, ); const summary = truncateTextToBudget( rawSummary || "(No structured summary)", targetSummaryBudget, "\n[Middle of structured summary omitted; leading and trailing file entries are preserved]\n", ); const detailBudget = Math.max(0, maxChars - summary.text.length); const details = buildBudgetedFileDiffs(fileDiffs, detailBudget); return { summary: summary.text, diff: details.text, truncated: summary.truncated || details.truncated, }; }