diff --git a/eslint.config.mjs b/eslint.config.mjs index 58306da60..8d1046583 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -67,22 +67,15 @@ export default [ }, }, - // Forcing function for the pino migration: warn on any new `console.*` in - // core source. Existing call sites carry an eslint-disable-next-line comment - // tagged `TODO(pino-migration)` so a `git grep` inventories what's left. - // Tests, bin scripts, and the logger module itself are exempt. - // CLI/server are exempt above (legitimate stdout output). - // Severity is `warn` (not `error`) for the baseline; future PRs can flip to - // `error` once the migration sweep lands. + // Forcing function for the pino migration. Severity is `error` β€” the + // codebase-wide migration is complete; new `console.*` in core source + // must fail lint. CLI/server are exempt above (legitimate stdout output). + // Tests, bin scripts, and the logger module itself remain exempt. { files: ['gitnexus/src/**/*.ts'], - ignores: [ - 'gitnexus/src/cli/**', - 'gitnexus/src/server/**', - 'gitnexus/src/core/logger.ts', - ], + ignores: ['gitnexus/src/cli/**', 'gitnexus/src/server/**', 'gitnexus/src/core/logger.ts'], rules: { - 'no-console': 'warn', + 'no-console': 'error', }, }, diff --git a/gitnexus/src/config/ignore-service.ts b/gitnexus/src/config/ignore-service.ts index f44a66cbd..ce1fda913 100644 --- a/gitnexus/src/config/ignore-service.ts +++ b/gitnexus/src/config/ignore-service.ts @@ -2,6 +2,7 @@ import ignore, { type Ignore } from 'ignore'; import fs from 'fs/promises'; import nodePath from 'path'; import type { Path } from 'path-scurry'; +import { logger } from '../core/logger.js'; const DEFAULT_IGNORE_LIST = new Set([ // Version Control @@ -365,8 +366,7 @@ export const loadIgnoreRules = async ( } catch (err: unknown) { const code = (err as NodeJS.ErrnoException).code; if (code !== 'ENOENT') { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn(` Warning: could not read ${filename}: ${(err as Error).message}`); + logger.warn(` Warning: could not read ${filename}: ${(err as Error).message}`); } } } diff --git a/gitnexus/src/core/embeddings/embedder.ts b/gitnexus/src/core/embeddings/embedder.ts index a4fe1cf1d..b37fb45f3 100644 --- a/gitnexus/src/core/embeddings/embedder.ts +++ b/gitnexus/src/core/embeddings/embedder.ts @@ -23,6 +23,7 @@ import { DEFAULT_EMBEDDING_CONFIG, type EmbeddingConfig, type ModelProgress } fr import { isHttpMode, getHttpDimensions, httpEmbed } from './http-client.js'; import { resolveEmbeddingConfig } from './config.js'; import { applyHfEnvOverrides } from './hf-env.js'; +import { logger } from '../logger.js'; /** * Check whether the onnxruntime-node package that @huggingface/transformers @@ -166,8 +167,7 @@ export const initEmbedder = async ( const isDev = process.env.NODE_ENV === 'development'; if (isDev) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log(`🧠 Loading embedding model: ${finalConfig.modelId}`); + logger.info(`🧠 Loading embedding model: ${finalConfig.modelId}`); } const progressCallback = onProgress @@ -193,17 +193,13 @@ export const initEmbedder = async ( for (const device of devicesToTry) { try { if (isDev && device === 'dml') { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log('πŸ”§ Trying DirectML (DirectX12) GPU backend...'); + logger.info('πŸ”§ Trying DirectML (DirectX12) GPU backend...'); } else if (isDev && device === 'cuda') { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log('πŸ”§ Trying CUDA GPU backend...'); + logger.info('πŸ”§ Trying CUDA GPU backend...'); } else if (isDev && device === 'cpu') { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log('πŸ”§ Using CPU backend...'); + logger.info('πŸ”§ Using CPU backend...'); } else if (isDev && device === 'wasm') { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log('πŸ”§ Using WASM backend (slower)...'); + logger.info('πŸ”§ Using WASM backend (slower)...'); } embedderInstance = await (pipeline as any)('feature-extraction', finalConfig.modelId, { @@ -226,18 +222,15 @@ export const initEmbedder = async ( : device === 'cuda' ? 'GPU (CUDA)' : device.toUpperCase(); - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log(`βœ… Using ${label} backend`); - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log('βœ… Embedding model loaded successfully'); + logger.info(`βœ… Using ${label} backend`); + logger.info('βœ… Embedding model loaded successfully'); } return embedderInstance!; } catch (deviceError) { if (isDev && (device === 'cuda' || device === 'dml')) { const gpuType = device === 'dml' ? 'DirectML' : 'CUDA'; - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log(`⚠️ ${gpuType} not available, falling back to CPU...`); + logger.info(`⚠️ ${gpuType} not available, falling back to CPU...`); } // Continue to next device in list if (device === devicesToTry[devicesToTry.length - 1]) { diff --git a/gitnexus/src/core/embeddings/embedding-pipeline.ts b/gitnexus/src/core/embeddings/embedding-pipeline.ts index f83239650..bcb072c50 100644 --- a/gitnexus/src/core/embeddings/embedding-pipeline.ts +++ b/gitnexus/src/core/embeddings/embedding-pipeline.ts @@ -44,6 +44,7 @@ import { } from '../lbug/schema.js'; import { loadVectorExtension } from '../lbug/lbug-adapter.js'; import { getExactScanLimit } from '../platform/capabilities.js'; +import { logger } from '../logger.js'; const isDev = process.env.NODE_ENV === 'development'; @@ -157,8 +158,7 @@ const queryEmbeddableNodes = async ( } } catch (error) { if (isDev) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn(`Query for ${label} nodes failed:`, error); + logger.warn({ error }, `Query for ${label} nodes failed:`); } } } @@ -213,8 +213,7 @@ const createVectorIndex = async ( return true; } catch (error) { if (isDev) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn('Vector index creation warning:', error); + logger.warn({ error }, 'Vector index creation warning:'); } return false; } @@ -258,8 +257,7 @@ export const runEmbeddingPipeline = async ( try { const vectorAvailable = await ensureVectorExtensionAvailable(); - // eslint-disable-next-line no-console -- TODO(pino-migration) - if (!vectorAvailable && isDev) console.warn(vectorUnavailableMessage); + logger.warn(vectorUnavailableMessage); // Phase 1: Load embedding model onProgress({ @@ -286,8 +284,7 @@ export const runEmbeddingPipeline = async ( }); if (isDev) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log('πŸ” Querying embeddable nodes...'); + logger.info('πŸ” Querying embeddable nodes...'); } // Phase 2: Query embeddable nodes @@ -329,8 +326,7 @@ export const runEmbeddingPipeline = async ( // (Kuzu forbids SET on vector-indexed properties; DELETE-then-INSERT is the sanctioned pattern) if (staleNodeIds.length > 0) { if (isDev) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log(`πŸ”„ Deleting ${staleNodeIds.length} stale embedding rows for re-embed`); + logger.info(`πŸ”„ Deleting ${staleNodeIds.length} stale embedding rows for re-embed`); } try { await executeWithReusedStatement( @@ -351,8 +347,7 @@ export const runEmbeddingPipeline = async ( } if (isDev) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log( + logger.info( `πŸ“¦ Incremental embeddings: ${beforeCount} total, ${existingEmbeddings.size} cached, ${staleNodeIds.length} stale, ${nodes.length} to embed`, ); } @@ -361,8 +356,7 @@ export const runEmbeddingPipeline = async ( const totalNodes = nodes.length; if (isDev) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log(`πŸ“Š Found ${totalNodes} embeddable nodes`); + logger.info(`πŸ“Š Found ${totalNodes} embeddable nodes`); } if (totalNodes === 0) { @@ -449,10 +443,9 @@ export const runEmbeddingPipeline = async ( ); } catch (chunkErr) { if (isDev) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn( + logger.warn( + { chunkErr }, `⚠️ AST chunking failed for ${node.label} "${node.name}" (${node.filePath}), falling back to character-based chunking:`, - chunkErr, ); } chunks = characterChunk(node.content, startLine, endLine, chunkSize, overlap); @@ -490,10 +483,9 @@ export const runEmbeddingPipeline = async ( try { embeddings = await embedBatch(subTexts); } catch (embedErr) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.error( + logger.error( + { embedErr }, `❌ embedBatch failed for ${subTexts.length} texts (first: "${subTexts[0]?.substring(0, 80)}..."):`, - embedErr, ); throw embedErr; } @@ -529,8 +521,7 @@ export const runEmbeddingPipeline = async ( }); if (isDev) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log('πŸ“‡ Creating vector index...'); + logger.info('πŸ“‡ Creating vector index...'); } const vectorIndexReady = await createVectorIndex(executeQuery); @@ -543,8 +534,7 @@ export const runEmbeddingPipeline = async ( }); if (isDev) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log( + logger.info( `βœ… Embedding pipeline complete! (${totalChunks} chunks from ${totalNodes} nodes)`, ); } @@ -558,8 +548,7 @@ export const runEmbeddingPipeline = async ( const errorMessage = error instanceof Error ? error.message : 'Unknown error'; if (isDev) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.error('❌ Embedding pipeline error:', error); + logger.error({ error }, '❌ Embedding pipeline error:'); } onProgress({ diff --git a/gitnexus/src/core/group/extractors/elixir-workspace-extractor.ts b/gitnexus/src/core/group/extractors/elixir-workspace-extractor.ts index d509a5870..7c184ba96 100644 --- a/gitnexus/src/core/group/extractors/elixir-workspace-extractor.ts +++ b/gitnexus/src/core/group/extractors/elixir-workspace-extractor.ts @@ -4,6 +4,7 @@ import type { CypherExecutor } from '../contract-extractor.js'; import type { GroupManifestLink, ContractRole } from '../types.js'; import { shouldIgnorePath, loadIgnoreRules } from '../../../config/ignore-service.js'; +import { logger } from '../../logger.js'; interface ElixirAppMeta { appName: string; modulePrefix: string; @@ -202,8 +203,7 @@ export async function extractElixirWorkspaceLinks( }; const existing = appsByName.get(manifest.appName); if (existing) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn( + logger.warn( `[elixir-workspace-extractor] duplicate app "${manifest.appName}" in "${groupPath}" and "${existing.groupPath}" β€” skipping "${groupPath}"`, ); continue; diff --git a/gitnexus/src/core/group/extractors/go-workspace-extractor.ts b/gitnexus/src/core/group/extractors/go-workspace-extractor.ts index adc339850..55e870090 100644 --- a/gitnexus/src/core/group/extractors/go-workspace-extractor.ts +++ b/gitnexus/src/core/group/extractors/go-workspace-extractor.ts @@ -4,6 +4,7 @@ import type { CypherExecutor } from '../contract-extractor.js'; import type { GroupManifestLink, ContractRole } from '../types.js'; import { shouldIgnorePath, loadIgnoreRules } from '../../../config/ignore-service.js'; +import { logger } from '../../logger.js'; interface GoModuleMeta { modulePath: string; groupPath: string; @@ -211,8 +212,7 @@ export async function extractGoWorkspaceLinks( }; const existing = modulesByPath.get(manifest.modulePath); if (existing) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn( + logger.warn( `[go-workspace-extractor] duplicate module "${manifest.modulePath}" in "${groupPath}" and "${existing.groupPath}" β€” skipping "${groupPath}"`, ); continue; diff --git a/gitnexus/src/core/group/extractors/grpc-extractor.ts b/gitnexus/src/core/group/extractors/grpc-extractor.ts index 4d7ad7932..c08ba7c36 100644 --- a/gitnexus/src/core/group/extractors/grpc-extractor.ts +++ b/gitnexus/src/core/group/extractors/grpc-extractor.ts @@ -5,6 +5,7 @@ import { createIgnoreFilter } from '../../../config/ignore-service.js'; import type { ContractExtractor, CypherExecutor } from '../contract-extractor.js'; import type { ExtractedContract, RepoHandle } from '../types.js'; import { readSafe } from './fs-utils.js'; +import { logger } from '../../logger.js'; import { GRPC_SCAN_GLOB, getPluginForFile, @@ -344,8 +345,7 @@ export function resolveProtoConflict( // services under a fabricated package-qualified contract id. if (winners.length !== 1) { const paths = candidates.map((c) => c.protoPath).join(', '); - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn( + logger.warn( `[grpc-extractor] Ambiguous proto resolution for service "${serviceName}" from ${sourceFilePath}: ${winners.length} candidates tied at score ${maxScore} among [${paths}] β€” skipping canonical contract`, ); return null; diff --git a/gitnexus/src/core/group/extractors/java-workspace-extractor.ts b/gitnexus/src/core/group/extractors/java-workspace-extractor.ts index 828e729ab..b6beed71c 100644 --- a/gitnexus/src/core/group/extractors/java-workspace-extractor.ts +++ b/gitnexus/src/core/group/extractors/java-workspace-extractor.ts @@ -4,6 +4,7 @@ import type { CypherExecutor } from '../contract-extractor.js'; import type { GroupManifestLink, ContractRole } from '../types.js'; import { shouldIgnorePath, loadIgnoreRules } from '../../../config/ignore-service.js'; +import { logger } from '../../logger.js'; interface JavaProjectMeta { groupId: string; artifactId: string; @@ -213,8 +214,7 @@ export async function extractJavaWorkspaceLinks( }; const existing = projectsByKey.get(key); if (existing) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn( + logger.warn( `[java-workspace-extractor] duplicate artifact "${key}" in "${groupPath}" and "${existing.groupPath}" β€” skipping "${groupPath}"`, ); continue; diff --git a/gitnexus/src/core/group/extractors/manifest-extractor.ts b/gitnexus/src/core/group/extractors/manifest-extractor.ts index bba74f554..ab9c97960 100644 --- a/gitnexus/src/core/group/extractors/manifest-extractor.ts +++ b/gitnexus/src/core/group/extractors/manifest-extractor.ts @@ -1,6 +1,7 @@ import type { ContractType, CrossLink, GroupManifestLink, StoredContract } from '../types.js'; import type { CypherExecutor } from '../contract-extractor.js'; +import { logger } from '../../logger.js'; export interface ManifestExtractResult { contracts: StoredContract[]; crossLinks: CrossLink[]; @@ -298,8 +299,7 @@ export class ManifestExtractor { // fail the whole manifest extraction. Unresolved contracts still // get a synthetic symbolUid below, so cross-impact can proceed. const message = err instanceof Error ? err.message : String(err); - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn( + logger.warn( `[manifest-extractor] resolveSymbol failed for ${link.type}:${link.contract} ` + `in ${repoPathKey}: ${message}`, ); diff --git a/gitnexus/src/core/group/extractors/node-workspace-extractor.ts b/gitnexus/src/core/group/extractors/node-workspace-extractor.ts index 8e8c7dc89..aa40ac088 100644 --- a/gitnexus/src/core/group/extractors/node-workspace-extractor.ts +++ b/gitnexus/src/core/group/extractors/node-workspace-extractor.ts @@ -4,6 +4,7 @@ import type { CypherExecutor } from '../contract-extractor.js'; import type { GroupManifestLink, ContractRole } from '../types.js'; import { shouldIgnorePath, loadIgnoreRules } from '../../../config/ignore-service.js'; +import { logger } from '../../logger.js'; interface PackageMeta { name: string; groupPath: string; @@ -205,8 +206,7 @@ export async function extractNodeWorkspaceLinks( }; const existing = packagesByName.get(manifest.name); if (existing) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn( + logger.warn( `[node-workspace-extractor] duplicate package name "${manifest.name}" in "${groupPath}" and "${existing.groupPath}" β€” skipping "${groupPath}"`, ); continue; diff --git a/gitnexus/src/core/group/extractors/python-workspace-extractor.ts b/gitnexus/src/core/group/extractors/python-workspace-extractor.ts index ccb3cd9f5..4453852a6 100644 --- a/gitnexus/src/core/group/extractors/python-workspace-extractor.ts +++ b/gitnexus/src/core/group/extractors/python-workspace-extractor.ts @@ -4,6 +4,7 @@ import type { CypherExecutor } from '../contract-extractor.js'; import type { GroupManifestLink, ContractRole } from '../types.js'; import { shouldIgnorePath, loadIgnoreRules } from '../../../config/ignore-service.js'; +import { logger } from '../../logger.js'; interface PythonPackageMeta { name: string; importName: string; @@ -204,8 +205,7 @@ export async function extractPythonWorkspaceLinks( }; const existing = packagesByImportName.get(manifest.importName); if (existing) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn( + logger.warn( `[python-workspace-extractor] duplicate package "${manifest.name}" in "${groupPath}" and "${existing.groupPath}" β€” skipping "${groupPath}"`, ); continue; diff --git a/gitnexus/src/core/group/extractors/rust-workspace-extractor.ts b/gitnexus/src/core/group/extractors/rust-workspace-extractor.ts index 29efb12f5..63fe7ea82 100644 --- a/gitnexus/src/core/group/extractors/rust-workspace-extractor.ts +++ b/gitnexus/src/core/group/extractors/rust-workspace-extractor.ts @@ -5,6 +5,7 @@ import type { GroupManifestLink, ContractRole } from '../types.js'; import { shouldIgnorePath } from '../../../config/ignore-service.js'; import { loadIgnoreRules } from '../../../config/ignore-service.js'; +import { logger } from '../../logger.js'; /** * Discover cross-crate contracts in a Rust workspace by reading each * member's `Cargo.toml` dependencies and scanning source files for @@ -224,8 +225,7 @@ export async function extractRustWorkspaceLinks( }; const existing = cratesByName.get(manifest.name); if (existing) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn( + logger.warn( `[rust-workspace-extractor] duplicate crate name "${manifest.name}" in "${groupPath}" and "${existing.groupPath}" β€” skipping "${groupPath}"`, ); continue; diff --git a/gitnexus/src/core/group/service.ts b/gitnexus/src/core/group/service.ts index dc942c7e5..de324e70b 100644 --- a/gitnexus/src/core/group/service.ts +++ b/gitnexus/src/core/group/service.ts @@ -14,6 +14,7 @@ import { } from './group-path-utils.js'; import { getDefaultGitnexusDir, getGroupDir, listGroups, readContractRegistry } from './storage.js'; import { syncGroup } from './sync.js'; +import { logger } from '../logger.js'; import type { ContractRegistry, CrossLink, @@ -170,13 +171,11 @@ async function loadContractRegistryResilient( contracts.push(row); } else { skippedCorrupt++; - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn('[group] skipping corrupt contract row in contracts.json'); + logger.warn('[group] skipping corrupt contract row in contracts.json'); } } catch { skippedCorrupt++; - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn('[group] skipping corrupt contract row in contracts.json'); + logger.warn('[group] skipping corrupt contract row in contracts.json'); } } } @@ -189,13 +188,11 @@ async function loadContractRegistryResilient( crossLinks.push(row); } else { skippedCorrupt++; - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn('[group] skipping corrupt crossLinks row in contracts.json'); + logger.warn('[group] skipping corrupt crossLinks row in contracts.json'); } } catch { skippedCorrupt++; - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn('[group] skipping corrupt crossLinks row in contracts.json'); + logger.warn('[group] skipping corrupt crossLinks row in contracts.json'); } } } diff --git a/gitnexus/src/core/group/sync.ts b/gitnexus/src/core/group/sync.ts index 4a2373f2b..48b367647 100644 --- a/gitnexus/src/core/group/sync.ts +++ b/gitnexus/src/core/group/sync.ts @@ -15,6 +15,7 @@ import type { CypherExecutor } from './contract-extractor.js'; import { writeContractRegistry } from './storage.js'; import type { ContractRegistry } from './types.js'; +import { logger } from '../logger.js'; export interface SyncOptions { extractorOverride?: | ((repo: RepoHandle) => Promise) @@ -198,8 +199,7 @@ export async function syncGroup(config: GroupConfig, opts?: SyncOptions): Promis allLinks = [...allLinks, ...wsResult.links]; if (opts?.verbose) { for (const s of wsResult.stats) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log( + logger.info( ` workspace-deps: discovered ${s.linkCount} cross-${s.ecosystem.toLowerCase()} links from ${s.projectCount} ${s.ecosystem} projects`, ); } @@ -218,8 +218,7 @@ export async function syncGroup(config: GroupConfig, opts?: SyncOptions): Promis for (const link of allLinks) { const dangling = [link.from, link.to].filter((r) => !knownRepos.has(r)); if (dangling.length > 0) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn( + logger.warn( `[group/sync] manifest link ${link.type}:${link.contract} references repos not in config.repos: ${dangling.join(', ')} β€” cross-links will use synthetic UIDs`, ); } @@ -230,8 +229,7 @@ export async function syncGroup(config: GroupConfig, opts?: SyncOptions): Promis autoContracts.push(...manifestResult.contracts); manifestCrossLinks = manifestResult.crossLinks; if (opts?.verbose) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log( + logger.info( ` manifest: ${manifestCrossLinks.length} cross-links from ${allLinks.length} links (${config.links.length} declared + ${allLinks.length - config.links.length} discovered)`, ); } diff --git a/gitnexus/src/core/ingestion/ast-cache.ts b/gitnexus/src/core/ingestion/ast-cache.ts index 69723abce..454c60df2 100644 --- a/gitnexus/src/core/ingestion/ast-cache.ts +++ b/gitnexus/src/core/ingestion/ast-cache.ts @@ -1,6 +1,7 @@ import { LRUCache } from 'lru-cache'; import Parser from 'tree-sitter'; +import { logger } from '../logger.js'; /** * Minimal structural shape consumers need when reading Trees back * through a phase-dependency boundary. Declared here so phases that @@ -49,8 +50,7 @@ export const createASTCache = (maxSize: number = 50): ASTCache => { // will hand freed memory to scope-resolution. (tree as unknown as { delete?: () => void }).delete?.(); } catch (e) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn('Failed to delete tree from WASM memory', e); + logger.warn({ e }, 'Failed to delete tree from WASM memory'); } }, }); diff --git a/gitnexus/src/core/ingestion/call-processor.ts b/gitnexus/src/core/ingestion/call-processor.ts index 203b14ede..6c59578b0 100644 --- a/gitnexus/src/core/ingestion/call-processor.ts +++ b/gitnexus/src/core/ingestion/call-processor.ts @@ -75,6 +75,7 @@ import { extractReturnTypeName, stripNullable } from './type-extractors/shared.j import type { LiteralTypeInferrer } from './type-extractors/types.js'; import type { SyntaxNode } from './utils/ast-helpers.js'; +import { logger } from '../logger.js'; /** Per-file resolved type bindings for exported symbols. * Populated during call processing, consumed by Phase 14 re-resolution pass. */ export type ExportedTypeMap = Map>; @@ -784,8 +785,7 @@ export const processCalls = async ( const query = new Parser.Query(lang, queryStr); matches = query.matches(tree.rootNode); } catch (queryError) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn(`Query error for ${file.path}:`, queryError); + logger.warn({ queryError }, `Query error for ${file.path}:`); continue; } @@ -1392,8 +1392,7 @@ export const processCalls = async ( if (skippedByLang && skippedByLang.size > 0) { for (const [lang, count] of skippedByLang.entries()) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn( + logger.warn( `[ingestion] Skipped ${count} ${lang} file(s) in call processing β€” ${lang} parser not available.`, ); } diff --git a/gitnexus/src/core/ingestion/cluster-enricher.ts b/gitnexus/src/core/ingestion/cluster-enricher.ts index f1291cb8b..06cd4d0cd 100644 --- a/gitnexus/src/core/ingestion/cluster-enricher.ts +++ b/gitnexus/src/core/ingestion/cluster-enricher.ts @@ -7,6 +7,7 @@ import { CommunityNode } from './community-processor.js'; +import { logger } from '../logger.js'; // ============================================================================ // TYPES // ============================================================================ @@ -128,8 +129,7 @@ export const enrichClusters = async ( enrichments.set(community.id, enrichment); } catch (error) { // On error, fallback to heuristic - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn(`Failed to enrich cluster ${community.id}:`, error); + logger.warn({ error }, `Failed to enrich cluster ${community.id}:`); enrichments.set(community.id, { name: community.heuristicLabel, keywords: [], @@ -211,8 +211,7 @@ Output JSON array: } } } catch (error) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn('Batch enrichment failed, falling back to heuristics:', error); + logger.warn({ error }, 'Batch enrichment failed, falling back to heuristics:'); // Fallback for this batch for (const community of batch) { enrichments.set(community.id, { diff --git a/gitnexus/src/core/ingestion/cobol/cobol-copy-expander.ts b/gitnexus/src/core/ingestion/cobol/cobol-copy-expander.ts index 156dc4a96..dda135b12 100644 --- a/gitnexus/src/core/ingestion/cobol/cobol-copy-expander.ts +++ b/gitnexus/src/core/ingestion/cobol/cobol-copy-expander.ts @@ -1,3 +1,4 @@ +import { logger } from '../../logger.js'; /** * COBOL COPY statement expansion engine. * @@ -454,8 +455,7 @@ export function expandCopies( if (visited.has(resolvedPath)) { if (!warnedCircular.has(resolvedPath)) { warnedCircular.add(resolvedPath); - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn( + logger.warn( `[cobol-copy-expander] Circular COPY detected: ${cs.target} (${resolvedPath}) ` + `includes itself. Skipping expansion.`, ); @@ -465,8 +465,7 @@ export function expandCopies( // Max depth exceeded β€” keep unexpanded if (depth >= maxDepth) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn( + logger.warn( `[cobol-copy-expander] Max expansion depth (${maxDepth}) reached for ` + `COPY ${cs.target} in ${srcPath}. Skipping expansion.`, ); @@ -477,8 +476,7 @@ export function expandCopies( if (++totalExpansions > MAX_TOTAL_EXPANSIONS) { if (!warnedCircular.has('__max_total__')) { warnedCircular.add('__max_total__'); - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn( + logger.warn( `[cobol-copy-expander] Max total expansions (${MAX_TOTAL_EXPANSIONS}) reached ` + `in ${srcPath}. Skipping further expansions.`, ); diff --git a/gitnexus/src/core/ingestion/filesystem-walker.ts b/gitnexus/src/core/ingestion/filesystem-walker.ts index 81ea95b95..4d6725e24 100644 --- a/gitnexus/src/core/ingestion/filesystem-walker.ts +++ b/gitnexus/src/core/ingestion/filesystem-walker.ts @@ -5,6 +5,7 @@ import path from 'path'; import { glob } from 'glob'; import { createIgnoreFilter } from '../../config/ignore-service.js'; +import { logger } from '../logger.js'; export interface FileEntry { path: string; content: string; @@ -74,12 +75,10 @@ export const walkRepositoryPaths = async ( if (skippedLarge > 0) { const isDefault = maxFileSizeBytes === DEFAULT_MAX_FILE_SIZE_BYTES; const suffix = isDefault ? ', likely generated/vendored' : ''; - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn(` Skipped ${skippedLarge} large files (>${maxFileSizeBytes / 1024}KB${suffix})`); + logger.warn(` Skipped ${skippedLarge} large files (>${maxFileSizeBytes / 1024}KB${suffix})`); if (isVerboseIngestionEnabled()) { for (const p of skippedLargePaths) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn(` - ${p}`); + logger.warn(` - ${p}`); } } } diff --git a/gitnexus/src/core/ingestion/heritage-processor.ts b/gitnexus/src/core/ingestion/heritage-processor.ts index 8e4de1bf2..2c973ad8e 100644 --- a/gitnexus/src/core/ingestion/heritage-processor.ts +++ b/gitnexus/src/core/ingestion/heritage-processor.ts @@ -34,6 +34,7 @@ import type { ResolutionContext } from './model/resolution-context.js'; import { TIER_CONFIDENCE } from './model/resolution-context.js'; import type { HeritageInfo } from './heritage-types.js'; +import { logger } from '../logger.js'; /** * Derive the heritage-resolution strategy for a language from its * `LanguageProvider`. This is the production wiring that `buildHeritageMap` @@ -237,8 +238,7 @@ export const processHeritage = async ( query = new Parser.Query(treeSitterLang, queryStr); matches = query.matches(tree.rootNode); } catch (queryError) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn(`Heritage query error for ${file.path}:`, queryError); + logger.warn({ queryError }, `Heritage query error for ${file.path}:`); continue; } @@ -268,8 +268,7 @@ export const processHeritage = async ( if (skippedByLang && skippedByLang.size > 0) { for (const [lang, count] of skippedByLang.entries()) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn( + logger.warn( `[ingestion] Skipped ${count} ${lang} file(s) in heritage processing β€” ${lang} parser not available.`, ); } diff --git a/gitnexus/src/core/ingestion/import-processor.ts b/gitnexus/src/core/ingestion/import-processor.ts index a8bcf2a69..03cbed5b7 100644 --- a/gitnexus/src/core/ingestion/import-processor.ts +++ b/gitnexus/src/core/ingestion/import-processor.ts @@ -27,6 +27,7 @@ import type { SyntaxNode } from './utils/ast-helpers.js'; import { isDev } from './utils/env.js'; import { isRegistryPrimary } from './registry-primary-flag.js'; +import { logger } from '../logger.js'; // Type: Map> // Stores all files that a given file imports from export type ImportMap = Map>; @@ -324,22 +325,18 @@ export const processImports = async ( matches = query.matches(tree.rootNode); } catch (queryError: any) { if (isDev) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.group(`πŸ”΄ Query Error: ${file.path}`); - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log('Language:', language); - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log('Query (first 200 chars):', queryStr.substring(0, 200) + '...'); - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log('Error:', queryError?.message || queryError); - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log('File content (first 300 chars):', file.content.substring(0, 300)); - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log('AST root type:', tree.rootNode?.type); - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log('AST has errors:', tree.rootNode?.hasError); - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.groupEnd(); + logger.error( + { + file: file.path, + language, + err: queryError?.message || queryError, + queryPreview: queryStr.substring(0, 200) + '...', + contentPreview: file.content.substring(0, 300), + astRootType: tree.rootNode?.type, + astHasError: tree.rootNode?.hasError, + }, + 'tree-sitter query error', + ); } if (wasReparsed) (tree as unknown as { delete?: () => void }).delete?.(); @@ -354,8 +351,7 @@ export const processImports = async ( const sourceNode = captureMap['import.source']; if (!sourceNode) { if (isDev) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log(`⚠️ Import captured but no source node in ${file.path}`); + logger.info(`⚠️ Import captured but no source node in ${file.path}`); } return; } @@ -408,16 +404,14 @@ export const processImports = async ( if (skippedByLang && skippedByLang.size > 0) { for (const [lang, count] of skippedByLang.entries()) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn( + logger.warn( `[ingestion] Skipped ${count} ${lang} file(s) in import processing β€” ${lang} parser not available.`, ); } } if (isDev) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log( + logger.info( `πŸ“Š Import processing complete: ${getResolvedCount()}/${totalImportsFound} imports resolved to graph edges`, ); } @@ -509,8 +503,7 @@ export const processImportsFromExtracted = async ( ); if (isDev) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log( + logger.info( `πŸ“Š Import processing (fast path): ${getResolvedCount()}/${totalImportsFound} imports resolved to graph edges`, ); } diff --git a/gitnexus/src/core/ingestion/language-config.ts b/gitnexus/src/core/ingestion/language-config.ts index a14a0d2ab..f51ef57c6 100644 --- a/gitnexus/src/core/ingestion/language-config.ts +++ b/gitnexus/src/core/ingestion/language-config.ts @@ -4,6 +4,7 @@ import type { ImportConfigs } from './import-resolvers/types.js'; import { isDev } from './utils/env.js'; +import { logger } from '../logger.js'; // ============================================================================ // LANGUAGE-SPECIFIC CONFIG TYPES // ============================================================================ @@ -82,8 +83,7 @@ export async function loadTsconfigPaths(repoRoot: string): Promise 0) { if (isDev) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log(`πŸ“¦ Loaded ${aliases.size} path aliases from ${filename}`); + logger.info(`πŸ“¦ Loaded ${aliases.size} path aliases from ${filename}`); } return { aliases, baseUrl }; } @@ -105,8 +105,7 @@ export async function loadGoModulePath(repoRoot: string): Promise 0) { if (isDev) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log(`πŸ“¦ Loaded ${targets.size} Swift package targets`); + logger.info(`πŸ“¦ Loaded ${targets.size} Swift package targets`); } return { targets }; } diff --git a/gitnexus/src/core/ingestion/method-extractors/generic.ts b/gitnexus/src/core/ingestion/method-extractors/generic.ts index 886f0756d..f02faef35 100644 --- a/gitnexus/src/core/ingestion/method-extractors/generic.ts +++ b/gitnexus/src/core/ingestion/method-extractors/generic.ts @@ -8,6 +8,7 @@ */ import type { SyntaxNode } from '../utils/ast-helpers.js'; +import { logger } from '../../logger.js'; import type { MethodExtractor, MethodExtractorContext, @@ -158,8 +159,7 @@ function findBodies(node: SyntaxNode, bodyNodeSet: Set): SyntaxNode[] { // Fallback: body field exists but its type is not in bodyNodeTypes. // This may indicate a config typo β€” log for debugging if NODE_ENV is development. if (process.env.NODE_ENV === 'development') { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn( + logger.warn( `[MethodExtractor] body field type '${bodyField.type}' not in bodyNodeTypes for node '${node.type}'`, ); } diff --git a/gitnexus/src/core/ingestion/parsing-processor.ts b/gitnexus/src/core/ingestion/parsing-processor.ts index e7191e554..8803ec023 100644 --- a/gitnexus/src/core/ingestion/parsing-processor.ts +++ b/gitnexus/src/core/ingestion/parsing-processor.ts @@ -34,6 +34,7 @@ import { import type { LanguageProvider } from './language-provider.js'; import type { ParsedFile } from 'gitnexus-shared'; import { WorkerPool } from './workers/worker-pool.js'; +import { logger } from '../logger.js'; import type { ParseWorkerResult, ParseWorkerInput, @@ -191,8 +192,7 @@ const processParsingWithWorkers = async ( const summary = Array.from(skippedLanguages.entries()) .map(([lang, count]) => `${lang}: ${count}`) .join(', '); - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn(` Skipped unsupported languages: ${summary}`); + logger.warn(` Skipped unsupported languages: ${summary}`); } // Final progress @@ -383,8 +383,7 @@ const processParsingSequential = async ( bufferSize: getTreeSitterBufferSize(parseContent), }); } catch (parseError) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn(`Skipping unparseable file: ${file.path}`); + logger.warn(`Skipping unparseable file: ${file.path}`); continue; } @@ -410,8 +409,7 @@ const processParsingSequential = async ( query = new Parser.Query(language, queryString); matches = query.matches(tree.rootNode); } catch (queryError) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn(`Query error for ${file.path}:`, queryError); + logger.warn({ queryError }, `Query error for ${file.path}:`); continue; } @@ -704,8 +702,7 @@ const processParsingSequential = async ( if (skippedByLang && skippedByLang.size > 0) { for (const [lang, count] of skippedByLang.entries()) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn( + logger.warn( `[ingestion] Skipped ${count} ${lang} file(s) in parsing processing β€” ${lang} parser not available.`, ); } @@ -746,8 +743,7 @@ export const processParsing = async ( // in scope-resolution with an empty cache and get re-parsed. // Surfacing this in PROF mode prevents silent perf cliffs when // a repo crosses the worker-pool threshold. - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn( + logger.warn( `[scope-resolution prof] worker pool engaged for ${files.length} files β€” cross-phase tree cache will be empty; scope-resolution re-parses.`, ); } @@ -762,8 +758,7 @@ export const processParsing = async ( ); } catch (err) { const message = err instanceof Error ? err.message : String(err); - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn('Worker pool parsing stopped; continuing with sequential parser:', message); + logger.warn({ message }, 'Worker pool parsing stopped; continuing with sequential parser:'); reportProgress?.( lastProgress, files.length, diff --git a/gitnexus/src/core/ingestion/pipeline-phases/cobol.ts b/gitnexus/src/core/ingestion/pipeline-phases/cobol.ts index 2ead44be3..c9332aabc 100644 --- a/gitnexus/src/core/ingestion/pipeline-phases/cobol.ts +++ b/gitnexus/src/core/ingestion/pipeline-phases/cobol.ts @@ -15,6 +15,7 @@ import { readFileContents } from '../filesystem-walker.js'; import type { StructureOutput } from './structure.js'; import { isDev } from '../utils/env.js'; +import { logger } from '../../logger.js'; export interface CobolOutput { programs: number; paragraphs: number; @@ -47,8 +48,7 @@ export const cobolPhase: PipelinePhase = { const cobolResult = processCobol(ctx.graph, cobolFiles, allPathSet); if (isDev) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log( + logger.info( ` COBOL: ${cobolResult.programs} programs, ${cobolResult.paragraphs} paragraphs, ${cobolResult.sections} sections from ${cobolFiles.length} files`, ); if ( @@ -56,14 +56,12 @@ export const cobolPhase: PipelinePhase = { cobolResult.execCicsBlocks > 0 || cobolResult.entryPoints > 0 ) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log( + logger.info( ` COBOL enriched: ${cobolResult.execSqlBlocks} SQL blocks, ${cobolResult.execCicsBlocks} CICS blocks, ${cobolResult.entryPoints} entry points, ${cobolResult.moves} moves, ${cobolResult.fileDeclarations} file declarations`, ); } if (cobolResult.jclJobs > 0) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log(` JCL: ${cobolResult.jclJobs} jobs, ${cobolResult.jclSteps} steps`); + logger.info(` JCL: ${cobolResult.jclJobs} jobs, ${cobolResult.jclSteps} steps`); } } diff --git a/gitnexus/src/core/ingestion/pipeline-phases/communities.ts b/gitnexus/src/core/ingestion/pipeline-phases/communities.ts index d68821871..0e29b6cc2 100644 --- a/gitnexus/src/core/ingestion/pipeline-phases/communities.ts +++ b/gitnexus/src/core/ingestion/pipeline-phases/communities.ts @@ -15,6 +15,7 @@ import type { StructureOutput } from './structure.js'; import { processCommunities, type CommunityDetectionResult } from '../community-processor.js'; import { isDev } from '../utils/env.js'; +import { logger } from '../../logger.js'; export interface CommunitiesOutput { communityResult: CommunityDetectionResult; } @@ -47,8 +48,7 @@ export const communitiesPhase: PipelinePhase = { }); if (isDev) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log( + logger.info( `🏘️ Community detection: ${communityResult.stats.totalCommunities} communities found (modularity: ${communityResult.stats.modularity.toFixed(3)})`, ); } diff --git a/gitnexus/src/core/ingestion/pipeline-phases/cross-file-impl.ts b/gitnexus/src/core/ingestion/pipeline-phases/cross-file-impl.ts index a8dd94d44..5c014ed73 100644 --- a/gitnexus/src/core/ingestion/pipeline-phases/cross-file-impl.ts +++ b/gitnexus/src/core/ingestion/pipeline-phases/cross-file-impl.ts @@ -23,6 +23,7 @@ import { topologicalLevelSort } from '../utils/graph-sort.js'; import type { KnowledgeGraph } from '../../graph/types.js'; import { isDev } from '../utils/env.js'; +import { logger } from '../../logger.js'; /** Max AST trees to keep in LRU cache for cross-file binding propagation. */ const AST_CACHE_CAP = 50; @@ -60,8 +61,7 @@ export async function runCrossFileBindingPropagation( const { levels, cycleCount } = topologicalLevelSort(ctx.importMap); if (isDev && cycleCount > 0) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log(`πŸ”„ ${cycleCount} files in import cycles (processed last in undefined order)`); + logger.info(`πŸ”„ ${cycleCount} files in import cycles (processed last in undefined order)`); } let filesWithGaps = 0; @@ -89,8 +89,7 @@ export async function runCrossFileBindingPropagation( const gapRatio = totalFiles > 0 ? filesWithGaps / totalFiles : 0; if (gapRatio < CROSS_FILE_SKIP_THRESHOLD && filesWithGaps < gapThreshold) { if (isDev) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log( + logger.info( `⏭️ Cross-file re-resolution skipped (${filesWithGaps}/${totalFiles} files, ${(gapRatio * 100).toFixed(1)}% < ${CROSS_FILE_SKIP_THRESHOLD * 100}% threshold)`, ); } @@ -195,8 +194,7 @@ export async function runCrossFileBindingPropagation( if (crossFileResolved >= MAX_CROSS_FILE_REPROCESS) { if (isDev) - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log(`⚠️ Cross-file re-resolution capped at ${MAX_CROSS_FILE_REPROCESS} files`); + logger.info(`⚠️ Cross-file re-resolution capped at ${MAX_CROSS_FILE_REPROCESS} files`); break; } } @@ -207,8 +205,7 @@ export async function runCrossFileBindingPropagation( const elapsed = Date.now() - crossFileStart; const totalElapsed = Date.now() - pipelineStart; const reResolutionPct = totalElapsed > 0 ? ((elapsed / totalElapsed) * 100).toFixed(1) : '0'; - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log( + logger.info( `πŸ”— Cross-file re-resolution: ${crossFileResolved} candidates re-processed` + ` in ${elapsed}ms (${reResolutionPct}% of total ingestion time so far)`, ); diff --git a/gitnexus/src/core/ingestion/pipeline-phases/cross-file.ts b/gitnexus/src/core/ingestion/pipeline-phases/cross-file.ts index ac3ee54c7..3ea604b4d 100644 --- a/gitnexus/src/core/ingestion/pipeline-phases/cross-file.ts +++ b/gitnexus/src/core/ingestion/pipeline-phases/cross-file.ts @@ -36,6 +36,7 @@ import type { ParseOutput } from './parse.js'; import { runCrossFileBindingPropagation } from './cross-file-impl.js'; import { isDev } from '../utils/env.js'; +import { logger } from '../../logger.js'; export interface CrossFileOutput { /** Number of files re-processed during cross-file propagation. */ filesReprocessed: number; @@ -59,13 +60,11 @@ export const crossFilePhase: PipelinePhase = { if (isDev) { if (bindingAccumulator.totalBindings > 0) { const memKB = Math.round(bindingAccumulator.estimateMemoryBytes() / 1024); - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log( + logger.info( `πŸ“¦ BindingAccumulator: ${bindingAccumulator.totalBindings} bindings across ${bindingAccumulator.fileCount} files (~${memKB} KB)`, ); } else if (totalFiles > 0) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log( + logger.info( `πŸ“¦ BindingAccumulator: EMPTY β€” 0 bindings across 0 files despite ${totalFiles} parsed files. If the codebase has typed bindings, this indicates an upstream regression.`, ); } diff --git a/gitnexus/src/core/ingestion/pipeline-phases/markdown.ts b/gitnexus/src/core/ingestion/pipeline-phases/markdown.ts index 43631a3a9..dd57518c1 100644 --- a/gitnexus/src/core/ingestion/pipeline-phases/markdown.ts +++ b/gitnexus/src/core/ingestion/pipeline-phases/markdown.ts @@ -15,6 +15,7 @@ import { readFileContents } from '../filesystem-walker.js'; import type { StructureOutput } from './structure.js'; import { isDev } from '../utils/env.js'; +import { logger } from '../../logger.js'; export interface MarkdownOutput { /** Number of markdown sections extracted. */ sections: number; @@ -48,8 +49,7 @@ export const markdownPhase: PipelinePhase = { const mdResult = processMarkdown(ctx.graph, mdFiles, allPathSet); if (isDev) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log( + logger.info( ` Markdown: ${mdResult.sections} sections, ${mdResult.links} cross-links from ${mdFiles.length} files`, ); } diff --git a/gitnexus/src/core/ingestion/pipeline-phases/mro.ts b/gitnexus/src/core/ingestion/pipeline-phases/mro.ts index 9f5bbb0a2..c098f2b7b 100644 --- a/gitnexus/src/core/ingestion/pipeline-phases/mro.ts +++ b/gitnexus/src/core/ingestion/pipeline-phases/mro.ts @@ -15,6 +15,7 @@ import type { StructureOutput } from './structure.js'; import { computeMRO } from '../mro-processor.js'; import { isDev } from '../utils/env.js'; +import { logger } from '../../logger.js'; export interface MROOutput { entries: number; ambiguityCount: number; @@ -42,8 +43,7 @@ export const mroPhase: PipelinePhase = { const mroResult = computeMRO(ctx.graph); if (isDev && mroResult.entries.length > 0) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log( + logger.info( `πŸ”€ MRO: ${mroResult.entries.length} classes analyzed, ${mroResult.ambiguityCount} ambiguities, ${mroResult.overrideEdges} METHOD_OVERRIDES, ${mroResult.methodImplementsEdges} METHOD_IMPLEMENTS`, ); } diff --git a/gitnexus/src/core/ingestion/pipeline-phases/orm.ts b/gitnexus/src/core/ingestion/pipeline-phases/orm.ts index 2f6082f76..4e6021efa 100644 --- a/gitnexus/src/core/ingestion/pipeline-phases/orm.ts +++ b/gitnexus/src/core/ingestion/pipeline-phases/orm.ts @@ -16,6 +16,7 @@ import type { ExtractedORMQuery } from '../workers/parse-worker.js'; import type { KnowledgeGraph } from '../../graph/types.js'; import { isDev } from '../utils/env.js'; +import { logger } from '../../logger.js'; export interface ORMOutput { edgesCreated: number; modelCount: number; @@ -91,8 +92,7 @@ function processORMQueries( } if (isDev) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log( + logger.info( `ORM dataflow: ${edgesCreated} QUERIES edges, ${modelNodes.size} models (${queries.length} total calls)`, ); } diff --git a/gitnexus/src/core/ingestion/pipeline-phases/parse-impl.ts b/gitnexus/src/core/ingestion/pipeline-phases/parse-impl.ts index b6f1c7047..bd39a4330 100644 --- a/gitnexus/src/core/ingestion/pipeline-phases/parse-impl.ts +++ b/gitnexus/src/core/ingestion/pipeline-phases/parse-impl.ts @@ -69,6 +69,7 @@ import { isDev } from '../utils/env.js'; import { synthesizeWildcardImportBindings, needsSynthesis } from './wildcard-synthesis.js'; import { extractORMQueriesInline } from './orm-extraction.js'; +import { logger } from '../../logger.js'; // ── Constants ────────────────────────────────────────────────────────────── /** Max bytes of source content to load per parse chunk. */ @@ -136,8 +137,7 @@ export async function runChunkedParseAndResolve( } } for (const [lang, count] of skippedByLang) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn( + logger.warn( `Skipping ${count} ${lang} file(s) β€” ${lang} parser not available (native binding may not have built). Try: npm rebuild tree-sitter-${lang}`, ); } @@ -172,8 +172,7 @@ export async function runChunkedParseAndResolve( if (isDev) { const totalMB = parseableScanned.reduce((s, f) => s + f.size, 0) / (1024 * 1024); - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log( + logger.info( `πŸ“‚ Scan: ${totalFiles} paths, ${totalParseable} parseable (${totalMB.toFixed(0)}MB), ${numChunks} chunks @ ${CHUNK_BYTE_BUDGET / (1024 * 1024)}MB budget`, ); } @@ -222,10 +221,9 @@ export async function runChunkedParseAndResolve( } workerPool = createWorkerPool(workerUrl); } catch (err) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn( + logger.warn( + { err: (err as Error).message }, 'Worker pool creation failed, using sequential fallback:', - (err as Error).message, ); } } @@ -342,8 +340,7 @@ export async function runChunkedParseAndResolve( exportedTypeMap, ); if (isDev && enrichedCount > 0) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log( + logger.info( `πŸ”— E1: Seeded ${enrichedCount} cross-file receiver types (chunk ${chunkIdx + 1})`, ); } @@ -542,8 +539,7 @@ export async function runChunkedParseAndResolve( const rcStats = ctx.getStats(); const total = rcStats.cacheHits + rcStats.cacheMisses; const hitRate = total > 0 ? ((rcStats.cacheHits / total) * 100).toFixed(1) : '0'; - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log( + logger.info( `πŸ” Resolution cache: ${rcStats.cacheHits} hits, ${rcStats.cacheMisses} misses (${hitRate}% hit rate)`, ); } @@ -559,17 +555,15 @@ export async function runChunkedParseAndResolve( bindingAccumulator.finalize(); const enriched = enrichExportedTypeMap(bindingAccumulator, graph, exportedTypeMap); if (isDev && enriched > 0) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log( + logger.info( `πŸ”— Worker TypeEnv enrichment: ${enriched} fixpoint-inferred exports added to ExportedTypeMap`, ); } } catch (enrichErr) { if (isDev) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn( + logger.warn( + { err: (enrichErr as Error).message }, 'Post-fallback finalize/enrich failed during cleanup:', - (enrichErr as Error).message, ); } } @@ -578,8 +572,7 @@ export async function runChunkedParseAndResolve( if (!hasSynthesized) { const synthesized = synthesizeWildcardImportBindings(graph, ctx); if (isDev && synthesized > 0) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log( + logger.info( `πŸ”— Synthesized ${synthesized} additional wildcard import bindings (Go/Ruby/C++/Swift/Python)`, ); } diff --git a/gitnexus/src/core/ingestion/pipeline-phases/processes.ts b/gitnexus/src/core/ingestion/pipeline-phases/processes.ts index 4358d24e5..166faea20 100644 --- a/gitnexus/src/core/ingestion/pipeline-phases/processes.ts +++ b/gitnexus/src/core/ingestion/pipeline-phases/processes.ts @@ -19,6 +19,7 @@ import { processProcesses, type ProcessDetectionResult } from '../process-proces import { generateId } from '../../../lib/utils.js'; import { isDev } from '../utils/env.js'; +import { logger } from '../../logger.js'; export interface ProcessesOutput { processResult: ProcessDetectionResult; } @@ -67,8 +68,7 @@ export const processesPhase: PipelinePhase = { ); if (isDev) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log( + logger.info( `πŸ”„ Process detection: ${processResult.stats.totalProcesses} processes found (${processResult.stats.crossCommunityCount} cross-community)`, ); } @@ -168,8 +168,7 @@ export const processesPhase: PipelinePhase = { } } if (isDev && linked > 0) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log(`πŸ”— Linked ${linked} Route/Tool nodes to execution flows`); + logger.info(`πŸ”— Linked ${linked} Route/Tool nodes to execution flows`); } } diff --git a/gitnexus/src/core/ingestion/pipeline-phases/routes.ts b/gitnexus/src/core/ingestion/pipeline-phases/routes.ts index 3dc72c114..de3a8ddb8 100644 --- a/gitnexus/src/core/ingestion/pipeline-phases/routes.ts +++ b/gitnexus/src/core/ingestion/pipeline-phases/routes.ts @@ -32,6 +32,7 @@ import { generateId } from '../../../lib/utils.js'; import { readFileContents } from '../filesystem-walker.js'; import { isDev } from '../utils/env.js'; +import { logger } from '../../logger.js'; const EXPO_NAV_PATTERNS = [ /router\.(push|replace|navigate)\(\s*['"`]([^'"`]+)['"`]/g, /]*href=\s*['"`]([^'"`]+)['"`]/g, @@ -174,8 +175,7 @@ export const routesPhase: PipelinePhase = { } if (isDev) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log( + logger.info( `πŸ—ΊοΈ Route registry: ${routeRegistry.size} routes${duplicateRoutes > 0 ? ` (${duplicateRoutes} duplicate URLs skipped)` : ''}`, ); } @@ -225,8 +225,7 @@ export const routesPhase: PipelinePhase = { linkedCount++; } if (isDev && linkedCount > 0) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log( + logger.info( `πŸ›‘οΈ Linked ${mwPath} middleware [${mwLabel.join(', ')}] to ${linkedCount} routes`, ); } @@ -292,8 +291,7 @@ export const routesPhase: PipelinePhase = { processNextjsFetchRoutes(ctx.graph, allFetchCalls, routeURLToFile, consumerContents); if (isDev) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log( + logger.info( `πŸ”— Processed ${allFetchCalls.length} fetch() calls against ${routeRegistry.size} routes`, ); } diff --git a/gitnexus/src/core/ingestion/pipeline-phases/runner.ts b/gitnexus/src/core/ingestion/pipeline-phases/runner.ts index d5c789e13..0bfc45bd4 100644 --- a/gitnexus/src/core/ingestion/pipeline-phases/runner.ts +++ b/gitnexus/src/core/ingestion/pipeline-phases/runner.ts @@ -15,6 +15,7 @@ import type { PipelinePhase, PipelineContext, PhaseResult } from './types.js'; import { isDev } from '../utils/env.js'; +import { logger } from '../../logger.js'; /** * Validate that the phases form a valid dependency graph (no cycles, all deps present). * Returns phases in topological execution order. @@ -176,8 +177,7 @@ export async function runPipeline( const start = Date.now(); if (isDev) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log(`β–Ά Phase: ${phase.name}`); + logger.info(`β–Ά Phase: ${phase.name}`); } // Only expose declared dependencies β€” prevents hidden coupling to undeclared phases. @@ -221,8 +221,7 @@ export async function runPipeline( }); if (isDev) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log(`βœ“ Phase: ${phase.name} (${durationMs}ms)`); + logger.info(`βœ“ Phase: ${phase.name} (${durationMs}ms)`); } } diff --git a/gitnexus/src/core/ingestion/pipeline-phases/tools.ts b/gitnexus/src/core/ingestion/pipeline-phases/tools.ts index 053a5a6f0..32a0ae71f 100644 --- a/gitnexus/src/core/ingestion/pipeline-phases/tools.ts +++ b/gitnexus/src/core/ingestion/pipeline-phases/tools.ts @@ -16,6 +16,7 @@ import { generateId } from '../../../lib/utils.js'; import { readFileContents } from '../filesystem-walker.js'; import { isDev } from '../utils/env.js'; +import { logger } from '../../logger.js'; export interface ToolDef { name: string; filePath: string; @@ -104,8 +105,7 @@ export const toolsPhase: PipelinePhase = { } if (isDev) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log(`πŸ”§ Tool registry: ${toolDefs.length} tools detected`); + logger.info(`πŸ”§ Tool registry: ${toolDefs.length} tools detected`); } } diff --git a/gitnexus/src/core/ingestion/process-processor.ts b/gitnexus/src/core/ingestion/process-processor.ts index 10228008b..aa744e54d 100644 --- a/gitnexus/src/core/ingestion/process-processor.ts +++ b/gitnexus/src/core/ingestion/process-processor.ts @@ -17,6 +17,7 @@ import { calculateEntryPointScore, isTestFile } from './entry-point-scoring.js'; import { SupportedLanguages } from 'gitnexus-shared'; import { isDev } from './utils/env.js'; +import { logger } from '../logger.js'; // ============================================================================ // CONFIGURATION // ============================================================================ @@ -319,16 +320,13 @@ const findEntryPoints = ( // DEBUG: Log top candidates with new scoring details if (sorted.length > 0 && isDev) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log(`[Process] Top 10 entry point candidates (new scoring):`); + logger.info(`[Process] Top 10 entry point candidates (new scoring):`); sorted.slice(0, 10).forEach((c, i) => { const node = graph.getNode(c.id); const exported = node?.properties.isExported ? 'βœ“' : 'βœ—'; const shortPath = node?.properties.filePath?.split('/').slice(-2).join('/') || ''; - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log(` ${i + 1}. ${node?.properties.name} [exported:${exported}] (${shortPath})`); - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log(` score: ${c.score.toFixed(2)} = [${c.reasons.join(' Γ— ')}]`); + logger.info(` ${i + 1}. ${node?.properties.name} [exported:${exported}] (${shortPath})`); + logger.info(` score: ${c.score.toFixed(2)} = [${c.reasons.join(' Γ— ')}]`); }); } diff --git a/gitnexus/src/core/ingestion/scope-extractor-bridge.ts b/gitnexus/src/core/ingestion/scope-extractor-bridge.ts index fc63f29bf..41a50193d 100644 --- a/gitnexus/src/core/ingestion/scope-extractor-bridge.ts +++ b/gitnexus/src/core/ingestion/scope-extractor-bridge.ts @@ -28,6 +28,7 @@ import type { ParsedFile } from 'gitnexus-shared'; import { extract as extractScope } from './scope-extractor.js'; import type { LanguageProvider } from './language-provider.js'; +import { logger } from '../logger.js'; /** Callback used to report scope-extraction warnings to the host (worker or direct). */ export type ScopeBridgeWarn = (message: string) => void; @@ -53,8 +54,7 @@ export function extractParsedFile( err instanceof Error ? err.message : String(err) }`; if (onWarn !== undefined) onWarn(message); - // eslint-disable-next-line no-console -- TODO(pino-migration) - else console.warn(message); + logger.warn(message); return undefined; } } diff --git a/gitnexus/src/core/ingestion/scope-resolution/pipeline/phase.ts b/gitnexus/src/core/ingestion/scope-resolution/pipeline/phase.ts index 900a7f1fd..c2fda9777 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/pipeline/phase.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/pipeline/phase.ts @@ -38,6 +38,7 @@ import { runScopeResolution } from './run.js'; import { SCOPE_RESOLVERS } from './registry.js'; import { isDev, isSemanticModelValidatorEnabled } from '../../utils/env.js'; +import { logger } from '../../../logger.js'; export interface ScopeResolutionOutput { /** True when at least one language ran. */ readonly ran: boolean; @@ -144,8 +145,7 @@ export const scopeResolutionPhase: PipelinePhase = { resolutionConfig, onWarn: (msg) => { if (isSemanticModelValidatorEnabled()) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn(`[scope-resolution:${lang}] ${msg}`); + logger.warn(`[scope-resolution:${lang}] ${msg}`); } }, }, @@ -163,8 +163,7 @@ export const scopeResolutionPhase: PipelinePhase = { }); if (isDev) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log( + logger.info( `[scope-resolution:${lang}] ${stats.filesProcessed} files β†’ ${stats.importsEmitted} IMPORTS + ${stats.referenceEdgesEmitted} reference edges (${stats.resolve.unresolved} unresolved sites, ${stats.referenceSkipped} skipped)`, ); } diff --git a/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts b/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts index 947db5eb7..558c9ef30 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts @@ -41,6 +41,7 @@ import { emitImportEdges } from '../graph-bridge/imports-to-edges.js'; import type { ScopeResolver } from '../contract/scope-resolver.js'; import { buildWorkspaceResolutionIndex } from '../workspace-index.js'; +import { logger } from '../../../logger.js'; interface RunScopeResolutionInput { readonly graph: KnowledgeGraph; /** @@ -279,8 +280,7 @@ export function runScopeResolution( if (PROF) { const tEnd = process.hrtime.bigint(); const ns = (a: bigint, b: bigint): number => Number(b - a) / 1_000_000; - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn( + logger.warn( `[scope-resolution prof] extract=${ns(tStart, tExtract).toFixed(0)}ms` + ` finalize=${ns(tExtract, tFinalize).toFixed(0)}ms` + ` propagate=${ns(tFinalize, tPropagate).toFixed(0)}ms` + diff --git a/gitnexus/src/core/ingestion/type-env.ts b/gitnexus/src/core/ingestion/type-env.ts index 011655f5f..a38df617b 100644 --- a/gitnexus/src/core/ingestion/type-env.ts +++ b/gitnexus/src/core/ingestion/type-env.ts @@ -24,6 +24,7 @@ import { import type { SemanticModel } from './model/index.js'; import type { NodeLabel } from 'gitnexus-shared'; +import { logger } from '../logger.js'; /** * Per-file scoped type environment: maps (scope, variableName) β†’ typeName. * Scope-aware: variables inside functions are keyed by function name, @@ -769,8 +770,7 @@ const resolveFixpointBindings = ( if (iter === MAX_FIXPOINT_ITERATIONS - 1 && process.env.GITNEXUS_DEBUG) { const unresolved = pendingItems.length - resolved.size; if (unresolved > 0) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn( + logger.warn( `[type-env] fixpoint hit iteration cap (${MAX_FIXPOINT_ITERATIONS}), ${unresolved} items unresolved`, ); } diff --git a/gitnexus/src/core/ingestion/utils/max-file-size.ts b/gitnexus/src/core/ingestion/utils/max-file-size.ts index 8363ec946..82013783a 100644 --- a/gitnexus/src/core/ingestion/utils/max-file-size.ts +++ b/gitnexus/src/core/ingestion/utils/max-file-size.ts @@ -1,5 +1,6 @@ import { TREE_SITTER_MAX_BUFFER } from '../constants.js'; +import { logger } from '../../logger.js'; /** Default threshold (512 KB). Files larger than this are skipped by the walker. */ export const DEFAULT_MAX_FILE_SIZE_BYTES = 512 * 1024; @@ -11,8 +12,7 @@ const warned = new Set(); const warnOnce = (key: string, message: string): void => { if (warned.has(key)) return; warned.add(key); - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn(message); + logger.warn(message); }; /** diff --git a/gitnexus/src/core/ingestion/workers/parse-worker.ts b/gitnexus/src/core/ingestion/workers/parse-worker.ts index 80ba5e7b7..40734d858 100644 --- a/gitnexus/src/core/ingestion/workers/parse-worker.ts +++ b/gitnexus/src/core/ingestion/workers/parse-worker.ts @@ -85,6 +85,7 @@ import type { LanguageProvider } from '../language-provider.js'; import type { ParsedFile } from 'gitnexus-shared'; import { extractParsedFile } from '../scope-extractor-bridge.js'; +import { logger } from '../../logger.js'; // ============================================================================ // Types for serializable results // ============================================================================ @@ -1385,8 +1386,7 @@ const processFileGroup = ( if (parentPort) { parentPort.postMessage({ type: 'warning', message }); } else { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn(message); + logger.warn(message); } return; } @@ -1415,8 +1415,7 @@ const processFileGroup = ( bufferSize: getTreeSitterBufferSize(parseContent), }); } catch (err) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn( + logger.warn( `Failed to parse file ${file.path}: ${err instanceof Error ? err.message : String(err)}`, ); continue; @@ -1429,8 +1428,7 @@ const processFileGroup = ( try { matches = query.matches(tree.rootNode); } catch (err) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn( + logger.warn( `Query execution failed for ${file.path}: ${err instanceof Error ? err.message : String(err)}`, ); continue; @@ -1450,8 +1448,7 @@ const processFileGroup = ( file.path, (message) => { if (parentPort) parentPort.postMessage({ type: 'warning', message }); - // eslint-disable-next-line no-console -- TODO(pino-migration) - else console.warn(message); + logger.warn(message); }, tree, ); diff --git a/gitnexus/src/core/ingestion/workers/worker-pool.ts b/gitnexus/src/core/ingestion/workers/worker-pool.ts index 5c02f2bd2..bcc9aa544 100644 --- a/gitnexus/src/core/ingestion/workers/worker-pool.ts +++ b/gitnexus/src/core/ingestion/workers/worker-pool.ts @@ -3,6 +3,7 @@ import os from 'node:os'; import fs from 'node:fs'; import { fileURLToPath } from 'node:url'; +import { logger } from '../../logger.js'; export interface WorkerPool { /** * Dispatch items across workers. Items are split into bounded jobs, each job @@ -258,8 +259,7 @@ export const createWorkerPool = ( splitDepth: job.splitDepth + 1, timeoutMs: nextTimeout, }; - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn( + logger.warn( `Worker ${workerIndex} parse job idle timeout after ${job.timeoutMs / 1000}s ` + `(${job.items.length} items, ${job.estimatedBytes} bytes, last progress: ${lastProgress}). ` + `Splitting into ${first.items.length}/${second.items.length} item jobs with ` + @@ -272,8 +272,7 @@ export const createWorkerPool = ( const nextAttempt = job.attempt + 1; if (nextAttempt <= poolOptions.maxTimeoutRetries) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn( + logger.warn( `Worker ${workerIndex} parse job idle timeout after ${job.timeoutMs / 1000}s ` + `(single item, attempt ${nextAttempt}/${poolOptions.maxTimeoutRetries + 1}). ` + `Retrying with ${nextTimeout / 1000}s timeout.`, @@ -367,8 +366,7 @@ export const createWorkerPool = ( reportProgress(); } else if (msg.type === 'warning') { resetIdleTimer(); - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn(msg.message); + logger.warn(msg.message); } else if (msg.type === 'sub-batch-done') { waitingForFlush = true; resetIdleTimer(); diff --git a/gitnexus/src/core/lbug/extension-loader.ts b/gitnexus/src/core/lbug/extension-loader.ts index 6ba78241b..582541942 100644 --- a/gitnexus/src/core/lbug/extension-loader.ts +++ b/gitnexus/src/core/lbug/extension-loader.ts @@ -1,6 +1,7 @@ import { spawn } from 'child_process'; import { fileURLToPath } from 'node:url'; import { LBUG_MAX_DB_SIZE } from './lbug-config.js'; +import { logger } from '../logger.js'; const DEFAULT_EXTENSION_INSTALL_TIMEOUT_MS = 15_000; const EXTENSION_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/; @@ -188,8 +189,7 @@ export class ExtensionManager { const policy = opts.policy ?? this.options.policy ?? resolvePolicyFromEnv(); const timeoutMs = opts.installTimeoutMs ?? this.options.installTimeoutMs ?? getExtensionInstallTimeoutMs(); - // eslint-disable-next-line no-console -- TODO(pino-migration) - const warn = this.options.warn ?? console.warn; + const warn = this.options.warn ?? ((msg: string) => logger.warn(msg)); if (policy === 'never') { this.markUnavailable(name, label, 'extension install policy is "never"', warn); diff --git a/gitnexus/src/core/lbug/lbug-adapter.ts b/gitnexus/src/core/lbug/lbug-adapter.ts index 61d856ce3..bbb8ef52c 100644 --- a/gitnexus/src/core/lbug/lbug-adapter.ts +++ b/gitnexus/src/core/lbug/lbug-adapter.ts @@ -24,6 +24,7 @@ import { } from './lbug-config.js'; import { isVectorExtensionSupportedByPlatform } from '../platform/capabilities.js'; +import { logger } from '../logger.js'; // --------------------------------------------------------------------------- // Relationship CSV splitting β€” extracted for testability (PR #818) // --------------------------------------------------------------------------- @@ -348,8 +349,7 @@ const doInitLbug = async (dbPath: string) => { } catch (err) { const msg = err instanceof Error ? err.message : String(err); if (!msg.includes('already exists')) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn(`⚠️ Schema creation warning: ${msg.slice(0, 120)}`); + logger.warn(`⚠️ Schema creation warning: ${msg.slice(0, 120)}`); } } } @@ -702,8 +702,7 @@ export const insertNodeToLbug = async ( return false; } catch (e: any) { // Node may already exist or other error - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.error(`Failed to insert ${label} node:`, e.message); + logger.error({ err: e.message }, `Failed to insert ${label} node:`); return false; } }; @@ -1030,16 +1029,14 @@ export const fetchExistingEmbeddingHashes = async ( const nodeId = r.nodeId ?? r[0]; if (nodeId) map.set(nodeId, STALE_HASH_SENTINEL); } - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log( + logger.info( `[embed] ${map.size} nodes in legacy DB (missing chunk-aware columns) β€” all treated as stale`, ); return map; } catch (fallbackErr: any) { const fallbackMsg = fallbackErr?.message ?? ''; if (isMissingColumnOrTableError(fallbackMsg)) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log( + logger.info( `[embed] CodeEmbedding table not yet present β€” full embedding run (${fallbackMsg})`, ); return undefined; diff --git a/gitnexus/src/core/logger.ts b/gitnexus/src/core/logger.ts index 3acbd0126..d97146a61 100644 --- a/gitnexus/src/core/logger.ts +++ b/gitnexus/src/core/logger.ts @@ -14,14 +14,23 @@ * const childLogger = createLogger('bridge-db', { debugEnvVar: 'GITNEXUS_DEBUG_BRIDGE' }); * * Operator semantics: - * - Default level: 'warn' + * - Default level: 'info' (matches pino default; preserves visibility of + * existing `console.log` migrations) * - When `opts.debugEnvVar` is set and that env var is truthy at * createLogger time, that named child logs at level 'debug' * - Output is NDJSON in production / CI / vitest. pino-pretty is used only * when stdout is a TTY AND CI is unset AND VITEST is unset, so test * and pipeline output stay parseable. + * + * Test capture: + * The exported `logger` singleton is a Proxy that forwards every call to a + * lazily-built pino instance. Tests use `_captureLogger()` to redirect that + * inner instance to a memory stream so they can assert on records the + * production code logged. See `gitnexus/test/unit/logger.test.ts` for the + * pattern. */ import pino, { type Logger, type LoggerOptions, type DestinationStream } from 'pino'; +import { Writable } from 'node:stream'; export interface CreateLoggerOptions { /** When set, this env var (truthy at construction time) bumps level to 'debug'. */ @@ -37,19 +46,26 @@ function isTruthyEnv(value: string | undefined): boolean { } function shouldUsePretty(): boolean { + // Logger writes to stderr (fd 2) so CLI data on stdout (fd 1) stays clean. + // Pretty-print only when stderr is a TTY and not in CI/test environments. return ( - process.stdout.isTTY === true && !isTruthyEnv(process.env.CI) && !isTruthyEnv(process.env.VITEST) + process.stderr.isTTY === true && + !isTruthyEnv(process.env.CI) && + !isTruthyEnv(process.env.VITEST) ); } /** - * Try to load pino-pretty synchronously via require(); fall back to raw NDJSON - * if it isn't installed (production install with devDependencies pruned). + * Default pino destination β€” writes to stderr (fd 2) so CLI commands can + * keep stdout (fd 1) clean for tool data output (#324). Pino defaults to + * stdout; we override here. */ +function defaultDestination(): DestinationStream { + return pino.destination({ dest: 2, sync: true }); +} + function tryBuildPrettyTransport(): LoggerOptions['transport'] | undefined { try { - // pino-pretty must be resolvable; if so, configure it as a transport target. - // pino's worker thread loads it by name at runtime. return { target: 'pino-pretty', options: { @@ -65,8 +81,8 @@ function tryBuildPrettyTransport(): LoggerOptions['transport'] | undefined { function buildBaseOptions(): LoggerOptions { const opts: LoggerOptions = { - level: 'warn', - base: undefined, // omit pid/hostname from each record + level: 'info', + base: undefined, }; if (shouldUsePretty()) { const transport = tryBuildPrettyTransport(); @@ -81,24 +97,101 @@ function buildBaseOptions(): LoggerOptions { * set and truthy at call time, the child runs at 'debug' level. */ export function createLogger(name: string, opts?: CreateLoggerOptions): Logger { - const debugRequested = opts?.debugEnvVar - ? isTruthyEnv(process.env[opts.debugEnvVar]) - : false; + const debugRequested = opts?.debugEnvVar ? isTruthyEnv(process.env[opts.debugEnvVar]) : false; - // When a custom destination is provided we cannot also use a transport - // (transports manage their own destination). Test path: just plain pino + - // destination stream. if (opts?.destination) { return pino( - { level: debugRequested ? 'debug' : 'warn', base: undefined, name }, + { level: debugRequested ? 'debug' : 'info', base: undefined, name }, opts.destination, ); } const base = buildBaseOptions(); - const root = pino({ ...base, level: debugRequested ? 'debug' : base.level }); + // When using a transport (pino-pretty), pino manages the destination + // internally and we cannot pass one explicitly. When transport is absent, + // route to stderr so stdout stays clean for CLI data output. + const root = base.transport + ? pino({ ...base, level: debugRequested ? 'debug' : base.level }) + : pino({ ...base, level: debugRequested ? 'debug' : base.level }, defaultDestination()); return root.child({ name }); } -/** Default singleton logger; module name "gitnexus". */ -export const logger: Logger = createLogger('gitnexus'); +/* ------------------------------------------------------------------ */ +/* Default singleton (Proxy-backed for test capture) */ +/* ------------------------------------------------------------------ */ + +let _activeDestination: DestinationStream | undefined; +let _cached: Logger | undefined; + +function _getInner(): Logger { + if (_cached) return _cached; + if (_activeDestination) { + _cached = pino({ level: 'info', base: undefined, name: 'gitnexus' }, _activeDestination); + } else { + // Use createLogger so the singleton honors the same stderr-by-default + // routing as named child loggers (CLI data on stdout stays clean). + _cached = createLogger('gitnexus'); + } + return _cached; +} + +/** + * Default singleton logger (`name: 'gitnexus'`). Backed by a Proxy so test + * capture (`_captureLogger()`) can redirect output without breaking modules + * that already imported the singleton at module-load time. + */ +export const logger = new Proxy({} as Logger, { + get(_target, prop) { + const inner = _getInner(); + const value = (inner as unknown as Record)[prop as string]; + if (typeof value === 'function') { + return (value as (...a: unknown[]) => unknown).bind(inner); + } + return value; + }, +}) as Logger; + +/** + * Test helper. Redirects the default `logger` singleton to an in-memory + * stream and returns a capture object plus a restore function. + * + * Pattern: + * let cap: ReturnType; + * beforeEach(() => { cap = _captureLogger(); }); + * afterEach(() => { cap.restore(); }); + * it('warns', () => { + * fnUnderTest(); + * expect(cap.records().some(r => r.msg?.includes('clamping'))).toBe(true); + * }); + * + * Not a public API; underscore-prefixed and called only from test code. + */ +export function _captureLogger(): { + records(): Array>; + text(): string; + restore(): void; +} { + class MemoryWritable extends Writable { + chunks: string[] = []; + _write(chunk: Buffer | string, _enc: BufferEncoding, cb: (err?: Error | null) => void): void { + this.chunks.push(typeof chunk === 'string' ? chunk : chunk.toString('utf-8')); + cb(); + } + } + const w = new MemoryWritable(); + _activeDestination = w; + _cached = undefined; + return { + records: () => + w.chunks + .join('') + .split('\n') + .filter((l) => l.length > 0) + .map((l) => JSON.parse(l) as Record), + text: () => w.chunks.join(''), + restore: () => { + _activeDestination = undefined; + _cached = undefined; + }, + }; +} diff --git a/gitnexus/src/core/tree-sitter/parser-loader.ts b/gitnexus/src/core/tree-sitter/parser-loader.ts index f5e65c5e1..1312dabf3 100644 --- a/gitnexus/src/core/tree-sitter/parser-loader.ts +++ b/gitnexus/src/core/tree-sitter/parser-loader.ts @@ -2,6 +2,7 @@ import Parser from 'tree-sitter'; import { createRequire } from 'node:module'; import { SupportedLanguages } from 'gitnexus-shared'; +import { logger } from '../logger.js'; const _require = createRequire(import.meta.url); /** @@ -175,10 +176,8 @@ const logFailure = (key: string, result: LoadResult): void => { logged.add(key); const message = `[gitnexus] ${result.note} (${result.error.message})`; - // eslint-disable-next-line no-console -- TODO(pino-migration) - if (result.severity === 'error') console.error(message); - // eslint-disable-next-line no-console -- TODO(pino-migration) - else console.warn(message); + logger.error(message); + logger.warn(message); }; export const resolveLanguageKey = (language: SupportedLanguages, filePath?: string): string => diff --git a/gitnexus/src/core/wiki/cursor-client.ts b/gitnexus/src/core/wiki/cursor-client.ts index e0b85463c..bf85f4183 100644 --- a/gitnexus/src/core/wiki/cursor-client.ts +++ b/gitnexus/src/core/wiki/cursor-client.ts @@ -10,6 +10,7 @@ import { spawn, execSync } from 'child_process'; import type { LLMResponse, CallLLMOptions } from './llm-client.js'; +import { logger } from '../logger.js'; export interface CursorConfig { model?: string; workingDirectory?: string; @@ -21,8 +22,7 @@ function isVerbose(): boolean { function verboseLog(...args: unknown[]): void { if (isVerbose()) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.log('[cursor-cli]', ...args); + logger.info({ args }, '[cursor-cli]'); } } diff --git a/gitnexus/src/core/wiki/llm-client.ts b/gitnexus/src/core/wiki/llm-client.ts index 1a69453e5..f68c2d3e7 100644 --- a/gitnexus/src/core/wiki/llm-client.ts +++ b/gitnexus/src/core/wiki/llm-client.ts @@ -1,3 +1,4 @@ +import { logger } from '../logger.js'; /** * LLM Client for Wiki Generation * @@ -135,8 +136,7 @@ export async function callLLM( // Warn when using Azure legacy deployment URL without api-version if (azure && !config.apiVersion && config.baseUrl.includes('/deployments/')) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.warn( + logger.warn( '[gitnexus] Warning: Azure legacy deployment URL detected but no api-version set. Add --api-version 2024-10-21 or use the v1 API format.', ); } diff --git a/gitnexus/src/mcp/core/embedder.ts b/gitnexus/src/mcp/core/embedder.ts index 241d8ee06..336c565b8 100644 --- a/gitnexus/src/mcp/core/embedder.ts +++ b/gitnexus/src/mcp/core/embedder.ts @@ -15,6 +15,7 @@ import { resolveEmbeddingConfig } from '../../core/embeddings/config.js'; import { applyHfEnvOverrides } from '../../core/embeddings/hf-env.js'; import { silenceStdout, restoreStdout, realStderrWrite } from '../../core/lbug/pool-adapter.js'; +import { logger } from '../../core/logger.js'; // Model config const MODEL_ID = 'Snowflake/snowflake-arctic-embed-xs'; @@ -51,8 +52,7 @@ export const initEmbedder = async (): Promise => { applyHfEnvOverrides(env); const embeddingConfig = resolveEmbeddingConfig(); - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.error('GitNexus: Loading embedding model (first search may take a moment)...'); + logger.error('GitNexus: Loading embedding model (first search may take a moment)...'); const devicesToTry: Array<'dml' | 'cuda' | 'cpu'> = embeddingConfig.device === 'dml' || embeddingConfig.device === 'cuda' @@ -83,8 +83,7 @@ export const initEmbedder = async (): Promise => { restoreStdout(); process.stderr.write = realStderrWrite; } - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.error(`GitNexus: Embedding model loaded (${device})`); + logger.error(`GitNexus: Embedding model loaded (${device})`); return embedderInstance!; } catch { if (device === 'cpu') throw new Error('Failed to load embedding model'); diff --git a/gitnexus/src/mcp/local/local-backend.ts b/gitnexus/src/mcp/local/local-backend.ts index d8fba7af0..fade093ea 100644 --- a/gitnexus/src/mcp/local/local-backend.ts +++ b/gitnexus/src/mcp/local/local-backend.ts @@ -41,6 +41,7 @@ import { } from '../../core/platform/capabilities.js'; import { PhaseTimer } from '../../core/search/phase-timer.js'; import { checkStaleness, checkCwdMatch } from '../../core/git-staleness.js'; +import { logger } from '../../core/logger.js'; // AI context generation is CLI-only (gitnexus analyze) // import { generateAIContextFiles } from '../../cli/ai-context.js'; @@ -164,8 +165,7 @@ const confidenceForRelType = (relType: string | undefined): number => /** Structured error logging for query failures β€” replaces empty catch blocks */ function logQueryError(context: string, err: unknown): void { const msg = err instanceof Error ? err.message : String(err); - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.error(`GitNexus [${context}]: ${msg}`); + logger.error(`GitNexus [${context}]: ${msg}`); } /** @@ -185,8 +185,7 @@ function logQueryError(context: string, err: unknown): void { function logQueryTiming(query: string, phases: Record): void { const totalMs = phases.wall ?? Object.values(phases).reduce((a, b) => a + b, 0); const truncated = query.length > 80 ? `${query.slice(0, 80)}…` : query; - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.error( + logger.error( `GitNexus [query:timing] query=${JSON.stringify(truncated)} totalMs=${totalMs} phases=${JSON.stringify(phases)}`, ); } @@ -289,8 +288,7 @@ export class LocalBackend { // If kuzu exists but lbug doesn't, warn so the user knows to re-analyze. const kuzu = await cleanupOldKuzuFiles(storagePath); if (kuzu.found && kuzu.needsReindex) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.error( + logger.error( `GitNexus: "${entry.name}" has a stale KuzuDB index. Run: gitnexus analyze ${entry.path}`, ); } @@ -640,8 +638,7 @@ export class LocalBackend { } this.warnedSiblingDrift.add(cacheKey); - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.error(`GitNexus: ${match.hint}`); + logger.error(`GitNexus: ${match.hint}`); } // ─── Tool Dispatch ─────────────────────────────────────────────── @@ -994,8 +991,10 @@ export class LocalBackend { try { bm25Results = await searchFTSFromLbug(query, limit, repo.id); } catch (err: any) { - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.error('GitNexus: BM25/FTS search failed (FTS indexes may not exist) -', err.message); + logger.error( + { err: err.message }, + 'GitNexus: BM25/FTS search failed (FTS indexes may not exist) -', + ); return { results: [], ftsUsed: false }; } @@ -1119,8 +1118,7 @@ export class LocalBackend { // policy. Emitted once per `LocalBackend` instance lifetime to avoid // noisy stderr on hot semantic-search paths (DoD Β§2.8). this.warnedVectorUnsupported = true; - // eslint-disable-next-line no-console -- TODO(pino-migration) - console.error( + logger.error( 'GitNexus [query:vector]: VECTOR extension not supported on this platform; using exact scan fallback', ); } diff --git a/gitnexus/test/integration/filesystem-walker.test.ts b/gitnexus/test/integration/filesystem-walker.test.ts index accb24e37..2a1257ccc 100644 --- a/gitnexus/test/integration/filesystem-walker.test.ts +++ b/gitnexus/test/integration/filesystem-walker.test.ts @@ -8,6 +8,7 @@ import { } from '../../src/core/ingestion/filesystem-walker.js'; import { _resetMaxFileSizeWarnings } from '../../src/core/ingestion/utils/max-file-size.js'; +import { _captureLogger } from '../../src/core/logger.js'; describe('filesystem-walker', () => { let tmpDir: string; @@ -328,7 +329,7 @@ describe('filesystem-walker', () => { const BIG_FILE = 'src/big.ts'; const BIG_FILE_BYTES = 600 * 1024; const ORIGINAL_ENV = process.env.GITNEXUS_MAX_FILE_SIZE; - let warnSpy: ReturnType; + let cap: ReturnType; beforeAll(async () => { sizeDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-walker-size-test-')); @@ -344,7 +345,7 @@ describe('filesystem-walker', () => { beforeEach(() => { delete process.env.GITNEXUS_MAX_FILE_SIZE; _resetMaxFileSizeWarnings(); - warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + cap = _captureLogger(); }); afterEach(() => { @@ -353,7 +354,7 @@ describe('filesystem-walker', () => { } else { process.env.GITNEXUS_MAX_FILE_SIZE = ORIGINAL_ENV; } - warnSpy.mockRestore(); + cap.restore(); }); it('skips a 600KB file by default', async () => { @@ -375,27 +376,27 @@ describe('filesystem-walker', () => { const files = await walkRepositoryPaths(sizeDir); const paths = files.map((f) => f.path.replace(/\\/g, '/')); expect(paths).not.toContain(BIG_FILE); - const invalidWarnings = warnSpy.mock.calls.filter((c) => - String(c[0]).includes('must be a positive integer'), - ); + const invalidWarnings = cap + .records() + .filter((r) => String(r.msg ?? '').includes('must be a positive integer')); expect(invalidWarnings).toHaveLength(1); }); it('omits the "generated/vendored" suffix when threshold is overridden', async () => { process.env.GITNEXUS_MAX_FILE_SIZE = '1'; await walkRepositoryPaths(sizeDir); - const skipWarnings = warnSpy.mock.calls.filter((c) => String(c[0]).includes('Skipped ')); + const skipWarnings = cap.records().filter((r) => String(r.msg ?? '').includes('Skipped ')); expect(skipWarnings.length).toBeGreaterThan(0); - for (const call of skipWarnings) { - expect(String(call[0])).not.toContain('generated/vendored'); + for (const r of skipWarnings) { + expect(String(r.msg ?? '')).not.toContain('generated/vendored'); } }); it('keeps the "generated/vendored" suffix under the default threshold', async () => { await walkRepositoryPaths(sizeDir); - const skipWarnings = warnSpy.mock.calls.filter((c) => String(c[0]).includes('Skipped ')); + const skipWarnings = cap.records().filter((r) => String(r.msg ?? '').includes('Skipped ')); expect(skipWarnings.length).toBeGreaterThan(0); - expect(String(skipWarnings[0][0])).toContain('generated/vendored'); + expect(String(skipWarnings[0].msg ?? '')).toContain('generated/vendored'); }); }); }); diff --git a/gitnexus/test/integration/worker-pool.test.ts b/gitnexus/test/integration/worker-pool.test.ts index eb0baebdf..f53375db0 100644 --- a/gitnexus/test/integration/worker-pool.test.ts +++ b/gitnexus/test/integration/worker-pool.test.ts @@ -13,6 +13,7 @@ import path from 'node:path'; import fs from 'node:fs'; import os from 'node:os'; +import { _captureLogger } from '../../src/core/logger.js'; const DIST_WORKER = path.resolve( __dirname, '..', @@ -211,7 +212,7 @@ describe('worker pool integration', () => { `, ); - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const cap = _captureLogger(); const workerUrl = pathToFileURL(workerPath) as URL; pool = createWorkerPool(workerUrl, 1); @@ -221,9 +222,9 @@ describe('worker pool integration', () => { ]); expect(results).toHaveLength(1); expect(results[0].fileCount).toBe(1); - expect(warnSpy).toHaveBeenCalledWith('warning before result'); + expect(cap.records().some((r) => r.msg === 'warning before result')).toBe(true); } finally { - warnSpy.mockRestore(); + cap.restore(); fs.rmSync(tempDir, { recursive: true, force: true }); } }); @@ -298,7 +299,7 @@ describe('worker pool integration', () => { `, ); - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const cap = _captureLogger(); pool = createWorkerPool(pathToFileURL(workerPath) as URL, 1, { subBatchIdleTimeoutMs: 150, maxTimeoutRetries: 1, @@ -308,9 +309,11 @@ describe('worker pool integration', () => { try { const results = await pool.dispatch([{ path: 'retry.ts', content: '' }]); expect(results).toEqual([{ fileCount: 1, recovered: true }]); - expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Retrying with 0.6s timeout')); + expect( + cap.records().some((r) => String(r.msg ?? '').includes('Retrying with 0.6s timeout')), + ).toBe(true); } finally { - warnSpy.mockRestore(); + cap.restore(); fs.rmSync(tempDir, { recursive: true, force: true }); } }); @@ -344,7 +347,7 @@ describe('worker pool integration', () => { `, ); - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const cap = _captureLogger(); pool = createWorkerPool(pathToFileURL(workerPath) as URL, 1, { subBatchSize: 2, subBatchIdleTimeoutMs: 150, @@ -372,9 +375,11 @@ describe('worker pool integration', () => { ]); expect(progressCalls).toEqual([...progressCalls].sort((a, b) => a - b)); expect(progressCalls.at(-1)).toBe(4); - expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Splitting into 1/1 item jobs')); + expect( + cap.records().some((r) => String(r.msg ?? '').includes('Splitting into 1/1 item jobs')), + ).toBe(true); } finally { - warnSpy.mockRestore(); + cap.restore(); fs.rmSync(tempDir, { recursive: true, force: true }); } }); @@ -440,7 +445,7 @@ describe('worker pool integration', () => { `, ); - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const cap = _captureLogger(); pool = createWorkerPool(pathToFileURL(workerPath) as URL, 2, { subBatchSize: 2, subBatchIdleTimeoutMs: 150, @@ -466,9 +471,11 @@ describe('worker pool integration', () => { 'tail-a.ts', 'tail-b.ts', ]); - expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Splitting into 1/1 item jobs')); + expect( + cap.records().some((r) => String(r.msg ?? '').includes('Splitting into 1/1 item jobs')), + ).toBe(true); } finally { - warnSpy.mockRestore(); + cap.restore(); fs.rmSync(tempDir, { recursive: true, force: true }); } }); @@ -504,7 +511,7 @@ describe('worker pool integration', () => { `, ); - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const cap = _captureLogger(); // 2 workers but subBatchSize=4 means all 4 items form 1 job; second worker stays idle. pool = createWorkerPool(pathToFileURL(workerPath) as URL, 2, { subBatchSize: 4, @@ -523,9 +530,9 @@ describe('worker pool integration', () => { const allPaths = results.flatMap((r: any) => r.paths); expect(allPaths.sort()).toEqual(['a.ts', 'b.ts', 'c.ts', 'd.ts']); - expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Splitting into')); + expect(cap.records().some((r) => String(r.msg ?? '').includes('Splitting into'))).toBe(true); } finally { - warnSpy.mockRestore(); + cap.restore(); fs.rmSync(tempDir, { recursive: true, force: true }); } }, 15_000); diff --git a/gitnexus/test/unit/analyze-worker-timeout.test.ts b/gitnexus/test/unit/analyze-worker-timeout.test.ts index dcf5a9dce..e23c0a906 100644 --- a/gitnexus/test/unit/analyze-worker-timeout.test.ts +++ b/gitnexus/test/unit/analyze-worker-timeout.test.ts @@ -39,6 +39,8 @@ describe('analyzeCommand worker timeout validation', () => { it.each(['0', 'abc', '-5', 'Infinity'])( 'rejects invalid --worker-timeout value %s before analysis starts', async (workerTimeout) => { + // CLI code (cli/analyze.ts) is exempt from the pino migration β€” + // user-facing stdout/stderr is the contract. Spy on console.error. const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); const { analyzeCommand } = await import('../../src/cli/analyze.js'); diff --git a/gitnexus/test/unit/calltool-dispatch.test.ts b/gitnexus/test/unit/calltool-dispatch.test.ts index aa422327b..f8d94d890 100644 --- a/gitnexus/test/unit/calltool-dispatch.test.ts +++ b/gitnexus/test/unit/calltool-dispatch.test.ts @@ -71,6 +71,7 @@ vi.mock('../../src/mcp/core/embedder.js', () => ({ import { LocalBackend } from '../../src/mcp/local/local-backend.js'; import { listRegisteredRepos, cleanupOldKuzuFiles } from '../../src/storage/repo-manager.js'; +import { _captureLogger } from '../../src/core/logger.js'; import { initLbug, executeQuery, @@ -194,7 +195,7 @@ describe('LocalBackend.callTool', () => { }); it('skips vector index query when VECTOR is unsupported by the platform', async () => { - const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined); + const cap = _captureLogger(); platformMocks.isVectorExtensionSupportedByPlatform.mockReturnValue(false); (executeQuery as any).mockImplementation(async (_repoId: string, cypher: string) => { if (cypher.includes('COUNT(*) AS cnt')) return [{ cnt: 1 }]; @@ -217,13 +218,17 @@ describe('LocalBackend.callTool', () => { cypher.includes('e.embedding AS embedding'), ), ).toBe(true); - expect(consoleError).toHaveBeenCalledWith( - expect.stringContaining( - 'GitNexus [query:vector]: VECTOR extension not supported on this platform', - ), - ); + expect( + cap + .records() + .some((r) => + String(r.msg ?? '').includes( + 'GitNexus [query:vector]: VECTOR extension not supported on this platform', + ), + ), + ).toBe(true); } finally { - consoleError.mockRestore(); + cap.restore(); } }); @@ -835,7 +840,7 @@ describe('LocalBackend.resolveRepo', () => { hint: '⚠️ stale sibling clone', }); - const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + const cap = _captureLogger(); try { await backend.init(); @@ -846,13 +851,15 @@ describe('LocalBackend.resolveRepo', () => { await backend.resolveRepo(); await backend.resolveRepo(); - const drift = errSpy.mock.calls.filter((c) => String(c[0]).includes('stale sibling clone')); + const drift = cap + .records() + .filter((r) => String(r.msg ?? '').includes('stale sibling clone')); expect(drift).toHaveLength(1); // checkCwdMatch should also only run once β€” the cache check // happens BEFORE the shellout-heavy match call. expect(checkCwdMatch).toHaveBeenCalledTimes(1); } finally { - errSpy.mockRestore(); + cap.restore(); (checkCwdMatch as any).mockResolvedValue({ match: 'none' }); } }); diff --git a/gitnexus/test/unit/group/grpc-extractor.test.ts b/gitnexus/test/unit/group/grpc-extractor.test.ts index a586b5c0d..fc05eac8e 100644 --- a/gitnexus/test/unit/group/grpc-extractor.test.ts +++ b/gitnexus/test/unit/group/grpc-extractor.test.ts @@ -11,6 +11,7 @@ import { } from '../../../src/core/group/extractors/grpc-extractor.js'; import type { ProtoServiceInfo } from '../../../src/core/group/extractors/grpc-extractor.js'; import type { RepoHandle } from '../../../src/core/group/types.js'; +import { _captureLogger } from '../../../src/core/logger.js'; describe('GrpcExtractor', () => { let tmpDir: string; @@ -581,6 +582,7 @@ service AuthService { import type { ClientGrpc } from '@nestjs/microservices'; import { AuthServiceClient } from './generated/auth'; +import { _captureLogger } from '../../../src/core/logger.js'; export class AuthGateway { constructor(private readonly client: ClientGrpc) {} @@ -797,18 +799,18 @@ describe('resolveProtoConflict', () => { }); it('test_all_zero_tie_returns_null', () => { - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const cap = _captureLogger(); const candidates = [ makeInfo('pkgA', 'totally/unrelated/a/svc.proto'), makeInfo('pkgB', 'completely/different/b/svc.proto'), ]; const result = resolveProtoConflict('Svc', 'src/main.go', candidates); expect(result).toBeNull(); - warnSpy.mockRestore(); + cap.restore(); }); it('test_positive_score_tie_returns_null', () => { - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const cap = _captureLogger(); // Both candidates share `src/proto` with the source dir β€” equal shared runs. const candidates = [ makeInfo('pkgA', 'src/proto/a/svc.proto'), @@ -816,11 +818,11 @@ describe('resolveProtoConflict', () => { ]; const result = resolveProtoConflict('Svc', 'src/proto/main.go', candidates); expect(result).toBeNull(); - warnSpy.mockRestore(); + cap.restore(); }); it('test_three_way_zero_tie_returns_null', () => { - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const cap = _captureLogger(); const candidates = [ makeInfo('pkgA', 'aaa/svc.proto'), makeInfo('pkgB', 'bbb/svc.proto'), @@ -828,7 +830,7 @@ describe('resolveProtoConflict', () => { ]; const result = resolveProtoConflict('Svc', 'src/main.go', candidates); expect(result).toBeNull(); - warnSpy.mockRestore(); + cap.restore(); }); it('test_unique_winner_among_ties', () => { @@ -843,19 +845,19 @@ describe('resolveProtoConflict', () => { }); it('test_ambiguous_emits_single_warn_with_service_and_paths', () => { - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const cap = _captureLogger(); const candidates = [ makeInfo('pkgA', 'totally/unrelated/a/svc.proto'), makeInfo('pkgB', 'completely/different/b/svc.proto'), ]; resolveProtoConflict('MyService', 'src/main.go', candidates); - expect(warnSpy).toHaveBeenCalledTimes(1); - const msg = String(warnSpy.mock.calls[0][0]); + expect(cap.records().length).toBe(1); + const msg = String(String(cap.records()[0]?.msg ?? '')); expect(msg).toContain('MyService'); expect(msg).toContain('src/main.go'); expect(msg).toContain('totally/unrelated/a/svc.proto'); expect(msg).toContain('completely/different/b/svc.proto'); - warnSpy.mockRestore(); + cap.restore(); }); }); @@ -879,7 +881,7 @@ describe('GrpcExtractor.extract ambiguous proto resolution', () => { }); it('test_ambiguous_short_name_across_unrelated_protos_yields_no_source_contract', async () => { - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const cap = _captureLogger(); // Two unrelated proto files defining the same short name `UserService` in // unrelated directories, neither sharing path segments with the Go source. await fsp.mkdir(path.join(tmpDir, 'billing-team', 'proto'), { recursive: true }); @@ -906,8 +908,8 @@ describe('GrpcExtractor.extract ambiguous proto resolution', () => { (c) => c.meta.source === 'go_client' && c.meta.service === 'UserService', ); expect(sourceContracts).toHaveLength(0); - expect(warnSpy).toHaveBeenCalled(); - warnSpy.mockRestore(); + expect(cap.records().length).toBeGreaterThan(0); + cap.restore(); }); }); diff --git a/gitnexus/test/unit/group/rust-workspace-extractor.test.ts b/gitnexus/test/unit/group/rust-workspace-extractor.test.ts index a9e277c36..3dd958ebf 100644 --- a/gitnexus/test/unit/group/rust-workspace-extractor.test.ts +++ b/gitnexus/test/unit/group/rust-workspace-extractor.test.ts @@ -3,6 +3,7 @@ import fs from 'node:fs/promises'; import path from 'node:path'; import os from 'node:os'; import { extractRustWorkspaceLinks } from '../../../src/core/group/extractors/rust-workspace-extractor.js'; +import { _captureLogger } from '../../../src/core/logger.js'; describe('RustWorkspaceExtractor', () => { let tmpDir: string; @@ -229,19 +230,17 @@ describe('RustWorkspaceExtractor', () => { ['consumer', path.join(tmpDir, 'consumer')], ]); - const warnings: string[] = []; - const origWarn = console.warn; - console.warn = (...args: unknown[]) => { - warnings.push(String(args[0])); - }; + const cap = _captureLogger(); try { const result = await extractRustWorkspaceLinks(repos, repoPaths); - expect(warnings.some((w) => w.includes('duplicate crate name "shared"'))).toBe(true); + expect( + cap.records().some((r) => String(r.msg ?? '').includes('duplicate crate name "shared"')), + ).toBe(true); expect(result.links).toHaveLength(1); expect(result.links[0].from).toBe('a'); } finally { - console.warn = origWarn; + cap.restore(); } }); diff --git a/gitnexus/test/unit/group/sync.test.ts b/gitnexus/test/unit/group/sync.test.ts index a6b6376c0..3cfc337a4 100644 --- a/gitnexus/test/unit/group/sync.test.ts +++ b/gitnexus/test/unit/group/sync.test.ts @@ -3,6 +3,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import * as os from 'node:os'; import { syncGroup, stableRepoPoolId } from '../../../src/core/group/sync.js'; +import { _captureLogger } from '../../../src/core/logger.js'; import type { GroupConfig, StoredContract, @@ -292,9 +293,7 @@ describe('syncGroup', () => { matching: { bm25_threshold: 0.7, embedding_threshold: 0.65, max_candidates_per_step: 3 }, }; - const warnings: string[] = []; - const origWarn = console.warn; - console.warn = (msg: string) => warnings.push(String(msg)); + const cap = _captureLogger(); try { const result = await syncGroup(config, { extractorOverride: async () => [], @@ -306,9 +305,9 @@ describe('syncGroup', () => { expect(result.crossLinks[0].to.symbolUid).toBe( 'manifest::app/dangling::http::POST::/api/missing', ); - expect(warnings.some((w) => w.includes('app/dangling'))).toBe(true); + expect(cap.records().some((r) => String(r.msg ?? '').includes('app/dangling'))).toBe(true); } finally { - console.warn = origWarn; + cap.restore(); } }); diff --git a/gitnexus/test/unit/ignore-service.test.ts b/gitnexus/test/unit/ignore-service.test.ts index de32eb8d9..05edf0e89 100644 --- a/gitnexus/test/unit/ignore-service.test.ts +++ b/gitnexus/test/unit/ignore-service.test.ts @@ -580,7 +580,7 @@ describe('loadIgnoreRules β€” error handling', () => { expect(result).toBeNull(); expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('.gitignore')); - warnSpy.mockRestore(); + cap.restore(); await fs.chmod(gitignorePath, 0o644); await fs.unlink(gitignorePath); }, diff --git a/gitnexus/test/unit/logger.test.ts b/gitnexus/test/unit/logger.test.ts index 32eadd3ed..b89b736a9 100644 --- a/gitnexus/test/unit/logger.test.ts +++ b/gitnexus/test/unit/logger.test.ts @@ -62,7 +62,7 @@ describe('createLogger β€” debugEnvVar gating', () => { delete process.env[ENV]; }); - it('without debugEnvVar, .debug() emits nothing (default warn level)', () => { + it('without debugEnvVar, .debug() emits nothing (default info level)', () => { const dest = new MemoryWritable(); const log = createLogger('t', { destination: dest }); log.debug('should not appear'); @@ -131,7 +131,12 @@ describe('createLogger β€” structured output safety', () => { // Exactly one record. The internal \r\n is JSON-escaped, not a record boundary. expect(dest.records().length).toBe(1); // Raw text has trailing newline as record terminator β€” count of \n == 1. - expect(dest.text().split('\n').filter((l) => l.length > 0).length).toBe(1); + expect( + dest + .text() + .split('\n') + .filter((l) => l.length > 0).length, + ).toBe(1); }); it('U+2028 / U+2029 in a string field stays inside one NDJSON record', () => { diff --git a/gitnexus/test/unit/max-file-size.test.ts b/gitnexus/test/unit/max-file-size.test.ts index 074e148e7..569b41eab 100644 --- a/gitnexus/test/unit/max-file-size.test.ts +++ b/gitnexus/test/unit/max-file-size.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { DEFAULT_MAX_FILE_SIZE_BYTES, MAX_FILE_SIZE_UPPER_BOUND_BYTES, @@ -6,15 +6,16 @@ import { getMaxFileSizeBannerMessage, _resetMaxFileSizeWarnings, } from '../../src/core/ingestion/utils/max-file-size.js'; +import { _captureLogger } from '../../src/core/logger.js'; describe('getMaxFileSizeBytes', () => { const ORIGINAL = process.env.GITNEXUS_MAX_FILE_SIZE; - let warnSpy: ReturnType; + let cap: ReturnType; beforeEach(() => { delete process.env.GITNEXUS_MAX_FILE_SIZE; _resetMaxFileSizeWarnings(); - warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + cap = _captureLogger(); }); afterEach(() => { @@ -23,43 +24,43 @@ describe('getMaxFileSizeBytes', () => { } else { process.env.GITNEXUS_MAX_FILE_SIZE = ORIGINAL; } - warnSpy.mockRestore(); + cap.restore(); }); it('returns the default when the env var is unset', () => { expect(getMaxFileSizeBytes()).toBe(DEFAULT_MAX_FILE_SIZE_BYTES); - expect(warnSpy).not.toHaveBeenCalled(); + expect(cap.records().length).toBe(0); }); it('parses a positive integer value as KB', () => { process.env.GITNEXUS_MAX_FILE_SIZE = '1024'; expect(getMaxFileSizeBytes()).toBe(1024 * 1024); - expect(warnSpy).not.toHaveBeenCalled(); + expect(cap.records().length).toBe(0); }); it('clamps values above the tree-sitter ceiling', () => { - // One KB above the 32 MB ceiling. const aboveCeilingKb = MAX_FILE_SIZE_UPPER_BOUND_BYTES / 1024 + 1; process.env.GITNEXUS_MAX_FILE_SIZE = String(aboveCeilingKb); expect(getMaxFileSizeBytes()).toBe(MAX_FILE_SIZE_UPPER_BOUND_BYTES); - expect(warnSpy).toHaveBeenCalledTimes(1); - expect(warnSpy.mock.calls[0][0]).toContain('clamping'); + const records = cap.records(); + expect(records.length).toBe(1); + expect(String(records[0].msg)).toContain('clamping'); }); it.each(['abc', '0', '-512', '1.5', 'NaN', ''])( 'falls back to the default and warns on invalid value %s', (raw) => { if (raw === '') { - // Empty string is treated as unset by the util (raw falsy check). process.env.GITNEXUS_MAX_FILE_SIZE = raw; expect(getMaxFileSizeBytes()).toBe(DEFAULT_MAX_FILE_SIZE_BYTES); - expect(warnSpy).not.toHaveBeenCalled(); + expect(cap.records().length).toBe(0); return; } process.env.GITNEXUS_MAX_FILE_SIZE = raw; expect(getMaxFileSizeBytes()).toBe(DEFAULT_MAX_FILE_SIZE_BYTES); - expect(warnSpy).toHaveBeenCalledTimes(1); - expect(warnSpy.mock.calls[0][0]).toContain('must be a positive integer'); + const records = cap.records(); + expect(records.length).toBe(1); + expect(String(records[0].msg)).toContain('must be a positive integer'); }, ); @@ -68,7 +69,7 @@ describe('getMaxFileSizeBytes', () => { getMaxFileSizeBytes(); getMaxFileSizeBytes(); getMaxFileSizeBytes(); - expect(warnSpy).toHaveBeenCalledTimes(1); + expect(cap.records().length).toBe(1); }); it('warns separately for distinct invalid values', () => { @@ -76,20 +77,20 @@ describe('getMaxFileSizeBytes', () => { getMaxFileSizeBytes(); process.env.GITNEXUS_MAX_FILE_SIZE = 'xyz'; getMaxFileSizeBytes(); - expect(warnSpy).toHaveBeenCalledTimes(2); + expect(cap.records().length).toBe(2); }); it('_resetMaxFileSizeWarnings re-enables warnings after reset', () => { process.env.GITNEXUS_MAX_FILE_SIZE = 'abc'; getMaxFileSizeBytes(); - expect(warnSpy).toHaveBeenCalledTimes(1); + expect(cap.records().length).toBe(1); getMaxFileSizeBytes(); - expect(warnSpy).toHaveBeenCalledTimes(1); + expect(cap.records().length).toBe(1); _resetMaxFileSizeWarnings(); getMaxFileSizeBytes(); - expect(warnSpy).toHaveBeenCalledTimes(2); + expect(cap.records().length).toBe(2); }); it('DEFAULT_MAX_FILE_SIZE_BYTES is 512 KB', () => { @@ -99,12 +100,12 @@ describe('getMaxFileSizeBytes', () => { describe('getMaxFileSizeBannerMessage', () => { const ORIGINAL = process.env.GITNEXUS_MAX_FILE_SIZE; - let warnSpy: ReturnType; + let cap: ReturnType; beforeEach(() => { delete process.env.GITNEXUS_MAX_FILE_SIZE; _resetMaxFileSizeWarnings(); - warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + cap = _captureLogger(); }); afterEach(() => { @@ -113,7 +114,7 @@ describe('getMaxFileSizeBannerMessage', () => { } else { process.env.GITNEXUS_MAX_FILE_SIZE = ORIGINAL; } - warnSpy.mockRestore(); + cap.restore(); }); it('returns null when the env var is unset (default threshold)', () => { diff --git a/gitnexus/test/unit/sequential-language-availability.test.ts b/gitnexus/test/unit/sequential-language-availability.test.ts index f48db5778..bd0e4d4d6 100644 --- a/gitnexus/test/unit/sequential-language-availability.test.ts +++ b/gitnexus/test/unit/sequential-language-availability.test.ts @@ -19,6 +19,7 @@ import { createSymbolTable } from '../../src/core/ingestion/model/symbol-table.j import { createResolutionContext } from '../../src/core/ingestion/model/resolution-context.js'; import * as parserLoader from '../../src/core/tree-sitter/parser-loader.js'; +import { _captureLogger } from '../../src/core/logger.js'; describe('sequential native parser availability', () => { beforeEach(() => { vi.clearAllMocks(); @@ -43,7 +44,7 @@ describe('sequential native parser availability', () => { }); it('warns when processImports skips files in verbose mode', async () => { - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const cap = _captureLogger(); const previous = process.env.GITNEXUS_VERBOSE; process.env.GITNEXUS_VERBOSE = '1'; vi.mocked(parserLoader.isLanguageAvailable).mockReturnValue(false); @@ -58,11 +59,17 @@ describe('sequential native parser availability', () => { ['App.swift'], ); - expect(warnSpy).toHaveBeenCalledWith( - '[ingestion] Skipped 1 swift file(s) in import processing β€” swift parser not available.', - ); + expect( + cap + .records() + .some( + (r) => + r.msg === + '[ingestion] Skipped 1 swift file(s) in import processing β€” swift parser not available.', + ), + ).toBe(true); - warnSpy.mockRestore(); + cap.restore(); if (previous === undefined) { delete process.env.GITNEXUS_VERBOSE; } else { @@ -86,7 +93,7 @@ describe('sequential native parser availability', () => { }); it('warns when processCalls skips files in verbose mode', async () => { - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const cap = _captureLogger(); const previous = process.env.GITNEXUS_VERBOSE; process.env.GITNEXUS_VERBOSE = '1'; vi.mocked(parserLoader.isLanguageAvailable).mockReturnValue(false); @@ -98,11 +105,17 @@ describe('sequential native parser availability', () => { createResolutionContext(), ); - expect(warnSpy).toHaveBeenCalledWith( - '[ingestion] Skipped 1 swift file(s) in call processing β€” swift parser not available.', - ); + expect( + cap + .records() + .some( + (r) => + r.msg === + '[ingestion] Skipped 1 swift file(s) in call processing β€” swift parser not available.', + ), + ).toBe(true); - warnSpy.mockRestore(); + cap.restore(); if (previous === undefined) { delete process.env.GITNEXUS_VERBOSE; } else { @@ -126,7 +139,7 @@ describe('sequential native parser availability', () => { }); it('warns when processHeritage skips files in verbose mode', async () => { - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const cap = _captureLogger(); const previous = process.env.GITNEXUS_VERBOSE; process.env.GITNEXUS_VERBOSE = '1'; vi.mocked(parserLoader.isLanguageAvailable).mockReturnValue(false); @@ -138,11 +151,17 @@ describe('sequential native parser availability', () => { createResolutionContext(), ); - expect(warnSpy).toHaveBeenCalledWith( - '[ingestion] Skipped 1 swift file(s) in heritage processing β€” swift parser not available.', - ); + expect( + cap + .records() + .some( + (r) => + r.msg === + '[ingestion] Skipped 1 swift file(s) in heritage processing β€” swift parser not available.', + ), + ).toBe(true); - warnSpy.mockRestore(); + cap.restore(); if (previous === undefined) { delete process.env.GITNEXUS_VERBOSE; } else { @@ -166,7 +185,7 @@ describe('sequential native parser availability', () => { }); it('warns when processParsing skips files in verbose mode', async () => { - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const cap = _captureLogger(); const previous = process.env.GITNEXUS_VERBOSE; process.env.GITNEXUS_VERBOSE = '1'; vi.mocked(parserLoader.isLanguageAvailable).mockReturnValue(false); @@ -178,11 +197,17 @@ describe('sequential native parser availability', () => { createASTCache(), ); - expect(warnSpy).toHaveBeenCalledWith( - '[ingestion] Skipped 1 swift file(s) in parsing processing β€” swift parser not available.', - ); + expect( + cap + .records() + .some( + (r) => + r.msg === + '[ingestion] Skipped 1 swift file(s) in parsing processing β€” swift parser not available.', + ), + ).toBe(true); - warnSpy.mockRestore(); + cap.restore(); if (previous === undefined) { delete process.env.GITNEXUS_VERBOSE; } else {