UNPKG

@ton-ai-core/vibecode-linter

Version:

Advanced TypeScript linter with Git integration, dependency analysis, and comprehensive error reporting

204 lines 7.49 kB
// CHANGE: Shared helpers for git-aware change tree formatting // WHY: Keep main formatter lean (<300 LOC) while preserving pure, testable helpers // QUOTE(ТЗ): "Разбить ... каждый файл желательно < 300 строк" // REF: user-request-project-info-tree // FORMAT THEOREM: All helpers are pure functions operating on immutable inputs // PURITY: CORE // INVARIANT: No IO; deterministic transformations only // COMPLEXITY: Documented per function below export const DEFAULT_INLINE_LIMIT = 5; const ROOT_RELATIVE_PATH = ""; const SLASH = "/"; const ELLIPSIS = "…"; function normalizeRelativePath(relativePath) { return relativePath === "." ? ROOT_RELATIVE_PATH : relativePath; } function createDirectoryNode(name, relativePath) { return { name, relativePath, directories: new Map(), files: new Map(), }; } function insertSnapshotFile(file, target) { const normalized = normalizeRelativePath(file.relativePath); target.files.set(file.name, { name: file.name, relativePath: normalized, }); } function insertSnapshotDirectory(source, target) { for (const entry of source.entries) { if (entry.kind === "directory") { const normalized = normalizeRelativePath(entry.relativePath); const child = createDirectoryNode(entry.name, normalized); target.directories.set(entry.name, child); insertSnapshotDirectory(entry, child); } else { insertSnapshotFile(entry, target); } } } function ensureDirectoryForPath(root, segments) { let current = root; for (const segment of segments) { let child = current.directories.get(segment); if (child === undefined) { const relativePath = current.relativePath.length === 0 ? segment : `${current.relativePath}${SLASH}${segment}`; child = createDirectoryNode(segment, relativePath); current.directories.set(segment, child); } current = child; } return current; } function ensureFileNode(root, relativePath) { const segments = relativePath .split(SLASH) .filter((segment) => segment.length > 0); const fileName = segments.pop(); if (fileName === undefined) return; const parent = ensureDirectoryForPath(root, segments); if (!parent.files.has(fileName)) { const parentPath = parent.relativePath; const fileRelativePath = parentPath.length === 0 ? fileName : `${parentPath}${SLASH}${fileName}`; parent.files.set(fileName, { name: fileName, relativePath: fileRelativePath, }); } } export function buildDirectoryTree(root, changeMap) { const rootNode = createDirectoryNode(root.name, ROOT_RELATIVE_PATH); insertSnapshotDirectory(root, rootNode); for (const [key, info] of changeMap.entries()) { if (info.isDirectory) continue; if (key.length === 0) continue; ensureFileNode(rootNode, key); } return rootNode; } const ZERO_TOTALS = { modified: 0, untracked: 0, additions: 0, deletions: 0, }; function cloneTotals(totals) { return { modified: totals.modified, untracked: totals.untracked, additions: totals.additions, deletions: totals.deletions, }; } function applyFileChange(change, totals) { if (change.category === "untracked") { totals.untracked += 1; return; } totals.modified += 1; totals.additions += change.additions; totals.deletions += change.deletions; } export function aggregateDirectorySummaries(node, changeMap, summaryMap) { const totals = cloneTotals(ZERO_TOTALS); for (const file of node.files.values()) { const change = changeMap.get(file.relativePath); if (change === undefined || change.isDirectory) continue; applyFileChange(change, totals); } for (const child of node.directories.values()) { const childSummary = aggregateDirectorySummaries(child, changeMap, summaryMap); totals.modified += childSummary.modifiedFiles; totals.untracked += childSummary.untrackedFiles; totals.additions += childSummary.additions; totals.deletions += childSummary.deletions; } const directoryChange = changeMap.get(node.relativePath); if (directoryChange?.isDirectory === true) { applyFileChange(directoryChange, totals); } const summary = { modifiedFiles: totals.modified, untrackedFiles: totals.untracked, additions: totals.additions, deletions: totals.deletions, }; summaryMap.set(node.relativePath, summary); return summary; } export function formatInlineFiles(files, changeMap, inlineLimit) { if (files.size === 0) return ""; const entries = []; const sorted = [...files.values()].sort((a, b) => a.name.localeCompare(b.name)); for (const file of sorted) { const rendered = formatFileEntry(file, changeMap.get(file.relativePath)); if (rendered !== null) { entries.push(rendered); } } if (entries.length > inlineLimit) { return [...entries.slice(0, inlineLimit), ELLIPSIS].join(", "); } return entries.join(", "); } function formatFileEntry(file, change) { if (change === undefined) { return file.name; } if (change.isDirectory) { return null; } const diff = change.category === "untracked" ? null : `(+${change.additions}/-${change.deletions})`; const label = change.statusLabel === "??" ? `?? ${file.name}` : `${change.statusLabel} ${file.name}`; return diff === null ? label : `${label} ${diff}`; } export function composeLine(nameColumn, summaryText, inlineText) { const segments = [nameColumn.trimEnd()]; if (summaryText.length > 0) segments.push(summaryText); if (inlineText.length > 0) segments.push(inlineText); return segments.join(" "); } export function renderDirectory(node, prefix, context) { const { summaryMap, changeMap, lines, inlineLimit } = context; const sortedChildren = [...node.directories.values()].sort((a, b) => a.name.localeCompare(b.name)); sortedChildren.forEach((child, index) => { const isLast = index === sortedChildren.length - 1; const connector = isLast ? "└─" : "├─"; const childPrefix = `${prefix}${isLast ? " " : "│ "}`; const summary = summaryMap.get(child.relativePath); const summaryText = summary === undefined ? "" : formatDirectorySummary(summary); const inlineText = formatInlineFiles(child.files, changeMap, inlineLimit); const base = `${prefix}${connector} ${child.name}${SLASH}`; lines.push(composeLine(base, summaryText, inlineText)); renderDirectory(child, childPrefix, context); }); } export function formatDirectorySummary(summary) { const tokens = []; if (summary.modifiedFiles > 0) tokens.push(`M${summary.modifiedFiles}`); if (summary.additions > 0 || summary.deletions > 0) { tokens.push(`+${summary.additions}/-${summary.deletions}`); } if (summary.untrackedFiles > 0) tokens.push(`?${summary.untrackedFiles}`); return tokens.length > 0 ? `[${tokens.join(" ")}]` : ""; } //# sourceMappingURL=change-tree-helpers.js.map