Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
335 lines
8.4 KiB
TypeScript
335 lines
8.4 KiB
TypeScript
import {
|
|
extractAgentPatchPath,
|
|
inferAgentPatchChangeType,
|
|
normalizeAgentPatchText,
|
|
type AgentPatchChangeType
|
|
} from "../../../rules/agentPatchMetadata";
|
|
import { isAgentUnifiedDiffText } from "../../../rules/agentUnifiedDiffValidation";
|
|
|
|
export { extractAgentPatchPath, inferAgentPatchChangeType };
|
|
export type AgentUnifiedDiffChangeType = AgentPatchChangeType;
|
|
|
|
export interface ParsedAgentUnifiedDiff {
|
|
oldString: string;
|
|
newString: string;
|
|
}
|
|
|
|
export interface ParsedAgentUnifiedDiffLine {
|
|
kind: "add" | "remove" | "context";
|
|
oldLineNumber: number | null;
|
|
newLineNumber: number | null;
|
|
text: string;
|
|
}
|
|
|
|
export type AgentFileChangeType =
|
|
| "created"
|
|
| "modified"
|
|
| "deleted"
|
|
| "unknown";
|
|
|
|
const DIFF_META_PREFIXES = [
|
|
"diff --git ",
|
|
"index ",
|
|
"--- ",
|
|
"+++ ",
|
|
"*** "
|
|
] as const;
|
|
|
|
export function parseAgentUnifiedDiff(
|
|
diffText: string
|
|
): ParsedAgentUnifiedDiff | null {
|
|
if (!isAgentUnifiedDiff(diffText)) {
|
|
return null;
|
|
}
|
|
|
|
const normalizedText = normalizeAgentPatchText(diffText);
|
|
const oldLines: string[] = [];
|
|
const newLines: string[] = [];
|
|
let sawChangeLine = false;
|
|
let sawHunkHeader = false;
|
|
let hunkActive = false;
|
|
|
|
for (const line of normalizedText.replace(/\r\n/g, "\n").split("\n")) {
|
|
if (/^@@(?: -\d|$)/.test(line)) {
|
|
if (sawHunkHeader && (oldLines.length > 0 || newLines.length > 0)) {
|
|
oldLines.push("");
|
|
newLines.push("");
|
|
}
|
|
sawHunkHeader = true;
|
|
hunkActive = true;
|
|
continue;
|
|
}
|
|
if (line === "@@" && isApplyPatch(normalizedText)) {
|
|
if (sawHunkHeader && (oldLines.length > 0 || newLines.length > 0)) {
|
|
oldLines.push("");
|
|
newLines.push("");
|
|
}
|
|
sawHunkHeader = true;
|
|
hunkActive = true;
|
|
continue;
|
|
}
|
|
if (hunkActive && line.startsWith("diff --git ")) {
|
|
hunkActive = false;
|
|
continue;
|
|
}
|
|
if (
|
|
(!hunkActive &&
|
|
DIFF_META_PREFIXES.some((prefix) => line.startsWith(prefix))) ||
|
|
(hunkActive && isApplyPatch(normalizedText) && line.startsWith("*** "))
|
|
) {
|
|
continue;
|
|
}
|
|
if (line === "\\ No newline at end of file") {
|
|
continue;
|
|
}
|
|
if (line.startsWith("+")) {
|
|
newLines.push(line.slice(1));
|
|
sawChangeLine = true;
|
|
continue;
|
|
}
|
|
if (line.startsWith("-")) {
|
|
oldLines.push(line.slice(1));
|
|
sawChangeLine = true;
|
|
continue;
|
|
}
|
|
if (line.startsWith(" ")) {
|
|
const content = line.slice(1);
|
|
oldLines.push(content);
|
|
newLines.push(content);
|
|
}
|
|
}
|
|
|
|
if (!sawChangeLine) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
oldString: oldLines.join("\n"),
|
|
newString: newLines.join("\n")
|
|
};
|
|
}
|
|
|
|
export function parseAgentUnifiedDiffLines(
|
|
diffText: string
|
|
): ParsedAgentUnifiedDiffLine[] {
|
|
if (!isAgentUnifiedDiff(diffText)) {
|
|
return [];
|
|
}
|
|
const normalized = normalizeAgentPatchText(diffText);
|
|
if (!normalized.trim()) {
|
|
return [];
|
|
}
|
|
|
|
const lines: ParsedAgentUnifiedDiffLine[] = [];
|
|
let oldLineNumber = 0;
|
|
let newLineNumber = 0;
|
|
let hunkActive = false;
|
|
|
|
for (const line of normalized.replace(/\r\n/g, "\n").split("\n")) {
|
|
const match = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
if (match) {
|
|
oldLineNumber = Number.parseInt(match[1] ?? "0", 10);
|
|
newLineNumber = Number.parseInt(match[2] ?? "0", 10);
|
|
hunkActive = true;
|
|
continue;
|
|
}
|
|
if (line === "@@" && isApplyPatch(normalized)) {
|
|
hunkActive = true;
|
|
continue;
|
|
}
|
|
if (hunkActive && line.startsWith("diff --git ")) {
|
|
hunkActive = false;
|
|
continue;
|
|
}
|
|
if (
|
|
(!hunkActive &&
|
|
DIFF_META_PREFIXES.some((prefix) => line.startsWith(prefix))) ||
|
|
(hunkActive && isApplyPatch(normalized) && line.startsWith("*** "))
|
|
) {
|
|
continue;
|
|
}
|
|
if (line === "\\ No newline at end of file") {
|
|
continue;
|
|
}
|
|
if (line.startsWith("+")) {
|
|
lines.push({
|
|
kind: "add",
|
|
oldLineNumber: null,
|
|
newLineNumber,
|
|
text: line.slice(1)
|
|
});
|
|
newLineNumber += 1;
|
|
continue;
|
|
}
|
|
if (line.startsWith("-")) {
|
|
lines.push({
|
|
kind: "remove",
|
|
oldLineNumber,
|
|
newLineNumber: null,
|
|
text: line.slice(1)
|
|
});
|
|
oldLineNumber += 1;
|
|
continue;
|
|
}
|
|
if (line.startsWith(" ")) {
|
|
lines.push({
|
|
kind: "context",
|
|
oldLineNumber,
|
|
newLineNumber,
|
|
text: line.slice(1)
|
|
});
|
|
oldLineNumber += 1;
|
|
newLineNumber += 1;
|
|
}
|
|
}
|
|
|
|
return lines;
|
|
}
|
|
|
|
export function isAgentUnifiedDiff(diffText: string): boolean {
|
|
return isAgentUnifiedDiffText(diffText);
|
|
}
|
|
|
|
export function countAgentTextLines(value: string | null | undefined): number {
|
|
if (typeof value !== "string" || value.length === 0) {
|
|
return 0;
|
|
}
|
|
const lines = value.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
if (lines.at(-1) === "") {
|
|
lines.pop();
|
|
}
|
|
return lines.length;
|
|
}
|
|
|
|
export function agentFileChangeStats({
|
|
changeType,
|
|
unifiedDiff,
|
|
content,
|
|
oldString,
|
|
newString
|
|
}: {
|
|
changeType: AgentFileChangeType;
|
|
unifiedDiff: string | null | undefined;
|
|
content: string | null | undefined;
|
|
oldString: string | null | undefined;
|
|
newString: string | null | undefined;
|
|
}): { added: number; removed: number } {
|
|
const diff = typeof unifiedDiff === "string" ? unifiedDiff : null;
|
|
if (diff && isAgentUnifiedDiff(diff)) {
|
|
return parseAgentUnifiedDiffStats(diff);
|
|
}
|
|
|
|
if (changeType === "deleted") {
|
|
return {
|
|
added: 0,
|
|
removed: countAgentTextLines(oldString ?? content ?? diff)
|
|
};
|
|
}
|
|
if (changeType === "created") {
|
|
return {
|
|
added: countAgentTextLines(content ?? newString ?? diff),
|
|
removed: 0
|
|
};
|
|
}
|
|
if (typeof oldString === "string" && typeof newString === "string") {
|
|
return agentTextDiffStats(oldString, newString);
|
|
}
|
|
return { added: 0, removed: 0 };
|
|
}
|
|
|
|
export function parseAgentUnifiedDiffStats(diffText: string): {
|
|
added: number;
|
|
removed: number;
|
|
} {
|
|
if (!isAgentUnifiedDiff(diffText)) {
|
|
return { added: 0, removed: 0 };
|
|
}
|
|
|
|
let added = 0;
|
|
let removed = 0;
|
|
let hunkActive = false;
|
|
const normalized = normalizeAgentPatchText(diffText);
|
|
for (const line of normalized.replace(/\r\n/g, "\n").split("\n")) {
|
|
if (/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.test(line)) {
|
|
hunkActive = true;
|
|
continue;
|
|
}
|
|
if (line === "@@" && isApplyPatch(normalized)) {
|
|
hunkActive = true;
|
|
continue;
|
|
}
|
|
if (hunkActive && line.startsWith("diff --git ")) {
|
|
hunkActive = false;
|
|
continue;
|
|
}
|
|
if (
|
|
(!hunkActive &&
|
|
DIFF_META_PREFIXES.some((prefix) => line.startsWith(prefix))) ||
|
|
(hunkActive && isApplyPatch(normalized) && line.startsWith("*** "))
|
|
) {
|
|
continue;
|
|
}
|
|
if (line === "\\ No newline at end of file" || !hunkActive) {
|
|
continue;
|
|
}
|
|
if (line.startsWith("+")) {
|
|
added += 1;
|
|
} else if (line.startsWith("-")) {
|
|
removed += 1;
|
|
}
|
|
}
|
|
return { added, removed };
|
|
}
|
|
|
|
function agentTextDiffStats(
|
|
oldString: string,
|
|
newString: string
|
|
): { added: number; removed: number } {
|
|
const oldLines = splitAgentTextLines(oldString);
|
|
const newLines = splitAgentTextLines(newString);
|
|
if (oldLines.length === 0 && newLines.length === 0) {
|
|
return { added: 0, removed: 0 };
|
|
}
|
|
if (oldLines.length * newLines.length > 2_000_000) {
|
|
return { added: 0, removed: 0 };
|
|
}
|
|
|
|
let previous = Array.from({ length: newLines.length + 1 }, () => 0);
|
|
for (const oldLine of oldLines) {
|
|
const current = Array.from({ length: newLines.length + 1 }, () => 0);
|
|
for (let newIndex = 1; newIndex <= newLines.length; newIndex += 1) {
|
|
current[newIndex] =
|
|
oldLine === newLines[newIndex - 1]
|
|
? previous[newIndex - 1]! + 1
|
|
: Math.max(previous[newIndex]!, current[newIndex - 1]!);
|
|
}
|
|
previous = current;
|
|
}
|
|
|
|
const commonLines = previous[newLines.length] ?? 0;
|
|
return {
|
|
added: newLines.length - commonLines,
|
|
removed: oldLines.length - commonLines
|
|
};
|
|
}
|
|
|
|
function splitAgentTextLines(value: string): string[] {
|
|
if (value.length === 0) {
|
|
return [];
|
|
}
|
|
const lines = value.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
if (lines.at(-1) === "") {
|
|
lines.pop();
|
|
}
|
|
return lines;
|
|
}
|
|
|
|
function isApplyPatch(value: string): boolean {
|
|
return (
|
|
value.includes("*** Begin Patch") &&
|
|
(value.includes("*** Add File:") ||
|
|
value.includes("*** Delete File:") ||
|
|
value.includes("*** Update File:"))
|
|
);
|
|
}
|