@ton-ai-core/vibecode-linter
Version:
Advanced TypeScript linter with Git integration, dependency analysis, and comprehensive error reporting
211 lines • 7.4 kB
JavaScript
// CHANGE: Shell collector for filesystem metrics
// WHY: Separate IO-bound traversal from pure metrics logic
// QUOTE(ТЗ): "SHELL → CORE, но не наоборот"
// REF: user-request-project-info
// SOURCE: n/a
// FORMAT THEOREM: collect(target) ⇒ records mapped to CORE snapshot builder
// PURITY: SHELL
// EFFECT: Effect<ReadonlyArray<ProjectFileRecord>, never>
// INVARIANT: Skips forbidden directories; never throws upstream
// COMPLEXITY: O(n) where n = файлов в целевой директории
import { Effect } from "effect";
import { deriveFileContentMetrics } from "../../core/project/metrics.js";
import { fs, path } from "../utils/node-mods.js";
const fsPromises = fs.promises;
const IGNORED_DIRECTORIES = new Set([
".git",
"node_modules",
"dist",
"coverage",
".turbo",
".next",
"build",
"out",
]);
function stringifyStructured(value) {
try {
const json = JSON.stringify(value);
if (typeof json === "string" && json.length > 0) {
return json;
}
}
catch {
return Object.prototype.toString.call(value);
}
return Object.prototype.toString.call(value);
}
function isErrorWithMessage(value) {
return value instanceof Error && value.message.length > 0;
}
function isNonEmptyString(value) {
return typeof value === "string" && value.length > 0;
}
function isBooleanOrNumber(value) {
return typeof value === "number" || typeof value === "boolean";
}
function isBigintOrSymbol(value) {
return typeof value === "bigint" || typeof value === "symbol";
}
function isNullish(value) {
return value == null;
}
function isPlainRecord(value) {
return (typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
!(value instanceof Error));
}
/**
* CHANGE: Normalize error-like values to human-readable strings.
* WHY: Cannot rely on `unknown`; enforce explicit structural type.
* QUOTE(ТЗ): "Строгая типизация"
* REF: user-request-project-info
* FORMAT THEOREM: errorMessage(e) ∈ string
* PURITY: CORE helper
* INVARIANT: Non-throwing for any input
* COMPLEXITY: O(1)
*/
function errorMessage(value) {
if (isErrorWithMessage(value)) {
return value.message;
}
if (isNonEmptyString(value)) {
return value;
}
if (isBooleanOrNumber(value)) {
return `${value}`;
}
if (isBigintOrSymbol(value)) {
return value.toString();
}
if (isNullish(value)) {
return String(value);
}
if (Array.isArray(value)) {
return stringifyStructured(value);
}
if (isPlainRecord(value)) {
return stringifyStructured(value);
}
return "unknown error";
}
/**
* CHANGE: Normalize relative path inside target root.
* WHY: Collector should always emit POSIX separators for CORE.
* QUOTE(ТЗ): "Математические инварианты" — один путь = одно представление.
* REF: user-request-project-info
* FORMAT THEOREM: relative("", name) = name; relative(a,b) = `${a}/${b}`
* PURITY: CORE (helper used by SHELL)
* INVARIANT: Never starts/ends с "/"
* COMPLEXITY: O(1)
*/
function joinRelative(base, name) {
if (base.length === 0)
return name;
return `${base}/${name}`;
}
/**
* CHANGE: Build ProjectFileRecord from absolute path.
* WHY: Keep IO confined while delegating metrics to CORE.
* QUOTE(ТЗ): "Functional Core, Imperative Shell"
* REF: user-request-project-info
* FORMAT THEOREM: record.metrics = deriveFileContentMetrics(content, ext)
* PURITY: SHELL
* EFFECT: Effect<ProjectFileRecord | null, never>
* INVARIANT: Returns null when file reading fails
* COMPLEXITY: O(n) for reading file
*/
async function createFileRecord(absolutePath, relativePath) {
try {
const stats = await fsPromises.stat(absolutePath);
if (!stats.isFile()) {
return null;
}
const buffer = await fsPromises.readFile(absolutePath, "utf8");
const extension = path.extname(absolutePath).toLowerCase();
return {
relativePath,
sizeBytes: stats.size,
extension,
metrics: deriveFileContentMetrics(buffer, extension),
};
}
catch (error) {
const formatted = error instanceof Error ? error : String(error);
console.warn(`⚠️ Skipped ${relativePath} (${errorMessage(formatted)})`);
return null;
}
}
/**
* CHANGE: Recursively traverse directories with ignore set.
* WHY: Need deterministic ordering + ability to skip heavy dirs (node_modules, dist, ...)
* QUOTE(ТЗ): "CORE никогда не вызывает SHELL"
* REF: user-request-project-info
* FORMAT THEOREM: walk(dir) returns Σ child records
* PURITY: SHELL
* EFFECT: Effect<ReadonlyArray<ProjectFileRecord>, never>
* INVARIANT: Dir entries sorted lexicographically
* COMPLEXITY: O(n)
*/
async function walkDirectory(absoluteDir, relativeBase) {
const dirents = await fsPromises.readdir(absoluteDir, {
withFileTypes: true,
});
const records = [];
const sorted = [...dirents].sort((a, b) => a.name.localeCompare(b.name));
for (const dirent of sorted) {
const name = dirent.name;
const relativePath = joinRelative(relativeBase, name);
const absolutePath = path.join(absoluteDir, name);
if (dirent.isDirectory()) {
if (IGNORED_DIRECTORIES.has(name)) {
continue;
}
const nested = await walkDirectory(absolutePath, relativePath);
records.push(...nested);
continue;
}
if (dirent.isFile()) {
const record = await createFileRecord(absolutePath, relativePath);
if (record !== null) {
records.push(record);
}
}
}
return records;
}
/**
* CHANGE: Collect ProjectFileRecord array under Effect discipline.
* WHY: Expose safe API for runLinter without throwing.
* QUOTE(ТЗ): "Effect-TS для всех эффектов"
* REF: user-request-project-info
* SOURCE: n/a
* FORMAT THEOREM: collectEffect(target).success ⇒ Promise resolves with records
* PURITY: SHELL
* EFFECT: Effect<ReadonlyArray<ProjectFileRecord>, never>
* INVARIANT: Returns [] при ошибках доступа
* COMPLEXITY: O(n)
*/
export function collectProjectFilesEffect(targetPath) {
return Effect.tryPromise(async () => {
const absoluteTarget = path.resolve(process.cwd(), targetPath);
try {
const stats = await fsPromises.stat(absoluteTarget);
if (stats.isFile()) {
const single = await createFileRecord(absoluteTarget, path.basename(absoluteTarget));
return single === null ? [] : [single];
}
if (stats.isDirectory()) {
return await walkDirectory(absoluteTarget, "");
}
console.warn(`⚠️ Target ${targetPath} is neither file nor directory.`);
return [];
}
catch (error) {
const formatted = error instanceof Error ? error : String(error);
console.warn(`⚠️ Unable to read ${targetPath}: ${errorMessage(formatted)}`);
return [];
}
}).pipe(Effect.catchAll(() => Effect.succeed([])));
}
//# sourceMappingURL=collector.js.map