Skip to content

feat(ingestion): log deferred resolution progress when verbose (#1741)#1773

Merged
magyargergo merged 13 commits into
abhigyanpatwari:mainfrom
magyargergo:fix/issue-1741-analyze-stall-profile
May 22, 2026
Merged

feat(ingestion): log deferred resolution progress when verbose (#1741)#1773
magyargergo merged 13 commits into
abhigyanpatwari:mainfrom
magyargergo:fix/issue-1741-analyze-stall-profile

Conversation

@magyargergo

Copy link
Copy Markdown
Collaborator

Summary

  • Adds [deferred-profile] structured logs for the post-chunk deferred resolution band: imports, heritage, buildHeritageMap, and legacy processCallsFromExtracted.
  • Logging turns on automatically with gitnexus analyze -v / GITNEXUS_VERBOSE=1, so reporters debugging large-repo analyze stalls (issue GitNexus analyze gets stuck at 49% during “Resolving calls”. #1741) do not need a separate env var.
  • In verbose mode, call resolution logs every 10 files (starting at file 1), flags slow files (default ≥3s), and reports heritage-map name-collision fan-out (lookupClassByName child×parent product).

Motivation

Issue #1741: analyze appears stuck at Resolving calls (all chunks) on ~25k-file Java/Kotlin repos while CPU stays high. The UI only advances every 100 files and intermediate stages are silent in logs. v1.3.11 completes quickly; 1.6.x defers full-repo call resolution.

These logs pinpoint whether time is spent in imports, heritage map build, or specific pathological files.

How to use

GITNEXUS_VERBOSE=1 npx gitnexus analyze --force --index-only \
  --skip-agents-md --skip-skills --worker-timeout 60 --workers 8 -v \
  2>&1 | tee gitnexus-analyze.log

Optional overrides:

  • GITNEXUS_PROFILE_DEFERRED=1 — enable without full verbose ingestion noise (100-file log interval)
  • GITNEXUS_PROFILE_DEFERRED_SLOW_MS=3000 — slow-file threshold

Look for lines prefixed with [deferred-profile].

Test plan

Add [deferred-profile] timing logs for post-chunk import, heritage, heritage-map, and legacy call resolution. Enabled on GITNEXUS_VERBOSE / analyze -v (and optionally GITNEXUS_PROFILE_DEFERRED) to diagnose analyze stalls on large repos (issue abhigyanpatwari#1741).

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel

vercel Bot commented May 22, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the NexusCore Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions

Copy link
Copy Markdown
Contributor

✨ PR Autofix

Found fixable formatting / unused-import issues across 29 changed lines. Comment /autofix on this PR to apply them, or run npm run lint:fix && npm run format locally.

{"schema":"gitnexus.pr-autofix/v2","state":"fixes-available","pr_number":1773,"changed_lines":29,"head_sha":"f701398dba598b63112a5ebaf510c951c768e520","run_id":"26271982997","apply_command":"/autofix"}

@magyargergo

Copy link
Copy Markdown
Collaborator Author

/autofix

@github-actions

github-actions Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Claude finished @magyargergo's task in 5m 39s —— View job


Production-Readiness Review: PR #1773

Todo List

  • Post initial comment
  • Read required docs (DoD.md, AGENTS.md, GUARDRAILS.md, CONTRIBUTING.md, TESTING.md, ARCHITECTURE.md)
  • Inspect git diff and branch hygiene
  • Read all changed files
  • Read related/context files
  • Trace runtime wiring (CLI → isVerboseIngestionEnabled → isDeferredResolutionProfileEnabled)
  • Verify registry-primary skip counter (pre-reported Finding Add support for Ollama as a local inference backend #1)
  • Verify E1 log gating by isDev (pre-reported Finding Welcome to GitNexus Discussions! #2)
  • Assess test coverage gap (pre-reported Finding readme #3)
  • Run Unicode/bidi scans (pre-reported Finding Embeddings pipeline #4)
  • Assess lint/format annotations (pre-reported Finding Embeddings pipeline #5)
  • Produce final review

Review bar for this PR

This PR is reviewed against DoD.md v2.0.0 §§2.1–2.9 and the PR-specific DoD stated in the review request. The bar is: observability-only change, accurate logs that enable #1741 reporters to pinpoint where deferred resolution time goes, no behavior regression, tests that would catch broken wiring, and clean CI.


Problem being solved

Issue #1741: gitnexus analyze stalls at "Resolving calls (all chunks)... (9000/18066 files)" for 24 hours on a large Java/Kotlin repo while CPU stays high. The UI progress bar advances only every 100 files and intermediate stages (imports, wildcard synthesis, E1 seeding, heritage map build) are entirely silent. This PR adds [deferred-profile] structured log lines to the post-chunk deferred resolution band so reporters can identify whether time is lost in imports, heritage processing, heritage map construction, or a specific pathological file during call resolution. It is purely observability — it does not fix the root cause.


Current PR state

Single commit f701398. Five files changed: 2 added (utility module + unit tests), 3 modified (parse-impl, call-processor, heritage-map). Diff is 204 additions, 3 deletions. GitHub Actions PR Autofix bot flagged fixable formatting/unused-import issues across 29 changed lines. The Vercel deployment authorization comment is expected and non-blocking for a CLI-only PR.


Merge status and mergeability

Could not directly verify required-check conclusions (cannot fetch CI without network access), but the PR Autofix bot output is visible evidence that lint/format checks have unresolved findings in the diff. This is a merge blocker per DoD §4.2, which requires npx prettier --check . to pass for files in the diff. The non-null assertion annotation in checks is on pre-existing code at call-processor.ts:1734 (hints.typeEnv!) — confirmed pre-existing, not introduced by this PR.


Repository history considered


Branch hygiene assessment

Clean. One commit, one domain (ingestion observability), no unrelated formatting churn, no generated artifacts, no dependency bumps, no workflow changes, no release-note edits. Diff maps exactly to #1741's stated need.


Understanding of the change

deferred-resolution-profile.ts (new utility): Reads GITNEXUS_VERBOSE (via isVerboseIngestionEnabled()) and GITNEXUS_PROFILE_DEFERRED to determine if profiling is enabled. Exposes profileNow() / profileElapsedMs() (thin wrappers over process.hrtime.bigint()), logDeferredProfile() (delegates to logger.info with [deferred-profile] prefix), and configurable constants for log interval (deferredCallLogEveryN) and slow-file threshold (deferredCallFileSlowMs).

parse-impl.ts: Adds a "deferred band start" log before each stage, wall-time logs after imports/wildcard-synth/heritage/routes/buildHeritageMap/calls complete, and a processCallsFromExtracted starting log with call-site count and heritage-map presence.

heritage-map.ts: Tracks ambiguousHeritageRecords (heritage entries where name lookup yields child×parent product > 1) and maxNameCartesian product; logs them after the heritage map is fully built.

call-processor.ts: Adds per-file progress logs at file 1 and every logEveryN files, slow-file detection at the end of each file's processing block, and a done summary with totalFiles, extractedCalls.length, and skippedRegistryPrimaryFiles.

Runtime activation path confirmed: gitnexus analyze -vanalyze.ts:614-615 sets process.env.GITNEXUS_VERBOSE = '1'isVerboseIngestionEnabled() returns trueisDeferredResolutionProfileEnabled() returns true. GITNEXUS_VERBOSE=1 as a standalone env var also works via the same check.


Findings

Finding 1 — Pre-reported: Registry-primary skip counter dead code

Risk: Claimed as a confirmed bug in the pre-analysis. Actual status: NOT a bug in this PR. The diff shows a single isRegistryPrimary check, correctly rewritten as { skippedRegistryPrimaryFiles++; continue; }. There is no dead second check.

However, a related accuracy issue is confirmed: the per-file progress log at call-processor.ts:2932–2936 fires before the isRegistryPrimary check. When a registry-primary file (e.g., a Python .py) happens to be the 1st file or falls on a log-interval boundary, the log emits:

[deferred-profile] calls 1/9000 file=src/SomeService.py sites=47

A reporter sees 47 call sites attributed to SomeService.py, but the file is immediately skipped. sites= here means "ExtractedCall batches for this file" not "call sites that will be resolved." This is inaccurate for a PR whose entire purpose is actionable diagnostics.

  • Evidence: call-processor.ts:2932–2936 (progress log), call-processor.ts:2940–2944 (registry-primary gate with counter).
  • Recommended fix: Move the progress log to after the registry-primary gate, and include (skipped, registry-primary) in the message when applicable; or change sites= to batched_sites= to clarify semantics. Minimally: move the log inside the non-registry-primary path so it only fires for files actually being resolved.
  • Blocks merge: maybe — misleading diagnostics for a diagnostics-only PR undercut the stated goal.

Finding 2 — Pre-reported: E1 log gated by isDev

Risk: Claimed as a potential bug. Actual status: NOT a bug. parse-impl.ts:782 reads:

if (enrichedCount > 0 && (isDev || deferredProfile)) {
  logDeferredProfile(`E1: seeded ${enrichedCount} cross-file receiver types (all chunks)`);
}

When deferredProfile=true and isDev=false, the condition correctly evaluates to true and the log emits. No outer isDev gate surrounds it.

Minor behavior change that is not a regression: the old logger.info('🔗 E1: ...') for dev-only environments is now replaced by logDeferredProfile('E1: ...') which uses the [deferred-profile] prefix. This slightly changes dev-mode experience but does not affect production.

  • Evidence: parse-impl.ts:782–784 (confirmed in the actual file).
  • Recommended fix: No fix required. Pre-analysis concern was not borne out.
  • Blocks merge: no.

Finding 3 — Pre-reported: Tests cover utility only, not runtime wiring

Risk: Confirmed. The unit test in deferred-resolution-profile.test.ts exercises only the utility module (isDeferredResolutionProfileEnabled, deferredCallLogEveryN, deferredCallFileSlowMs, profileNow, profileElapsedMs). No test verifies that buildHeritageMap, processCallsFromExtracted, or runChunkedParseAndResolve actually emit log lines when GITNEXUS_PROFILE_DEFERRED=1. The existing heritage-map.test.ts and call-processor.test.ts call the production functions but do not spy on logger.info or verify the new log paths.

DoD.md §2.7 states: "Tests cover the real changed path — they would fail if behavior, wiring, or contracts were broken." For a purely observability PR, removing all logDeferredProfile(...) calls from parse-impl.ts and heritage-map.ts would leave the utility tests 100% passing, which violates this DoD gate.

  • Evidence: test/unit/deferred-resolution-profile.test.ts:1–45 (utility-only coverage); no test in test/unit/heritage-map.test.ts or test/unit/call-processor.test.ts sets GITNEXUS_PROFILE_DEFERRED=1 or spies on logger output.
  • Recommended fix: Add at least two focused tests:
    1. In or alongside heritage-map.test.ts: set process.env.GITNEXUS_PROFILE_DEFERRED = '1', call buildHeritageMap with a fixture that produces ambiguous lookups, spy on logger.info and assert a [deferred-profile] buildHeritageMap: line is emitted.
    2. In or alongside call-processor.test.ts's existing processCallsFromExtracted coverage: set process.env.GITNEXUS_PROFILE_DEFERRED = '1', spy on logger.info, run processCallsFromExtracted with a small fixture, assert the "done" summary line fires and skippedRegistryPrimaryFiles count is correct.
    • parse-impl.ts/runChunkedParseAndResolve integration coverage is harder to add; if the above two are added, documenting the parse-impl wiring gap explicitly in the PR is acceptable.
  • Blocks merge: yes per DoD §2.7 — this is the core observability wiring under test, not an internal utility.

Finding 4 — Pre-reported: Hidden Unicode

Actual status: Clean. The bidi/control character scan (grep -P "[\u202A-\u202E\u2066-\u2069\u200B\u200C\u200D\uFEFF]") found zero matches. Visible non-ASCII characters in the diff ( in the utility file comment, × in the heritage-map log string at heritage-map.ts:388) are standard Unicode punctuation already used throughout the codebase. No merge blocker.

  • Blocks merge: no.

Finding 5 — Pre-reported: Lint/format unresolved

Risk: The PR Autofix bot reported fixable formatting / unused-import issues across 29 changed lines and suggested npm run lint:fix && npm run format. I cannot run the linter directly in this environment, but the bot output is direct evidence that ci-quality.yml checks will fail or flag issues on commit f701398. DoD §4.2 requires npx prettier --check . to pass for files in the diff.

  • Evidence: GitHub Actions PR Autofix comment on this PR: {"changed_lines":29,"state":"fixes-available"}.
  • Recommended fix: Run /autofix as suggested by the bot, or run cd gitnexus && npm run lint:fix && npm run format locally and amend the commit.
  • Blocks merge: yes — required quality checks are flagged as having fixable issues.

Finding 6 — New: buildHeritageMap timing log fires even when function is not called

Risk: At parse-impl.ts:831–838, tBuildHeritage is always set when deferredProfile=true, but buildHeritageMap is only called when deferredWorkerHeritage.length > 0. When heritage is empty (e.g., pure-JavaScript repos), the log still emits:

[deferred-profile] buildHeritageMap wall: 0ms

This implies the function ran in 0ms when it was actually skipped entirely. A reporter investigating a stall seeing "0ms" for heritage map build when the actual time was "not called" may draw a wrong conclusion.

  • Evidence: parse-impl.ts:831–838 (timer set unconditionally, conditional on heritage length only for the actual call).
  • Recommended fix: Wrap the timer and log in the same guard:
    if (deferredWorkerHeritage.length > 0) {
      const tBuildHeritage = deferredProfile ? profileNow() : 0n;
      fullWorkerHeritageMap = buildHeritageMap(...);
      if (tBuildHeritage !== 0n) logDeferredProfile(`buildHeritageMap wall: ...`);
    } else if (deferredProfile) {
      logDeferredProfile('buildHeritageMap: skipped (no heritage records)');
    }
  • Blocks merge: maybe — the log is specifically meant to be accurate; "0ms" for a skipped stage misreads.

Finding 7 — New: GITNEXUS_PROFILE_DEFERRED and GITNEXUS_PROFILE_DEFERRED_SLOW_MS absent from ANALYZE_CLI_ENV_KEYS

Risk: analyze.ts:474–483 defines ANALYZE_CLI_ENV_KEYS which snapshots and restores env vars around each analyze call. GITNEXUS_VERBOSE is in this list; the two new env vars are not. If a user sets GITNEXUS_PROFILE_DEFERRED=1 and multiple analyze calls happen in-process (test suites, programmatic consumers), the env vars will not be isolated. The afterEach in the unit test correctly cleans up, so tests are safe, but the production isolation contract is inconsistent.

  • Evidence: analyze.ts:474–483 (ANALYZE_CLI_ENV_KEYS list excludes new vars).
  • Recommended fix: Add GITNEXUS_PROFILE_DEFERRED and GITNEXUS_PROFILE_DEFERRED_SLOW_MS to ANALYZE_CLI_ENV_KEYS.
  • Blocks merge: no — low-risk for debug-only env vars that callers set intentionally; not a stall-diagnostic correctness issue.

PR-specific assessment sections

Lane 1 — Runtime logging activation and gating: PASSES
gitnexus analyze -vanalyze.ts:615 sets GITNEXUS_VERBOSE=1isVerboseIngestionEnabled()isDeferredResolutionProfileEnabled()=true. GITNEXUS_VERBOSE=1 independently works via verbose.ts. GITNEXUS_PROFILE_DEFERRED=1 correctly enables profiling without requiring verbose mode. Invalid env values (e.g., GITNEXUS_PROFILE_DEFERRED=0, =no) are handled safely by truthyEnv(). Log interval is correctly 10 (verbose) vs 100 (profile-only). Slow-file threshold is correctly 3000ms (verbose) vs 5000ms (profile-only) with override via GITNEXUS_PROFILE_DEFERRED_SLOW_MS.

Lane 2 — Deferred-band coverage and progress usefulness: MOSTLY PASSES
All major stages are instrumented: imports (start + elapsed), wildcard synthesis (elapsed), E1 seeding (count when nonzero), heritage processing (elapsed + count), route processing (elapsed + count), heritage map build (elapsed, see Finding 6 for gap when empty), call resolution (starting + per-file progress + slow-file + done + total). The only silent stage in a stall scenario is a single very-slow individual file between progress log intervals — per-file progress logs every 10 files (verbose) provide adequate signal. Stages log before they begin (calls: "starting") and after they complete; long stages (imports, calls) also log their elapsed time. A reporter who was stuck for 24 hours will immediately see which stage is not completing.

Lane 3 — Counter and metric correctness: ONE CONFIRMED ISSUE
filesProcessed correctly counts all files in byFile including registry-primary ones. skippedRegistryPrimaryFiles correctly increments when isRegistryPrimary is true (Finding 1 pre-analysis was wrong — no dead code). extractedCalls.length in the done summary correctly counts total input call sites. ambiguousHeritageRecords and maxNameCartesian are correctly computed. One issue (Finding 1 detail): the per-file progress log fires for registry-primary files before they're gated, showing a sites=N count for files that won't be resolved.

Lane 4 — Performance and hot-path safety: PASSES
When disabled: only two cheap function calls (isDeferredResolutionProfileEnabled() = one env read + one OR) plus variable initializations. No hot-path overhead. When enabled: logs at every 10 files (verbose) or 100 files (profile-only) using simple string interpolation — no JSON.stringify, no deep object traversal. profileNow() / profileElapsedMs() are direct process.hrtime.bigint() calls with no async or scheduling side-effects. Slow-file threshold is configurable and defaults are reasonable (3s/5s). No data is retained in memory by the profiling code.

Lane 5 — Tests and CI evidence: FAILS DoD
Unit tests cover utility module only. No test verifies runtime log emission from the three instrumented production modules. CI Autofix bot flagged 29 lines. These two issues prevent the PR from meeting DoD §2.7 and §4.2. (See Findings 3 and 5.)

Cross-domain: Change is strictly ingestion-observability. No MCP/HTTP/web/CLI behavior changes. No doc/release-note requirement triggered (debug env vars are not user-facing contracts). Vercel check is non-blocking for a CLI-only PR.


Back-and-forth avoided by verifying

  1. Pre-reported Finding Add support for Ollama as a local inference backend #1 (dead counter) — verified directly from the code: the PR correctly has a single isRegistryPrimary check with skippedRegistryPrimaryFiles++; continue;. Not dead. The related accuracy issue (progress log precedes the gate) is a different concern.
  2. Pre-reported Finding Welcome to GitNexus Discussions! #2 (E1 gated by isDev) — verified directly: condition is enrichedCount > 0 && (isDev || deferredProfile). Correctly gated; fires when deferredProfile=true and isDev=false.
  3. Non-null assertion annotation — confirmed at call-processor.ts:1734 in pre-existing code; not introduced by this PR.
  4. Vercel deployment auth — confirmed non-blocking for CLI-only PR.
  5. ANALYZE_CLI_ENV_KEYS — confirmed directly: new vars are absent (Finding 7, non-blocking).

Open questions

None that can be resolved without running CI. The remaining gaps (Finding 3, Finding 5) have clear remediation paths the author can verify locally.


Final verdict

not production-ready

Two DoD gates are unmet. First, the GitHub Actions PR Autofix bot has confirmed fixable lint/format issues across 29 changed lines (Finding 5, DoD §4.2); required quality checks cannot be verified as passing until /autofix or npm run lint:fix && npm run format is applied and re-committed. Second, the tests cover only the utility helper module — no test would fail if all logDeferredProfile(...) calls were deleted from parse-impl.ts, heritage-map.ts, or call-processor.ts, violating DoD §2.7's requirement that tests cover the real changed path (Finding 3).

Both blockers have clear, targeted fixes: run /autofix and add two focused logger-spy tests for buildHeritageMap and processCallsFromExtracted under GITNEXUS_PROFILE_DEFERRED=1. Findings 6 (misleading "0ms" when heritage map is skipped) and 1-detail (progress log fires for registry-primary files before the gate) are accuracy concerns worth fixing since the PR's entire purpose is accurate diagnostics. Findings 4 and 7 are non-blocking.

The runtime activation wiring, gating logic, counter correctness, performance, and deferred-band coverage are otherwise sound.

@github-actions

Copy link
Copy Markdown
Contributor

✅ Applied autofix and pushed a commit. (apply run)

…review

Move deferred call progress logs after the registry-primary skip so sites= counts match files actually resolved. Only time buildHeritageMap when heritage records exist; otherwise log an explicit skip. Add wiring tests that assert [deferred-profile] emission from buildHeritageMap and processCallsFromExtracted. Snapshot GITNEXUS_PROFILE_DEFERRED env vars in analyze CLI isolation.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

github-actions Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

CI Report

All checks passed

Pipeline Status

Stage Status Details
✅ Typecheck success tsc --noEmit
✅ Tests success unit tests, 3 platforms
✅ E2E success gitnexus-web changes only

Test Results

Tests Passed Failed Skipped Duration
9507 9506 0 1 429s

✅ All 9506 tests passed

1 test(s) skipped — expand for details
  • buildTypeEnv > known limitations (documented skip tests) > Ruby block parameter: users.each { |user| } — closure param inference, different feature

Code Coverage

Tests

Metric Coverage Covered Base Delta Status
Statements 77.83% 31357/40284 N/A% 🟢 ███████████████░░░░░
Branches 66.5% 19974/30032 N/A% 🟢 █████████████░░░░░░░
Functions 81.45% 3202/3931 N/A% 🟢 ████████████████░░░░
Lines 81.26% 28280/34799 N/A% 🟢 ████████████████░░░░

📋 View full run · Generated by CI

Test and others added 10 commits May 22, 2026 08:14
P0
- Replace forbidden toBeGreaterThanOrEqual/toBeLessThan in
  profileElapsedMs test with exact-arithmetic vi.spyOn(hrtime.bigint)
  asserting .toBe(2.5) and .toBe(0). DoD §2.7 compliance.

P2
- Use Number() (not parseInt) when parsing
  GITNEXUS_PROFILE_DEFERRED_SLOW_MS so scientific notation like '1e9'
  doesn't silently parse to 1 and turn the slow-file log into a per-file
  log storm.
- Introduce startTimer(enabled): bigint | null and endTimer(start,
  format) helpers in deferred-resolution-profile.ts; refactor 6+
  timing blocks in parse-impl.ts and call-processor.ts to use them.
  Removes the 0n sentinel that conflated 'disabled' with 'zero
  elapsed time' and let TS narrow correctly.
- Split the call-processor file counter: filesProcessed (all iterated)
  vs resolvedFiles (post registry-primary skip). Key the every-N
  progress log and the start-of-phase log on resolvedFiles so mixed
  Python+JVM repos where the skipped language sorts first still emit
  'calls 1/1 file=...' on the first non-skipped file. Adds a wiring
  test for the mixed-language ordering case.

P3
- Restore the original isDev '🔗 E1: Seeded ...' logger.info line so
  log scrapers keyed on the emoji marker still match; emit the
  [deferred-profile] variant only when deferredProfile && !isDev.
- Move tFile = startTimer(profileCalls) below the registry-primary
  skip so skipped files don't trigger an hrtime.bigint() call.
- Document GITNEXUS_PROFILE_DEFERRED and
  GITNEXUS_PROFILE_DEFERRED_SLOW_MS in the README env-var table.
Three narrow-form env-var truthy checkers (verbose.ts, registry-primary-flag.ts,
deferred-resolution-profile.ts) each had their own `'1' | 'true' | 'yes'` parser
with subtle divergences (trim or no trim, set vs disjunction). Consolidate on a
single `parseTruthyEnv(raw)` helper in utils/env.ts — the module already serves
as the centralization point for shared ingestion env constants.

logger.ts's broader `isTruthyEnv` (negative-list, pino-debug convention) stays
untouched — different intent, different semantics.

New table-driven test at test/unit/env.test.ts covers case variants,
whitespace, and rejection of falsy / unknown tokens.
Replace magic literals 10 / 100 / 3_000 / 5_000 in
deferred-resolution-profile.ts with module-private named constants
LOG_EVERY_N_VERBOSE, LOG_EVERY_N_PROFILE, DEFAULT_SLOW_MS_VERBOSE,
DEFAULT_SLOW_MS. Not exported — internal tuning knobs. Pure refactor;
existing tests assert the exact values and still pass unchanged.
The live per-file denominator in processCallsFromExtracted previously
read `totalFiles - skippedRegistryPrimaryFiles` at log time. On mixed
Python+JVM repos where the skipped language interleaves with the
resolved one, the denominator drifts upward as the loop iterates —
files iterated before later skips have been seen carry an inflated
denominator. The live ratio only self-corrects after the final file
has been classified.

Fix: one-pass pre-count over byFile.keys() before the work loop
computes resolvedTotal once. The denominator is then stable from the
first emission onward. The pre-pass runs only on the enabled path
(profileCalls=true) so the disabled path keeps zero extra work.

Adds a wiring test exercising the alternating [ts, py, ts, py, ...]
order that triggered the drift, asserting every emitted line uses
`/4` and no other denominator slips through.
… (U2, A2)

The post-chunk E1 enrichment log used `if (isDev) {...} else if
(deferredProfile) {...}` which is mutually exclusive. On combined runs
(NODE_ENV=development + GITNEXUS_PROFILE_DEFERRED=1) the [deferred-
profile] line was silently swallowed — operators grepping that prefix
saw a gap between wildcard-synth and heritage timings, while the
inline comment promised dual emission.

Fix: two independent `if` statements so both branches fire when both
flags are set. The original emoji-prefixed `🔗 E1: Seeded` line keeps
its phrasing for any dev-mode log scrapers that depend on the marker.

Pinning test (parse-impl-e1-emission-shape.test.ts) reads the source
and asserts (a) both branches exist as standalone `if` statements and
(b) the closing `}` of the isDev branch is followed by `if`, not
`else if`. Source-shape pins are the right test scope for a purely
structural change — the regression we are guarding against is exactly
how a future reader greps for it.
The existing maxNameCartesian / ambiguousHeritageRecords counters in
buildHeritageMap only observed records where BOTH the child and parent
name lookups resolved. On JVM monorepos the actual pathological case is
one side empty (typically an unresolved external supertype with many
same-named children, or vice versa) — those records were silently
dropped from the metric.

Add `unresolvedChildLookups` and `unresolvedParentLookups` in a
separate `if (profileHeritage)` block placed immediately after the two
`lookupClassByName` calls (so it observes the unresolved cases the
length-guarded ambiguity block below cannot see). Both counters reuse
the existing childDefs / parentDefs values — no additional lookups.

Done-summary log extended to include the two new counters. Wiring test
covers both directions (unresolved parent, unresolved child) plus the
existing "both resolved" baseline now asserts the new counters report
zero for that case.
Wrap the format callback in endTimer in a try/catch so a throwing
formatter (custom toString, JSON.stringify on a circular object,
future heavier serializers) cannot abort the deferred resolution
band. Observability code must never escalate to a load-bearing
failure mode.

On catch we emit a single `[deferred-profile] formatter error: …`
line via logDeferredProfile and return; the caller's stage continues
as if profiling had no-op'd for this timer. DoD §2.8 is satisfied —
the failure is surfaced, not silently swallowed.

Tests cover the four cases: happy path emits the formatted line, null
start no-ops without invoking the formatter, throwing formatter is
caught and surfaces one error line, non-Error throws are coerced via
String() in the message.
…Profile (U4)

Wrap logger.info inside logDeferredProfile in a try/catch so a throwing
underlying logger cannot abort the deferred resolution band. Pino with
sync:false (the current SonicBoom destination) does not throw
synchronously for `info(string)` calls, but first-use construction
paths (pino-pretty resolve, level validation) and any future transport
reconfiguration could. The wrap is belt-and-suspenders coverage; the
counter makes silent failures visible.

A module-private droppedLogLines counter accumulates dropped lines.
Two helpers — getDeferredProfileDroppedCount() and
resetDeferredProfileDroppedCount() — expose the counter. The handler
deliberately does NOT call the failing logger; that would risk an
infinite loop if the failure is steady-state.

processCallsFromExtracted resets the counter at entry (so each analyze
run gets a fresh count rather than accumulating across the process
lifetime — relevant for the MCP server, eval harness, integration
tests), and surfaces the count in the done-summary as `note: N profile
log lines dropped (logger errors)` when greater than zero. DoD §2.8
(no silent diagnostic catches) is satisfied.

Tests cover the helper API (zero at entry, idempotent reset) and the
happy path; the catch arm is pinned via source-shape assertion since
the logger Proxy can't be vi.spyOn'd directly (lazy `get` trap, no
own-property to wrap).
The env-var row mentioned integer / scientific notation only, but the
underlying parser (`Number(raw)` since the U2 fix in PR abhigyanpatwari#1773) also
accepts decimals like `.5` and hex like `0x10`. Document the actual
acceptance set plus the non-finite / non-positive fallback so operators
setting unusual values know what to expect.
@magyargergo magyargergo merged commit d15f8be into abhigyanpatwari:main May 22, 2026
32 of 33 checks passed
@magyargergo magyargergo deleted the fix/issue-1741-analyze-stall-profile branch May 22, 2026 11:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant