gitnexus 1.6.6-rc.39 → 1.6.6-rc.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -374,6 +374,8 @@ async function ensureHeap() {
374
374
  */
375
375
  const ANALYZE_CLI_ENV_KEYS = [
376
376
  'GITNEXUS_VERBOSE',
377
+ 'GITNEXUS_PROFILE_DEFERRED',
378
+ 'GITNEXUS_PROFILE_DEFERRED_SLOW_MS',
377
379
  'GITNEXUS_MAX_FILE_SIZE',
378
380
  'GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS',
379
381
  'GITNEXUS_EMBEDDING_THREADS',
@@ -28,6 +28,7 @@ import { generateId } from '../../lib/utils.js';
28
28
  import { getLanguageFromFilename, SupportedLanguages } from '../../_shared/index.js';
29
29
  import { isRegistryPrimary } from './registry-primary-flag.js';
30
30
  import { isVerboseIngestionEnabled } from './utils/verbose.js';
31
+ import { deferredCallFileSlowMs, deferredCallLogEveryN, getDeferredProfileDroppedCount, isDeferredResolutionProfileEnabled, logDeferredProfile, profileElapsedMs, resetDeferredProfileDroppedCount, startTimer, } from './utils/deferred-resolution-profile.js';
31
32
  import { yieldToEventLoop } from './utils/event-loop.js';
32
33
  import { parseSourceSafe } from '../tree-sitter/safe-parse.js';
33
34
  import { CLASS_CONTAINER_TYPES, FUNCTION_NODE_TYPES, findEnclosingClassInfo, genericFuncName, inferFunctionLabel, } from './utils/ast-helpers.js';
@@ -2292,6 +2293,39 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
2292
2293
  }
2293
2294
  const totalFiles = byFile.size;
2294
2295
  let filesProcessed = 0;
2296
+ // Counts only files that survived the registry-primary skip — what the user
2297
+ // is actually waiting on. Keyed by this counter, the first per-file progress
2298
+ // log fires on the first *resolved* file rather than file #1 of byFile,
2299
+ // which would silently land inside the skip block on mixed Python+JVM repos
2300
+ // where the skipped language sorts first.
2301
+ let resolvedFiles = 0;
2302
+ const profileCalls = isDeferredResolutionProfileEnabled();
2303
+ const slowFileMs = profileCalls ? deferredCallFileSlowMs() : 0;
2304
+ const logEveryN = profileCalls ? deferredCallLogEveryN() : 0;
2305
+ let skippedRegistryPrimaryFiles = 0;
2306
+ // Fresh dropped-log counter per analyze run — the module-private counter
2307
+ // in deferred-resolution-profile.ts is process-lived, so without a reset
2308
+ // here it would accumulate across consecutive analyze invocations in the
2309
+ // same Node process (e.g., the MCP server, eval harness, integration
2310
+ // tests).
2311
+ if (profileCalls)
2312
+ resetDeferredProfileDroppedCount();
2313
+ // One-pass pre-count of the eventual non-skipped total so the live progress
2314
+ // denominator stays stable as the loop iterates. Otherwise `${totalFiles -
2315
+ // skippedRegistryPrimaryFiles}` drifts upward — files iterated before later
2316
+ // registry-primary skips have been seen carry an inflated denominator, and
2317
+ // the ratio only self-corrects after every file has been classified. Pre-
2318
+ // count runs only on the enabled path so the disabled path stays free of
2319
+ // the extra Map iteration. Defaults to 0 on the disabled path; the live log
2320
+ // gate is also disabled there, so the value is never read.
2321
+ let resolvedTotal = 0;
2322
+ if (profileCalls) {
2323
+ for (const filePath of byFile.keys()) {
2324
+ const lang = getLanguageFromFilename(filePath);
2325
+ if (!lang || !isRegistryPrimary(lang))
2326
+ resolvedTotal++;
2327
+ }
2328
+ }
2295
2329
  for (const [filePath, calls] of byFile) {
2296
2330
  filesProcessed++;
2297
2331
  if (filesProcessed % 100 === 0) {
@@ -2301,8 +2335,15 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
2301
2335
  // Registry-primary gate: skip Python (etc.) entirely when the
2302
2336
  // scope-based phase owns CALLS for this language.
2303
2337
  const fileLanguage = getLanguageFromFilename(filePath);
2304
- if (fileLanguage && isRegistryPrimary(fileLanguage))
2338
+ if (fileLanguage && isRegistryPrimary(fileLanguage)) {
2339
+ skippedRegistryPrimaryFiles++;
2305
2340
  continue;
2341
+ }
2342
+ resolvedFiles++;
2343
+ const tFile = startTimer(profileCalls);
2344
+ if (profileCalls && (resolvedFiles === 1 || resolvedFiles % logEveryN === 0)) {
2345
+ logDeferredProfile(`calls ${resolvedFiles}/${resolvedTotal} file=${filePath} sites=${calls.length}`);
2346
+ }
2306
2347
  ctx.enableCache(filePath);
2307
2348
  const widenCache = new Map();
2308
2349
  const receiverMap = fileReceiverTypes.get(filePath);
@@ -2408,6 +2449,19 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
2408
2449
  }
2409
2450
  }
2410
2451
  ctx.clearCache();
2452
+ if (tFile !== null) {
2453
+ const elapsed = profileElapsedMs(tFile);
2454
+ if (elapsed >= slowFileMs) {
2455
+ logDeferredProfile(`slow file ${elapsed.toFixed(0)}ms path=${filePath} calls=${calls.length} lang=${fileLanguage ?? 'unknown'}`);
2456
+ }
2457
+ }
2458
+ }
2459
+ if (profileCalls) {
2460
+ logDeferredProfile(`processCallsFromExtracted done: ${totalFiles} files, ${extractedCalls.length} call sites, skipped registry-primary files=${skippedRegistryPrimaryFiles}`);
2461
+ const droppedCount = getDeferredProfileDroppedCount();
2462
+ if (droppedCount > 0) {
2463
+ logDeferredProfile(`note: ${droppedCount} profile log lines dropped (logger errors)`);
2464
+ }
2411
2465
  }
2412
2466
  onProgress?.(totalFiles, totalFiles);
2413
2467
  };
@@ -13,6 +13,7 @@
13
13
  * classes implementing a given interface)
14
14
  */
15
15
  import { getLanguageFromFilename } from '../../../_shared/index.js';
16
+ import { isDeferredResolutionProfileEnabled, logDeferredProfile, } from '../utils/deferred-resolution-profile.js';
16
17
  /**
17
18
  * Determine whether a heritage.extends capture is actually an IMPLEMENTS
18
19
  * relationship. Consults the symbol table first (authoritative — Tier 1 /
@@ -72,10 +73,35 @@ export const buildHeritageMap = (heritage, ctx, getHeritageStrategy) => {
72
73
  const seenParents = new Map();
73
74
  // interfaceName → Set<filePath> (implementor lookup for interface dispatch)
74
75
  const implementorFiles = new Map();
76
+ const profileHeritage = isDeferredResolutionProfileEnabled();
77
+ let maxNameCartesian = 0;
78
+ let ambiguousHeritageRecords = 0;
79
+ let unresolvedChildLookups = 0;
80
+ let unresolvedParentLookups = 0;
75
81
  for (const h of heritage) {
76
82
  // ── Parent lookup (nodeId-based) ────────────────────────────────
77
83
  const childDefs = ctx.model.types.lookupClassByName(h.className);
78
84
  const parentDefs = ctx.model.types.lookupClassByName(h.parentName);
85
+ // Unresolved-side counters live in a separate guard so they observe
86
+ // records the ambiguity block below skips. On JVM monorepos the
87
+ // pathological fan-out case is precisely "many same-named children
88
+ // with an unresolved external supertype" (or the inverse) — both
89
+ // sides non-empty is the case `ambiguousHeritageRecords` already
90
+ // covers; the unresolved cases were silently dropped from the
91
+ // metric before this counter.
92
+ if (profileHeritage) {
93
+ if (childDefs.length === 0)
94
+ unresolvedChildLookups++;
95
+ if (parentDefs.length === 0)
96
+ unresolvedParentLookups++;
97
+ }
98
+ if (profileHeritage && childDefs.length > 0 && parentDefs.length > 0) {
99
+ const product = childDefs.length * parentDefs.length;
100
+ if (product > 1)
101
+ ambiguousHeritageRecords++;
102
+ if (product > maxNameCartesian)
103
+ maxNameCartesian = product;
104
+ }
79
105
  if (childDefs.length > 0 && parentDefs.length > 0) {
80
106
  for (const child of childDefs) {
81
107
  for (const parent of parentDefs) {
@@ -249,6 +275,14 @@ export const buildHeritageMap = (heritage, ctx, getHeritageStrategy) => {
249
275
  const getImplementorFiles = (interfaceName) => {
250
276
  return implementorFiles.get(interfaceName) ?? EMPTY_SET;
251
277
  };
278
+ if (profileHeritage) {
279
+ logDeferredProfile(`buildHeritageMap: ${heritage.length} heritage records, ` +
280
+ `${ambiguousHeritageRecords} with child×parent lookup product >1, ` +
281
+ `max product ${maxNameCartesian}, ` +
282
+ `${unresolvedChildLookups} unresolved child lookups, ` +
283
+ `${unresolvedParentLookups} unresolved parent lookups, ` +
284
+ `${implementorFiles.size} interface implementor keys`);
285
+ }
252
286
  return {
253
287
  getParents,
254
288
  getAncestors,
@@ -31,6 +31,7 @@ import path from 'node:path';
31
31
  import { fileURLToPath, pathToFileURL } from 'node:url';
32
32
  import { isDev } from '../utils/env.js';
33
33
  import { isVerboseIngestionEnabled } from '../utils/verbose.js';
34
+ import { endTimer, isDeferredResolutionProfileEnabled, logDeferredProfile, startTimer, } from '../utils/deferred-resolution-profile.js';
34
35
  import { synthesizeWildcardImportBindings, needsSynthesis } from './wildcard-synthesis.js';
35
36
  import { extractORMQueriesInline } from './orm-extraction.js';
36
37
  import { logger } from '../../logger.js';
@@ -564,7 +565,13 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
564
565
  // heritage: 75 -> 80 (5)
565
566
  // routes: 80 -> 85 (5)
566
567
  // calls: 85 -> 95 (10)
568
+ const deferredProfile = isDeferredResolutionProfileEnabled();
569
+ if (deferredProfile) {
570
+ logDeferredProfile(`deferred band start: imports=${deferredWorkerImports.length} heritage=${deferredWorkerHeritage.length} ` +
571
+ `calls=${deferredWorkerCalls.length} routes=${allExtractedRoutes.length}`);
572
+ }
567
573
  if (deferredWorkerImports.length > 0) {
574
+ const tImports = startTimer(deferredProfile);
568
575
  await processImportsFromExtracted(graph, allPathObjects, deferredWorkerImports, ctx, (current, total) => {
569
576
  const ratio = total > 0 ? current / total : 1;
570
577
  onProgress({
@@ -579,6 +586,7 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
579
586
  },
580
587
  });
581
588
  }, repoPath, importCtx);
589
+ endTimer(tImports, (ms) => `processImportsFromExtracted: ${ms.toFixed(0)}ms (${deferredWorkerImports.length} import batches before drain)`);
582
590
  // U15 (lightweight M1): processImportsFromExtracted is the sole
583
591
  // consumer of `deferredWorkerImports`. Free the array now so the
584
592
  // GC can reclaim the per-file ExtractedImport records before the
@@ -590,8 +598,10 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
590
598
  deferredWorkerImports.length = 0;
591
599
  }
592
600
  if (anyChunkNeedsWildcardSynth) {
601
+ const tWildcard = startTimer(deferredProfile);
593
602
  synthesizeWildcardImportBindings(graph, ctx);
594
603
  hasSynthesized = true;
604
+ endTimer(tWildcard, (ms) => `synthesizeWildcardImportBindings: ${ms.toFixed(0)}ms`);
595
605
  }
596
606
  // L5 from PR #1693 review: populate `exportedTypeMap` from the in-progress
597
607
  // graph BEFORE `seedCrossFileReceiverTypes` runs. Previously the seeding
@@ -609,11 +619,22 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
609
619
  }
610
620
  if (exportedTypeMap.size > 0 && ctx.namedImportMap.size > 0 && deferredWorkerCalls.length > 0) {
611
621
  const { enrichedCount } = seedCrossFileReceiverTypes(deferredWorkerCalls, ctx.namedImportMap, exportedTypeMap);
612
- if (isDev && enrichedCount > 0) {
613
- logger.info(`🔗 E1: Seeded ${enrichedCount} cross-file receiver types (all chunks)`);
622
+ if (enrichedCount > 0) {
623
+ // Two independent gates, not else-if: when both isDev AND
624
+ // deferredProfile are active, BOTH lines fire — log scrapers keyed
625
+ // on the original "🔗 E1" emoji marker keep matching, AND operators
626
+ // grepping the [deferred-profile] prefix see no gap between the
627
+ // wildcard-synth and heritage timings.
628
+ if (isDev) {
629
+ logger.info(`🔗 E1: Seeded ${enrichedCount} cross-file receiver types (all chunks)`);
630
+ }
631
+ if (deferredProfile) {
632
+ logDeferredProfile(`E1: seeded ${enrichedCount} cross-file receiver types (all chunks)`);
633
+ }
614
634
  }
615
635
  }
616
636
  if (deferredWorkerHeritage.length > 0) {
637
+ const tHeritage = startTimer(deferredProfile);
617
638
  await processHeritageFromExtracted(graph, deferredWorkerHeritage, ctx, (current, total) => {
618
639
  const ratio = total > 0 ? current / total : 1;
619
640
  onProgress({
@@ -628,8 +649,10 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
628
649
  },
629
650
  });
630
651
  });
652
+ endTimer(tHeritage, (ms) => `processHeritageFromExtracted: ${ms.toFixed(0)}ms (${deferredWorkerHeritage.length} records)`);
631
653
  }
632
654
  if (allExtractedRoutes.length > 0) {
655
+ const tRoutes = startTimer(deferredProfile);
633
656
  await processRoutesFromExtracted(graph, allExtractedRoutes, ctx, (current, total) => {
634
657
  const ratio = total > 0 ? current / total : 1;
635
658
  onProgress({
@@ -644,10 +667,17 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
644
667
  },
645
668
  });
646
669
  });
670
+ endTimer(tRoutes, (ms) => `processRoutesFromExtracted: ${ms.toFixed(0)}ms (${allExtractedRoutes.length} routes)`);
671
+ }
672
+ let fullWorkerHeritageMap;
673
+ if (deferredWorkerHeritage.length > 0) {
674
+ const tBuildHeritage = startTimer(deferredProfile);
675
+ fullWorkerHeritageMap = buildHeritageMap(deferredWorkerHeritage, ctx, getHeritageStrategyForLanguage);
676
+ endTimer(tBuildHeritage, (ms) => `buildHeritageMap wall: ${ms.toFixed(0)}ms`);
677
+ }
678
+ else if (deferredProfile) {
679
+ logDeferredProfile('buildHeritageMap: skipped (no heritage records)');
647
680
  }
648
- const fullWorkerHeritageMap = deferredWorkerHeritage.length > 0
649
- ? buildHeritageMap(deferredWorkerHeritage, ctx, getHeritageStrategyForLanguage)
650
- : undefined;
651
681
  // U15 (lightweight M1): buildHeritageMap is the LAST consumer of the
652
682
  // raw `deferredWorkerHeritage` records — processCallsFromExtracted
653
683
  // below reads from the derived `fullWorkerHeritageMap` instead. Free
@@ -656,6 +686,10 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
656
686
  // earlier was a read-only consumer (pushed to graph, didn't drain).
657
687
  deferredWorkerHeritage.length = 0;
658
688
  if (deferredWorkerCalls.length > 0) {
689
+ if (deferredProfile) {
690
+ logDeferredProfile(`processCallsFromExtracted: starting (${deferredWorkerCalls.length} call sites, heritageMap=${fullWorkerHeritageMap !== undefined})`);
691
+ }
692
+ const tCalls = startTimer(deferredProfile);
659
693
  await processCallsFromExtracted(graph, deferredWorkerCalls, ctx, (current, total) => {
660
694
  const ratio = total > 0 ? current / total : 1;
661
695
  onProgress({
@@ -673,6 +707,7 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
673
707
  },
674
708
  });
675
709
  }, deferredConstructorBindings.length > 0 ? deferredConstructorBindings : undefined, fullWorkerHeritageMap, bindingAccumulator);
710
+ endTimer(tCalls, (ms) => `processCallsFromExtracted: ${ms.toFixed(0)}ms total`);
676
711
  }
677
712
  if (deferredAssignments.length > 0) {
678
713
  processAssignmentsFromExtracted(graph, deferredAssignments, ctx, deferredConstructorBindings.length > 0 ? deferredConstructorBindings : undefined, bindingAccumulator);
@@ -36,6 +36,7 @@
36
36
  * lives in `shadow-harness.ts` (#923), not here.
37
37
  */
38
38
  import { SupportedLanguages } from '../../_shared/index.js';
39
+ import { parseTruthyEnv } from './utils/env.js';
39
40
  /**
40
41
  * Languages whose RFC #909 Ring 3 scope-resolution migration is complete.
41
42
  *
@@ -110,10 +111,6 @@ export function primaryLanguages() {
110
111
  return out;
111
112
  }
112
113
  // ─── Internal ───────────────────────────────────────────────────────────────
113
- /** Accepted truthy strings (case-insensitive, trimmed). */
114
- const TRUTHY_VALUES = new Set(['true', '1', 'yes']);
115
114
  function parseFlag(raw) {
116
- if (raw === undefined)
117
- return false;
118
- return TRUTHY_VALUES.has(raw.trim().toLowerCase());
115
+ return parseTruthyEnv(raw);
119
116
  }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Wall-clock logging for the post-chunk deferred resolution band
3
+ * (imports → heritage → heritage map → legacy call resolution).
4
+ *
5
+ * Enabled when either:
6
+ * - `GITNEXUS_VERBOSE=1` / `gitnexus analyze -v` (primary path for #1741), or
7
+ * - `GITNEXUS_PROFILE_DEFERRED=1` (force on without full verbose ingestion noise)
8
+ *
9
+ * Issue #1741: large Java/Kotlin repos appear stuck at "Resolving calls"
10
+ * because the UI progress bar updates every 100 files and intermediate
11
+ * stages emit little to the log.
12
+ */
13
+ /** True when deferred-stage timing / progress logs should emit. */
14
+ export declare const isDeferredResolutionProfileEnabled: () => boolean;
15
+ /** Log a call-resolution progress line every N files (finer when verbose). */
16
+ export declare const deferredCallLogEveryN: () => number;
17
+ /** Per-file call-resolution log threshold (ms). Lower default when verbose. */
18
+ export declare const deferredCallFileSlowMs: () => number;
19
+ export declare const profileNow: () => bigint;
20
+ export declare const profileElapsedMs: (start: bigint) => number;
21
+ /**
22
+ * Number of `logDeferredProfile` calls whose underlying `logger.info` threw.
23
+ * Surfaced in the deferred-band done-summary when greater than zero.
24
+ */
25
+ export declare const getDeferredProfileDroppedCount: () => number;
26
+ /**
27
+ * Reset the dropped-line counter. Call from test `afterEach` to keep the
28
+ * module-private state from leaking across tests. Also used inside
29
+ * `processCallsFromExtracted` at function entry so each analyze run gets
30
+ * a fresh count rather than accumulating across the process lifetime.
31
+ */
32
+ export declare const resetDeferredProfileDroppedCount: () => void;
33
+ export declare const logDeferredProfile: (message: string) => void;
34
+ /**
35
+ * Capture a monotonic timestamp when profiling is enabled; otherwise return null.
36
+ * Pair with `endTimer` so the type system narrows correctly — using `null` instead
37
+ * of a `0n` sentinel makes "profiling disabled" structurally distinct from
38
+ * "zero elapsed time" and lets TypeScript catch missing guards.
39
+ */
40
+ export declare const startTimer: (enabled: boolean) => bigint | null;
41
+ /**
42
+ * Emit a `[deferred-profile]` log line for a captured timer. No-op when the
43
+ * timer is `null` (profiling was disabled at capture time). The formatter
44
+ * receives elapsed ms so the call sites stay readable.
45
+ *
46
+ * The format callback runs inside a try/catch so a throwing formatter
47
+ * (custom toString, JSON.stringify on a circular object) cannot abort the
48
+ * deferred resolution band — observability code must never escalate to a
49
+ * load-bearing failure. On catch we emit a single `formatter error: …`
50
+ * line via logDeferredProfile and return; the caller's stage continues
51
+ * as if profiling had no-op'd for this timer. DoD §2.8 ("no silent
52
+ * catches that swallow diagnostics") is satisfied by surfacing the
53
+ * failure message rather than dropping it.
54
+ */
55
+ export declare const endTimer: (start: bigint | null, format: (elapsedMs: number) => string) => void;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Wall-clock logging for the post-chunk deferred resolution band
3
+ * (imports → heritage → heritage map → legacy call resolution).
4
+ *
5
+ * Enabled when either:
6
+ * - `GITNEXUS_VERBOSE=1` / `gitnexus analyze -v` (primary path for #1741), or
7
+ * - `GITNEXUS_PROFILE_DEFERRED=1` (force on without full verbose ingestion noise)
8
+ *
9
+ * Issue #1741: large Java/Kotlin repos appear stuck at "Resolving calls"
10
+ * because the UI progress bar updates every 100 files and intermediate
11
+ * stages emit little to the log.
12
+ */
13
+ import { logger } from '../../logger.js';
14
+ import { parseTruthyEnv } from './env.js';
15
+ import { isVerboseIngestionEnabled } from './verbose.js';
16
+ // Module-private tuning constants for the gates below. Not exported — these
17
+ // are internal knobs, not part of the module's API surface.
18
+ const LOG_EVERY_N_VERBOSE = 10;
19
+ const LOG_EVERY_N_PROFILE = 100;
20
+ const DEFAULT_SLOW_MS_VERBOSE = 3_000;
21
+ const DEFAULT_SLOW_MS = 5_000;
22
+ /** True when deferred-stage timing / progress logs should emit. */
23
+ export const isDeferredResolutionProfileEnabled = () => isVerboseIngestionEnabled() || parseTruthyEnv(process.env.GITNEXUS_PROFILE_DEFERRED);
24
+ /** Log a call-resolution progress line every N files (finer when verbose). */
25
+ export const deferredCallLogEveryN = () => isVerboseIngestionEnabled() ? LOG_EVERY_N_VERBOSE : LOG_EVERY_N_PROFILE;
26
+ /** Per-file call-resolution log threshold (ms). Lower default when verbose. */
27
+ export const deferredCallFileSlowMs = () => {
28
+ const raw = process.env.GITNEXUS_PROFILE_DEFERRED_SLOW_MS;
29
+ if (raw) {
30
+ // Use Number() not parseInt: parseInt('1e9', 10) === 1 (prefix-parses, drops the exponent),
31
+ // which would turn a user-intended "effectively disabled" threshold into a 1 ms log storm.
32
+ const n = Number(raw);
33
+ if (Number.isFinite(n) && n > 0)
34
+ return n;
35
+ }
36
+ return isVerboseIngestionEnabled() ? DEFAULT_SLOW_MS_VERBOSE : DEFAULT_SLOW_MS;
37
+ };
38
+ export const profileNow = () => process.hrtime.bigint();
39
+ export const profileElapsedMs = (start) => Number(process.hrtime.bigint() - start) / 1e6;
40
+ // Module-private counter for `[deferred-profile]` log lines the underlying
41
+ // logger refused to accept. Pino's SonicBoom transport is sync:false today,
42
+ // so steady-state `logger.info(string)` calls don't throw — but first-use
43
+ // construction paths (pino-pretty resolve, level validation) and any future
44
+ // transport reconfiguration could. The wrap below catches and counts so a
45
+ // failing logger cannot abort the deferred band, and the count surfaces in
46
+ // the deferred-band done-summary (see processCallsFromExtracted) so the
47
+ // failure is visible rather than silently swallowed (DoD §2.8).
48
+ let droppedLogLines = 0;
49
+ /**
50
+ * Number of `logDeferredProfile` calls whose underlying `logger.info` threw.
51
+ * Surfaced in the deferred-band done-summary when greater than zero.
52
+ */
53
+ export const getDeferredProfileDroppedCount = () => droppedLogLines;
54
+ /**
55
+ * Reset the dropped-line counter. Call from test `afterEach` to keep the
56
+ * module-private state from leaking across tests. Also used inside
57
+ * `processCallsFromExtracted` at function entry so each analyze run gets
58
+ * a fresh count rather than accumulating across the process lifetime.
59
+ */
60
+ export const resetDeferredProfileDroppedCount = () => {
61
+ droppedLogLines = 0;
62
+ };
63
+ export const logDeferredProfile = (message) => {
64
+ try {
65
+ logger.info(`[deferred-profile] ${message}`);
66
+ }
67
+ catch {
68
+ // Do not call the failing logger from the handler — that would risk
69
+ // an infinite loop if the failure mode is steady-state. Just count.
70
+ droppedLogLines++;
71
+ }
72
+ };
73
+ /**
74
+ * Capture a monotonic timestamp when profiling is enabled; otherwise return null.
75
+ * Pair with `endTimer` so the type system narrows correctly — using `null` instead
76
+ * of a `0n` sentinel makes "profiling disabled" structurally distinct from
77
+ * "zero elapsed time" and lets TypeScript catch missing guards.
78
+ */
79
+ export const startTimer = (enabled) => enabled ? process.hrtime.bigint() : null;
80
+ /**
81
+ * Emit a `[deferred-profile]` log line for a captured timer. No-op when the
82
+ * timer is `null` (profiling was disabled at capture time). The formatter
83
+ * receives elapsed ms so the call sites stay readable.
84
+ *
85
+ * The format callback runs inside a try/catch so a throwing formatter
86
+ * (custom toString, JSON.stringify on a circular object) cannot abort the
87
+ * deferred resolution band — observability code must never escalate to a
88
+ * load-bearing failure. On catch we emit a single `formatter error: …`
89
+ * line via logDeferredProfile and return; the caller's stage continues
90
+ * as if profiling had no-op'd for this timer. DoD §2.8 ("no silent
91
+ * catches that swallow diagnostics") is satisfied by surfacing the
92
+ * failure message rather than dropping it.
93
+ */
94
+ export const endTimer = (start, format) => {
95
+ if (start === null)
96
+ return;
97
+ const elapsedMs = profileElapsedMs(start);
98
+ let message;
99
+ try {
100
+ message = format(elapsedMs);
101
+ }
102
+ catch (err) {
103
+ logDeferredProfile(`formatter error: ${err instanceof Error ? err.message : String(err)}`);
104
+ return;
105
+ }
106
+ logDeferredProfile(message);
107
+ };
@@ -8,6 +8,19 @@
8
8
  */
9
9
  /** Whether we're running in development mode (enables verbose console logging). */
10
10
  export declare const isDev: boolean;
11
+ /**
12
+ * Parse a narrow-form truthy env-var value. Accepts `'1'`, `'true'`, `'yes'`
13
+ * (case-insensitive, whitespace-trimmed). Anything else — including
14
+ * `undefined`, empty string, `'0'`, `'false'`, `'no'`, or unknown tokens —
15
+ * returns `false`.
16
+ *
17
+ * This is the shared helper for narrow-form truthy parsing across the
18
+ * ingestion module. `logger.ts` uses a broader negative-list form
19
+ * (`isTruthyEnv`) that intentionally accepts anything except a small set of
20
+ * falsy tokens — that lives separately because it follows pino-debug
21
+ * conventions and serves a different purpose.
22
+ */
23
+ export declare const parseTruthyEnv: (raw: string | undefined) => boolean;
11
24
  /**
12
25
  * Whether scope-resolution dev validators (e.g. `validateBindingsImmutability`)
13
26
  * should run AND emit warnings. Off by default in CLI runs to avoid silent
@@ -8,6 +8,24 @@
8
8
  */
9
9
  /** Whether we're running in development mode (enables verbose console logging). */
10
10
  export const isDev = process.env.NODE_ENV === 'development';
11
+ /**
12
+ * Parse a narrow-form truthy env-var value. Accepts `'1'`, `'true'`, `'yes'`
13
+ * (case-insensitive, whitespace-trimmed). Anything else — including
14
+ * `undefined`, empty string, `'0'`, `'false'`, `'no'`, or unknown tokens —
15
+ * returns `false`.
16
+ *
17
+ * This is the shared helper for narrow-form truthy parsing across the
18
+ * ingestion module. `logger.ts` uses a broader negative-list form
19
+ * (`isTruthyEnv`) that intentionally accepts anything except a small set of
20
+ * falsy tokens — that lives separately because it follows pino-debug
21
+ * conventions and serves a different purpose.
22
+ */
23
+ export const parseTruthyEnv = (raw) => {
24
+ if (raw === undefined)
25
+ return false;
26
+ const value = raw.trim().toLowerCase();
27
+ return value === '1' || value === 'true' || value === 'yes';
28
+ };
11
29
  /**
12
30
  * Whether scope-resolution dev validators (e.g. `validateBindingsImmutability`)
13
31
  * should run AND emit warnings. Off by default in CLI runs to avoid silent
@@ -1,7 +1,2 @@
1
- export const isVerboseIngestionEnabled = () => {
2
- const raw = process.env.GITNEXUS_VERBOSE;
3
- if (!raw)
4
- return false;
5
- const value = raw.toLowerCase();
6
- return value === '1' || value === 'true' || value === 'yes';
7
- };
1
+ import { parseTruthyEnv } from './env.js';
2
+ export const isVerboseIngestionEnabled = () => parseTruthyEnv(process.env.GITNEXUS_VERBOSE);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.6-rc.39",
3
+ "version": "1.6.6-rc.40",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",