@ton-ai-core/vibecode-linter
Version:
Advanced TypeScript linter with Git integration, dependency analysis, and comprehensive error reporting
273 lines • 11.2 kB
JavaScript
// CHANGE: Console reporter for project + git insights
// WHY: User requested npm run lint to output git status, commits, tree metrics
// QUOTE(USER): "Вывод текущей информации по проекту... Отображать git status... Отображать последние 5 гиткомитов"
// REF: user-request-project-info
// SOURCE: n/a
// FORMAT THEOREM: ∀target: Effect.run(reportProjectInsightsEffect) prints deterministic sections
// PURITY: SHELL
// EFFECT: Effect<void, never>
// INVARIANT: Does not throw; logging confined to this module
// COMPLEXITY: O(n + g) where n=files scanned, g=git queries
import { Effect } from "effect";
import { formatChangeTree } from "../../core/project/change-tree.js";
import { createProjectSnapshot } from "../../core/project/tree.js";
import { path } from "../utils/node-mods.js";
import { collectProjectFilesEffect } from "./collector.js";
import { fetchGitInsightEffect, } from "./git.js";
import { collectGitChangeInfoEffect } from "./git-changes.js";
const numberFormatter = new Intl.NumberFormat("en-US");
/**
* CHANGE: Deterministic formatting for commit log entries.
* WHY: Avoid duplicating string templates between local/upstream sections.
* QUOTE(ТЗ): "Математические инварианты"
* REF: user-request-project-info
* SOURCE: n/a
* FORMAT THEOREM: formatCommitEntry(c) = `git show ${hash} | cat :: ${date} :: ${author} — ${subject}`
* PURITY: CORE helper
* INVARIANT: Subject preserved verbatim
* COMPLEXITY: O(1)
*/
function formatCommitEntry(commit) {
return `git show ${commit.shortHash} | cat :: ${commit.date} :: ${commit.author} — ${commit.subject}`;
}
/**
* CHANGE: Compare commit sequences by hash to detect divergence.
* WHY: Reporter chooses whether to show one or two timelines.
* QUOTE(ТЗ): "Математические инварианты"
* REF: user-request-project-info
* SOURCE: n/a
* FORMAT THEOREM: equalCommits(a,b) ⇔ ∀i: hash_a(i) = hash_b(i)
* PURITY: CORE helper
* INVARIANT: Symmetric, reflexive
* COMPLEXITY: O(n)
*/
function commitSequencesEqual(left, right) {
if (left.length !== right.length) {
return false;
}
for (let index = 0; index < left.length; index += 1) {
const leftHash = left[index]?.shortHash;
const rightHash = right[index]?.shortHash;
if (leftHash !== rightHash) {
return false;
}
}
return true;
}
/**
* CHANGE: Format bytes into human-readable units for summary line.
* WHY: Provide compact overview of project size.
* QUOTE(ТЗ): "Вывод текущей информации по проекту"
* REF: user-request-project-info
* FORMAT THEOREM: summarizeBytes(b) = `${value} unit`
* PURITY: CORE helper
* INVARIANT: Returns "0 B" when sizeBytes = 0
* COMPLEXITY: O(1)
*/
function summarizeBytes(sizeBytes) {
if (sizeBytes <= 0)
return "0 B";
const tiers = [
{ unit: "GB", factor: 1024 ** 3 },
{ unit: "MB", factor: 1024 ** 2 },
{ unit: "KB", factor: 1024 },
{ unit: "B", factor: 1 },
];
for (const tier of tiers) {
if (sizeBytes >= tier.factor || tier.unit === "B") {
const normalized = sizeBytes / tier.factor;
const precision = normalized >= 10 || tier.unit === "B" ? 0 : 1;
return `${normalized.toFixed(precision)} ${tier.unit}`;
}
}
return `${sizeBytes} B`;
}
/**
* CHANGE: Derive label for tree root relative to cwd.
* WHY: Keep output stable regardless of absolute paths.
* QUOTE(ТЗ): "Каждая функция — это теорема."
* REF: user-request-project-info
* FORMAT THEOREM: rootLabel(target) = relative path or "."
* PURITY: CORE helper
* INVARIANT: Never empty string
* COMPLEXITY: O(k)
*/
function deriveRootLabel(targetPath) {
const absolute = path.resolve(process.cwd(), targetPath);
const relative = path.relative(process.cwd(), absolute);
const normalized = relative.replace(/\\/g, "/");
if (normalized.length === 0)
return ".";
return normalized;
}
/**
* CHANGE: Print aggregated metrics section.
* WHY: Provide "Вывод текущей информации по проекту"
* QUOTE(USER): "Вывод текущей информации по проекту"
* REF: user-request-project-info
* FORMAT THEOREM: logSummary(totals) outputs deterministic lines
* PURITY: SHELL
* EFFECT: Effect<void, never>
* INVARIANT: Values formatted with thousands separators
* COMPLEXITY: O(1)
*/
function logProjectSummary(targetPath, totals) {
console.log("\n📦 Project snapshot");
console.log(` • Target: ${targetPath}`);
console.log(` • Files/Dirs: ${numberFormatter.format(totals.fileCount)} files, ${numberFormatter.format(Math.max(totals.directoryCount - 1, 0))} dirs`);
console.log(` • Lines: ${numberFormatter.format(totals.lines)} | Characters: ${numberFormatter.format(totals.characters)} | Functions: ${numberFormatter.format(totals.functions)}`);
console.log(` • Total size: ${summarizeBytes(totals.sizeBytes)}`);
}
/**
* CHANGE: Predicate for presence of commit data.
* WHY: Keep logRecentCommits cyclomatic complexity under lint threshold.
* QUOTE(ТЗ): "Математические инварианты"
* REF: user-request-project-info
* SOURCE: n/a
* FORMAT THEOREМ: hasAnyCommits(head, upstream) ⇔ |head| > 0 ∨ |upstream| > 0
* PURITY: CORE helper
* INVARIANT: Symmetric with respect to arguments
* COMPLEXITY: O(1)
*/
function hasAnyCommits(headCommits, upstreamCommits) {
return headCommits.length > 0 || upstreamCommits.length > 0;
}
/**
* CHANGE: Predicate for divergent timelines.
* WHY: Keeps branching out of logRecentCommits body.
* QUOTE(ТЗ): "Математические инварианты"
* REF: user-request-project-info
* SOURCE: n/a
* FORMAT THEOREМ: hasDistinctHistories(head, upstream) ⇔ |head|>0 ∧ |upstream|>0 ∧ ¬equalCommits
* PURITY: CORE helper
* INVARIANT: Returns false if either array empty
* COMPLEXITY: O(n)
*/
function hasDistinctHistories(headCommits, upstreamCommits) {
if (headCommits.length === 0 || upstreamCommits.length === 0) {
return false;
}
return !commitSequencesEqual(headCommits, upstreamCommits);
}
/**
* CHANGE: Print recent commits with upstream awareness.
* WHY: Separate shell concern to keep logGitSection concise and lint-compliant.
* QUOTE(ТЗ): "CORE никогда не вызывает SHELL"
* REF: user-request-project-info
* SOURCE: n/a
* FORMAT THEOREМ: logRecentCommits(status, head, upstream) outputs ≤ 2 timelines
* PURITY: SHELL
* EFFECT: Effect<void, never>
* INVARIANT: Avoids duplicate timelines when histories match
* COMPLEXITY: O(n) where n = commits inspected
*/
function logRecentCommits(status, headCommits, upstreamCommits) {
console.log("\n🧾 Recent commits (last 5)");
const headPresent = headCommits.length > 0;
if (!hasAnyCommits(headCommits, upstreamCommits)) {
console.log(" • No commit information available");
return;
}
if (!hasDistinctHistories(headCommits, upstreamCommits)) {
const canonicalCommits = headPresent ? headCommits : upstreamCommits;
canonicalCommits.forEach((commit) => {
console.log(` • ${formatCommitEntry(commit)}`);
});
return;
}
const aheadLabel = status.aheadBehind ?? "ahead of upstream";
console.log(` • Local HEAD commits (${aheadLabel}):`);
headCommits.forEach((commit) => {
console.log(` • ${formatCommitEntry(commit)}`);
});
const upstreamLabel = status.upstreamBranch ?? "upstream";
console.log(` • ${upstreamLabel} commits:`);
upstreamCommits.forEach((commit) => {
console.log(` • ${formatCommitEntry(commit)}`);
});
}
/**
* CHANGE: Print git section (status + cleanliness).
* WHY: User explicitly requested git status + info on uncommitted items.
* QUOTE(USER): "Отображать git status. Есть ли не закомиченные элементы"
* REF: user-request-project-info
* FORMAT THEOREM: logGitSection(status) enumerates branch info & pending files
* PURITY: SHELL
* EFFECT: Effect<void, never>
* INVARIANT: Works even outside git repo (prints fallback)
* COMPLEXITY: O(k) where k=status lines
*/
function logGitSection(insight) {
const { status } = insight;
console.log("\n🌿 Git status");
if (!status.isRepository) {
console.log(" • Not a git repository");
return;
}
console.log(` • Branch: ${status.branch}`);
if (status.aheadBehind !== null) {
console.log(` • Tracking: ${status.aheadBehind}`);
}
const cleanliness = status.hasUncommitted
? `dirty (${status.statusLines.length} files)`
: "clean";
console.log(` • Working tree: ${cleanliness}`);
if (status.statusLines.length === 0) {
console.log(" • No pending changes");
}
else {
console.log(" • Pending changes:");
status.statusLines.forEach((line) => {
console.log(` ${line}`);
});
}
logRecentCommits(status, insight.headCommits, insight.upstreamCommits);
}
/**
* CHANGE: Print formatted tree with indent.
* WHY: User asked for tree + metrics per file.
* QUOTE(USER): "Отображать дерево папки с количество строк/символов..."
* REF: user-request-project-info
* FORMAT THEOREM: logTree(lines) outputs each line with indentation prefix
* PURITY: SHELL
* EFFECT: Effect<void, never>
* INVARIANT: Handles empty tree gracefully
* COMPLEXITY: O(n)
*/
function logChangeTree(lines) {
console.log("\n🌳 Git change tree (status + +/- lines)");
if (lines.length === 0) {
console.log(" • No files discovered");
return;
}
lines.forEach((line) => {
console.log(` ${line}`);
});
}
/**
* CHANGE: Public Effect to orchestrate report.
* WHY: Hook runLinter shell into new reporting requirement.
* QUOTE(USER): "Я думаю добавить в npm run lint ... вывод текущей информации по проекту"
* REF: user-request-project-info
* FORMAT THEOREM: reportProjectInsightsEffect(target) composes filesystem + git data
* PURITY: SHELL
* EFFECT: Effect<void, never>
* INVARIANT: Runs after linting; no impact on exit code
* COMPLEXITY: O(n + g)
*/
export function reportProjectInsightsEffect(targetPath) {
return Effect.gen(function* () {
const rootLabel = deriveRootLabel(targetPath);
const files = yield* collectProjectFilesEffect(targetPath);
const snapshot = createProjectSnapshot(rootLabel, files);
const gitInsight = yield* fetchGitInsightEffect();
const changeMap = yield* collectGitChangeInfoEffect(targetPath);
const changeTreeLines = formatChangeTree(snapshot.root, changeMap, {
maxInlineEntries: 5,
});
logProjectSummary(rootLabel, snapshot.totals);
logGitSection(gitInsight);
logChangeTree(changeTreeLines);
});
}
//# sourceMappingURL=report.js.map